| name | sprint |
| description | Use when the user wants to execute an epic, run a sprint, work through a planned epic's stories and tasks, or coordinate multi-agent task execution end-to-end. Routes the epic by complexity (SIMPLE → direct implementation-plan, MODERATE → lightweight preplanning, COMPLEX → full preplanning), runs an SC-coverage gate at haiku/sonnet/opus tiers to confirm story coverage of epic success criteria, plans the task graph, dispatches sub-agents in batches with file-overlap and semantic-conflict checks, runs per-task review and post-batch validation (test gate, lint, AC verification, visual verification for UI), commits/pushes results, and verifies epic completion via the dso:completion-verifier agent before close. Trigger phrases include 'work the epic', 'execute the sprint', 'run the epic', 'sprint this epic', 'work through the stories', 'implement the planned tasks', 'kick off the sprint'. |
| user-invocable | true |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
Requires Agent tool. If running as a sub-agent (Agent tool unavailable), STOP and return: "ERROR: /dso:sprint requires Agent tool; invoke from orchestrator."
Purpose
You are Senior Orchestrator Agent that follows a clearly defined sprint process and uses sub-agents to execute actions. You are protective of your context window, using sub-agents to investigate, edit, or resolve.
Execute Epic: Multi-Agent Orchestration
Config Resolution (reads project .claude/dso-config.conf)
At activation, load project commands via read-config.sh before executing any steps:
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT}"
PLUGIN_SCRIPTS="$PLUGIN_ROOT/scripts"
TEST_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.test)
LINT_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.lint)
FORMAT_CHECK_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.format_check)
VISUAL_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.test_visual)
E2E_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.test_e2e)
SPRINT_MODE=$(bash "$PLUGIN_SCRIPTS/mode-detect.sh")
Resolution order: See ${CLAUDE_PLUGIN_ROOT}/docs/CONFIG-RESOLUTION.md.
Resolved commands used in this skill:
TEST_CMD — replaces make test-unit-only in post-batch and remediation validation; interpolated as {TEST_CMD} in task-execution.md
LINT_CMD — replaces make lint in validation steps; interpolated as {LINT_CMD} in task-execution.md
FORMAT_CHECK_CMD — replaces make format-check in validation steps; interpolated as {FORMAT_CHECK_CMD} in task-execution.md
VISUAL_CMD — replaces make test-visual in post-batch checks
E2E_CMD — replaces make test-e2e in post-batch checks
SPRINT_MODE — ci-pr or local; governs per-story PR mechanisms. Resolved here at activation so every Phase A ci-pr-only block (Ruleset Preflight, Draft PR Creation) can read it safely. The Mode Banner subsection later in Phase A only emits the banner.
Migration Check
Idempotently apply plugin-shipped ticket migrations (marker-gated; no-op once migrated, never blocks the skill):
bash "$PLUGIN_SCRIPTS/ticket-migrate-brainstorm-tags.sh" 2>/dev/null || true
bash "$PLUGIN_SCRIPTS/ticket-migrate-schema-hardening.sh" 2>/dev/null || true
bash "$PLUGIN_SCRIPTS/migrate-design-notes-to-design-md.sh" 2>/dev/null || true
Stage-Boundary Entry Check
Source the preconditions validator library and run the entry check for the sprint stage (fail-open: || true prevents blocking when no upstream implementation-plan event exists yet):
source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/preconditions-validator-lib.sh" 2>/dev/null || true
_dso_pv_entry_check "sprint" "implementation-plan" "${primary_ticket_id:-}" || true
Orchestration Flow
Flow: P1 (Init) → Preplanning Gate
→ [0 children/ambiguous] /dso:preplanning → P2
→ [children exist & clear] P2 (Task Analysis)
P2 → [stories without impl tasks?] layer-stratify → sequential Skill-tool dispatch (per-layer) → STATUS:complete→tasks created | STATUS:blocked→ask user → Re-gather → P3
P2 → [all have impl tasks] P3 (Batch Preparation)
P3 → [execute] P4 (Sub-Agent Launch) → P5 (Post-Batch)
P5 → [context >=70%] /compact → P3 (proactive, safe — all work committed)
P5 → [involuntary compaction detected] P8 (Graceful Shutdown)
P5 → [more ready tasks] P3
P5 → [all done] P6 (Validation)
P6 → [score=5] P8 (Completion)
P6 → [score<5] P7 (Remediation) → P3
Filing bugs discovered during the sprint: tooling, infrastructure, or otherwise-unrelated bugs found while orchestrating an epic are top-level tickets, not children of the epic under sprint. Pass --parent <epic-id> only when the bug's resolution is required for one of the epic's Success Criteria. See ${CLAUDE_PLUGIN_ROOT}/skills/create-bug/SKILL.md § "Parent Linkage Policy" (bug 7f23-1a14).
Phase A: Initialization & Primary Ticket Selection (/dso:sprint)
Parse Arguments
<primary-ticket-id>: The primary ticket to execute
If No Primary Ticket ID Provided
-
Run the epic discovery script:
.claude/scripts/dso ticket list-epics --all --has-tag=brainstorm:complete
This outputs tab-separated lines in three categories:
<id>\tP*\t<title>\t<child_count>[\tBLOCKING] for in-progress epics (4 or 5 fields; P* replaces priority)
<id>\tP<priority>\t<title>\t<child_count>[\tBLOCKING] for unblocked open epics (4 or 5 fields)
BLOCKED\t<id>\tP<priority>\t<title>\t<child_count>\t<blocker_ids> for blocked ones (6 fields; with --all)
The <child_count> field is the number of child tickets. The <blocker_ids> field is a comma-separated list of open blocker epic IDs.
Exit codes:
- Exit code 1 → no open epics exist, report and exit
If no eligible epics remain after applying --has-tag=brainstorm:complete (i.e., the filtered output is empty or exit code 1):
- Report: "No epics with the brainstorm:complete tag are ready to execute."
- Run the same command without
--has-tag=brainstorm:complete to count how many epics were hidden:
.claude/scripts/dso ticket list-epics --all
- If there are epics without brainstorm:complete that were filtered out, show: "There are N epics without the brainstorm:complete tag. Run
/dso:brainstorm on one to complete scrutiny review before executing."
- Exit.
-
Parse the output and print a numbered list. CRITICAL: You MUST output the formatted list as visible text. Number in-progress (P*) epics first, then unblocked. Blocked epics are informational only (not selectable). Render BLOCKING epics in bold.
-
Display the text: "Enter the number or epic ID to execute:" and wait for the user's text input.
-
Map the user's response (number or epic ID) back to the corresponding epic and proceed
Validate Primary Ticket
Set primary_ticket_id = <the resolved ticket ID>.
-
Run .claude/scripts/dso ticket show <primary_ticket_id> — confirm status is open or in_progress
-
If the ticket status is in_progress:
Load skills/sprint/prompts/auto-resume.md and follow the instructions it contains.
- Mark ticket in-progress:
.claude/scripts/dso ticket transition <primary_ticket_id> in_progress
Post WORKTREE_TRACKING:start on the epic ticket (fail silently if .tickets-tracker/ unavailable): # tickets-boundary-ok
_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
.claude/scripts/dso ticket comment <primary_ticket_id> "WORKTREE_TRACKING:start branch=${_BRANCH} session_branch=${_BRANCH} timestamp=${_TS}" 2>/dev/null || true
Set vars.SPRINT_SESSION_ID: Set the SPRINT_SESSION_ID repo variable so resolve-session-branch.sh can discover the session branch as a fallback (step 2 of its 3-step fallback chain). PATCH first (update existing); POST as fallback (initial creation). || true ensures failure (no gh auth, no actions:write permission, fork repo) does not block sprint execution.
_SESSION_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
_GH_REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)
if [[ -n "$_GH_REPO" ]]; then
gh api --method PATCH "repos/$_GH_REPO/actions/variables/SPRINT_SESSION_ID" \
-f value="$_SESSION_BRANCH" 2>/dev/null || \
gh api --method POST "repos/$_GH_REPO/actions/variables" \
-f name="SPRINT_SESSION_ID" -f value="$_SESSION_BRANCH" 2>/dev/null || true
fi
The .sprint-active marker is gitignored and scoped to the session worktree. It enables the session-merge-only-check pre-commit hook. Phase I removes it.
touch "$(git rev-parse --show-toplevel)/.sprint-active"
Ruleset Preflight (ci-pr mode only)
When SPRINT_MODE=ci-pr, run the Ruleset preflight check before continuing:
if [[ "${SPRINT_MODE:-}" == "ci-pr" ]]; then
if .claude/scripts/dso sprint/check-ruleset-preflight.sh 2>/dev/null; then
echo "Ruleset preflight: OK"
else
echo "WARNING: Ruleset preflight failed — session-* branch protection may not be configured. See INSTALL.md#github-rulesets-for-session-branches" >&2
echo "Continuing sprint — preflight is advisory, not blocking."
fi
fi
This check is NON-BLOCKING (advisory only). If Rulesets aren't configured, warn but continue. Integration boundary: this preflight is the dso-94ya integration point between sprint orchestration and the GitHub Ruleset enforcement layer.
Draft PR Creation (ci-pr mode only): When SPRINT_MODE=ci-pr, open a long-lived draft PR before Phase E dispatch using create-sprint-draft-pr.sh:
See also: create-sprint-draft-pr.sh for Phase A draft PR creation (ci-pr mode only)
if [[ "${SPRINT_MODE:-}" == "ci-pr" ]]; then
DRAFT_PR_URL=$(SESSION_BRANCH="${_BRANCH}" PRIMARY_TICKET_ID="${primary_ticket_id}" EPIC_TITLE="${_EPIC_TITLE:-}" \
"$(git rev-parse --show-toplevel)/.claude/scripts/dso" create-sprint-draft-pr.sh)
_draft_rc=$?
if [[ $_draft_rc -ne 0 || "$DRAFT_PR_URL" != https://* ]]; then
echo "ERROR: Phase A draft PR creation failed (rc=$_draft_rc) — halting before Phase E dispatch" >&2
exit 1
fi
echo "Draft PR: $DRAFT_PR_URL"
fi
Phase E dispatch must not begin until this block completes.
Cascade-counter entry init (before the epic/non-epic branch): initialize the replan cascade counter once here so every routing path — epic (Drift Detection / Phase B) and non-epic story/task (which routes REPLAN_ESCALATE: into d-replan-collect but skips both the Drift and Phase B inits) — has an initialized counter for the replan_cycle_count >= max_replan_cycles cap comparison:
replan_cycle_count = replan_cycle_count ?? 0
max_replan_cycles = read_config("sprint.max_replan_cycles", default=2)
Downstream inits (Drift Detection, Phase B Step 2) are idempotent (?? 0) and must not reset this value.
Non-epic routing: After validation, check the ticket type and route accordingly:
| Ticket type | Route |
|---|
epic | Continue to Drift Detection → Preplanning Gate (standard flow) |
bug | Dispatch /dso:fix-bug as sub-skill — see Bug Routing below |
story or task | Run complexity evaluation then optional /dso:implementation-plan — see Non-Epic Routing below |
Bug Routing (SC4)
When ticket type is bug:
- Log:
"Primary ticket <primary_ticket_id> is a bug — dispatching /dso:fix-bug."
- Invoke
/dso:fix-bug <primary_ticket_id> via Skill tool.
- Exit Phase A and proceed to Phase I (Session Close). Do not continue to the Preplanning Gate or Phase B.
Non-Epic Routing
When ticket type is story or task:
-
Log: "Primary ticket <primary_ticket_id> is a <type> — running complexity evaluation."
-
Dispatch the dso:complexity-evaluator agent (dso:complexity-evaluator is an agent file identifier, NOT a valid subagent_type value). Read agents/complexity-evaluator.md inline and use subagent_type: "general-purpose" with model: "haiku". Pass tier_schema=TRIVIAL to classify the ticket.
Fallback: If the agents/complexity-evaluator.md file is missing, log a warning and fall back to inline complexity assessment using the story description and acceptance criteria.
-
Route based on the complexity classification:
- TRIVIAL (high): Skip
/dso:implementation-plan. Before proceeding, run a file-count guard: estimate the number of files the task will touch by running enrich-file-impact.sh or by counting file paths mentioned in the ticket description. If the estimated file count exceeds 30, split the task into parallel sub-tasks by directory or alphabetical range (each sub-task ≤ 30 files), create child task tickets for each subset, and proceed to Phase C with the split tasks. If ≤ 30 files, proceed directly to Phase C (Batch Preparation) with the ticket as the sole task.
- TRIVIAL (medium) or MODERATE/COMPLEX (any): Invoke
/dso:implementation-plan <primary_ticket_id> via Skill tool. When the Skill tool returns, parse the STATUS line and proceed immediately to step 4 — do not pause or wait for user input.
-
After the Skill tool returns, route on STATUS and continue to Phase C:
STATUS:complete → proceed to Phase C
STATUS:blocked → surface blocked questions to user, then proceed to Phase C once answered
STATUS:bypass REASON:copy_story → the story is a copy story; skip implementation-plan decomposition and proceed directly to Phase C — Phase E "Copy Story Dispatch" routes it to dso:gov-copy-writer
REPLAN_ESCALATE: → route to d-replan-collect machinery
Non-epics skip the Preplanning Gate and proceed directly to Phase C.
Drift Detection Check
After validating the epic, check for codebase drift before proceeding to the Preplanning Gate.
Initialize the cascade counter (if not already set from a prior phase — drift-triggered REPLAN_ESCALATE feeds into the same machinery as Phase B):
replan_cycle_count = replan_cycle_count ?? 0
max_replan_cycles = read_config("sprint.max_replan_cycles", default=2)
Run the drift check:
DRIFT_RESULT=$(.claude/scripts/dso sprint/sprint-drift-check.sh <epic-id>)
If DRIFT_DETECTED:
- Parse the drifted file list from
DRIFT_RESULT (everything after DRIFT_DETECTED: ).
- Log:
"Codebase drift detected — files modified since task creation: <files>"
- Record a REPLAN_TRIGGER comment on the epic (see
docs/contracts/replan-observability.md for signal format): # shim-exempt: internal documentation reference
.claude/scripts/dso ticket comment <epic-id> "REPLAN_TRIGGER: drift — Files drifted: <files>. Re-invoking implementation-plan for affected stories."
- Identify which stories' tasks reference any of the drifted files (inspect each child task's
## File Impact or ## Files to Modify section).
- For each affected story, re-invoke
/dso:implementation-plan <story-id> via the Skill tool. When the Skill tool returns, parse the STATUS line immediately and continue to the next story — do not pause.
- On success (
STATUS:complete): continue to the next story.
- On
STATUS:blocked: surface the story as blocked for user input (same handling as Phase B blocked-stories list).
- On
STATUS:bypass REASON:copy_story: the story is a copy story; do NOT add tasks. The Phase E "Copy Story Dispatch" section handles dispatch via dso:gov-copy-writer. Continue to the next story.
- On
REPLAN_ESCALATE: brainstorm EXPLANATION:<text>: add the story and its explanation to the replan-stories list and route through the existing d-replan-collect cascade machinery (Phase B step d-replan-collect). The replan_cycle_count / max_replan_cycles initialized above are shared with Phase B — do not reinitialize them.
- After all re-invocations complete (and no REPLAN_ESCALATE is outstanding), record:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: implementation-plan — Drift re-planning complete for <N> stories."
- Proceed to Preplanning Gate.
Note: DRIFT_DETECTED and RELATES_TO_DRIFT are independent signals — both may appear in the same DRIFT_RESULT output. Process each block that matches, in order. They are NOT mutually exclusive branches.
If RELATES_TO_DRIFT lines are present in DRIFT_RESULT:
- Parse each
RELATES_TO_DRIFT: <epic-id> <summary> line from DRIFT_RESULT.
- Log:
"Relates_to drift detected — related epic <epic-id> closed after implementation plan: <summary>" for each line.
- Record a REPLAN_TRIGGER comment on the epic (see
docs/contracts/replan-observability.md for signal format): # shim-exempt: internal documentation reference
.claude/scripts/dso ticket comment <epic-id> "REPLAN_TRIGGER: drift — Relates_to epic <closed-epic-id> closed after implementation plan. <summary>. Re-invoking implementation-plan for affected stories."
- Identify which stories' tasks reference any of the drifted relates_to epics (inspect each child task's
## File Impact or ## Files to Modify section, or cross-reference the task's dependency/relates-to links).
- For each affected story, re-invoke
/dso:implementation-plan <story-id> via the Skill tool. When the Skill tool returns, parse the STATUS line immediately and continue to the next story — do not pause.
- On success (
STATUS:complete): continue to the next story.
- On
STATUS:blocked: surface the story as blocked for user input (same handling as Phase B blocked-stories list).
- On
STATUS:bypass REASON:copy_story: the story is a copy story; do NOT add tasks. The Phase E "Copy Story Dispatch" section handles dispatch via dso:gov-copy-writer. Continue to the next story.
- On
REPLAN_ESCALATE: brainstorm EXPLANATION:<text>: add the story and its explanation to the replan-stories list and route through the existing d-replan-collect cascade machinery (Phase B step d-replan-collect). The replan_cycle_count / max_replan_cycles initialized above are shared with Phase B — do not reinitialize them.
- After all re-invocations complete (and no REPLAN_ESCALATE is outstanding), record:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: implementation-plan — Relates_to drift re-planning complete for <N> stories."
- Proceed to Preplanning Gate.
If NO_DRIFT:
Log: "No codebase drift detected — proceeding to Preplanning Gate." Continue normally.
Clarity Gate
The Clarity Gate is a three-layer check that runs for epic-typed tickets only before entering the Preplanning Gate. It prevents sprint execution from starting when the ticket intent is unclear.
CHECKPOINT: clarity-gate-start — record this before running the gate.
Layer 1: Structural Clarity Check
Run the ticket clarity check script:
.claude/scripts/dso ticket-clarity-check.sh <primary_ticket_id>
Parse the result:
- Exit 0 (CLEAR): ticket passes structural check; proceed to Layer 2.
- Exit 1 (UNCLEAR): log the reason; proceed to User Escalation (Layer 3).
- Exit 2 (ERROR/ABSENT): script is missing or encountered an error; emit a warning (
"ticket-clarity-check.sh unavailable — falling through to Layer 2"); proceed to Layer 2 (fail-open).
Layer 2: Scope Certainty Assessment
Dispatch the dso:complexity-evaluator agent (dso:complexity-evaluator is an agent file identifier, NOT a valid subagent_type value — the Agent tool only accepts built-in types). Read agents/complexity-evaluator.md inline and use subagent_type: "general-purpose" with model: "haiku". Pass the primary ticket context to evaluate scope_certainty:
subagent_type: "general-purpose"
model: haiku
prompt: |
{verbatim content of agents/complexity-evaluator.md}
ticket_id: <primary_ticket_id>
tier_schema: SIMPLE
Parse scope_certainty from the evaluator's JSON output:
High or Medium: proceed to Preplanning Gate.
Low: proceed to User Escalation (Layer 3).
- Unrecognized value: treat as
Low — proceed to User Escalation (Layer 3).
- Agent unavailability (timeout, dispatch failure, API key absent): log
"WARNING: complexity-evaluator unavailable — falling through to Layer 3." and proceed to User Escalation (Layer 3).
Layer 3: User Escalation (AskUserQuestion)
When either Layer 1 or Layer 2 signals low clarity, present options via AskUserQuestion:
"The primary ticket <primary_ticket_id> has low clarity. How would you like to proceed?
(a) Run /dso:fix-bug if this is actually a defect
(b) Run /dso:brainstorm to enrich the ticket before executing
(c) Proceed anyway with the current ticket as-is"
Wait for user response and route accordingly:
- (a) fix-bug: dispatch
/dso:fix-bug <primary_ticket_id>, then exit to Phase I.
- (b) brainstorm: invoke
/dso:brainstorm <primary_ticket_id> via Skill tool, then re-enter Preplanning Gate.
- (c) proceed: log
"User elected to proceed with low-clarity ticket.", continue to Preplanning Gate.
Mode Banner
SPRINT_MODE is resolved during the Config Resolution block at the top of Phase A. This subsection only emits the banner.
Emit exactly one banner based on the result:
| SPRINT_MODE | Banner |
|---|
ci-pr | MODE: ci-pr |
local | MODE: local — per-story PR mechanisms inactive |
When SPRINT_MODE=local: all ci-pr-only mechanisms (per-story PR creation, trailer enforcement, story-level review gates, merge orchestration, cross-story diff analysis) are inactive for this sprint run. Do not attempt to create PRs or enforce story-level trailers.
When SPRINT_MODE=ci-pr: per-story PR mechanisms are active. Proceed with normal sprint flow.
Context Efficiency Rules
Status checks: Use .claude/scripts/dso issue-summary.sh <id> or .claude/scripts/dso ticket list --parent=<epic-id> (scope to the epic under sprint) for orchestrator status checks (is it done? what's blocking?). Reserve full .claude/scripts/dso ticket show <id> only when sub-agents need to read their complete task context.
Ticket-as-prompt: Before dispatch, run the quality gate:
.claude/scripts/dso issue-quality-check.sh <id>
- Exit 0 (quality pass): Use the ticket-as-prompt template (
task-execution.md) — sub-agent reads its own context
- Exit 1 (too sparse): Fall back to inline prompt — orchestrator runs
.claude/scripts/dso ticket show <id> and includes output in the Task prompt
Writing quality ticket: When creating tasks for sub-agent execution, include:
- Concrete file paths (
src/, tests/)
- Acceptance criteria with keywords: "must", "should", "Given/When/Then"
- A
## File Impact or ### Files to modify section listing source and test files
- At least 5 lines of description
File impact enrichment: If a ticket is missing a file impact section, run .claude/scripts/dso enrich-file-impact.sh <id> to auto-generate it. Use --dry-run to preview. Gracefully degrades if ANTHROPIC_API_KEY is unset.
Preplanning Gate
Step 1: Check for Existing Children (/dso:sprint)
OPEN=$(.claude/scripts/dso ticket list --parent=<epic-id> --status=open,in_progress 2>/dev/null | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))' || echo 0)
ALL=$(.claude/scripts/dso ticket list --parent=<epic-id> --include-archived --exclude-deleted 2>/dev/null | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))' || echo 0)
OPEN > 0: → Step 2 (Existing Children Readiness Check)
OPEN == 0 and ALL > 0: All children closed. Log "Preplanning Gate: all children closed — routing to Phase G." Skip to Phase G. Do NOT route to Step 7.
ALL == 0: → Step 7 (Epic Complexity Evaluation)
Step 2: Existing Children Readiness Check (/dso:sprint)
Trigger /dso:preplanning (full mode) if ANY of the following are true:
| Condition | How to Detect |
|---|
| Ambiguous tasks | Any child task description lacks concrete success criteria (no Gherkin-style Given/When/Then, no bullet-list acceptance criteria, and no specific file paths or measurable outcomes) |
| Vague epic description | Epic description is fewer than 3 sentences AND has no success criteria section |
| All children are epics/features | Children are high-level containers, not implementable tasks |
Ambiguity heuristic: A task is considered ambiguous if its description:
- Contains no testable acceptance criteria (no
Given/When/Then, no "should", no "must", no bullet list of outcomes)
- AND references no specific files, functions, or endpoints
- AND is shorter than 2 sentences
If more than half of the children are ambiguous, trigger preplanning for the entire epic.
If any trigger condition is met:
- Log:
"Epic has ambiguous tasks — running /dso:preplanning to decompose before execution."
- Invoke
/dso:preplanning <epic-id> (full mode)
- After preplanning completes, continue to Phase B
If no trigger condition is met, proceed to Step 3 (SC Coverage Haiku Gate).
Step 3: SC Coverage Haiku Gate (/dso:sprint)
Purpose: Fast-path check that all epic success criteria (SCs) are traceable to at least one child story or task. Uses a haiku sub-agent for speed. This is a read-only advisory gate — it never blocks execution. Sonnet/opus escalation for ESCALATE verdicts is handled by Step 4.
ORCHESTRATOR_RESUME idempotency: If your resume context contains SC_COVERAGE_HAIKU_GATE: complete for this epic, skip this entire sub-step and proceed to Step 4 if any ESCALATE verdicts were recorded, otherwise proceed to Phase B.
Step 1 — Collect inputs:
- Retrieve the epic's success criteria list from the epic ticket description.
- Retrieve child descriptions (already fetched by Step 1 and Step 2 above).
- If the epic has 0 SCs (empty success criteria list): log
"0-SC epic: skipping SC coverage haiku gate — no SCs to validate" and proceed directly to Phase B. Do not dispatch the haiku sub-agent.
Step 2 — Dispatch haiku sub-agent:
Dispatch a subagent_type: general-purpose sub-agent with model: haiku. Load the prompt from skills/sprint/prompts/sc-coverage-haiku.md. Provide:
epic_sc_list: an array of { "sc_id": "<id>", "sc_text": "<text>" } objects. Assign sequential IDs (e.g. sc-1, sc-2) from the epic's SC list in order.
children: an array of { "child_id": "<id>", "child_title": "<title>", "child_description": "<description>" } for each child ticket
Step 3 — Parse output:
The haiku sub-agent returns a JSON object with this structure:
{
"results": [
{
"sc_id": "<matches input sc_id>",
"verdict": "COVERED" | "ESCALATE",
"covering_child_id": "<child_id or null>",
"citation_reason": "<explanation or null>"
}
]
}
Parse the results array. Check verdict on each entry. On any missing key, null results, or invalid JSON: trigger the fail-open path below.
On parse failure (malformed JSON, missing fields, timeout, or empty output): this gate is fail-open — log a warning "SC coverage haiku gate: parse failure — skipping gate, proceeding to Phase B" and proceed directly to Phase B. Do not block execution.
Step 4 — Emit idempotency marker and route on verdicts:
Emit SC_COVERAGE_HAIKU_GATE: complete to your output so that ORCHESTRATOR_RESUME can detect it on resume.
- ALL verdicts are
COVERED: log "SC coverage haiku gate: all SCs covered — proceeding to Phase B" and proceed to Phase B normally. Skip Step 4.
- ANY verdict is
ESCALATE: collect the ESCALATE SCs into an escalation list and proceed to Step 4 (SC Coverage Sonnet Tier).
Step 4: SC Coverage Sonnet Tier (/dso:sprint)
Trigger: Only runs if the haiku gate (Step 3) returned ANY ESCALATE verdict. If haiku marked ALL SCs as COVERED (empty escalation list), skip this sub-step entirely and proceed to Phase B.
Purpose: Deeper evaluation of SCs that haiku could not conclusively mark as COVERED. Sonnet evaluates each escalated SC independently, with no knowledge of haiku's verdicts.
ORCHESTRATOR_RESUME idempotency: If your resume context contains SC_COVERAGE_SONNET_GATE: complete for this epic, skip this entire sub-step and:
- If any UNSURE verdicts were recorded → proceed to Step 5 (opus dispatch)
- If any MISSING verdicts were recorded (no UNSURE) → proceed to REPLAN_TRIGGER Routing (no opus dispatch)
- If all verdicts are COVERED → proceed to Phase B directly
Step 1 — Prepare input:
From the haiku escalation list, collect only the SCs marked ESCALATE. Build the input payload:
{
"sc_list": [
{ "sc_id": "sc-1", "sc_text": "<original SC text — no haiku verdicts, no escalation reasoning>" }
],
"children": [
{ "child_id": "<id>", "child_title": "<title>", "child_description": "<description>" }
]
}
Important input contract: Pass ONLY the original SC text and children descriptions to sonnet. Do NOT include haiku verdicts, escalation reasoning, or haiku output in the prompt. Sonnet must evaluate independently.
Step 2 — Dispatch sonnet sub-agent:
Dispatch a subagent_type: general-purpose sub-agent with model: sonnet. Load the prompt from skills/sprint/prompts/sc-coverage-sonnet.md. Pass the input payload constructed above.
Step 3 — Parse output:
The sonnet sub-agent returns a JSON object:
{
"results": [
{
"sc_id": "sc-1",
"verdict": "COVERED" | "MISSING" | "UNSURE",
"reasoning": "<explanation>"
}
]
}
Parse the results array. Check verdict on each entry.
On parse failure (malformed JSON, missing fields, timeout, or empty output): this gate is fail-open — log a warning "SC coverage sonnet gate: parse failure — treating all sonnet SCs as UNSURE, escalating to opus (Step 5)" and treat ALL sonnet-evaluated SCs as UNSURE. Proceed to Step 5.
Step 4 — Collect verdicts:
For each SC in the sonnet results:
COVERED: SC is sufficiently covered — remove from escalation tracking.
MISSING: SC has a real gap — add to the sc_coverage_missing list.
UNSURE: Sonnet could not determine coverage — collect into the sc_coverage_unsure list for opus escalation.
Step 5 — Emit idempotency marker and route on UNSURE list:
Emit SC_COVERAGE_SONNET_GATE: complete to your output so that ORCHESTRATOR_RESUME can detect it on resume.
- If the UNSURE list is empty: proceed directly to REPLAN_TRIGGER Routing below (no opus dispatch needed). Log:
"SC coverage sonnet gate: no UNSURE SCs — opus escalation skipped".
- If any SCs are UNSURE: proceed to Step 5 (SC Coverage Opus Tier) with the UNSURE SCs passed as input to opus.
Step 5: SC Coverage Opus Tier (/dso:sprint)
Trigger: Only dispatch opus if the UNSURE list from Step 4 is non-empty. If the UNSURE list is empty (all SCs resolved by haiku + sonnet), skip opus entirely — log "SC coverage opus tier: UNSURE list empty — skipping opus, proceeding to REPLAN_TRIGGER routing" and proceed directly to REPLAN_TRIGGER routing below.
Purpose: Opus is the final arbiter for SCs that sonnet could not conclusively classify as COVERED or MISSING. Opus returns only COVERED or MISSING — no UNSURE. This terminates the escalation cascade.
ORCHESTRATOR_RESUME idempotency: If your resume context contains SC_COVERAGE_OPUS_GATE: complete for this epic, skip this sub-step and proceed directly to REPLAN_TRIGGER routing.
Step 1 — Prepare input:
Build the opus input payload using only the SCs in sc_coverage_unsure (no MISSING SCs, no haiku/sonnet context):
{
"unsure_scs": [
{ "sc_id": "sc-1", "sc_text": "<original SC text only — no prior tier verdicts>" }
],
"children": [
{ "child_id": "<id>", "child_title": "<title>", "child_description": "<description>" }
]
}
Step 2 — Dispatch opus sub-agent:
Dispatch a subagent_type: general-purpose sub-agent with model: opus. Load the prompt from skills/sprint/prompts/sc-coverage-opus.md. Pass the input payload constructed above.
Step 3 — Parse output:
The opus sub-agent returns a JSON object:
{
"results": [
{
"sc_id": "sc-1",
"verdict": "COVERED" | "MISSING"
}
]
}
Parse the results array. Check verdict on each entry. Opus returns COVERED or MISSING only — no UNSURE.
On parse failure (malformed JSON, missing fields, timeout, or empty output): fail-open conservative — log a warning "SC coverage opus gate: parse failure — treating all unparseable SCs as MISSING (conservative fail-open)" and treat ALL opus-evaluated SCs as MISSING. Add them to the sc_coverage_missing list.
Step 4 — Collect opus verdicts:
For each SC in the opus results:
COVERED: SC is confirmed covered — remove from outstanding list.
MISSING: SC has a confirmed gap — add to the sc_coverage_missing list.
Step 5 — Emit idempotency marker:
Emit SC_COVERAGE_OPUS_GATE: complete to your output so that ORCHESTRATOR_RESUME can detect it on resume.
Step 6: REPLAN_TRIGGER Routing — SC Coverage Gaps (/dso:sprint)
After completing all applicable escalation tiers, evaluate the sc_coverage_missing list:
If ALL SCs are COVERED (empty sc_coverage_missing list):
- Log:
"SC coverage check complete: all SCs covered — proceeding to Phase B normally."
- Continue to Phase B.
If ANY SCs are MISSING (non-empty sc_coverage_missing list):
Prerequisite — Retrieve child ticket types: Ensure ticket_type is known for each child before routing. The children list was fetched earlier in the Preplanning Gate via ticket deps. If ticket_type was not preserved from that fetch, run:
.claude/scripts/dso ticket show <child_id>
for each child to retrieve the ticket_type field. This is required to determine the routing path: story children → /dso:preplanning; task-only children → /dso:implementation-plan.
-
Record a REPLAN_TRIGGER: sc_coverage comment on the epic listing the missing SCs:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_TRIGGER: sc_coverage — Missing SCs: <comma-separated list of missing sc_ids and sc_text>. Routing to decomposition skill to add coverage."
-
Based on the child ticket types (from the children already fetched above), invoke:
If at least one child has ticket_type: story (route to /dso:preplanning):
Invoke /dso:preplanning <epic-id> via Skill tool. When the Skill tool returns, proceed to Phase B.
If all children have ticket_type: task (route to /dso:implementation-plan):
Invoke /dso:implementation-plan <epic-id> via Skill tool. When the Skill tool returns, proceed to Phase B.
Otherwise (children are all epics or have unexpected ticket types): log a warning "SC coverage REPLAN_TRIGGER: unexpected child types — no story or task children found; proceeding to Phase B without decomposition routing" and proceed to Phase B.
-
Continue to Phase B.
Step 7: Epic Complexity Evaluation (/dso:sprint)
When the epic has zero children, dispatch the dso:complexity-evaluator agent to classify the epic's complexity before deciding the decomposition path. (dso:complexity-evaluator is an agent file identifier, NOT a valid subagent_type value — the Agent tool only accepts built-in types.)
Dispatch the evaluator:
Read agents/complexity-evaluator.md inline and use subagent_type: "general-purpose" with model: "haiku". Pass the epic ID as the task argument. Pass tier_schema=SIMPLE as a field in the task context so the agent outputs SIMPLE/MODERATE/COMPLEX tier vocabulary.
Fallback: If agents/complexity-evaluator.md is missing, fall back to subagent_type: "general-purpose" and load the shared rubric prompt from $PLUGIN_ROOT/skills/sprint/prompts/ (see epic-complexity-evaluator prompt file in that directory).
Route based on classification:
| Classification | Confidence | Route |
|---|
| SIMPLE | high | Step 8 (Direct Implementation Planning) |
| SIMPLE | medium | Treat as MODERATE |
| MODERATE | high | Step 9 (Lightweight Preplanning) |
| MODERATE | medium | Treat as COMPLEX |
| COMPLEX | any | Step 10 (Full Preplanning) |
Log the classification: "Epic <id> classified as <CLASSIFICATION> (confidence: <confidence>) — routing to <path>."
Step 8: Direct Implementation Planning (SIMPLE epics) (/dso:sprint)
-
Log: "Epic <id> classified as SIMPLE — running /dso:implementation-plan directly on epic."
-
Invoke /dso:implementation-plan via Skill tool with the epic ID as the argument:
Skill("dso:implementation-plan", args="<epic-id>")
The skill handles epic type detection and runs inline (no sub-agent dispatch needed). When the Skill tool returns, immediately proceed to step 3.
-
Parse the skill's output using the same STATUS protocol as Phase B's Implementation Planning Gate
-
Set epic_routing = "SIMPLE" — this flag tells Phase B to skip the Implementation Planning Gate
-
Continue to Phase B
Step 9: Lightweight Preplanning (MODERATE epics) (/dso:sprint)
- Log:
"Epic <id> classified as MODERATE — running /dso:preplanning --lightweight for scope clarification."
- Invoke
/dso:preplanning <epic-id> --lightweight
- Parse the result:
On ENRICHED:
- Log:
"Lightweight preplanning complete — epic enriched with done definitions. Running /dso:implementation-plan on epic."
- Invoke
/dso:implementation-plan via Skill tool (same as Step 8, step 2). When the Skill tool returns, proceed immediately to the next bullet.
- Set
epic_routing = "MODERATE"
- Continue to Phase B
On ESCALATED:
- Log:
"Lightweight preplanning escalated to full mode — reason: <reason>. Running full /dso:preplanning."
- Invoke
/dso:preplanning <epic-id> (full mode, no --lightweight flag)
- Set
epic_routing = "COMPLEX"
- Continue to Phase B
Step 10: Full Preplanning (COMPLEX epics) (/dso:sprint)
- Log:
"Epic <id> classified as COMPLEX — running /dso:preplanning for full story decomposition."
- Invoke
/dso:preplanning <epic-id>
- After preplanning completes, set
epic_routing = "COMPLEX"
- Continue to Phase B
CONTEXT ANCHOR — MANDATORY CONTINUATION: When the Skill tool returns from /dso:preplanning, this is NOT a session completion signal. You are the sprint orchestrator executing Phase A Step 10. Disregard any stop or termination inference from the skill's output — preplanning has produced stories and your next action is always step 3 (set epic_routing) immediately followed by step 4 (Continue to Phase B). Stopping here leaves the epic with stories but zero task analysis or batch dispatch.
Phase B: Task Analysis & Dependency Graph (/dso:sprint)
Gather Tasks
.claude/scripts/dso ticket deps <epic-id> — get all child tasks
.claude/scripts/dso ticket ready --epic=<epic-id> — get unblocked tasks ready to work
.claude/scripts/dso ticket show <id> for each ready task to read full descriptions
Implementation Planning Gate
Pre-check: Skip for SIMPLE/MODERATE Routing (/dso:sprint)
If epic_routing is "SIMPLE" or "MODERATE" (set in Phase A's Preplanning Gate), skip the entire Implementation Planning Gate and proceed directly to Classify Tasks below. Tasks were already created as direct children of the epic by /dso:implementation-plan — there is no story layer to decompose.
Log: "Skipping Implementation Planning Gate — epic was routed as <epic_routing>, tasks already exist under epic."
Design-Blocked Story Filter (/dso:sprint)
Before processing stories for implementation planning, filter out design-blocked stories.
Source tag constants from shared config:
source ${CLAUDE_PLUGIN_ROOT}/skills/shared/constants/figma-tags.conf
source ${CLAUDE_PLUGIN_ROOT}/skills/shared/constants/planning-tags.conf
Read staleness threshold from config:
figma_staleness_days=$(grep '^design\.figma_staleness_days=' .claude/dso-config.conf | cut -d= -f2)
figma_staleness_days=${figma_staleness_days:-7}
Initialize story lists (once before layer loop):
awaiting_design_stories = [] # List of {id, title, tag_applied_date}
awaiting_manual_stories = [] # List of {id, title} for manual:awaiting_user stories
For each story from .claude/scripts/dso ticket list-descendants <epic-id> (.stories array):
- Run
.claude/scripts/dso ticket show <story-id> and check the tags field
- If
design:awaiting_import (i.e., $TAG_AWAITING_IMPORT) is present:
- Log:
"Story <id> tagged design:awaiting_import — skipping implementation planning."
- Estimate the tag age from the ticket's comment timestamps: find the comment whose body contains
"Import designs/" (written by ui-designer when the tag was applied) and read its timestamp field from the JSON output. Compute days elapsed: $(( ($(date +%s) - comment_timestamp_epoch) / 86400 )). If no such comment exists, treat tag age as unknown (no staleness warning).
- Add the story to the
awaiting_design_stories list: {id: "<story-id>", title: "<story-title>", tag_applied_date: "<date or unknown>"}
- Do not add this story to the needs-planning list. Skip all further processing for this story (no complexity eval, no implementation-plan dispatch, no batch dispatch in Phase E).
- Only stories without the
design:awaiting_import tag proceed to the manual:awaiting_user check below.
Manual-awaiting-user check (runs when planning.external_dependency_block_enabled=true):
Read the flag:
_manual_flag=$(grep '^planning.external_dependency_block_enabled=' .claude/dso-config.conf 2>/dev/null | cut -d= -f2)
For each story that passed the design filter:
- If
_manual_flag is true: check the tags field for $TAG_MANUAL_AWAITING_USER (manual:awaiting_user)
- If present:
- Log:
"Story <id> tagged manual:awaiting_user — deferring to manual-pause handshake."
- Add to
awaiting_manual_stories list: {id: "<story-id>", title: "<story-title>"}
- Do not add this story to the needs-planning list. Skip complexity eval and implementation-plan dispatch.
- Only stories without
manual:awaiting_user (or with _manual_flag != true) proceed to Step 1 below.
Topological sort for manual stories:
After populating awaiting_manual_stories, sort them so manual stories appear before their transitive autonomous dependents:
- Build a dependency graph from
.claude/scripts/dso ticket deps for each manual story
- Sort: if manual story M1 is a dependency of M2, M1 appears first
- Cycle detection: if M1 and M2 are both
manual:awaiting_user and M1 blocks M2 AND M2 blocks M1, log "CYCLE_DETECTED: manual stories <M1-id> and <M2-id> have mutual dependency" and escalate to user — do not continue Phase D.
Step 1: Identify Stories Needing Implementation Planning (/dso:sprint)
For each ready task from .claude/scripts/dso ticket ready --epic=<epic-id>:
- Run
.claude/scripts/dso ticket deps <task-id> to check if the story already has child implementation tasks
- If it has children → skip (already planned)
- If it has zero children → run the complexity evaluator:
Dispatch a haiku complexity-evaluator sub-agent to classify the story. Read agents/complexity-evaluator.md inline and use subagent_type: "general-purpose" with model: "haiku". (dso:complexity-evaluator is an agent file identifier, NOT a valid subagent_type value — the Agent tool only accepts built-in types.) Pass the story ID as the task argument. Pass tier_schema=TRIVIAL as a field in the task context so the agent outputs TRIVIAL/MODERATE/COMPLEX tier vocabulary.
Fallback: If agents/complexity-evaluator.md is missing, fall back to subagent_type: "general-purpose" and load the shared rubric prompt from $PLUGIN_ROOT/skills/sprint/prompts/ (see complexity-evaluator prompt file in that directory).
Routing based on classification:
| Classification | Confidence | Action |
|---|
| TRIVIAL | high | Skip /dso:implementation-plan. File-count guard: estimate the file count from the story description or enrich-file-impact.sh. If > 30 files, split into child tasks (≤ 30 files each) by directory or alphabetical range before proceeding. Log: "Story <id> classified as TRIVIAL — skipping /dso:implementation-plan" |
| TRIVIAL | medium | Treat as COMPLEX (medium confidence = plan) |
| MODERATE | any | Run /dso:implementation-plan via Skill tool (see Step 2) |
| COMPLEX | any | Run /dso:implementation-plan via Skill tool (see Step 2) |
HARD PROHIBITION — never create tasks directly for stories without tasks. When a story has zero children tasks, the orchestrator MUST follow this routing table. Creating tasks directly using ticket create task is ALWAYS prohibited. The planning gate is satisfied only when STATUS=READY is received from the formal planning pipeline (complexity evaluator → /dso:implementation-plan Skill invocation → STATUS parse). Bypassing the pipeline skips: proposal generation, distinctness gate, approach-decision-maker, and plan review.
Post-routing action for COMPLEX stories: After routing a story to /dso:implementation-plan, tag it so Phase E can upgrade implementation task models:
.claude/scripts/dso ticket comment <story-id> "COMPLEXITY_CLASSIFICATION: COMPLEX"
Dependency Layer Stratification (/dso:sprint)
Before invoking /dso:implementation-plan for any stories, group the stories that need decomposition into topological layers based on their intra-sprint dependencies.
Step A: Collect intra-sprint dependency edges
For each story in the needs-planning list:
- Run
.claude/scripts/dso ticket show <story-id> and read the DEPENDS ON field
- For each dependency listed, check whether it is also in the needs-planning list
- Record the edge only if both the story and its blocker are in the needs-planning list (ignore cross-sprint or already-completed dependencies)
Step B: Assign layers
Assign each story to a layer:
- Layer 0: stories with no intra-sprint blockers
- Layer N: stories whose all blockers are in Layers 0 through N-1
If a cycle is detected, log a warning and treat both as Layer 0.
Step C: Output layer assignment
Log the layer assignment: "Dependency layers: Layer 0: <ids>, Layer 1: <ids>, ...". Proceed to Step 2 using this layer ordering.
Step D: Post-planning file-overlap promotion
After /dso:implementation-plan completes for all stories in a layer, before beginning the next layer, check for file-level overlap between stories in the same layer:
-
Collect file sets per story: For each story in the layer, run .claude/scripts/dso ticket deps <story-id> to list its tasks, then for each task run .claude/scripts/dso ticket show <task-id> and extract every file path listed under ## Files to Modify or ## File Impact. Build a dict story_files[<story-id>] = set(<file-paths>).
-
Detect pairwise overlaps: For each pair of stories (A, B) in the same layer where A has higher ticket priority (lower numeric priority value) than B:
- Compute
overlap = story_files[A] ∩ story_files[B]
- If
|overlap| > 0, log: "FILE_OVERLAP: stories <A> and <B> share <N> files — promoting <B> to next layer. Shared: <first 5 overlap paths>..."
- Add a dependency:
.claude/scripts/dso ticket link <B-id> <A-id> depends_on
- Reassign story B to
current_layer + 1 in the layer map
-
Re-log updated assignment: After applying all promotions, log: "Dependency layers after overlap check: Layer 0: <ids>, Layer 1: <ids>, ..." so the audit trail reflects the final assignment.
-
When priority is equal: If A and B have equal numeric priority, prefer keeping the story that appears first in the layer list (by creation order) and promoting the other.
-
Skip condition: If a layer contains only one story, skip the overlap check for that layer (no pairs to compare). Log: "FILE_OVERLAP check: Layer <N> has only 1 story — skipping.".
This step fires per layer, after implementation-plan returns for the whole layer and before moving to the next. It is not applied retroactively to already-executed layers.
Step 2: Run Implementation Planning (/dso:sprint)
Process stories in layer order — Layer 0 first, then Layer 1, etc. Within each layer, invoke /dso:implementation-plan sequentially via Skill tool for each story that needs decomposition. Wait for all stories in the layer to complete before processing the next layer.
Epic-level cascade counter (idempotent init — do NOT clobber a value set earlier in Phase A):
replan_cycle_count = replan_cycle_count ?? 0
max_replan_cycles = read_config("sprint.max_replan_cycles", default=2)
Use ?? 0 (not = 0): the counter may already be initialized — and incremented — by the Phase A entry init (before the epic/non-epic branch) and the Drift Detection cascade. Unconditionally resetting to 0 here would discard drift-consumed cycles and weaken the cascade cap. This counter is shared across all stories in the epic. Each full brainstorm → preplanning → implementation-plan cascade iteration (regardless of which story triggered it) increments the counter by 1. This prevents unbounded loops when multiple stories each emit REPLAN_ESCALATE across cascade iterations.
Per-story UNCERTAIN counter (initialize once before the layer loop):
story_uncertain_counts = {}
This dictionary tracks the number of STATUS:pass + UNCERTAIN signals received per story across all batch iterations. Keys are parent story IDs (not task IDs). The counter persists across the Phase F → Phase C batch loop — do NOT re-initialize between batches. See Phase F Step 4 for parsing logic and Phase C Step 4 for double-failure detection.
Out-of-scope review findings accumulator (initialize once before the layer loop):
batch_out_of_scope_findings = []
This list collects out-of-scope files detected by sprint-review-scope-check.sh. In worktree-isolation mode (the default) the check runs per-worktree inside per-worktree-review-commit.md (post-review) and appends here; in shared-directory mode it is populated by Phase F Step 14. Each entry is a dict {"task_id": "<id>", "story_id": "<parent>", "files": ["file1", ...]}. The list is consumed between batches in Step 20 and cleared after processing. Do NOT process these findings mid-batch — they are only routed between batches to avoid task injection conflicts.
For each layer (in order Layer 0, Layer 1, ...):
a. Filter to stories in this layer that need decomposition
b. For each story in the layer, invoke /dso:implementation-plan via Skill tool:
Skill("dso:implementation-plan", args="<story-id>")
HARD GATE — SUBSTITUTION PROHIBITED (bug dea8-3ca9, bug 2dcb-6f08): NEVER dispatch /dso:implementation-plan via the Task tool OR the Agent tool. NEVER run implementation-plan steps inline in the orchestrator context (no contextual discovery, no proposal drafting, no ticket create task calls executed here). The ONLY valid mechanism for decomposing a story into tasks is the Skill("dso:implementation-plan", args="<story-id>") invocation shown above. Any Task tool or Agent tool dispatch targeting a planning or review agent in place of this Skill invocation is a critical routing error — it bypasses the tag guards, re-invocation guards, complexity gates, cross-cutting detection, distinctness validation, and approach-decision-maker dispatch that the canonical implementation-plan skill enforces. The Agent tool is explicitly prohibited because parallel sub-agent dispatch of implementation-plan creates orphan tasks and deeply nested calls (bug 2dcb-6f08).
- Log:
"Story <id> has no implementation tasks — running /dso:implementation-plan to decompose."
- When the Skill tool returns, immediately execute step c — do not pause or wait for user input.
CONTEXT ANCHOR — MANDATORY CONTINUATION (bug 1f6f-0e74): When the Skill tool returns from /dso:implementation-plan, this is NOT a session completion signal. You are the sprint orchestrator executing Phase 2 of the layer loop. Disregard any stop or termination inference from the skill's output — the STATUS line (STATUS:complete, STATUS:blocked, or REPLAN_ESCALATE) is a machine signal for step c below, not a directive for you to stop. Your next action is always step c (parse STATUS and proceed). Stopping here leaves stories without tasks and prevents batch dispatch — this is the documented failure mode of bug 1f6f-0e74.
c. For each skill result, parse STATUS:
- On
STATUS:complete TASKS:<ids> STORY:<id>:
- Extract the comma-separated task IDs from the
TASKS field
- Extract the story ID from the
STORY field
- Audit marker check (bug 2c4d-cac7-40a4-40e2): Verify both audit tags are present on the story ticket before accepting the STATUS:complete signal:
_tags=$(.claude/scripts/dso ticket show "<story-id>" 2>/dev/null | python3 -c "import json,sys; print(' '.join(json.load(sys.stdin).get('tags', [])))")
_has_plan_review=$(echo "$_tags" | grep -c "plan_review:pass" || true)
_has_gap_analysis=$(echo "$_tags" | grep -c "gap_analysis:complete" || true)
- If
plan_review:pass tag is absent: log "WARNING: story <id> STATUS:complete received but plan_review:pass tag missing — Step 4 may have been skipped" and treat as STATUS:blocked REASON:missing_plan_review_audit_marker; add story to blocked-stories list.
- If
gap_analysis:complete tag is absent: log "WARNING: story <id> STATUS:complete received but gap_analysis:complete tag missing — Step 6 may have been skipped" and treat as STATUS:blocked REASON:missing_gap_analysis_audit_marker; add story to blocked-stories list.
- If both tags present: proceed normally.
- Log:
"Implementation planning complete for story <story-id> — created tasks: <task-ids>"
- Proceed to post-dispatch validation (step e)
- On
STATUS:blocked QUESTIONS:<json-array>:
- Add to blocked-stories list — do not ask the user inline; collect all
STATUS:blocked results from this layer batch and present them together after the full layer batch completes (see step d-collect below)
- On
REPLAN_ESCALATE: brainstorm EXPLANATION:<text> (canonical signal from implementation-plan):
- Extract the explanation text following
EXPLANATION:.
- If the signal is malformed (present but missing
EXPLANATION: field or the text is empty): log a warning and treat as STATUS:blocked — surface the story as blocked for user input. Do not enter the cascade.
- Otherwise: add the story and its explanation to the replan-stories list — do not present to the user inline. Collect all
REPLAN_ESCALATE results from this layer batch and handle them together after the full layer batch completes (see step d-replan-collect below).
- Fallback — if no STATUS line in skill output:
- Run
.claude/scripts/dso ticket deps <story-id> to check whether tasks were created
- If children exist → treat as success; log a warning:
"WARNING: skill returned no STATUS line for story <id>, but .claude/scripts/dso ticket deps shows tasks — continuing"; proceed to post-dispatch validation
- If no children → retry the skill invocation once (same parameters)
- If retry also produces no children → revert story to open (
.claude/scripts/dso ticket transition <story-id> open); log: "ERROR: /dso:implementation-plan failed for story <id> after retry — story reverted to open"; skip to next story
d-collect. Collect and present blocked-layer stories — after the full layer batch completes, for each story with STATUS:blocked:
- Parsing STATUS:blocked: When
/dso:implementation-plan returns STATUS:blocked QUESTIONS:[...], parse the JSON array and present each question in human-readable format:
- Separate questions by kind: "blocking" (must be answered before proceeding) vs "defaultable" (have a default, can be skipped)
- Number each question
- Present blocking questions first, then defaultable questions with their defaults shown
Do NOT display the raw
STATUS:blocked line to the user.
- Important: Do NOT display the raw
STATUS:blocked QUESTIONS:<json> line to the user. This is an internal machine signal. Capture it silently, parse the JSON, then present only the formatted question list (see below) to the user.
- Parse the QUESTIONS field: Extract the JSON array from the
STATUS:blocked line. If parsing fails (malformed JSON) or the array is empty ([]), treat as a sub-agent failure:
- Revert the story to open:
.claude/scripts/dso ticket transition <story-id> open
- Log:
"ERROR: /dso:implementation-plan returned STATUS:blocked with no parseable questions for story <story-id> — story reverted to open"
- Remove story from blocked-stories list
- Present all remaining blocked stories' questions to the user at once — separate by
kind field:
/dso:implementation-plan needs clarification for story <story-id>:
Blocking (cannot plan without answers):
1. <question text for kind="blocking">
...
Defaultable (will use stated assumption unless you say otherwise):
1. <question text for kind="defaultable" — already includes assumption>
...
Please answer the blocking questions. Confirm or override any defaultable assumptions you want to change.
If all questions are one kind, omit the empty section header.
- Collect user responses: Wait for the user to reply. Accept free-text response.
- Persist answers to story description:
.claude/scripts/dso ticket comment <story-id> "## Clarifications (from sprint orchestrator)
Q1: <question 1 text>
A1: <user answer 1>
Q2: <question 2 text>
A2: <user answer 2>"
- Re-invoke the skill: Call the Skill tool again with the same story ID.
- If the re-invoked skill returns
STATUS:blocked again: Do not ask the user a second time. Treat as failure: revert story to open (.claude/scripts/dso ticket transition <story-id> open), log "ERROR: /dso:implementation-plan returned STATUS:blocked twice for story <story-id> — story reverted to open", and skip to the next story.
d-replan-collect. Collect and handle all REPLAN_ESCALATE stories — after the full layer batch completes, if any stories are in the replan-stories list:
- Non-interactive mode check (before all other steps): If the session is non-interactive (interactivity mode declared at session start as non-interactive), do NOT block for user input. For each story in the replan-stories list, record:
.claude/scripts/dso ticket comment <epic-id> "INTERACTIVITY_DEFERRED: brainstorm — implementation-plan emitted REPLAN_ESCALATE for story <story-id>: <explanation>. Re-run sprint interactively to address."
Skip the brainstorm cascade entirely. Do NOT write REPLAN_RESOLVED. Continue with any remaining work (the affected stories remain in their current state, pending a follow-up interactive session). See docs/contracts/replan-observability.md for the INTERACTIVITY_DEFERRED signal format. # shim-exempt: internal documentation reference
- Check cycle cap first (before presenting anything to the user):
- If
replan_cycle_count >= max_replan_cycles: Present the cap-exhausted user prompt from prompts/replan-user-prompt.md, substituting the story list and using {{proceed_label}} = "accept the current plan as-is and continue sprint execution". See skills/sprint/docs/cascade-replan-protocol.md §"When Max Cycles Are Hit". # shim-exempt: internal documentation reference
- If cap is not yet exhausted: Present the cap-not-exhausted user prompt from
prompts/replan-user-prompt.md, substituting the story list and using {{proceed_label}} = "accept the current state and continue sprint with these stories as-is".
- If user selects (b) or (c): act accordingly — proceed or abort. Do not enter cascade.
- If user selects (a): Enter the cascade replan per
skills/sprint/docs/cascade-replan-protocol.md: # shim-exempt: internal documentation reference
- Invoke
/dso:brainstorm <epic-id> via Skill tool
- Invoke
/dso:preplanning <epic-id> via Skill tool
- Increment
replan_cycle_count += 1
- Re-run Step 2 (implementation planning) for all stories in the epic — re-enter the layer loop from the beginning
- If implementation-plan returns no
REPLAN_ESCALATE for any story: write the resolved signal, then cascade exits — proceed to step e normally (plan accepted):
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: brainstorm — Stories re-planned after brainstorm cascade."
- If implementation-plan still emits
REPLAN_ESCALATE: repeat from d-replan-collect (check cap first, then present to user)
e. Post-layer-batch ticket validation:
.claude/scripts/dso validate-issues.sh --quick --terse
Log any warnings but do not block on non-critical results
f. Re-run .claude/scripts/dso ticket ready --epic=<parent-id> to pick up newly created implementation tasks before processing the next layer
Step 3: Continue to Classification (/dso:sprint)
Proceed to task classification with the updated task list.
Classify Tasks
Classification is performed automatically by ticket next-batch in Phase C (Batch Preparation). Each TASK: line in its output already includes model, subagent, and class fields — no separate classification step is needed here. Proceed directly to building the dependency graph below.
Build Dependency Graph
Output a textual dependency graph showing:
- All child tasks with status
- Blocking relationships (arrows)
- Batch assignment for ready tasks
Exit Condition
If no ready tasks exist:
- Parse the
skipped_blocked_story / skipped_overlap / skipped_in_progress / skipped_needs_planning arrays from the most recent .claude/scripts/dso ticket next-batch <epic-id> --json output (already computed in Phase C). These arrays identify the epic-scoped tasks that were eligible but deferred and the reason for each.
- For transitive chains (A blocks B blocks C), run
.claude/scripts/dso ticket deps <id> on each surfaced blocked ticket to walk blockers one hop at a time. Do NOT re-read the full ticket list.
- Report which tasks are blocked and by what
- Exit with recommendation
Phase C: Batch Preparation (/dso:sprint)
Step 1: Pre-Batch Checks
Before launching each batch, run the shared pre-batch check script:
REPO_ROOT=$(git rev-parse --show-toplevel)
$PLUGIN_SCRIPTS/agent-batch-lifecycle.sh pre-check
$PLUGIN_SCRIPTS/agent-batch-lifecycle.sh pre-check --db
The script outputs structured key-value pairs:
MAX_AGENTS: unlimited | N | 0 — use as max_agents (see protocol below)
SESSION_USAGE: normal | high | critical
GIT_CLEAN: true | false — if false, commit previous batch first
DB_STATUS: running | stopped | skipped — if stopped, ask user to start DB
Clean the discovery directory:
$PLUGIN_SCRIPTS/agent-batch-lifecycle.sh cleanup-discoveries
Output: DISCOVERIES_CLEANED: <N>. Exit 0 always (best-effort).
Recipe Engine Pre-flight
After cleaning discoveries, validate engine availability for recipe-tagged tasks in the upcoming batch:
_TASK_FILE=$(mktemp /tmp/task-list.XXXXXX.json)
.claude/scripts/dso ticket next-batch <epic-id> --json > "$_TASK_FILE" 2>/dev/null || echo "[]" > "$_TASK_FILE"
RECIPE_REGISTRY_PATH="${CLAUDE_PLUGIN_ROOT}/recipes/recipe-registry.yaml" \
TASK_LIST_FILE="$_TASK_FILE" \
bash "$PLUGIN_SCRIPTS/sprint/check-recipe-engines.sh"
rm -f "$_TASK_FILE"
Parse output and act:
NO_RECIPE_TASKS: Log "No recipe tasks in batch — skipping engine pre-flight" and continue.
ENGINES_OK: Log "Engine pre-flight passed" and continue.
MISSING_ENGINE: <e> or OUTDATED_ENGINE: <e>: Surface a warning listing missing/outdated engines. Store the MISSING_ENGINES_LIST=<csv> value from output in session context for S5 fallback consumption.
Pre-flight does NOT block sprint execution — warn and continue regardless of engine availability.
MAX_AGENTS protocol (3-tier):
max_agents value | Behavior |
|---|
unlimited | Dispatch all ready tasks in a single batch with no artificial cap. Pass --limit=unlimited (or omit --limit) to ticket next-batch. |
N (positive integer) | Cap the batch at N sub-agents. Pass --limit=N to ticket next-batch. Log: "Session usage elevated, limiting to N sub-agent(s)." |
0 | Skip sub-agent dispatch entirely. Write a ticket comment with utilization percentages and estimated reset time, then proceed to Phase F Step 20 (Continuation Decision). Log: "MAX_AGENTS=0 — session at critical utilization, skipping dispatch." Comment format: .claude/scripts/dso ticket comment <epic-id> "BATCH_SKIPPED: MAX_AGENTS=0. Session utilization: <SESSION_USAGE>. Estimated reset: next session." |
All Task tool calls use run_in_background: true.
Step 2: Claim Tasks
For each task in the batch:
.claude/scripts/dso ticket transition <id> in_progress
Step 3: Update from Main
Pull the latest code from main before launching sub-agents:
git fetch origin main && git merge origin/main --no-edit
This syncs the worktree branch with the latest main. Ticket branch syncing happens automatically during merge-to-main.sh at end-of-sprint (not during mid-sprint sync).
Step 3.5: Two-Pass Migration Ordering
Before composing the batch, wire migration task pair ordering so that the verification/cleanup half of each pair is scheduled after the half it validates. This step reads the persisted MIGRATION_CLASS: marker (written by /dso:implementation-plan Step 1) from each descendant story's ticket comments — it does NOT recompute detection.
Scope: This step is epic-scoped. Enumerate all descendant story IDs for the current epic, then invoke the helper once per story that has migration-role-tagged tasks:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/sprint/apply-two-pass-ordering.sh" \
--story-id <story-id> \
--tasks "<task-id>:<role> <task-id>:<role>"
Idempotency: The helper checks for existing depends_on edges before writing. Repeated per-batch invocation is safe — no once-per-story guard is needed.
Three reserved pair types (wired by the helper):
| Migration class | Pair roles | Edge written |
|---|
sweep | automated-sweep / manual-verification | manual-verification depends_on automated-sweep |
db | forward-migration / rollback-verification | rollback-verification depends_on forward-migration |
| feature-flag (role presence only) | flag-cutover / flag-cleanup | flag-cleanup depends_on flag-cutover |
Note: the feature-flag pair is detected by role presence alone — it does not require a MIGRATION_CLASS: marker value.
Absent-marker fallback: When a story has no MIGRATION_CLASS: marker (or the marker value is inconclusive) and no feature-flag pair roles are present on its tasks, the helper emits NO_MARKER on stderr and writes no edges. Standard batch ordering from ticket next-batch applies unchanged. This is the expected path for stories with no migration task pairs; it is not an error.
Story enumeration: Obtain descendant story IDs via:
.claude/scripts/dso ticket list-descendants <epic-id>
Filter the output to story type tickets. For each story, collect tasks tagged migration-role:<role> and pass them to the helper. Stories with no migration-role-tagged tasks are skipped (the helper call is unnecessary for them, but safe if inadvertently invoked with no --tasks argument).
Step 4: Batch Composition
Inject Prior Batch Discoveries (Batch 2+ only)
For Batch 2+, collect discoveries for injection into sub-agent prompts via {prior_batch_discoveries} in task-execution.md:
PRIOR_BATCH_DISCOVERIES=$(.claude/scripts/dso collect-discoveries.sh --format=prompt 2>/dev/null) || PRIOR_BATCH_DISCOVERIES="None."
- For Batch 1 (no prior discoveries), set
PRIOR_BATCH_DISCOVERIES="None."
- For Batch 2+, replace
{prior_batch_discoveries} with the script output
- Graceful degradation: If
collect-discoveries.sh --format=prompt fails, log a warning
and use "None." as the fallback value. Discovery injection failure must not block the sprint.
Compose Batch
Run the deterministic batch selector:
.claude/scripts/dso ticket next-batch <epic-id>
.claude/scripts/dso ticket next-batch <epic-id> --limit=N
max_agents: Determined by Step 1's pre-batch check (3-tier: unlimited, N, or 0).
unlimited: Returns the full non-conflicting pool — dispatch all candidates.
N (positive integer): Caps batch at N tasks.
0: Skip dispatch entirely — do not call ticket next-batch. Write the utilization comment per Phase C Step 1 protocol and proceed to Phase F Step 20.
Output format
TASK: lines are tab-separated — no further .claude/scripts/dso ticket show or classify-task.sh calls required:
TASK: <id> P<priority> <issue-type> <model> <subagent-type> <class> <title> [story:<id>]
| Line prefix | Meaning |
|---|
EPIC: <id> <title> | Epic being planned |
AVAILABLE_POOL: N | Candidates before overlap/cap filtering |
BATCH_SIZE: N | Tasks selected for this batch |
TASK: ... (tab-separated) | id, P<priority>, type, model, subagent, class, title |
SKIPPED_OVERLAP: <id> ... | Deferred — file conflict with higher-priority task |
SKIPPED_OPUS_CAP: <id> ... | Deferred — opus cap (2) already reached |
SKIPPED_BLOCKED_STORY: <id> ... | Deferred — parent story has open blockers |
SKIPPED_IN_PROGRESS: <id> ... | Already claimed by another agent |
SKIPPED_DESIGN_AWAITING: <id> <title> | Deferred — story tagged design:awaiting_import (Figma designs not yet finalized) |
SKIPPED_MANUAL_AWAITING: <id> <title> | Deferred — story tagged manual:awaiting_user (manual user input required; only emitted when planning.external_dependency_block_enabled=true) |
Parsing SKIPPED_DESIGN_AWAITING lines: After running ticket next-batch, parse any SKIPPED_DESIGN_AWAITING lines from the output. For each such line, extract the story ID and title and add them to the awaiting_design_stories list (if not already present from Phase B filtering). These stories are surfaced in the Phase F Batch Completion Summary "Awaiting designer input" section.
manual:awaiting_user filter (when planning.external_dependency_block_enabled=true): Stories tagged manual:awaiting_user are excluded from the autonomous batch and surfaced as SKIPPED_MANUAL_AWAITING lines. After autonomous stories drain, sprint enters Phase D (Manual-Pause Handshake), which presents a blocking handshake listing per-story instructions and an optional verification_command. Accepts: done, done <story-id>, skip. Handles verification_command execution (timeout: planning.verification_command_timeout_seconds, default 30s) and confirmation-token audit logging. Topological sort surfaces manual stories before their transitive autonomous dependents. Schema: ${CLAUDE_PLUGIN_ROOT}/docs/contracts/external-dependencies-block.md.
Interaction Conflict Filter (/dso:sprint)
Before dispatching any task from the batch, filter out tasks whose parent epic is tagged interaction:deferred.
For each TASK: line returned by ticket next-batch:
- Identify the parent epic of the task (via
story:<id> field if present, or the <epic-id> directly for top-level tasks).
- Run
.claude/scripts/dso ticket show <epic-id> and check the tags field.
- If
interaction:deferred is present in the tags:
- Log:
"Epic <id> skipped — interaction:deferred tag present. Resolve cross-epic conflicts in /dso:brainstorm first."
- Remove this task from the dispatch batch. Do NOT mark the task
in_progress and do NOT dispatch a sub-agent for it.
- If
interaction:deferred is NOT present: include the task in the batch normally.
Failure contract: If ticket show fails for a given epic, treat the tag as absent and include the task (fail-open).
No error is thrown when tasks are filtered — sprint continues with the remaining batch. If all tasks are filtered and BATCH_SIZE drops to 0 after filtering, proceed to Phase F Step 20 (Continuation Decision) rather than treating it as a blocking error. Log: "All batch tasks filtered — interaction:deferred tag present on parent epic(s). Resolve cross-epic conflicts to proceed."
Use --json for machine-readable output with full detail including file lists.
What the script handles (no orchestrator action required)
- Story-level blocking: Blocked story → all child tasks deferred
- File overlap: Higher-priority task wins; lower defers to next cycle
- Classification: TASK lines include
model, subagent, class sorted by classify priority then ticket priority
- Opus cap: At most 2
model=opus tasks per batch; extras deferred
Exit condition
If BATCH_SIZE: 0, parse the skipped_* arrays from the sprint-next-batch.sh --json
output (already produced above) to surface the blocking chain. Walk transitive blockers
via .claude/scripts/dso ticket deps <id> on each surfaced blocked ticket. Report to
the user and exit.
Dependency-Aware Overlap Analysis (optional, when sg is available)
After running ticket next-batch, use ast-grep (sg) for structural dependency
analysis on batch candidates to surface cross-file import relationships that string
search would miss. This supplements — but does not replace — the script's built-in
file-overlap detection.
if command -v sg >/dev/null 2>&1; then
sg --pattern 'from $MODULE import $_' --lang python .
sg --pattern 'import $MODULE' --lang python .
sg --pattern 'source $PATH' --lang bash .
else
grep -rn "import $MODULE\|from $MODULE\|source.*$MODULE" --include='*.py' --include='*.sh' .
fi
Use the results to identify hidden dependencies between batch candidates. If two
candidates share a cross-file dependency not reflected in their file_list, add a
dependency link (.claude/scripts/dso ticket link <src> <tgt> depends_on) before
finalizing the batch to avoid parallel conflicts.
Double-Failure Detection (per story)
After composing the batch, check each task's parent story against the story_uncertain_counts map (initialized in Phase B Step 2) before dispatching:
-
For each TASK: line in the batch output, extract the parent story ID from the story:<id> field.
-
Look up story_uncertain_counts[<story-id>]. If the count is >= 2, do NOT dispatch the task. Instead:
a. Record the re-plan trigger on the epic before invoking implementation-plan (so the audit trail exists even if re-planning fails):
.claude/scripts/dso ticket comment <epic-id> "REPLAN_TRIGGER: failure — Story <story-id> had 2+ UNCERTAIN signals. Routing to implementation-plan."
b. Re-invoke /dso:implementation-plan <story-id> via the Skill tool to re-plan the story. When the Skill tool returns, proceed immediately to step c.
c. After re-planning completes, record resolution:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: implementation-plan — Story <story-id> re-planned after confidence failures."
d. Reset story_uncertain_counts[<story-id>] = 0 so the story does not immediately re-trigger on the next batch.
e. Remove the affected task(s) from the current batch and proceed with the remaining tasks. The re-planned story's new tasks will be picked up in the next batch cycle.
-
Tasks whose parent story has a count of 0 or 1 are dispatched normally.
Key invariant: Only STATUS:pass + UNCERTAIN signals (tracked in Phase F Step 4) count toward this threshold. STATUS:fail tasks are handled via revert-to-open in Phase F Step 16 and do not affect this counter.
Pre-Dispatch: Push Session Branch (worktree isolation fix)
Before dispatching any sub-agents, detect whether the orchestrator is running in a session worktree: