canon-ai 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.5.0] — 2026-09-29
8
+
9
+ ### Added
10
+
11
+ - **A reviewer that can't reach a verdict now stops the run for a human.** If a code-review lens returns nothing usable twice, the foreman sets `code_review` to `blocked` instead of being pushed to invent a verdict; recover with `canon task reset-code-review` for each bundle member, then `canon run`.
12
+ - **Plans include a short feasibility check.** Pipeline, reroute, and `/canon-spec` XS plans now confirm the code they rely on exists, trace the real call path and callers, and name the input that makes each new test fail without the fix.
13
+ - **`CLAUDE_MODEL_LIGHT` and `CLAUDE_MODEL_STRONG` override each tier's model.** Existing per-phase pins and `CLAUDE_MODEL` still work and take precedence in that order.
14
+
15
+ ### Changed
16
+
17
+ - **Claude defaults are re-baselined for the 5.5 generation.** Spec, plan, and code review run Sonnet `medium` for XS/S, Opus `medium` for M/L, and Opus `high` for XL/delicate; QA runs Sonnet `medium` at every size, and nothing runs at `xhigh`. A model pin now replaces only the model, so a Sonnet pin on M/L review or plan drops from `high` to `medium` effort.
18
+ - **Unattended Claude phases get an explicit unattended-session contract.** Like Codex's headless preamble, it says no one will answer questions, finishing means the artifact is written and the phase command has run, and the phase must not start other phases or reviewers.
19
+ - **The code-review foreman accepts clean lens results and bounds delegation.** A lens's empty result is now valid rather than "a bug in the lens," and the foreman spawns only its two lenses plus one re-spawn.
20
+ - **`/canon-spec-review` no longer requires a task ID.** With no argument it reviews the task under discussion, or asks which one when that's ambiguous.
21
+
7
22
  ## [3.4.0] — 2026-09-27
8
23
 
9
24
  ### Added
package/README.md CHANGED
@@ -86,7 +86,7 @@ The pipeline supports two tiers:
86
86
  - **Fast tier** (XS tasks): spec + plan in one Claude session, skip Codex spec review, human gate replaces it.
87
87
  - **Full tier** (S / M / L / XL / delicate tasks): every phase runs separately, Codex reviews specs before they reach the human.
88
88
 
89
- `delicate: true` (set in `status.json` at task creation) promotes any task to full tier and upgrades the orchestrator's model and effort across every phase. Use it for sensitive surfaces — auth, payments, persistent storage, anything where a regression has unbounded blast radius.
89
+ `delicate: true` (set in `status.json` at task creation) promotes any task to full tier and upgrades the model and effort for spec, plan, and code review, plus Codex spec review and implementation; QA and the cold-Codex review lens stay on their regular cells. Use it for sensitive surfaces — auth, payments, persistent storage, anything where a regression has unbounded blast radius.
90
90
 
91
91
  A single command runs a task end-to-end:
92
92
 
package/dist/cli/index.js CHANGED
@@ -555,20 +555,6 @@ import path4 from "path";
555
555
  import { fileURLToPath } from "url";
556
556
  var __filename = fileURLToPath(import.meta.url);
557
557
  var __dirname = path4.dirname(__filename);
558
- function parseMaxReviewLoops(raw) {
559
- if (!raw) return null;
560
- const trimmed = raw.trim();
561
- if (!/^-?\d+$/.test(trimmed)) {
562
- warn(`Invalid MAX_REVIEW_LOOPS value "${raw}"; using the size-aware default.`);
563
- return null;
564
- }
565
- const parsed = Number(trimmed);
566
- if (!Number.isInteger(parsed) || parsed < 0) {
567
- warn(`Invalid MAX_REVIEW_LOOPS value "${raw}"; using the size-aware default.`);
568
- return null;
569
- }
570
- return parsed;
571
- }
572
558
  function resolveRepoRoot() {
573
559
  try {
574
560
  const result = spawnSync("git", ["rev-parse", "--git-common-dir"], { encoding: "utf8" });
@@ -601,15 +587,6 @@ function resolveProjectName() {
601
587
  }
602
588
  var config = {
603
589
  projectName: resolveProjectName(),
604
- claudeBudget: process.env.CLAUDE_BUDGET ?? null,
605
- claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? process.env.CLAUDE_MODEL ?? "opus",
606
- claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? process.env.CLAUDE_MODEL ?? "sonnet",
607
- claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? process.env.CLAUDE_MODEL ?? "sonnet",
608
- claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? process.env.CLAUDE_MODEL ?? "opus",
609
- claudeModelQa: process.env.CLAUDE_MODEL_QA ?? process.env.CLAUDE_MODEL ?? "sonnet",
610
- codexModelMini: process.env.CODEX_MODEL_MINI ?? process.env.CODEX_MODEL_DEFAULT ?? "gpt-6-luna",
611
- codexModelFull: process.env.CODEX_MODEL_FULL ?? process.env.CODEX_MODEL_DELICATE ?? "gpt-6-sol",
612
- maxReviewLoops: parseMaxReviewLoops(process.env.MAX_REVIEW_LOOPS),
613
590
  maxContextBytes: Number.parseInt(process.env.MAX_CONTEXT_BYTES ?? String(64 * 1024), 10)
614
591
  };
615
592
 
@@ -1247,7 +1224,7 @@ function checkTemplates(cwd) {
1247
1224
  }
1248
1225
  function checkCanonVersion(cwd) {
1249
1226
  const versionPath = join(cwd, ".canon", "version");
1250
- const installedVersion = "3.4.0";
1227
+ const installedVersion = "3.5.0";
1251
1228
  if (!existsSync(versionPath)) {
1252
1229
  return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
1253
1230
  }
@@ -1744,7 +1721,7 @@ function initCmd(_args) {
1744
1721
  }
1745
1722
  function writeCanonVersion(cwd) {
1746
1723
  const versionPath = join2(cwd, ".canon", "version");
1747
- const version = "3.4.0";
1724
+ const version = "3.5.0";
1748
1725
  mkdirSync(dirname2(versionPath), { recursive: true });
1749
1726
  writeFileSync(versionPath, version + "\n");
1750
1727
  }
@@ -2754,7 +2731,7 @@ function isInstalledSourcePath(sourcePath) {
2754
2731
  return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
2755
2732
  }
2756
2733
  function resolveCanonVersion(explicit) {
2757
- return explicit ?? "3.4.0" ?? "dev";
2734
+ return explicit ?? "3.5.0" ?? "dev";
2758
2735
  }
2759
2736
  function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
2760
2737
  const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
@@ -4880,7 +4857,7 @@ function currentPinFromManifest(manifest) {
4880
4857
  return "unknown";
4881
4858
  }
4882
4859
  function bakedVersion() {
4883
- return "3.4.0";
4860
+ return "3.5.0";
4884
4861
  }
4885
4862
  function ownPackageName(pkgDir) {
4886
4863
  try {
@@ -5356,7 +5333,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
5356
5333
  cutoverWarnings.push(docsRefsCheckRel);
5357
5334
  }
5358
5335
  const versionPath = join5(cwd, ".canon", "version");
5359
- const newVersion = "3.4.0";
5336
+ const newVersion = "3.5.0";
5360
5337
  const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
5361
5338
  if (currentVersion !== newVersion) {
5362
5339
  pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
@@ -5664,7 +5641,7 @@ Global:
5664
5641
  `);
5665
5642
  }
5666
5643
  function printVersion() {
5667
- console.log("3.4.0");
5644
+ console.log("3.5.0");
5668
5645
  }
5669
5646
  switch (command) {
5670
5647
  case "doctor":
@@ -357,7 +357,7 @@ var WORKTREES_ROOT = process.env.CANON_WORKTREES_ROOT ? path.resolve(REPO_ROOT,
357
357
  var STALL_TIMEOUT_MS = Number(process.env.PIPELINE_STALL_TIMEOUT_MS) || 10 * 60 * 1e3;
358
358
  var STALL_KILL_GRACE_MS = 3e3;
359
359
  var LEGACY_FALLBACK_ENV_VARS = [
360
- { old: "CLAUDE_MODEL", replacement: "CLAUDE_MODEL_SPEC / _PLAN / _REVIEW (still honored as fallback for those three; not applied to qa)" },
360
+ { old: "CLAUDE_MODEL", replacement: "CLAUDE_MODEL_LIGHT / CLAUDE_MODEL_STRONG (still honored as a fallback for every Claude phase, including qa)" },
361
361
  { old: "CODEX_MODEL_DEFAULT", replacement: "CODEX_MODEL_MINI (still honored as fallback)" },
362
362
  { old: "CODEX_MODEL_DELICATE", replacement: "CODEX_MODEL_FULL (still honored as fallback)" }
363
363
  ];
@@ -416,15 +416,6 @@ function resolveProjectName() {
416
416
  }
417
417
  var config = {
418
418
  projectName: resolveProjectName(),
419
- claudeBudget: process.env.CLAUDE_BUDGET ?? null,
420
- claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? process.env.CLAUDE_MODEL ?? "opus",
421
- claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? process.env.CLAUDE_MODEL ?? "sonnet",
422
- claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? process.env.CLAUDE_MODEL ?? "sonnet",
423
- claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? process.env.CLAUDE_MODEL ?? "opus",
424
- claudeModelQa: process.env.CLAUDE_MODEL_QA ?? process.env.CLAUDE_MODEL ?? "sonnet",
425
- codexModelMini: process.env.CODEX_MODEL_MINI ?? process.env.CODEX_MODEL_DEFAULT ?? "gpt-6-luna",
426
- codexModelFull: process.env.CODEX_MODEL_FULL ?? process.env.CODEX_MODEL_DELICATE ?? "gpt-6-sol",
427
- maxReviewLoops: parseMaxReviewLoops(process.env.MAX_REVIEW_LOOPS),
428
419
  maxContextBytes: Number.parseInt(process.env.MAX_CONTEXT_BYTES ?? String(64 * 1024), 10)
429
420
  };
430
421
 
@@ -1733,7 +1724,37 @@ function codexMatrix(config3) {
1733
1724
  }
1734
1725
  };
1735
1726
  }
1736
- function claudeModelFor(config3, phase) {
1727
+ var CLAUDE_CELLS = {
1728
+ spec: {
1729
+ XS: { tier: "light", effort: "medium" },
1730
+ S: { tier: "light", effort: "medium" },
1731
+ M: { tier: "strong", effort: "medium" },
1732
+ L: { tier: "strong", effort: "medium" },
1733
+ XL: { tier: "strong", effort: "high" }
1734
+ },
1735
+ plan: {
1736
+ XS: { tier: "light", effort: "medium" },
1737
+ S: { tier: "light", effort: "medium" },
1738
+ M: { tier: "strong", effort: "medium" },
1739
+ L: { tier: "strong", effort: "medium" },
1740
+ XL: { tier: "strong", effort: "high" }
1741
+ },
1742
+ code_review: {
1743
+ XS: { tier: "light", effort: "medium" },
1744
+ S: { tier: "light", effort: "medium" },
1745
+ M: { tier: "strong", effort: "medium" },
1746
+ L: { tier: "strong", effort: "medium" },
1747
+ XL: { tier: "strong", effort: "high" }
1748
+ },
1749
+ qa: {
1750
+ XS: { tier: "light", effort: "medium" },
1751
+ S: { tier: "light", effort: "medium" },
1752
+ M: { tier: "light", effort: "medium" },
1753
+ L: { tier: "light", effort: "medium" },
1754
+ XL: { tier: "light", effort: "medium" }
1755
+ }
1756
+ };
1757
+ function claudePinFor(phase, size, config3) {
1737
1758
  switch (phase) {
1738
1759
  case "spec":
1739
1760
  return config3.claudeModelSpec;
@@ -1741,48 +1762,36 @@ function claudeModelFor(config3, phase) {
1741
1762
  return config3.claudeModelPlan;
1742
1763
  case "qa":
1743
1764
  return config3.claudeModelQa;
1744
- // code_review is size-keyed (see codeReviewMatrix in claudeMatrix); not
1745
- // resolved through this helper. spec_review, implement, human_review
1746
- // aren't Claude phases; fall back to the spec model so resumed Claude
1747
- // sessions survive accidental use.
1748
- default:
1749
- return config3.claudeModelSpec;
1765
+ case "code_review":
1766
+ return size === "XL" ? config3.claudeModelReviewLarge : config3.claudeModelReview;
1750
1767
  }
1751
1768
  }
1769
+ function resolveClaudeModel(phase, size, config3) {
1770
+ const pin = claudePinFor(phase, size, config3);
1771
+ if (pin !== null) return pin;
1772
+ const tier = CLAUDE_CELLS[phase][size].tier;
1773
+ const tierModel = tier === "light" ? config3.claudeModelLight : config3.claudeModelStrong;
1774
+ if (tierModel !== null) return tierModel;
1775
+ if (config3.claudeModelLegacy !== null) return config3.claudeModelLegacy;
1776
+ return tier === "light" ? "sonnet" : "opus";
1777
+ }
1752
1778
  function claudeMatrix(config3) {
1753
- const buildHigh = (phase, xlEffort = "xhigh") => {
1754
- const model = claudeModelFor(config3, phase);
1755
- return {
1756
- XS: { model, effort: "medium" },
1757
- S: { model, effort: "medium" },
1758
- M: { model, effort: "high" },
1759
- L: { model, effort: "high" },
1760
- XL: { model, effort: xlEffort }
1761
- };
1762
- };
1763
- const buildMedium = (phase) => {
1764
- const model = claudeModelFor(config3, phase);
1765
- return {
1766
- XS: { model, effort: "medium" },
1767
- S: { model, effort: "medium" },
1768
- M: { model, effort: "medium" },
1769
- L: { model, effort: "high" },
1770
- XL: { model, effort: "high" }
1771
- };
1772
- };
1773
- const codeReviewMatrix = () => ({
1774
- XS: { model: config3.claudeModelReview, effort: "medium" },
1775
- S: { model: config3.claudeModelReview, effort: "medium" },
1776
- M: { model: config3.claudeModelReview, effort: "high" },
1777
- L: { model: config3.claudeModelReview, effort: "high" },
1778
- XL: { model: config3.claudeModelReviewLarge, effort: "xhigh" }
1779
+ const cell = (phase, size) => ({
1780
+ model: resolveClaudeModel(phase, size, config3),
1781
+ effort: CLAUDE_CELLS[phase][size].effort
1782
+ });
1783
+ const row = (phase) => ({
1784
+ XS: cell(phase, "XS"),
1785
+ S: cell(phase, "S"),
1786
+ M: cell(phase, "M"),
1787
+ L: cell(phase, "L"),
1788
+ XL: cell(phase, "XL")
1779
1789
  });
1780
1790
  return {
1781
- spec: buildHigh("spec"),
1782
- plan: buildHigh("plan", "high"),
1783
- // sonnet doesn't support xhigh
1784
- code_review: codeReviewMatrix(),
1785
- qa: buildMedium("qa")
1791
+ spec: row("spec"),
1792
+ plan: row("plan"),
1793
+ code_review: row("code_review"),
1794
+ qa: row("qa")
1786
1795
  };
1787
1796
  }
1788
1797
  function getPipelinePolicy(tasks, config3) {
@@ -1808,11 +1817,14 @@ function getPipelinePolicy(tasks, config3) {
1808
1817
 
1809
1818
  // src/orchestrator/policy.ts
1810
1819
  var config2 = {
1811
- claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? process.env.CLAUDE_MODEL ?? "opus",
1812
- claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? process.env.CLAUDE_MODEL ?? "sonnet",
1813
- claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? process.env.CLAUDE_MODEL ?? "sonnet",
1814
- claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? process.env.CLAUDE_MODEL ?? "opus",
1815
- claudeModelQa: process.env.CLAUDE_MODEL_QA ?? process.env.CLAUDE_MODEL ?? "sonnet",
1820
+ claudeModelSpec: process.env.CLAUDE_MODEL_SPEC ?? null,
1821
+ claudeModelPlan: process.env.CLAUDE_MODEL_PLAN ?? null,
1822
+ claudeModelReview: process.env.CLAUDE_MODEL_REVIEW ?? null,
1823
+ claudeModelReviewLarge: process.env.CLAUDE_MODEL_REVIEW_LARGE ?? null,
1824
+ claudeModelQa: process.env.CLAUDE_MODEL_QA ?? null,
1825
+ claudeModelLight: process.env.CLAUDE_MODEL_LIGHT || null,
1826
+ claudeModelStrong: process.env.CLAUDE_MODEL_STRONG || null,
1827
+ claudeModelLegacy: process.env.CLAUDE_MODEL ?? null,
1816
1828
  codexModelMini: process.env.CODEX_MODEL_MINI ?? process.env.CODEX_MODEL_DEFAULT ?? "gpt-6-luna",
1817
1829
  codexModelFull: process.env.CODEX_MODEL_FULL ?? process.env.CODEX_MODEL_DELICATE ?? "gpt-6-sol",
1818
1830
  maxReviewLoops: parseMaxReviewLoops(process.env.MAX_REVIEW_LOOPS),
@@ -1825,6 +1837,9 @@ function policyConfig() {
1825
1837
  claudeModelReview: config2.claudeModelReview,
1826
1838
  claudeModelReviewLarge: config2.claudeModelReviewLarge,
1827
1839
  claudeModelQa: config2.claudeModelQa,
1840
+ claudeModelLight: config2.claudeModelLight,
1841
+ claudeModelStrong: config2.claudeModelStrong,
1842
+ claudeModelLegacy: config2.claudeModelLegacy,
1828
1843
  codexModelMini: config2.codexModelMini,
1829
1844
  codexModelFull: config2.codexModelFull,
1830
1845
  maxReviewLoops: config2.maxReviewLoops,
@@ -1894,6 +1909,7 @@ var CLAUDE_STARTUP = "Read docs/patterns.md before starting.\nSkim docs/lessons-
1894
1909
  var CODEX_STARTUP = 'Read docs/patterns.md and docs/codebase-map.md before starting.\nSkim docs/lessons-learned.md for entries relevant to your task area.\nRead docs/decisions.md if a settled decision governs the area you are changing, or if the task would revisit one.\nRead docs/product-context.md if the task touches user-visible behavior, product terminology, or a business rule.\nGround every claim in the current file, diff, or artifact before you state it. Do not rely on prior-session memory for code existence, validation results, or completion status.\nOn resumed sessions, re-read the task-specific files named in the prompt and inspect the current working tree before saying anything is already done.\n\nGit ownership: the pipeline orchestrator handles staging, committing, and pushing \u2014 do NOT run `git add`, `git commit`, or `git push`. Edit files in the working tree only; the orchestrator reads `git status` after your session and stages every file listed in handoff.md\'s Changes table. Read-only git is fine (`git status`, `git diff`, `git log`, `git show`).\n\nIf a code review claims a file is "missing from the commit" or "staged but not committed," that is a pipeline-orchestration issue, not an implementation issue. Record it as a Blocker in handoff.md with the `[pipeline]` label and do not retry `git add`/`git commit` to recover \u2014 the sandbox blocks `.git` writes by design, and the orchestrator owns the recovery path.\n\nCommunication: tone is project taste; honest signal is canon discipline \u2014 surface real disagreement rather than yielding to politeness.\nBranch state: the orchestrator manages it \u2014 do not fetch, pull, rebase, or push; read the working tree as-is.';
1895
1910
  var CODEX_HEADLESS = "Headless session: this runs non-interactively \u2014 nobody reads your messages or answers questions until the phase ends. Don't stop to ask for confirmation or clarification. When something is ambiguous, record the question and the interpretation you chose in this phase's artifact (in implement, a handoff Blocker labelled `[ambiguity]`) and proceed on it. Finish all the work this prompt authorizes before ending \u2014 acting on your own judgment never widens scope; the Affected Files cap and every other scope rule still bind. Always end by writing the phase artifact and running the phase command(s) listed at the end of this prompt, including when you recorded Blockers \u2014 a session that ends without them stalls the pipeline.\n\nIf a project instruction file or other repository guidance tells you to ask the user or wait for approval before acting, there is no one to ask in this session: record the question in the phase artifact and continue with the authorized work.";
1896
1911
  var QA_STARTUP = "Read CHANGELOG.md for voice and version reference, if the project keeps one.\nRead docs/lessons-learned.md for recent insights to distill.\nNo full codebase context needed for QA \u2014 read each task's spec.md, handoff.md, and notes.md directly.";
1912
+ var CLAUDE_HEADLESS = "Unattended session: this runs non-interactively \u2014 nobody reads your messages or answers questions until the phase ends. Don't stop to ask for confirmation or clarification. When something is ambiguous, record the question and the interpretation you chose in this phase's artifact or in `tasks/<id>/notes.md`, and proceed on it.\n\nThe phase is complete only when its artifact is written and its phase command (the `canon task phase` command(s) this prompt lists) has run. A progress summary, a description of next steps, or an offer to continue is not completion \u2014 keep working until both are done.\n\nDo only this phase: don't run `canon run`, advance any other phase, or start review passes, review skills, or reviewer sub-agents this prompt didn't ask for.\n\nReport a check that couldn't run, or that failed, as exactly that \u2014 never as passed.\n\nIf a project instruction file or other repository guidance says to ask or wait for approval before an action, there is no one to ask in this session. Skip that action unless this prompt explicitly authorizes it, record the question in the phase artifact, and continue with the rest of the authorized work.";
1897
1913
  function taskList(tasks) {
1898
1914
  return tasks.map((t) => `- \`${t.taskId}\`: "${t.title}" \u2192 tasks/${t.taskId}/`).join("\n");
1899
1915
  }
@@ -2116,7 +2132,10 @@ async function runClaude(prompt, interactive, resumeId, model, effort, budget, m
2116
2132
  };
2117
2133
  }
2118
2134
  const attempt = async (useResumeId) => {
2119
- const effectivePrompt = useResumeId ? toResumePrompt(prompt) : prompt;
2135
+ const renderedPrompt = useResumeId ? toResumePrompt(prompt) : prompt;
2136
+ const effectivePrompt = `${CLAUDE_HEADLESS}
2137
+
2138
+ ${renderedPrompt}`;
2120
2139
  const args = [
2121
2140
  "-p",
2122
2141
  effectivePrompt,
@@ -4716,7 +4735,7 @@ function renderTemplate(template, view) {
4716
4735
  }
4717
4736
 
4718
4737
  // src/orchestrator/prompts/templates/code-review-foreman.md
4719
- var code_review_foreman_default = "You are the synthesis foreman for the code review phase for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n## Code-Review Rules of Thumb (Foreman)\n\n- **Reviewer diffs against the task baseline, not `main`, on release branches**: on a shared release branch ahead of `main`, always diff against the task's baseline \u2014 diffing against `main` attributes unrelated work to the task.\n- **Use `git -C <absolute-path>` for every worktree git op, not `cd` + git**: when operating across REPO_ROOT and a task worktree, `git -C /absolute/path` avoids silent cwd reversion between tool calls.\n- **Don't infer one git invariant from another**: `git status --porcelain` empty \u2260 origin matches HEAD; `origin/<branch>` exists \u2260 origin matches HEAD; PR exists \u2260 PR is in the expected state. Do the actual check directly.\n- **A cross-cutting invariant belongs in one shared helper, not patched per call site**: when the same rule must hold at multiple enforcement points, implement it once. The tell: findings come back round after round as the same bug class at a new location. At \u22653 sites, extract the shared helper and route all sites through it.\n\nYour job is to synthesize three review inputs: the anchored Claude lens, the cold-Claude lens, and the pre-obtained cold-Codex findings injected below. You spawn the Claude lenses as isolated sub-agents, collect their findings, adjudicate all three inputs using the spec (which you hold and the cold lenses do not), then write one `review.md` and set the verdict. Do not run `codex` yourself.\n\nTasks:\n{{{taskLines}}}\n\n{{#isRound1}}\nThis is Round 1, the initial code review.\n{{/isRound1}}\n{{^isRound1}}\nThis is Round {{roundN}}: re-review after iteration {{priorIteration}}. All three lenses run again this round. Direct the anchored lens to read the Iteration {{priorIteration}} section of `handoff.md` that addresses review round {{priorIteration}}.\n{{#tightenLine}}\n{{{tightenLine}}}\n{{/tightenLine}}\n{{/isRound1}}\n\n**Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}\n\n{{#hasDiff}}\nTask diff against {{{baseBranch}}}:\n\n```diff\n{{{diffContent}}}\n```\n{{#diffTruncated}}\n> Diff truncated at 50 000 bytes. Give the Claude lenses the visible diff first; for the omitted remainder, direct them to inspect only the changed files named in the handoff Changes table. Do not give the cold-Claude lens spec, AC, or canon-doc context.\n{{/diffTruncated}}\n{{/hasDiff}}\n{{^hasDiff}}\nRetrieve the task diff with `git diff {{{baseBranch}}}...HEAD`.\n{{/hasDiff}}\n{{#isDeltaScope}}\n\n{{#hasDeltaDiff}}\nDelta diff (`{{{deltaBase}}}..HEAD`) \u2014 give this delta to the cold-Claude lens:\n\n```diff\n{{{deltaDiffContent}}}\n```\n{{#deltaDiffTruncated}}\n> Delta diff truncated at 50 000 bytes. Give the cold-Claude lens the visible delta first; for the remainder, direct it to `git diff {{{deltaBase}}}..HEAD`.\n{{/deltaDiffTruncated}}\n{{/hasDeltaDiff}}\n{{^hasDeltaDiff}}\nThe delta diff could not be injected. Retrieve it with `git diff {{{deltaBase}}}..HEAD` and give it to the cold-Claude lens before it reviews.\n{{/hasDeltaDiff}}\n{{/isDeltaScope}}\n\n## Injected Cold-Codex Findings\n\n{{#hasColdCodexFindings}}\nThe orchestrator ran `codex review` before spawning you. Its findings are reproduced below. These are unanchored: Codex reviewed adversarially without the spec as a checklist. Treat them as the third lens input. Do not re-run Codex; synthesize these findings alongside the Claude lens outputs.\n{{#isDeltaScope}}\nFor this round, cold-Codex reviewed only `{{{deltaBase}}}..HEAD`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\nFor this round, cold-Codex reviewed the full task diff against `{{{baseBranch}}}`.\n{{/isDeltaScope}}\n\n{{{coldCodexFindings}}}\n{{/hasColdCodexFindings}}\n{{^hasColdCodexFindings}}\nNo cold-Codex findings were provided to this prompt. In production code_review, the orchestrator must obtain that artifact before foreman synthesis; do not treat a missing cold-Codex lens as approval evidence.\n{{/hasColdCodexFindings}}\n\n{{#hasOutOfScopeFiles}}\n## Full-Send Scope Judgment\n\nThe task changed these files outside the spec's `### Affected Files` table:\n\n{{{outOfScopeFilesList}}}\n\nJudge each file against the owning task's ACs and record its outcome and reason in `review.md`:\n\n1. **Appropriate scope expansion:** If needed to meet the ACs and consistent with the spec's intent, append an `## Amendment (full-send scope)` section to the owning task's `spec.md` (use exactly that heading; a bare `## Amendment` is reserved for human reroute amendments). Include an exact `### Affected Files` heading and table row naming the file, plus a one-line reason that calls it a full-send scope expansion.\n2. **Spec miss:** If the spec should have listed the file, append the same `## Amendment (full-send scope)` section and exact `### Affected Files` table row, with a one-line reason that calls it a spec miss.\n3. **Should not have been changed:** Do not amend the spec. Write a finding requiring the file to be reverted or removed, and set the verdict to `changes_requested`.\n\nFor a bundle, amend the member spec whose ACs the file serves. The orchestrator checks after code review completes: every listed file must be in a member's Affected Files or a `changes_requested` verdict must route it back to implement. The legacy `needs_re_review` verdict routes the same way. A `spec_gap` verdict blocks for human triage and names any files still outside Affected Files; blessing that verdict does not amend the spec. An approved review with unjudged files auto-blocks.\n\n{{/hasOutOfScopeFiles}}\n## Foreman Protocol\n\n### 1. Spawn Claude Lenses In Parallel\n\nSpawn both Claude lenses with the sub-agent tool (`Agent`, called `Task` in older harnesses) in a single message so they run concurrently, and run them **in the foreground** (`run_in_background: false`) so the call returns their findings. Do not spawn them in the background and do not end your turn to wait for them: a turn that ends while a lens is still running ends the code review with no `review.md` and no verdict, and the phase is retried from scratch. Your turn ends only after step 5 below has run.\n\n**Anchored lens** (`subagent_type: code-review-anchored`)\n{{#isDeltaScope}}\n- Retrieve the full task diff with `git diff {{{baseBranch}}}...HEAD` and give it that full diff, not the delta, plus `spec.md`, `handoff.md`, and prior `review.md`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff, `spec.md`, `handoff.md`, and prior `review.md` if this is a re-review.\n{{/isDeltaScope}}\n- It applies canon's anchored Stage 1 / Stage 2 code-review charter.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\n**Cold-Claude lens** (`subagent_type: code-review-cold`)\n{{#isDeltaScope}}\n- This round is delta-scoped. Give it the delta (`{{{deltaBase}}}..HEAD`) and the delta base ref only.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff and base ref only.\n{{/isDeltaScope}}\n- Do not give it `spec.md`, ACs, handoff rationale, canon docs, known risks, or your anchored-lens prompt.\n- If it needs to inspect files for truncated diff context, constrain it to changed files only and preserve the spec-blind framing.\n- Separately, its sibling-site sweep may search other code files for guards, checks, and invariants that need the same treatment. Keep the search spec-blind.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\nThe injected cold-Codex findings above are the third lens input. Do not spawn a Codex agent or shell out to Codex yourself. Do not let a Claude lens see another lens's output.\n\n### 2. Adjudicate\n\nUse the three lens inputs and the spec. Do not perform a new full diff review for novel bugs; your role is synthesis and adjudication.\n\nThe lenses are instructed to over-report \u2014 to surface low-confidence and low-severity findings rather than self-censor. Filtering is **your** job, not theirs: a quiet lens output is a bug in the lens, not a clean diff. Rank surviving findings by confidence \xD7 severity. A low-confidence, low-severity finding is a nit or gets dismissed; it does not by itself drive `changes_requested`. Do not discard a finding merely because a lens marked it low-confidence \u2014 verify it against the spec/diff first, then rank.\n\n1. Dedup: if 2+ lenses flagged the same behavior, collapse it to one finding and record \"flagged by N lenses.\" A finding flagged by 2+ lenses is higher-confidence regardless of any lens's self-tag. Cross-model agreement \u2014 the same behavior flagged by cold-Claude and cold-Codex \u2014 must not be dismissed as spec-intended without explicit spec evidence cited in `review.md`.\n2. Keep the two reconciliation checks separate:\n - Does it hold against the code? For cold findings (cold-Claude and cold-Codex), verify each against the diff/code. Codex P-levels are claims to check, not verdicts. A finding that does not hold gets recorded as `Dismissed (cold-Claude): <finding> - <reason>` or `Dismissed (cold-Codex): <finding> - <reason>`.\n - Is it in spec scope? Apply this only to anchored-lens findings as part of the Stage 1 / Stage 2 charter.\n - Forbidden: do not dismiss a verified cold-Claude or cold-Codex finding merely for being off-AC or out of spec scope. A real bug caught by a cold lens is still a bug even if no AC named it.\n3. Altitude classification: every surviving finding is either:\n - `code-bug`: the implementation is wrong or test integrity is compromised.\n - `spec-gap`: the implementation may match the written spec, but the spec is missing, wrong, or too ambiguous for the implementer to fix.\n\n### 3. Choose Verdict\n\n- Any `code-bug` finding -> `changes_requested`.\n- Any `spec-gap` finding and no code-bugs -> `spec_gap`.\n- Optional nits or cleanup without blocking findings -> `approved_with_nits`.\n- No surviving findings -> `approved`.\n\nTest-integrity findings are always code-bugs.\n\n### 4. Write `review.md`\n\nFor each task, write `tasks/<id>/review.md`.\n\nRound 1 fills the existing template structure directly \u2014 do **not** wrap it in a `## Round 1` section; the `## Stage 1` and `## Stage 2` headings stay at H2. Re-review appends a new `## Round {{roundN}}` section near the bottom (with `### Stage 1` / `### Stage 2` sub-headings), preserving earlier rounds.\n\nEvery round \u2014 the Round 1 body and each `## Round N` section \u2014 includes one scope line near the top: **Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}. The numbered cold-Codex archive header is the machine-readable record.\n\nInclude:\n- Stage 1: anchored lens validation gate result and AC table.\n- Stage 2 / Findings: surviving findings with altitude (`code-bug` or `spec-gap`), source lens, and file:line.\n- Dismissed Cold Findings: every dropped cold finding plus the reason, including `Dismissed (cold-Claude): ...` and `Dismissed (cold-Codex): ...` entries where applicable.\n- Final Verdict: check exactly one verdict checkbox, including `Spec gap` when applicable.\n\n### 5. Set Phase Verdict\n\nRun one command per task with the actual verdict:\n{{{phaseCommands}}}\n";
4738
+ var code_review_foreman_default = "You are the synthesis foreman for the code review phase for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n## Code-Review Rules of Thumb (Foreman)\n\n- **Reviewer diffs against the task baseline, not `main`, on release branches**: on a shared release branch ahead of `main`, always diff against the task's baseline \u2014 diffing against `main` attributes unrelated work to the task.\n- **Use `git -C <absolute-path>` for every worktree git op, not `cd` + git**: when operating across REPO_ROOT and a task worktree, `git -C /absolute/path` avoids silent cwd reversion between tool calls.\n- **Don't infer one git invariant from another**: `git status --porcelain` empty \u2260 origin matches HEAD; `origin/<branch>` exists \u2260 origin matches HEAD; PR exists \u2260 PR is in the expected state. Do the actual check directly.\n- **A cross-cutting invariant belongs in one shared helper, not patched per call site**: when the same rule must hold at multiple enforcement points, implement it once. The tell: findings come back round after round as the same bug class at a new location. At \u22653 sites, extract the shared helper and route all sites through it.\n\nYour job is to synthesize three review inputs: the anchored Claude lens, the cold-Claude lens, and the pre-obtained cold-Codex findings injected below. You spawn the Claude lenses as isolated sub-agents, collect their findings, adjudicate all three inputs using the spec (which you hold and the cold lenses do not), then write one `review.md` and set the verdict. Do not run `codex` yourself.\n\nTasks:\n{{{taskLines}}}\n\n{{#isRound1}}\nThis is Round 1, the initial code review.\n{{/isRound1}}\n{{^isRound1}}\nThis is Round {{roundN}}: re-review after iteration {{priorIteration}}. All three lenses run again this round. Direct the anchored lens to read the Iteration {{priorIteration}} section of `handoff.md` that addresses review round {{priorIteration}}.\n{{#tightenLine}}\n{{{tightenLine}}}\n{{/tightenLine}}\n{{/isRound1}}\n\n**Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}\n\n{{#hasDiff}}\nTask diff against {{{baseBranch}}}:\n\n```diff\n{{{diffContent}}}\n```\n{{#diffTruncated}}\n> Diff truncated at 50 000 bytes. Give the Claude lenses the visible diff first; for the omitted remainder, direct them to inspect only the changed files named in the handoff Changes table. Do not give the cold-Claude lens spec, AC, or canon-doc context.\n{{/diffTruncated}}\n{{/hasDiff}}\n{{^hasDiff}}\nRetrieve the task diff with `git diff {{{baseBranch}}}...HEAD`.\n{{/hasDiff}}\n{{#isDeltaScope}}\n\n{{#hasDeltaDiff}}\nDelta diff (`{{{deltaBase}}}..HEAD`) \u2014 give this delta to the cold-Claude lens:\n\n```diff\n{{{deltaDiffContent}}}\n```\n{{#deltaDiffTruncated}}\n> Delta diff truncated at 50 000 bytes. Give the cold-Claude lens the visible delta first; for the remainder, direct it to `git diff {{{deltaBase}}}..HEAD`.\n{{/deltaDiffTruncated}}\n{{/hasDeltaDiff}}\n{{^hasDeltaDiff}}\nThe delta diff could not be injected. Retrieve it with `git diff {{{deltaBase}}}..HEAD` and give it to the cold-Claude lens before it reviews.\n{{/hasDeltaDiff}}\n{{/isDeltaScope}}\n\n## Injected Cold-Codex Findings\n\n{{#hasColdCodexFindings}}\nThe orchestrator ran `codex review` before spawning you. Its findings are reproduced below. These are unanchored: Codex reviewed adversarially without the spec as a checklist. Treat them as the third lens input. Do not re-run Codex; synthesize these findings alongside the Claude lens outputs.\n{{#isDeltaScope}}\nFor this round, cold-Codex reviewed only `{{{deltaBase}}}..HEAD`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\nFor this round, cold-Codex reviewed the full task diff against `{{{baseBranch}}}`.\n{{/isDeltaScope}}\n\n{{{coldCodexFindings}}}\n{{/hasColdCodexFindings}}\n{{^hasColdCodexFindings}}\nNo cold-Codex findings were provided to this prompt. In production code_review, the orchestrator must obtain that artifact before foreman synthesis; do not treat a missing cold-Codex lens as approval evidence.\n{{/hasColdCodexFindings}}\n\n{{#hasOutOfScopeFiles}}\n## Full-Send Scope Judgment\n\nThe task changed these files outside the spec's `### Affected Files` table:\n\n{{{outOfScopeFilesList}}}\n\nJudge each file against the owning task's ACs and record its outcome and reason in `review.md`:\n\n1. **Appropriate scope expansion:** If needed to meet the ACs and consistent with the spec's intent, append an `## Amendment (full-send scope)` section to the owning task's `spec.md` (use exactly that heading; a bare `## Amendment` is reserved for human reroute amendments). Include an exact `### Affected Files` heading and table row naming the file, plus a one-line reason that calls it a full-send scope expansion.\n2. **Spec miss:** If the spec should have listed the file, append the same `## Amendment (full-send scope)` section and exact `### Affected Files` table row, with a one-line reason that calls it a spec miss.\n3. **Should not have been changed:** Do not amend the spec. Write a finding requiring the file to be reverted or removed, and set the verdict to `changes_requested`.\n\nFor a bundle, amend the member spec whose ACs the file serves. The orchestrator checks after code review completes: every listed file must be in a member's Affected Files or a `changes_requested` verdict must route it back to implement. The legacy `needs_re_review` verdict routes the same way. A `spec_gap` verdict blocks for human triage and names any files still outside Affected Files; blessing that verdict does not amend the spec. An approved review with unjudged files auto-blocks.\n\n{{/hasOutOfScopeFiles}}\n## Foreman Protocol\n\n### 1. Spawn Claude Lenses In Parallel\n\nSpawn both Claude lenses with the sub-agent tool (`Agent`, called `Task` in older harnesses) in a single message so they run concurrently, and run them **in the foreground** (`run_in_background: false`) so the call returns their findings. Do not spawn them in the background and do not end your turn to wait for them: a turn that ends while a lens is still running ends the code review with no `review.md` and no verdict, and the phase is retried from scratch. Your turn ends only after step 5 below has run.\n\n**Anchored lens** (`subagent_type: code-review-anchored`)\n{{#isDeltaScope}}\n- Retrieve the full task diff with `git diff {{{baseBranch}}}...HEAD` and give it that full diff, not the delta, plus `spec.md`, `handoff.md`, and prior `review.md`.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff, `spec.md`, `handoff.md`, and prior `review.md` if this is a re-review.\n{{/isDeltaScope}}\n- It applies canon's anchored Stage 1 / Stage 2 code-review charter.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\n**Cold-Claude lens** (`subagent_type: code-review-cold`)\n{{#isDeltaScope}}\n- This round is delta-scoped. Give it the delta (`{{{deltaBase}}}..HEAD`) and the delta base ref only.\n{{/isDeltaScope}}\n{{^isDeltaScope}}\n- Give it the full diff and base ref only.\n{{/isDeltaScope}}\n- Do not give it `spec.md`, ACs, handoff rationale, canon docs, known risks, or your anchored-lens prompt.\n- If it needs to inspect files for truncated diff context, constrain it to changed files only and preserve the spec-blind framing.\n- Separately, its sibling-site sweep may search other code files for guards, checks, and invariants that need the same treatment. Keep the search spec-blind.\n- It returns structured findings to you. It must not write `review.md` or run `canon task phase`.\n\nThe injected cold-Codex findings above are the third lens input. Do not spawn a Codex agent or shell out to Codex yourself. Do not let a Claude lens see another lens's output.\n\nSpawn exactly the two Claude lenses above each round, plus at most one re-spawn of a lens whose return is missing. Spawn no other reviewers or review agents, run no review skills, and never substitute your own review for a lens's return.\n\nA lens return is valid when it follows that lens's charter return format. This includes a clean empty form (`STAGE_2_FINDINGS: (none)` or `COLD_FINDINGS: (none)`) and the anchored Stage 1 fail form (`STAGE_1: fail`, no Stage 2 findings, `OVERALL_SIGNAL: changes_requested`). A return is missing if absent or outside the charter format. Re-spawn a missing lens once with the same inputs. If still missing, do not approve or guess at its findings: it is not approval evidence. Write `review.md` recording which lens was missing and what came back, check no verdict box, then run the `code_review blocked` commands in step 5 for every task. Other lenses' findings may be recorded but not adjudicated into a verdict.\n\n### 2. Adjudicate\n\nUse the three lens inputs and the spec. Do not perform a new full diff review for novel bugs; your role is synthesis and adjudication.\n\nThe lenses are instructed to over-report \u2014 to surface low-confidence and low-severity findings rather than self-censor. Filtering is **your** job, not theirs. A clean return in the charter's empty form is a valid result; only a missing return (step 1) is a lens failure. Rank surviving findings by confidence \xD7 severity. A low-confidence, low-severity finding is a nit or gets dismissed; it does not by itself drive `changes_requested`. Do not discard a finding merely because a lens marked it low-confidence \u2014 verify it against the spec/diff first, then rank.\n\n1. Dedup: if 2+ lenses flagged the same behavior, collapse it to one finding and record \"flagged by N lenses.\" A finding flagged by 2+ lenses is higher-confidence regardless of any lens's self-tag. Cross-model agreement \u2014 the same behavior flagged by cold-Claude and cold-Codex \u2014 must not be dismissed as spec-intended without explicit spec evidence cited in `review.md`.\n2. Keep the two reconciliation checks separate:\n - Does it hold against the code? For cold findings (cold-Claude and cold-Codex), verify each against the diff/code. Codex P-levels are claims to check, not verdicts. A finding that does not hold gets recorded as `Dismissed (cold-Claude): <finding> - <reason>` or `Dismissed (cold-Codex): <finding> - <reason>`.\n - Is it in spec scope? Apply this only to anchored-lens findings as part of the Stage 1 / Stage 2 charter.\n - Forbidden: do not dismiss a verified cold-Claude or cold-Codex finding merely for being off-AC or out of spec scope. A real bug caught by a cold lens is still a bug even if no AC named it.\n3. Altitude classification: every surviving finding is either:\n - `code-bug`: the implementation is wrong or test integrity is compromised.\n - `spec-gap`: the implementation may match the written spec, but the spec is missing, wrong, or too ambiguous for the implementer to fix.\n\n### 3. Choose Verdict\n\n- Any `code-bug` finding -> `changes_requested`.\n- Any `spec-gap` finding and no code-bugs -> `spec_gap`.\n- Optional nits or cleanup without blocking findings -> `approved_with_nits`.\n- No surviving findings -> `approved`.\n\nTest-integrity findings are always code-bugs.\n\n### 4. Write `review.md`\n\nFor each task, write `tasks/<id>/review.md`.\n\nRound 1 fills the existing template structure directly \u2014 do **not** wrap it in a `## Round 1` section; the `## Stage 1` and `## Stage 2` headings stay at H2. Re-review appends a new `## Round {{roundN}}` section near the bottom (with `### Stage 1` / `### Stage 2` sub-headings), preserving earlier rounds.\n\nEvery round \u2014 the Round 1 body and each `## Round N` section \u2014 includes one scope line near the top: **Scope:** {{{scopeWord}}} \u2014 base `{{{scopeBase}}}` \u2014 reason: {{{scopeReason}}}. The numbered cold-Codex archive header is the machine-readable record.\n\nInclude:\n- Stage 1: anchored lens validation gate result and AC table.\n- Stage 2 / Findings: surviving findings with altitude (`code-bug` or `spec-gap`), source lens, and file:line.\n- Dismissed Cold Findings: every dropped cold finding plus the reason, including `Dismissed (cold-Claude): ...` and `Dismissed (cold-Codex): ...` entries where applicable.\n- Final Verdict: check exactly one verdict checkbox, including `Spec gap` when applicable (except the missing-lens stop in step 1, which checks none).\n\n### 5. Set Phase Verdict\n\nRun one command per task with the actual verdict:\n{{{phaseCommands}}}\n\nIf a lens was still missing after its re-spawn (step 1), write `review.md` first, then run these instead, with no verdict \u2014 this stops the run for the human:\n{{{blockedCommands}}}\n";
4720
4739
 
4721
4740
  // src/orchestrator/prompts/templates/implement.md
4722
4741
  var implement_default = 'You are implementing {{taskScope}} for {{projectName}}.\n\n{{{stateHeader}}}\n{{{startup}}}\n{{{risksBlock}}}{{{pitfallsBlock}}}{{{contextBlock}}}\n{{{affectedFilesBlock}}}\nTasks to implement:\n{{{taskLines}}}{{#isBundle}}\nThese tasks are related \u2014 implement them together. Consider shared code paths and cross-task interactions.{{/isBundle}}\n\nGrounding rule: before you write handoff.md, re-open the files you changed and verify the current diff against the spec. Do not treat a previous session\'s memory as proof that the work is already in place.\n\n**Spec ACs are binding. Plan approach is guidance.**\n- Every Acceptance Criterion in spec.md MUST be met \u2014 these are non-negotiable.\n- If you find a better implementation approach than what\'s in the plan, use it. Document every deviation in handoff.md under "Deviations" with specific rationale.\n- You may NOT silently drop an AC, skip a required validation check, or omit a spec requirement.\n- If an AC is infeasible as written, document it in Blockers \u2014 do not silently skip.\n- If an AC is ambiguous enough that two reasonable implementations exist, document your interpretation in handoff.md under Blockers with label `[ambiguity]` \u2014 do not silently guess. Claude will evaluate whether the interpretation was correct.\n\n## Implementation Rules\n\n**Safe-First Rules** \u2014 always applicable regardless of stack:\n1. For storage, reload, sync, or data-affecting flows: ship the safer guarded behavior first.\n2. Behavior that reloads the app, replaces local state, or dismisses user work must be gated by explicit user action.\n3. Prefer shared types over duplicating signatures.\n\n**Scope Discipline** \u2014 always applicable; the spec is the contract:\n1. **Affected Files is the scope cap.** If satisfying an AC genuinely requires editing files outside the spec\'s *Affected Files* table, do not make that edit. Record the gap in `handoff.md` under *Blockers* \u2014 the handoff is how it reaches a human \u2014 then finish the remaining in-scope work, the handoff, and the phase command. Do not silently expand scope. New helpers, fixtures, and test files introduced during implementation count against this cap too.\n2. **No unauthorized new abstractions.** Do not introduce new top-level modules, services, packages, or routing layers that the spec did not authorize. Minor refactors within an authorized file are fine; new abstractions are an architecture decision and belong in the spec.\n3. **No incidental dependency changes.** Do not add, remove, upgrade, or downgrade dependencies (or their pinned versions) unless the spec explicitly requests it.\n\n**Lint & Type Safety Policy** \u2014 always applicable:\n1. **Suppressing a lint or type error is a last resort**, not a convenience escape hatch. Never add a suppression without a same-line justification explaining *why the rule is wrong for this specific case*.\n2. **`any` / dynamic typing**: When the shape is truly unknown at the boundary, type as `unknown` and narrow explicitly.\n\n**Bug/Flake-Fix Red-First Checkpoint** \u2014 applicable when a spec\'s ACs include a red-first regression test:\nWrite the test and run it against the pre-fix code first; confirm it fails *for the reason the spec states*, then apply the fix and confirm it passes. Report the red run (command + observed failure) in handoff.md. If the test cannot be made to fail on the pre-fix code for the stated reason, do not implement a fix on a premise you could not reproduce \u2014 record a `[wrong-premise]` Blocker in handoff.md instead, then finish the remaining in-scope work, the handoff, and the phase command. If the spec instead uses the environment-bound-and-impractical escape, run its named deterministic alternative before fixing if it is executable in your sandbox (e.g. an integration fixture) and report the outcome in handoff.md the same way; if it is not executable (e.g. a documented manual repro), state that in handoff.md instead \u2014 never report an outcome for a run that did not happen.\n\n**Parsing Structured Input** \u2014 always applicable when implementing a parser for author-facing structured input:\nParse cell-by-cell with explicit rejection, not a permissive whole-string regex. Anchor each cell to exactly one expected shape and reject malformed cells with a specific reason at the parse boundary.\n\nRun ALL applicable validation checks before writing handoff. See "Validation Required" in each spec.md. The universal change-type \u2192 check-category matrix:\n\n| Change Type | Required Check Categories |\n|---|---|\n| Most changes | Linting, type checking, unit tests |\n| Docs references | Docs references |\n| Routes / config / build | Full build |\n| UI / interaction changes | End-to-end tests |\n| Content / SEO / metadata | Prerender / sitemap / feed regeneration |\n| Schema / migration | Migration runner + manual review |\n| Cross-platform | Subset of the above on each platform |\n\nFor which command runs each category: see `docs/architecture.md` \xA7Validation (project command bindings). Required checks must be recorded as Pass or Fail; do not mark a required check N/A unless the spec explicitly removed it.\n\n**Test flakiness in your sandbox.** Validation suites \u2014 especially E2E or integration tests \u2014 can hit transient failures (timing races, environment quirks, network jitter) that have nothing to do with the code in your spec\'s Affected Files. **If a failure is in a test / file outside your Affected Files table, do NOT fix it.** Note the observed test name, file, line, and a one-line repro hint in handoff.md \u2192 Blockers (or "Validation Outcomes" Notes column with status `Fail \u2013 unrelated`), then continue. `Fail \u2013 unrelated` is only valid for failures in files outside your Affected Files; a failure in a file you changed is yours to fix. Scope discipline > fixing adjacent bugs you spot during validation. The reviewer/operator will decide whether to triage the unrelated failure separately.\n\nFor each task, write tasks/<id>/handoff.md using the template. The Validation Outcomes table must have no Fail results EXCEPT for unrelated-flake rows clearly labeled in the Notes column.\nAppend to tasks/<id>/notes.md for any surprising codebase behavior (prefix: [implement]).\n\nWhen done, run:\n{{{phaseCommands}}}\n';
@@ -4728,10 +4747,10 @@ var implement_reroute_default = "A human wrote a spec amendment and rerouted {{t
4728
4747
  var implement_revisions_default = '{{{iterBanner}}}\n\n{{{stateHeader}}}\n{{{startup}}}\n\n{{{affectedFilesBlock}}}\n\n{{#hasReviewFindings}}\nYour prior iteration shipped; the reviewer (Claude) appended findings to `review.md` as `## Round {{priorRound}}`. If you\'re resuming the prior session, the full task framing (spec, plan, repo conventions) is already in context \u2014 skip the re-read. If your context is cold, re-read `tasks/<id>/spec.md` and `tasks/<id>/plan.md` before addressing findings.\n\nTasks with new review feedback:\n{{{reviewLines}}}\n\nFor each task:\n1. Read the most recent `## Round {{priorRound}}` section of `tasks/<id>/review.md`. That is the entire scope of this iteration.\n2. Address every `correctness bug`, `risk/guardrail`, and `spec gap` finding from that round (blocking). `optional cleanup/nit` is at your discretion{{#tightenLine}}{{{tightenLine}}}{{/tightenLine}}\n3. Re-run only the validation checks affected by your changes (typically lint, type-check, plus whatever the diff touches).\n4. **APPEND** to `tasks/<id>/handoff.md` a new section `{{{handoffAppend}}}` (the template\'s "On revision rounds" comment shows the shape). Do NOT rewrite the file from scratch \u2014 earlier iterations stay as the cumulative record. Include only the delta: findings addressed, AC deltas, re-run validation results.\n{{/hasReviewFindings}}\n{{#hasPreflightFindings}}\nYour prior iteration was rejected by the orchestrator\'s pre-flight gate **before any Claude review ran**. The rejection details are recorded in `review.md` under `## Validation Gate` / `## Pre-Flight Rejection`.\n\nTasks with pre-flight rejection feedback:\n{{{reviewLines}}}\n\nFor each task:\n1. Read the pre-flight block in `tasks/<id>/review.md` and follow **whichever framing it carries**:\n - **"Fix the handoff"** items \u2192 fix `handoff.md` (Validation Outcomes rows, AC Coverage table, Changes table).\n - **"Fix the code"** items \u2192 a required check failed on a file you changed. Fix the regression, re-run the check, and update the handoff.\n - Both framings may be present \u2014 address all items from both before resubmitting.\n2. **APPEND** to `tasks/<id>/handoff.md` a new section `{{{handoffAppend}}}`. Include the delta: which items you addressed and how.\n{{/hasPreflightFindings}}\n\nSpec ACs remain binding. If the review identifies a dropped AC, restore it.\n\n**Iteration rules:**\n\n- **Reverting a file**: For a byte-perfect revert to the task baseline, use `git show origin/<base-branch>:<path>` (read-only git, always allowed) and write the output to the file.\n - *Perfect revert* (file no longer in `git diff base...HEAD`): delete it from all prior iteration Changes tables in `handoff.md`.\n - *Imperfect revert* (trailing newline or other residual remains): add it to the current iteration\'s Changes table with "Reverted to original (describe residual diff)".\n- **Referencing deleted (or not-yet-created) files in artifacts**: `docs-refs-check` flags a backtick path-ref to a file that does not exist. Referencing deleted paths in the handoff Changes-table first column must use `[path](path)` markdown-link form only \u2014 backtick form fails both checks.\n- **Rerouted / revised tasks \u2014 the pre-flight diff is cumulative**: the verifier checks the union of all Changes tables against `git diff <base>...HEAD`. Before submitting, run `git diff <base>...HEAD --name-only` and confirm every listed path is covered by at least one Changes-table row across ALL iterations.\n- **Bug/flake-fix red-first checkpoint**: if this round adds or modifies the spec\'s red-first regression test, write and run the test **before** making this round\'s fix edits and confirm it fails *for the reason the spec states*; then apply the fix and confirm it passes. Report the red run in the handoff delta. If it cannot be made to fail for that reason, do not implement around an unconfirmed premise \u2014 record a `[wrong-premise]` Blocker in handoff.md instead, then finish the remaining in-scope work, the handoff, and the phase command. If the spec instead uses the environment-bound-and-impractical escape, run its named deterministic alternative if it is executable in your sandbox and report the outcome in the handoff delta; if it is not executable (e.g. a documented manual repro), state that instead \u2014 never report an outcome for a run that did not happen.\n\nAppend to `tasks/<id>/notes.md` for new pitfalls found (prefix: `[implement-revision]`).\n\nWhen done, run:\n{{{phaseCommands}}}\n';
4729
4748
 
4730
4749
  // src/orchestrator/prompts/templates/plan-reroute.md
4731
- var plan_reroute_default = "You are updating the implementation plan for {{taskScope}} for {{projectName}} after a human reroute.\n\n{{{startup}}}\n\nA human amended the spec and rerouted the task after a completed implementation round. Codex has reviewed the amendment. Your job is to **append** a reroute plan section to `plan.md`; do not rewrite or remove existing plan content.\n\n{{{roundBanner}}}Amendment review verdicts:\n{{{verdictLines}}}\n\nFor each task \u2014 EXCEPT tasks whose line above marks them EXEMPT (those have no amendment; skip every step below for them and append NO Reroute Plan section to their plan.md):\n1. Read `tasks/<id>/spec.md` from your current directory, including the amendment for the round listed above.\n2. Read `tasks/<id>/plan.md` to understand the prior plan.\n3. Read `tasks/<id>/handoff.md` to understand what Codex previously shipped.\n4. Read `tasks/<id>/spec-review.md` for the latest reroute amendment review and any nits to incorporate.\n5. Append a new section to `tasks/<id>/plan.md`:\n - Round 1: `## Reroute Plan`\n - Round N >= 2: `## Reroute Plan Round N`\n6. Plan only the delta from the amendment. Reference specific files, functions, and existing patterns. Acknowledge prior plan steps that still apply without re-planning them.\n\nDo **not** rewrite or remove existing sections from `plan.md`. The appended reroute plan is what implement-reroute reads as its delta guide.\n\nWhen done, run:\n{{{phaseCommands}}}\n\n<!-- per-round append shape:\n## Reroute Plan [Round N]\n### Delta\n- ...ordered steps for the amendment delta only...\n-->\n";
4750
+ var plan_reroute_default = "You are updating the implementation plan for {{taskScope}} for {{projectName}} after a human reroute.\n\n{{{startup}}}\n\nA human amended the spec and rerouted the task after a completed implementation round. Codex has reviewed the amendment. Your job is to **append** a reroute plan section to `plan.md`; do not rewrite or remove existing plan content.\n\n{{{roundBanner}}}Amendment review verdicts:\n{{{verdictLines}}}\n\nFor each task \u2014 EXCEPT tasks whose line above marks them EXEMPT (those have no amendment; skip every step below for them and append NO Reroute Plan section to their plan.md):\n1. Read `tasks/<id>/spec.md` from your current directory, including the amendment for the round listed above.\n2. Read `tasks/<id>/plan.md` to understand the prior plan.\n3. Read `tasks/<id>/handoff.md` to understand what Codex previously shipped.\n4. Read `tasks/<id>/spec-review.md` for the latest reroute amendment review and any nits to incorporate.\n5. Append a new section to `tasks/<id>/plan.md`:\n - Round 1: `## Reroute Plan`\n - Round N >= 2: `## Reroute Plan Round N`\n6. Plan only the delta from the amendment. Reference specific files, functions, and existing patterns. Acknowledge prior plan steps that still apply without re-planning them.\n7. {{{feasibilityCheck}}}\n\nDo **not** rewrite or remove existing sections from `plan.md`. The appended reroute plan is what implement-reroute reads as its delta guide.\n\nWhen done, run:\n{{{phaseCommands}}}\n\n<!-- per-round append shape:\n## Reroute Plan [Round N]\n### Delta\n- Feasibility: ...\n- ...ordered steps for the amendment delta only...\n-->\n";
4732
4751
 
4733
4752
  // src/orchestrator/prompts/templates/plan.md
4734
- var plan_default = "You are writing implementation plans for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n{{{verdictLines}}}\n\nFor each task, read tasks/<id>/spec.md and tasks/<id>/spec-review.md. Address any `changes_requested` items before writing the plan. If the verdict is `approved_with_nits`, incorporate the nits into the plan \u2014 they don't require spec changes but should inform implementation decisions.\n\nWrite tasks/<id>/plan.md for each task with ordered implementation steps. Reference specific files, existing patterns, and code examples from the codebase. Codex implements directly from this plan.\n\nIf you encounter spec gaps, append to tasks/<id>/notes.md (prefix: [plan]).\n\nWhen done, run:\n{{{phaseCommands}}}\n";
4753
+ var plan_default = "You are writing implementation plans for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\n{{{verdictLines}}}\n\nFor each task, read tasks/<id>/spec.md and tasks/<id>/spec-review.md. Address any `changes_requested` items before writing the plan. If the verdict is `approved_with_nits`, incorporate the nits into the plan \u2014 they don't require spec changes but should inform implementation decisions.\n\nWrite tasks/<id>/plan.md for each task with ordered implementation steps. Reference specific files, existing patterns, and code examples from the codebase. Codex implements directly from this plan.\n\n{{{feasibilityCheck}}}\n\nIf you encounter spec gaps, append to tasks/<id>/notes.md (prefix: [plan]).\n\nWhen done, run:\n{{{phaseCommands}}}\n";
4735
4754
 
4736
4755
  // src/orchestrator/prompts/templates/qa.md
4737
4756
  var qa_default = 'You are writing QA summaries for {{taskScope}} for {{projectName}}.\n\n{{{startup}}}\n\nTasks:\n{{{taskLines}}}\n\nFor each task:\n1. **Use the Write tool** to create tasks/<id>/done.md \u2014 plain-English summary for the human. Include: what changed, files changed, how to test, test results, human verification required, decisions made, open questions.\n \u26A0\uFE0F CRITICAL: Use the `Write` tool \u2014 do NOT simply output the done.md content as text in your response. Content in your chat reply does not get saved to disk. The pipeline validates that done.md contains real content (not the template) before advancing. Write the file.\n2. Read the latest `## Validation Outcomes` table in `tasks/<id>/handoff.md`, including any later iteration `### Re-run validation` tables. If any check\'s latest result is `human_pending`, include a **Human Verification Required** section in done.md that lists each pending check and its Notes. If none remain, write `None.` in that section. Do not hide `human_pending` checks inside the generic Test Results table.\n - If the human chooses to waive or defer a pending check later, the waiver line in done.md must begin with `Acknowledged:`. The `human_review` gate only treats that explicit prefix as a waiver.\n - Preserve `deferred_by_spec` rows in Test Results with the spec citation from Notes; do not translate them to `Pass`.\n3. Include a **Proposed Changelog** section in done.md \u2014 the changelog **entry text only**. Do **NOT** include a version number, a "Proposed version" line, or a bump tier (Patch/Minor/Major): the version is decided at the release step (`/canon-changelog` + human), never by QA.\n - **Canon release rules (non-negotiable)**: (1) Agents do not bump versions or land changelog edits without explicit scope authorization. (2) The QA step proposes a draft changelog entry text only \u2014 not the version number. (3) Changelog + version bump are committed separately from code changes (when a project versions its releases). (4) No major versioning surprises: if a task introduces a breaking change the spec didn\'t flag, raise it before shipping.\n - Read `docs/decisions.md` \xA7"Versioning and release policy" for this project\'s changelog **scope** (which changes warrant an entry). The bump tier / SemVer interpretation is the release step\'s call, not QA\'s \u2014 do not propose one.\n - If CHANGELOG.md exists, read the top of it (the most recent version section) to calibrate on scope and voice.\n - Apply the "would a user notice" test to every candidate bullet (or the project\'s equivalent scope test): if a candidate falls outside the project\'s defined changelog scope, omit it. If a task is entirely out of scope, say so explicitly ("no user-facing change \u2014 omit from changelog") rather than inventing a bullet.\n - Implementation mechanics belong in the "What Changed" section above \u2014 not in the proposed changelog.\n The human finalizes the changelog entry.\n4. **For single tasks only \u2014 use the Write tool** to create `tasks/<id>/pr-body.md` \u2014 the outward-facing PR body draft for `--pr`. Write it as if a human wrote it after doing the work.\n {{#prTemplate}}\n The repo has this PR template. Fill every section with specifics from what shipped. Keep the headings; replace every placeholder:\n\n {{{prTemplate}}}\n {{/prTemplate}}\n {{^prTemplate}}\n No PR template found. Use this structure:\n\n ## Summary\n 1\u20133 bullets: what changed and why.\n\n ## Changes\n Key files or areas touched, described for a reviewer.\n\n ## How to Test\n Steps a reviewer can follow to verify the change.\n\n ## Notes for Reviewer\n Any context, caveats, or follow-up items.\n {{/prTemplate}}\n \u26A0\uFE0F Write as the human engineer who did the work \u2014 not as the AI or tool that produced it (Claude, Codex, canon, an LLM). \u2705 e.g. "Fix the pagination off-by-one that dropped the last row." \u274C e.g. "\u{1F916} Generated with Claude Code."\n Skip this step entirely for bundle tasks \u2014 per-task bodies are not combined for bundle PRs.\n\nAfter writing all done.md files:\n- Read tasks/<id>/notes.md for each task. For each insight, ask: "would this have changed how a *different* task was approached?" If yes, **append** one new entry for *this* task to docs/lessons-learned.md. If no, the detail stays in notes.md only. Append-only: never edit, prune, promote, reorganize, or delete existing entries \u2014 not this task\'s earlier entries, and never another task\'s. Promoting entries into permanent docs (patterns.md / decisions.md) and pruning the buffer is a **human-initiated, human-approved** action \u2014 never perform it during QA, and no entry count ever triggers it. (See docs/lessons-learned.md \u2192 "How to use this doc".)\n- Add a `## Quality Log` section to done.md with the five judgment cells the pipeline cannot derive from status.json. Do **not** edit docs/task-quality-log.md directly \u2014 the qa \u2192 done transition writes or upserts that row automatically from status.json plus this section.\n\n ```markdown\n ## Quality Log\n - Spec verdict: <the FIRST spec_review verdict this task ever received \u2014 approved / approved_with_nits / changes_requested; leave blank only if truly unknown>\n - Human reroute?: <Yes/No \u2014 did the human reject at human_review and force a re-implement? Do not infer this from any reroute counter; if this is a fresh QA pass with no human_review rejection yet, answer No>\n - Dropped ACs: <count of ACs the implementation missed, caught in code review>\n - Validation gaps: <count of validation checks that should have run but did not>\n - Notes: <one-line summary of anything notable \u2014 single line, no embedded line breaks>\n ```\n\n On a re-upsert after a reroute, only fill in cells that changed or that you are correcting \u2014 an omitted cell keeps its previously recorded value. In particular, do not overwrite an already-recorded `Spec verdict` with the current status.json verdict; that column means the first spec_review verdict, and status.json only retains the latest.\n\n**Handoff Validation pre-merge checklist** (include in `done.md` Human Verification section if any item cannot be confirmed):\n- [ ] Version correct (per project policy; skip if unversioned)\n- [ ] Changelog updated if needed (per project policy; skip if unversioned)\n- [ ] PR body current\n- [ ] Final CI/CD checks green\n- [ ] Final diff matches spec intent\n\n**Output Format for Human** \u2014 `done.md` must contain:\n1. One-paragraph plain-English summary\n2. Files changed\n3. How to test (product-level steps, not code)\n4. Test results table\n5. Decisions made during implementation\n6. Open questions needing human input\n\n**Code is Canonical; Docs Reference Symbols**: Code is the source of truth for anything derivable from code: numbers, thresholds, file locations, function signatures, type shapes, observable behavior. Docs that restate these facts inline rot silently \u2014 reference the symbol or path; do not restate the value.\n\n**Commit Ownership** \u2014 three change categories:\n- Code changes \u2192 task branch, committed by the orchestrator after Codex static validation.\n- Pre-implement scaffold \u2192 base branch, committed by the orchestrator before first implement.\n- Changelog + version bump (if versioned) \u2192 separate commit, human + Claude, after human_review.\n\n- **Docs freshness \u2014 Two-checkpoint**: scan the five protected docs (`docs/architecture.md`, `docs/codebase-map.md`, `docs/patterns.md`, `docs/product-context.md`, `docs/decisions.md`) for references that {{docsScope}} *contradicts* \u2014 a renamed symbol, a moved file, a behavior this task changed \u2014 and correct those stale references. That is the only edit QA makes to permanent docs. Do not add new lessons, pitfalls, or decisions here, and do not promote buffer entries \u2014 promotion is the human sweep, not Docs freshness.\n- **Buffer signal** (not an action): after appending, if docs/lessons-learned.md now holds more than ~15 entries, add one line to this task\'s done.md \u2014 `Maintenance: lessons-learned.md has N entries; a human lessons sweep is due (see docs/lessons-learned.md \u2192 "How to use this doc").` Do not perform the sweep yourself.\n\nWhen done, run (use the Bash tool \u2014 do not just output the command as text):\n{{{phaseCommands}}}\n';
@@ -4792,6 +4811,20 @@ function buildAffectedFilesBlock(affectedFiles, baseBranch) {
4792
4811
  ""
4793
4812
  ].join("\n");
4794
4813
  }
4814
+ function planFeasibilityCheck(placement, indent = "") {
4815
+ const lines = [
4816
+ `**Feasibility check.** Before writing steps, confirm each item below and record the results in a few lines ${placement}. Mark an item that doesn't apply \`N/A\` in a word.`,
4817
+ "- **Exists:** the functions, files, and patterns the plan relies on exist \u2014 found by searching the code, not recalled.",
4818
+ "- **Real path:** the actual runtime call path the change sits on, including any existing code on it that already does part of the work.",
4819
+ "- **Callers:** every caller of each function whose behavior or error contract changes, found and accounted for.",
4820
+ "- **Tests that can fail:** for each regression test the plan prescribes, the input or state that sends it through the changed path, so it fails without the change.",
4821
+ "- **Predicates:** for each condition or state check the plan writes out, its boundary values (for example zero, empty, or non-numeric) and every state it must handle.",
4822
+ "- **Scope:** every file the steps change is inside the spec's Affected Files.",
4823
+ "- **Async/stateful:** for async or stateful changes \u2014 re-entry, cancellation or unmount, stale state, and ownership.",
4824
+ "A finding that changes a step goes in that step. If the check contradicts the spec, record the contradiction in the plan and in `tasks/<id>/notes.md` with a `[plan]` prefix. Do not change the spec or widen scope to resolve it."
4825
+ ];
4826
+ return lines.map((line, index) => index === 0 ? line : indent + line).join("\n");
4827
+ }
4795
4828
  function promptSpec(state) {
4796
4829
  const { tasks, tier, isBundle } = state;
4797
4830
  const combined = tier === "fast";
@@ -4807,9 +4840,13 @@ Directory: tasks/${task.taskId}/`,
4807
4840
  startup: CLAUDE_STARTUP,
4808
4841
  instructions: isBundle ? tasks.map(
4809
4842
  (t) => `**Task \`${t.taskId}\`**: Write tasks/${t.taskId}/spec.md using the template.` + (combined ? ` Also write tasks/${t.taskId}/plan.md with ordered implementation steps, specific file references, and existing patterns.` : "")
4810
- ).join("\n\n") : `Write tasks/${task.taskId}/spec.md using the template in .canon/templates/spec.md. Be concrete \u2014 Codex implements directly from this.` + (combined ? `
4843
+ ).join("\n\n") + (combined ? `
4811
4844
 
4812
- Also write tasks/${task.taskId}/plan.md with ordered implementation steps, specific file references, and existing patterns to use.` : ""),
4845
+ ${planFeasibilityCheck("in each plan.md's Approach section")}` : "") : `Write tasks/${task.taskId}/spec.md using the template in .canon/templates/spec.md. Be concrete \u2014 Codex implements directly from this.` + (combined ? `
4846
+
4847
+ Also write tasks/${task.taskId}/plan.md with ordered implementation steps, specific file references, and existing patterns to use.
4848
+
4849
+ ${planFeasibilityCheck("in plan.md's Approach section")}` : ""),
4813
4850
  bundleNote: isBundle ? "\nThese tasks are related \u2014 consider cross-task interactions while speccing." : "",
4814
4851
  doneNote: combined ? "The orchestrator will handle spec_review and plan-phase advancement automatically for fast-tier tasks." : "",
4815
4852
  selfCheck: [
@@ -4817,6 +4854,7 @@ Also write tasks/${task.taskId}/plan.md with ordered implementation steps, speci
4817
4854
  '- Every AC is verifiable with a specific test (not just "it works" \u2014 state exactly how to verify)',
4818
4855
  "- Affected Files lists specific files (not directories) with specific, actionable change descriptions",
4819
4856
  combined ? "- Plan steps reference actual function/file names from the codebase (not just concepts)" : null,
4857
+ combined ? "- plan.md's Approach records the feasibility check (exists, real path, callers, tests that can fail, predicates, Affected Files, async/stateful; N/A where it doesn't apply)" : null,
4820
4858
  "- Known Risks covers failure modes for the trickiest ACs",
4821
4859
  "- Human Test Plan describes product behavior only (no code, no file names, no TypeScript)",
4822
4860
  '- Validation Required has at least one entry checked (or explicitly "None" with a reason)',
@@ -4920,6 +4958,7 @@ function promptPlan(state) {
4920
4958
  taskScope: tasks.length > 1 ? "a bundle of tasks" : `task "${tasks[0].taskId}"`,
4921
4959
  roundBanner,
4922
4960
  verdictLines: verdictLines2,
4961
+ feasibilityCheck: planFeasibilityCheck("at the top of the appended `### Delta` section", " "),
4923
4962
  phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "plan", "done")
4924
4963
  });
4925
4964
  }
@@ -4931,6 +4970,7 @@ function promptPlan(state) {
4931
4970
  startup: CLAUDE_STARTUP,
4932
4971
  taskScope: tasks.length > 1 ? "a bundle of tasks" : `task "${tasks[0].taskId}"`,
4933
4972
  verdictLines,
4973
+ feasibilityCheck: planFeasibilityCheck("in the plan's Approach section"),
4934
4974
  phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "plan", "done")
4935
4975
  });
4936
4976
  }
@@ -5119,7 +5159,8 @@ function promptCodeReview(state, baseBranch, scopedDiff = null, coldCodexFinding
5119
5159
  hasColdCodexFindings: coldCodexFindings !== null,
5120
5160
  hasOutOfScopeFiles: outOfScopeFiles.length > 0,
5121
5161
  outOfScopeFilesList: outOfScopeFiles.map((file) => `- \`${file}\``).join("\n"),
5122
- phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "code_review", "done", "<verdict>")
5162
+ phaseCommands: phaseCommands(tasks.map((t) => t.taskId), "code_review", "done", "<verdict>"),
5163
+ blockedCommands: phaseCommands(tasks.map((t) => t.taskId), "code_review", "blocked")
5123
5164
  });
5124
5165
  }
5125
5166
  function promptQa(state, prTemplate) {
@@ -5153,7 +5194,7 @@ function isInstalledSourcePath(sourcePath) {
5153
5194
  return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
5154
5195
  }
5155
5196
  function resolveCanonVersion(explicit) {
5156
- return explicit ?? "3.4.0" ?? "dev";
5197
+ return explicit ?? "3.5.0" ?? "dev";
5157
5198
  }
5158
5199
  function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
5159
5200
  const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
@@ -8641,8 +8682,30 @@ async function recoverPhaseForTask(taskId, phase, initialStatus) {
8641
8682
  warn2(`Retry did not recover '${taskId}' ${phase} (${postEvidence.note}).`);
8642
8683
  return false;
8643
8684
  }
8685
+ function stopForReviewerBlock(taskIds, blockedIds, statuses) {
8686
+ const maxIter = statuses.reduce((max, status) => Math.max(max, getIterations(status)), 0);
8687
+ const recovery = taskIds.map((id) => `canon task reset-code-review ${id}`).join("; ");
8688
+ const reason = `Code review stopped without a verdict for task(s): ${blockedIds.join(", ")}. The reviewer set code_review blocked; see ${taskIds.map((id) => `tasks/${id}/review.md`).join(", ")}. No evidence recovery or retry was attempted. Recovery for the full blocked bundle [${taskIds.join(" ")}]: ${recovery}; then canon run ${taskIds.join(" ")}.`;
8689
+ console.log("");
8690
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
8691
+ console.log(" \u270B CODE REVIEW STOPPED \u2014 the reviewer could not reach a verdict.");
8692
+ console.log(" Read the stopped review:");
8693
+ for (const id of taskIds) console.log(` tasks/${id}/review.md`);
8694
+ console.log(" Reset every task in the bundle, then run again:");
8695
+ for (const id of taskIds) console.log(` canon task reset-code-review ${id}`);
8696
+ console.log(` canon run ${taskIds.join(" ")}`);
8697
+ console.log(" Re-running without the reset starts a fresh review pass without archiving the stopped review.md.");
8698
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
8699
+ console.log("");
8700
+ autoBlockPhase(taskIds, "code_review", maxIter, reason);
8701
+ process.exit(2);
8702
+ }
8644
8703
  async function checkAndRoute(phase, taskIds) {
8645
8704
  let statuses = taskIds.map(readStatus);
8705
+ if (phase === "code_review") {
8706
+ const blockedIds = taskIds.filter((_, index) => getPhaseStatus(statuses[index], "code_review") === "blocked");
8707
+ if (blockedIds.length > 0) stopForReviewerBlock(taskIds, blockedIds, statuses);
8708
+ }
8646
8709
  for (let i = 0; i < taskIds.length; i += 1) {
8647
8710
  const phaseStatus = getPhaseStatus(statuses[i], phase);
8648
8711
  if (phase === "implement" && phaseStatus === "done") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canon-ai",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "A spec-first, multi-agent AI coding pipeline you drop into any repo. The law your agents work under.",
6
6
  "engines": {
@@ -8,6 +8,7 @@ You are the anchored code-review lens in canon's three-lens review pipeline. You
8
8
  Do not write `review.md`.
9
9
  Do not run `canon task phase`.
10
10
  Do not ask the user for permission to edit files.
11
+ Do this review yourself: spawn no sub-agents and run no review skills or other reviewers.
11
12
 
12
13
  This agent intentionally declares no model override; it inherits the foreman's selected `code_review` model and effort.
13
14
 
@@ -54,3 +55,14 @@ OVERALL_SIGNAL: approve | changes_requested
54
55
  ```
55
56
 
56
57
  If Stage 1 failed, omit Stage 2 findings and set `OVERALL_SIGNAL: changes_requested`.
58
+
59
+ If Stage 1 passes and you find no Stage 2 issues, return:
60
+
61
+ ```text
62
+ STAGE_1: pass
63
+ AC_TABLE:
64
+ | AC | Met/Partial/Not Met | note |
65
+ STAGE_1_GAPS: (none)
66
+ STAGE_2_FINDINGS: (none)
67
+ OVERALL_SIGNAL: approve
68
+ ```
@@ -8,6 +8,7 @@ You are the cold-Claude code-review lens in canon's three-lens review pipeline.
8
8
  Do not write `review.md`.
9
9
  Do not run `canon task phase`.
10
10
  Do not ask the user for permission to edit files.
11
+ Do this review yourself: spawn no sub-agents and run no review skills or other reviewers.
11
12
 
12
13
  This agent intentionally declares no model override; it inherits the foreman's selected `code_review` model and effort.
13
14
 
@@ -5,6 +5,7 @@ Patterns from real production use for when the pipeline gets stuck. Use them as
5
5
  ## Contents
6
6
 
7
7
  - Auto-block on `spec_review` or `code_review` (loop cap hit)
8
+ - Code review stopped without a verdict (reviewer-set `blocked`)
8
9
  - Findings keep clustering in the same mechanism (try a size bump before another loop)
9
10
  - Phase mismatch — pipeline routes to `spec` when you expected `spec_review`
10
11
  - `--ship` refuses: wrong phase
@@ -35,6 +36,26 @@ Stop the reviewer loop once findings turn wording-only. Self-grep for flagged ph
35
36
 
36
37
  **Never reset the iteration counter** to bypass the cap. Counter is durable signal of how many review rounds the task has burned — losing it hides cost from future operators.
37
38
 
39
+ ---
40
+
41
+ ## Code review stopped without a verdict (reviewer-set `blocked`)
42
+
43
+ **Symptom:** the run exits with `✋ CODE REVIEW STOPPED — the reviewer could not reach a verdict`, and `code_review` is `blocked` with an escalation for every task in the bundle. No verdict box is checked in `review.md`.
44
+
45
+ **Cause:** one of the Claude review lenses returned nothing usable twice (absent, truncated, or not in its return format), so the foreman stopped rather than approve on partial evidence or invent findings. The pipeline deliberately does not retry this stop.
46
+
47
+ **Fix:**
48
+ 1. Read `tasks/<id>/review.md` — it names the missing lens and what came back. Look for the cause: a budget cap hit, a timeout, or a harness error in the run log. Raise `CLAUDE_BUDGET` if the lens ran out of budget.
49
+ 2. Reset every task in the bundle, then run the whole bundle again. The stop's banner prints these exact commands:
50
+ ```bash
51
+ canon task reset-code-review <task-id-1>
52
+ canon task reset-code-review <task-id-2>
53
+ canon run <task-id-1> <task-id-2>
54
+ ```
55
+ Run all bundle members together. A `canon run` with only one ID reviews the shared branch against that one task's spec and Affected Files, so the siblings' changes look out of scope.
56
+
57
+ `reset-code-review` archives the stopped `review.md` and starts a fresh review pass. Re-running without the reset also starts a fresh pass, but without archiving the stopped review. Don't hand-check a verdict box or set a verdict with `canon task phase` to get past the stop: the review that would justify it never happened.
58
+
38
59
  ## Findings keep clustering in the same mechanism (try a size bump before another loop)
39
60
 
40
61
  Most multi-round tasks are healthy — each round closes findings scattered across different, previously-untouched parts of the diff. That's normal iterative discovery, not a sizing problem; raising the loop cap (above) is the right tool for it.
@@ -165,7 +165,16 @@ Self-check before presenting:
165
165
  ### Phase 6 — After spec approval
166
166
 
167
167
  **XS tasks:**
168
- 1. Write `tasks/TASK-ID/plan.md` using `.canon/templates/plan.md` as structure.
168
+ 1. Write `tasks/TASK-ID/plan.md` using `.canon/templates/plan.md` as structure. Before writing steps, run the same feasibility check the pipeline's planner runs, and record the results in a few lines in the plan's Approach section. Mark an item that doesn't apply `N/A` in a word:
169
+ - **Exists:** the functions, files, and patterns the plan relies on exist — found by searching the code, not recalled.
170
+ - **Real path:** the actual runtime call path the change sits on, including any existing code on it that already does part of the work.
171
+ - **Callers:** every caller of each function whose behavior or error contract changes, found and accounted for.
172
+ - **Tests that can fail:** for each regression test the plan prescribes, the input or state that sends it through the changed path, so it fails without the change.
173
+ - **Predicates:** for each condition or state check the plan writes out, its boundary values (for example zero, empty, or non-numeric) and every state it must handle.
174
+ - **Scope:** every file the steps change is inside the spec's Affected Files.
175
+ - **Async/stateful:** for async or stateful changes — re-entry, cancellation or unmount, stale state, and ownership.
176
+
177
+ A finding that changes a step goes in that step. If the check contradicts the spec, don't work around it in the plan — on the fast tier no Codex `spec_review` will catch the mismatch. Amend the spec, re-present the change to the human, and wait for renewed approval. The earlier Phase 5 approval does not cover a spec changed after it, so don't record approval in step 2 until the human has approved the amended spec.
169
178
  2. Record the human's approval in `tasks/TASK-ID/spec-review.md`: check the **Approved** box and add a one-line note ("Fast tier — human conversational spec approval; Codex spec review skipped"). The phase gate reads this artifact before letting `spec_review` advance.
170
179
  3. Advance the phases with the helpers (they rederive the top-level `status` pointer):
171
180
  ```bash
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: canon-spec-review
3
3
  description: Use when a canon task spec is written and the human wants to surface BLOCKING issues before invoking `canon run <id>`. Triggers on "/canon-spec-review", "review the spec", "pre-flight the spec", "what would Codex catch", or before kicking off the pipeline. Useful for any spec with logic — for full-tier (S/M/L/XL/delicate) it pre-empts Codex spec_review iterations, for fast-tier (XS non-delicate) it's the only automated review layer since Codex spec_review auto-approves.
4
- argument-hint: "<task-id>"
5
- allowed-tools: Read Glob Grep Bash(canon task list*) Bash(git status*) Agent
4
+ argument-hint: "[task-id]"
5
+ allowed-tools: Read Glob Grep Bash(canon task list*) Bash(git status*) Agent AskUserQuestion
6
6
  effort: high
7
7
  ---
8
8
 
@@ -23,7 +23,11 @@ Don't use for already-committed code review (use `codex review --commit <SHA>` o
23
23
 
24
24
  ### 1. Verify inputs
25
25
 
26
- The task ID is `$ARGUMENTS`. If empty, stop: usage is `/canon-spec-review <task-id>`.
26
+ Resolve the task ID:
27
+
28
+ - **`$ARGUMENTS` names a task** → use it. This is how to review a task other than the one under discussion, or to run from a fresh session.
29
+ - **`$ARGUMENTS` is empty** → take the task from the conversation: the one just authored with `/canon-spec`, or the one the human has been discussing. Don't re-confirm an unambiguous pick.
30
+ - **Still unclear** (fresh session, or several tasks in play) → run `canon task list`, narrow to tasks at `spec` or `spec_review`, plus tasks at `plan` whose `status.json` shows `phases.plan.status: "pending"` (full tier waits at the human spec gate in that state). Use `AskUserQuestion` to pick one. If none match, say so rather than offering tasks already past the gate. Don't guess.
27
31
 
28
32
  Verify `tasks/<id>/spec.md` exists and is filled out (no `<placeholder>` text or "TBD" stubs). If it's still a template, stop and say so.
29
33
 
@@ -168,8 +168,8 @@ Claude writes QA summary → Human tests
168
168
 
169
169
  - Spec and plan are written in separate Claude sessions.
170
170
  - Codex runs a real spec review before the gate. Spec review starts with a **Shape Check** (is the problem real? is the framing right? is there a materially simpler solution? is the AC decomposition right?) before the implementability probe.
171
- - Codex model/effort scales with effective size (matrix below).
172
- - **Optional pre-pipeline self-review**: before invoking `canon run`, the operator can run `/canon-spec-review <task-id>` to dispatch three parallel sub-agents (structural / factual / spec-quality) at the spec and surface BLOCKING / STRONG / NIT findings inline. Catches the class of issues Codex's spec_review would surface across 2-3 iterations in one ~15-min pass. Opt-in; most valuable when iteration cost is real.
171
+ - Effective size selects both the Codex and Claude model/effort cells in the matrices below.
172
+ - **Optional pre-pipeline self-review**: before invoking `canon run`, the operator can run `/canon-spec-review` (task ID optional — it defaults to the task under discussion) to dispatch three parallel sub-agents (structural / factual / spec-quality) at the spec and surface BLOCKING / STRONG / NIT findings inline. Catches the class of issues Codex's spec_review would surface across 2-3 iterations in one ~15-min pass. Opt-in; most valuable when iteration cost is real.
173
173
 
174
174
  **Where validation happens**: Project-specific checks (lint, type-check, unit tests, e2e, etc.) run inside agent phases — Codex runs them during `implement` and records outcomes in the handoff; Claude verifies the outcomes table in Stage 1 code review and re-runs selectively when anything looks off. There is no separate orchestrator-run validation phase.
175
175
 
@@ -183,8 +183,8 @@ Set in `status.json` at task creation:
183
183
 
184
184
  | Field | Values | Purpose |
185
185
  |---|---|---|
186
- | `task_size` | `XS \| S \| M \| L \| XL` | Drives Codex model + effort selection and the pipeline tier. XS is fast-tier; S+ runs the full pipeline (including Codex spec review). |
187
- | `delicate` | `true \| false` | Forces the XL bucket (full Codex model, high implement effort) regardless of nominal size. Set when an undetected bug has materially harder-to-recover blast radius than a normal bug — common examples: auth, payments, premium gating, persistent storage migrations, security-sensitive cryptography. Project-specific surfaces also qualify (medical PHI, scientific reproducibility, regulated data). The bar is *blast radius*, not difficulty. |
186
+ | `task_size` | `XS \| S \| M \| L \| XL` | Drives the pipeline tier and effective-size model/effort cells for Codex and Claude. XS is fast-tier; S+ runs the full pipeline (including Codex spec review). |
187
+ | `delicate` | `true \| false` | Forces the XL bucket (full Codex model and high Claude spec/plan/code-review effort) regardless of nominal size. Set when an undetected bug has materially harder-to-recover blast radius than a normal bug — common examples: auth, payments, premium gating, persistent storage migrations, security-sensitive cryptography. Project-specific surfaces also qualify (medical PHI, scientific reproducibility, regulated data). The bar is *blast radius*, not difficulty. |
188
188
  | `human_spec_gate` | `true \| false` | **Single-use latch**, not a persistent toggle. `true` arms a one-time halt after `spec_review`, before planning (default: `true`). The orchestrator flips it to `false` *at the moment it halts* — so `false` means "the gate already fired (or was pre-cleared)," not "review was skipped." See [Spec gate is a single-use latch](#spec-gate-is-a-single-use-latch). |
189
189
  | `worktree` | `true \| false` | Worktree isolation. `canon task new` scaffolds this to `true`, so worktree mode is the effective default for every scaffolded task — set it to `false` to opt out and run in the main checkout. The orchestrator treats an *absent* field as `false`, but that fallback only applies to hand-rolled `status.json` files. See Worktree Isolation below. |
190
190
  | `base_branch` | string (default `"main"`) | Branch the task branches off and PRs against. Auto-set by `canon task new` from the current git checkout at task creation. |
@@ -223,7 +223,20 @@ Codex is tuned for token efficiency — the mini model handles most phases; the
223
223
 
224
224
  The cold-Codex `code_review` lens is the exception to size scaling: as a mandatory hard-fail gate, it runs at flat `high` effort and stays on the mini model at every size, including XL/delicate.
225
225
 
226
- `spec_review` runs at `high` from M upward. Task-history analysis found M's excess code_review iterations weren't an implement-quality gap (non-rerouted M and L tasks ran at nearly identical iteration counts) but a reroute-severity gap, with M's lighter spec_review effort the leading hypothesis. M and L also differ on loop cap, budget, and QA effort, so this isn't a proven sole cause; if you retune the matrix, re-measure your own M vs. L reroute rate.
226
+ `spec_review` runs at `high` from M upward. Task-history analysis found M's excess code_review iterations weren't an implement-quality gap (non-rerouted M and L tasks ran at nearly identical iteration counts) but a reroute-severity gap, with M's lighter spec_review effort the leading hypothesis. M and L also differ on loop cap and budget, so this isn't a proven sole cause; if you retune the matrix, re-measure your own M vs. L reroute rate.
227
+
228
+ ## Claude Model/Effort Matrix
229
+
230
+ Claude model and effort scale with effective task size. The light tier defaults to `sonnet`, and the strong tier defaults to `opus`:
231
+
232
+ | Phase | XS | S | M | L | XL / delicate |
233
+ |---|---|---|---|---|---|
234
+ | `spec` | sonnet / medium | sonnet / medium | opus / medium | opus / medium | opus / high |
235
+ | `plan` | sonnet / medium | sonnet / medium | opus / medium | opus / medium | opus / high |
236
+ | `code_review` | sonnet / medium | sonnet / medium | opus / medium | opus / medium | opus / high |
237
+ | `qa` | sonnet / medium | sonnet / medium | sonnet / medium | sonnet / medium | sonnet / medium |
238
+
239
+ `spec`, `plan`, and `code_review` move to the strong tier at M, where sustained judgment matters; only XL/delicate raises effort to `high`. `qa` writes completion artifacts and cannot block or reroute a task, so it stays light/medium at every size. No Claude cell runs above `high`.
227
240
 
228
241
  ## Claude Budget Matrix
229
242
 
@@ -238,15 +251,18 @@ Claude phase budgets scale with task size. `spec`/`plan`/`qa` are single-pass Cl
238
251
 
239
252
  ## Environment Variables
240
253
 
241
- Claude is tuned for correctness — Opus on phases where false negatives cascade, Sonnet on structured/templated phases.
254
+ Claude uses the light tier for XS/S spec, plan, and code review and for QA at every size. M and above use the strong tier for spec, plan, and code review. A phase pin changes only the model; effort comes from the matrix.
242
255
 
243
256
  | Variable | Default | Purpose |
244
257
  |---|---|---|
245
- | `CLAUDE_MODEL_SPEC` | `opus` | Spec phase (foundational; cascades into every downstream phase). |
246
- | `CLAUDE_MODEL_PLAN` | `sonnet` | Plan phase (structured translation of spec → steps). |
247
- | `CLAUDE_MODEL_REVIEW` | `sonnet` | Code review for XS/S/M/L (Sonnet 4.6 matches the prior Opus flagship on long-horizon / lifecycle / state-machine bug detection — re-baselined 2026-06; L was Opus on Sonnet 4.5). |
248
- | `CLAUDE_MODEL_REVIEW_LARGE` | `opus` | Code review for XL/delicate only — the highest-blast-radius tier where the subtlest cross-file bugs warrant Opus. |
249
- | `CLAUDE_MODEL_QA` | `sonnet` | QA phase. |
258
+ | `CLAUDE_MODEL_LIGHT` | `sonnet` | Model for light-tier cells: XS/S `spec`/`plan`/`code_review`, and `qa` at every size, unless a phase pin applies. |
259
+ | `CLAUDE_MODEL_STRONG` | `opus` | Model for strong-tier cells: M and above `spec`/`plan`/`code_review`, unless a phase pin applies. |
260
+ | `CLAUDE_MODEL_SPEC` | _(unset)_ | Pins the `spec` model at every size, replacing the model only; effort still comes from the matrix. |
261
+ | `CLAUDE_MODEL_PLAN` | _(unset)_ | Pins the `plan` model at every size where a separate plan phase runs (XS fast tier combines spec and plan under the `spec` cell), replacing the model only; effort still comes from the matrix. |
262
+ | `CLAUDE_MODEL_REVIEW` | _(unset)_ | Pins the `code_review` model for XS/S/M/L only, replacing the model only; effort still comes from the matrix. Does not reach XL/delicate. |
263
+ | `CLAUDE_MODEL_REVIEW_LARGE` | _(unset)_ | Pins the `code_review` model for XL/delicate only, replacing the model only; effort still comes from the matrix. `_LARGE` means task size, not model tier. Does not reach XS–L. |
264
+ | `CLAUDE_MODEL_QA` | _(unset)_ | Pins the `qa` model at every size, replacing the model only; effort still comes from the matrix. |
265
+ | `CLAUDE_MODEL` | _(unset; deprecated)_ | Legacy catch-all model for every Claude phase, including `qa`, only where no phase pin or light/strong tier variable is set. Phase pin → tier variable → this fallback → tier default. |
250
266
  | `CLAUDE_BUDGET` | _(phase- and size-aware)_ | Max spend per Claude phase (USD). Unset → resolved from the Claude Budget Matrix above (phase × size). Set → flat cap applied uniformly across every phase and size (e.g. `CLAUDE_BUDGET=15.00` overrides every phase to $15). |
251
267
  | `CANON_PROJECT_NAME` | _(reads `package.json` "name" or "your project")_ | Name injected into agent prompts. |
252
268
  | `CANON_WORKTREES_ROOT` | .canon/worktrees | Where task worktrees are created. When overridden, the orchestrator warns if the path isn't in `.claude/settings*.json` `additionalDirectories`. |
@@ -361,6 +377,7 @@ After `spec_review` or `code_review`, the orchestrator checks the verdict.
361
377
  | `spec_review` | `changes_requested` | Loop back to `spec` automatically, except reroute-amendment reviews block for human revision. |
362
378
  | `code_review` | `changes_requested` / `needs_re_review` | Loop back to `implement` automatically. |
363
379
  | `code_review` | `spec_gap` | Block the whole `code_review` bundle with an escalation. Fix path: amend the `spec_gap` task specs and run `canon run <ids> --reroute`. Bless path: `canon task accept <ids> code_review --reason "<why>"`. |
380
+ | `code_review` | reviewer-set `blocked` (no verdict) | Block the whole `code_review` bundle with an escalation and exit `2`; no evidence recovery or retry is attempted. The stop names each task's `review.md`. Recovery: `canon task reset-code-review <id>` for each task, then `canon run <ids>`. |
364
381
  | `spec_review` / `code_review` | `approved` / `approved_with_nits` / `sanctioned` | Continue to the next phase. `sanctioned` is status-only and written by `canon task accept`, not by review artifacts; `canon task accept` refuses a review phase with no recorded verdict unless `--force` is passed. |
365
382
 
366
383
  **Auto-block on runaway loops**: When spec review or code review reaches or exceeds the size-aware cap (3 for XS/S/M, 5 for L/XL, or `MAX_REVIEW_LOOPS` if set), the orchestrator auto-blocks that review phase and appends an entry to `escalations` in `status.json`. The primary check runs at the next revision phase's entry, before a spec revision or re-implementation starts; the review phase retains the same check as a defense-in-depth backstop for unexpected state. For code review the cap check is the **sum** of `iterations_current_loop` and `preflight_rejections_current_loop` — handoffs bounced by the deterministic pre-flight gate count toward the same budget, and recovery requires resetting both counters (the block message says which). Separately, a pre-flight where every blocker is a `blocked` validation row auto-blocks immediately without consuming loop budget. On approval, `iterations_current_loop` resets to `0` while `iterations_total` (lifetime verdict count) and `auto_block_count` are preserved — history is never erased.