skills/chain/SKILL.md
Execute a dependency chain in parallel waves using background agents
npx skillsauth add gioe/tusk chainInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Orchestrates parallel execution of a dependency sub-DAG. Validates the head task(s), displays the scope tree, executes the head task(s) first, then spawns parallel background agents wave-by-wave for each frontier of ready tasks until the entire chain is complete.
Use
/create-taskfor task creation — handles decomposition, deduplication, criteria, and deps. Usetusk task-insertonly for bulk/automated inserts.
Accepts one or more head task IDs and optional flags: /chain <head_task_id1> [<head_task_id2> ...] [--on-failure skip|abort]
When multiple IDs are provided, all heads are treated as wave 0 (run in parallel), and subsequent waves use the union of their downstream sub-DAGs.
| Flag | Values | Description |
|------|--------|-------------|
| --on-failure | skip, abort | Unattended failure strategy applied when an agent finishes without completing its task. skip — log a warning and continue to the next wave. abort — stop the chain immediately and report all incomplete tasks. Omit for interactive mode (default). |
Before Step 1, extract flags from the skill arguments:
--on-failure <strategy> from the argument string. Valid values: skip, abort.--on-failure is present with a valid value, store it as on_failure_strategy.--on-failure is absent or the value is invalid, on_failure_strategy is unset (interactive mode).Record the start of this chain run so cost can be captured when the chain finishes:
tusk skill-run start chain --task-id <head_task_id>
Pass --task-id only when exactly one head task ID was provided. With multiple heads the chain spans more than one task and cost can't be attributed to a single row — omit --task-id in that case:
tusk skill-run start chain
This prints {"run_id": N, "started_at": "...", "task_id": N | null}. Capture run_id — it's referenced by every exit path below.
Early-exit cleanup: If any step below causes the chain to stop before reaching the final report in Step 7, first call
tusk skill-run cancel <run_id>to close the open row, then stop. Otherwise the row lingers as(open)intusk skill-run listforever. The explicit cancel calls below cover the known early-exit paths; if you hit an unexpected bail-out, cancel before returning.
For each provided task ID, run:
tusk -header -column "SELECT id, summary, status, priority, complexity, assignee FROM tasks WHERE id = <task_id>"
tusk skill-run cancel <run_id>, then abort — "Task <task_id> not found."To Do and not In Progress: run tusk skill-run cancel <run_id>, then abort — "Task <task_id> has status <status> — only To Do or In Progress tasks can start a chain."tusk chain scope <head_task_id1> [<head_task_id2> ...]
Parse the returned JSON. The head_task_ids array lists all head IDs. Fetch assignees for all scope task IDs:
tusk -header -column "SELECT id, assignee FROM tasks WHERE id IN (<comma-separated scope IDs>)"
Display the sub-DAG as an indented tree grouped by depth:
Chain scope for Task(s) <id(s)>: <summary(ies)>
══════════════════════════════════════════════════════════════
Depth 0 (head):
[<id>] <summary> (<status> | <complexity> | <assignee or "unassigned">)
Depth 1:
[<id>] <summary> (<status> | <complexity> | <assignee or "unassigned">)
[<id>] <summary> (<status> | <complexity> | <assignee or "unassigned">)
Depth 2:
[<id>] <summary> (<status> | <complexity> | <assignee or "unassigned">)
Progress: <completed>/<total> tasks completed (<percent>%)
Scope validation:
tusk chain validate-scope <head_task_id1> [<head_task_id2> ...]
Parse the returned JSON. It has two fields: scope_type and skip_head_execution:
no-downstream: run tusk skill-run cancel <run_id>, inform the user there is no chain downstream — suggest /tusk <id> for each head instead. Stop here.all-done: run tusk skill-run cancel <run_id>, inform the user the chain is already complete. Stop here.heads-done-only (skip_head_execution: true): all head tasks are already Done — skip Step 3 and go directly to Step 4 (wave loop).active-chain: proceed normally to Step 3.The head task(s) must complete before any dependents can be spawned.
Single head: Spawn a single background agent and monitor it as before.
Multiple heads: Spawn all heads as parallel wave 0 background agents — issue all Task tool calls in a single message, just like a wave in Step 4. Monitor all of them before proceeding to Step 4.
For each head task, fetch its full details:
tusk task-get-multi <head_id1> [<head_id2> ...]
This returns a JSON array of full task objects (same fields as tusk task-get). Use the returned task, acceptance_criteria, and task_progress fields to populate the agent prompt.
Serialization format for placeholders:
{acceptance_criteria} — format as a numbered list, one criterion per line, with its ID in brackets:
1. [#<id>] <criterion text>
2. [#<id>] <criterion text>
If the array is empty, write None.{task_progress} — use the next_steps string from the most recent progress entry (index 0 of the task_progress array). If the array is empty or next_steps is blank, write None.{workflow} — the task's workflow field value. If null, write None.Spawn parallel background agents (one per head task):
Task tool call (for EACH head task):
description: "TASK-<id> <first 3 words of summary>"
subagent_type: general-purpose
run_in_background: true
prompt: <AGENT-PROMPT.md content with {placeholders} filled from task details>
Each filled prompt calls tusk task-worktree create and works in the recorded workspace (reusing one if already present) — the workspace plus commit-time scope guard handle isolation, no runtime hint needed.
After spawning, store the agent task ID and output file path returned by the Task tool (this is separate from the tusk task ID). Keep a running list of all output file paths across the entire chain — these are needed for the post-chain retro in Step 6. Monitor until all head tasks reach Done status or all agents have finished.
Monitoring loop:
The Task tool spawned each head agent with run_in_background: true, so the runtime emits an automatic completion notification when each agent exits. Do not chain sleep 30 && tusk ... — the runtime blocks long leading sleeps and emits a tool error every time, even though the agents still complete via the auto-notification.
Primary path: wait for auto-completion notifications.
No active polling required — the runtime delivers a notification when each background agent exits. As notifications arrive, fall through to the Resolve state sub-step below.
Stall detection (no notification within ~2.5 min):
If you have been waiting without a completion notification for ~2.5 minutes, an agent may be looping or running a long-running command. Use a short-sleep until-loop — the runtime sleep guard allows sleep 2 inside an until body — that exits as soon as no head tasks remain in non-Done status OR the wall-clock deadline elapses:
HEAD_IDS="<comma-separated head task IDs>"
DEADLINE=$(($(date +%s) + 150))
until [ -z "$(tusk "SELECT id FROM tasks WHERE id IN ($HEAD_IDS) AND status <> 'Done'")" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; do
sleep 2
done
After the loop exits, fall through to the Resolve state sub-step.
Resolve state:
Re-query DB status and decide how to proceed:
tusk "SELECT id, status FROM tasks WHERE id IN (<head_ids>) AND status <> 'Done'"
No rows → all head tasks completed successfully. Exit the loop and proceed to Step 4.
Rows returned (some tasks not Done) → check whether each agent has finished using TaskOutput with block: false and the agent task ID:
Done, those agents likely exhausted turn limits or hit unrecoverable errors. Break out of the loop and proceed to recovery below.Recovery (agents completed, tasks not Done):
Read the agents' output files to capture any final messages.
If on_failure_strategy is set, apply it automatically without prompting:
<id> (<summary>) did not complete (status: <status>). Skipping due to --on-failure skip." — then proceed to Step 4.tusk skill-run cancel <run_id>, then stop immediately. Report that the chain was aborted due to --on-failure abort and list which tasks completed vs. which did not.Otherwise (interactive), report to the user:
Agent(s) for Task(s)
<ids>have finished, but the task status is still<status>. Agent output file(s):<output_file_paths>How would you like to proceed?
- Resume — spawn new agents to continue where the previous ones left off
- Skip — leave these tasks as-is and stop the chain
- Abort — stop the entire chain
tusk task-start) and restart the monitoring loop.tusk skill-run cancel <run_id>, do not proceed to Step 4. Report that the chain was stopped.tusk skill-run cancel <run_id>, stop entirely. Report that the chain was aborted.Repeat the following until the chain is complete:
tusk chain frontier-check <head_task_id1> [<head_task_id2> ...]
Parse the returned JSON. It has two fields:
status — one of complete, stuck, or continuefrontier — array of ready tasks (non-empty only when status=continue)complete: all tasks in the subgraph are Done — break out of the wave loop and go to Step 5.stuck: tasks remain but no ready tasks exist in the frontier. Display the chain status for context:
tusk chain status <head_task_id1> [<head_task_id2> ...] --format text
The default output is compact JSON; --format text renders the human-readable multi-line table. Show the output to the user and ask how to proceed. If the user chooses to stop the chain here, run tusk skill-run cancel <run_id> before returning.continue: the frontier array contains at least one ready task — proceed to Step 4c.For each frontier task, fetch its full details:
tusk task-get-multi <frontier_id1> [<frontier_id2> ...]
This returns a JSON array of full task objects (same fields as tusk task-get). Use the returned task, acceptance_criteria, and task_progress fields to populate the agent prompt.
Serialization format for placeholders:
{acceptance_criteria} — format as a numbered list, one criterion per line, with its ID in brackets:
1. [#<id>] <criterion text>
2. [#<id>] <criterion text>
If the array is empty, write None.{task_progress} — use the next_steps string from the most recent progress entry (index 0 of the task_progress array). If the array is empty or next_steps is blank, write None.{workflow} — the task's workflow field value. If null, write None.Spawn parallel background agents — one per frontier task. Issue all Task tool calls in a single message:
Task tool call (for EACH frontier task):
description: "TASK-<id> <first 3 words of summary>"
subagent_type: general-purpose
run_in_background: true
prompt: <AGENT-PROMPT.md content with {placeholders} filled from task details>
Per Step 3's isolation contract, each agent owns its tusk task-worktree create workspace — reuse an existing recorded one rather than create an overlapping branch.
Build a map of tusk task ID → agent task ID → output file path for every agent spawned in this wave. Add each output file path to your running list for the post-chain retro (Step 6).
Monitoring loop:
The Task tool spawned each wave agent with run_in_background: true, so the runtime emits an automatic completion notification when each agent exits. Do not chain sleep 30 && tusk ... — the runtime blocks long leading sleeps and emits a tool error every time, even though the agents still complete via the auto-notification.
Primary path: wait for auto-completion notifications.
No active polling required — the runtime delivers a notification when each background agent exits. As notifications arrive, fall through to the Resolve state sub-step below.
Stall detection (no notification within ~2.5 min):
If you have been waiting without a completion notification for ~2.5 minutes, an agent may be looping or running a long-running command. Use a short-sleep until-loop — the runtime sleep guard allows sleep 2 inside an until body — that exits as soon as no wave task IDs remain in non-Done status OR the wall-clock deadline elapses:
WAVE_IDS="<comma-separated wave task IDs>"
DEADLINE=$(($(date +%s) + 150))
until [ -z "$(tusk "SELECT id FROM tasks WHERE id IN ($WAVE_IDS) AND status <> 'Done'")" ] || [ "$(date +%s)" -ge "$DEADLINE" ]; do
sleep 2
done
After the loop exits, fall through to the Resolve state sub-step.
Resolve state:
Re-query DB status and decide how to proceed:
tusk "SELECT id, status FROM tasks WHERE id IN (<wave_ids>) AND status <> 'Done'"
No rows → all wave tasks completed successfully. Exit the loop and go back to 4a for the next frontier.
Rows returned (some tasks not Done) → check whether each agent has finished using TaskOutput with block: false and the agent task ID:
Done, those agents likely exhausted turn limits or hit unrecoverable errors. Break out of the loop and proceed to recovery below.Recovery (agents completed, tasks not Done):
Read the agents' output files to capture any final messages.
If on_failure_strategy is set, apply it automatically without prompting:
<id> (<summary>) did not complete (status: <status>). Skipping due to --on-failure skip." — then proceed to 4a for the next frontier. Note that downstream tasks depending on skipped tasks will never become ready — if the chain gets stuck later, report this to the user.tusk skill-run cancel <run_id>, then stop immediately. Report that the chain was aborted due to --on-failure abort and list which tasks completed vs. which did not.Otherwise (interactive), report to the user, listing each stuck task with its summary, status, and agent output path:
Agent(s) for the following wave task(s) have finished, but the task status is still not
Done:
- Task
<id>(<summary>) — status<status>— agent output:<output_file_path>- Task
<id>(<summary>) — status<status>— agent output:<output_file_path>How would you like to proceed?
- Resume — spawn new agents to continue where the previous ones left off
- Skip — leave these tasks as-is and proceed to the next frontier (downstream tasks depending on skipped tasks will never become ready; if the chain gets stuck later, this is why)
- Abort — stop the entire chain
tusk task-start) and restart the monitoring loop.tusk skill-run cancel <run_id>, stop entirely. Report that the chain was aborted.After all waves are complete, do a single VERSION bump and CHANGELOG update covering the entire chain. This avoids merge conflicts that occur when parallel agents each try to bump independently.
Skip this step if:
Consolidation procedure:
Collect the list of completed tasks in the chain:
tusk chain scope <head_task_id1> [<head_task_id2> ...]
Filter to tasks with status = Done that were completed during this chain run.
Bump VERSION and update CHANGELOG in one step each:
tusk version-bump
tusk changelog-add <task_id1> [<task_id2> ...]
tusk version-bump reads VERSION, increments by 1, writes it back, stages it, and prints the new version number.
tusk changelog-add prepends a dated ## [N] - YYYY-MM-DD heading to CHANGELOG.md with a bullet for each task ID, stages CHANGELOG.md, then outputs the inserted block to stdout for review. The version is sourced from the just-staged VERSION file — omit it from the args to avoid drift (issue #814).
Review the changelog output, then commit, push, and merge:
git checkout main && git pull origin main
git checkout -b chore/chain-<head_task_ids>-version-bump
git commit -m "Bump VERSION to <new_version> for chain <head_task_ids>"
git push -u origin chore/chain-<head_task_ids>-version-bump
gh pr create --base main --title "Bump VERSION to <new_version> (chain <head_task_ids>)" --body "Consolidates VERSION bump for all tasks completed in chain <head_task_ids>."
gh pr merge --squash --delete-branch
Mark deferred-to-chain criteria as done for all completed chain tasks. Individual chain agents skip VERSION/CHANGELOG criteria using tusk criteria skip <id> --reason chain; the orchestrator completes them here:
tusk criteria finish-deferred --reason chain <task_id1> [<task_id2> ...]
This marks all is_deferred=1, deferred_reason=chain, is_completed=0 criteria for the given tasks and prints {"marked": N}.
After the chain completes, run a retrospective across all agent transcripts to capture cross-agent learnings. This uses the output file paths you collected during Steps 3 and 4.
Read the companion file for the full procedure:
Read file: <base_directory>/POST-CHAIN-RETRO.md
Where <base_directory> is the skill base directory shown at the top of this file.
Skip this step if:
/retro instead for single-task sessions).Display the completed chain status:
tusk chain status <head_task_id1> [<head_task_id2> ...] --format text
The default output is compact JSON; --format text is used here for the human-readable final report. Summarize:
Then close out the chain skill-run so its cost is captured:
tusk skill-run finish <run_id>
Read the template from the companion file and fill in {placeholders} with values from the task query:
Read file: <base_directory>/AGENT-PROMPT.md
data-ai
Autonomously work through the backlog — dispatches /chain for chain heads, /tusk for standalone tasks, repeating until empty
tools
Investigate the scope of a problem and form an honest assessment — task creation is optional
data-ai
Groom the backlog by closing completed tickets, removing redundant/stale tickets, reprioritizing, and assigning agents
tools
File a GitHub issue against the tusk repo itself — tusk bugs, CLI limitations, skill improvements, or missing features. Use anytime the user identifies a gap in tusk (not in their own project's code).