Skip to content

feat: add debug state for run-failure root-cause analysis and routing #264

Description

@sarath-soman

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:

  1. For each failure, re-run that test with instrumentation (RUST_BACKTRACE=1, --nocapture, verbose flags from CLAUDE.md).
  2. Read the assertion site + surrounding control flow in both test code and impl code.
  3. Capture runtime values, stack traces, branch decisions at the failure point.
  4. Compare empirical behaviour against the spec (validate.md).
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions