| 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 workflow-config.yaml)
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)
VISUAL_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.test_visual)
E2E_CMD=$(bash "$PLUGIN_SCRIPTS/read-config.sh" commands.test_e2e)
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
LINT_CMD — replaces make lint in validation steps
VISUAL_CMD — replaces make test-visual in post-batch checks
E2E_CMD — replaces make test-e2e in post-batch checks
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
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 → parallel dispatch (≤3/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
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
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
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
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
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.
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 2>/dev/null; .claude/scripts/dso ticket list --parent=<epic-id> --status=in_progress 2>/dev/null; } | grep -c '"ticket_id"' || echo 0)
ALL=$(.claude/scripts/dso ticket list --parent=<epic-id> --include-archived 2>/dev/null | grep -c '"ticket_id"' || 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
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 — regardless of how much codebase context the orchestrator has accumulated, how "detailed" the story description is, or how "obvious" the implementation appears. The formal planning pipeline (complexity evaluator → /dso:implementation-plan Skill invocation → STATUS parse) is not optional. Bypassing it skips: proposal generation, distinctness gate, approach-decision-maker, and plan review. These gates exist precisely because "I know what to do" is not the same as "the planning gate has been satisfied." (bug e903-873d)
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 (initialize once before the layer loop):
replan_cycle_count = 0
max_replan_cycles = read_config("sprint.max_replan_cycles", default=2)
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 during 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 13 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>")
- 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
- 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).
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 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.
Dry-Run Mode
If --dry-run was specified:
- Run
ticket next-batch <epic-id> (no --limit) to get the full pool
- For each story that needs implementation planning (Phase B gate), output one line per story:
Dispatching impl-plan sub-agent for story <story-id>: <story-title>
- Output the batch plan: task IDs, titles, model, subagent, class
- Stop — do not execute any sub-agents
Phase D: Manual-Pause Handshake (/dso:sprint)
This phase runs only when all of the following are true:
planning.external_dependency_block_enabled=true
awaiting_manual_stories list is non-empty
- All autonomous tasks in the current batch have completed (or there are no autonomous tasks in this batch)
Pause State Management
Before starting the handshake, manage the pause state file via sprint/sprint-pause-state.sh (the SIGURG recovery state manager):
.claude/scripts/dso sprint/sprint-pause-state.sh stale-cleanup
.claude/scripts/dso sprint/sprint-pause-state.sh is-fresh <epic-id>
- If
is-fresh exits 0 (fresh state exists): a prior SIGURG interrupted the handshake. Present the user with: "Found existing pause state for epic <epic-id>. Use --resume when re-invoking sprint to continue the handshake." Then call sprint/sprint-pause-state.sh resume-context <epic-id> to get the first unanswered story and rehydrate the handshake from that story forward.
- If
is-fresh exits non-zero (no fresh state): call sprint/sprint-pause-state.sh init <epic-id> to create a fresh state file.
.claude/scripts/dso sprint/sprint-pause-state.sh init <epic-id>
SIGURG trap: register _spause_sigurg_handler <epic-id> (by sourcing sprint/sprint-pause-state.sh) as the SIGURG handler. On interrupt, the handler sets in_progress_marker=false without removing the state file — the state is preserved for --resume on re-invocation.
Per-story state writes: after each manual story answer is collected via sprint-manual-drain.sh, record the answer:
.claude/scripts/dso sprint/sprint-pause-state.sh write <epic-id> <story-id> <answer>
After all stories answered: call sprint/sprint-pause-state.sh cleanup <epic-id> to remove the state file.
Handshake Input Contract
Stories tagged manual:awaiting_user are collected into awaiting_manual_stories and presented to the practitioner one at a time by sprint-manual-drain.sh. The script accepts three inputs per story:
done — story complete; if a verification_command is present, execute it in a constrained subshell (timeout: planning.verification_command_timeout_seconds, default 30s); if absent, require a user-typed confirmation token (MANUAL_CONFIRMATION_TOKEN) and log it as a ticket comment audit entry.
done <story-id> — same as done but explicitly names the story, used when multiple stories are presented.
skip — mark the story skipped; sprint-manual-drain.sh writes a sentinel with handshake_outcome=skip and propagates skip to transitive dependents.
Confirmation-token audit path: when verification_command is omitted, sprint-manual-drain.sh prompts for a MANUAL_CONFIRMATION_TOKEN (a short user-typed string) and writes it as a MANUAL_PAUSE_SENTINEL ticket comment. dso:completion-verifier reads this sentinel at Phase F Step 18 to verify the manual story without re-executing the manual step.
Steps:
-
Write the sorted manual story list to a temp JSON file. Each entry must include the fields expected by sprint-manual-drain.sh:
MANUAL_JSON_FILE=$(mktemp /tmp/sprint-manual-stories-XXXXXX.json)
-
Call the manual-drain script via the host-project shim:
.claude/scripts/dso sprint/sprint-manual-drain.sh "$MANUAL_JSON_FILE"
-
Parse the exit code:
- 0: all manual stories handled — proceed to Phase E (or Phase F if no autonomous tasks remain in this batch)
- 1: skip propagation applied — log skipped stories and proceed; skipped stories have a sentinel written with
handshake_outcome=skip
- 2: re-prompt required (this should not occur —
sprint-manual-drain.sh handles re-prompting internally; if it surfaces here, log as an error and escalate to user)
-
After handshake completes: run .claude/scripts/dso ticket next-batch <epic-id> again to pick up any autonomous stories that were unblocked by the manual step completion.
-
Clean up: rm -f "$MANUAL_JSON_FILE" and sprint/sprint-pause-state.sh cleanup <epic-id>
Phase E: Sub-Agent Launch (/dso:sprint)
Do NOT implement any task directly using Edit, Write, or other file-modification tools. ALL implementation tasks must be dispatched to sub-agents via the Task tool — regardless of how small, simple, or obvious the change appears. "Small markdown edit", "single-line change", "user already approved", or "sub-agent dispatch is overhead" are not valid exceptions. Direct implementation by the orchestrator bypasses checkpoint protocol, code review, and acceptance criteria gates.
Do NOT improvise new patterns, variables, or approaches when a user rejects the approved plan. When the user rejects an approach mid-execution:
- STOP the current batch — do not apply ad-hoc substitutes.
- Record
REPLAN_TRIGGER: user_rejection — User rejected <approach>. Reason: <reason>. on the epic.
- Re-invoke
/dso:implementation-plan for the affected stories to produce a revised plan that incorporates the user's feedback.
- Only resume execution after the revised plan passes
/dso:plan-review.
Inventing unauthorized patterns (new variables, alternative sed commands, manual workarounds) to work around a rejected plan is the exact failure mode this gate prevents — it produces untested, unreviewed changes that bypass the re-planning protocol (2f26-430e).
Explore dispatch parallelism rule (7c45-ee60): When dispatching Explore sub-agents for search tasks, each Explore call MUST be scoped to a single, targeted search objective. Do NOT dispatch a single Explore sub-agent to search for multiple unrelated code patterns, files, or references in one call.
Instead, parallelize: dispatch one Explore sub-agent per distinct search objective within the same message using run_in_background: true. For example:
- BAD: Single Explore dispatched to "find isolation guard code AND all references to it"
- GOOD: Two parallel Explore dispatches — one for "find isolation guard code" and one for "find all references to isolation guard"
A single broad Explore dispatch is a known anti-pattern that produces lower quality results and misses edge cases. Always parallelize independent search objectives.
Launch up to max_agents sub-agents (determined by Phase C Step 1's MAX_AGENTS protocol — unlimited, N, or 0) via the Task tool. When max_agents=0, this phase is skipped entirely (see Phase C Step 1). Each sub-agent gets a structured prompt:
Retry Budget Parsing (sub-agent dispatch)
Before dispatching each sub-agent task, the orchestrator MUST parse the ## Retry Budget block from the task description and honour it during the dispatch loop. This is distinct from the red-test-writer Tier 1/2/3 escalation in ### RED Task Dispatch — Escalation Protocol below — it governs the dispatch retry budget for the task sub-agent itself.
Fields parsed from each task description's ## Retry Budget block:
MAX_ATTEMPTS — per-tier attempt cap (default: 3). The orchestrator retries the sub-agent up to this many times on the base tier before escalating model.
MODEL_TIER_ORDER — ordered list of model tiers, e.g. sonnet, opus. The first tier is the base; subsequent tiers are escalation targets.
ESCALATION_DIAGNOSTICS — when escalating tiers, the orchestrator collects all prior failure messages and forwards them as diagnostic context to the next tier.
Sub-agent failure protocol (MAX_ATTEMPTS-driven, sonnet→opus escalation):
- Base tier (sonnet) — Retry the sub-agent up to
MAX_ATTEMPTS (default: 3) on sonnet. Each retry receives the prior failure message in its prompt.
- Escalate to opus — After 3 sonnet failures (MAX_ATTEMPTS exhausted on the base tier), collect all 3 failure messages and re-dispatch on
opus with the concatenated diagnostic context. The opus tier also gets MAX_ATTEMPTS (default: 3) attempts.
- Escalate to user — After 3 opus failures (6 total attempts across both tiers), STOP the dispatch loop and escalate to the user with the full failure history (all 6 messages plus diagnostic context). Do NOT silently drop the task or mark it complete.
- MAX_AGENTS: 0 mid-escalation — If the throttle verdict reaches
MAX_AGENTS: 0 at the sonnet→opus escalation boundary, SKIP the opus tier entirely and escalate to the user immediately. This avoids burning the larger model budget under throttle.
Defaults when ## Retry Budget block is absent from a task description: MAX_ATTEMPTS=3, MODEL_TIER_ORDER=sonnet,opus. Tasks without an explicit retry budget still receive the same sonnet→opus escalation behaviour for backward compatibility.
Display Batch Task List
Print a numbered list of all tasks in the batch. Each line must show the task ID and title:
1. [dso-abc1] Fix authentication bug
2. [dso-def2] Add rate limiting to API endpoints
3. [dso-ghi3] Refactor session management
Titles are parsed from the TASK: tab-separated lines produced by ticket next-batch — the last field in each TASK: line is the title. No additional .claude/scripts/dso ticket show calls are needed.
Blackboard Write and File Ownership Context
Before dispatching sub-agents, create the blackboard file and build per-agent file ownership context:
-
Write the blackboard: Pipe the batch JSON (from ticket next-batch --json in Phase C Step 4) to write-blackboard.sh:
echo "$BATCH_JSON" | .claude/scripts/dso write-blackboard.sh
If write-blackboard.sh fails, log a warning and continue without blackboard — sub-agents will receive empty {file_ownership_context}. Blackboard failure must not block sub-agent dispatch.
-
Build file ownership context:
REPO_ROOT=$(git rev-parse --show-toplevel)
BLACKBOARD="${TMPDIR:-/tmp}/dso-blackboard-$(basename "$REPO_ROOT")/blackboard.json"
For each agent (task), build a file_ownership_context string with the format:
You own: file1.py, file2.py. Other agents own: <task-id-X> owns file3.py, file4.py; <task-id-Y> owns file5.py.
If the blackboard file does not exist, use an empty string for file_ownership_context.
-
Populate the placeholder: Replace {file_ownership_context} in task-execution.md with the per-agent ownership string.
Worktree Isolation Configuration
Before dispatching sub-agents, read and apply skills/shared/prompts/worktree-dispatch.md for worktree isolation configuration.
Read the config key:
ISOLATION_ENABLED=$(bash "$(git rev-parse --show-toplevel)/.claude/scripts/dso" read-config worktree.isolation_enabled 2>/dev/null || true)
When ISOLATION_ENABLED equals true, add isolation: "worktree" to each Agent/Task dispatch call and pass ORCHESTRATOR_ROOT=$(git rev-parse --show-toplevel) in each sub-agent's prompt so sub-agents can verify isolation. When ISOLATION_ENABLED is false, empty, or absent, omit the isolation parameter entirely.
Parallel dispatch and stale HEAD: All worktrees in a batch branch from the session HEAD at the moment of dispatch. When sub-agents are dispatched in parallel and harvested serially, the second and later harvests will encounter merge conflicts because those worktrees were created before earlier harvests advanced the session HEAD. This is expected and normal — not a bug. The per-worktree-review-commit.md conflict queue protocol (Step 6 exit 1) handles this case: after the clean worktrees are harvested, conflicting worktrees are rebased onto the updated session HEAD and re-processed through the review → commit → harvest pipeline. No full task re-implementation is needed for conflicts that arise solely from this ordering effect.
Design Context Population
Before dispatch, source the figma tag constants and check whether the parent story has the design:approved tag:
REPO_ROOT=$(git rev-parse --show-toplevel)
source "${CLAUDE_PLUGIN_ROOT}/skills/shared/constants/figma-tags.conf"
For each task, look up the parent story's tags (already fetched during COMPLEX detection):
If parent story has design:approved tag:
Note: design:approved guarantees the revision PNG exists — the approval command (design-approve.sh) validates PNG existence before applying the tag. No additional file existence check is needed here.
- Find the design UUID from story comments — search for a comment whose body matches the pattern
designs/([^/]+)/ (e.g., "Design Manifest: designs/550e8400-.../manifest.md"). Extract the UUID from the first capture group. If multiple comments match, use the most recent one.
- Build the
design_context string:
## Design Artifacts
Manifest path: designs/<uuid>/spatial-layout.json
Revision image path: designs/<uuid>/figma-revision.png
- Replace
{design_context} in task-execution.md with this string.
- Set
STORY_HAS_DESIGN_APPROVED=true for model tier enforcement (see Subagent Type and Model Selection).
If parent story does NOT have design:approved tag:
- Replace
{design_context} in task-execution.md with an empty string.
- Set
STORY_HAS_DESIGN_APPROVED=false. No model override.
Sub-Agent Prompt Template
For each task, launch a Task with the appropriate subagent_type.
Quality gate (ticket-as-prompt): Before dispatch, run the quality check:
.claude/scripts/dso issue-quality-check.sh <task-id>
- Exit 0 (quality pass): Use ticket-as-prompt template (
$PLUGIN_ROOT/skills/sprint/prompts/task-execution.md), fill in {id} and {escalation_policy} (see COMPLEX detection and escalation policy extraction below).
- Exit 1 (too sparse): Try
.claude/scripts/dso enrich-file-impact.sh <task-id>, re-run check. If still failing, fall back to inline prompt via .claude/scripts/dso ticket show <id>.
Acceptance criteria gate: After the quality gate, run:
.claude/scripts/dso check-acceptance-criteria.sh <task-id>
- Exit 0: Proceed with dispatch — task has structured AC block
- Exit 1: Do NOT dispatch. Read
${CLAUDE_PLUGIN_ROOT}/docs/ACCEPTANCE-CRITERIA-LIBRARY.md, compose AC, add via .claude/scripts/dso ticket comment <id> "## Acceptance Criteria\n<criteria>". Re-run check. If criteria undeterminable, ask user.
Subagent Type and Model Selection
Use the model and subagent fields from the TASK: lines produced by
ticket next-batch in Phase C Step 4 — no additional classify-task.sh call needed.
When launching each Task tool call, set subagent_type and model from the TASK line, then apply the decision table below in order (first matching row wins):
| parent_story_has_design_approved | parent_story_complex | task_model | task_class | action |
|---|
true (revision image present) | any | any | any | Override model to minimum sonnet (if current model is haiku, upgrade to sonnet; if already sonnet or opus, no change). Log: "design:approved story — enforcing sonnet minimum for multimodal." |
| any | any | any | any (doc-story title match) | Use subagent_type: "general-purpose" with model: "sonnet". Read agents/doc-writer.md inline and pass its content verbatim as the prompt. (dso:doc-writer is an agent file identifier, NOT a valid subagent_type value — the Agent tool only accepts built-in types.) Pass epic_context and git_diff context fields (see Documentation Story Dispatch below). Log: "Documentation story detected — dispatching to dso:doc-writer instead of generic agent." |
| any | COMPLEX | sonnet | skill-guided | No model upgrade. Append skill check guidance to prompt (see below). |
| any | COMPLEX | sonnet | any other | Override model to opus. Log: "Story <parent-id> classified COMPLEX — upgrading task <task-id> model to opus." |
| any | COMPLEX | opus | any | No change (already opus). |
| any | not COMPLEX | any | skill-guided | No model upgrade. Append skill check guidance to prompt (see below). |
| any | not COMPLEX | any | any other | No change — use model and subagent from TASK line as-is. |
Doc-story title match: Task title or parent story title matches Update project docs to reflect.
Doc-story detection heuristics (apply ALL of these — not just title match):
A story is a documentation story if ANY of the following are true:
- Story title contains "doc", "document", "update", "add to", "CLAUDE.md", "KNOWN-ISSUES", "design-notes", "README"
- Story title starts with "As a" AND acceptance criteria mention documentation files
- Any child task references a
.md file in .claude/docs/, docs/, or the repo root
CLAUDE.md-specific rule (79d9-f97a): When the target file is CLAUDE.md, the dso:doc-writer dispatch MUST include a bloat-review flag in the task context:
doc_target: CLAUDE.md
bloat_review_required: true
max_tokens_budget: 12000
The doc-writer agent enforces its CLAUDE.md Read-Only Guard. Do NOT edit CLAUDE.md directly — always route through dso:doc-writer. Direct CLAUDE.md edits are blocked by this rule.
COMPLEX detection and escalation policy extraction: Run .claude/scripts/dso ticket show <task-id> and read the parent field; if a parent story ID exists, run .claude/scripts/dso ticket show <parent-story-id> and from that output: (1) grep with grep -Fx "COMPLEXITY_CLASSIFICATION: COMPLEX" (exact full-line match to avoid false positives); (2) extract the ## Escalation Policy section by capturing all lines between ## Escalation Policy and the next ## heading (or end of description). Store the extracted text as escalation_policy_text. If no ## Escalation Policy section is present (Autonomous mode omits it), set escalation_policy_text to "Proceed with best judgment. Make and document reasonable assumptions. Do not escalate for uncertainty." When populating task-execution.md, replace {escalation_policy} with escalation_policy_text.
Skill check guidance (appended to prompt when class is skill-guided): "Before implementing, check if a skill applies to this task type (e.g., /writing-skills for skill files, /claude-md-improver for CLAUDE.md updates, /writing-rules for hookify rules)."
Documentation Story Dispatch
When the doc-story title match triggers (9f13-655a): do NOT implement documentation changes directly — this gate is unconditional. Even if the required change seems trivial, the doc-writer agent enforces structural and bloat constraints that the orchestrator does not. Read agents/doc-writer.md inline and dispatch as subagent_type: "general-purpose" with model: "sonnet". The doc-writer agent receives two named context fields:
subagent_type: "general-purpose"
model: "sonnet"
context:
epic_context: |
## Epic ID
<epic-id>
## Story Descriptions
<full output of `.claude/scripts/dso ticket show <epic-id>`>
git_diff: |
<full output of `git diff main...HEAD`>
Agent description: 3-5 word summary from ticket title (e.g., Fix review gate hash).
Important: Launch ALL sub-agents in the batch within a single message, each with run_in_background: true. The number of Task calls is governed by max_agents from Phase C Step 1 (unlimited = all candidates, N = cap at N, 0 = skip dispatch).
Stale HEAD warning (4ad5-25df): When ISOLATION_ENABLED=true, all agent worktrees are branched from the session HEAD at the moment of dispatch. Agents that complete later will be missing commits from agents that were harvested earlier in the same batch. This is expected and handled by the conflict queue protocol in per-worktree-review-commit.md Step 6: if harvest-worktree.sh returns exit 1 (merge conflict), the conflicting worktree is queued for post-batch resolution (rebase first, full re-implementation only as a last resort). Do NOT attempt to resolve conflicts during the serial harvest loop — finish all non-conflicting harvests first, then work through the conflict queue.
Worktree boundary: If in a worktree, append to every sub-agent prompt: "IMPORTANT: Only modify files under $(git rev-parse --show-toplevel). Do NOT write to any other path." When ISOLATION_ENABLED=true, also add isolation: "worktree" to the Task dispatch call (see Worktree Isolation Configuration above).
Testing Mode Routing
Before dispatching sub-agents, extract the ## Testing Mode value from each task's description:
TASK_DESC=$(.claude/scripts/dso ticket show <task-id>)
TESTING_MODE=$(echo "$TASK_DESC" | python3 -c "
import sys, re
desc = sys.stdin.read()
m = re.search(r'## Testing Mode\s*\n([^\n#]+)', desc)
print(m.group(1).strip() if m else '')
")
Route based on TESTING_MODE:
| testing_mode value | Action |
|---|
RED | Dispatch dso:red-test-writer before implementation (existing behavior) |
GREEN | Skip RED test dispatch entirely. Sub-agent validates existing tests pass after implementation. |
UPDATE | Sub-agent modifies existing tests to assert new behavior before implementing. Do NOT dispatch dso:red-test-writer. |
| absent / empty | Default to RED behavior (backward compatibility — tasks created before this field was introduced) |
GREEN mode: Pass the following instruction to the sub-agent's Step 4 in task-execution.md: skip writing new tests; after implementation, validate that existing tests still pass.
UPDATE mode: Pass the following instruction to the sub-agent's Step 4 in task-execution.md: modify the existing test file(s) listed in the file impact table to assert the new expected behavior before implementing the source change. The test must fail (RED) on the current code before the fix.
Backward compatibility: When TESTING_MODE is absent or empty, treat as RED — dispatch dso:red-test-writer as normal.
RED Task Dispatch — Escalation Protocol
Detect RED tasks: Check whether the subagent field equals dso:red-test-writer.
When subagent = dso:red-test-writer, do NOT use normal dispatch. Follow prompts/red-task-escalation.md:
Tier 1 — Dispatch dso:red-test-writer (sonnet):
- Pass the full task context: task description, story context, and file impact table
- Parse the leading
TEST_RESULT: line from the output:
TEST_RESULT:written → Success. Proceed to TDD setup using TEST_FILE and RED_ASSERTION fields. Do NOT escalate.
TEST_RESULT:no_new_tests_needed → Success. No new test was needed. Do NOT escalate to Tier 2. Proceed to normal task execution without TDD setup.
TEST_RESULT:rejected → Escalate to Tier 2. This is not a dispatch failure — do not route to Phase F Step 1.
- Timeout / malformed / non-zero exit → Treat as
TEST_RESULT:rejected with REJECTION_REASON: ambiguous_spec. Escalate to Tier 2.
Tier 2 — Dispatch dso:red-test-evaluator (opus):
Tier 3 — Re-dispatch dso:red-test-writer (opus model override):
- Re-dispatch the original task to
dso:red-test-writer with model overridden to opus
- Pass the same task context as Tier 1, augmented with the evaluator's
VERDICT:REJECT payload
- Parse the leading
TEST_RESULT: line:
TEST_RESULT:written → Success. Proceed to TDD setup normally.
TEST_RESULT:no_new_tests_needed → Success. No new test was needed. Do NOT escalate to Tier 2. Proceed to normal task execution without TDD setup.
TEST_RESULT:rejected → Terminal failure. Escalate to the user with: the Tier 1 rejection payload, the Tier 2 VERDICT:REJECT reason, and the Tier 3 rejection payload. Do not retry further.
- Timeout / malformed / non-zero exit → Terminal failure. Escalate to the user.
See prompts/red-task-escalation.md for the complete escalation reference.
Phase F: Post-Batch Processing (/dso:sprint)
After ALL sub-agents in the batch return, follow the Orchestrator Checkpoint Protocol from CLAUDE.md.
Worktree Isolation Mode: Per-Worktree Serial Review and Commit
When worktree.isolation_enabled is true and sub-agents returned with isolation:worktree, do NOT proceed to the shared-directory batch review flow (Step 13). Instead, process each worktree serially using the per-worktree protocol:
Read and execute skills/sprint/prompts/per-worktree-review-commit.md for each worktree, in completion order (first-pass-first-merge). This means: for each worktree — run review in the worktree context, commit to the worktree branch, merge the worktree branch into the session branch, then remove the worktree and its branch (Step 13) — before moving to the next worktree.
Git log note: In worktree isolation mode, git log on the session branch shows one commit per worktree (no combined batch commits). Each worktree's changes are merged independently into the session branch.
merge-to-main.sh note: merge-to-main.sh runs once at session end (Phase I), not per worktree. Each per-worktree merge is worktree-branch → session-branch only.
After all worktrees have been processed via per-worktree-review-commit.md, skip Steps 7 and 10 (which apply only in shared-directory mode) and proceed directly to Steps 8, 9, 10a, 11, and 13.
When worktree.isolation_enabled is false, empty, or absent (shared-directory mode), proceed through Steps 0–13 as written below, including Step 13 (formal code review) and Step 17 (commit and push).
Step 1: Dispatch Failure Recovery (/dso:sprint)
Check whether any sub-agent Task call returned an infrastructure-level dispatch failure (no STATUS: line, no FILES_MODIFIED: line, error message references agent type/tool availability/internal errors).
RED test task exception: If the failed task's subagent field was dso:red-test-writer, do NOT fall back to general-purpose. A TEST_RESULT:rejected response triggers the three-tier escalation protocol (Phase E RED Task Dispatch). Only true dispatch failures (no TEST_RESULT: line, no STATUS: line, tool-level error indicators) qualify for the recovery flow below.
For each sub-agent that returned a dispatch failure:
- Detect: The Task result contains no
STATUS: or FILES_MODIFIED: lines AND includes error indicators (e.g., "unknown subagent_type", "agent unavailable", "internal error", "Tool result missing")
- Retry with general-purpose: Re-dispatch the same task immediately using
subagent_type="general-purpose" with the same model and prompt. Log: "Dispatch failure for task <id> with subagent_type=<original-type> — retrying with general-purpose."
- If retry succeeds: Continue to Step 2 with the retry result
- If retry also fails: Escalate model (sonnet → opus) and retry once more with
subagent_type="general-purpose". Log: "Retry with general-purpose also failed for task <id> — escalating model to opus."
- If all retries fail: Mark the task as failed and proceed to Step 16
Important: Dispatch failure retries happen sequentially. Do not count retries toward the max_agents cap.
Step 2: Verify Results (/dso:sprint)
For each sub-agent (including any that succeeded on retry), check the Task tool result:
- Did it report success?
- Are the expected files present? (spot-check with Glob)
- Were tests passing?
Step 3: Migration Behavioral Verification (/dso:sprint)
For each sub-agent in the batch, check if its task description contains migration keywords (remove, delete, migrate, move, replace). For migration tasks:
- Verify the replacement exists: Run the first task-specific AC
Verify: command. If it fails, mark the task as failed.
- Behavioral smoke test: If the task migrates a command/skill/script, invoke or test the migrated artifact. Log:
"Migration behavioral check for <task-id>: <pass|fail>"
Step 4: Confidence Signal Parsing (/dso:sprint)
For each sub-agent result, scan for the confidence signal line (see docs/contracts/confidence-signal.md): # shim-exempt: internal contract reference
-
Parse the confidence signal: Scan the sub-agent output for a line that is exactly CONFIDENT or begins with UNCERTAIN:.
CONFIDENT — high confidence; no action needed beyond normal processing.
UNCERTAIN:<reason> — low confidence; proceed to steps below.
- Absent or malformed signal (no confidence line, bare
UNCERTAIN with no colon, UNCERTAIN: with empty reason) — treat as UNCERTAIN with reason "no confidence signal emitted". Log a warning: "Warning: task <task-id> emitted no valid confidence signal — treating as UNCERTAIN."
-
Only count STATUS:pass + UNCERTAIN signals toward the threshold. STATUS:fail tasks already trigger revert-to-open in Step 16 through the normal failure path — the UNCERTAIN signal on a failing task does not change routing.
-
For each task where STATUS:pass + UNCERTAIN:
a. Identify the parent story ID from the task's story:<id> field in the TASK line (from Phase E batch list).
b. Record the signal: .claude/scripts/dso ticket comment <story-id> "UNCERTAIN_SIGNAL: task <task-id> — <reason>"
c. Increment the per-story counter: story_uncertain_counts[<story-id>] += 1 (initialize to 0 if not yet set).
d. Log: "UNCERTAIN signal from task <task-id> under story <story-id> — count now <N>."
Step 5: Integrate Discovered Tasks (/dso:sprint)
For each sub-agent result, check the TASKS_CREATED line:
- If
none → skip
- If
error: <reason> → log the error, no action needed
- If task IDs listed (e.g.,
ticket-042, ticket-043):
- Run
.claude/scripts/dso ticket show <id> for each created task to review title and description
- Wire dependencies via
.claude/scripts/dso ticket link if the new task blocks or is blocked by existing work
- Log: "Sub-agent for discovered N new tasks: "
After processing all sub-agents in the batch, if any tasks were created:
.claude/scripts/dso validate-issues.sh --quick --terse
Step 6: Collect Agent Discoveries (/dso:sprint)
Collect structured discovery files from sub-agent execution (propagated to next batch via {prior_batch_discoveries} in Phase C Step 10).
DISCOVERIES=$(.claude/scripts/dso collect-discoveries.sh 2>/dev/null) || DISCOVERIES="[]"
- If
collect-discoveries.sh succeeds, DISCOVERIES contains a JSON array of discovery objects
- Store the result for use in Phase C Step 10 when composing the next batch's sub-agent prompts
- Graceful degradation: If discovery collection fails (script error, malformed JSON), log a
warning and continue with
DISCOVERIES="[]". Discovery collection failure must not block the
sprint. The script itself handles per-file validation — malformed individual files are skipped
with warnings to stderr.
Step 7: Acceptance Criteria Validation (/dso:sprint)
Batched shared criteria (run ONCE per batch, not per-task):
Universal criteria (test, lint, format) are already verified by Phase F Step 17
(validate-phase.sh post-batch). Do not re-run per task.
Per-task structural criteria:
For each task in the batch, extract the Acceptance Criteria block from .claude/scripts/dso ticket show <id> output
and run each task-specific (non-universal) Verify: command:
- File existence:
test -f {file} — exit 0 = pass
- Class importable:
python -c "from {module} import {class}" — exit 0 = pass
- Test count:
grep -c "def test_" {file} — compare to threshold
- Grep-verifiable: run the grep pattern — exit 0 = pass
Criteria without a Verify: command are logged but not machine-verified —
caught by the formal code review (Step 13).
If any machine-verifiable criterion fails:
- Log the failed criterion and its
Verify: output
- Mark the task as failed in Step 16 (revert to open)
- Include the failed criterion text in the re-dispatch prompt
Batch Completion Summary
Print a completion summary. Each line must show the task ID, title, and pass/fail result:
✓ [dso-abc1] Task title (pass)
✗ [dso-abc2] Other task (fail — reverted to open)
Titles are retained from the pre-launch batch list printed in Phase E — no additional .claude/scripts/dso ticket show calls are needed.
Awaiting Designer Input Section
After the per-task completion lines, if awaiting_design_stories is non-empty, print a blocked status section:
Awaiting designer input:
- [<story-id>] <story-title> (awaiting since <date>)
- [<story-id>] <story-title> (awaiting since <date>) ⚠️ STALE (><figma_staleness_days> days)
Staleness logic:
- For each story in
awaiting_design_stories, compute tag age in days from tag_applied_date to today.
- If
tag_applied_date is unknown, omit the staleness warning for that story.
- If tag age exceeds
figma_staleness_days (read from design.figma_staleness_days in .claude/dso-config.conf, default 7), append ⚠️ STALE (>N days) to that story's line.
- Stories in this section are not counted as batch failures — they are explicitly blocked pending designer delivery.
These stories are excluded from Phase B implementation-plan dispatch and Phase E sub-agent dispatch. They are surfaced here to give the user visibility into what is blocked on design. No action is required from the orchestrator — the sprint continues with non-blocked stories.
Step 8: File Overlap Check (Safety Net) (/dso:sprint)
Check for actual file conflicts before committing:
- For each sub-agent, collect its modified files from the Task result
- Run the overlap detection script:
$PLUGIN_SCRIPTS/agent-batch-lifecycle.sh file-overlap \
--agent=<task-id-1>:<file1>,<file2> \
--agent=<task-id-2>:<file3>,<file4>
The script outputs CONFLICTS: <N> followed by one CONFLICT: line per overlap.
Exit 0 = no conflicts, exit 1 = conflicts detected.
- If conflicts are detected, resolution (same protocol as
/dso:debug-everything Phase H Step 10):
a. Identify the primary agent for each conflicting file (highest priority)
b. Revert ALL secondary agents' changes to conflicting files
c. Re-run secondary agents one at a time in priority order (not parallel),
each with original prompt + Conflict Resolution Context (captured diff,
instruction to respect current file state). Commit after each re-run.
d. After each re-run: if agent only touched non-conflicting files -> merge OK.
If it re-modified the same conflicting files -> escalate to user.
- If no conflicts -> proceed to Step 10
Step 9: Semantic Conflict Check (/dso:sprint)
After the file overlap check, run the LLM-based semantic conflict detector on the
batch's combined diff:
SEMANTIC_RESULT=$(git diff | python3 "$PLUGIN_SCRIPTS/semantic-conflict-check.py" 2>/dev/null) || SEMANTIC_RESULT='{"conflicts":[],"clean":true,"error":"script failed"}'
Parse the JSON output:
- If
clean is true: log "Semantic conflict check: clean" and proceed
- If
clean is false: log each conflict (files, description, severity) and escalate
high-severity conflicts to the user before committing
- Graceful degradation:
semantic-conflict-check.py always exits 0 — on failure it
returns {"conflicts":[], "clean":true, "error":"<message>"}. Check for the error
key: if present, log "Semantic conflict check warning: <error>" and proceed. Semantic
conflict check failure is non-fatal and must not block the sprint.
Step 10: Run Validation (/dso:sprint)
$PLUGIN_SCRIPTS/validate-phase.sh post-batch
If validation fails, identify which sub-agent's code is broken and note it.
Test Failure Sub-Agent Delegation (Phase F Step 17)
When validate-phase.sh post-batch fails, dispatch a debugging sub-agent BEFORE reverting tasks to open. Follow prompts/test-failure-dispatch-protocol.md with these caller-specific fields:
test_command: the validate-phase.sh post-batch command that failed
changed_files: files modified by the batch (git diff --name-only)
task_id: the task ID of the sub-agent that likely caused the failure
context: sprint-post-batch
batch_task_ids: IDs of all tasks in the current batch
On PASS: re-run validate-phase.sh post-batch to confirm, then continue to Step 11.
Step 11: Persistence Coverage Check (/dso:sprint)
If any task in the batch touched persistence-critical files (job_store, document_processor,
DB models, DB clients), run the persistence coverage check:
.claude/scripts/dso check-persistence-coverage.sh
If the check fails:
- Log:
"Persistence coverage check failed — persistence source changed without test coverage."
- Do not commit. Instead:
a. If a sub-agent was responsible for the persistence change, re-run it with an updated prompt
requiring a persistence test (DB round-trip or cross-worker test).
b. If the persistence change was made by the orchestrator, write the missing test directly.
- After adding the test, re-run the check and proceed only when it passes.
Step 12: Visual Verification (UI tasks only) (/dso:sprint)
If any task in the batch modified templates, CSS, or frontend code:
REPO_ROOT=$(git rev-parse --show-toplevel)
cd $REPO_ROOT/app && make test-visual 2>&1
- Pass → proceed
- Fail → Use
/dso:playwright-debug Tier 2. If still failing, revert task to open.
- No baselines → Use
/dso:playwright-debug full 3-tier. Verify local env: $PLUGIN_SCRIPTS/check-local-env.sh. # shim-exempt: internal orchestration script
Step 13: Formal Code Review (/dso:sprint) — Shared-Directory Mode Only
This step applies only when worktree.isolation_enabled is false (shared-directory mode). When worktree isolation is enabled, review is handled per-worktree via per-worktree-review-commit.md (see Worktree Isolation Mode section at the top of Phase F). Skip this step in worktree isolation mode.
Execute the review workflow (REVIEW-WORKFLOW.md). If already read earlier in this conversation, use the version in context. Produces a review state file at $(get_artifacts_dir)/review-status.
Do NOT dispatch any dso:code-reviewer-* agent directly. You MUST execute REVIEW-WORKFLOW.md Step 3 first to obtain REVIEW_TIER and REVIEW_AGENT from the complexity classifier before dispatching. Hardcoding dso:code-reviewer-light or any other tier is prohibited — the classifier determines the tier based on diff characteristics.
Snapshot exclusion: Exclude snapshot baselines from review diffs:
".claude/scripts/dso capture-review-diff.sh" "$DIFF_FILE" "$STAT_FILE" \
':!app/tests/unit/templates/snapshots/*.html'
Interpret results:
- No Critical, Important, or Fragile findings → proceed to Step 15
- Critical, Important, or Fragile findings found → Enter Autonomous Resolution Loop per REVIEW-WORKFLOW.md. No inline fixes by orchestrator. Failed tasks: revert to open, add issue details, re-run with reviewer feedback. Critical, Important, or Fragile findings are NOT a pass — do NOT suggest graceful shutdown or proceed to commit. Apply the fix or escalate.
- Minor issues only → proceed (note them in ticket but don't block)
- Autonomous resolution: Up to
review.max_resolution_attempts (default: 5) fix/defend attempts before tier escalation (light → standard → deep). When attempts are exhausted, upgrade to the next tier before escalating to user — the deep tier (3 sonnet + opus synthesis) must be tried before user escalation. Resolution sub-agent applies fixes, then orchestrator dispatches separate re-review sub-agent (no nesting). If issues persist after deep tier, escalate to the user — do NOT commit or initiate graceful shutdown. The review loop continues until the review passes OR the user explicitly approves proceeding.
- Stale or invalid review findings: If the review gate rejects a commit because findings are stale (from a different context, wrong diff hash, or prior session), do NOT work around the gate. Re-run REVIEW-WORKFLOW.md from Step 0 (which clears stale artifacts) to get a fresh review. Never dispatch a generic agent to write
reviewer-findings.json — this is fabrication regardless of whether the orchestrator writes the file directly or delegates it to a non-reviewer agent.
CONTEXT ANCHOR: When the review sub-agent returns no critical/important/fragile findings (FINDING_COUNT with all minor or 0), this is NOT a session completion signal. Proceed immediately to Step 14 → Step 15 → Step 16 → Step 17. Do NOT stop, wait for user input, or treat review completion as a stopping point.
Step 14: Out-of-Scope Review Feedback Detection (/dso:sprint)
After review resolution completes (Step 13) and before proceeding to Step 15, check whether accepted review findings reference files outside the task's scope.
For each task in the batch that completed review:
- Run
sprint-review-scope-check.sh with the reviewer-findings path and task ID:
SCOPE_RESULT=$(.claude/scripts/dso sprint/sprint-review-scope-check.sh "$(get_artifacts_dir)/reviewer-findings.json" "<task-id>")
- If
SCOPE_RESULT starts with OUT_OF_SCOPE:
a. Parse the out-of-scope file list (everything after OUT_OF_SCOPE: ).
b. Log: "Review accepted findings for out-of-scope files: <files> (task <task-id>)"
c. Append to the accumulator:
batch_out_of_scope_findings.append({
"task_id": "<task-id>",
"story_id": "<parent-story-id>",
"files": [<out-of-scope files>]
})
d. DO NOT route to implementation-plan here. Out-of-scope findings are collected during the batch and processed only between batches (Step 20) to avoid mid-batch task injection conflicts.
- If
IN_SCOPE → no action needed; proceed normally.
- If the script fails (non-zero exit) → log a warning and continue. Scope checking failure must not block the sprint.
Step 15: Update Ticket Notes (/dso:sprint)
For each task in the batch, write checkpoint-format notes for crash recovery:
| Outcome | Command |
|---|
| Success | .claude/scripts/dso ticket comment <id> "CHECKPOINT:batch-complete — Done ✓ — Files: <files created/modified>. Tests: pass." |
| Failure (pre-review) | .claude/scripts/dso ticket comment <id> "CHECKPOINT:implementation-done — Failed at review — <error summary>. Files modified: <files>. Resume from: review." |
| Failure (post-review) | .claude/scripts/dso ticket comment <id> "CHECKPOINT:review-passed — Failed at validation — <error summary>. Resume from: validation." |
Use semantic checkpoint names to describe progress phase:
CHECKPOINT:implementation-done — code written, not yet reviewed
CHECKPOINT:review-passed — code reviewed, not yet validated
CHECKPOINT:validation-passed — batch validation passed
CHECKPOINT:batch-complete — all substeps done
Step 16: Handle Failures (/dso:sprint)
For tasks that failed:
- Revert to open:
.claude/scripts/dso ticket transition <id> open
- Record the failure reason in notes (already done in Step 15)
Step 17: Commit & Push (/dso:sprint) — Shared-Directory Mode Only
This step applies only when worktree.isolation_enabled is false (shared-directory mode). When worktree isolation is enabled, commits are made per-worktree via per-worktree-review-commit.md (see Worktree Isolation Mode section at the top of Phase F). Skip this step in worktree isolation mode.
Read and execute ${CLAUDE_PLUGIN_ROOT}/docs/workflows/COMMIT-WORKFLOW.md.
SIZE_WARNING path: When SIZE_ACTION=warn, log the SIZE_WARNING to the user and continue with review dispatch. Do NOT halt, split, or escalate based on warn alone.
Push the worktree branch:
git push -u origin HEAD
Do NOT merge to main here.
Blackboard cleanup: After the commit, run write-blackboard.sh --clean to remove the blackboard file:
.claude/scripts/dso write-blackboard.sh --clean
Do NOT proceed to Step 19 until Step 18 (completion-verifier dispatch) has completed and returned an overall_verdict. The orchestrator is biased toward confirming its own work — CLAUDE.md rule 24 exists because this step has been skipped in past sessions. "All tests pass" and "all tasks closed" do NOT substitute for independent verification.
Do NOT rationalize skipping Step 18. Prior evidence ("RED tests are GREEN", "CI passes", "AC verified") does not satisfy the completion-verifier requirement. The verifier checks done-definitions that task-level AC verification does not cover.
Do NOT use the /dso:commit Skill tool here — read and execute COMMIT-WORKFLOW.md inline to avoid nested skill invocations that may not return control.
After git push -u origin HEAD and blackboard cleanup are done, proceed to Step 18 then Step 19 then Step 20. Do NOT close the epic or invoke /dso:end-session here.
CONTINUE: After commit and push, proceed immediately to Step 18. Do NOT stop, wait for user input, or initiate graceful shutdown here.
Step 18: Close Completed Tasks (/dso:sprint)
After the batch commit and git push -u origin HEAD succeed, close each task whose code was successfully committed:
Pre-dispatch child closure check (Step 18 prerequisite):
Before dispatching dso:completion-verifier, verify all child tasks of this story are closed:
OPEN_CHILDREN=$(.claude/scripts/dso ticket list --status=open 2>/dev/null | \
python3 -c "import json,sys; data=json.load(sys.stdin); \
children=[t for t in data if t.get('parent_id')=='<story-id>']; \
print(len(children))")
If OPEN_CHILDREN > 0:
- Do NOT dispatch dso:completion-verifier
- Do NOT close the story
- Transition story back to in_progress:
.claude/scripts/dso ticket transition <story-id> in_progress
- Add a comment:
.claude/scripts/dso ticket comment <story-id> "Step 18 blocked: <N> child tasks still open: <list IDs>. Complete them before closure."
- Resume Phase C to close the remaining tasks
Only when OPEN_CHILDREN == 0, proceed to dispatch dso:completion-verifier.
Manual story sentinel path (Step 18): When the story has the manual:awaiting_user tag, dso:completion-verifier reads the MANUAL_PAUSE_SENTINEL comment written by sprint-manual-drain.sh to determine the verdict (see completion-verifier.md Step 9). The orchestrator takes no special action — dispatch the verifier normally and let it apply the sentinel verdict rules automatically.
Do NOT close this story, do NOT transition it to closed, and do NOT proceed to Step 19 until dso:completion-verifier has been dispatched via Task tool and its verdict received. This gate applies regardless of whether:
- All RED tests are GREEN
- All child tasks are closed
- CI passes
- The orchestrator believes the story is complete
"All tests pass" is not a substitute for the completion-verifier dispatch. Dispatch the verifier NOW before reading any further.
MANDATORY (3f26-4c70 gate): First confirm OPEN_CHILDREN == 0 from the check above. If OPEN_CHILDREN > 0 at this point, STOP — do NOT dispatch the verifier; follow the blocked path above instead. Only when OPEN_CHILDREN is confirmed 0: Read agents/completion-verifier.md inline and dispatch as subagent_type: "general-purpose" with model: sonnet and the story ID (CLAUDE.md rule #24 — inline dispatch is the required path; no skipping).
overall_verdict: PASS → proceed with closure
overall_verdict: FAIL → see branching logic below
- Fallback (technical failure only): On timeout/unparseable JSON, log warning and proceed with closure.
Re-dispatch rule (d039-ac65): If the completion-verifier returned overall_verdict: FAIL on ANY prior run during this story's lifecycle AND a fix was subsequently applied (remediation tasks completed, Phase C re-entry executed), you MUST re-dispatch the completion-verifier before closing the story — even when confidence is high that the fix addressed the failing criterion. High confidence is NOT a valid bypass. The verifier must confirm the fix did not introduce regressions on other criteria. Only technical failure (timeout, unparseable JSON) permits proceeding without re-verification. "I fixed the exact criterion that failed" is NOT a substitute for re-dispatch.
Story validation failure detection — when overall_verdict: FAIL:
Check whether all tasks under the story are closed (no open or in-progress tasks remain):
- If open/in-progress tasks still exist: create bug tasks from
remediation_tasks_created and return to Phase C (Batch Preparation) as normal.
- If all tasks are closed but validation fails (story-level done definition not satisfied despite no remaining tasks):
-
Do NOT close the story.
-
Log: "Story <id> validation failed despite all tasks closed — creating TDD remediation tasks"
-
Record a REPLAN_TRIGGER comment 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: validation — Story <story-id> validation failed with all tasks closed. Creating TDD remediation tasks."
-
Re-invoke /dso:implementation-plan <story-id> via the Skill tool on the story to create remediation tasks. The implementation-plan re-invocation guard will detect existing closed children and produce a diff plan (new tasks only for uncovered success criteria — no duplication). If implementation-plan emits REPLAN_ESCALATE: brainstorm: add the story to the replan-stories list and route to d-replan-collect (Phase B replan logic). The cascade counter (sprint.max_replan_cycles) applies — if the cap is reached, escalate to the user. Do NOT assume implementation-plan always succeeds here. When the Skill tool returns, proceed immediately to step 5.
-
Implementation-plan will create TDD remediation tasks following standard flow: RED test task first (failing test targeting the unmet done definition), then implementation task depending on the RED test. No special logic is needed in sprint to enforce this ordering.
-
After re-planning completes (no REPLAN_ESCALATE), record resolution:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: implementation-plan — Remediation tasks created for story <story-id>."
-
Return to Phase C (Batch Preparation) to execute the new remediation tasks.
Do NOT rationalize around a FAIL verdict. The verifier's verdict is final — scope-scoping arguments ("pre-existing failures," "out-of-scope tests," "RED marker tolerance," "already tracked as a separate bug") do not override the FAIL → Phase C path. The orchestrator's judgment about whether the FAIL "really applies" is exactly the bias the verifier was designed to counteract. Only `overall_verdict: PASS` or technical failure (timeout/unparseable JSON) permits proceeding past this step.
RED marker cleanup (before closure): After overall_verdict: PASS, check .test-index for stale RED markers associated with tests from this story's scope. If any [test_name] entries exist for tests that now pass (GREEN), remove them before closing the story. Stale markers accumulate across story completions and block epic closure.
grep -n "\[.*\]" .test-index || true
.claude/scripts/dso ticket comment <id> "Fixed: <summary>"
.claude/scripts/dso ticket transition <id> open closed
Do NOT close tasks that are still open or in a failed state.
Step 19: Context Compaction Check (/dso:sprint)
Pre-Step 19 gate (5b10-0d02): Before doing anything else in Step 19, confirm that Step 18 completed successfully this story cycle: dso:completion-verifier was dispatched via the Task tool AND returned an overall_verdict. If you cannot confirm this (e.g., Step 18 was skipped or the verifier result is not in context), STOP and return to Step 18 now. Do NOT proceed to Step 19 without the verifier verdict.
Between batches — after all work is committed and pushed — check whether the session context is at least 70% capacity.
Run the context check:
REPO_ROOT=$(git rev-parse --show-toplevel)
context_exit=0
$PLUGIN_SCRIPTS/agent-batch-lifecycle.sh context-check || context_exit=$?
| Output | Exit Code | Meaning | Action |
|---|
CONTEXT_LEVEL: normal | 0 | <70% usage | Proceed to Step 20 normally |
CONTEXT_LEVEL: medium | 10 | 70–90% usage | Compact before next batch (see below) |
CONTEXT_LEVEL: high | 11 | >90% usage | Compact before next batch |
Detection signals: CLAUDE_CONTEXT_WINDOW_USAGE env var (if set by Claude Code) and $HOME/.claude/check-session-usage.sh. If neither is available, self-assess based on accumulated context. When in doubt after multiple batches, prefer compacting.
If CONTEXT_LEVEL: medium or high (or Claude self-assesses as >=70%):
- Log:
"Context usage >=70% — compacting before batch N+1 to prevent mid-work compaction."
- Verify the working tree is clean:
git status --short (all work must be committed before compacting)
- Write a compact-intent state file. Use the actual epic ID (e.g.,
LPL-42):
echo "voluntary" > "${TMPDIR:-/tmp}/sprint-compact-intent-<actual-epic-id>"
Important: Note the epic ID explicitly in the log message — e.g., "Compacting before batch N+1 for epic LPL-42." — the epic ID must survive compaction.
- Invoke compaction:
/compact
- After compaction, check for
${TMPDIR:-/tmp}/sprint-compact-intent-<epic-id>. Continue directly to Phase C. Do NOT go to Phase I.
- Agent-count after compact: No special action needed — Phase C Step 2's pre-check re-evaluates
MAX_AGENTS (may return unlimited, N, or 0) automatically.
Step 20: Continuation Decision (/dso:sprint)
Out-of-Scope Review Feedback Routing (between batches)
Before evaluating the continuation decision, process any out-of-scope review findings collected during the batch (Step 14). This fires ONLY between batches — never mid-batch.
If batch_out_of_scope_findings is non-empty:
-
Deduplicate by story: group all out-of-scope files by story_id.
-
For each affected story:
a. Collect the full list of out-of-scope files across all tasks in that story.
b. 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: review — Out-of-scope files from review: <files>. Routing to implementation-plan for story <story-id>."
c. Check the cascade cycle cap before invoking implementation-plan:
- If
replan_cycle_count >= max_replan_cycles: Cap is exhausted. Present the out-of-scope files and inform the user the cascade limit has been reached:
Out-of-scope review files require re-planning for story <story-id>:
<file list>
The cascade replan limit (max_replan_cycles=<N>) has been reached.
Options:
(a) Proceed — skip re-planning for these files and continue sprint execution
(b) Abort — stop the sprint for this epic; it will remain open for manual adjustment
(c) Manual adjustment — edit the relevant story or epic tickets manually, then resume the sprint
Wait for user input. Act on their choice. Do NOT invoke implementation-plan.
- If cap is not yet exhausted: proceed to step d.
d. Invoke
/dso:implementation-plan <story-id> via the Skill tool to create tasks covering the out-of-scope files. When the Skill tool returns, proceed immediately to step e.
e. Handle REPLAN_ESCALATE: If implementation-plan emits REPLAN_ESCALATE: brainstorm: add the story and its explanation to the replan-stories list (processed in step 2a below).
f. After re-planning completes (no REPLAN_ESCALATE), record resolution:
.claude/scripts/dso ticket comment <epic-id> "REPLAN_RESOLVED: implementation-plan — Tasks created for out-of-scope review feedback on story <story-id>."
2a. Handle collected REPLAN_ESCALATE stories — if any stories were added to the replan-stories list during step 2e above:
- Clear the accumulator:
batch_out_of_scope_findings = []
- Return to Phase C (Batch Preparation) to include the newly created tasks.
If batch_out_of_scope_findings is empty, proceed to the standard continuation decision below.
Standard Continuation Decision
Decision: Involuntary compaction detected? → Yes: P8 (Graceful Shutdown)
→ No: More ready tasks? → Yes: Return to P3
→ No: P6 (Validation)
Voluntary vs involuntary compaction: If ${TMPDIR:-/tmp}/sprint-compact-intent-<epic-id> exists, delete it and continue to Phase C. If no intent file exists, the compaction was involuntary — go to Phase I.
- If involuntary context compaction has occurred (no intent file) → Phase I (graceful shutdown)
- If more ready tasks exist (
.claude/scripts/dso ticket ready --epic=<epic-id>) → return to Phase C
- If no more ready tasks and some tasks are still blocked → report blocking chain, Phase I
- If all tasks are closed → Phase G is MANDATORY — proceed to Phase G (validation). Phase G has a HARD-GATE requiring completion-verifier dispatch (Phase G Step 2) before any other Phase G step executes. Do NOT skip the Phase G HARD-GATE.
Phase G: Post-Primary Ticket Validation (/dso:sprint)
Triggered when: all child tasks are closed (or all remaining are failed/blocked).
Do NOT execute any Phase G step until Step 2 (completion-verifier dispatch) has completed and returned an overall_verdict for the epic. Do NOT skip Step 2 because "all stories are closed" or "all tasks passed" — those are orchestrator-level observations, not independent verification. CLAUDE.md rule 24: the verifier exists because the orchestrator is biased toward confirming its own work.
Do NOT proceed to Step 3 (/dso:validate-work) or Phase I (Session Close) without the completion-verifier result. Phase G steps must execute in order: Step 2 → Step 3 → Step 4 → Step 5 → Step 6 → Step 7.
Step 1: Integration Test Gate, CI Verification, and E2E Tests
Read and execute prompts/epic-ci-and-e2e-gates.md for the integration test gate, CI verification, and E2E testing. After completing those steps, proceed to Step 2 below.
Step 2: Completion Verification (/dso:sprint)
MANDATORY: Read agents/completion-verifier.md inline and dispatch as subagent_type: "general-purpose" with model: sonnet and the epic ID (CLAUDE.md rule #24; inline dispatch is the required path — see CLAUDE.md Agent dispatch section).
overall_verdict: PASS → proceed to Step 3
overall_verdict: FAIL → STOP. Do NOT proceed to Phase H or epic closure under ANY circumstances. Create bug tasks from remediation_tasks_created and return to Phase C (Batch Preparation).
- Fallback (technical failure only): On timeout/unparseable JSON, log warning and proceed to Step 3.
Do NOT rationalize around a FAIL verdict (7c1d-9acf). The verifier's verdict is final — scope-scoping arguments ("pre-existing failures," "out-of-scope tests," "RED marker tolerance," "already tracked as a separate bug") do not override the FAIL → Phase C path. The orchestrator's judgment about whether the FAIL "really applies" is exactly the bias the verifier was designed to counteract. Only `overall_verdict: PASS` or technical failure (timeout/unparseable JSON) permits proceeding to Step 3.
On FAIL: the ONLY valid responses are (a) return to Phase C to create and complete remediation tasks, or (b) if the user explicitly says to stop the sprint (not "close the epic anyway"), escalate for sprint abort. Do NOT present FAIL findings with waiver arguments. Do NOT ask the user if criteria can be skipped. Do NOT proceed to Phase H.
Step 3: Run /dso:validate-work (/dso:sprint)
Before invoking /dso:validate-work, gather the changed files:
CHANGED_FILES=$(git diff --name-only main...HEAD 2>/dev/null || git diff --name-only HEAD~1..HEAD 2>/dev/null || echo "")
echo "$CHANGED_FILES"
Invoke /dso:validate-work. Append this context block (substitute actual file list):
### Sprint Change Scope
CHANGED_FILES:
app/src/agents/enrichment.py
app/src/api/status/status_routes.py
scripts/validate.sh
Interpret the report:
- All 5 domains PASS → proceed to Step 4
- Any domain FAIL → create remediation tasks and return to Phase C (Batch Preparation)
- Staging test SKIPPED (staging down) → proceed to Step 4 but note in the final report that staging was not verified
Step 4: Determine Epic Type (/dso:sprint)
Scan the epic description and child task titles for UI keywords:
- UI keywords:
template, page, route, component, CSS, frontend, upload, form, layout, button, HTML, style, responsive, modal, dialog
- Classification: If any UI keyword found → UI epic; otherwise → backend-only epic
Step 5: Gather Changed Files (/dso:sprint)
git diff --name-only main...HEAD
Step 6: Launch Epic-Specific Validation Sub-Agent (/dso:sprint)
Launch a Task tool with the appropriate subagent type:
- UI epic:
subagent_type="full-stack-orchestration:test-automator"
- Backend-only epic:
subagent_type="general-purpose" (use routing category test_write via discover-agents.sh to resolve the appropriate agent)
Validation Agent Prompt: Read and fill in the externalized prompt template:
REPO_ROOT=$(git rev-parse --show-toplevel)
Step 7: Parse Validation Output (/dso:sprint)
Extract the SCORE from the validation agent's output:
- Score = 5 → Phase I (completion)
- Score < 5 → Phase H (remediation)
Phase H: Remediation Loop (/dso:sprint)
Trigger: Epic validation score < 5 (from Phase G Step 7).
Read and execute prompts/remediation-loop.md for the full remediation protocol (gap classification, oscillation check, user confirmation, task creation, and safety bounds).
Stage-Boundary Exit Write
Before entering Phase I (Primary Ticket Closure), write the preconditions exit event for the sprint stage (fail-open):
_dso_pv_exit_write "sprint" "${_UPSTREAM_EVENT_ID:-}" "${SPEC_HASH:-}" "${primary_ticket_id:-}" || true
Phase I: Primary Ticket Closure (/dso:sprint)
Phase I delegates to /dso:end-session, which handles closing issues, committing, running merge-to-main.sh, and reporting.
On Success (Score = 5)
Pre-condition: Phase G Step 2 must have returned overall_verdict: PASS during this session. If the completion-verifier returned FAIL at any point and no remediation batch was executed after the FAIL (i.e., the FAIL was not addressed via Phase C re-entry), do NOT proceed with epic closure — return to Phase C to address the FAIL findings first.
FAIL is unconditionally blocking. If the completion-verifier returned overall_verdict: FAIL at any point (Phase F Step 18 story-level or Phase G Step 2 epic-level) and no subsequent remediation batch resolved the FAIL findings, do NOT proceed to epic closure. Do NOT:
- Present the FAIL verdict to the user with rationalizations
- Ask the user whether failing criteria can be waived
- Suggest that "most" criteria passing is sufficient
- Offer to close the epic with caveats
The only valid actions on FAIL are: (a) return to Phase C to address the findings, or (b) explicitly confirm with the user that they want to STOP the sprint entirely (not close the epic as "done").
Before closing the epic, confirm that dso:completion-verifier was dispatched at Phase G Step 2 with the EPIC ID (not a story ID) and returned overall_verdict: PASS during THIS session. Story-level verifier results from Phase F Step 18 do NOT satisfy this requirement — each story verifier runs against one story's done definition; only the epic-level verifier (Phase G Step 2) runs against all epic-level success criteria simultaneously. If Phase G Step 2 has not yet been dispatched for the epic, stop and return to Phase G Step 2 NOW. Do NOT proceed to epic closure until the epic-level verifier verdict is received.
-
Verify all changes are merged before closing the epic (399f-abad):
git merge-base --is-ancestor HEAD main
If this exits non-zero, do NOT close the epic — changes have not been merged to main. Run merge-to-main.sh first and resolve any conflicts before proceeding. Only close the epic after merge-base --is-ancestor exits 0. When merge-to-main.sh completes, proceed immediately to step 2 — merge-to-main.sh returning is NOT a sprint completion signal. Exception: if its output begins with ESCALATE:, stop and surface the escalation to the user per end-session Step 4.
-
Close the epic:
.claude/scripts/dso ticket comment <epic-id> "Epic complete: all tasks closed, validation score 5/5, branch merged to main"
.claude/scripts/dso ticket transition <epic-id> in_progress closed
After ticket transition completes, proceed immediately to step 3. Closing the epic ticket is NOT a sprint completion signal. The command emits REMINDER: Epic closed — run /dso:end-session to complete the sprint cleanly. — this REMINDER is purely informational; it is satisfied by steps 3–5 below. Do NOT relay it to the user or stop here.
-
Set sprint context for /dso:end-session report:
- Epic ID and title
- Total tasks completed this session
- Validation score: 5/5
-
Multi-sprint routing check — ask the user exactly this question before invoking session close:
Present to the user:
Epic is complete.
Is there another epic to sprint in this session, or should I close the session now?
- To sprint another epic:
/dso:sprint <next-epic-id>
- To close the session: reply "close" or just press Enter
Wait for the user's response:
- If the user provides a next epic ID or says they want to continue sprinting: print
/dso:sprint <next-epic-id> as a reminder and EXIT Phase I here. Do NOT invoke
/dso:end-session — the session is not ending.
- If the user replies "close", presses Enter, or gives no further epic to sprint:
proceed to step 5.
This question is a workflow routing decision, not permission-seeking for
/dso:end-session. Asking "Is there another epic?" is required. Asking "Would you
like me to run /dso:end-session?" is the sycophantic anti-pattern (c26f-be3f) that
is still prohibited.
-
Invoke /dso:end-session --bump minor via the Skill tool:
Skill({skill: "dso:end-session", args: "--bump minor"})
If version.file_path is not configured in dso-config.conf, the --bump minor flag is a no-op.
This MUST be done using the Skill tool — not interpreted as a bash command, not
printed as text, and not deferred for the user to run. The slash-command notation
above is a Skill tool invocation shorthand. Use the Skill tool directly.
Do NOT ask the user "Would you like me to run /dso:end-session?" — that phrasing is
the sycophantic permission-seeking anti-pattern (c26f-be3f). The multi-sprint routing
question in step 4 is the ONLY permitted user interaction at this point. If the user
chose to close the session (step 4), invoke /dso:end-session immediately — do not ask
again.
Closing the epic in step 2 and running merge-to-main.sh in step 1 do NOT complete
Phase I — they are prerequisites for /dso:end-session, not substitutes. Exiting
after steps 1–4 without invoking /dso:end-session (when the user chose session close)
is the specific anti-pattern this gate prevents (bug 89fe-bad1).
On Graceful Shutdown (Compaction, Failures)
- Do NOT launch new sub-agents
- Wait for any running sub-agents to complete
- Run final validation:
.claude/scripts/dso validate.sh --ci
- Update ALL in-progress tasks with checkpoint-format progress notes:
.claude/scripts/dso ticket comment <id> "CHECKPOINT:<phase-name>:SESSION_END — Progress: <summary>. Next: <what remains>."
Use the highest semantic checkpoint name actually reached (e.g., CHECKPOINT:implementation-done:SESSION_END, CHECKPOINT:review-passed:SESSION_END, CHECKPOINT:validation-passed:SESSION_END).
- Set sprint context for
/dso:end-session report:
- Tasks completed this session
- Tasks remaining (with IDs and titles)
- Resume command:
/dso:sprint <epic-id>
- Invoke
/dso:end-session via the Skill tool. Pass --bump minor if the epic reached Phase G completion-verifier PASS this session; omit --bump for incomplete sprints (no version bump earned):
Skill({skill: "dso:end-session", args: "--bump minor"}) # on success
Skill({skill: "dso:end-session"}) # on graceful shutdown
This MUST be done using the Skill tool — not interpreted as a bash command.
Do NOT ask the user whether to run /dso:end-session. Invoke it directly.