canon-ai 3.3.1 → 3.4.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,30 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.4.0] — 2026-09-27
8
+
9
+ ### Added
10
+
11
+ - **`code_review` catches files outside the spec's Affected Files at pre-flight instead of at `--pr`.** Previously, a helper, fixture, or rebuilt file the spec didn't list could pass `code_review`, only for `--pr`'s base-drift check to reject it. That forced a spec edit and a second `--pr`. The same scope check now runs just before `code_review`. A normal run halts before any reviewer starts, names the file, and offers two fixes: add it to Affected Files and re-run, or reroute to remove it. In a full-send run, the code-review foreman judges each out-of-scope file instead. It classes each one as an appropriate scope expansion, a spec miss, or something that shouldn't have changed, and records the outcome in `review.md`. For the first two, it also adds a `## Amendment (full-send scope)` section to the spec. That heading does not satisfy the human amendment gate on `--reroute`. `--pr`'s base-drift message now says whether the base branch advanced or the file is out of scope. It no longer labels both cases as scope violations.
12
+ - **The anchored code-review lens checks changed code against `docs/patterns.md`.** When the project keeps that file, the lens reads it first, using its Trigger Table if it has one. It reports each violation of a written project rule as a finding that names the rule, not as a nit. The check runs every round and covers only code the diff changes.
13
+
14
+ ### Changed
15
+
16
+ - **`code_review` re-rounds review only the fix, not the whole diff, when that's safe.** A full review still runs in these cases:
17
+ - Round 1.
18
+ - Every round of an XL or delicate task.
19
+ - Any round where the previous round's record is missing or unusable.
20
+ - The fix touches files outside the previous round's change set.
21
+ - The fix exceeds a size threshold.
22
+
23
+ Otherwise, cold-Codex and the cold-Claude lens review only the changes since the commit the previous round reviewed. The anchored lens keeps the full diff, spec, and handoff. No lens is skipped. `review.md` states each round's scope, base, and reason. Each cold-Codex round now writes its own `tasks/<id>/review-cold-codex-run-N.md` instead of overwriting the previous one. The cold-Claude lens also gains a standing sibling-site sweep: when a diff adds or changes a guard or invariant, it looks for other sites that need the same treatment.
24
+ - **The handoff template's guidance on directory paths matches `docs-refs-check`.** Backticking an existing directory is fine. A directory the task deleted should be described in prose or written as a markdown link.
25
+
26
+ ### Fixed
27
+
28
+ - **The post-implement auto-commit no longer fails when the spec's Affected Files lists a deleted or renamed file.** A path that git had already staged as deleted or renamed away made `git add` fail with "pathspec did not match". All three commit sites now drop paths that are neither tracked nor on disk before staging.
29
+ - **`docs-refs-check` accepts backticked paths to existing directories.** It had reported them as missing files.
30
+
7
31
  ## [3.3.1] — 2026-09-24
8
32
 
9
33
  ### Fixed
package/dist/cli/index.js CHANGED
@@ -1247,7 +1247,7 @@ function checkTemplates(cwd) {
1247
1247
  }
1248
1248
  function checkCanonVersion(cwd) {
1249
1249
  const versionPath = join(cwd, ".canon", "version");
1250
- const installedVersion = "3.3.1";
1250
+ const installedVersion = "3.4.0";
1251
1251
  if (!existsSync(versionPath)) {
1252
1252
  return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
1253
1253
  }
@@ -1744,7 +1744,7 @@ function initCmd(_args) {
1744
1744
  }
1745
1745
  function writeCanonVersion(cwd) {
1746
1746
  const versionPath = join2(cwd, ".canon", "version");
1747
- const version = "3.3.1";
1747
+ const version = "3.4.0";
1748
1748
  mkdirSync(dirname2(versionPath), { recursive: true });
1749
1749
  writeFileSync(versionPath, version + "\n");
1750
1750
  }
@@ -2696,16 +2696,17 @@ function watchCmd(args2, deps = {}) {
2696
2696
 
2697
2697
  // src/task/index.ts
2698
2698
  import { spawnSync as spawnSync7 } from "child_process";
2699
- import fs13 from "fs";
2699
+ import fs14 from "fs";
2700
2700
  import path14 from "path";
2701
2701
 
2702
2702
  // src/orchestrator/canon-snapshot.ts
2703
2703
  import { spawnSync as spawnSync6 } from "child_process";
2704
- import fs9 from "fs";
2704
+ import fs10 from "fs";
2705
2705
  import path10 from "path";
2706
2706
 
2707
2707
  // src/orchestrator/git.ts
2708
2708
  import { spawnSync as spawnSync5 } from "child_process";
2709
+ import fs9 from "fs";
2709
2710
  import path9 from "path";
2710
2711
 
2711
2712
  // src/orchestrator/worktree.ts
@@ -2753,7 +2754,7 @@ function isInstalledSourcePath(sourcePath) {
2753
2754
  return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
2754
2755
  }
2755
2756
  function resolveCanonVersion(explicit) {
2756
- return explicit ?? "3.3.1" ?? "dev";
2757
+ return explicit ?? "3.4.0" ?? "dev";
2757
2758
  }
2758
2759
  function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
2759
2760
  const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
@@ -2832,24 +2833,24 @@ function applyCanonSnapshot(status, canon) {
2832
2833
  return next;
2833
2834
  }
2834
2835
  function refreshCanonSnapshotAtPath(statusFilePath, options = {}) {
2835
- const status = JSON.parse(fs9.readFileSync(statusFilePath, "utf8"));
2836
+ const status = JSON.parse(fs10.readFileSync(statusFilePath, "utf8"));
2836
2837
  const canon = captureCanonSnapshot(REPO_ROOT, options);
2837
2838
  const next = applyCanonSnapshot(status, canon);
2838
2839
  const serialized = `${JSON.stringify(next, null, 2)}
2839
2840
  `;
2840
- const current = fs9.readFileSync(statusFilePath, "utf8");
2841
+ const current = fs10.readFileSync(statusFilePath, "utf8");
2841
2842
  if (current !== serialized) {
2842
- fs9.writeFileSync(statusFilePath, serialized, "utf8");
2843
+ fs10.writeFileSync(statusFilePath, serialized, "utf8");
2843
2844
  }
2844
2845
  return canon;
2845
2846
  }
2846
2847
 
2847
2848
  // src/orchestrator/review-archive.ts
2848
- import fs12 from "fs";
2849
+ import fs13 from "fs";
2849
2850
  import path13 from "path";
2850
2851
 
2851
2852
  // src/task/templates.ts
2852
- import fs10 from "fs";
2853
+ import fs11 from "fs";
2853
2854
  import path11 from "path";
2854
2855
  function tasksRoot() {
2855
2856
  return process.env.CANON_TASKS_DIR_OVERRIDE ?? "tasks";
@@ -2862,17 +2863,17 @@ function taskTemplateOverrideRoot() {
2862
2863
  }
2863
2864
  function resolveTaskTemplateSource(basename2) {
2864
2865
  const override = path11.join(taskTemplateOverrideRoot(), basename2);
2865
- if (fs10.existsSync(override)) return override;
2866
+ if (fs11.existsSync(override)) return override;
2866
2867
  const managed = path11.join(templatesRoot(), basename2);
2867
- return fs10.existsSync(managed) ? managed : null;
2868
+ return fs11.existsSync(managed) ? managed : null;
2868
2869
  }
2869
2870
  function renderTaskTemplate(source, taskId, title) {
2870
- return fs10.readFileSync(source, "utf8").replaceAll("[TASK-ID]", taskId).replaceAll("[Title]", title);
2871
+ return fs11.readFileSync(source, "utf8").replaceAll("[TASK-ID]", taskId).replaceAll("[Title]", title);
2871
2872
  }
2872
2873
  function scaffoldTaskArtifact(taskDir, basename2, taskId, title) {
2873
2874
  const source = resolveTaskTemplateSource(basename2);
2874
2875
  if (!source) return null;
2875
- fs10.writeFileSync(path11.join(taskDir, basename2), renderTaskTemplate(source, taskId, title), "utf8");
2876
+ fs11.writeFileSync(path11.join(taskDir, basename2), renderTaskTemplate(source, taskId, title), "utf8");
2876
2877
  return source;
2877
2878
  }
2878
2879
  function isPristineTaskArtifact(content, basename2, taskId, title) {
@@ -2887,7 +2888,7 @@ function isPristineTaskArtifact(content, basename2, taskId, title) {
2887
2888
  }
2888
2889
 
2889
2890
  // src/orchestrator/validation.ts
2890
- import fs11 from "fs";
2891
+ import fs12 from "fs";
2891
2892
  import path12 from "path";
2892
2893
  function computeLatestValidationResults(handoffContent) {
2893
2894
  const latest = /* @__PURE__ */ new Map();
@@ -3019,7 +3020,7 @@ function isTemplateUnfilled(content) {
3019
3020
  function isDoneMdTemplate(donePath) {
3020
3021
  let content;
3021
3022
  try {
3022
- content = fs11.readFileSync(donePath, "utf8");
3023
+ content = fs12.readFileSync(donePath, "utf8");
3023
3024
  } catch {
3024
3025
  return true;
3025
3026
  }
@@ -3058,7 +3059,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
3058
3059
  const artifactPath = path12.join(taskDir, config2.artifactName);
3059
3060
  let content;
3060
3061
  try {
3061
- content = fs11.readFileSync(artifactPath, "utf8");
3062
+ content = fs12.readFileSync(artifactPath, "utf8");
3062
3063
  } catch {
3063
3064
  return { ok: false, reason: `${config2.artifactName} is missing for phase '${phase}'` };
3064
3065
  }
@@ -3070,7 +3071,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
3070
3071
  if (phase === "spec_review" || phase === "plan") {
3071
3072
  let statusRaw;
3072
3073
  try {
3073
- statusRaw = fs11.readFileSync(path12.join(taskDir, "status.json"), "utf8");
3074
+ statusRaw = fs12.readFileSync(path12.join(taskDir, "status.json"), "utf8");
3074
3075
  } catch {
3075
3076
  return { ok: false, reason: `cannot determine reroute state for '${phase}': status.json in ${taskDir} is missing or unreadable` };
3076
3077
  }
@@ -3108,7 +3109,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
3108
3109
  const handoffPath = path12.join(taskDir, "handoff.md");
3109
3110
  let handoffContent;
3110
3111
  try {
3111
- handoffContent = fs11.readFileSync(handoffPath, "utf8");
3112
+ handoffContent = fs12.readFileSync(handoffPath, "utf8");
3112
3113
  } catch {
3113
3114
  return { ok: false, reason: `closing human_review requires a handoff.md \u2014 none found in ${taskDir}` };
3114
3115
  }
@@ -3117,7 +3118,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
3117
3118
  const donePath = path12.join(taskDir, "done.md");
3118
3119
  let doneContent = "";
3119
3120
  try {
3120
- doneContent = fs11.readFileSync(donePath, "utf8");
3121
+ doneContent = fs12.readFileSync(donePath, "utf8");
3121
3122
  } catch {
3122
3123
  }
3123
3124
  if (hasHumanPendingWaiver(doneContent)) return { ok: true };
@@ -3135,7 +3136,7 @@ function parseHandoffChangesRows(taskId) {
3135
3136
  const handoffPath = path12.join(taskDirFor(taskId), "handoff.md");
3136
3137
  let content;
3137
3138
  try {
3138
- content = fs11.readFileSync(handoffPath, "utf8");
3139
+ content = fs12.readFileSync(handoffPath, "utf8");
3139
3140
  } catch {
3140
3141
  return { files: [], malformed: [] };
3141
3142
  }
@@ -3439,7 +3440,7 @@ var REVIEW_ARCHIVE_PREFIX = "review-prior-";
3439
3440
  var REVIEW_ARCHIVE_RE = new RegExp(`^${REVIEW_ARCHIVE_PREFIX}(\\d+)\\.md$`);
3440
3441
  function newestReviewArchiveNumber(taskDir) {
3441
3442
  let newest = 0;
3442
- for (const name of fs12.readdirSync(taskDir)) {
3443
+ for (const name of fs13.readdirSync(taskDir)) {
3443
3444
  const match = REVIEW_ARCHIVE_RE.exec(name);
3444
3445
  if (match) newest = Math.max(newest, Number(match[1]));
3445
3446
  }
@@ -3447,21 +3448,21 @@ function newestReviewArchiveNumber(taskDir) {
3447
3448
  }
3448
3449
  function archivePriorReview(taskDir, options = {}) {
3449
3450
  const reviewPath = path13.join(taskDir, "review.md");
3450
- if (!fs12.existsSync(reviewPath)) return null;
3451
+ if (!fs13.existsSync(reviewPath)) return null;
3451
3452
  if (options.skipUnfilledTemplate) {
3452
- const content = fs12.readFileSync(reviewPath, "utf8");
3453
+ const content = fs13.readFileSync(reviewPath, "utf8");
3453
3454
  if (isTemplateUnfilled(content)) return null;
3454
3455
  if (options.scaffold && isPristineTaskArtifact(content, "review.md", options.scaffold.taskId, options.scaffold.title)) {
3455
3456
  return null;
3456
3457
  }
3457
3458
  }
3458
3459
  const archiveName = `${REVIEW_ARCHIVE_PREFIX}${newestReviewArchiveNumber(taskDir) + 1}.md`;
3459
- fs12.renameSync(reviewPath, path13.join(taskDir, archiveName));
3460
+ fs13.renameSync(reviewPath, path13.join(taskDir, archiveName));
3460
3461
  return archiveName;
3461
3462
  }
3462
3463
  function rescaffoldReview(taskDir, identity) {
3463
3464
  try {
3464
- if (fs12.existsSync(path13.join(taskDir, "review.md"))) return { outcome: "exists" };
3465
+ if (fs13.existsSync(path13.join(taskDir, "review.md"))) return { outcome: "exists" };
3465
3466
  const source = scaffoldTaskArtifact(taskDir, "review.md", identity.taskId, identity.title);
3466
3467
  return source ? { outcome: "written", source } : { outcome: "no-template" };
3467
3468
  } catch (error) {
@@ -3534,7 +3535,7 @@ function taskRootForGate(cwd) {
3534
3535
  }
3535
3536
  function readJsonFile(filePath) {
3536
3537
  try {
3537
- return JSON.parse(fs13.readFileSync(filePath, "utf8"));
3538
+ return JSON.parse(fs14.readFileSync(filePath, "utf8"));
3538
3539
  } catch (error) {
3539
3540
  const message = error instanceof Error ? error.message : String(error);
3540
3541
  throw new Error(`Error: failed to read ${filePath}: ${message}`);
@@ -3542,9 +3543,9 @@ function readJsonFile(filePath) {
3542
3543
  }
3543
3544
  function writeJsonAtomic(filePath, data) {
3544
3545
  const tmpFile = `${filePath}.tmp`;
3545
- fs13.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}
3546
+ fs14.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}
3546
3547
  `, "utf8");
3547
- fs13.renameSync(tmpFile, filePath);
3548
+ fs14.renameSync(tmpFile, filePath);
3548
3549
  }
3549
3550
  function writeStatusAtomic(filePath, status) {
3550
3551
  status.status = deriveTopLevelStatus(status);
@@ -3585,15 +3586,15 @@ function currentBranchOrEmpty() {
3585
3586
  }
3586
3587
  function listTemplateFiles() {
3587
3588
  const root = templatesRoot();
3588
- if (!fs13.existsSync(root)) {
3589
+ if (!fs14.existsSync(root)) {
3589
3590
  throw new Error(`Error: templates directory not found at ${root}`);
3590
3591
  }
3591
- return fs13.readdirSync(root).filter((name) => name.endsWith(".md") || name.endsWith(".json")).sort();
3592
+ return fs14.readdirSync(root).filter((name) => name.endsWith(".md") || name.endsWith(".json")).sort();
3592
3593
  }
3593
3594
  function printCreatedTask(taskDir, baseBranch) {
3594
3595
  console.log(`Created task: ${taskDir}`);
3595
3596
  console.log("Files:");
3596
- for (const file of fs13.readdirSync(taskDir).sort()) {
3597
+ for (const file of fs14.readdirSync(taskDir).sort()) {
3597
3598
  console.log(file);
3598
3599
  }
3599
3600
  console.log("");
@@ -3631,16 +3632,16 @@ function taskNew(args2) {
3631
3632
  throw new Error("Error: title must be single-line (no embedded newlines).");
3632
3633
  }
3633
3634
  const taskDir = taskDirFromRoot(id);
3634
- if (fs13.existsSync(taskDir)) {
3635
+ if (fs14.existsSync(taskDir)) {
3635
3636
  throw new Error(`Error: Task directory ${taskDir} already exists.`);
3636
3637
  }
3637
3638
  if (!baseBranch) {
3638
3639
  baseBranch = currentBranchOrEmpty() || "main";
3639
3640
  }
3640
- fs13.mkdirSync(taskDir, { recursive: true });
3641
+ fs14.mkdirSync(taskDir, { recursive: true });
3641
3642
  for (const basename2 of listTemplateFiles()) {
3642
3643
  const source = resolveTaskTemplateSource(basename2) ?? path14.join(templatesRoot(), basename2);
3643
- fs13.writeFileSync(path14.join(taskDir, basename2), renderTaskTemplate(source, id, title), "utf8");
3644
+ fs14.writeFileSync(path14.join(taskDir, basename2), renderTaskTemplate(source, id, title), "utf8");
3644
3645
  }
3645
3646
  const statusPath = path14.join(taskDir, "status.json");
3646
3647
  const status = readJsonFile(statusPath);
@@ -3663,13 +3664,13 @@ function derivePhase(status) {
3663
3664
  }
3664
3665
  function taskList() {
3665
3666
  const root = tasksRoot();
3666
- if (!fs13.existsSync(root)) {
3667
+ if (!fs14.existsSync(root)) {
3667
3668
  console.log("No tasks found.");
3668
3669
  return;
3669
3670
  }
3670
3671
  const rows = [];
3671
3672
  let invalidCount = 0;
3672
- for (const entry of fs13.readdirSync(root).sort()) {
3673
+ for (const entry of fs14.readdirSync(root).sort()) {
3673
3674
  if (entry === "_archive" || entry === "_templates") continue;
3674
3675
  if (isOrphanedWorktreeState(entry)) {
3675
3676
  invalidCount += 1;
@@ -3687,7 +3688,7 @@ function taskList() {
3687
3688
  continue;
3688
3689
  }
3689
3690
  const statusPath = path14.join(taskDirForCwd(process.cwd(), entry), "status.json");
3690
- if (!fs13.existsSync(statusPath)) continue;
3691
+ if (!fs14.existsSync(statusPath)) continue;
3691
3692
  try {
3692
3693
  const status = readJsonFile(statusPath);
3693
3694
  const phase = derivePhase(status);
@@ -3724,7 +3725,7 @@ function taskStatus(id) {
3724
3725
  validateTaskId(id);
3725
3726
  const cwd = resolveTaskCwd(id);
3726
3727
  const statusPath = taskStatusFileForCwd(cwd, id);
3727
- if (!fs13.existsSync(statusPath)) {
3728
+ if (!fs14.existsSync(statusPath)) {
3728
3729
  throw new Error(`Error: No status.json found for task ${id}`);
3729
3730
  }
3730
3731
  const status = readJsonFile(statusPath);
@@ -3797,7 +3798,7 @@ function taskPhase(id, phaseArg, statusArg, verdictArg) {
3797
3798
  assertValidVerdict(phaseArg, verdictArg);
3798
3799
  const taskCwd = resolveTaskCwd(id);
3799
3800
  const statusPath = taskStatusFileForCwd(taskCwd, id);
3800
- if (!fs13.existsSync(statusPath)) {
3801
+ if (!fs14.existsSync(statusPath)) {
3801
3802
  throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
3802
3803
  }
3803
3804
  const status = readJsonFile(statusPath);
@@ -3863,7 +3864,7 @@ function taskAccept(ids, phaseArg, options = {}) {
3863
3864
  for (const id of ids) {
3864
3865
  const taskCwd = resolveTaskCwd(id);
3865
3866
  const statusPath = taskStatusFileForCwd(taskCwd, id);
3866
- if (!fs13.existsSync(statusPath)) {
3867
+ if (!fs14.existsSync(statusPath)) {
3867
3868
  throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
3868
3869
  }
3869
3870
  const status = readJsonFile(statusPath);
@@ -3965,7 +3966,7 @@ function taskAccept(ids, phaseArg, options = {}) {
3965
3966
  const originalSnapshots2 = /* @__PURE__ */ new Map();
3966
3967
  for (const ctx of ctxByTask.values()) {
3967
3968
  try {
3968
- originalSnapshots2.set(ctx.statusPath, fs13.readFileSync(ctx.statusPath, "utf8"));
3969
+ originalSnapshots2.set(ctx.statusPath, fs14.readFileSync(ctx.statusPath, "utf8"));
3969
3970
  } catch (error) {
3970
3971
  const message = error instanceof Error ? error.message : String(error);
3971
3972
  throw new Error(`Error: failed to read ${ctx.statusPath} for rollback snapshot: ${message}`);
@@ -4002,8 +4003,8 @@ function taskAccept(ids, phaseArg, options = {}) {
4002
4003
  if (original === void 0) continue;
4003
4004
  try {
4004
4005
  const tmpFile = `${filePath}.rollback.tmp`;
4005
- fs13.writeFileSync(tmpFile, original, "utf8");
4006
- fs13.renameSync(tmpFile, filePath);
4006
+ fs14.writeFileSync(tmpFile, original, "utf8");
4007
+ fs14.renameSync(tmpFile, filePath);
4007
4008
  } catch (rollbackErr) {
4008
4009
  const message = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
4009
4010
  rollbackErrors.push(` ${filePath}: ${message}`);
@@ -4027,12 +4028,12 @@ ${rollbackErrors.join("\n")}`
4027
4028
  const bundleNote = ids.length > 1 ? ` Bundle: ${ids.join(", ")}.` : "";
4028
4029
  const noteLine = `[${today()}] Operator accepted ${phaseArg} via \`canon task accept\` \u2014 ${sanctioned ? "sanctioned (agent verdict overridden)" : "unblocked (advancing verdict preserved)"}. Reason: ${reason}.${bundleNote}`;
4029
4030
  try {
4030
- if (fs13.existsSync(notesPath)) {
4031
- fs13.appendFileSync(notesPath, `
4031
+ if (fs14.existsSync(notesPath)) {
4032
+ fs14.appendFileSync(notesPath, `
4032
4033
  ${noteLine}
4033
4034
  `, "utf8");
4034
4035
  } else {
4035
- fs13.writeFileSync(notesPath, `${noteLine}
4036
+ fs14.writeFileSync(notesPath, `${noteLine}
4036
4037
  `, "utf8");
4037
4038
  }
4038
4039
  } catch (error) {
@@ -4155,7 +4156,7 @@ ${noteLine}
4155
4156
  const originalSnapshots = /* @__PURE__ */ new Map();
4156
4157
  for (const ctx of ctxByTask.values()) {
4157
4158
  try {
4158
- originalSnapshots.set(ctx.statusPath, fs13.readFileSync(ctx.statusPath, "utf8"));
4159
+ originalSnapshots.set(ctx.statusPath, fs14.readFileSync(ctx.statusPath, "utf8"));
4159
4160
  } catch (error) {
4160
4161
  const message = error instanceof Error ? error.message : String(error);
4161
4162
  throw new Error(`Error: failed to read ${ctx.statusPath} for rollback snapshot: ${message}`);
@@ -4180,8 +4181,8 @@ ${noteLine}
4180
4181
  if (original === void 0) continue;
4181
4182
  try {
4182
4183
  const tmpFile = `${filePath}.rollback.tmp`;
4183
- fs13.writeFileSync(tmpFile, original, "utf8");
4184
- fs13.renameSync(tmpFile, filePath);
4184
+ fs14.writeFileSync(tmpFile, original, "utf8");
4185
+ fs14.renameSync(tmpFile, filePath);
4185
4186
  } catch (rollbackErr) {
4186
4187
  const message = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
4187
4188
  rollbackErrors.push(` ${filePath}: ${message}`);
@@ -4203,12 +4204,12 @@ ${noteLine}
4203
4204
  const notesPath = path14.join(taskDirForCwd(ctx.taskCwd, ctx.id), "notes.md");
4204
4205
  const noteLine = `[${today()}] Operator accepted implement phase via \`canon task accept\` \u2014 auto-commit will be skipped.${options.force ? " (--force)" : ""}`;
4205
4206
  try {
4206
- if (fs13.existsSync(notesPath)) {
4207
- fs13.appendFileSync(notesPath, `
4207
+ if (fs14.existsSync(notesPath)) {
4208
+ fs14.appendFileSync(notesPath, `
4208
4209
  ${noteLine}
4209
4210
  `, "utf8");
4210
4211
  } else {
4211
- fs13.writeFileSync(notesPath, `${noteLine}
4212
+ fs14.writeFileSync(notesPath, `${noteLine}
4212
4213
  `, "utf8");
4213
4214
  }
4214
4215
  } catch (error) {
@@ -4228,14 +4229,14 @@ function taskResetSpecReview(id) {
4228
4229
  const taskCwd = resolveTaskCwd(id);
4229
4230
  const taskDir = taskDirForCwd(taskCwd, id);
4230
4231
  const statusPath = path14.join(taskDir, "status.json");
4231
- if (!fs13.existsSync(statusPath)) {
4232
+ if (!fs14.existsSync(statusPath)) {
4232
4233
  throw new Error(`Error: no status.json at ${statusPath}`);
4233
4234
  }
4234
4235
  const reviewPath = path14.join(taskDir, "spec-review.md");
4235
- if (fs13.existsSync(reviewPath)) {
4236
+ if (fs14.existsSync(reviewPath)) {
4236
4237
  let n = 1;
4237
- while (fs13.existsSync(path14.join(taskDir, `spec-review-prior-${n}.md`))) n += 1;
4238
- fs13.renameSync(reviewPath, path14.join(taskDir, `spec-review-prior-${n}.md`));
4238
+ while (fs14.existsSync(path14.join(taskDir, `spec-review-prior-${n}.md`))) n += 1;
4239
+ fs14.renameSync(reviewPath, path14.join(taskDir, `spec-review-prior-${n}.md`));
4239
4240
  console.log(`Archived prior spec-review.md \u2192 spec-review-prior-${n}.md`);
4240
4241
  }
4241
4242
  const status = readJsonFile(statusPath);
@@ -4259,7 +4260,7 @@ function taskResetCodeReview(id) {
4259
4260
  const taskCwd = resolveTaskCwd(id);
4260
4261
  const taskDir = taskDirForCwd(taskCwd, id);
4261
4262
  const statusPath = path14.join(taskDir, "status.json");
4262
- if (!fs13.existsSync(statusPath)) {
4263
+ if (!fs14.existsSync(statusPath)) {
4263
4264
  throw new Error(`Error: no status.json at ${statusPath}`);
4264
4265
  }
4265
4266
  const status = readJsonFile(statusPath);
@@ -4345,12 +4346,12 @@ function resolveMainCheckoutRoot() {
4345
4346
  }
4346
4347
  function safeRealpath(target) {
4347
4348
  try {
4348
- return fs13.realpathSync(target);
4349
+ return fs14.realpathSync(target);
4349
4350
  } catch {
4350
4351
  const parent = path14.dirname(target);
4351
4352
  if (parent === target) return target;
4352
4353
  try {
4353
- return path14.join(fs13.realpathSync(parent), path14.basename(target));
4354
+ return path14.join(fs14.realpathSync(parent), path14.basename(target));
4354
4355
  } catch {
4355
4356
  return target;
4356
4357
  }
@@ -4458,12 +4459,12 @@ function taskPostMergeSync(branchArg) {
4458
4459
  }
4459
4460
  function nudgeShippableTasks() {
4460
4461
  const root = tasksRoot();
4461
- if (!fs13.existsSync(root)) return;
4462
+ if (!fs14.existsSync(root)) return;
4462
4463
  const shippable = [];
4463
- for (const entry of fs13.readdirSync(root).sort()) {
4464
+ for (const entry of fs14.readdirSync(root).sort()) {
4464
4465
  if (entry === "_archive" || entry.startsWith("_")) continue;
4465
4466
  const statusPath = path14.join(root, entry, "status.json");
4466
- if (!fs13.existsSync(statusPath)) continue;
4467
+ if (!fs14.existsSync(statusPath)) continue;
4467
4468
  let status;
4468
4469
  try {
4469
4470
  status = readJsonFile(statusPath);
@@ -4538,7 +4539,7 @@ function taskSet(args2) {
4538
4539
  validateTaskId(id);
4539
4540
  const taskCwd = resolveTaskCwd(id);
4540
4541
  const statusPath = taskStatusFileForCwd(taskCwd, id);
4541
- if (!fs13.existsSync(statusPath)) {
4542
+ if (!fs14.existsSync(statusPath)) {
4542
4543
  throw new Error(`Error: No status.json found for task ${id}`);
4543
4544
  }
4544
4545
  const status = readJsonFile(statusPath);
@@ -4879,7 +4880,7 @@ function currentPinFromManifest(manifest) {
4879
4880
  return "unknown";
4880
4881
  }
4881
4882
  function bakedVersion() {
4882
- return "3.3.1";
4883
+ return "3.4.0";
4883
4884
  }
4884
4885
  function ownPackageName(pkgDir) {
4885
4886
  try {
@@ -5355,7 +5356,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
5355
5356
  cutoverWarnings.push(docsRefsCheckRel);
5356
5357
  }
5357
5358
  const versionPath = join5(cwd, ".canon", "version");
5358
- const newVersion = "3.3.1";
5359
+ const newVersion = "3.4.0";
5359
5360
  const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
5360
5361
  if (currentVersion !== newVersion) {
5361
5362
  pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
@@ -5663,7 +5664,7 @@ Global:
5663
5664
  `);
5664
5665
  }
5665
5666
  function printVersion() {
5666
- console.log("3.3.1");
5667
+ console.log("3.4.0");
5667
5668
  }
5668
5669
  switch (command) {
5669
5670
  case "doctor":