canon-ai 3.3.0 → 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 +30 -0
- package/dist/cli/index.js +70 -68
- package/dist/orchestrator/run-task.js +460 -177
- package/package.json +1 -1
- package/templates/.canon/templates/handoff.md +1 -1
- package/templates/.canon/templates/review.md +4 -0
- package/templates/.claude/agents/code-review-anchored.md +2 -0
- package/templates/.claude/agents/code-review-cold.md +4 -2
- package/templates/docs/pipeline-orchestrator.md +9 -21
- package/templates/scripts/docs-refs-check.mjs +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@
|
|
|
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
|
+
|
|
31
|
+
## [3.3.1] — 2026-09-24
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`canon update` works on npm 12.** npm 12 prints `npm view <pkg>@<version> version --json` as a one-element array instead of a bare string, so the pre-install registry check rejected every published version with "could not verify canon-ai@<version> on the npm registry (no output)". The check now accepts both output shapes; an unpublished version still gets the "not yet on the npm registry" message. Because the broken check is in the installed version, reach 3.3.1 by installing it with npm directly once (`npm install -g canon-ai@3.3.1` for a global install).
|
|
36
|
+
|
|
7
37
|
## [3.3.0] — 2026-09-24
|
|
8
38
|
|
|
9
39
|
### Added
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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 =
|
|
2841
|
+
const current = fs10.readFileSync(statusFilePath, "utf8");
|
|
2841
2842
|
if (current !== serialized) {
|
|
2842
|
-
|
|
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
|
|
2849
|
+
import fs13 from "fs";
|
|
2849
2850
|
import path13 from "path";
|
|
2850
2851
|
|
|
2851
2852
|
// src/task/templates.ts
|
|
2852
|
-
import
|
|
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 (
|
|
2866
|
+
if (fs11.existsSync(override)) return override;
|
|
2866
2867
|
const managed = path11.join(templatesRoot(), basename2);
|
|
2867
|
-
return
|
|
2868
|
+
return fs11.existsSync(managed) ? managed : null;
|
|
2868
2869
|
}
|
|
2869
2870
|
function renderTaskTemplate(source, taskId, title) {
|
|
2870
|
-
return
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
3451
|
+
if (!fs13.existsSync(reviewPath)) return null;
|
|
3451
3452
|
if (options.skipUnfilledTemplate) {
|
|
3452
|
-
const content =
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
3546
|
+
fs14.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}
|
|
3546
3547
|
`, "utf8");
|
|
3547
|
-
|
|
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 (!
|
|
3589
|
+
if (!fs14.existsSync(root)) {
|
|
3589
3590
|
throw new Error(`Error: templates directory not found at ${root}`);
|
|
3590
3591
|
}
|
|
3591
|
-
return
|
|
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
|
|
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 (
|
|
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
|
-
|
|
3641
|
+
fs14.mkdirSync(taskDir, { recursive: true });
|
|
3641
3642
|
for (const basename2 of listTemplateFiles()) {
|
|
3642
3643
|
const source = resolveTaskTemplateSource(basename2) ?? path14.join(templatesRoot(), basename2);
|
|
3643
|
-
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
4006
|
-
|
|
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 (
|
|
4031
|
-
|
|
4031
|
+
if (fs14.existsSync(notesPath)) {
|
|
4032
|
+
fs14.appendFileSync(notesPath, `
|
|
4032
4033
|
${noteLine}
|
|
4033
4034
|
`, "utf8");
|
|
4034
4035
|
} else {
|
|
4035
|
-
|
|
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,
|
|
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
|
-
|
|
4184
|
-
|
|
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 (
|
|
4207
|
-
|
|
4207
|
+
if (fs14.existsSync(notesPath)) {
|
|
4208
|
+
fs14.appendFileSync(notesPath, `
|
|
4208
4209
|
${noteLine}
|
|
4209
4210
|
`, "utf8");
|
|
4210
4211
|
} else {
|
|
4211
|
-
|
|
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 (!
|
|
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 (
|
|
4236
|
+
if (fs14.existsSync(reviewPath)) {
|
|
4236
4237
|
let n = 1;
|
|
4237
|
-
while (
|
|
4238
|
-
|
|
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 (!
|
|
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
|
|
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(
|
|
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 (!
|
|
4462
|
+
if (!fs14.existsSync(root)) return;
|
|
4462
4463
|
const shippable = [];
|
|
4463
|
-
for (const entry of
|
|
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 (!
|
|
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 (!
|
|
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.
|
|
4883
|
+
return "3.4.0";
|
|
4883
4884
|
}
|
|
4884
4885
|
function ownPackageName(pkgDir) {
|
|
4885
4886
|
try {
|
|
@@ -4904,7 +4905,8 @@ function checkRegistryVersion(pkgName, version, runner, cwd, global = false) {
|
|
|
4904
4905
|
} catch {
|
|
4905
4906
|
parsed = void 0;
|
|
4906
4907
|
}
|
|
4907
|
-
|
|
4908
|
+
const published = parsed === version || Array.isArray(parsed) && parsed.includes(version);
|
|
4909
|
+
if (result.status === 0 && published) return { ok: true };
|
|
4908
4910
|
if (typeof parsed === "object" && parsed !== null && parsed.error?.code === "E404") {
|
|
4909
4911
|
return {
|
|
4910
4912
|
ok: false,
|
|
@@ -5354,7 +5356,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5354
5356
|
cutoverWarnings.push(docsRefsCheckRel);
|
|
5355
5357
|
}
|
|
5356
5358
|
const versionPath = join5(cwd, ".canon", "version");
|
|
5357
|
-
const newVersion = "3.
|
|
5359
|
+
const newVersion = "3.4.0";
|
|
5358
5360
|
const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
|
|
5359
5361
|
if (currentVersion !== newVersion) {
|
|
5360
5362
|
pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
|
|
@@ -5662,7 +5664,7 @@ Global:
|
|
|
5662
5664
|
`);
|
|
5663
5665
|
}
|
|
5664
5666
|
function printVersion() {
|
|
5665
|
-
console.log("3.
|
|
5667
|
+
console.log("3.4.0");
|
|
5666
5668
|
}
|
|
5667
5669
|
switch (command) {
|
|
5668
5670
|
case "doctor":
|