Transferred from sarath-soman/ship-archive#5 when ship moved into the pramana monorepo. The original issue stays in the archive for history; this is the working copy in its new home.
When the run state reports test failures, the pipeline currently halts at publish with escalate. The operator has to manually triage whether each failure is a code bug or a test bug, fix it, and re-run. For unattended automation (and for ship-proxy's CI watcher in #3), this is the missing closing of the loop.
The naive fix — feed failures back to the implementer with "make these pass" — is the verifier-overfit pattern explicitly forbidden in ship-character (the model can "fix" by weakening assertions or suppressing failures). We need a closing of the loop that respects that constraint.
Design
A single new state, debug, runs after run only when there are failures. It investigates root cause empirically and routes the fix.
Pipeline shape
validate → strategy → (impl ∥ e2e) → review → run → [if failed: debug] → publish
│
├ code → strategy → impl → run → publish
├ test → e2e → run → publish
└ unknown → escalate
Why combined debug-and-classify (single agent, not split)
Earlier sketch had two agents: one for evidence-gathering, one for triage. Collapsed to one because investigation and judgement are inseparable in real debugging — you form a hypothesis as you investigate, then verify it. Splitting into pure-evidence + pure-judgement creates artificial boundaries the work doesn't actually have.
debug agent
Inputs:
run.md (failure list — which tests failed, expected vs actual)
validate.md (the spec — source of truth for what should happen)
e2e.md (test manifest — what each test asserts and why)
- the project itself (test code, impl code, ability to run them)
Notably NOT impl.md. Runtime evidence supersedes implementer summaries.
Procedure:
- For each failure, re-run that test with instrumentation (
RUST_BACKTRACE=1, --nocapture, verbose flags from CLAUDE.md).
- Read the assertion site + surrounding control flow in both test code and impl code.
- Capture runtime values, stack traces, branch decisions at the failure point.
- Compare empirical behaviour against the spec (validate.md).
- Classify cause as
code, test, or unknown.
Refusals (in addition to shared substrate):
- I refuse to read
impl.md. Runtime evidence is the source of truth about what the program does — not the implementer's summary of what they thought they did.
- I refuse to derive test correctness from implementation behaviour. Tests are judged against the spec, never against the impl. If the impl does X but the test asserts Y and the spec says Y, the impl is wrong — not the test.
- I refuse to classify cause as
code unless empirical evidence shows impl behaviour contradicts spec.
- I refuse to classify cause as
test unless empirical evidence shows the test's assertion contradicts spec.
- I refuse to "fix" anything. I classify; the orchestrator routes; downstream agents fix.
- I refuse to weaken or work around test assertions. If a test's strictness is the issue, the cause is
test and the fix is at the e2e-test-author level (revising the test against the spec).
Output:
# Debug report
## Summary
- failures investigated: N
- classified: <n code> / <n test> / <n unknown>
## Failures
### <test name 1>
- cause: <code | test | unknown>
- evidence: <runtime observation; verbatim where possible>
- spec citation: <validate.md section / line>
- suggested fix scope: <which agent / state should pick this up>
### <test name 2>
...
## Action
<route_code | route_test | escalate>
## Reason
<one-line — why this routing>
Routing logic (orchestrator-level)
| Action |
Effect |
route_code |
Re-run strategy → impl → run. Strategy gets a feedback artifact pointing at the debug findings; may revise the step ordering or add edge-case handling the original strategy missed. |
route_test |
Re-run e2e → run. The e2e-test-author corrects the test against spec. The implementer is not invoked. |
escalate |
Halt at debug with comment posted to issue. Operator decides. |
Bounded ≤1 cycle
After the routed re-run, if run still fails:
- The orchestrator does not re-invoke
debug.
- Pipeline halts with
escalate, comment posted.
- Operator triages.
This is where the no-retry-until-pass principle is enforced. One debug → route → re-run is bounded judgement (similar shape to review's bounded ≤2 cycle). A second debug pass would be the start of an unbounded loop chasing a moving verifier; explicitly forbidden.
What this is NOT
- Not "run failures auto-fix themselves." Failures still halt the pipeline; debug only routes to which upstream agent re-engages.
- Not "make the tests pass." Debug refuses to weaken assertions; cause must be diagnosed against the spec.
- Not a substitute for human review on subtle failures.
unknown exists because some discrepancies require human judgement (spec ambiguity, design questions).
Implementation surfaces
- New agent:
debugger.md (with the refusals above).
- New state in orchestrator with the routing logic.
- New "feedback mode" prompts for
strategy and e2e agents (similar to implementer's existing review-feedback mode).
- Updated
setup_cmd.rs to embed debugger.md.
- Updated docs/architecture.md, README.md.
- Issue commenting on
escalate from debug (the debug.md report becomes the comment body).
Acceptance
- A run with deliberately broken impl (test correctly asserts spec; impl wrongly does the opposite) is classified
code and re-routed through strategy → impl → run; the corrected impl passes on re-run.
- A run with a deliberately wrong test (test asserts X but spec says Y; impl correctly does Y) is classified
test and re-routed through e2e → run; the corrected test passes on re-run.
- A run with genuinely ambiguous failure (spec doesn't disambiguate) is classified
unknown and escalates with a clear comment on the issue.
- Second-pass failures (after one debug → route → re-run) escalate without a second debug invocation.
- The debug agent never reads
impl.md and never classifies test based on what the impl does.
When the
runstate reports test failures, the pipeline currently halts atpublishwithescalate. The operator has to manually triage whether each failure is a code bug or a test bug, fix it, and re-run. For unattended automation (and for ship-proxy's CI watcher in #3), this is the missing closing of the loop.The naive fix — feed failures back to the implementer with "make these pass" — is the verifier-overfit pattern explicitly forbidden in
ship-character(the model can "fix" by weakening assertions or suppressing failures). We need a closing of the loop that respects that constraint.Design
A single new state,
debug, runs afterrunonly when there are failures. It investigates root cause empirically and routes the fix.Pipeline shape
Why combined debug-and-classify (single agent, not split)
Earlier sketch had two agents: one for evidence-gathering, one for triage. Collapsed to one because investigation and judgement are inseparable in real debugging — you form a hypothesis as you investigate, then verify it. Splitting into pure-evidence + pure-judgement creates artificial boundaries the work doesn't actually have.
debugagentInputs:
run.md(failure list — which tests failed, expected vs actual)validate.md(the spec — source of truth for what should happen)e2e.md(test manifest — what each test asserts and why)Notably NOT
impl.md. Runtime evidence supersedes implementer summaries.Procedure:
RUST_BACKTRACE=1,--nocapture, verbose flags from CLAUDE.md).code,test, orunknown.Refusals (in addition to shared substrate):
impl.md. Runtime evidence is the source of truth about what the program does — not the implementer's summary of what they thought they did.codeunless empirical evidence shows impl behaviour contradicts spec.testunless empirical evidence shows the test's assertion contradicts spec.testand the fix is at the e2e-test-author level (revising the test against the spec).Output:
Routing logic (orchestrator-level)
route_codestrategy → impl → run. Strategy gets a feedback artifact pointing at the debug findings; may revise the step ordering or add edge-case handling the original strategy missed.route_teste2e → run. The e2e-test-author corrects the test against spec. The implementer is not invoked.escalatedebugwith comment posted to issue. Operator decides.Bounded ≤1 cycle
After the routed re-run, if
runstill fails:debug.escalate, comment posted.This is where the no-retry-until-pass principle is enforced. One debug → route → re-run is bounded judgement (similar shape to review's bounded ≤2 cycle). A second debug pass would be the start of an unbounded loop chasing a moving verifier; explicitly forbidden.
What this is NOT
unknownexists because some discrepancies require human judgement (spec ambiguity, design questions).Implementation surfaces
debugger.md(with the refusals above).strategyande2eagents (similar to implementer's existing review-feedback mode).setup_cmd.rsto embed debugger.md.escalatefrom debug (the debug.md report becomes the comment body).Acceptance
codeand re-routed through strategy → impl → run; the corrected impl passes on re-run.testand re-routed through e2e → run; the corrected test passes on re-run.unknownand escalates with a clear comment on the issue.impl.mdand never classifiestestbased on what the impl does.