canon-ai 3.18.0 → 3.19.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,26 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.19.0] — 2026-11-09
8
+
9
+ ### Added
10
+
11
+ - **Node 26 is supported alongside Node 24.** canon's `engines` range is `24.x || 26.x`, so installing on Node 26 no longer warns. `canon doctor` passes on 24 and 26, warns on other majors from 24 up (such as 25 and 27), and still fails below 24.
12
+ - **Implementers self-check before handoff, and the pipeline checks the record.** Implement prompts list the files that reference the task's Affected Files and the files containing identifiers and flags the spec names, as a bounded map that leaves out `linguist-generated` files and canon's log docs. The handoff gains a `## Self-Check` table covering test integrity, restatement sweep, and sibling parity, each with evidence or `N/A` and a reason. Before code review, the pipeline sends back a handoff with missing rows, a missing baseline section, or evidence that is empty, a bare `N/A`, or still the template placeholder. The anchored reviewer checks each row against the diff. A task already in review when you upgrade gets one rejection to add the baseline section. Editing a file outside Affected Files still requires a Blocker.
13
+
14
+ ### Changed
15
+
16
+ - **Implementers see the pitfalls and lessons that touch their task's files.** Implement prompts no longer inject the whole `## Known Pitfalls` section. They select the entries, Trigger Table rows, and lessons that cite a path in the spec's Affected Files (a listed directory covers the paths under it) and show the others as one-line titles within a size cap. A Known Pitfalls section written as prose, with no `###` headings, is included in full. Split pitfall docs are indexed by path glob. To be selected, a pitfall must cite its path inside backticks or as a link target, using a trailing `/` for a directory or `*` for a glob. The scaffold's `docs/patterns.md` shows the convention.
17
+ - **Spec prompts carry the term-renames rule.** When a spec renames a term used across the codebase, the author and reviser gate each string family with its own zero-result search instead of a hand-listed set of hits.
18
+ - **`canon watch` labels an automatic loop-back `(review loop)`.** `(reroute)` now appears only after a human `canon run --reroute`.
19
+ - **`/canon-spec` asks for "go" before a delicate full-send.** It used to promise a 5-second abort window that it never honored.
20
+
21
+ ### Fixed
22
+
23
+ - **`--pr` no longer stops on broken references in code-review notes.** Code review gets the same citation rule and reference fix-up as implement and QA. A fix that would change the verdict, or touch any file other than the review, blocks code review instead.
24
+ - **Agent runs no longer fail when a prompt embeds content containing NUL bytes.** NULs are escaped before the prompt is passed to the agent CLI.
25
+ - **`/canon-status` reports a full-tier task waiting at the human spec gate**, and `/canon-spec-review` rates an unlisted judgment call BLOCKING, as the pipeline's `spec_review` does.
26
+
7
27
  ## [3.18.0] — 2026-11-04
8
28
 
9
29
  ### Changed
package/README.md CHANGED
@@ -32,7 +32,7 @@ canon task new my-first-feature "Short description"
32
32
  canon run my-first-feature
33
33
  ```
34
34
 
35
- Needs Node 24.x and git. See [Getting started](#getting-started) below for prerequisites, permission allowlists, and the trust model before running this on a repo you care about.
35
+ Needs Node 24.x or 26.x and git. See [Getting started](#getting-started) below for prerequisites, permission allowlists, and the trust model before running this on a repo you care about.
36
36
 
37
37
  ## What this is
38
38
 
@@ -59,7 +59,7 @@ Canon is what happens when you treat those failure modes as engineering problems
59
59
  |---|---|
60
60
  | Agent jumps to implementation before requirements are clear | **Spec phase** before code, written conversationally with grilling for non-trivial tasks |
61
61
  | Agent silently drops a requirement | **Two-stage code review** — Stage 1 is a spec-compliance gate that fails the whole review if any AC is missing from the cross-reference table |
62
- | Agent reinvents an existing pattern | **`docs/patterns.md` trigger table** + Known Pitfalls injected into implement prompts |
62
+ | Agent reinvents an existing pattern | **`docs/patterns.md` trigger table** + pitfalls and lessons selected by spec Affected Files and injected into implement prompts; a Known Pitfalls section without `###` entries is injected whole except blockquotes and index tables, and a directory in Affected Files selects pitfalls citing paths inside it |
63
63
  | Agent re-debates a settled decision | **`docs/decisions.md`** — agents read, don't re-propose |
64
64
  | Agent forgets a past mistake | **`docs/lessons-learned.md`** — distilled cross-task wisdom that promotes into permanent rules over time |
65
65
  | Reviews loop forever on the same disagreement | **Auto-block on runaway review loops** — per-size caps (see `pipeline-policy.ts`), escalation to human after the cap |
@@ -113,7 +113,7 @@ Multiple task IDs in one invocation activates **bundle mode** — `canon run id1
113
113
 
114
114
  ### Prerequisites
115
115
 
116
- - **Node 24.x**
116
+ - **Node 24.x or 26.x**
117
117
  - **git**
118
118
  - **Claude Code (≥ 2.1.72)** — `npm install -g @anthropic-ai/claude-code`
119
119
  - **Codex CLI** (required for the default `mixed` provider and for `/canon-inline-review` under either provider; `CANON_PROVIDER=claude` pipeline runs don't need it, and Claude Code's `/code-review` covers inline review without it) — `npm install -g @openai/codex`
@@ -153,7 +153,7 @@ cd your-project
153
153
  canon init
154
154
  ```
155
155
 
156
- `canon init` installs the canon task templates and Claude Code skills in your project. Open Claude Code in your project directory and run `/canon-init` to start the interactive setup. The skill grills Claude on your codebase — one question at a time, with recommended answers — and generates the `docs/` knowledge corpus tailored to your project. If you want agent files, generate them separately with the built-in `/init` in Claude Code or Codex; those files are adopter-owned and canon does not scaffold, modify, or read them.
156
+ `canon init` installs the canon task templates, Claude Code skills, the review sub-agents under `.claude/agents/`, the guard hook, and the pipeline-orchestrator doc in your project. Open Claude Code in your project directory and run `/canon-init` to start the interactive setup. The skill grills Claude on your codebase — one question at a time, with recommended answers — and generates the `docs/` knowledge corpus tailored to your project. If you want agent files, generate them separately with the built-in `/init` in Claude Code or Codex; those files are adopter-owned and canon does not scaffold, modify, or read them.
157
157
 
158
158
  ### Generate your agent files with the built-in `/init`
159
159
 
@@ -189,7 +189,7 @@ canon task new my-first-feature "Short description"
189
189
  canon run my-first-feature
190
190
  ```
191
191
 
192
- Run `canon doctor` after install to verify your environment — it checks Node/git/Claude Code versions, allowlist coverage, and that any local Claude settings file is gitignored, plus Codex CLI presence and codex project trust when `CANON_PROVIDER` is `mixed`, empty, or unset. Doctor checks that environment setting, not providers pinned on existing tasks, so mixed-pinned tasks still need Codex.
192
+ Run `canon doctor` after install to verify your environment — it checks Node (24.x or 26.x; warns on other majors >= 24), git, and Claude Code versions, allowlist coverage, the `.canon/config.json` docs-freshness config, and that any local Claude settings file is gitignored, plus Codex CLI presence and codex project trust when `CANON_PROVIDER` is `mixed`, empty, or unset. Doctor checks that environment setting, not providers pinned on existing tasks, so mixed-pinned tasks still need Codex.
193
193
 
194
194
  ### Discovery nudge (recommended)
195
195
 
@@ -334,7 +334,7 @@ Run canon on repositories where you would be comfortable running your test suite
334
334
  | Command | What it does |
335
335
  |---|---|
336
336
  | `canon init` | Install canon into the current repo. Refuses symlinks at destination files or parent directories; replace them with regular files/directories first. |
337
- | `canon doctor` | Verify environment and canon setup — Node/git versions, Claude Code (≥ 2.1.72), Codex CLI presence and codex project trust (when `CANON_PROVIDER` is `mixed`, empty, or unset), recommended-permission coverage, guard-hook registration, and that any local settings file is gitignored. |
337
+ | `canon doctor` | Verify environment and canon setup — Node (24.x or 26.x; warns on other majors >= 24), git, Claude Code (≥ 2.1.72), Codex CLI presence and codex project trust (when `CANON_PROVIDER` is `mixed`, empty, or unset), recommended-permission coverage, guard-hook registration, the `.canon/config.json` docs-freshness config, and that any local settings file is gitignored. |
338
338
  | `canon completion <shell>` | Print a completion script for bash, zsh, fish, or tcsh; see Shell completion. |
339
339
  | `canon task new <id> "Title"` | Scaffold a new task from templates |
340
340
  | `canon task list` | Show all tasks and their pipeline phase |
@@ -457,21 +457,13 @@ Drop this into any repo and you have:
457
457
  - Full pipeline orchestrator with phase routing, worktree isolation, session resumption, auto-block, bundle mode, `--reroute`, `--ship`. Bundled into `dist/orchestrator/run-task.js` and invoked via `canon run`.
458
458
  - Pure routing policy module (tier/sizing/model/effort matrix), table-tested.
459
459
  - `canon task` lifecycle CLI (new / list / status / phase / accept / set / reset-spec-review / reset-code-review / post-merge-sync), in-process TS.
460
- - `.canon/templates/` — artifact templates (status, spec, spec-review, plan, handoff, review, done, notes)
460
+ - `.canon/templates/` — artifact templates (status, spec, spec-review, plan, handoff, review, done, notes, pr-body)
461
461
  - Agent rules and per-agent guidance delivered just-in-time via per-phase prompt templates (`src/orchestrator/prompts/templates/`) and the `/canon-*` skills; adopters generate their own `AGENTS.md` / `CLAUDE.md` with the built-in `/init`
462
462
  - `docs/` — knowledge corpus templates with detailed scaffolding
463
463
  - `.claude/hooks/canon-guard.mjs` — Claude Code guard hook (registration is recommend-only; canon never writes `.claude/settings.json`)
464
464
  - Claude Code skills installed by `canon init`: `/canon-init` (knowledge-corpus bootstrap), `/canon-spec` (new task authoring), `/canon-spec-review` (pre-flight a spec), `/canon-inline-review` (independent cross-review of below-pipeline work), `/canon-pipeline` (drive an existing task), `/canon-status` (in-flight task map), `/canon-changelog` (release notes for versioned projects), `/canon-sweep` (triage the lessons-learned buffer)
465
465
  - Unit-test suite covering the policy module, orchestrator extractors, and validation parsers (`npm test`)
466
466
 
467
- ## Where to Go Deeper
468
-
469
- - `docs/pipeline-orchestrator.md` for orchestrator mechanics, reroute, worktrees, and PR/ship flow.
470
- - `docs/release-process.md` for release-cut mechanics and branch/version flow.
471
- - `docs/patterns.md` for implementation pitfalls and load-bearing patterns.
472
- - `docs/decisions.md` for settled rules and architecture decisions.
473
- - `docs/codebase-map.md` for file locations and entry points.
474
-
475
467
  🚧 **Stubbed with `TODO[canon]:` markers — fill in for your project:**
476
468
 
477
469
  - Validation command bindings in `docs/architecture.md` §Validation (which commands satisfy each check category — the universal category matrix ships complete in `implement.md` / `.canon/templates/spec.md`)
@@ -484,11 +476,19 @@ Drop this into any repo and you have:
484
476
  - Pre-built docs-check / external-API-citation tooling (project-specific)
485
477
  - Per-language project bootstrappers (Python / Rust / Go variants)
486
478
 
479
+ ## Where to Go Deeper
480
+
481
+ - `docs/pipeline-orchestrator.md` for orchestrator mechanics, reroute, worktrees, and PR/ship flow.
482
+ - `docs/release-process.md` for release-cut mechanics and branch/version flow.
483
+ - `docs/patterns.md` for implementation pitfalls and load-bearing patterns.
484
+ - `docs/decisions.md` for settled rules and architecture decisions.
485
+ - `docs/codebase-map.md` for file locations and entry points.
486
+
487
487
  ## Supported platforms
488
488
 
489
489
  - **macOS** and **Linux** are the supported targets. Canon's worktree helpers shell out to `git` for `worktree add`/`remove` and `worktree list --porcelain` parsing.
490
490
  - **Windows is not supported.** Use WSL2.
491
- - **Node**: 24.x.
491
+ - **Node**: 24.x or 26.x.
492
492
 
493
493
  ## The canon philosophy
494
494
 
@@ -502,7 +502,7 @@ The implication: **the value of canon compounds with use**. A 6-month-old canon
502
502
 
503
503
  **Phase 1 (now)**: Layer 1 + Layer 2 ship. Full CLI, open source, installable from npm or GitHub. `/canon-init` skill for interactive project bootstrap. Validate against real projects.
504
504
 
505
- **Phase 2 (next)**: Make the implementer slot pluggable — adapter interface for Codex CLI, Gemini CLI, Aider, others. Architect slot stays Claude Code (skills are load-bearing).
505
+ **Phase 2 (next)**: Make the implementer slot pluggable — an adapter interface beyond Claude Code and Codex CLI (Gemini CLI, Aider, others). Architect slot stays Claude Code (skills are load-bearing).
506
506
 
507
507
  **Phase 3 (research)**: Productize. Hosted bootstrap service? Per-language scaffolds? Marketplace for `docs/patterns.md` starter packs (Next.js patterns, Rails patterns, etc.)? TBD based on Phase 1 validation.
508
508
 
package/dist/cli/index.js CHANGED
@@ -1925,14 +1925,24 @@ function checkPlatform() {
1925
1925
  detail: "Windows without WSL \u2014 canon is untested here; use WSL for best results"
1926
1926
  };
1927
1927
  }
1928
- function checkNodeVersion() {
1929
- const match = process.version.match(/^v(\d+)/);
1928
+ var SUPPORTED_NODE_MAJORS = [24, 26];
1929
+ var NODE_BUILD_FLOOR = 24;
1930
+ function checkNodeVersion(version = process.version) {
1931
+ const match = version.match(/^v(\d+)/);
1930
1932
  const major = match ? parseInt(match[1], 10) : 0;
1931
- if (major >= 24) return { label: `node ${process.version}`, status: "pass" };
1933
+ const label = `node ${version}`;
1934
+ if (major < NODE_BUILD_FLOOR) {
1935
+ return {
1936
+ label,
1937
+ status: "fail",
1938
+ detail: "node 24+ required \u2014 https://nodejs.org"
1939
+ };
1940
+ }
1941
+ if (SUPPORTED_NODE_MAJORS.includes(major)) return { label, status: "pass" };
1932
1942
  return {
1933
- label: `node ${process.version}`,
1934
- status: "fail",
1935
- detail: "node 24+ required \u2014 https://nodejs.org"
1943
+ label,
1944
+ status: "warn",
1945
+ detail: `untested Node major; canon supports ${SUPPORTED_NODE_MAJORS.join(" and ")} \u2014 https://nodejs.org`
1936
1946
  };
1937
1947
  }
1938
1948
  function checkBinary(cmd, required, hint) {
@@ -2025,7 +2035,7 @@ function checkTemplates(cwd) {
2025
2035
  }
2026
2036
  function checkCanonVersion(cwd) {
2027
2037
  const versionPath = join(cwd, ".canon", "version");
2028
- const installedVersion = "3.18.0";
2038
+ const installedVersion = "3.19.0";
2029
2039
  if (!existsSync(versionPath)) {
2030
2040
  return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
2031
2041
  }
@@ -2569,7 +2579,7 @@ function initCmd(args2) {
2569
2579
  }
2570
2580
  function writeCanonVersion(cwd) {
2571
2581
  const versionPath = join2(cwd, ".canon", "version");
2572
- const version = "3.18.0";
2582
+ const version = "3.19.0";
2573
2583
  mkdirSync(dirname2(versionPath), { recursive: true });
2574
2584
  writeFileSync(versionPath, version + "\n");
2575
2585
  }
@@ -2967,26 +2977,31 @@ function findPreviousDonePhase(status, beforePhase) {
2967
2977
  function formatPhaseTransition(from, to) {
2968
2978
  return `${from}\u2192${to}`;
2969
2979
  }
2970
- function formatPhasePointerTransition(from, to) {
2980
+ function formatPhasePointerTransition(from, to, humanReroute) {
2971
2981
  const fromIdx = PHASE_ORDER.indexOf(from);
2972
2982
  const toIdx = PHASE_ORDER.indexOf(to);
2973
- const reroute = fromIdx !== -1 && toIdx !== -1 && toIdx < fromIdx;
2974
- return `${from} \u2192 ${to}${reroute ? " (reroute)" : ""}`;
2983
+ const backwards = fromIdx !== -1 && toIdx !== -1 && toIdx < fromIdx;
2984
+ if (!backwards) return `${from} \u2192 ${to}`;
2985
+ return `${from} \u2192 ${to} ${humanReroute ? "(reroute)" : "(review loop)"}`;
2975
2986
  }
2976
2987
  function displayedPhasePointer(ctx) {
2977
2988
  if (ctx.statusResult.kind !== "ok" || !isStatusJson(ctx.statusResult.status)) return null;
2978
2989
  return deriveTopLevelStatus(ctx.statusResult.status);
2979
2990
  }
2980
- var REROUTE_VERDICTS = {
2991
+ function rerouteCount(ctx) {
2992
+ if (ctx.statusResult.kind !== "ok" || !isStatusJson(ctx.statusResult.status)) return 0;
2993
+ return ctx.statusResult.status.phases.implement?.reroute_count ?? 0;
2994
+ }
2995
+ var REVIEW_LOOP_VERDICTS = {
2981
2996
  spec_review: ["changes_requested"],
2982
2997
  code_review: ["changes_requested", "needs_re_review"]
2983
2998
  };
2984
- function pointerIsMidReroute(ctx) {
2999
+ function pointerIsMidReviewLoop(ctx) {
2985
3000
  if (ctx.statusResult.kind !== "ok" || !isStatusJson(ctx.statusResult.status)) return false;
2986
3001
  for (const phase of ["spec_review", "code_review"]) {
2987
3002
  const entry = ctx.statusResult.status.phases[phase];
2988
3003
  if (entry?.status !== "done") continue;
2989
- if (entry.verdict != null && REROUTE_VERDICTS[phase].includes(entry.verdict)) return true;
3004
+ if (entry.verdict != null && REVIEW_LOOP_VERDICTS[phase].includes(entry.verdict)) return true;
2990
3005
  }
2991
3006
  return false;
2992
3007
  }
@@ -3479,16 +3494,19 @@ ${unknownOptionMessage("watch", preScan.argument).split("\n")[1]}` : "";
3479
3494
  return reportInitialFailure(initialAttach);
3480
3495
  }
3481
3496
  let previousPhasePointer = displayedPhasePointer(ctx);
3497
+ let previousRerouteCount = rerouteCount(ctx);
3482
3498
  const reportPhasePointer = (pollCtx) => {
3483
3499
  let currentPhase = displayedPhasePointer(pollCtx);
3484
- if (currentPhase != null && previousPhasePointer != null && pointerIsMidReroute(pollCtx)) {
3485
- currentPhase = previousPhasePointer;
3486
- }
3500
+ const holding = currentPhase != null && previousPhasePointer != null && pointerIsMidReviewLoop(pollCtx);
3501
+ if (holding) currentPhase = previousPhasePointer;
3502
+ const currentRerouteCount = rerouteCount(pollCtx);
3487
3503
  if (previousPhasePointer != null && currentPhase != null && previousPhasePointer !== currentPhase) {
3488
- stderr(`canon watch: phase ${formatPhasePointerTransition(previousPhasePointer, currentPhase)}
3504
+ const humanReroute = currentRerouteCount > previousRerouteCount;
3505
+ stderr(`canon watch: phase ${formatPhasePointerTransition(previousPhasePointer, currentPhase, humanReroute)}
3489
3506
  `);
3490
3507
  }
3491
3508
  previousPhasePointer = currentPhase;
3509
+ if (!holding) previousRerouteCount = currentRerouteCount;
3492
3510
  };
3493
3511
  if (initialAttach.kind === "launch_window") {
3494
3512
  const remaining = remainingTimeoutMs();
@@ -3515,6 +3533,7 @@ ${unknownOptionMessage("watch", preScan.argument).split("\n")[1]}` : "";
3515
3533
  return reportInitialFailure(postWaitAttach);
3516
3534
  }
3517
3535
  previousPhasePointer = displayedPhasePointer(ctx);
3536
+ previousRerouteCount = rerouteCount(ctx);
3518
3537
  } else if (waitResult.kind === "pid-died") {
3519
3538
  emitSummary(stdout, { state: "in_progress", reason: "death" });
3520
3539
  return exit(4);
@@ -3649,7 +3668,7 @@ function isInstalledSourcePath(sourcePath) {
3649
3668
  return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
3650
3669
  }
3651
3670
  function resolveCanonVersion(explicit) {
3652
- return explicit ?? "3.18.0" ?? "dev";
3671
+ return explicit ?? "3.19.0" ?? "dev";
3653
3672
  }
3654
3673
  function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
3655
3674
  const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
@@ -5957,7 +5976,7 @@ function currentPinFromManifest(manifest) {
5957
5976
  return "unknown";
5958
5977
  }
5959
5978
  function bakedVersion() {
5960
- return "3.18.0";
5979
+ return "3.19.0";
5961
5980
  }
5962
5981
  function ownPackageName(pkgDir) {
5963
5982
  try {
@@ -6454,7 +6473,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
6454
6473
  cutoverWarnings.push(docsRefsCheckRel);
6455
6474
  }
6456
6475
  const versionPath = join5(cwd, ".canon", "version");
6457
- const newVersion = "3.18.0";
6476
+ const newVersion = "3.19.0";
6458
6477
  const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
6459
6478
  if (currentVersion !== newVersion) {
6460
6479
  pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
@@ -7073,7 +7092,7 @@ Global:
7073
7092
  Run 'canon <command> --help' for details.`);
7074
7093
  }
7075
7094
  function printVersion() {
7076
- console.log("3.18.0");
7095
+ console.log("3.19.0");
7077
7096
  }
7078
7097
  switch (command) {
7079
7098
  case "doctor":