Troubleshooting
Real failure modes, sourced from the skillâs stage files and scripts, with what the run does about each and what you do. For the design behind these behaviors see how it works, the gate model, and architecture.
For the operatorâs side of a run that is technically working and still not doing what you meant, see operating tips. It covers the driver stop reasons that need a decision, why row order is the only sequencing mechanism, and the verification traps that produce confident false greens.
Start from the symptom you observed and follow it to the section that explains it:
flowchart TD
S["What did you observe"] --> P{"Run never launched"}
P -->|"yes"| PRE["See: Preflight cannot reach budget-zero"]
P -->|"no"| G{"Stopped at a gate, status NOT_EVALUATED or escalate"}
G -->|"yes"| GATE["See: gate_eval reports blocked / escalate"]
G -->|"no"| H{"A commit slipped past a guard"}
H -->|"yes"| HOOK["See: Hooks not firing"]
H -->|"no"| B{"A story stopped re-looping, escalation marker appeared"}
B -->|"yes"| BUD["See: Budget exhausted mid-story"]
B -->|"no"| R{"Run was interrupted, want to continue"}
R -->|"yes"| RES["See: Resume after an interruption"]
R -->|"no"| D{"A drive stopped and you cannot tell why"}
D -->|"yes"| OPS["See: operating tips, the driver's stop reasons"]
D -->|"no"| LOG["Read the tail of .decision-log.md"]
classDef accent fill:#6366F1,stroke:#4F46E5,color:#fff
class PRE,GATE,HOOK,BUD,RES,OPS,LOG accent
Preflight canât reach budget-zero
Section titled âPreflight canât reach budget-zeroâSymptom. The run stops at Stage 2 (or, headless, emits {"status":"blocked", ...} with a one-line reason) and never launches.
What happened. Preflight is a hard gate: the run launches only when the post-remediation mechanical budget == 0 and the semantic scan found no RED. The auto-remediation pass clears the fixable mechanical blockers: it scaffolds the test framework, generates missing acceptance criteria, pre-creates the TEA output dirs, ensures one project-context.md, ensures sprint-status.yaml, and prompts once (interactively) for secrets, then re-runs the check.
What stays red. Some things the remediation pass cannot fix, by design:
- Undecided product or architecture decisions. An open question, a âTBDâ/âTODO: decideâ on a load-bearing requirement, a PRDâADR contradiction, or a story whose âdoneâ is undefinable. The fix is a human decision; an unattended run guessing it produces confidently wrong work. Resolve the decision in the artifacts, then re-run.
- Missing secrets in headless. Headless never prompts, so a secret that cannot be resolved becomes a RED blocker rather than a question. Provide the secret (out of git) before the headless run, or run attended so preflight can prompt once.
- Claude Code below the minimum versions. The primitive-version blocker is marked non-remediable: the script canât upgrade the host. Update Claude Code.
The decision log carries the full blocker list with what each needs to clear. Read it, clear the items, re-run.
gate_eval reports blocked / escalate on a missing gate-decision.json
Section titled âgate_eval reports blocked / escalate on a missing gate-decision.jsonâSymptom. Stage 5 returns gate_status: NOT_EVALUATED and verdict escalate, with a reasons entry like neither gate-decision.json nor e2e-trace-summary.json present in <dir>.
What happened. gate_eval.py reads TEAâs gate artifact from the trace output directory. NOT_EVALUATED means neither the slim gate-decision.json nor the fallback e2e-trace-summary.json was found there, or the run carried no gate fields. Almost always this is one of:
- The TEA trace gate did not run. In production, Stage 5 must backfill evidence first (
bmad-testarch-automateâbmad-testarch-traceâbmad-testarch-nfr) before the gate;bmad-testarch-traceis what writes the gate decision. If it didnât run, there is nothing to read. - Wrong
trace_output_dir. The script reads the directory passed as--trace-output(resolved from{workflow.trace_output_dir}). If TEA wrote elsewhere, or the output dirs were never pre-created at preflight, the artifact is real but in a different place. Confirmtrace_output_dirmatches where TEA actually wrote.
Note this is fail-closed on purpose: a missing or unreadable gate artifact escalates rather than being assumed green. The slim fileâs absence alone is not the problem: the script falls back to the summary, and that fallback is explicitly not a failure. See the gate model for how the artifact resolves into a gate_status and the full verdict mapping.
Hooks not firing
Section titled âHooks not firingâSymptom. A commit lands on a protected branch, or a commit lands before a storyâs tests ran: the invariants the PreToolUse hook should enforce did not block.
What to check.
- Older Claude Code. The hook returns a
denydecision in the hook JSON and also exits 2 with the reason on stderr precisely so older clients that ignore the JSON still block. If neither path fired, the client may not be honoring PreToolUse hooks at all. Update Claude Code. settings.local.jsonnot merged. The hooks are merged into{project-root}/.claude/settings.local.jsonat preflight, and the skill asserts they are active before going unattended. If the file wasnât merged (or the workspace trust dialog wasnât accepted), the hooks arenât loaded. Re-run preflight; verify the two hook entries are present in the resolved settings.- A
customize.tomloverride that silently no-ops. Both hooks read config from env first and fall back to hardcoded defaults (main/master,25,ultracode/epic-). Aprotected_branchesor turn-budget override incustomize.tomlonly reaches the hook if preflight injected it into the hook env (ULTRACODE_PROTECTED_BRANCHES, etc.). If your custom protected branch isnât being guarded, the override didnât reach the enforcement layer. Confirm preflight passed it through.
Budget exhausted mid-story
Section titled âBudget exhausted mid-storyâSymptom. A story stops re-looping; an escalation marker (<impl-artifacts>/.escalation-<story>.md) appears; the run surfaces a budget message.
What happened. A runaway story is bounded three ways. The real in-loop bound is the literal ââŚor stop after N turnsâ clause inside the /goal condition. The gateâs re-loop budget is deterministic: a reloop that would exceed max_turns_per_story becomes an escalate instead. The Stop hook (budget_stop.py) is the defensive third layer: it counts turns and, on overrun, writes the escalation marker and lets the stop proceed.
Its documented limitation. A Stop hook fires only when Claude is already trying to stop: it cannot interrupt a /goal condition mid-turn. So at this layer the ceiling is advisory; the hard bounds are the in-condition turn clause and the gate re-loop budget. If a story keeps consuming budget, that is the signal to re-scope, split, or hand it off, not to raise the budget and hope.
Resume after an interruption
Section titled âResume after an interruptionâSymptom. A run was interrupted (Ctrl-C, a crash, a compaction) and you want to continue rather than restart.
What happens. The runâs .decision-log.md is canonical memory and recovers full state regardless of compaction. On resume the skill surfaces the existing log with its last session date and offers to resume. Execute re-enters at the first story whose last logged gate verdict is not advance; already-advanced stories are not re-run. The Epic branch, hooks, and allowlist are re-asserted, not rebuilt, before continuing. You do not need to reconstruct state by hand. Point the skill at the same Epic and accept the resume offer.
The retired --parallel flag
Section titled âThe retired --parallel flagâ--parallel (the experimental worktree fan-out) is retired. The flag is still accepted so an old invocation does not error: the run logs one .decision-log.md note that it was accepted and ignored, then executes the sequential spine. The Epic still ships; it ships sequentially. parallel_max_concurrency in customize.toml is likewise deprecated and no-op.