[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6XCVSWW705CN3C69FQMPZXN":3,"skill-same:01M6XCVSWW705CN3C69FQMPZXN":82},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":52,"linkOnly":53,"markdownContent":54,"markdownBody":55,"frontMatter":56,"refName":58,"commitSha":59,"skillDirectory":60,"forksCount":61,"repositoryHtmlUrl":62,"repositoryHomepageUrl":63,"ownerHtmlUrl":64,"ownerBlogUrl":65,"ownerDescription":66,"updatedAt":67,"install":68,"resources":81},"01M6XCVSWW705CN3C69FQMPZXN","canary-lab-run",null,"Use when running or healing an EXISTING Canary Lab feature locally — \"run \u003Cfeature>\", \"fix the failing run\", \"rerun 7cvh\", \"drive the heal loop\" — through the repair MCP tools (start_run, wait_for_heal_task, signal_run, get_heal_context, pause_run, cancel_heal, abort_run). Fix failing runs by editing app\u002Fservice code, not tests. For end-to-end onboarding of a new repo use canary-lab (flight); for deployed-environment checks use canary-lab-verify.","ferterahadi\u002Fcanary-lab","ferterahadi","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F148174839?v=4","01M6XCHCY39C7QEF3JZJ3VZGNZ","Fer Terahadi","01M6XCHD1ZE9926NBC97ZMZZRY",4,"MIT","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fagent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab-run\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51],"run","canary lab","canary lab run","lab","canary","Tools","loop","context","fix","get","end","new repo","app","wait","start","rerun","when","failing","code","MCP","2026-11-10 10:12:30",false,"---\nname: canary-lab-run\ndescription: Use when running or healing an EXISTING Canary Lab feature locally — \"run \u003Cfeature>\", \"fix the failing run\", \"rerun 7cvh\", \"drive the heal loop\" — through the repair MCP tools (start_run, wait_for_heal_task, signal_run, get_heal_context, pause_run, cancel_heal, abort_run). Fix failing runs by editing app\u002Fservice code, not tests. For end-to-end onboarding of a new repo use canary-lab (flight); for deployed-environment checks use canary-lab-verify.\ntype: skill\n---\n\n# Canary Lab — Run + Heal Loop\n\n## User input through MCP 2.0\n\nUse the native Canary connector in the requesting chat so SDK 2.0 elicitation\n(`input_required`) reaches the human there. Do not substitute a shell MCP client,\nanswer a form yourself, or recreate the question in chat. Existing explicit\ninstructions and autopilot choices still apply without another ask.\n\nForm decisions have one shared record. The native form links to the same approval\nin Canary Notifications, also surfaced above the Flight page. On `needs-input`\nwith `approvalId` and `reviewUrl`, show that link and call `wait_for_approval` with\nthe ID; repeat on `still_waiting`. The human answers once in either surface.\nA browser answer resolves the server decision; some clients keep their native\nform visible until the human dismisses it. Dismissal then returns the stored\nresult. Client decline\u002Fcancel alone does not represent a human decision.\n\nWithout an `approvalId`, leave `needs-input` pending after decline\u002Fcancel, stale\ninput, or an unfinished UI action; do not automatically retry or repeat the\nquestion. Setup\u002Freconnection can use chat while MCP is unavailable. Never collect\npasswords, API keys, or access tokens in chat or form elicitation; use the returned\nCanary UI URL for secret entry.\n\n## MCP Invocation\n\nSetup and the plugin expose one public Canary Lab MCP tool: `exec` (usually\nrendered as `mcp__Canary_Lab__exec`). Every\nCanary Lab tool name below is the exact `command` value, not a separate public\ntool. For a feature-scoped command, replace both placeholders in this shape:\n\n```json\n{\"command\":\"\u003Cexact_tool_name>\",\"arguments\":{\"feature\":\"\u003Cfeature_name>\"}}\n```\n\nThis is the envelope shape, not every command's complete schema. Add the\nfields that command declares inside `arguments`; call `describe_tool` when a\nfield is uncertain.\n\nNever invent a wrapper verb such as `learn` or `call`, embed JSON in a command\nstring, or turn arguments into flags. Keep fields such as `confirm: true` inside\n`arguments`. Use `list_tools`, `search_tools`, or `describe_tool` as the\n`command` when discovery is needed. A deliberately selected focused or `full`\nprofile still exposes atomic tools for debugging; the setup-installed path is\n`compact` + `exec`.\n\nCanary Lab owns the run verdicts and artifacts; this client applies the\nfixes. These tools arrive via the Canary Lab MCP server. If this client is\nalready connected (the plugin connects with `compact`), skip this step. To\nconfigure the same connection manually: `npx canary-lab mcp --profile compact`.\n\n## Arguments\n\nAn invocation argument (`\u002Fcanary-lab-run \u003Csuite>` — the Getting Started\nguide's \"Repair a Broken Suite\" card emits exactly this shape) is a suite\n(feature) name in the connected workspace: pass it to `start_run` directly.\n\n## Workspace Bootstrap\n\n1. Find the LIVE server first: read `~\u002F.canary-lab\u002Factive-servers.json`, which records `projectRoot`, `port` and `pid` for every UI that registered. A stopped server's entry LINGERS — the file is only rewritten when the next server registers — so an entry is a candidate, not proof: the health check below is what confirms it. One entry → that is your server and its `port`. Several → take the one whose `projectRoot` is the workspace the user means. None → fall back to `~\u002F.canary-lab\u002Fworkspaces.json` (Windows: `%USERPROFILE%\\.canary-lab\\workspaces.json`): one workspace → use it, several → ask which, none → ask the user to run `npx canary-lab setup`. Do NOT start from a guessed port.\n2. Then CONFIRM it is the right server: `curl -s http:\u002F\u002F127.0.0.1:\u003Cport>\u002Fmcp\u002Fhealth` and check that `projectRoot` is the workspace you intended. A healthy response does **not** settle the question on its own — a stale UI left behind by a demo or a tarball smoke test answers a port just as convincingly as the right one, and that is how a flight ends up running in someone's throwaway workspace. `projectRoot` matches what you intended → continue and tell the user which workspace. It names a DIFFERENT workspace → this is the wrong server; go back to step 1 rather than adopting it. It is under a temp directory (`\u002Ftmp`, `\u002Fprivate\u002Fvar\u002Ffolders`, `%TEMP%`) → never auto-select it; those are throwaway demo workspaces, so use one only when the user names it explicitly. Only when no live server serves the workspace you want does one need starting.\n3. If the health check fails, start `npx canary-lab ui` from the workspace in a visible long-running terminal; if this client cannot run long-lived commands, ask the user to run `npx canary-lab ui` from the workspace and confirm when it's up.\n4. A healthy `\u002Fmcp\u002Fhealth` means the server is live. On the setup-installed `compact` profile, atomic names such as `get_feature_coverage` are deliberately absent from `tools\u002Flist`; only `exec` is public. Call `exec` with `{\"command\":\"list_tools\",\"arguments\":{}}` before concluding the connection is missing. Only an unknown-tool error for `exec` means this session is not connected — ask the user to run `npx canary-lab setup --force` and reconnect\u002Frestart the client, then retry. Never drive `\u002Fmcp` with a hand-written HTTP\u002FJSON-RPC client (curl included; the health check above is the only direct HTTP use): a custom client bypasses client detection and reconnect handling.\n\n## Keep ownership until an explicit outcome\n\nA successful heal claim makes this run your unfinished task. Claiming does not\nstart a background repair agent.\n\n- After `start_run` returns an ordinary run with your heal claim, or\n  `claim_heal` succeeds, your next run action is `wait_for_heal_task` with that\n  `runId` and the same `session_id`.\n  If you needed to load this skill or `get_workflow_guide(workflow: \"repair\")`, make that\n  call immediately after reading it. A promise to wait is not the call.\n- On `still_waiting`, call the bounded wait again. On `needs_heal`, work on this\n  run's repair, then `signal_run` once and wait again. Give progress updates\n  between calls; do not end the turn with “done while waiting” or switch to suite\n  authoring, coverage mapping, or another run while this duty is unresolved.\n- The wait sends heartbeats only while its call is open. During a long repair,\n  use the run-specific `heartbeat` with `status: \"healing\"` at work milestones\n  before the claim goes stale. Chat, unrelated tool calls, and `get_run_actions`\n  do not supervise this run. Do not send heartbeats merely to keep an abandoned\n  task appearing active.\n- If a tool fails, distinguish a retryable call error from a permission denial\n  or missing connection. Correct an invalid argument and retry the same run;\n  never bypass a denied action through shell commands or a different API.\n  If you cannot continue, tell the user the run ID, last confirmed state, exact\n  blocker, and the action needed to resume or stop. Do not claim it is paused,\n  cancelled, or supervised unless a tool result confirms that outcome.\n- Before yielding to unrelated work or ending with an active run, resolve who\n  will drive it or follow the user's stop instruction. To stop an in-flight\n  heal when the user authorizes stopping repair, call `cancel_heal` and check its\n  result; it ends the run as failed. Do not substitute `abort_run` for this heal\n  action or claim there is no direct stop tool without checking `cancel_heal`.\n  An `abort_run` denial does not prove `cancel_heal` is unavailable, but a denied\n  stop action is not permission to try alternate tools for the same effect.\n  `abort_run` is for a user-requested full abort and requires the human stop form.\n  `pause_run` enters heal mode, and `release_heal` only releases ownership:\n  neither parks nor cancels the run. If stopping is blocked or needs user\n  input, report that the run is still active and needs attention.\n\n## External Run Loop\n\nIf `start_run` returns `type: \"getting_started_busy\"`, a Getting Started demo already owns the workspace. Follow the returned active target (a run, Flight, coverage job, draft, portify, or export) in its current owner; do not start another workflow.\n\n1. Call `list_features` and choose the requested feature. Then pick the mode from the target: a run boots the suite's services from its repos and repairs that code. When the target is a deployed, staging or live host the suite does not boot (\"the staging run\", \"check prod\"), use canary-lab-verify instead — a repair cycle cannot change a remote deployment, so the only \"fix\" left would be editing the test.\n2. 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\u002FCodex 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\"`.\n   Before a fresh run, stale test-to-requirement coverage opens a human elicitation: update coverage first, or run now with stale coverage. Never answer it yourself. Update-first returns `type: \"coverage_update_required\"` and starts nothing: respect `activeJobId` \u002F `flightId` ownership, complete the returned coverage next steps, confirm freshness, then retry `start_run`. Run-now is diagnostic; `coverageStale: true` means the historical percentage is still not current. Decline\u002Fcancel starts nothing. Reusing an active run or restarting a failed\u002Faborted run with `run_ref` continues its recorded suite without this gate.\n   Before a fresh run, a suite that boots no services and whose envset names a deployed host opens a human elicitation: verify the deployed target, or run with repair anyway. It is asked before coverage. Never answer it yourself. Verify returns `type: \"verify_instead\"` with the deployed `targetOrigins` and starts nothing — continue with canary-lab-verify (`list_verification_configs` → `execute_verification`). Only if the fallback `type: \"remote_target_requires_choice\"` reaches you, ask the user; only after they choose run-anyway re-call `start_run` with `remote_target: \"run\"`. Never pass it on your own.\n3. A run boots the commit the feature's repo checkout is sitting on — not the branch's latest. If the user wants the current upstream tip (\"run against latest main\", \"pull first\"), pass `update_repos: true`: before the worktree is cut, Canary Lab does `git fetch` + `git merge --ff-only` on each pinned repo. A repo declared with `track: 'upstream'` in `feature.config.cjs` does this on every run without the flag; `update_repos: false` boots the checkout as-is. If `start_run` returns `type: \"repo_update_refused\"`, nothing started: `repos[]` says per repo why (`dirty`, `diverged`, `detached`, `wrong-branch`, `fetch-failed`, `in-use`). Tell the user which repo and why and let them reconcile — never stash, reset, or discard their work to make the update go through — then re-call `start_run` (or re-call with `update_repos: false` to boot the checked-out commit anyway). `get_feature_repo_status` reports `behindUpstream` \u002F `aheadUpstream` so you can say how stale a checkout is before starting.\n   If `start_run` returns `type: \"repo_collision_requires_choice\"`, another run is using the same app\u002Frepo. The tool requests this choice through elicitation when supported. Only if this fallback result reaches you, 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.\n4. 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 through the `abort_run` human confirmation form 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.\n5. 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\u002Fportify 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\u002FCodex client or the web UI.\n6. 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.\n7. 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` \u002F `passed` \u002F `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.)\n8. If it returns `passed`, summarize using `result.counts.statusLine` and stop.\n9. If it returns `failed`, report the terminal status using `result.counts.statusLine` and relevant failure summary.\n   - If `result.newRunRequired` is present, the suite's one external-effect attempt was already claimed. The app repair is captured but unverified. Review\u002Fpromote its Changes patch, obtain the suite's required approval, and start a fresh run **without** `run_ref`. Do not restart or hand off the spent run.\n   - If `result.serverExited` is present, the Canary Lab server driving the run stopped mid-run, so its services and heal loop stopped too and the run is settled `aborted` — no verdict, nothing left to signal. `signal_run`, `pause_run` and `cancel_heal` on such a run answer `server-exited` the same way. Relay `serverExited.message`, then restart it with `start_run` and `run_ref` set to its run id; its recorded suite and journal carry over.\n   - If the result carries `dirtyTests` (a test file 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.\n   - If the result carries `specEdits` (test files changed **after this run started** — can appear on `passed`, `failed`, or `needs_heal`), the run used the tests recorded at run start, so the changed files were **not tested** and the result says nothing about them. Relay `result.specEdits.message` and follow `result.specEdits.nextSteps`: call get_test_review(runId), show the exact patch (read patchPath if needed), then call review_test_changes(runId, review_revision) WITHOUT wait_for_decision. The human chooses `Accept & commit` or `Restore recorded files`; cancel\u002Fdecline leaves pending. Acceptance binds approval to the displayed revision, commits every reviewed file including helpers\u002Ffixtures\u002Fconfig, and returns a durable receipt with Git and execution outcomes. For an active run continue with wait_for_heal_task and do not signal twice; for an ended passed\u002Ffailed\u002Faborted run call start_run **without** run_ref when the receipt says `new-run-required`. Acceptance preserves the old result and is never a pass. reviewUrl is optional side-by-side inspection, never a required operational step. If elicitation is unavailable or the client declines without a recorded decision, say no human decision was recorded; with a blocked request_id use the browser watcher rule below. For other reviews, use a capable client or the human-chosen browser fallback. A Git commit, clean tree, restart, or chat statement is not a persisted review receipt; only the explicit decision creates one. Changed revisions require a fresh review. No MCP tool can self-approve a test-file change, and a `passed` run with pending `specEdits` is a pass of the **recorded** tests — never report the changed tests as passed. `result.specEdits.hints` is the advisory check: a `kind: \"weaker\"` hint means an assertion was removed or loosened relative to the test that ran — restore it, a weaker assertion is never a repair; `kind: \"cannot-classify\"` means review by hand. Quote `specEdits.disclosure` with any hint you quote.\n10. 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:\n    - **`context.singleAttempt.claimed` true**: the suite's receipt says this run used its one external-effect attempt. Fix app\u002Fservice code, then `signal_run` once with the journal fields. Canary finalizes this run failed\u002Funverified and captures its patch; it does **not** restart services or rerun Playwright. Call `wait_for_heal_task` for the terminal result. A fresh approved run with a new ID is required to verify the promoted fix.\n    - **`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`).\n    - **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.\n    - **`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.\n    - **`context.failedTests` empty AND `context.bootFailure` set**: a **service failed to boot** — no tests ran. Inspect its reason\u002Fclassification, command\u002Fcwd, exit\u002Fsignal, and bounded redacted excerpt before `Read`ing the full logPath. `classification: \"underlying-cause-not-preserved\"` means an outer wrapper did not preserve the underlying rejection or child-process failure: do not guess a root cause; fix that product wrapper to log and rethrow. Then fix the service\u002Fapp code and `signal_run` `kind: \"restart\"` (`context.nextSteps` already reflects this).\n    - **`context.serviceFailure` set**: a required service failed after readiness. Playwright may have partial results; do not infer failures or passes for tests that did not run. Inspect the recorded kind, excerpt, and service log, fix the service\u002Fapp code, then `signal_run` `kind: \"restart\"`. This failure enters healing automatically; `signal_run` means the fix is ready for runner verification.\n\n    - **`context.dependencyBlockers` present**: startup is blocked by incompatible dependencies. Each record names the repository, all affected services, a stable `cause`, `requiredAction`, `worktreePath`, and `logPath` when available. Repair deterministic in-scope problems without elicitation. Ask only for a genuine user choice or missing authority, such as changing another shared checkout. Never offer continue-anyway, mark-compatible, or a revision change as a bypass; different generator inputs do not prove that generated output differs. Fix app\u002Fservice dependencies, not tests or the validator. Then `signal_run` `kind: \"restart\"` on the same run. Both restart and rerun perform a fresh dependency preflight and persist its evidence before any service spawn or test verification. These blockers remain in repeat waits; `get_heal_context` recovers them after reconnect.\n\n    Call `get_run_snapshot` only when you need the verbose raw summary, full counts, or deeper debugging fields.\n11. Follow the run diagnosis policy in `context.nextSteps` or its heal prompt. The default is **parent-only**: read the failure index yourself, diagnose every failure in the parent, and do **not** spawn diagnosis sub-agents. Keep a failure-ID ledger — each failure with its evidence, candidate cause, and addressed or explicitly unresolved status; similar symptoms or a shared service log alone do not prove a common cause. If contradictory evidence or a failed verification cycle needs delegation, record the escalation (failure IDs + reason) in your hypothesis first; escalated children are read-only and return evidence plus a proposed patch. An explicit `per-failure` or `adaptive` policy overrides this default for that run. Under **`per-failure`**, when several tests fail, dispatch one read-only sub-agent per failure in a single parallel round (up to 5 at once), hand each the `failureId`, and have it call `get_failure_detail(runId, failureId)` to investigate just that slice and report back a hypothesis **plus a concrete proposed patch** (the exact file edits \u002F unified diff). Under every policy, sub-agents are read-only — they must **not** touch the working tree or call `signal_run` — and **you** apply all fixes yourself, serially, reconciling by hand if two touch the same file, then `signal_run` once. Applying and signalling stay single-threaded. A sub-agent that comes back empty, or a failure you could not explain, has **not** been cleared — say so in the hypothesis rather than signalling as though its test were addressed.\n12. Fix app\u002Fservice code, not tests, unless the test is provably wrong.\n    - **Edit where the run actually booted.** When the heal context carries `context.worktrees` (repo name → path), this run is isolated and the services under test were booted from those per-run git worktrees — every **portified** feature runs this way, and so does any run the user isolated after a collision. Apply your fix under the `context.worktrees` path. `context.repoBranches[].path` is the **product** repo, which an isolated run never reads (`repoBranches[].sha` is the commit the run booted, and `updatedFromUpstream` says when the start pulled to get there): patch only that and the rerun comes back byte-identical, which reads as \"my fix did not work\" when the fix was right and simply never loaded. The worktree edits are captured at teardown, so nothing is lost. When `context.worktrees` is absent, the run boots the repos in place and `repoBranches[].path` is the edit target.\n13. Call `signal_run` **once** per cycle with `kind: \"rerun\"` for test-only\u002Fapp-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.\n14. Do not call a separate journal-writing tool; the runner records failing tests, changed files, signal, outcome, and diff.\n15. Repeat from `wait_for_heal_task` (looping on `still_waiting`) until the run passes or reaches terminal failure.\n\n## `wait_for_heal_task` result types\n\n| Result | Action |\n| --- | --- |\n| `still_waiting` | Not terminal — re-call with the same `runId` + `session_id`. |\n| `needs_heal` | Fix (see step 10), `signal_run` once, then wait again. |\n| `passed` | Report `result.counts.statusLine`, plus what happened to your fix (see below), then stop. |\n| `failed` | Report `result.counts.statusLine` + failure summary, stop. |\n| `boot_session` | Report which services came up, stop — never wait again. |\n\n## What happens to your fix when the run passes\n\nYour edits are never left only in your head or only on disk. At teardown Canary Lab diffs the\nrun's working copy, saves the result as a patch per repo, and — unless the workspace turned it\noff — opens a **draft pull request** from it. So on `passed`:\n\n- **Do not open a pull request yourself**, and do not push a branch. The run does it, on a branch\n  named per feature + repo, so healing the same feature again updates that same pull request\n  instead of opening a second one.\n- Report what the run reports: the pull request URL when there is one, or the per-repo reason\n  there isn't (gh not signed in, no push rights, the patch no longer applies). Both live on the\n  run's `prAttempt`; the diff itself is on the run's **Changes** tab in the Canary Lab UI.\n- A run that ends red, or that gives up after its cycle cap, opens nothing — a fix that didn't\n  make the tests pass is not a fix to propose.\n\n## Guardrails\n\n- Envset values belong in `features\u002F\u003Cfeature>\u002Fenvsets\u002F\u003Cenv>\u002F\u003Cslot>` in the selected\n  workspace. A suite `.env` is a materialized consumer target, not a second source.\n  Never create pointer-only envsets or target `.runtime\u002Fenvsets`, an envset source,\n  or a personal source checkout merely because an old variable points there.\n  If the user authorizes an envset repair, read\n  `get_workflow_guide(workflow:\"author\")` and trace source → target → consumer\n  before updating the values and consumer together. Otherwise report the mapping\n  problem; repair permission for app code does not authorize a suite migration.\n- `session_id` recipe: generate one id at the start of the conversation (any unique string) and pass that identical value in every tool call for the rest of the conversation.\n- When a blocked start returns `request_id`, carry it into `get_test_review` and `review_test_changes`. On an accepted receipt, resume with `start_run(feature, request_id, session_id)` in the original external client; Restore recorded files cancels that external request, so stop without starting. Canary resumes internal requests itself. Never create a replacement request or switch owners. If the form is unavailable or the client declines without a recorded human decision, show reviewUrl and keep waiting: spawn exactly one background agent for this request if supported, otherwise wait in this turn. The watcher calls `get_test_review` in its own MCP session for a fresh read-only `browser_wait_token`, then repeats `review_test_changes` with `wait_for_decision:true`. It stops on restored\u002Fcancelled or changed revision. After reconnect, get a fresh token and read the same durable request. A wait, transport notification, Git commit, or agent-written argument never approves changes.\n- A FLIGHT can hand you a run via an `external-work` checkpoint carrying a `runId`: the run was started in external-heal mode UNCLAIMED for you — `claim_heal` it with your own `session_id` and drive this exact loop, then re-call `get_flight` and release with `respond_flight_checkpoint(flightId, choice: \"submit\")` once the run is terminal (a failed run is a valid terminal answer; Canary reads the verdict from the run record). If that re-check carries `checkpoint.data.takeoverRequestedAt`, stop your repair work and release with `choice: \"run-internally\"` instead — do not submit. Never `abort_run` a flight-owned run to escape it, and never start another run for that feature while the flight owns it. The flight will wait on you indefinitely — a parked hand-off has no deadline — so do not end your turn with it open; after 45 minutes with no `get_flight` contact the read reports `handOffIdle` on it.\n- `heartbeat` is a low-level liveness refresh for long local repair stretches. `wait_for_heal_task` heartbeats while waiting, and `signal_run` and `get_heal_context` refresh liveness, so call explicit `heartbeat` only before or after a long stretch of local `Read` \u002F `Edit` \u002F `Write` \u002F `Bash` work.\n- `start_run` is the single entrypoint for start\u002Fresume\u002Frestart intent. A fresh run pauses for a human choice when coverage mapping is stale; no agent flag bypasses that gate. A matching active run is continued even with `force_new:true`; intentional concurrent runs of the same feature start from the Run panel. Across different features, a same-app collision returns `repo_collision_requires_choice` (resolve with `isolation: \"worktree\"` or `\"queue\"`); over the resource budget, the run is `queued` and starts automatically.\n- For requests like \"rerun 7cvh\", `start_run` resolves the run suffix and restarts that same failed\u002Faborted run in remaining-test mode. Canary Lab reruns failed tests first, then skipped tests, then pending\u002Fnot-run tests; do not tell the user no test filter exists.\n- To re-execute a run, reuse it: prefer this rerun (`start_run` with `run_ref`) over `abort_run` + a fresh start — rerun re-executes only the failed\u002Fskipped\u002Fpending tests and avoids the abort + repo-collision dance. The rerun already re-runs skipped **and** pending tests, so it is complete — do **not** `force_new` just to avoid \"skipped\" tests. On a **portified** feature, `force_new` spins a brand-new per-run worktree and resets the heal journal to Iteration 1, discarding the prior cycles. Start a fresh full run only when prior passes are genuinely invalidated (e.g. a global data\u002Fstate change), and you rarely need to `abort_run` first to do so.\n- After changing code or tests, never call `start_run` to verify. The signal requests runner verification; it is not a claim that the fix already passes. Do not start services or run Playwright, smoke, end-to-end, or other runtime checks yourself. Canary Lab owns affected-service restart, health checks, and targeted Playwright verification after the signal. If an edit command failed or syntax is uncertain, run at most one fast non-network static check before signalling. Verification means `signal_run` with `hypothesis` and `fixDescription`, then `wait_for_heal_task` on the same `runId`.\n- `wait_for_heal_task` waits while a submitted signal is pending and while a live runner transitions from a test failure into healing. Only a finalized run returns a terminal result; keep waiting on this run.\n- During normal healing, omit `run_ref`\u002F`force_new` so `start_run` continues the active run. `force_new` cannot replace an active run. Pending suite changes block fresh starts until `get_test_review` then `review_test_changes` records the human's `Accept & commit` or `Restore recorded files` decision for the exact revision. The acceptance receipt says whether the active run reran or a new run is required; start the latter without `run_ref`. Restarting preserves recorded tests and review history but cannot turn acceptance into tested evidence.\n- Environment exclusions must be declared in the recorded suite before execution with the `canary:environments` annotation. `counts.notApplicable` stays part of `counts.skipped`, never `counts.passed`; other skips and not-run tests still block completion. Never add an exclusion to evade a failure. Changing applicability during a repair requires the same human test review as any other test edit.\n- Never compute passed count as `summary.total - summary.failed.length`. Use `result.counts.statusLine`, `result.counts.passed`, or `summary.passed` for pass counts. Treat tests absent from `passedNames`, `failed`, and `skippedNames` as not run, not passed.\n- Do not call `abort_run` unless the user asks. Its human form must be accepted; agent-written `confirm:true` is not authorization. If forms are unavailable, the human uses Stop in the Run panel. Never answer that form yourself.\n- Prefer compact `get_heal_context` when you need to refresh failure artifacts outside the wait loop. Use `get_run_snapshot` only for verbose fallback\u002Fdebugging context.\n- Read responses are slim by default to protect your context, but the full data is always one step away — pull it whenever a failure needs deeper detail (just never in a wait loop): `get_run` omits the raw `lifecycleEvents`\u002F`playwrightArtifacts`\u002F`playbackEvents` (call it again with `includeRaw:true` to inline them); `get_run_snapshot` returns the heal-index and journal as **paths** to `Read`; `list_runs` returns the newest 20 (raise `limit`).\n- On any terminal run, recommend the exact next skill invocation\n  `\u002Fcanary-lab-export \u003CrunId>`, replacing the placeholder with the terminal\n  run's returned ID. Do not substitute `npx canary-lab export`; the export skill\n  uses the connected MCP server and preserves the exact run selection.\n- Record concise, factual journal notes. Do not paste raw transcripts.\n- When the run is waiting for external heal, Canary Lab is the source of truth for status, artifacts, and rerun\u002Frestart signals.\n","# Canary Lab — Run + Heal Loop\n\n## User input through MCP 2.0\n\nUse the native Canary connector in the requesting chat so SDK 2.0 elicitation\n(`input_required`) reaches the human there. Do not substitute a shell MCP client,\nanswer a form yourself, or recreate the question in chat. Existing explicit\ninstructions and autopilot choices still apply without another ask.\n\nForm decisions have one shared record. The native form links to the same approval\nin Canary Notifications, also surfaced above the Flight page. On `needs-input`\nwith `approvalId` and `reviewUrl`, show that link and call `wait_for_approval` with\nthe ID; repeat on `still_waiting`. The human answers once in either surface.\nA browser answer resolves the server decision; some clients keep their native\nform visible until the human dismisses it. Dismissal then returns the stored\nresult. Client decline\u002Fcancel alone does not represent a human decision.\n\nWithout an `approvalId`, leave `needs-input` pending after decline\u002Fcancel, stale\ninput, or an unfinished UI action; do not automatically retry or repeat the\nquestion. Setup\u002Freconnection can use chat while MCP is unavailable. Never collect\npasswords, API keys, or access tokens in chat or form elicitation; use the returned\nCanary UI URL for secret entry.\n\n## MCP Invocation\n\nSetup and the plugin expose one public Canary Lab MCP tool: `exec` (usually\nrendered as `mcp__Canary_Lab__exec`). Every\nCanary Lab tool name below is the exact `command` value, not a separate public\ntool. For a feature-scoped command, replace both placeholders in this shape:\n\n```json\n{\"command\":\"\u003Cexact_tool_name>\",\"arguments\":{\"feature\":\"\u003Cfeature_name>\"}}\n```\n\nThis is the envelope shape, not every command's complete schema. Add the\nfields that command declares inside `arguments`; call `describe_tool` when a\nfield is uncertain.\n\nNever invent a wrapper verb such as `learn` or `call`, embed JSON in a command\nstring, or turn arguments into flags. Keep fields such as `confirm: true` inside\n`arguments`. Use `list_tools`, `search_tools`, or `describe_tool` as the\n`command` when discovery is needed. A deliberately selected focused or `full`\nprofile still exposes atomic tools for debugging; the setup-installed path is\n`compact` + `exec`.\n\nCanary Lab owns the run verdicts and artifacts; this client applies the\nfixes. These tools arrive via the Canary Lab MCP server. If this client is\nalready connected (the plugin connects with `compact`), skip this step. To\nconfigure the same connection manually: `npx canary-lab mcp --profile compact`.\n\n## Arguments\n\nAn invocation argument (`\u002Fcanary-lab-run \u003Csuite>` — the Getting Started\nguide's \"Repair a Broken Suite\" card emits exactly this shape) is a suite\n(feature) name in the connected workspace: pass it to `start_run` directly.\n\n## Workspace Bootstrap\n\n1. Find the LIVE server first: read `~\u002F.canary-lab\u002Factive-servers.json`, which records `projectRoot`, `port` and `pid` for every UI that registered. A stopped server's entry LINGERS — the file is only rewritten when the next server registers — so an entry is a candidate, not proof: the health check below is what confirms it. One entry → that is your server and its `port`. Several → take the one whose `projectRoot` is the workspace the user means. None → fall back to `~\u002F.canary-lab\u002Fworkspaces.json` (Windows: `%USERPROFILE%\\.canary-lab\\workspaces.json`): one workspace → use it, several → ask which, none → ask the user to run `npx canary-lab setup`. Do NOT start from a guessed port.\n2. Then CONFIRM it is the right server: `curl -s http:\u002F\u002F127.0.0.1:\u003Cport>\u002Fmcp\u002Fhealth` and check that `projectRoot` is the workspace you intended. A healthy response does **not** settle the question on its own — a stale UI left behind by a demo or a tarball smoke test answers a port just as convincingly as the right one, and that is how a flight ends up running in someone's throwaway workspace. `projectRoot` matches what you intended → continue and tell the user which workspace. It names a DIFFERENT workspace → this is the wrong server; go back to step 1 rather than adopting it. It is under a temp directory (`\u002Ftmp`, `\u002Fprivate\u002Fvar\u002Ffolders`, `%TEMP%`) → never auto-select it; those are throwaway demo workspaces, so use one only when the user names it explicitly. Only when no live server serves the workspace you want does one need starting.\n3. If the health check fails, start `npx canary-lab ui` from the workspace in a visible long-running terminal; if this client cannot run long-lived commands, ask the user to run `npx canary-lab ui` from the workspace and confirm when it's up.\n4. A healthy `\u002Fmcp\u002Fhealth` means the server is live. On the setup-installed `compact` profile, atomic names such as `get_feature_coverage` are deliberately absent from `tools\u002Flist`; only `exec` is public. Call `exec` with `{\"command\":\"list_tools\",\"arguments\":{}}` before concluding the connection is missing. Only an unknown-tool error for `exec` means this session is not connected — ask the user to run `npx canary-lab setup --force` and reconnect\u002Frestart the client, then retry. Never drive `\u002Fmcp` with a hand-written HTTP\u002FJSON-RPC client (curl included; the health check above is the only direct HTTP use): a custom client bypasses client detection and reconnect handling.\n\n## Keep ownership until an explicit outcome\n\nA successful heal claim makes this run your unfinished task. Claiming does not\nstart a background repair agent.\n\n- After `start_run` returns an ordinary run with your heal claim, or\n  `claim_heal` succeeds, your next run action is `wait_for_heal_task` with that\n  `runId` and the same `session_id`.\n  If you needed to load this skill or `get_workflow_guide(workflow: \"repair\")`, make that\n  call immediately after reading it. A promise to wait is not the call.\n- On `still_waiting`, call the bounded wait again. On `needs_heal`, work on this\n  run's repair, then `signal_run` once and wait again. Give progress updates\n  between calls; do not end the turn with “done while waiting” or switch to suite\n  authoring, coverage mapping, or another run while this duty is unresolved.\n- The wait sends heartbeats only while its call is open. During a long repair,\n  use the run-specific `heartbeat` with `status: \"healing\"` at work milestones\n  before the claim goes stale. Chat, unrelated tool calls, and `get_run_actions`\n  do not supervise this run. Do not send heartbeats merely to keep an abandoned\n  task appearing active.\n- If a tool fails, distinguish a retryable call error from a permission denial\n  or missing connection. Correct an invalid argument and retry the same run;\n  never bypass a denied action through shell commands or a different API.\n  If you cannot continue, tell the user the run ID, last confirmed state, exact\n  blocker, and the action needed to resume or stop. Do not claim it is paused,\n  cancelled, or supervised unless a tool result confirms that outcome.\n- Before yielding to unrelated work or ending with an active run, resolve who\n  will drive it or follow the user's stop instruction. To stop an in-flight\n  heal when the user authorizes stopping repair, call `cancel_heal` and check its\n  result; it ends the run as failed. Do not substitute `abort_run` for this heal\n  action or claim there is no direct stop tool without checking `cancel_heal`.\n  An `abort_run` denial does not prove `cancel_heal` is unavailable, but a denied\n  stop action is not permission to try alternate tools for the same effect.\n  `abort_run` is for a user-requested full abort and requires the human stop form.\n  `pause_run` enters heal mode, and `release_heal` only releases ownership:\n  neither parks nor cancels the run. If stopping is blocked or needs user\n  input, report that the run is still active and needs attention.\n\n## External Run Loop\n\nIf `start_run` returns `type: \"getting_started_busy\"`, a Getting Started demo already owns the workspace. Follow the returned active target (a run, Flight, coverage job, draft, portify, or export) in its current owner; do not start another workflow.\n\n1. Call `list_features` and choose the requested feature. Then pick the mode from the target: a run boots the suite's services from its repos and repairs that code. When the target is a deployed, staging or live host the suite does not boot (\"the staging run\", \"check prod\"), use canary-lab-verify instead — a repair cycle cannot change a remote deployment, so the only \"fix\" left would be editing the test.\n2. 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\u002FCodex 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\"`.\n   Before a fresh run, stale test-to-requirement coverage opens a human elicitation: update coverage first, or run now with stale coverage. Never answer it yourself. Update-first returns `type: \"coverage_update_required\"` and starts nothing: respect `activeJobId` \u002F `flightId` ownership, complete the returned coverage next steps, confirm freshness, then retry `start_run`. Run-now is diagnostic; `coverageStale: true` means the historical percentage is still not current. Decline\u002Fcancel starts nothing. Reusing an active run or restarting a failed\u002Faborted run with `run_ref` continues its recorded suite without this gate.\n   Before a fresh run, a suite that boots no services and whose envset names a deployed host opens a human elicitation: verify the deployed target, or run with repair anyway. It is asked before coverage. Never answer it yourself. Verify returns `type: \"verify_instead\"` with the deployed `targetOrigins` and starts nothing — continue with canary-lab-verify (`list_verification_configs` → `execute_verification`). Only if the fallback `type: \"remote_target_requires_choice\"` reaches you, ask the user; only after they choose run-anyway re-call `start_run` with `remote_target: \"run\"`. Never pass it on your own.\n3. A run boots the commit the feature's repo checkout is sitting on — not the branch's latest. If the user wants the current upstream tip (\"run against latest main\", \"pull first\"), pass `update_repos: true`: before the worktree is cut, Canary Lab does `git fetch` + `git merge --ff-only` on each pinned repo. A repo declared with `track: 'upstream'` in `feature.config.cjs` does this on every run without the flag; `update_repos: false` boots the checkout as-is. If `start_run` returns `type: \"repo_update_refused\"`, nothing started: `repos[]` says per repo why (`dirty`, `diverged`, `detached`, `wrong-branch`, `fetch-failed`, `in-use`). Tell the user which repo and why and let them reconcile — never stash, reset, or discard their work to make the update go through — then re-call `start_run` (or re-call with `update_repos: false` to boot the checked-out commit anyway). `get_feature_repo_status` reports `behindUpstream` \u002F `aheadUpstream` so you can say how stale a checkout is before starting.\n   If `start_run` returns `type: \"repo_collision_requires_choice\"`, another run is using the same app\u002Frepo. The tool requests this choice through elicitation when supported. Only if this fallback result reaches you, 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.\n4. 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 through the `abort_run` human confirmation form 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.\n5. 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\u002Fportify 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\u002FCodex client or the web UI.\n6. 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.\n7. 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` \u002F `passed` \u002F `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.)\n8. If it returns `passed`, summarize using `result.counts.statusLine` and stop.\n9. If it returns `failed`, report the terminal status using `result.counts.statusLine` and relevant failure summary.\n   - If `result.newRunRequired` is present, the suite's one external-effect attempt was already claimed. The app repair is captured but unverified. Review\u002Fpromote its Changes patch, obtain the suite's required approval, and start a fresh run **without** `run_ref`. Do not restart or hand off the spent run.\n   - If `result.serverExited` is present, the Canary Lab server driving the run stopped mid-run, so its services and heal loop stopped too and the run is settled `aborted` — no verdict, nothing left to signal. `signal_run`, `pause_run` and `cancel_heal` on such a run answer `server-exited` the same way. Relay `serverExited.message`, then restart it with `start_run` and `run_ref` set to its run id; its recorded suite and journal carry over.\n   - If the result carries `dirtyTests` (a test file 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.\n   - If the result carries `specEdits` (test files changed **after this run started** — can appear on `passed`, `failed`, or `needs_heal`), the run used the tests recorded at run start, so the changed files were **not tested** and the result says nothing about them. Relay `result.specEdits.message` and follow `result.specEdits.nextSteps`: call get_test_review(runId), show the exact patch (read patchPath if needed), then call review_test_changes(runId, review_revision) WITHOUT wait_for_decision. The human chooses `Accept & commit` or `Restore recorded files`; cancel\u002Fdecline leaves pending. Acceptance binds approval to the displayed revision, commits every reviewed file including helpers\u002Ffixtures\u002Fconfig, and returns a durable receipt with Git and execution outcomes. For an active run continue with wait_for_heal_task and do not signal twice; for an ended passed\u002Ffailed\u002Faborted run call start_run **without** run_ref when the receipt says `new-run-required`. Acceptance preserves the old result and is never a pass. reviewUrl is optional side-by-side inspection, never a required operational step. If elicitation is unavailable or the client declines without a recorded decision, say no human decision was recorded; with a blocked request_id use the browser watcher rule below. For other reviews, use a capable client or the human-chosen browser fallback. A Git commit, clean tree, restart, or chat statement is not a persisted review receipt; only the explicit decision creates one. Changed revisions require a fresh review. No MCP tool can self-approve a test-file change, and a `passed` run with pending `specEdits` is a pass of the **recorded** tests — never report the changed tests as passed. `result.specEdits.hints` is the advisory check: a `kind: \"weaker\"` hint means an assertion was removed or loosened relative to the test that ran — restore it, a weaker assertion is never a repair; `kind: \"cannot-classify\"` means review by hand. Quote `specEdits.disclosure` with any hint you quote.\n10. 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:\n    - **`context.singleAttempt.claimed` true**: the suite's receipt says this run used its one external-effect attempt. Fix app\u002Fservice code, then `signal_run` once with the journal fields. Canary finalizes this run failed\u002Funverified and captures its patch; it does **not** restart services or rerun Playwright. Call `wait_for_heal_task` for the terminal result. A fresh approved run with a new ID is required to verify the promoted fix.\n    - **`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`).\n    - **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.\n    - **`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.\n    - **`context.failedTests` empty AND `context.bootFailure` set**: a **service failed to boot** — no tests ran. Inspect its reason\u002Fclassification, command\u002Fcwd, exit\u002Fsignal, and bounded redacted excerpt before `Read`ing the full logPath. `classification: \"underlying-cause-not-preserved\"` means an outer wrapper did not preserve the underlying rejection or child-process failure: do not guess a root cause; fix that product wrapper to log and rethrow. Then fix the service\u002Fapp code and `signal_run` `kind: \"restart\"` (`context.nextSteps` already reflects this).\n    - **`context.serviceFailure` set**: a required service failed after readiness. Playwright may have partial results; do not infer failures or passes for tests that did not run. Inspect the recorded kind, excerpt, and service log, fix the service\u002Fapp code, then `signal_run` `kind: \"restart\"`. This failure enters healing automatically; `signal_run` means the fix is ready for runner verification.\n\n    - **`context.dependencyBlockers` present**: startup is blocked by incompatible dependencies. Each record names the repository, all affected services, a stable `cause`, `requiredAction`, `worktreePath`, and `logPath` when available. Repair deterministic in-scope problems without elicitation. Ask only for a genuine user choice or missing authority, such as changing another shared checkout. Never offer continue-anyway, mark-compatible, or a revision change as a bypass; different generator inputs do not prove that generated output differs. Fix app\u002Fservice dependencies, not tests or the validator. Then `signal_run` `kind: \"restart\"` on the same run. Both restart and rerun perform a fresh dependency preflight and persist its evidence before any service spawn or test verification. These blockers remain in repeat waits; `get_heal_context` recovers them after reconnect.\n\n    Call `get_run_snapshot` only when you need the verbose raw summary, full counts, or deeper debugging fields.\n11. Follow the run diagnosis policy in `context.nextSteps` or its heal prompt. The default is **parent-only**: read the failure index yourself, diagnose every failure in the parent, and do **not** spawn diagnosis sub-agents. Keep a failure-ID ledger — each failure with its evidence, candidate cause, and addressed or explicitly unresolved status; similar symptoms or a shared service log alone do not prove a common cause. If contradictory evidence or a failed verification cycle needs delegation, record the escalation (failure IDs + reason) in your hypothesis first; escalated children are read-only and return evidence plus a proposed patch. An explicit `per-failure` or `adaptive` policy overrides this default for that run. Under **`per-failure`**, when several tests fail, dispatch one read-only sub-agent per failure in a single parallel round (up to 5 at once), hand each the `failureId`, and have it call `get_failure_detail(runId, failureId)` to investigate just that slice and report back a hypothesis **plus a concrete proposed patch** (the exact file edits \u002F unified diff). Under every policy, sub-agents are read-only — they must **not** touch the working tree or call `signal_run` — and **you** apply all fixes yourself, serially, reconciling by hand if two touch the same file, then `signal_run` once. Applying and signalling stay single-threaded. A sub-agent that comes back empty, or a failure you could not explain, has **not** been cleared — say so in the hypothesis rather than signalling as though its test were addressed.\n12. Fix app\u002Fservice code, not tests, unless the test is provably wrong.\n    - **Edit where the run actually booted.** When the heal context carries `context.worktrees` (repo name → path), this run is isolated and the services under test were booted from those per-run git worktrees — every **portified** feature runs this way, and so does any run the user isolated after a collision. Apply your fix under the `context.worktrees` path. `context.repoBranches[].path` is the **product** repo, which an isolated run never reads (`repoBranches[].sha` is the commit the run booted, and `updatedFromUpstream` says when the start pulled to get there): patch only that and the rerun comes back byte-identical, which reads as \"my fix did not work\" when the fix was right and simply never loaded. The worktree edits are captured at teardown, so nothing is lost. When `context.worktrees` is absent, the run boots the repos in place and `repoBranches[].path` is the edit target.\n13. Call `signal_run` **once** per cycle with `kind: \"rerun\"` for test-only\u002Fapp-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.\n14. Do not call a separate journal-writing tool; the runner records failing tests, changed files, signal, outcome, and diff.\n15. Repeat from `wait_for_heal_task` (looping on `still_waiting`) until the run passes or reaches terminal failure.\n\n## `wait_for_heal_task` result types\n\n| Result | Action |\n| --- | --- |\n| `still_waiting` | Not terminal — re-call with the same `runId` + `session_id`. |\n| `needs_heal` | Fix (see step 10), `signal_run` once, then wait again. |\n| `passed` | Report `result.counts.statusLine`, plus what happened to your fix (see below), then stop. |\n| `failed` | Report `result.counts.statusLine` + failure summary, stop. |\n| `boot_session` | Report which services came up, stop — never wait again. |\n\n## What happens to your fix when the run passes\n\nYour edits are never left only in your head or only on disk. At teardown Canary Lab diffs the\nrun's working copy, saves the result as a patch per repo, and — unless the workspace turned it\noff — opens a **draft pull request** from it. So on `passed`:\n\n- **Do not open a pull request yourself**, and do not push a branch. The run does it, on a branch\n  named per feature + repo, so healing the same feature again updates that same pull request\n  instead of opening a second one.\n- Report what the run reports: the pull request URL when there is one, or the per-repo reason\n  there isn't (gh not signed in, no push rights, the patch no longer applies). Both live on the\n  run's `prAttempt`; the diff itself is on the run's **Changes** tab in the Canary Lab UI.\n- A run that ends red, or that gives up after its cycle cap, opens nothing — a fix that didn't\n  make the tests pass is not a fix to propose.\n\n## Guardrails\n\n- Envset values belong in `features\u002F\u003Cfeature>\u002Fenvsets\u002F\u003Cenv>\u002F\u003Cslot>` in the selected\n  workspace. A suite `.env` is a materialized consumer target, not a second source.\n  Never create pointer-only envsets or target `.runtime\u002Fenvsets`, an envset source,\n  or a personal source checkout merely because an old variable points there.\n  If the user authorizes an envset repair, read\n  `get_workflow_guide(workflow:\"author\")` and trace source → target → consumer\n  before updating the values and consumer together. Otherwise report the mapping\n  problem; repair permission for app code does not authorize a suite migration.\n- `session_id` recipe: generate one id at the start of the conversation (any unique string) and pass that identical value in every tool call for the rest of the conversation.\n- When a blocked start returns `request_id`, carry it into `get_test_review` and `review_test_changes`. On an accepted receipt, resume with `start_run(feature, request_id, session_id)` in the original external client; Restore recorded files cancels that external request, so stop without starting. Canary resumes internal requests itself. Never create a replacement request or switch owners. If the form is unavailable or the client declines without a recorded human decision, show reviewUrl and keep waiting: spawn exactly one background agent for this request if supported, otherwise wait in this turn. The watcher calls `get_test_review` in its own MCP session for a fresh read-only `browser_wait_token`, then repeats `review_test_changes` with `wait_for_decision:true`. It stops on restored\u002Fcancelled or changed revision. After reconnect, get a fresh token and read the same durable request. A wait, transport notification, Git commit, or agent-written argument never approves changes.\n- A FLIGHT can hand you a run via an `external-work` checkpoint carrying a `runId`: the run was started in external-heal mode UNCLAIMED for you — `claim_heal` it with your own `session_id` and drive this exact loop, then re-call `get_flight` and release with `respond_flight_checkpoint(flightId, choice: \"submit\")` once the run is terminal (a failed run is a valid terminal answer; Canary reads the verdict from the run record). If that re-check carries `checkpoint.data.takeoverRequestedAt`, stop your repair work and release with `choice: \"run-internally\"` instead — do not submit. Never `abort_run` a flight-owned run to escape it, and never start another run for that feature while the flight owns it. The flight will wait on you indefinitely — a parked hand-off has no deadline — so do not end your turn with it open; after 45 minutes with no `get_flight` contact the read reports `handOffIdle` on it.\n- `heartbeat` is a low-level liveness refresh for long local repair stretches. `wait_for_heal_task` heartbeats while waiting, and `signal_run` and `get_heal_context` refresh liveness, so call explicit `heartbeat` only before or after a long stretch of local `Read` \u002F `Edit` \u002F `Write` \u002F `Bash` work.\n- `start_run` is the single entrypoint for start\u002Fresume\u002Frestart intent. A fresh run pauses for a human choice when coverage mapping is stale; no agent flag bypasses that gate. A matching active run is continued even with `force_new:true`; intentional concurrent runs of the same feature start from the Run panel. Across different features, a same-app collision returns `repo_collision_requires_choice` (resolve with `isolation: \"worktree\"` or `\"queue\"`); over the resource budget, the run is `queued` and starts automatically.\n- For requests like \"rerun 7cvh\", `start_run` resolves the run suffix and restarts that same failed\u002Faborted run in remaining-test mode. Canary Lab reruns failed tests first, then skipped tests, then pending\u002Fnot-run tests; do not tell the user no test filter exists.\n- To re-execute a run, reuse it: prefer this rerun (`start_run` with `run_ref`) over `abort_run` + a fresh start — rerun re-executes only the failed\u002Fskipped\u002Fpending tests and avoids the abort + repo-collision dance. The rerun already re-runs skipped **and** pending tests, so it is complete — do **not** `force_new` just to avoid \"skipped\" tests. On a **portified** feature, `force_new` spins a brand-new per-run worktree and resets the heal journal to Iteration 1, discarding the prior cycles. Start a fresh full run only when prior passes are genuinely invalidated (e.g. a global data\u002Fstate change), and you rarely need to `abort_run` first to do so.\n- After changing code or tests, never call `start_run` to verify. The signal requests runner verification; it is not a claim that the fix already passes. Do not start services or run Playwright, smoke, end-to-end, or other runtime checks yourself. Canary Lab owns affected-service restart, health checks, and targeted Playwright verification after the signal. If an edit command failed or syntax is uncertain, run at most one fast non-network static check before signalling. Verification means `signal_run` with `hypothesis` and `fixDescription`, then `wait_for_heal_task` on the same `runId`.\n- `wait_for_heal_task` waits while a submitted signal is pending and while a live runner transitions from a test failure into healing. Only a finalized run returns a terminal result; keep waiting on this run.\n- During normal healing, omit `run_ref`\u002F`force_new` so `start_run` continues the active run. `force_new` cannot replace an active run. Pending suite changes block fresh starts until `get_test_review` then `review_test_changes` records the human's `Accept & commit` or `Restore recorded files` decision for the exact revision. The acceptance receipt says whether the active run reran or a new run is required; start the latter without `run_ref`. Restarting preserves recorded tests and review history but cannot turn acceptance into tested evidence.\n- Environment exclusions must be declared in the recorded suite before execution with the `canary:environments` annotation. `counts.notApplicable` stays part of `counts.skipped`, never `counts.passed`; other skips and not-run tests still block completion. Never add an exclusion to evade a failure. Changing applicability during a repair requires the same human test review as any other test edit.\n- Never compute passed count as `summary.total - summary.failed.length`. Use `result.counts.statusLine`, `result.counts.passed`, or `summary.passed` for pass counts. Treat tests absent from `passedNames`, `failed`, and `skippedNames` as not run, not passed.\n- Do not call `abort_run` unless the user asks. Its human form must be accepted; agent-written `confirm:true` is not authorization. If forms are unavailable, the human uses Stop in the Run panel. Never answer that form yourself.\n- Prefer compact `get_heal_context` when you need to refresh failure artifacts outside the wait loop. Use `get_run_snapshot` only for verbose fallback\u002Fdebugging context.\n- Read responses are slim by default to protect your context, but the full data is always one step away — pull it whenever a failure needs deeper detail (just never in a wait loop): `get_run` omits the raw `lifecycleEvents`\u002F`playwrightArtifacts`\u002F`playbackEvents` (call it again with `includeRaw:true` to inline them); `get_run_snapshot` returns the heal-index and journal as **paths** to `Read`; `list_runs` returns the newest 20 (raise `limit`).\n- On any terminal run, recommend the exact next skill invocation\n  `\u002Fcanary-lab-export \u003CrunId>`, replacing the placeholder with the terminal\n  run's returned ID. Do not substitute `npx canary-lab export`; the export skill\n  uses the connected MCP server and preserves the exact run selection.\n- Record concise, factual journal notes. Do not paste raw transcripts.\n- When the run is waiting for external heal, Canary Lab is the source of truth for status, artifacts, and rerun\u002Frestart signals.\n",{"name":5,"type":57,"description":7},"skill","main","2c85305cab69e5f773d59ea79a64a0b049a98345","agent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab-run",0,"https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fcanary-lab","https:\u002F\u002Fgithub.com\u002Fferterahadi","https:\u002F\u002Fgithub.com\u002Fferterahadi-oddle","Shipping well-tested, production-ready, vibe-coded code using today’s latest form of engineering - harness, agentic, loop, graph, or whatever drops next","2026-11-05 11:59:56",{"source":8,"skillName":5,"commands":69,"prompt":79,"zipAvailable":80},[70,73,76],{"tool":71,"command":72},"npx","npx skills add ferterahadi\u002Fcanary-lab --skill canary-lab-run",{"tool":74,"command":75},"bunx","bunx skills add ferterahadi\u002Fcanary-lab --skill canary-lab-run",{"tool":77,"command":78},"pnpm","pnpm dlx skills add ferterahadi\u002Fcanary-lab --skill canary-lab-run","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fagent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab-run\u002FSKILL.md",true,[],[83,92,102,117,136,154],{"skillNo":84,"name":85,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":90,"publishedAt":52},"01M6XCWFJ75NNVF0N8C0XGNC17","off-by-one","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fapps\u002Fweb-server\u002Fprompts\u002Fsabotage-skills\u002Foff-by-one\u002Fskill.md",[88,89],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[91],"off by one",{"skillNo":93,"name":94,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":99,"publishedAt":52},"01M6XCWBYW1QHVFKSDQCCRRRDT","multi-failure-cascade","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fapps\u002Fweb-server\u002Fprompts\u002Fsabotage-skills\u002Fmulti-failure-cascade\u002Fskill.md",[97,98],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[100,101],"multi","multi failure cascade",{"skillNo":103,"name":104,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":112,"publishedAt":52},"01M6XCW8XA7QY74Q18D9PGFFMD","broken-delete-contract","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fapps\u002Fweb-server\u002Fprompts\u002Fsabotage-skills\u002Fbroken-delete-contract\u002Fskill.md",[107,108,109],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":110,"categoryName":111,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000037","律师与法务",[113,114,115,116],"broken delete contract","delete","contract","broken",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":125,"publishedAt":52},"01M6XCW439KA486AN2QZACNYTC","canary-lab","Use when the user wants Canary Lab to take a product repo end to end — \"test this app\", \"onboard this repo\", \"run a flight\", \"evaluate what I just built\" — through the flight pipeline (start_flight \u002F get_flight \u002F respond_flight_checkpoint over MCP). One conducted pipeline continues a configured suite or takes bare repo(s) to a green, covered, healed run and publishes a downloadable Report; Parallel setup continues as Canary-owned background work. Use a focused skill only when the user asks for that single capability without asking for a Flight or \u002Fcanary-lab.","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fagent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab\u002FSKILL.md",[123,124],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[36,35,33,32,126,46,127,51,128,129,130,131,41,42,44,132,133,134,135,48],"product","pipeline","work","take","only","green","respond","suite","capability","bare",{"skillNo":137,"name":138,"nameZh":6,"description":139,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":140,"categories":141,"keywords":144,"publishedAt":52},"01M6XCVYQ31KC4ZXCD5QMRKXEE","canary-lab-verify","Use when verifying a Canary Lab feature against a RUNNING target — a deployed environment (\"verify checkout on staging\", \"run the saved verify config\", \"check the deployed app\") or a locally booted app (\"verify a running app\") — through the verify MCP tools (list\u002Fget\u002Fcreate\u002Fupdate_verification_config, boot_services, execute_verification, get_verification_result). No heal loop. For run + heal use canary-lab-run; for end-to-end onboarding use canary-lab (flight).","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fagent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab-verify\u002FSKILL.md",[142,143],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[145,33,35,146,36,44,51,48,147,148,32,149,150,37,38,151,41,42,152,153],"canary lab verify","verify","config","create","update","verification","list","checkout","result",{"skillNo":155,"name":156,"nameZh":6,"description":157,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":158,"categories":159,"keywords":162,"publishedAt":52},"01M6XCVKRQMFVJ76MBN1Z4T68H","canary-lab-repair-discovery","Repair Playwright test discovery for an existing Canary Lab suite, reporting live progress to the Tests column through MCP. Use when the test list cannot load or Canary offers an In your agent discovery-repair command. Does not execute tests or repair a failing test run.","https:\u002F\u002Fgithub.com\u002Fferterahadi\u002Fcanary-lab\u002Fblob\u002Fmain\u002Fagent-integrations\u002Fplugin\u002Fcanary-lab\u002Fskills\u002Fcanary-lab-repair-discovery\u002FSKILL.md",[160,161],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[33,163,35,164,165,36,32,166,167,151,168,169,170,171,172,173,133,48,49,51],"canary lab repair discovery","discovery","repair","reporting","command","test list","live","tests","progress","agent discovery","for"]