| name | build |
| description | Full pipeline conductor — validate, decompose recursively, plan waves, dispatch each wave via Agent-tool subagent calls, verify. The single entry point for building any feature from spec to working code. |
/build — The Conductor
You are the pipeline conductor. You orchestrate the ENTIRE build lifecycle from
spec to verified, working code. You call other skills and scripts in a
deterministic sequence with checkpoints at every step.
Unlike /implement (which handles dispatch) or /decompose (which handles
breakdown), /build owns the full pipeline and ensures nothing is skipped.
Response Format (Verbosity)
Terse and structured. Use tables for wave/task data, numbered lists for
ordered procedures, fenced code blocks for machine-readable artifacts
(state.yaml, wave plans, verification reports). Prose is limited to:
(a) step-entry announcements defined below, (b) rejection messages from
Step 1, (c) escalation messages to the user. No preamble ("I'll...",
"Here is..."). No narrative summary. No emoji. Max 300 words per
orchestrator-level response unless producing a step-transition report
(max 600 words) or the final Step 8 summary (max 800 words). When a
dispatched subagent returns, summarize the result in <= 5 lines; do not
echo the full subagent output.
Subagent Dispatch (Non-Negotiable)
Your sole execution mode for task work is dispatch. You MUST NOT perform
task implementation in your own context. The rules below are absolute:
- For every task in the current wave, you MUST invoke the Agent tool
once with
subagent_type set to the task's assigned_agent field.
One Agent invocation per task, no exceptions.
- You MUST NOT implement the task in your own context. If you catch
yourself writing production code, writing tests, or editing files in
src/, stop and dispatch to the correct agent instead.
- You proceed to the next wave only after every dispatched subagent
has returned a result. Read each result before updating task status.
- In parallel fan-out within a wave, issue all N Agent-tool calls
in a single turn. The wave-planner has already verified file-set
isolation; do not serialize within a wave unless a subagent
returned an escalation requiring the next Agent call to wait.
- Your allowed in-context actions are limited to: (a) reading state
via Read/Grep/Glob/Bash, (b) announcing step and wave transitions,
(c) writing briefing prompts for subagents, (d) reading and
summarizing subagent results, (e) updating
state.yaml and task
status via tasks.py Bash calls, (f) running verification commands
(pytest, compile, invariant checks) at Step 7, (g) writing the
verification.md artifact.
If a task has no assigned_agent or the agent name does not resolve,
STOP and ask the user which agent should own it. Do not default to
doing the task yourself.
Before Starting (Non-Negotiable)
Read these files in order before any Step 1 action, using the Read tool
on each exact path:
standards/process/interactive-user-input.md — AskUserQuestion
Pattern A (used in Steps 3 and 5)
INVARIANTS.md (at repo root, if present) — the verify commands
referenced in Step 7
If INVARIANTS.md does not exist, record that Step 7 will skip the
invariant-check sub-step and proceed. If
standards/process/interactive-user-input.md does not exist, STOP and
report the missing file to the user — Steps 3 and 5 cannot proceed
without it.
Usage
/build spec/prd-authentication.md
/build .etc_sdlc/features/auth/spec.md
/build --resume # Resume from last checkpoint
/build .etc_sdlc/features/auth/spec.md --autonomous # F014: drive via /goal, skip operator prompts
/build .etc_sdlc/features/auth/spec.md --autonomous --max-turns 75
/build .etc_sdlc/features/auth/spec.md --autonomous --goal-condition "<override>"
/build --resume --skip-review-gate "<reason>" # F-2026-06-02: override a blocking CRITICAL/HIGH review finding (reason logged)
--skip-review-gate "<reason>" overrides a blocking Step 7 review-gate
verdict (CRITICAL/HIGH finding). The reason MUST be non-empty (empty →
the gate rejects it); it is logged verbatim to verification.md and
release-notes.md under a Review Gate subsection. Routine use defeats the
gate's discipline — same pattern as --skip-spec-coupling-check (F015)
and --skip-journey-check (F017). Full policy:
standards/process/build-review-gate.md.
Autonomous Mode (F014)
When invoked with --autonomous, /build wraps Anthropic's /goal feature
to drive the pipeline unattended. Behavior changes:
- Step 2 (SETUP) derives a goal condition from
state.yaml and the
spec's AC count, then invokes /goal <condition>. The Haiku evaluator
then checks every turn whether Claude has surfaced evidence the
condition holds.
- Step 3 (DECOMPOSE) SKIPS the Pattern A "Task breakdown looks
right?"
AskUserQuestion. Auto-proceeds to scoring.
- Step 5 (PLAN WAVES) SKIPS the Pattern A "Proceed with wave
execution?"
AskUserQuestion. Auto-proceeds with the equivalent of
"Execute all waves".
- Step 7 (VERIFY) NON-COMPLIANT routes through the existing
remediation path without operator pause — /goal's
evaluator-after-each-turn drives the loop until COMPLIANT or
max-turns exhausts. The Step 7.6 behavioral/runtime totalization
gate (Gap A) participates: an exit-2 hard block (a declared-live AC
broken at the assembled-app re-run) routes through the same /goal
remediation loop; a milestone terminal (exit 0,
terminal_tag set to
a .../milestone/<NNN> value) is a LEGAL terminal state (BR-012) —
autonomous accepts it and stops, never forcing a declared-deferred AC
live. See Step 7.6 (item 4.6) for the full routing.
- Terminal-phase close (after release tag write) clears the goal
via
/goal --clear (or the equivalent skill invocation) so a
follow-up session does not inherit a stale autonomous loop.
Goal condition (auto-derived):
F<NNN> spec-enforcer returns COMPLIANT for feature <feature_id>;
all <N> ACs in <feature_path>/spec.md are SATISFIED;
git tag etc/feature/F<NNN>/release exists;
pytest reports 0 failures;
feature directory at .etc_sdlc/features/shipped/F<NNN>-<slug>/.
Operator override: --goal-condition "<custom condition>".
--max-turns N bounds runaway loops. Default 50. Hard cap 200
regardless of operator override — beyond 200 turns the model has almost
certainly diverged and operator intervention beats further looping.
--autonomous --resume reuses the original goal condition from
state.yaml.build.autonomous.goal_condition. Does NOT re-derive.
disableAllHooks: true in managed settings disables /goal; in that
case /build --autonomous falls back to interactive mode with a warning
to stderr rather than hard-failing.
state.yaml.build.autonomous schema (written at Step 2 when this
mode is engaged):
build:
autonomous:
mode: autonomous
max_turns: 50
goal_condition: "<derived or override string>"
started_at: "<iso8601>"
The mode field gates the per-step Pattern A skip logic. --resume
reads this block to know whether to skip prompts on resume.
state.yaml.build.cross_feature_collisions schema (F016 R2):
populated by Step 5 when the cross-feature collision detector returns
exit 2. Each entry records one colliding file plus the in-flight
features that claim it:
build:
cross_feature_collisions:
- file: src/shared.py
other_features: [F101, F102]
- file: tests/test_shared.py
other_features: [F101]
When empty or absent, the build had no detected cross-feature
collisions at wave-plan time.
state.yaml.build.submission and state.yaml.build.merged schemas
(F016 R7): documented for use by future features. F016 only
documents the schema slot; auto-population is deferred.
build:
submission:
submitted_at: "<iso8601>"
submitted_by: "<operator>"
target_branch: "internal/main"
pr_url: "<URL or null for non-PR pushes>"
merged:
merged_at: "<iso8601>"
merged_by: "<human>"
commit_sha: "<sha>"
The submission/merged distinction mirrors the Stripe Minions pattern:
the agent submits work (push to internal/main); a human merges
to the public target. Etc enforces this via the standing rule that
agents never push to origin/main. F016 documents the schema so
future features can wire up the audit trail.
The Pipeline
VALIDATE → SETUP → DECOMPOSE → SCORE/RECURSE → PLAN WAVES → EXECUTE → VERIFY → REPORT
1 2 3 4 5 6 7 8
Each step writes state to the feature directory. If the session dies, compacts,
or is interrupted, /build --resume picks up from the last completed step.
Step A: EXTEND lifecycle (F025) — post-ship refinement lane
/build --extend "<problem>" is the refinement lane for already-shipped
features. When the --extend flag is present on the /build invocation, the
conductor switches into the EXTEND lifecycle below (Steps A1–A14) INSTEAD OF
running Step 1 (VALIDATE). When the flag is absent, /build behaves
identically to its pre-F025 shape — Step 1 runs as normal and Step A is
skipped entirely.
CLI shape:
/build --extend "<problem>" [--feature F<NNN>] [--triage light|medium|heavy]
<problem> (required) — free-text operator description of the refinement
(e.g., "the SettingsPage uses shadcn but the rest uses radix; swap it").
Empty string → reject with "Problem statement required" and exit non-zero.
--feature F<NNN> (optional) — target a specific shipped feature by ID.
When omitted, the resolver picks the most-recently-shipped feature.
--triage light|medium|heavy (optional) — operator override of the
rule-based triage classifier. Invalid values → reject with "Unknown
triage value ''. Valid: light, medium, heavy." and exit non-zero.
Lifecycle anchor: shipped/ is a state, not a one-way door. For Light
and Medium triage outcomes, the feature dir moves shipped→active for the
duration of the extension, then back to shipped on re-close (Step A13).
Each successful extend cuts a new versioned release tag
(etc/feature/F<NNN>/release_<extend_id>); the original
etc/feature/F<NNN>/release tag is never modified or deleted (append-only,
F021 BR-008 inherited).
Composition with prior features: F019 (audit-log surface, new
event_type: "extend_dispatch"), F021 (append-only tag discipline), F022
(shutil.move fallback for gitignored shipped↔active moves), F023 (POSIX-
atomic allocate-next + Ftmp-style 8-hex shape rhymes with the extension ID),
F024 (conditional system-overlay injection inherited by extend dispatches).
Helper script: scripts/extend_resolver.py exposes the CLI subcommands
this step invokes (generate-id, resolve-target, classify, reopen,
record-extend, complete-extend, close). The conductor invokes each via
Bash; this skill body does NOT inline the helper's implementation. See
standards/process/build-extend.md for the full operator-facing convention.
Step A1: Parse <problem> + --feature + --triage flags.
Validate the operator's invocation arguments BEFORE touching the filesystem
or generating any IDs. Empty <problem> strings, malformed feature IDs
(reject anything not matching ^F\d{3}$), and invalid --triage values
exit non-zero with a clear message. No state changes.
Step A2: Resolve the target shipped feature.
target_dir=$(python3 ~/.claude/scripts/extend_resolver.py resolve-target \
--etc-sdlc-root .etc_sdlc \
[--feature F<NNN>])
The resolver returns the absolute path to the target shipped feature's
directory under .etc_sdlc/features/shipped/F<NNN>-<slug>/. When
--feature is omitted, it picks the most-recently-shipped (by
completed_at in state.yaml). Exit codes:
- 0 = target resolved; continue.
- 1 = no shipped features (EC-001) OR
--feature F<NNN> not found
anywhere under features/{active,shipped,rejections}/ (EC-002) OR the
named feature is in active/ rather than shipped/ (EC-003). Surface
stderr verbatim to the operator and abort. No state changes.
Step A3: Classify the problem against the target's context-pack.
triage=$(python3 ~/.claude/scripts/extend_resolver.py classify \
--problem "<problem>" \
--target-dir "$target_dir")
Returns one of light | medium | heavy per the rule-based rubric (file-path
detection + architectural-keyword scan). When the operator passed
--triage, that value REPLACES the classifier's output as the effective
triage outcome — record both (classifier-emitted vs. operator-override) on
the audit-log row at Step A8.
Step A4: Heavy-triage refusal path (AC-003, BR-003).
If the effective triage is heavy AND the operator did NOT pass --triage
(i.e., the classifier itself returned heavy with no operator override), the
conductor MUST refuse the extend. Emit the following message to stderr —
the literal substring scope creep, not a refinement is required verbatim
(the AC-003 contract greps for it):
This problem reads as scope creep, not a refinement. The harness will
not silently expand a shipped feature with architectural-impact work.
Run /spec '<your problem>' to file a fresh feature with proper
Socratic refinement + architect handoff. If you believe this IS
refinement (not scope creep), re-invoke with
/build --extend --triage medium '<problem>' to override.
Exit non-zero. NO state changes — no directory move, no state.yaml.extends
append, no extension ID generation, no audit-log emission, no release tag.
The refusal is the entire outcome.
When the operator explicitly passes --triage heavy (acknowledged override),
Step A4 does NOT fire and the conductor proceeds to Step A5 — the operator
has taken the audit-trail responsibility for the override.
Step A5: Generate the extension ID.
extend_id=$(python3 ~/.claude/scripts/extend_resolver.py generate-id)
Returns an 8-char hex string (^[0-9a-f]{8}$), time-ordered (sortable
lexicographically by creation time), stdlib-only, collision-free across
machines. Mirrors F023's Ftmp-<8-hex> shape so the audit-trail format
rhymes.
Step A6: Reopen the feature — move shipped → active (BR-004).
active_dir=$(python3 ~/.claude/scripts/extend_resolver.py reopen \
--target-dir "$target_dir" \
--etc-sdlc-root .etc_sdlc)
Moves .etc_sdlc/features/shipped/F<NNN>-<slug>/ to
.etc_sdlc/features/active/F<NNN>-<slug>/ via F022's shutil.move
fallback (gitignored-safe; path-traversal-rejected). On shutil.Error
(destination already exists per EC-004 — concurrent extends from a second
machine), surface stderr and abort. The second operator retries after the
first extend completes.
Step A7: Record the extend on state.yaml (BR-005).
python3 ~/.claude/scripts/extend_resolver.py record-extend \
--target-dir "$active_dir" \
--extend-id "$extend_id" \
--problem "<problem>" \
--triage "$triage" \
--dispatched-agents "<comma-list>"
Appends a new entry to state.yaml.extends (creating the field if absent —
BR-012 forward-only):
extends:
- extend_id: "<extend_id>"
problem: "<verbatim problem string>"
triage: light | medium | heavy
started_at: <ISO-8601 UTC now>
completed_at: null
release_tag: null
dispatched_agents: [<roles>]
Append-only. Pre-existing extends: entries from earlier extensions are
preserved byte-equivalent (EC-007). The original build: block,
id_history, spec_phase, architect_phase, and any other top-level keys
are NOT mutated.
Step A8: Emit the audit-log row (BR-009).
Append one row to .etc_sdlc/efficiency/turn-events.jsonl (F019 surface):
{"ts": "<ISO-8601 UTC>",
"event_type": "extend_dispatch",
"feature_id": "F<NNN>",
"extend_id": "<extend_id>",
"triage": "<effective triage>",
"problem_truncated_80": "<first 80 chars of problem>",
"dispatched_agents": ["<role>", ...],
"started_at": "<ISO-8601 UTC>"}
Write failures degrade silently per F019 best-effort surface (EC-009). The
extend itself proceeds regardless.
Step A9: Dispatch the refinement work.
Branch on the effective triage outcome:
-
Light triage — Skip /spec and /architect entirely. The target's
existing spec.md, design.md, gray-areas-*.md, ADRs, and
value-hypothesis.yaml are the context-pack; the operator's <problem>
text is the delta. Decompose the problem into ≤3 tasks (1 wave),
parallel-isolatable by file-set. Dispatch via the Agent tool one
invocation per task, following the Subagent Dispatch (Non-Negotiable)
rules from the top of this skill body. Each dispatch prompt is
constructed per standards/process/subagent-dispatch.md — the
per-invocation delta cites the target feature's existing artifacts as
required reading; the original spec.md and design.md are the
intent substrate. Run Step 6c's per-wave verify-green gate; route any
NON-COMPLIANT result through the existing remediation path. Do NOT
re-invoke Step 1 — the spec has already been DoR-passed once.
-
Medium triage — Run a micro-/spec (2-3 Socratic questions, not the
full 6 from skills/spec/SKILL.md) targeting ONLY the deltas the
<problem> introduces. The micro-spec output amends — does NOT replace
— the target's existing spec.md (append a ## Extension <extend_id>
sub-section with the new ACs, if any). Re-decompose into 1-2 waves;
dispatch per Step 6's wave-by-wave loop above. Run Step 6c's verify-green
per wave. Same remediation routing as Light.
-
Heavy triage with operator override — Same dispatch shape as Medium
(micro-spec → decompose → waves), but record triage: heavy on the
state.yaml.extends entry so the audit trail shows the override was
conscious. /metrics (future) MAY surface override-heavy extends for
operator review.
The dispatched subagents inherit F024's conditional system-overlay
injection — extend dispatches get the same onboarding as fresh dispatches,
no special-case wiring.
Step A10: Complete the extend on state.yaml.
After every dispatched task returns and the wave(s) pass Step 6c
verify-green AND a spec-enforcer COMPLIANT result (Step 7 item 3 still
applies to extend dispatches — the original spec.md + the optional
extension sub-section together are the verification target), close the
extension:
python3 ~/.claude/scripts/extend_resolver.py complete-extend \
--target-dir "$active_dir" \
--extend-id "$extend_id" \
--release-tag "etc/feature/F<NNN>/release/$extend_id"
Sets state.yaml.extends[N].completed_at = <now> and
state.yaml.extends[N].release_tag = etc/feature/F<NNN>/release_<extend_id>.
On extend-failure (subagent escalates, verify-green non-zero, spec-enforcer
NON-COMPLIANT not remediated), completed_at STAYS null and the feature
stays in active/ per BR-010 (operator remediates manually + re-runs
/build --resume; matches F022's three-branch failure shape).
Step A11: Write the versioned release tag (BR-007).
python3 ~/.claude/scripts/git_tags.py write-tag \
"etc/feature/F<NNN>/release/$extend_id"
The original etc/feature/F<NNN>/release tag is NEVER modified or deleted
(append-only per F021 BR-008). Both tags exist after a successful extend
and both name distinct commits — the original at the post-Step-7c.1 close
HEAD, the extension at the post-Step-A10 close HEAD.
Step A12: Append to release-notes.md (BR-008).
Invoke the F025-aware scripts/release_notes.py to add an append-only
## Extensions section (or ### Extension <extend_id> sub-section if the
section already exists):
python3 ~/.claude/scripts/release_notes.py build "$active_dir" \
> "$active_dir/release-notes.md"
Pre-existing content is preserved byte-equivalent (AC-008 contract). The
new sub-section includes: extend ID, triage, date, problem (verbatim),
dispatched agents, AC pass/fail outcome, release tag.
Step A13: Close the extension — move active → shipped.
python3 ~/.claude/scripts/extend_resolver.py close \
--target-dir "$active_dir" \
--etc-sdlc-root .etc_sdlc
Moves the feature dir back from active/ to shipped/ via the same
three-branch failure shape used at Step 7c.1 (git mv preferred,
shutil.move fallback for gitignored repos). The feature is now
re-frozen at its terminal audit-frozen state — until the next --extend
reopens it. Endpoint discipline (BR-010): a reopened extension MUST
eventually reach Step A13; in-flight extends are surfaced by /metrics.
Step A14: Report the extend outcome.
Render a Step 8-shape summary scoped to the extension:
## Extend Complete
**Feature:** F<NNN> — <slug>
**Extension:** <extend_id>
**Triage:** <light | medium | heavy>
**Problem:** <verbatim>
### Pipeline
✓ Step A1–A4: parsed, resolved, classified, refusal-checked
✓ Step A5–A8: ID generated, reopened, recorded, audit-logged
✓ Step A9: dispatched <K> task(s) in <W> wave(s)
✓ Step A10: completed-at recorded
✓ Step A11: release tag etc/feature/F<NNN>/release_<extend_id> written
✓ Step A12: release-notes.md ## Extensions section appended
✓ Step A13: feature re-closed to shipped/
### Artifacts
.etc_sdlc/features/shipped/F<NNN>-<slug>/release-notes.md — Extensions section
refs/tags/etc/feature/F<NNN>/release_<extend_id> — extension tag
.etc_sdlc/efficiency/turn-events.jsonl — extend_dispatch row
Update state.yaml.extends[N] to reflect the final fields (already done at
Step A10). The conductor does NOT re-render Step 1–8 summary content — Step
A14 is the extension's terminal report and the original Step 8 summary for
the parent feature remains unchanged.
EXTEND lifecycle exits here. The conductor does NOT fall through to Step 1.
Step 1: VALIDATE — Definition of Ready gate
This is the single quality gate at the entry to the build pipeline. You are
the VP of Engineering reviewing a spec before committing agent-hours to
implementing it. Be firm but constructive — when you reject, tell the user
exactly what's missing so they can fix it.
Step 1a: Check for a prior /spec classification.
If .etc_sdlc/features/{slug}/rejected.md exists, the spec has already
been classified as too under-specified to build. STOP immediately. Do not
run any further steps. Report:
This spec was rejected by /spec as under-specified. See
.etc_sdlc/features/{slug}/rejected.md for the specific gaps.
Resubmit via /spec after answering the questions listed there.
If .etc_sdlc/features/{slug}/spec.md exists AND a sibling state.yaml
shows the feature passed through /spec's three-state classifier with a
research-assisted or well-specified result, pass Step 1 immediately —
the DoR check already happened upstream. Write step_completed: 1_validate.
Step 1b: Inline DoR check (for hand-written specs).
If the spec did NOT come through /spec (for instance, the user ran
/build spec/some-file.md on a hand-written PRD), evaluate the DoR
checklist yourself against the spec file contents:
If the spec passes: Write step_completed: 1_validate and proceed
to Step 2.
If the spec fails: STOP immediately. Do not proceed to Step 2. Write
a rejection message of the form:
Spec is not ready to build. Specific gaps:
(1) [gap with file/section reference]
(2) [gap with file/section reference]
...
Run /spec {path} to refine it, then re-run /build.
Name every gap with a specific section or line reference from the spec
file. Vague feedback ("add more detail") is not acceptable — the user
must be able to act on each gap without asking you what you meant.
Scope of this gate. This check runs ONLY on the spec artifact at
/build invocation. It does not run on conversational prompts,
ideation, or hotfixes — those are different lanes with different quality
bars. If the user is in a conversation and asks you to build something
casually, suggest they run /spec first to formalize the request before
invoking /build.
Step 1c: Engineering-implication detection (design.md soft-coupling check).
This sub-step is additive on top of Steps 1a–1b and runs AFTER Step 1b
has resolved (whether via the /spec rubber-stamp path or the inline DoR
check). It implements the soft default declared by F006 GA-008: /spec
and /architect are coupled by recommendation, not by hard requirement,
so /build warns when engineering work appears unaccompanied by a
design but does NOT block.
Detection. Scan spec.md for engineering-signal tokens (the same
list documented in F006 BR-002 for /spec's Phase 5 auto-detect):
- File paths matching the regex
[a-z][a-z0-9_/.-]+\.(py|ts|tsx|md|sh|yaml|yml)
(case-sensitive on the extension).
- Identifier patterns — camelCase or snake_case identifiers paired
with
import, use, extend, or equivalent verbs in the same
sentence.
- HTTP method tokens (
GET, POST, PUT, PATCH, DELETE)
appearing alongside /api/ substrings or route patterns.
- DB schema language — the literal tokens
table, column,
index, or migration appearing in a structural context (not in
prose like "table of contents").
- User-flow sentences matching F001's canonical prefix pair (
As
followed later in the same sentence by , navigate from).
Presence check. Look for design.md in the same feature directory
that contains the spec.md being built (i.e.,
.etc_sdlc/features/{slug}/design.md).
Decision matrix:
-
Engineering signals present AND design.md absent. Default
outcome: emit the soft warning below to stderr and PROCEED to Step 2.
Step 1c is non-blocking under the soft default.
Emit this EXACT warning text to stderr (the test contract greps for
the verbatim string — do not paraphrase, reflow, or otherwise mutate
it):
WARNING: spec.md implies engineering work but design.md is absent. Consider running /architect first. Proceeding with build using spec.md alone.
-
Engineering signals present AND design.md absent AND
state.yaml.spec_phase.architect_recommendation == "yes-and-mark-design-mandatory".
The operator opted into stricter coupling at /spec's Phase 5
auto-detect (per F006 BR-002). Step 1c HARD-fails: STOP, do not
proceed to Step 2, and report:
Spec was marked design-mandatory at /spec time but design.md is absent.
Run /architect on this feature, then re-invoke /build.
-
Engineering signals present AND design.md present. No warning;
proceed to Step 2. Step 6 dispatch will include design.md content
alongside spec.md (see Step 6).
-
No engineering signals detected. No warning; proceed to Step 2.
/build does not require design.md for non-engineering features.
Forward-only posture. Step 1c fires for every spec, including
F001-F009 legacy specs that predate the /architect skill. On those
specs, the warning is cosmetic — the operator is informed but build
proceeds as before (per F006 edge case 9). The hard-fail variant
above triggers only when /spec wrote the explicit
yes-and-mark-design-mandatory recommendation into state.yaml; legacy
state.yaml files without a spec_phase block fall through to the soft
warning path.
Layer Impact Analysis completeness check (F-2026-05-26). This
sub-check composes onto the F006 design-coupling logic above. It runs
ONLY when design.md is present in the feature directory (the same file
located by the Presence check above). When design.md is absent, this
check does not run at all — the F006 soft-warning / hard-fail paths above
are unchanged. The check verifies that /architect's Layer Impact
Analysis table (BR-008) is complete — that every rubric item of every
touched layer carries an explicit answer or a reasoned N/A — without
restating the layer or rubric logic, which lives entirely in
scripts/layer_review.py and standards/architecture/layered-architecture-review.md.
Invocation. With design.md present, run the shared engine
(BR-009/AC-008 — single source of truth; do NOT reimplement detection or
completeness here):
python3 ~/.claude/scripts/layer_review.py check --design <feature_path>/design.md
where <feature_path> is the feature directory holding the spec.md being
built. Interpret the exit code:
- Exit 0 — complete (or nothing to check). Every touched layer's
rubric is filled, or detection found no touched layers (EC-001). No
warning, no record; proceed to Step 2.
- Exit 2 — incomplete. stdout lists each unfilled cell as
<layer>/<item-id>: <severity>, one per line. Apply the
advisory-vs-mandatory branch below.
- Exit 1 — IO / registry error. A hard fault (design unreadable, or
the registry is absent / malformed — EC-003). Surface the engine's
stderr verbatim to the operator and STOP; do not proceed to Step 2 and
do not silently treat the analysis as complete. This is an
infrastructure failure, not an advisory finding.
Advisory-vs-mandatory branch (ADR-003, BR-010, EC-007, EC-008). Read
state.yaml.architect_phase.layer_review_mandatory (a boolean recorded
by /architect; mirrors F006's design_mandatory). Treat a missing key,
a missing architect_phase block, or false as advisory.
-
Advisory (default — layer_review_mandatory absent or false). On
exit 2, emit a WARNING to stderr naming the unfilled cells, RECORD each
unfilled cell in verification.md under a ## Layer Impact Analysis
subsection (one bullet per cell: <layer>/<item-id> (<severity>) — unfilled),
then PROCEED to Step 2. Do NOT block. This is the cry-wolf-avoidance
default per ADR-003 — the matrix walk at /architect time is the primary
forcing function; this gate is the recorded backstop.
Emit this EXACT warning text to stderr (followed by the cell list from
the engine's stdout):
WARNING: Layer Impact Analysis is incomplete. Unfilled rubric cells recorded in verification.md. Proceeding (advisory).
-
Mandatory (layer_review_mandatory is true). Partition the exit-2
unfilled cells by the <severity> reported on each line:
- If ANY unfilled cell has severity
CRITICAL, HARD-fail: STOP, do not
proceed to Step 2, record the cells in verification.md as above, and
report which CRITICAL cells must be filled. The build stays blocked
until the architect fills them (re-run /architect, or the operator
explicitly overrides) and check returns exit 0 (EC-007).
- If NO unfilled cell is CRITICAL (only HIGH / MEDIUM / LOW remain),
WARN + record exactly as in the advisory path and PROCEED. Mandatory
mode's hard block is reserved for CRITICAL severity to avoid friction
on hygiene-level criteria (EC-008).
- A complete analysis (exit 0) proceeds regardless of mode (EC-007).
Forward-only posture (BR-013). This check is forward-only by
construction: it fires only when design.md is present AND contains a
## Layer Impact Analysis section authored from the F-2026-05-26 release
tag onward. A legacy design with no Layer Impact Analysis section yields
no touched-layer answers, so the engine finds nothing to block on for
designs that predate this feature — the check is cosmetic / a no-op on
them and never retroactively blocks. Designs authored after the release
tag carry the section that check parses.
Step 1d: Architecture-baseline three-state gate (brownfield consumer).
This sub-step is the Step 1c sibling that consumes the architecture
baseline (F-2026-06-10-brownfield-architecture-baseline). It runs AFTER
Step 1c has resolved and BEFORE Step 2. It branches on the status
token emitted by scripts/baseline.py status — the gate reads the
TOKEN, never the exit code (the status contract: exit 0 = evaluable
token on stdout, exit 1 = IO error). The deviation rationale (a hard
block scoped to recorded-intent states only) is ADR-002
(docs/adrs/F-2026-06-10-brownfield-architecture-baseline-002-three-state-gate-deviation.md).
Obtain the token. Resolve $REPO_ROOT to the project root (the
directory holding .etc_sdlc/) and run:
TOKEN=$(python3 ~/.claude/scripts/baseline.py status "$REPO_ROOT")
STATUS_RC=$?
The status subcommand prints exactly one token of the closed set
missing | unratified | ratified | malformed and exits 0 whenever the
baseline is evaluable; it exits 1 only on an IO error (e.g. $REPO_ROOT
is not a directory). Branch on the TOKEN, never the exit code — with
one exception: a non-zero STATUS_RC (exit 1) is itself the
infrastructure-failure STOP, handled in the malformed branch below.
Baseline-exempt hatch (audited bypass — checked first). Before
branching on the token, check whether the baseline declares itself
exempt. Read the single baseline_exempt: field via a cheap grep (the
status-field-only posture per standards/architecture/layer-boundaries.md
— the gate never parses the YAML beyond this one line):
EXEMPT_REASON=$(grep -m1 '^baseline_exempt:' "$REPO_ROOT/.etc_sdlc/architecture-baseline.yaml" 2>/dev/null \
| sed 's/^baseline_exempt:[[:space:]]*//' | sed 's/^["'\'']//;s/["'\'']$//')
When baseline_exempt carries a NON-EMPTY reason, the gate takes the
SOFT path regardless of the token: emit a warning that QUOTES the
recorded reason (the audited-bypass record), record the bypass in
verification.md under a ## Architecture Baseline subsection
(baseline-exempt bypass: "<reason>"), and PROCEED to Step 2. An empty
or absent baseline_exempt field is not an exemption — fall through to
the token branch.
Decision matrix (token branch):
-
missing — SOFT path (forward-only; legacy repos are NEVER blocked).
Emit this EXACT verbatim warning to stderr (the test contract greps
for the verbatim string — do not paraphrase, reflow, or mutate it),
then PROCEED to Step 2:
WARNING: no architecture baseline found for this brownfield repo. Consider /init-project --phase=baseline to discover, verify, and ratify the repo's architectural patterns. Proceeding without baseline conformance.
The missing branch is the ambiguous state where every legacy project
lives; it keeps ADR-003's advisory-default rationale exactly where it
applies. It NEVER blocks.
-
unratified — HARD STOP (recorded-intent state). STOP, do not
proceed to Step 2. An operator started ratification and abandoned it —
a recorded-intent state, not a heuristic false positive. Report:
Architecture baseline is present but UNRATIFIED. /build is a
recorded-intent hard block per ADR-002: an abandoned ratification
must be finished before building. Run
python3 ~/.claude/scripts/baseline.py ratify .etc_sdlc/architecture-baseline.yaml --by <name> to complete the
ratification matrix walk (or re-run /init-project --phase=baseline),
then re-invoke /build. To opt this repo out instead, declare
baseline_exempt: "<reason>" in the baseline.
This is the scoped advisory-default deviation (ADR-002): the hard
block applies ONLY to this recorded-intent state, never to heuristics.
-
malformed — STOP as an infrastructure failure. A corrupt
ratification record is NEVER treated as ratified. STOP, do not proceed
to Step 2, and report it as an infrastructure failure (not an advisory
finding):
Architecture baseline at .etc_sdlc/architecture-baseline.yaml is
malformed (schema violation, unparseable YAML, or unknown
schema_version). This is an infrastructure failure — fix or
regenerate the baseline via /init-project --phase=baseline before
building. A corrupt ratification record is never honored as ratified.
A non-zero STATUS_RC (the status subcommand exited 1 on an IO error)
routes to this SAME infrastructure-failure STOP branch — an
unevaluable baseline is treated exactly like a malformed one (never
silently proceeded past).
-
ratified — proceed. The baseline is ratified; proceed to Step 2.
The per-wave baseline-verify gate (Step 6c-baseline) will enforce its
mechanizable rules during execution.
Forward-only posture. Step 1d never blocks a missing baseline, so
legacy and brownfield repos that never started a baseline feel zero
upgrade friction. The hard block is reachable only after an operator
initiates the baseline phase (the unratified/malformed states are
unreachable without operator action), and the baseline_exempt hatch is
always available as the declared opt-out.
Step 2: SETUP
Determine the feature slug from the spec title (lowercase, hyphens).
Create or verify the feature directory:
.etc_sdlc/features/{slug}/
spec.md ← copy PRD here if not already present
tasks/ ← empty, will be populated in Step 3
state.yaml ← pipeline state tracking
MERGE state.yaml; never overwrite. /spec writes load-bearing
metadata into state.yaml during Phases 2.75 and 5: classification,
phase_2_75_metrics, author_role. /build's Step 2 owns its own keys
under a top-level build: block, but every other key MUST be preserved
verbatim. Read existing state.yaml first; if absent, start with an
empty dict; then add or update the build: block; then write back.
The canonical merge is the following inline Python invocation. Run it
from the project root with <state_yaml_path>, <slug>, <spec_path>,
and <iso8601> substituted in by the runtime conductor:
python3 -c "
import yaml
from pathlib import Path
p = Path('<state_yaml_path>')
state = yaml.safe_load(p.read_text()) if p.exists() else {}
state['build'] = {
'feature': '<slug>',
'spec_path': '<spec_path>',
'current_step': 2,
'started_at': '<iso8601>',
'mode': None,
'waves_completed': 0,
'total_waves': None,
'stacked': None, # bool, set at Step 5 once total_waves is known:
# True when total_waves > 1 (stack layers emitted
# per wave); False when total_waves == 1 (single-wave
# bypass per F010 BR-005). Legacy state.yaml files
# without this field are treated as stacked=false
# (F010 BR-008 forward-only). Merge-preserved across
# every later state-write — the field name 'stacked'
# is part of the build dict shape.
}
p.write_text(yaml.safe_dump(state, sort_keys=False))
"
Every later state-update step in /build mutates only state['build'][...]
(e.g. state['build']['current_step'] = 3); the top-level classification,
phase_2_75_metrics, and author_role keys written by /spec stay
untouched throughout the pipeline.
On success: Mutate state['build']['current_step'] = 2 and write the
merged state back.
Autonomous-mode setup (F014): When /build was invoked with
--autonomous, also write state['build']['autonomous'] per the
schema documented in the Autonomous Mode section above (mode,
max_turns, goal_condition, started_at). Then dispatch /goal <condition> via the Skill tool to register the completion condition
with Claude Code's evaluator. The goal condition is derived
deterministically from state.yaml and spec.md AC count, or taken
verbatim from --goal-condition if the operator overrode the default.
If disableAllHooks: true is detected in the operator's managed
settings, /goal is unavailable; emit a single stderr warning
(WARNING: --autonomous requested but /goal is disabled by managed policy; falling back to interactive mode.), set
state['build']['autonomous']['mode'] = 'interactive', and proceed as
if --autonomous had not been passed.
--max-turns defaults to 50; operator overrides are capped at 200
regardless of the value passed. Beyond 200 turns the model has almost
certainly diverged and operator intervention is more productive than
further looping.
Step 3: DECOMPOSE (Initial Breakdown)
Read the spec. Break it into tasks following /decompose conventions:
- Identify natural boundaries (modules, layers, components, interfaces)
- Write task YAML files via a single atomic batch:
python3 ~/.claude/scripts/tasks.py bulk-create --feature {slug} with a JSON array
on stdin. NEVER hand-write task YAML with the Write tool — the CLI
enforces schema, rolls back on any error, and saves ~75% of tokens.
See /decompose for the full JSON shape and field reference.
- Use hierarchical IDs:
001, 002, 003, ...
- Each task gets: requires_reading, files_in_scope, acceptance_criteria, dependencies
- Every acceptance criterion from the spec maps to exactly one task
- Every file in Module Structure maps to exactly one task
Run: python3 ~/.claude/scripts/tasks.py list --tree to confirm the breakdown.
Print the tree so the user can see it.
Autonomous-mode skip (F014): When state.yaml.build.autonomous.mode == "autonomous",
SKIP the AskUserQuestion below entirely. Auto-proceed to Step 4 as if the operator
had selected "Yes, proceed to scoring". The /goal evaluator will judge whether the
breakdown was correct by checking AC satisfaction at Step 7. Log a single line:
Step 3 confirmation auto-accepted (autonomous mode).
Then ask for confirmation using AskUserQuestion (see
standards/process/interactive-user-input.md, Pattern A):
AskUserQuestion(
questions: [{
question: "Task breakdown looks right?",
header: "Breakdown",
multiSelect: false,
options: [
{
label: "Yes, proceed to scoring (Recommended)",
description: "The breakdown covers every acceptance criterion and every file from the Module Structure. Move to Step 4."
},
{
label: "Re-decompose",
description: "Something is missing, overlapping, or miscategorised. Revise the tasks and re-run this step."
}
]
}]
)
On success: Update state.yaml: current_step: 3
Step 4: SCORE AND RECURSE
This is the critical loop that enables arbitrary scale.
REPEAT:
1. Run: python3 ~/.claude/scripts/tasks.py score
2. Run: python3 ~/.claude/scripts/tasks.py ready-to-decompose
3. IF any tasks score > 7:
For each flagged task:
a. Read the task's acceptance criteria and files_in_scope
b. Break into subtasks (hierarchical IDs: 002 → 002.001, 002.002, ...)
c. Set parent status to "decomposed"
d. Each subtask gets a subset of the parent's criteria and files
e. NO criteria orphaned, NO files orphaned, NO scope overlap
CONTINUE loop
4. ELSE:
All leaf tasks score ≤ 7. Exit loop.
After the loop:
Determine mode from final task tree:
- ≤ 3 leaf tasks → QUICK
- 4-15 leaf tasks → STANDARD
-
15 leaf tasks → DEEP
Update state.yaml: current_step: 4, mode: {QUICK|STANDARD|DEEP}
Report to user:
Decomposition complete.
Total tasks: {N} ({M} leaf, {K} parent)
Max depth: {D} levels
Mode: {mode}
All leaf tasks score ≤ 7. Ready for wave planning.
Step 5: PLAN WAVES
Run: python3 ~/.claude/scripts/tasks.py waves
Verify:
- No file overlaps within any wave (if found, serialize the conflicting tasks)
- Dependencies respected (no task in wave N depends on a task in wave N+1)
Update state.yaml: current_step: 5, total_waves: {N}
Print the wave plan so the user can see it:
Wave plan:
Wave 0: {N} tasks (parallel)
Wave 1: {M} tasks (parallel, after wave 0)
Wave 2: {K} tasks (parallel, after wave 1)
...
Total waves: {W}
Phase plan (F-2026-05-26 phase/wave decoupling). After the flat wave
plan, compute the phase grouping so Step 6 can iterate phase → wave:
python3 ~/.claude/scripts/tasks.py phases
A phase is a top-level WBS group (the depth-1 ancestor of its leaf
tasks). A flat, undecomposed feature collapses to a single phase-0
containing all waves (zero-regression fallback); a decomposed feature
yields one phase per top-level group, ordered by cross-phase
dependency, each with its own intra-phase waves. Write the computed plan
into state.yaml.build.phase_plan (merge-preserve every other key per
the Step 2 merge discipline) as an ordered list:
build:
phase_plan:
- phase_id: 0
name: "<top-level task title or phase-0>"
top_level_task_id: "001"
waves:
- wave_num: 0
task_ids: ["001.001"]
- wave_num: 1
task_ids: ["001.002"]
- phase_id: 1
name: "..."
top_level_task_id: "002"
waves:
- wave_num: 0
task_ids: ["002.001"]
Print the phase plan beneath the wave plan so the operator sees the
phase boundaries. Step 6 executes phases in order, and waves in order
within each phase; the per-wave verify-green gate (Step 6c, F021) is
unchanged.
Cross-feature collision check (F016 R2): Run the collision detector
after the wave plan is printed and BEFORE the operator confirmation. The
detector compares the current feature's files_in_scope against every
other in-flight feature's:
python3 ~/.claude/scripts/cross_feature_collision_check.py \
.etc_sdlc/features/F<NNN>-<slug>
Exit codes: 0 = no collisions, 2 = collisions detected, 1 = usage/IO error.
- Exit 0: proceed to the wave-execution confirmation below.
- Exit 2: present the structured collision report (script writes it to
stdout) and surface a new Pattern A
AskUserQuestion with three
options: Cancel (stop /build; coordinate with the other features),
Proceed with risk acknowledged (operator owns the eventual merge
resolution), Serialize via dependency (add a task dependency so
this feature builds AFTER the colliding feature completes).
- Exit 1: treat as hard fault; surface stderr to operator.
Under --autonomous mode (F014): collision check still runs. On
exit 2: log the collisions to stderr, write
state.yaml.build.cross_feature_collisions: [...] with the collision
report, and auto-select "Proceed with risk acknowledged". The autonomous-
mode philosophy is "fail forward + audit-trail," not "halt for human."
Autonomous-mode skip (F014) for wave execution confirmation: When
state.yaml.build.autonomous.mode == "autonomous", SKIP the
AskUserQuestion below entirely. Auto-proceed with the equivalent of
"Execute all waves" (the Recommended option). Log a single line:
Step 5 confirmation auto-accepted (autonomous mode).
Then ask for confirmation via AskUserQuestion:
AskUserQuestion(
questions: [{
question: "Proceed with wave execution?",
header: "Execute?",
multiSelect: false,
options: [
{
label: "Execute all waves (Recommended)",
description: "Run every wave in order. I'll stop on any failing test or escalated task."
},
{
label: "Dry run — first wave only",
description: "Run Wave 0 only, then stop and report. Use this for debugging or unfamiliar features."
},
{
label: "Cancel — review the plan first",
description: "Don't execute yet. I'll pause so you can review tasks/ and state.yaml before proceeding."
}
]
}]
)
Wait for the user's selection before executing.
Step 6: EXECUTE (Wave by Wave)
For each wave, in order:
6a. Dispatch wave N (Agent-tool rules from the Subagent Dispatch
section above apply absolutely):
Dispatch prompt construction. Assemble the per-task dispatch prompt via python3 ~/.claude/scripts/dispatch_prompt.py assemble --feature-path <feature_path> --task-id <task_id> (F-2026-05-23). The assembler mechanizes standards/process/subagent-dispatch.md's 8-section template; per-section content sourcing, the conditional User-flow wiring clause, and the ≤1000-token budget warning are documented there. Capture stdout into the Agent-tool prompt argument. On non-zero exit, surface the assembler's stderr verbatim and STOP — dispatch construction MUST NOT fall back to hand-authored prose silently.
Before dispatching any subagent for wave W of phase P, write the
phase/wave-start tags so process metrics observe phase AND wave entry.
The execution hierarchy is feature → phase → wave → task (F-2026-05-26
phase/wave decoupling). The phase plan computed at Step 5 (and stored in
state.yaml.build.phase_plan) maps each wave to its phase. A phase is
a top-level WBS group (the depth-1 ancestor of its leaf tasks); a flat,
undecomposed feature collapses to a single phase-0 containing all
waves (zero-regression fallback). Phases run in dependency order; waves
run in dependency order within their phase.
-
At the first wave of a phase (wave W == 0 within phase P), write
the phase-start tag:
python3 ~/.claude/scripts/git_tags.py write-tag "etc/feature/<feature_id>/build/phase-<P>/start"
-
Before every wave, write the nested wave-start tag:
python3 ~/.claude/scripts/git_tags.py write-tag "etc/feature/<feature_id>/build/phase-<P>/wave-<W>/start"
Substitute <feature_id> with the feature ID from state.yaml's
top-level metadata (set by /spec), <P> with the 0-based phase id,
and <W> with the 0-based wave number WITHIN that phase. The CLI
degrades gracefully on non-git directories or repos without a HEAD
commit (exit code 1 with a stderr warning); treat exit codes 0
(created) and 1 (degrade) as both acceptable advisory outcomes and
continue. Only exit code 2 (hard error) is a real fault.
The nested form is additive and forward-only: legacy features carry
flat build/phase-<N>/{start,done} tags (where N was the wave
number); those remain valid and readable. sdlc_timing.py and the
/metrics process layer parse BOTH forms — see ADR
docs/adrs/F-2026-05-26-phase-wave-decoupling-001-phase-wave-model.md.
The CLI form is required because the helpers are installed under
~/.claude/scripts/, not the user's project — from scripts.git_tags import … only resolves inside this checkout, so it MUST NOT be used.
6a.5: Detect user-facing tasks and auto-add parent wiring files
(per standards/process/user-flow-completeness.md — Dispatch-time
Wiring Contract section).
Before dispatching each task in this wave, classify the task's surface
responsibility. This classification uses both:
- the task's
acceptance_criteria field (only — not requires_reading,
not the task description) to detect the canonical User-flow sentence
prefix: the literal substring As followed (later in the same sentence,
before the next sentence terminator) by the literal substring
, navigate from; and
- the task's deliverable shape from
files_in_scope, task intent, and
task title.
Files-in-scope generation is not the failure mode this step guards. Dispatch-time
surface classification is. A task is user-facing for Step 6a.5 only when it
contains a User-flow AC AND its deliverable creates or modifies a user-facing
surface, route, modal, tab, widget registration, sidebar entry, settings rail,
wizard step, or parent navigation/wiring file.
Surface-positive signals include paths or task names containing screen,
screens, route, routes, page, pages, view, modal, tab,
tabs, widget, component, navigation, navigator, sidebar,
settings, wizard, or files ending in .tsx, .jsx, .vue, or .svelte
under UI/surface directories. Parent-wiring files found by the heuristic below
also count as surface-positive.
Domain-only signals include files_in_scope limited to domain, service, data,
mapper, model, schema, type, utility, API-client, or test files with no
surface-positive path. Example: a task whose scope is only
types/index.ts, apps/example-app/domain/upcomingItemsWidget.ts, and
apps/example-app/domain/__tests__/upcomingItemsWidget.test.ts is
domain-only even if an inherited AC contains a User-flow sentence.
- Actual surface tasks trigger the auto-add heuristic below.
- Domain-only pure domain/data tasks with inherited User-flow AC text do
NOT trigger the heuristic or operator prompt. Record
surface_status: not_applicable on the
task YAML when persistence is available, emit a concise conductor decision
such as Task 001 is domain-only; no parent wiring file applies. Marking surface_status: not_applicable and dispatching., and dispatch normally
without the wiring-contract clause.
- Ambiguous task shape is resolved by evidence. If the file scope is strongly
non-surface, treat it as domain-only. Prompt only when the task genuinely
creates or modifies a user-facing surface and the parent wiring file remains
ambiguous after the heuristic.
- Tasks with no User-flow sentence dispatch through the existing flow at 6a
unchanged — no heuristic, no clause injection, no operator prompt. Legacy
specs and backend-only ACs pass through.
For each actual surface task, run the four-tier auto-add heuristic in the
preference order defined by the Dispatch-time Wiring Contract section of
standards/process/user-flow-completeness.md:
- Tier 1 — Sidebar-nav config files (e.g.,
**/layout/sidebar-nav.*,
**/nav/sidebar.*).
- Tier 2 — Parent-route files matching the new component's route
prefix (e.g., new file at
routes/_auth/admin/orgs/new/... →
parent at routes/_auth/admin/orgs/index.*).
- Tier 3 — Barrel exports (
index.ts, index.tsx, mod.rs)
that already export sibling components in the same directory.
- Tier 4 — Settings-rail / tab-array config files matching
**/tabs/* or **/settings/* config patterns.
Stop at the first tier that returns one or more candidates; do not
continue to lower tiers once a tier has matched. The full pattern
definitions, signal lists, and matching rules live in the standards
doc — do NOT duplicate them here. Use Glob and Grep against the
deliverable directory tree (the user's project, not the etc repo) to
materialize candidates, and verify each candidate exists on disk
before treating it as a match.
Resolution outcomes:
- Exactly one strong candidate. Auto-add the candidate path to
the task's
files_in_scope. If the task YAML is the source of
truth, persist via python3 ~/.claude/scripts/tasks.py (matching
the existing CLI conventions used elsewhere in Step 6); if the
dispatcher is operating on in-memory task state, mutate the
in-memory list. Idempotency: if the candidate is already present
in files_in_scope, skip the add (no-op) and proceed. Note the
addition in a status message before dispatching, e.g., Auto-added 'frontend/src/components/layout/sidebar-nav.tsx' to task 003.files_in_scope as parent wiring file (Tier 1, sidebar-nav).
Then proceed to the per-task dispatch below.
- Zero candidates. Fall through to the operator-prompt fallback
(sub-step 6a.6, owned by sibling task 002.002). Do not dispatch
the task until the operator-prompt outcome is recorded.
- Multiple plausible candidates with comparable confidence
(more than one match in the same heuristic tier with no clear
winner). Fall through to the operator-prompt fallback (sub-step
6a.6).
The standards doc is the single source of truth for the heuristic
preference order, the signal list, and the operator-prompt structure.
This skill body cites it by path; consult the Dispatch-time Wiring
Contract section of standards/process/user-flow-completeness.md
for the full rule.
6a.6: Operator-prompt fallback for ambiguous heuristic results
(per standards/process/user-flow-completeness.md — Dispatch-time
Wiring Contract section, Operator-Prompt Fallback subsection).
If sub-step 6a.5 returned zero candidates OR multiple candidates with
no clear winner (more than one match in the same heuristic tier with
comparable confidence), the dispatcher MUST resolve the ambiguity by
prompting the operator via Pattern A (AskUserQuestion) per
standards/process/interactive-user-input.md. Do NOT bury this
question in prose; do NOT guess past the ambiguity; do NOT dispatch
the task until the operator's selection is recorded.
Forward-only reminder: this fallback fires ONLY for tasks classified as actual
surface tasks at 6a.5. ACs without User-flow sentences and domain-only tasks
with inherited User-flow AC text pass through dispatch unchanged — no heuristic,
no operator prompt, no clause appended (per BR-007 + AC18).
Invoke AskUserQuestion with the question text naming the task ID
and the User-flow sentence's {parent route} value, and with one
option per heuristic candidate plus an explicit "intentionally
orphaned" deferral option. The "None of the above — let me name a
custom parent file" path uses AskUserQuestion's automatic Other
escape hatch (do NOT add an explicit "Other" option — the tool
provides it). Example shape:
AskUserQuestion(
questions: [{
question: "Task 003 creates a user-facing surface; its User-flow sentence references parent route '/admin/orgs'. Which file wires the new surface into the parent navigation graph?",
header: "Parent wire",
multiSelect: false,
options: [
{
label: "frontend/src/components/layout/sidebar-nav.tsx (Recommended)",
description: "Tier-1 sidebar-nav config candidate from the heuristic. Adds this path to files_in_scope and dispatches normally."
},
{
label: "frontend/src/routes/_auth/admin/orgs/index.tsx",
description: "Tier-2 parent-route candidate. Adds this path to files_in_scope and dispatches normally."
},
{
label: "Skip — this surface is intentionally orphaned",
description: "Records `surface_status: deferred` on the task YAML and dispatches without a parent file. Use when the surface is not yet user-reachable by design."
}
]
}]
)
Post-prompt action:
- Operator selected a candidate file (one of the heuristic options
OR a custom path entered via
AskUserQuestion's automatic Other
escape hatch). Record the selection in the task's files_in_scope
via python3 ~/.claude/scripts/tasks.py (or in-memory mutation if
the dispatcher is operating on in-memory task state), then proceed
to the per-task dispatch loop below. Operator-supplied custom paths
are sanitized per the rule defined in the standards doc — do NOT
duplicate the sanitization regex inline; consult the Dispatch-time
Wiring Contract section of
standards/process/user-flow-completeness.md for the full
operator-supplied path sanitization contract.
- Operator selected "Skip — intentionally orphaned". Record
surface_status: deferred as a top-level line on the task YAML
(via tasks.py or in-memory mutation), then proceed to the
per-task dispatch loop. The dispatched agent still receives the
wiring-contract clause in its prompt (see below) so it understands
that wiring is part of the deliverable; the deferral is an audited
exception, not a silent skip.
After the operator-prompt outcome is recorded, dispatch proceeds at
the existing per-task loop below. The standards doc owns the full
contract for the prompt structure, the candidate-set construction,
the operator-supplied-path sanitization rule, and the deferral
recording format — see the Operator-Prompt Fallback subsection of
standards/process/user-flow-completeness.md.
For each task in the current wave:
-
Update task status via python3 ~/.claude/scripts/tasks.py set-status --id {task_id} --status in_progress
-
Invoke the Agent tool ONCE with subagent_type set to the task's
assigned_agent field. The prompt argument is the stdout of
python3 ~/.claude/scripts/dispatch_prompt.py assemble --feature-path <feature_path> --task-id <task_id> (per Step 6a "Dispatch prompt
construction" above). The assembler emits the eight required sections
from standards/process/subagent-dispatch.md — task identifier,
required reading, files in scope, acceptance criteria, feature intent,
cross-task awareness, report-back format, and (conditionally) task
intent + wiring-contract. Do NOT append "Dispatch hooks will enforce
TDD..." or any other system-overlay reminder — those live in the
hooks (hooks/inject-standards.sh) and the role manifest, per
standards-doc anti-pattern #4.
-
spec.md + design.md briefing context (F006 BR-005, revised by ADR-Ftmp-19e49f7c-001). The assembler embeds the contents of <feature_path>/spec.md's Summary first paragraph as the Feature intent section (per standards/process/subagent-dispatch.md item 1). When <feature_path>/design.md ALSO exists, the assembler does NOT inline its body — instead, the task YAML's requires_reading list includes the design.md path, and the subagent Reads it on demand (cite-only). This matches the standards doc's anti-pattern #2 ("Inlining design.md content") and the observed pattern in .etc_sdlc/incidents/2026-05-22-dispatch-examples/. The rest of the per-task briefing structure — task YAML path, requires_reading, files_in_scope, acceptance criteria, and the cross-task awareness section — is sourced from the task YAML by the assembler.
-
For User-flow-sentenced tasks (those detected at sub-step 6a.5), the
prompt MUST also include the wiring-contract clause from
standards/process/user-flow-completeness.md (Dispatch-time Wiring
Contract section, "The Wiring Contract" subsection), appended
verbatim as a blockquote so the dispatched agent reads it as part
of its onboarding context. The clause body is:
Your task creates a user-facing surface (route/modal/tab/sidebar entry/wizard step) per the User-flow sentence in your AC. The surface is NOT done until it is wired into the parent navigation graph in the SAME commit as the new surface. Your files_in_scope includes the parent wiring file at <path> for this purpose. Before reporting success, run grep -rn "<your-route-or-component-name>" <project>/frontend/src (or the equivalent for your stack) and confirm at least one parent surface references it via <Link>, <Tab>, sidebar-config entry, or equivalent. If the parent file does not contain a working reference after your edits, do not report success. See standards/process/user-flow-completeness.md (Dispatch-time Wiring Contract section) for the full rule.
Substitute <path> with the parent wiring file path resolved at
6a.5 (auto-add) or 6a.6 (operator selection). For tasks marked
surface_status: deferred at 6a.6, the clause is still appended
(the agent must understand wiring is part of the deliverable even
when no parent file is in scope) and <path> is rendered as
(deferred — no parent file in scope; escalate if you discover the surface needs to be wired). ACs without User-flow sentences and
domain-only tasks marked surface_status: not_applicable pass through
dispatch unchanged: no clause appended, no operator prompt fired, prompt
content matches the pre-edit shape byte-equivalently.
-
You MUST NOT read, edit, or write any file listed in the task's
files_in_scope in your own context. That work belongs to the
dispatched subagent.
Dispatch all tasks in the wave in a single turn (parallel fan-out).
The wave-planner from Step 5 has already verified file-set isolation;
do not serialize within a wave.
6b. Wait for wave completion:
- Every dispatched subagent must return a result before you proceed.
- For each returned result, set task status to
completed (if the
subagent reported success) or escalated (if the subagent reported
a blocker) via tasks.py set-status.
6c. Verify wave:
- Run profile-aware tests via the F020 dispatcher:
printf '{"cwd":"%s"}' "$CWD" | bash hooks/verify-green.sh
- If tests fail: STOP. Report the failure. Do NOT proceed to next wave.
Do not write the phase-N/done tag. The phase-N/start tag from
step 6a remains in place — it is append-only and records that the
wave was attempted. Earlier successful phase tags also remain.
- If any task status is
escalated: STOP. Report the escalation to
the user. Do not write the phase-N/done tag. The phase-N/start
tag remains. Earlier successful phase tags are kept (no rollback).
Quality gate (per-wave, profile-dispatched) — F021 BR-003 + BR-004.
After per-wave tests pass and before the phase-N/done tag is written,
invoke the F020 profile-aware dispatcher against the wave's working tree:
printf '{"cwd":"%s"}' "$CWD" | bash hooks/verify-green.sh
Inherits F020 exit semantics (per ADR-F021-004 — read-only inheritance,
no modifications to verify-green.sh): exit 0 = pass (or no-profile
warn-and-skip per ADR-F020-003); non-zero = quality-tool failure.
Zero-tolerance contract per F021 BR-004: any non-zero exit fails the
wave. The conductor MUST NOT write the phase-N/done tag. Surface
verify-green's stdout and stderr verbatim to the operator in the wave's
verification.md (Step 7), and STOP. There is NO threshold-based,
delta-based, or count-based exception; "the wave produced fewer errors
than the previous wave" is not a passing condition. Zero is zero. No
exception flag is offered or accepted — by design, per ADR-F021-003.
Pre-existing errors in the project are the operator's responsibility to
resolve before the first /build invocation (BR-005); once etc is
installed, no quality-tool error is ever permitted to ship through a
wave boundary.
See standards/process/diagnostic-discipline.md for the full rule and
ADR-F021-003 + ADR-F021-005 for the design rationale.
6c-runtime. Per-wave behavioral runtime sibling (Gap A — AC-3).
After the structural verify-green gate above passes and before the
phase-N/done tag is written (6d), fire the behavioral sibling for the
ACs this wave is contracted to make live. This is the per-wave,
fast-feedback firing point of the runtime gate; the authoritative re-run
is Step 7.6. The full rule — declaration-gating, the profile wire
contract, zero-tolerance, and the opt-out hatches — lives in
standards/process/behavioral-runtime-dod.md and is NOT duplicated here;
this step cites it and orchestrates.
Declaration-gating (fires ONLY for live_at == <current wave>). Read
the Gap B liveness block at
state.yaml.spec_phase.contract_completeness.liveness[]. Each entry
carries ac_id, outcome, live_at, and (for deferrals) deferred_reason.
Select the subset whose live_at names the wave just closed (the
0-based wave number WITHIN the current phase, matching 6a/6d). Map waves
to live ACs from live_at only — the wave plan is a consumer and never
re-declares liveness; the spec is the source of truth (a wave/spec
mismatch surfaces as a planning warning, not a re-declaration).
Skip-silently conditions (half-built waves are legal). Do NOT
dispatch — and do NOT fail — when any of these hold:
state.yaml.spec_phase.infrastructure_only: true (whole-feature exempt
— e.g. this very dogfood feature).
- No liveness block is present (forward-only: legacy features are never
gated, never mutated).
- The liveness block's
schema_version is higher than known
(warn-and-skip per Gap B ADR-002).
- No AC has
live_at == <current wave> (nothing is contracted live yet
— silence is correct, not a gap).
Dispatch (only when the live subset is non-empty). Invoke the
runtime-verify dispatcher, passing the feature path and the selected live
AC ids as JSON on stdin (never as shell args — metacharacter-injection
defense per the profile wire contract):
printf '{"feature_path":"%s","live_ac_ids":%s,"cwd":"%s"}' \
"$FEATURE_PATH" "$LIVE_AC_IDS_JSON" "$CWD" | bash hooks/runtime-verify.sh
A missing profiles.lock or absent profile runtime-verify.sh →
warn-and-skip (parity with verify-green). The dispatcher returns
{"results":[{"ac_id","status","evidence",...}]} with status in the
closed enum pass | fail | no-test.
Zero-tolerance close (mirrors verify-green). Any selected live AC
returning fail OR no-test FAILS the wave: STOP, do NOT write the
phase-N/done tag, surface the offending AC ids and their evidence
verbatim into the wave's verification.md (Step 7), and route to the
existing 6e failure/remediation path. A declared-live outcome with no
matching runtime test (no-test) is a failure — a contracted-live AC
with no runtime assertion cannot pass. There is no threshold or
delta-based exception; this mirrors 6c's zero-is-zero contract one level
up (behavioral, not structural).
Per-wave results are advisory fast feedback; the authoritative,
totalizing re-run at Step 7.6 re-checks every declared-live AC against
the assembled app (ADR-003) and is the gate that routes the terminal tag.
6c-baseline. Per-wave architecture-baseline conformance sibling
(F-2026-06-10-brownfield-architecture-baseline).
After the 6c-runtime behavioral gate above passes and before the
phase-N/done tag is written (6d), fire the architecture-baseline
conformance gate for the wave's working tree. This is the ENFORCE-stage
consumer of the ratified baseline's mechanizable rules; it runs the same
dispatcher the /rule-sweep skill uses. The full rule lives in
standards/process/architecture-baseline.md; this step cites it and
orchestrates.
Dispatch (conductor side — written against the dispatcher contract,
not its implementation). Pipe the dispatcher JSON contract to
hooks/baseline-verify.sh on stdin (never as shell args —
metacharacter-injection defense, parity with runtime-verify). The wave
gate checks ALL mechanizable rules, so rule_ids is null:
printf '{"repo_root":"%s","rule_ids":null,"cwd":"%s"}' \
"$REPO_ROOT" "$CWD" | bash hooks/baseline-verify.sh
The dispatcher aggregates per-profile results and returns
{"results":[{"rule_id","status","evidence"},...]} with status in the
closed enum pass | fail | no-check. The dispatcher ALWAYS exits 0 —
the verdicts live in the results JSON. Read the verdicts from the JSON,
NEVER from the dispatcher exit code. Unknown status values aggregate
as fail (fail-closed, per the contract-completeness declaration).
Skip-silently conditions (parity with verify-green warn-and-skip). Do
NOT fail — proceed straight to 6d — when any of these hold:
- No baseline exists (
scripts/baseline.py status would report
missing): there is nothing to enforce. (A missing baseline never
reaches a build wave anyway, because Step 1d's missing branch is the
soft path; this is the belt-and-suspenders skip.)
- No mechanizable rules are present: the dispatcher returns an empty
results array (or all no-check), which is not a failure.
- A missing
profiles.lock or an absent profile baseline-verify.sh →
the dispatcher emits a bracketed stderr WARN and skips that profile
(F020-003 warn-and-skip format).
Zero-tolerance close (mirrors 6c verify-green). Any rule whose
results[].status is fail FAILS the wave: STOP, do NOT write the
phase-N/done tag, surface the offending rule_ids and their
evidence verbatim into the wave's verification.md (Step 7), and route
to the existing 6e failure/remediation path. There is no threshold or
delta-based exception; this mirrors 6c's zero-is-zero contract, applied
to baseline conformance. The verdict is read from the results JSON, never
from the dispatcher's (always-zero) exit code.
6d. Checkpoint and phase/wave-done tags:
Only after step 6c confirms tests pass and no task is escalated — i.e.
on a successful wave exit — write the wave-done tag (and, when this was
the LAST wave of the phase, the phase-done tag) and update state:
6d.5: Write per-phase completion report.
After the phase-done tag is written and before the waves_completed
state update, write a per-phase completion-report.md so
scripts/release_notes.py can roll it up at terminal close (Step 7.5b).
The completion-report.md lands at
<feature_path>/build/phase-<N>/completion-report.md and is the
canonical audit-trail artifact for this wave's outcome.
Trigger condition: matches the phase-done tag — only on successful
wave exit (Step 6c tests passed, no task escalated). Failed phases
produce no report; the absence of phase-N/done plus the absence of
completion-report.md is the existing failure signal.
Source the report's content from the wave's task YAMLs:
prd-title: read the first # PRD: <title> heading from
<feature_path>/spec.md. Fall back to the feature directory slug
if no # PRD: heading is present.
prd-id: read feature_id from <feature_path>/state.yaml. Fall
back to the feature directory name (e.g., F005-build-completion- reports) if the field is absent.
ac-passed: collect every acceptance_criteria entry from each
task YAML in <feature_path>/tasks/ whose status is completed
and whose phase membership corresponds to wave N. Because the
phase-done tag is gated on successful wave exit, every AC in the
wave's task list is treated as passed at write time. Concatenate
into a temp file (one AC per line); pass via --ac-passed-file.
ac-failed: empty (the wave passed Step 6c verification before
reaching 6d.5; no failed ACs land in this report). Pass an empty
temp file via --ac-failed-file.
deferred: collect any surface_status: deferred markers from
the wave's task YAMLs (introduced by F003 — see
standards/process/user-flow-completeness.md's Operator-Prompt
Fallback subsection). Concatenate into a temp file; pass via
--deferred-file. If none found, write an empty file (the helper
emits - (none) automatically).
limitations: default to an empty file. The helper emits
- (none). The operator can hand-amend the resulting
completion-report.md after write if known limitations should be
recorded; the amendment lands in release-notes.md at Step 7.5b.
Invoke the helper:
python3 ~/.claude/scripts/completion_report.py write \
--feature-dir "<feature_path>" \
--phase <N> \
--prd-title "<prd-title>" \
--prd-id "<prd-id>" \
--ac-passed-file "<temp-file-of-ac-list>" \
--ac-failed-file "<empty-temp-file>" \
--deferred-file "<temp-file-of-deferred-list>" \
--limitations-file "<empty-temp-file>"
The CLI form is required because the helper lives at
~/.claude/scripts/, not the user's project — from scripts.completion_report import write would only resolve inside
this etc checkout, so it MUST NOT be used.
Exit codes follow the F004 + git_tags + value_hypothesis convention
(0 created, 1 hard fault). On exit code 1, the conductor surfaces
stderr to the operator and STOPS — completion-report.md must exist
before advancing to 6d's waves_completed update.
- Update
state['build']['waves_completed'] = N in state.yaml using
the same merge-preserving read/mutate/write pattern from Step 2;
the top-level /spec metadata stays untouched.
- This enables resume from the last completed wave if session dies.
Discipline (BR-008, edge case 4): Tags written by git_tags.write_tag()
are append-only. The harness never deletes, retags, or force-updates a
tag it has written. On any failure inside step 6c, the phase-N/done tag
is NOT written for the failing wave; phase-N/start tags and any
phase-M/start|done tags from earlier successful waves remain (preserved).
Resume continues from the last successfully completed wave.
6d.7: Emit stack layer (F010).
After 6d's phase-N/done tag AND 6d.5's completion-report both succeed,
emit the wave's diff as a distinct GitHub PR stack layer via gh-stack.
Tag FIRST (6d) so the append-only tag captures the wave's close even if
6d.7 fails; 6d.7 runs AFTER, never before.
Single-wave bypass (BR-005, AC7). If total_waves == 1, SKIP 6d.7
entirely. Set state['build']['stacked'] = false (merge-preserve pattern
from Step 2) and fall through to 6e. Multi-wave builds (total_waves > 1)
set state['build']['stacked'] = true and proceed.
Layer branch naming (BR-003, AC4). Branches: <feature-slug>-L<N>,
slug from state.yaml.build.feature, <N> is 1-indexed wave number.
Example: F010 wave 0 → stacked-prs-from-build-L1. Verbatim regex:
^[a-z][a-z0-9-]+-L[0-9]+$
Sanitization: characters outside [a-z0-9-] are replaced with - and
the slug is lowercased at branch-creation time. On-disk slug unchanged.
Squash-commit (GA-002). Collect the wave's modified files and
squash-commit on the new branch <feature-slug>-L<N>. Base: previous
layer branch when N > 1, or main when N == 1. One squash-commit
per wave matches F005's one-report-per-wave + F008's wave-as-isolation.
gh-stack invocation (BR-002, AC3). Argv-list subprocess.run —
never shell string — mirrors F008's git mv precedent:
import subprocess
result = subprocess.run(
["gh", "stack", "push", "--base", "<previous_layer_branch>"],
capture_output=True, text=True,
cwd="<feature_repo_worktree>",
)
<previous_layer_branch> = <feature-slug>-L<N-1> (or main when N==1).
No auto-push (BR-010); operator runs gh stack submit after terminal close.
Soft LOC warning (BR-004, AC5). Compute net_loc = additions + deletions
from git diff --shortstat (use abs(net_loc) so deletion-only waves
warn too — edge case 2). When net_loc > 500, emit this VERBATIM line
to stderr (the test contract greps for prefix WARNING: layer L):
WARNING: layer L<N> contains <K> LOC (target: 500). Consider splitting the wave for review tractability. Proceeding with stack emission.
Non-blocking. The 500 threshold is a module-level constant
LAYER_LOC_SOFT_TARGET = 500 in the implementing script — future tuning
is a one-line edit; never inline 500 at the check site.
Failure semantics (edge cases 3, 5). If git commit non-zero OR
gh stack push non-zero, STOP. Do NOT proceed to 6e. Write
state['build']['stacked_failure'] = <wave_num> for --resume. The
phase-N/done tag from 6d remains. Surface stderr verbatim. /build does
NOT degrade to monolithic-PR mode silently.
Empty wave (edge case 1). Zero file changes → skip layer emission;
log note: wave <N> produced no file changes; skipping layer emission.
Layer N-1 remains the head; subsequent layers base off N-1.
6e. Proceed to next wave or finish.
On escalation or test failure:
⚠ Wave {N} failed.
Failing tests: {list}
Escalated tasks: {list}
The pipeline is paused. Options: