task

This skill should be used when the user wants to "create a task", "start a new task", "split a task", "make this an epic", "mark a task in progress", "mark a task done", "complete a task", "run this t

SKILL.md

GitHub

Task

A task is one unit of work inside a project: a folder holding task.json (every field a script reads) and task.md (the goal, in prose, that nothing parses). This skill makes one, moves an old one into place, changes its state, splits it into a parent with children, or sets its run mode. It does not run any of the six stages, and it does not pick which task is active: that is /aida:next.

Every action below needs the active project's own folder (the one holding project.json, never the code folder). Resolve that first, before using anything here. Take the project folder from the project-folder: line of the ## AIDA block. Then run "${CLAUDE_PLUGIN_ROOT}"/skills/project/scripts/project-actions.sh report once, and show the lines it prints. When it exits non-zero, invoke the project skill, whose table reads that exit. With no project-folder: line, invoke the project skill.

task-actions.sh takes a text value from a file. Write the text with the Write tool in the session's scratchpad folder. With no scratchpad, write it in the task folder, and delete it after the call. Then pass @/<absolute path> as the value. Use this for a text that names a command, such as git. Claude Code refuses a command whose quoted text names git when the session works in a worktree.

Determine the run mode

Look for a stated run mode on the task active in this conversation, when one is already active. Found, and it says autonomous or light: act autonomously through this whole invocation, passing --run-mode autonomous on every call to the script below. Anything else, including no active task, such as the moment create itself runs: act interactively, the safe default. Decide this once, at the start. A mode that names stages in brackets covers this call only when it names the stage this call runs inside.

task-actions.sh never asks a question on its own. Every fact below that this skill would otherwise ask for must be decided before the script runs; the script only writes what it is given and reports what happened. Every action prints summary lines: task-file: with the path, id:, state:, parent:, children: and runMode:, with the stages the mode covers in brackets when it covers fewer than all. It never prints the record. Read the file at the printed path when another field is needed.

create <name>

Makes the task and nothing else: no contract, no interview, no stage. It takes a name and a goal. Run it only when the person asked for this task in this conversation, by name or by a yes to an offer. Another skill's hand-off carries that yes. Nothing here invents one.

1. Name. Ask what to call it, unless already said. Check it against ^[a-z0-9][a-z0-9-]*$: lowercase letters, digits and hyphens, starting with a letter or digit. The worktree folder takes this name and becomes a hostname label, and DDEV lowercases and rewrites the rest. Existing tasks keep their ids. On a mismatch, say so and ask again; the script refuses it too, so this check only saves a round trip. Autonomous with no name given: halt. A task cannot be filed without one.

2. Goal. Ask what this task is for, in the spirit of a user story: what someone wants to accomplish and why, not a ticket. Write it back in one or two sentences and go on. Scope reads the goal with the person before anything is built. Autonomous with no goal given or implied by the conversation: halt. A task with no stated goal is not a record of anything.

3. Write it. First the worktree base. A project made before this setting may record none. Run:

"${CLAUDE_PLUGIN_ROOT}"/skills/project/scripts/project-actions.sh read-worktree-base "<projectPath>"

Exit 0: a base is recorded. Use it silently. Exit 1: it printed the default, and the tree goes there. Nothing is asked. Name that folder once, and say that the project's set-worktree-base action moves the next tree elsewhere:

"${CLAUDE_PLUGIN_ROOT}"/skills/project/scripts/project-actions.sh --run-mode <interactive|autonomous> \
  set-worktree-base "<projectPath>" ["<folder>"]

A tree once made stays where it is.

Then run the create, with the run mode set as decided above. Add --after <task-id> when the person said this task builds on another task of this project:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  create --project "<projectPath>" --name "<name>" [--after <task-id> [--in-tree]] [--tree-at-start] -- <goal...>

It writes the folder, task.json with state: "new", and task.md with the goal under ## Goal. It then makes the task's own git worktree in one folder per repository beside the code path, at <parent of codePath>/<slug of the code folder>.worktrees/<slug of the code folder>-<name>. The worktree base recorded above takes the place of <parent of codePath>. The tree is on the branch feature/<name>. The script records both in task.json and commits. The folder name is the slug of the code folder plus the task name, so a site name is predictable and unique. The tree is outside the code path for one reason. A nested worktree is invisible to a tool that registers projects by folder, and DDEV hands it to the parent project. A tree that task.json already records on disk stays where it is. The tree is cut from the trunk, HEAD of the code path, wherever create runs. The base: line names the commit. Show it. With --tree-at-start, create makes no tree: start makes it. Completion makes each follow-up task this way, so a follow-up nobody starts costs no tree. Skip steps 4 and 5 then. With --after, the tree is cut from that task's branch, and task.json records after. Use it for a chain of tasks made up front, because a tree cut now from trunk holds none of the earlier builds. While that task's build is unfinished, no tree is made: worktree: reads none and after-build: reads unfinished. Skip steps 4 and 5 then. start makes the tree once that build is finished. Add --in-tree with --after when the person runs the chain one task after another. The task then takes over that task's tree, on the new branch feature/<name> from its tip. So the chain pays one checkout and one dependency sync. That task's review runs in its tree, so the take-over waits until that review has closed. While it is open, create makes no tree and prints in-tree:. Skip steps 4 and 5 then. start takes the tree over, or cuts a new tree if the review is still open, and says which. That tree must hold no uncommitted change. Both tasks then record one tree, and the branch checked out there says which task builds in it now. A stage action of the other task exits 3, and its message names the git switch that gives it the tree. When no tree holds that task's branch, a new tree is cut as with --after alone. Show the whole output. Exit code 3 has five causes. The name collided with an existing task, or it failed the name rule. --after named a task this project does not hold, or --in-tree came without --after. Or the worktree could not be made. In the last case the folder is removed. Say what it printed. For a name, ask for a different one. For the worktree, name the repair the message gives and stop.

4. Enter the tree. Every stage action of this task runs inside that worktree, and refuses from anywhere else. The worktree: line names it. Enter it as /aida:next says under "Enter the tree", the one copy of that rule. Go on to step 5 either way.

5. Offer the site. Runs here after step 4, and again at start whenever the task record still has no environment, whoever called start. A worktree has the branch's files and no site, so a review or a baseline taken there would capture the served checkout instead. Run recipe-point once, as the surfaces skill does, with --point worktree-environment and one --framework flag per framework the project records. When the project record has surfaces.e2e.enabled, add --point e2e-setup. When it has surfaces.visualRegression.enabled, add --point visual-regression. up then installs that harness in the tree. Pass the worktree-environment answer= as --recipe <framework>=<path> or --lookup-failed <framework>=<word>, one flag per framework. Pass each setup recipe as --setup-recipe <kind>=<path>, where the kind is e2e or visual-regression. A setup lookup that returns no path gives no flag, and up says that harness is not installed. Then run environment <name> show. The word is no-recipe, listing-unreachable or fetch-failed; the script refuses any other. not-applicable from show means no framework has a recipe: record it with environment <name> not-applicable -- <that reason>, and say once that this worktree has files and no site. show runs the precondition checks and exits 3 when one fails. A failing check means the site cannot come up, so make no offer. Show the check's output and the remedy it names. When the remedy says to commit, give the environment: lines that name the branches. Record nothing, so the offer comes again at start. Otherwise, interactive: show the commands and the prose, and ask once whether to bring the site up now. A yes runs environment <name> up with the same flags. A no is recorded too, with the person's reason, through the same not-applicable call, so nothing offers again. Say up with the same flags still brings it up later. Autonomous: never bring it up, record nothing, and say once that the offer waits for a person.

environment <task-id> <show|up|down|not-applicable>

The worktree's own running site, from the framework's worktree-environment recipe. The recipe holds the commands; this plugin holds none. show and up take the recipe flags step 5 names, and the --setup-recipe flags. down takes none: it reads the recipe path the record holds. not-applicable takes the person's reason after --.

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  environment --project "<projectPath>" "<task-id>" <show|up|down> <recipe flags>
"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  environment --project "<projectPath>" "<task-id>" not-applicable -- <reason...>

not-applicable writes environment as the reason alone, and commits. up replaces it; down with it recorded says nothing was up. show prints the recipe path, the preconditions prose and the build-in-place prose. It prints the token, bring-up, address and tear-down commands with {codePath} filled, and runs none of them. It prints the paths of the ## Files blocks, the files up writes, and one precondition: line per ## Preconditions command up runs. show runs each ## Preconditions line the way up does, with the same code. It removes the files it wrote for the check and puts back the ones it replaced. So it commits nothing and leaves the tree as it found it. A failing check exits 3 and prints what up prints. A worktree not on disk prints precondition check: not run, because only up makes a tree again. A recipe with no bring-up block, or no address block, exits 3 from show too, so its exit code says what up would do. When the recipe has a ## Status block and the worktree is on disk, show runs its one line there. Exit 0 prints status: up. Any other exit prints status: down and the line's first output. The line may run a script from ## Files. The script is written where absent, and taken out after the line. The implementation step runs the same line before an order's verify lines.

up needs no person's answer, because down takes the site away again. It runs in the task's worktree, with each command as it ran and that command's own output in records/environment-up.txt, in this order, and one line in that record marking the address command, so down can find that command's output later. A site the worktree already runs is adopted. This applies when task.json records no address and no marker, and the recipe's ## Status line says the site is up. Then up writes and commits the ## Files blocks, runs the address command with its root: check, and records the address with adopted: true. The root check runs before the commit, so a site in another tree leaves nothing. It runs no precondition, token or bring-up line, because the seed would replace that site's database. It records no named file in worktree.recipeChanges, and prints environment: adopted in place of environment: coming-up. It also says that no bring-up took a seed. A configuration gate then restores the seed the worktree already holds, whose age is unknown. Say so to the person. show then prints precondition check: not run. First it writes each ## Files block absent from the worktree. A file that holds the block of an earlier version of the recipe is replaced. Any other file with other content refuses at 3. The earlier versions are the ones the navigator store holds, so nothing is fetched. Then it runs each ## Preconditions line, after the files because the check is a script the recipe ships. A failing line stops at 3, prints its output, removes the files this run wrote, puts back the ones it replaced, and commits nothing. When a line ran and failed, it also names the task branch and the branch the worktree was cut from. An older task names the branch the code path is on now, and says so. A failed commit, or an interrupt before the commit, also removes the written files, their index entries and the output file, and says so. Then it commits the written and replaced files alone, so other changed or staged work is never taken in. Then it records in worktree.recipeChanges each ## Files path, and each file the ## Preconditions prose names that the branch changed, with the content each holds. It skips a file git tracks neither at HEAD nor where the branch started. No order owns them, so the owned-files checks in the build and in review set them aside until an edit changes that content. A site brought up before this record existed has none. Run up again to write it: files already committed are not committed again. Then each ## Tokens command, whose first output line is the token's value. A token command that prints nothing or fails refuses at 4 by the token's name. Then it writes the marker into environment: state: coming-up, the recipe, the time and each token's value. down fills those values into the tear-down, so it can remove what the bring-up made in the main checkout. The record names the site before the site exists, so a failure during the bring-up leaves a site down can still find. The marker keeps every other field the record held, so a second up over a site that is up does not drop that site's address while the bring-up runs again. Then the bring-up lines before the ## Address heading. Then one line marking the address command in the record, so down can find that command's output later. Then the address command, whose output is key: value lines. address: is required, and every other key is a token for the later lines and for the tear-down. A root: line that is not the worktree stops at 3 before the later lines: the environment resolved to another tree, and the marker stays for down. Then it completes the record, in place of the marker: the address, the recipe, when, and the other address keys. Then the bring-up lines after the heading. Then, for each surfaces kind the project has on, the ## Install lines of the setup recipe given as --setup-recipe. It commits nothing after that; what the install left uncommitted is named and stays for the task's own commit. With no path for a kind it says so and goes on, and the harness is the person's next step. A line still holding an unfilled {token} stops at 3 and names it. A failing line stops at 4 with a first: line. Show that line; do not bring the site up by hand. Any refusal after the marker leaves the marker, so run down before up again. It prints environment: coming-up, then address:. Running it twice is safe: the recipe promises every step runs again cleanly.

down runs the tear-down lines, each one and its output to records/environment-down.txt, and removes environment from task.json. It runs unattended too: tearing a copy down loses nothing. An adopted site is no copy, so down runs no tear-down line for it. It keeps adopted and the filled tear-down lines, commits, and says the site stays up in the worktree. Prune refuses a tree whose record says adopted. A tree another task holds stays, and that record stays with it. A second down runs the recipe's ## Status line in the worktree. A site that is down loses the mark, so prune can remove the tree. A site still up is refused at 3 with the tear-down lines. With no ## Status line, only a person can say the site is gone, so it refuses unattended at 70. With nothing up it says so and exits 0. It reads the recipe from the marker as it reads it from a finished record, so a site that was coming up is torn down the same way. A marker with no address key holds none of the other address keys either. up marks the address command in records/environment-up.txt before it runs it. down fills a tear-down token from the output under that mark, and from nothing else in that file. No mark means the address command never ran, so down reads nothing there. A token nothing fills stops it at 3 and names that token. Run it before the worktree is removed, or the framework keeps an orphaned registry entry; the completion body names it when a site is up or coming up. Review and baseline read environment.address before asking for a base URL.

The files in worktree.recipeChanges are for the worktree's own site, and trunk must not receive them. Completion takes the site down. Then it commits each file back to its content where the branch started. A DDEV worktree then holds the main checkout's name: line again. So a site command there can reach the main checkout's site. The content decides, not who made the commit or its subject. The test reads the changed files that existed where the branch started. When HEAD or the working tree holds one of them at that content again, up and down refuse at 3. They name the latest commit on those files. show then prints status: not run. A ## Files script the branch dropped does not count, because it names no site. To work on the site again, put the change back first. The test can refuse wrongly: a file the preconditions prose only names, changed before up and then changed back, counts too. It also misses a recipe that names the site in a file absent where the branch started.

prune [<task-id>]...

The worktrees of complete tasks. A task nobody started reaches here once it is complete, such as a moot follow-up closed with its reason. An open task keeps its tree. A worktree kept after completion costs disk and a DDEV project each, and a tree removed too early loses uncommitted work. So this lists first, asks once, and removes only what a person named. Run it from the main checkout, never from inside a tree it may remove.

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  prune --project "<projectPath>" [--all] [<task-id>]...

1. List. With no id the script prints one id: line per complete task that records a worktree. The line holds the path, the branch, and whether the branch is merged into the code path's current branch. It also says whether the tree is on disk and whether a site is up. A site reads coming-up for a tree whose bring-up did not finish. prune: none means nothing to remove. Show the lines. Autonomous: this is the whole action. Say once that a tree goes only on a person's yes, and stop. The script refuses an id unattended at 70.

2. Ask once. Show the list and ask which trees go. The answer names the trees, or says all of them. Say first which branches are unmerged: the tree goes, that branch stays.

3. Remove. Run the action once with every id the answer named, in the order given, or with --all when it named every tree. For each tree the script tears the site down when one is up, then removes the tree. It removes the group folder when it is empty, never the worktree base itself. It deletes the branch when git deletes it as merged. Otherwise the branch stays, and the pruned: line names it, so the person decides. A task that records no tree prints unchanged:. It clears worktree and environment from task.json and commits. It prints one pruned: line per tree naming what happened to the branch. A tree that another task also records stays, and its line says kept: a later task made with --in-tree builds in it. Show them. Exit 3 names the tree it stopped at and why. The task is not complete, git refused a tree with uncommitted changes, or the tear-down failed. Nothing after that tree was touched, and nothing is ever forced. Say what it printed and stop.

repair <old-task-folder>

Moves an old task into the project's own tasks/ folder, the first time it is opened. This is the only move in the whole skill: everything else here changes a field, never a location.

Given the path to a task folder still sitting under implementation_process/in_progress/ or implementation_process/completed/ inside the project folder, or under a repaired parent's own in_progress/ or completed/ in tasks/, run:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh repair --project "<projectPath>" "<old-task-folder>"

No confirmation is needed: nothing here is destructive. The script refuses outright when the destination already exists. When the old folder is gone and tasks/ holds the task, it was repaired already: it prints UNCHANGED and exits 0. It reads the goal under ## Goal, or under ## Problem when there is no ## Goal, the heading some version 5 records used. With neither it refuses, naming both, rather than moving something it cannot verify. It prints goal-heading: with the one it read. It reads the goal, and the parent and children when an old header carries them, back from the new location before it reports success. Show the whole output either way.

A version 5 parent task holds its children under in_progress/ and completed/ inside its own folder. The same call moves each one to tasks/<child id>/, sets the child's parent to that task and adds it to the task's children. It prints one MOVED: line per child. A child it cannot move, because its goal heading is missing or its destination exists, stays where it sits with a LEFT: line naming why. The parent's own repair is never refused for a child. Fix the child there, then run this action on that path. It moves to tasks/<child id>/, takes the parent from the folder it sat in, and joins that parent's children.

Inside the new folder it renames alignment.md, research.md, architecture.md and research/ to alignment.v5.md, research.v5.md, architecture.v5.md and research.v5/, each when present. It prints one KEPT: line per rename. Version 6 writes under those names, and the old files are the input the first run of each stage reads. It refuses, before moving anything, when a .v5 name already exists in the old folder.

Which old tasks still need this is /aida:next's job: it lists them as kind: legacy and runs this action on the one it loads.

start <task-id>

A task becomes in progress the moment a stage first writes an artifact into it. Each stage's own script calls this once, before its first write: scope init, research start, design start, implement start and review checks. It skips the call when the task is already in progress. So it is not usually a person typing a command. By hand, run:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  start --project "<projectPath>" "<task-id>" -- <why...>

Already in_progress: prints UNCHANGED and does nothing further. Already complete: refused at 3, since a completed task is not reopened here. Otherwise it makes the tree when the task records none, as a follow-up does, cut from the trunk. Then it writes the new state, commits, and runs the task check. Show the whole output. The check reports and never repairs, and start exits 0 whatever it found. A finding here is the one thing to repair now, before the stage writes anything.

A task made with --after starts only when the task it builds on has finished its build. That task then holds implementation/finished.json, or reads complete. Otherwise start refuses at 3, and the stage that called it writes nothing. Say which task to build first. When it starts, start cuts the tree from that task's branch, and worktree: names the tree. That task may be complete with its branch merged and pruned. The tree is then cut by the base rule above, because that work is on trunk. A tree that cannot be cut exits 3, and no state is written. Enter it as /aida:next says under "Enter the tree".

When the output holds environment: none, run create's step 5 now, whoever called start: a person by hand, or a stage's script through scope's init. A stage's script passes that line through. Unattended, the line says the offer waits for a person, and nothing is recorded.

When the output holds environment: coming-up, a bring-up did not finish and a site may be running. Run environment <task-id> down before any stage runs. Then offer the bring-up again.

complete <task-id>

Writes state: complete. This action is the one writer of that state, and the completion skill calls it last. A person runs the completion skill, not this action: completion reads the review verdict, records on what grounds the task closed, and writes the pull request body. This action records none of that, so a task closed here has a state and no grounds.

The completion skill passes the summary the person gave, after --. The script refuses an empty one outright. By hand, the call is:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  complete --project "<projectPath>" "<task-id>" -- <summary...>

A task nobody started closes here too, such as a follow-up the person finds moot. The summary is the reason, and prune then removes its tree. It sets the state to complete, appends a dated ## Completed section holding the summary to task.md, and commits this task's folder, tasks/<id>, alone. Every task action commits that way, never tasks/ whole, so another task's uncommitted file is never taken. That commit carries the completion record and the body when completion called it. Already complete: prints UNCHANGED. Show the whole output.

split <parent-task-id>

Turns one task into a parent with two or more children. Nothing moves, so there is no temporary build, no atomic swap and no rollback copy: version 5 needed all of that because splitting moved folders; here nothing does.

Every fact this needs must already be decided before calling it: which children, each child's own goal, and which criteria hand down to it. This skill never derives them. The research skill's split advisor recommends the children and their criteria after research closes. That skill's split-read action checks that every criterion is claimed once, before this action runs. This only performs the mechanical split.

Each child takes the parent's after, so it waits for the same build. It gets its tree when it starts, so a tree refused for one child never leaves a split half written. The two-level limit stays: a task that already has a parent cannot be split again, and a child always lives in the same project as its parent, never another one. Run:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  split --project "<projectPath>" "<parent-task-id>" \
  --child "<child-id-1>" --goal "<goal-1>" [--criterion "<text>"]... \
  --child "<child-id-2>" --goal "<goal-2>" [--criterion "<text>"]...

Repeat --child ... --goal ... [--criterion ...] once per child, at least twice. It creates each child's folder and task.json with parent set to the split task's own id, writes each child's goal and any handed-down criteria into its task.md, adds every new id to the parent's own children list, reads all of that back, and commits everything together. Exit code 3 before any write means the named task does not exist, a child id is refused, or the two-level limit stopped it. Say which and stop. Show the whole output otherwise.

set-run-mode <task-id> <autonomous|light|interactive> [--stage <stage>]...

Run mode is written only when a person explicitly asks for an autonomous or a light run on this task. Nothing above asks about it on its own, and nothing here proposes it either. Only call this when asked. A change needs the person's answer first, in every mode, because the run mode decides which person checks still read it. Record it with person-answer.sh below, step task set-run-mode. The words are the mode, then each stage, one space apart, such as autonomous implement. Without it, the call refuses (exit 70) and prints the command. Then run:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  set-run-mode --project "<projectPath>" "<task-id>" <autonomous|light|interactive> [--stage <stage>]...

autonomous writes the field. --stage, repeatable, limits it to the stages named, one of scope, research, design, implement, review, completion. A person who asks for the build alone unattended passes --stage implement. No --stage covers every stage. interactive removes the field and the stages: there is no "interactive" value to write, since the field's absence already means that. Show the whole output.

light is an autonomous run over every stage that skips named steps, and it takes no --stage. Each skip is logged in COMPROMISES.md in the task folder. The path-script: line says what light does about the one script that walks the demo path. registered: the project has it, and no order writes or extends it. create: the first order writes it. off: say that a person may set up end to end with /aida:surfaces e2e before the run. declined: light keeps no such script.

A light or autonomous run still stops for a person, and a dispatched agent cannot answer for one. So when one session coordinates agents that run the tasks, that session holds every person-only call. An agent never runs person-answer.sh, and a hook refuses it. It reports the stop to the coordinator once and stops. The coordinator puts the stop to the person, records the answer, and runs the call itself.

When the mode covers review, two lines can follow, on light or autonomous. The sign-off: line names the criteria a person answers, so an unattended review writes no verdict. The review-tools: line names the review tools that preconditions found absent. Before preconditions has run, it says that preconditions will check them. Read both to the person.

person-answer.sh <task-id> --step <step> [--order <id>] -- <words>

Some steps are a person's decision only. Some take the person's answer from the chat when the session is attended and the stage runs interactive: ask in the chat, and run the step only on the person's answer, with their words. Every other one refuses until the person's answer is on record. The refusal prints this command with the step, the order and the words to use. Run it:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/person-answer.sh --project "<projectPath>" "<task-id>" \
  --step "<step>" [--order <order id>] -- <the person's words>

Words that run over one line go in a file, passed as @/<file>. Claude Code shows its permission prompt for this script, and the person approves or denies it. Never answer for the person. A denied prompt records nothing. In bypassPermissions or dontAsk mode the hook refuses the script, because no prompt can show there. Then stop and report the step to the person.

The script appends one entry to person-answers.json in the task folder. The step it serves marks the entry used, or refused when the step refuses for a reason other than an argument fault. So each attempt needs its own answer. No entry is removed. A hook refuses every other write of that file, and of task.json, from any session. The implement skill's references/finish.md lists the steps and their words.

set-budget <task-id> [--dispatches <n>] [--minutes <n>]

A budget is the ceiling on one implementation run, and a person sets it. Nothing above asks about it, and nothing here proposes one. Only call this when asked. Run:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  set-budget --project "<projectPath>" "<task-id>" [--dispatches <n>] [--minutes <n>]

--dispatches counts the roles implementation dispatches over the whole run. --minutes counts wall-clock time from the ledger's own start. Either one alone is a ceiling, and both may be given. Neither is refused. Each takes a whole number of 1 or more, and the action refuses anything else. The task check does not read the schema's minimum, so a zero would pass it and stop the first dispatch. A number this call does not name keeps the value it had. There is no action that removes a budget.

The build stops the order it was about to dispatch when either number is reached, with budget spent. To answer that stop a person raises the number here first. Then they go on with go-on in the implementation skill, which clears the stop. The order is the whole point. The spend is recomputed from the ledger at every dispatch, so going on before the raise brings the stop straight back. Show the whole output.

save <task-id>

A person stops mid-stage, and a decision this conversation made is in no record yet. This writes it down for the next window. Only a person invokes it; nothing dispatches it.

The current stage is the stage the next skill's report prints for this task, the first whose close record is absent. That is scope while alignment.json lacks pluginVersion, which only scope's close stamps. Research is the stage without records/research-check.json at exitCode 0 and with no design folder yet, and design without design-closed.json. A task in state new, or whose stage has no record on disk yet, skips the distiller: nothing exists to distill. The stage's first record is alignment.json for scope, research/*.json for research, design/*.json for design. Say which stage it would have been and that none exists, then go on to the list. Otherwise, when the stage's sidecar records/<stage>-distill.json exists, run the stage's own distill action from the task's worktree, the call started with cd <worktree> &&:

"${CLAUDE_PLUGIN_ROOT}"/skills/research/scripts/research-actions.sh distill "<task_folder>"
"${CLAUDE_PLUGIN_ROOT}"/skills/design/scripts/design-actions.sh distill "<task_folder>"

Scope needs no call, because its sidecar is its close record. The action prints standsAlone: stale when the sidecar is older than the last change to the stage's records. A stale sidecar counts as absent. So does one out of shape: the action sets it aside and exits 4. When none exists, dispatch the distiller role with the run mode, the task folder, the stage, and the stage's record paths, one per line. Then read the sidecar it writes. A record path absent mid-stage is normal; the distiller names it as a gap.

Name what this conversation decided that neither the sidecar's decisions nor the stage's own files hold. Each is one sentence: what was decided and what it applies to. Write them, then show what was saved. Nothing to save is said in one line, and save still runs with no text. That records the time, so a later manual compaction writes no warning.

Run once per sentence, or once with all of them:

"${CLAUDE_PLUGIN_ROOT}"/skills/task/scripts/task-actions.sh --run-mode <interactive|autonomous> \
  save --project "<projectPath>" "<task-id>" -- <text...>

It appends the text to <task_folder>/notes/<date>.md under a ## <UTC time> heading. It records savedAt in task.json, commits, and prints savedAt: and note: with the path. With no text it writes no note and prints savedAt: only. When the stage had no record, it prints distill: none naming the stage. Show the lines.

A note is never a stage record: the stage action that later records the same decision makes it stale, and the record wins. The session-start hook names the newest note after Stage:, and /aida:next lists its date, so the next window reads it before its first turn.