Canary Lab owns the run verdicts and artifacts; this client applies the
fixes. These tools arrive via the Canary Lab MCP server. If this client is
already connected (the plugin connects with full), skip this step. To
configure a connection manually: npx canary-lab mcp --profile repair (the
composite lifecycle/full profiles carry the same tools).
-
Call list_features and choose the requested feature.
-
Call start_run with claim_heal: true, a stable session_id, and a useful conversation_name. Do not pass client_kind — the MCP bridge auto-detects it from the connection; guessing it yourself can mis-set it and suppress heal claim. Heal claiming is open to interactive Claude/Codex clients (Desktop or CLI alike) — only runner-spawned PTY agents are blocked — so an ordinary CLI session like this one can own the heal loop. For requests like "rerun 7cvh", pass run_ref: "7cvh".
-
If start_run returns type: "repo_collision_requires_choice", another run is using the same app/repo. Ask the user whether to run this one isolated in a per-run git worktree (runs now, concurrently) or queue it until the other run finishes, then re-call start_run with isolation: "worktree" or isolation: "queue". Do not guess. If start_run returns queued: true, tell the user the run is parked (queueReason) and will start automatically when capacity frees; wait_for_heal_task still blocks until it starts and needs fixes.
-
If start_run returns an active run, continue that run. But if it returns type: "boot_session" (or executionType: "boot"), the run is a held boot-only session — services are up, no tests run, and there is no heal task. Do not claim heal or call wait_for_heal_task; report that services are ready and that the user can stop them with abort_run (confirm:true) when done. A service that fails its readiness probe is marked failed (its status shows timeout) but the session stays held — boot never self-aborts on a health-check failure, so report which services came up and which failed; only abort_run tears it down.
-
If start_run reports already-claimed, stop and tell the user which session owns the run. If it returns claimSuppressed: true, this session is a runner-spawned PTY agent (the benchmark/portify sessions Canary Lab launches itself) and cannot own the heal loop — interactive clients are not suppressed, so you normally won't hit this. The run still runs in External-client heal mode (it does not fall back to the project's configured heal agent — it waits for a drive); do not call wait_for_heal_task. Report the run id and tell the user to drive heal from an interactive Claude/Codex client or the web UI.
-
Handle user interrupts explicitly: "pause", "intercept", or "pause and heal" means call pause_run; "stop heal" or "cancel repair" means call cancel_heal; "abort", "kill the run", or "stop everything" means call abort_run only with the required confirmation.
-
Call wait_for_heal_task with the runId from start_run and the same session_id. It blocks for a short bounded window and heartbeats for you. If it returns type: "still_waiting", the run is still active and the window just elapsed — this is not terminal: immediately call wait_for_heal_task again with the same runId + session_id. Loop on still_waiting until you get needs_heal / passed / failed. (If it ever returns type: "boot_session", the run is a held boot-only session — report services are up and stop here; do not wait again.)
-
If it returns passed, summarize using result.counts.statusLine and stop.
-
If it returns failed, report the terminal status using result.counts.statusLine and relevant failure summary.
- If the result carries
dirtyTests (a test spec changed since the last green run — can appear on passed, failed, or needs_heal), relay result.dirtyTests.message to the user verbatim (e.g. "⚠️ Tests have been modified, please review.") alongside the outcome. Do not block, gate, re-run, revert, or edit the test files to "fix" it — it's an awareness signal so the user can review or commit the change, not an error to act on.
-
If it returns needs_heal, treat the returned heal context as the compact first-stop packet — which part of it matters depends on the situation:
context.failedTests non-empty, first cycle (context.healPrompt present): inspect context.healPrompt.startHere first, then use context.healPrompt.resources, current failures, and the checked-out source code. The packet is slim — context.healIndex and context.journal are paths (Read them when needed), and each context.failedTests[] entry carries a failureId plus pointer dirs (errorPath, traceDir, playwrightMcpDir).
- cycle ≥ 2 (
context.guidance present, no context.healPrompt): context.healPrompt and context.nextSteps are sent on the first needs_heal only — on repeat cycles the context carries just the changed failure packet plus a context.guidance breadcrumb. Reuse the cycle-1 map, or call get_heal_context to re-fetch it.
context.escalation present: the same tests failed 3+ cycles running — you're stuck. Read context.escalation.readFirst and follow context.escalation.tactics (change tactic — revert or build on the prior diff, don't fire a fresh unrelated hypothesis) instead of repeating the last fix.
context.failedTests empty AND context.bootFailure set: a service failed to boot — no tests ran. Read context.bootFailure.logPath to find why the service won't serve, fix the service/app code, then signal_run kind: "restart" (context.nextSteps already reflects this).
Call get_run_snapshot only when you need the verbose raw summary, full counts, or deeper debugging fields.
-
When several tests fail, fan out the diagnosis and the fix-drafting: dispatch one read-only sub-agent per failure, hand it the failureId, and have it call get_failure_detail(runId, failureId) to investigate just that slice in parallel and report back a hypothesis plus a concrete proposed patch (the exact file edits / unified diff) for its failure. The sub-agents are read-only — they must not touch the working tree or call signal_run; they only investigate and draft. Then you apply the patches yourself, serially — reconcile by hand if two patches touch the same file — then re-test and signal_run once. Only investigation + drafting fan out; applying, re-testing, and signalling stay single-threaded. (For a single failure, just investigate and fix it directly.)
-
Fix app/service code, not tests, unless the test is provably wrong.
-
Call signal_run once per cycle with kind: "rerun" for test-only/app-code fixes that do not need service restart, or kind: "restart" when services or env need restarting. Include hypothesis and fixDescription; Canary Lab writes the journal from that signal and its observed git diff. One accountable signal per cycle, even when you fixed several failures.
-
Do not call a separate journal-writing tool; the runner records failing tests, changed files, signal, outcome, and diff.
-
Repeat from wait_for_heal_task (looping on still_waiting) until the run passes or reaches terminal failure.