Help us improve
Share bugs, ideas, or general feedback.
From aweek
Initializes aweek projects by creating the .aweek data directory and optionally installing a 10-minute heartbeat scheduler via launchd on macOS or crontab on Linux/POSIX.
npx claudepluginhub runbear-io/aweek --plugin aweekHow this skill is triggered — by the user, by Claude, or both
Slash command
/aweek:setupThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Bootstrap a project for the aweek agent scheduler. Most users never need to
Manages scheduled Claude Code tasks: add recurring/one-off skills/prompts/scripts, list/pause/resume/remove, view results/logs, test execution with safety controls and notifications. Cross-platform (macOS/Linux/Windows).
Manages persistent scheduled reminders (crons): list, add from natural language (English/Spanish), delete, pause, resume, reconcile, import from OpenClaw. Auto-activates on reminder phrases.
Share bugs, ideas, or general feedback.
Bootstrap a project for the aweek agent scheduler. Most users never need to
run this manually — every other skill auto-bootstraps the project when it
first runs. Use /aweek:setup when you want to explicitly control the
heartbeat installation, or to reset a sticky heartbeat decision so other
skills re-prompt.
It prepares the filesystem layout and (optionally) installs the 10-minute heartbeat that drives agent execution.
The heartbeat is installed via the platform's native scheduler:
~/Library/LaunchAgents/io.aweek.heartbeat.<hash>.plist. This is the default on darwin because cron jobs can't access the macOS Keychain, and Claude Code's subscription OAuth tokens live there — the launchd user agent runs inside your aqua session, so Keychain access works exactly like from Terminal.*/10 * * * *). If a legacy crontab entry exists on a macOS machine from an earlier aweek version, it is migrated automatically the next time /aweek:setup installs the launchd agent.Slash-command discovery is handled by the Claude Code plugin system — this
skill does not copy markdown into .claude/commands/. Users who installed
aweek via /plugin install aweek@… already have every /aweek:* command
available.
This skill is a thin UX wrapper on top of src/skills/setup.ts. All filesystem
mutations, crontab writes, and idempotency checks live in that module — do
not edit .aweek/ or the user's crontab directly.
/aweek:setup is interactive and takes no positional arguments. The underlying
runInit() function in src/skills/setup.ts accepts the following optional
options for advanced / non-interactive invocation:
| Option | Type | Default | Description |
|---|---|---|---|
projectDir | string | process.cwd() | Project root where .aweek/ will be created |
dataDir | string | .aweek/agents | Path (relative to projectDir) for the agent data directory |
installHeartbeat | boolean | false | Whether to install the 10-minute heartbeat crontab entry (destructive — requires confirmation) |
heartbeatSchedule | string | */10 * * * * | Cron schedule used when installHeartbeat is true |
confirmed | boolean | false | Must be true to allow destructive operations (crontab install, overwriting an existing data dir) |
Never pass confirmed: true without collecting explicit confirmation via
AskUserQuestion.
/aweek:setup is safe to run repeatedly on an already-initialized project.
Each step reports one of three outcomes:
| Outcome | Meaning |
|---|---|
created | The resource did not exist and was created by this run |
skipped | The resource already existed with the expected shape — no action taken |
updated | The resource existed but was refreshed (e.g. skill markdown changed upstream) |
The skill MUST present these outcomes to the user so they can see exactly what changed versus what was left alone.
On a re-run against an already-initialized project the skill MUST:
detectInitState() FIRST and use the returned flags to decide which
steps to skip silently vs. report as already-done. Do not run
ensureDataDir or installHeartbeat when the corresponding
needsWork.* flag is false — just report the step as skipped and
move on.finalizeInit() — it will return
mode: 'add-another' so the wizard can offer the user a chance to hire
another agent via /aweek:hire. Re-runs must not feel like dead-ends.Per project policy, every destructive init step requires explicit user confirmation before the change is applied.
| Step | Destructive | Confirmation required |
|---|---|---|
Create .aweek/agents/ dir | No | No |
| Install heartbeat (launchd on macOS / cron elsewhere) | Yes | Yes |
| Overwrite existing data dir | Yes | Yes |
You MUST follow this exact workflow when this skill is invoked. Use the
Node.js modules in src/skills/setup.ts for every filesystem or crontab
mutation.
Run the detection helper to figure out which steps are already complete:
aweek exec setup detectInitState
The returned object has this shape:
{
"dataDir": { "path": ".aweek/agents", "exists": true, "agentCount": 0 },
"heartbeat": { "installed": false, "schedule": null }
}
Present a short, readable summary of this state to the user before taking any action. Do NOT proceed past Step 1 if the user cancels.
If state.dataDir.exists === false, create it:
echo '{"dataDir":".aweek/agents"}' \
| aweek exec setup ensureDataDir --input-json -
If the directory already exists, report skipped and move on — do NOT
overwrite or wipe an existing .aweek/agents/ unless the user has explicitly
asked for a clean-slate reset AND confirmed via AskUserQuestion.
Ask the user via AskUserQuestion:
Install the 10-minute heartbeat? This registers a scheduled task so agents run automatically every 10 minutes.
On macOS this creates a launchd user agent at
~/Library/LaunchAgents/io.aweek.heartbeat.<hash>.plist. You can remove it later withlaunchctl bootout gui/$UID/<label>.On Linux it writes an entry to your user crontab. You can remove it later with
crontab -e.
yes— install now
no— skip (you can install it later by re-running/aweek:setup)
Only if they answer yes, run:
echo '{"schedule":"*/10 * * * *","confirmed":true}' \
| aweek exec setup installHeartbeat --input-json -
The response includes a backend field ("launchd" or "cron") plus an
outcome of created, updated, skipped, or migrated. migrated
means a legacy cron entry for this project was also removed — include
that in the final summary so the user knows the crontab line is gone.
If the user declines, report that the heartbeat was skipped and remind them they can install it later.
If the heartbeat is already installed (detected in Step 1), skip the prompt
entirely and report skipped — never re-install unprompted.
Print a final summary table showing each step's outcome:
=== aweek init ===
Project: /path/to/project
Data directory : created (.aweek/agents)
Heartbeat (launchd) : skipped (user declined)
Next steps:
1. Run /aweek:hire to create your first agent
2. Run /aweek:plan to approve its initial weekly plan
3. Run /aweek:summary to see the dashboard
Infrastructure setup is just the foundation; the user's real goal is to have at least one working agent. After the summary prints, always run the post-init hire decision. The flow adapts to the project's current state:
.claude/agents/<slug>.md
(i.e. the subagent .md is on disk but there is no matching
.aweek/agents/<slug>.json), show the full four-option menu./aweek:hire (create-new branch). Asking the
user to pick between "Create new" and "Skip" on an empty roster is noise —
there is nothing to adopt and the only useful next action is to launch the
create-new wizard. (Sub-AC 3 of AC 6.)Resolve the decision via resolveInitHireMenu from src/skills/setup-hire-menu.ts.
This helper composes buildInitHireMenu (which calls listUnhiredSubagents
under the hood and filters plugin-namespaced slugs) with the fall-through rule
so the markdown gets one of two stable shapes back.
DECISION=$(aweek exec setup-hire-menu resolveInitHireMenu)
echo "$DECISION"
# When not falling through, render the human-readable prompt too.
FALL_THROUGH=$(aweek json get fallThrough <<<"$DECISION")
if [ "$FALL_THROUGH" != "true" ]; then
echo '---'
aweek json get menu <<<"$DECISION" \
| aweek exec setup-hire-menu formatInitHireMenuPrompt \
--input-json - --format text
fi
The returned object has one of two shapes:
Choose path (one or more unhired subagents):
{
"fallThrough": false,
"menu": {
"hasUnhired": true,
"unhired": ["analyst", "writer"],
"options": [
{ "value": "hire-all", "label": "Hire all", "description": "...", "requiresUnhired": true },
{ "value": "select-some", "label": "Select some", "description": "...", "requiresUnhired": true },
{ "value": "create-new", "label": "Create new", "description": "...", "requiresUnhired": false },
{ "value": "skip", "label": "Skip", "description": "...", "requiresUnhired": false }
],
"promptText": "Infrastructure setup is complete. How would you like to hire subagents into aweek?"
},
"route": null,
"reason": null
}
Fall-through path (no unhired subagents — auto-delegation to /aweek:hire):
{
"fallThrough": true,
"menu": { "hasUnhired": false, "unhired": [], "options": [...], "promptText": "..." },
"route": {
"action": "create-new",
"nextSkill": "/aweek:hire",
"route": "create-new",
"slugs": [],
"bulk": false,
"reason": "No unhired subagents were found under .claude/agents/. Auto-delegating to /aweek:hire (create-new) — there is nothing to adopt and the only useful next action is to create a new agent.",
"fallThrough": true
},
"reason": "No unhired subagents were found under .claude/agents/. Auto-delegating to /aweek:hire (create-new) — there is nothing to adopt and the only useful next action is to create a new agent."
}
If decision.fallThrough is true:
decision.reason to the user (one short line — they should see
why the menu was skipped, not be left guessing).AskUserQuestion. Skip Step 5.1 entirely./aweek:hire immediately. Honor the decision.route descriptor —
route: 'create-new' means launch the three-field create-new wizard with
no pre-filled fields.This auto-delegation is non-destructive (the hire wizard creates new state,
never overwrites). No additional confirmed: true gate is required.
If decision.fallThrough is false, proceed to Step 5.1 to present the menu
as documented below. The remainder of Step 5 only applies on the choose path.
Display menu.promptText and, when menu.hasUnhired is true, the list of
unhired subagent slugs. Then invoke AskUserQuestion with the options from
menu.options — pass them verbatim so the labels, descriptions, and value
identifiers stay in sync with the backing module.
| Choice | Available when | Meaning |
|---|---|---|
| Hire all | hasUnhired === true | Wrap every slug in menu.unhired into an aweek scheduling JSON in one pass. |
| Select some | hasUnhired === true | Prompt the user a second time with a multi-select over menu.unhired and hire only the picked slugs. See Step 5.2b for the helper pipeline (buildSelectSomeChoices → AskUserQuestion multi-select → runSelectSomeHire). |
| Create new | Always | Skip adoption and launch the /aweek:hire wizard's create-new path (three-field identity capture). |
| Skip | Always | Finish init without hiring. The user can always run /aweek:hire later. |
Call routeInitHireMenuChoice to convert the user's selection into a stable
handler descriptor:
MENU=$(aweek exec setup-hire-menu buildInitHireMenu)
aweek json compose menu="$MENU" choice='<USER_CHOICE>' selected='[<SELECTED_SLUGS>]' \
| aweek exec setup-hire-menu routeInitHireMenuChoice --input-json -
<USER_CHOICE> is one of hire-all, select-some, create-new, skip.
selected is required only when choice === 'select-some'; otherwise
pass [].
The returned route descriptor tells the markdown exactly what to do next:
route.action | route.nextSkill | route.route | route.slugs | route.bulk |
|---|---|---|---|---|
hire-all | /aweek:hire | pick-existing | every unhired | true |
select-some | /aweek:hire | pick-existing | user's picks | true |
create-new | /aweek:hire | create-new | [] | false |
skip | null | null | [] | false |
Dispatch based on the descriptor:
hire-all (bulk: true): hand route.slugs to the non-interactive
hireAllSubagents handler (src/skills/hire-all.js). The handler iterates
internally, wrapping every slug with a minimal aweek scheduling JSON shell
(empty goals / plans, default budget — identity already lives in
.claude/agents/<slug>.md). Per-slug outcomes land under created /
skipped / failed so you can echo the summary verbatim:
RESULT=$(echo '{"slugs":[<SLUGS>]}' \
| aweek exec hire-all hireAllSubagents --input-json -)
echo "$RESULT" | aweek exec hire-all formatHireAllSummary \
--input-json - --format text
if [ "$(aweek json get success <<<"$RESULT")" != "true" ]; then
exit 1
fi
Substitute <SLUGS> with the route.slugs array from
routeInitHireMenuChoice.
The handler is idempotent (re-running on an already-hired slug is a skip,
not an error) and defensive (plugin-namespaced slugs, missing .md
files, and invalid slug shapes surface as structured skipped / failed
entries instead of throwing). Users who want per-agent goals / objectives /
tasks should run /aweek:plan against each newly-hired slug afterwards —
bulk hires intentionally leave plans empty so the user can tailor each
roadmap rather than inheriting a generic template.
select-some (bulk: true): see Step 5.2b below — it requires a
second AskUserQuestion (a multi-select) before the wrapper pass.
create-new (bulk: false): delegate to /aweek:hire's create-new
wizard so the user collects the three-field identity (name, description,
system prompt) and both the .claude/agents/<slug>.md AND the aweek JSON
wrapper land together. Do not pre-fill any of the three fields; the wizard
collects them interactively.
The canonical dispatch is the buildCreateNewLaunchInstruction descriptor
from src/skills/hire-create-new-menu.js — it returns a stable
{ skill, route, projectDir, promptText, reason } shape the markdown can
render before invoking the interactive skill:
aweek exec hire-create-new-menu buildCreateNewLaunchInstruction
After rendering the prompt, invoke /aweek:hire and let the interactive
wizard walk the user through Steps 1-6 of that skill.
Alternatively, when the three identity fields have already been collected
(e.g. from a non-interactive test harness), call runCreateNewHire to run
the same two-step flow the wizard's Step 1a + 6b would run — it writes or
adopts .claude/agents/<slug>.md via createNewSubagent, then delegates
to hireAllSubagents so the aweek JSON shell shape matches every other
menu branch:
RESULT=$(echo '{
"name": "<NAME>",
"description": "<DESCRIPTION>",
"systemPrompt": "<SYSTEM_PROMPT>",
"weeklyTokenLimit": <LIMIT>
}' | aweek exec hire-create-new-menu runCreateNewHire --input-json -)
echo "$RESULT" | aweek exec hire-create-new-menu formatCreateNewResult \
--input-json - --format text
if [ "$(aweek json get success <<<"$RESULT")" != "true" ]; then
exit 1
fi
weeklyTokenLimit is optional — omit the field entirely to inherit
DEFAULT_HIRE_ALL_WEEKLY_TOKEN_LIMIT.
The returned result shape is:
{
"success": true,
"validation": { "valid": true, "errors": [], "slug": "content-writer" },
"subagent": { "success": true, "adopted": false, "slug": "content-writer", "path": "...", "content": "..." },
"hire": { "success": true, "created": ["content-writer"], "skipped": [], "failed": [] }
}
Three failure modes, each with its own remediation:
result.validation.valid === false): the three
fields were invalid (empty, name slugifies to nothing, etc.). Nothing
was written. Surface formatCreateNewResult(result) (starts with
"Input rejected — …") and re-prompt the user.result.subagent.success === false): the
.md write failed and — critically — the aweek JSON wrapper was
never attempted. Surface the "Subagent file error" block and
resolve the underlying filesystem issue before retrying.result.hire.success === false): the .md landed
but the aweek JSON shell failed (schema error, filesystem issue).
Surface the nested formatHireAllSummary block so the user sees which
slug failed and why.Adoption is a first-class outcome, not a failure: when .claude/agents/<slug>.md
already exists, the helper returns subagent.adopted: true and keeps the
on-disk .md verbatim. The typed description + system prompt are
discarded per the "single source of truth" constraint. Tell the user
adoption happened and display subagent.content so they can confirm what
they are wiring into aweek scheduling.
skip: finish here. Remind the user they can run /aweek:hire at any
time to add an agent later.
When route.action === 'select-some' the markdown must run a second
interactive prompt — a multi-select over menu.unhired — before touching the
filesystem. The helpers in src/skills/hire-select-some.js own this flow so
the markdown does not have to hand-roll the choice payload or the
validation-and-dispatch glue.
Step 5.2b-1 — Build the multi-select payload. Use buildSelectSomeChoices
to turn menu.unhired into a ready-to-show AskUserQuestion payload. Each
choice is enriched with the live name + description from
.claude/agents/<slug>.md so users see what they are picking:
MENU=$(aweek exec setup-hire-menu buildInitHireMenu)
echo "$MENU" | aweek json wrap menu \
| aweek exec hire-select-some buildSelectSomeChoices --input-json -
The returned payload has this shape:
{
"promptText": "Select the subagents to wrap into aweek scheduling JSONs (pick one or more):",
"multiSelect": true,
"slugs": ["analyst", "writer"],
"choices": [
{ "value": "analyst", "label": "Analyst", "description": "Analyse things.", "missing": false, "path": ".../analyst.md" },
{ "value": "writer", "label": "Writer", "description": "Write things.", "missing": false, "path": ".../writer.md" }
]
}
Step 5.2b-2 — Render the multi-select. Display payload.promptText and
invoke AskUserQuestion with payload.choices, enabling checkbox / multi-pick
semantics (multiSelect: true). Users MUST be able to pick more than one slug
— a single-select would defeat the purpose of this branch. Collect the user's
picks as a string[] — call it selectedSlugs.
Step 5.2b-3 — Validate + wrap. Pass selectedSlugs to runSelectSomeHire
which re-validates the selection against the menu's unhired list (defense in
depth against stale menus or slugs hired concurrently) and then delegates to
hireAllSubagents to wrap every picked slug:
MENU=$(aweek exec setup-hire-menu buildInitHireMenu)
RESULT=$(aweek json compose menu="$MENU" selected='[<SELECTED_SLUGS>]' \
| aweek exec hire-select-some runSelectSomeHire --input-json -)
echo "$RESULT" | aweek exec hire-select-some formatSelectSomeResult \
--input-json - --format text
if [ "$(aweek json get success <<<"$RESULT")" != "true" ]; then
exit 1
fi
The returned shape is:
{
"success": true,
"validation": { "valid": true, "errors": [] },
"hire": {
"success": true,
"created": ["writer", "analyst"],
"skipped": [],
"failed": []
}
}
Two failure modes, each with its own remediation:
result.validation.valid === false): the user's
selection itself was malformed (empty, contained a slug not in
menu.unhired, duplicate slug, non-string entry). result.hire is null
— no wrapper was written. Surface
formatSelectSomeResult(result) (starts with "Selection rejected — …") and
re-render the multi-select so the user can pick a valid subset.result.hire.success === false): the selection was
structurally valid but one or more slugs failed to wrap at the hire-all
layer (the .md vanished between discovery and dispatch, a filesystem
error, etc.). The nested result.hire.failed list has per-slug error
details. Echo formatSelectSomeResult(result) so the user sees exactly
which slugs wrapped and which did not.The handler is intentionally idempotent — re-selecting an already-hired slug
produces a skipped entry (not a failure) and re-selecting never overwrites
a pre-existing aweek JSON. No confirmed: true gate is required because the
only filesystem writes are new .aweek/agents/<slug>.json files; no
.md or crontab entries are touched.
Never skip Step 5 — the init flow exists to give the user a clear next action, whether this is their first agent or a re-run after adding more subagents.
routeInitHireMenuChoice throws EINIT_HIRE_MENU_BAD_CHOICE, re-present
the menu (the user's selection did not match any available option, usually
because they tried to pick hire-all / select-some on an empty menu).routeInitHireMenuChoice throws EINIT_HIRE_MENU_BAD_SELECTION during
select-some, surface the error and re-prompt for a valid subset of
menu.unhired.skip — never fall
through silently to create-new without explicit consent.The menu is non-destructive: it only reads the filesystem. Launching
/aweek:hire afterwards creates new state but is itself non-destructive (no
.md overwrites — adopt-on-collision is enforced inside createNewSubagent).
No additional confirmed: true gate is required for Step 5.
.aweek/agents/ exists but is not a directory, report the error and
stop — never delete unexpected files without explicit user consent.claude/commands/ cannot be created (permission denied), report the
underlying EACCES error verbatimcrontab installed or their shell rejects
crontab -l / crontab -, surface the error and recommend installing
the heartbeat manuallyAfter a successful init, tell the user:
/aweek:hire/aweek:plan/aweek:summary/aweek:manageAfter the hire menu closes (whether the user hired an agent, skipped, or was
auto-delegated), clear any sticky heartbeat decision from .aweek/config.json
so the next skill invocation (e.g. /aweek:hire) re-prompts about the
heartbeat rather than silently re-using the old answer:
aweek exec setup clearHeartbeatDecision
This is a best-effort step — if it fails (e.g. no config file yet), log a warning and continue. The heartbeat decision is advisory cache; losing it is not an error.
The default data directory is .aweek/agents/ relative to the project root.
Agent config files live directly inside as <agent-id>.json. Per-agent
sub-directories hold weekly plans, token usage, inbox queues, and lock files.