canon-ai 3.1.0 → 3.1.2
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 +17 -0
- package/README.md +2 -2
- package/dist/cli/index.js +223 -125
- package/dist/orchestrator/run-task.js +251 -160
- package/package.json +1 -1
- package/templates/.claude/skills/canon-inline-review/SKILL.md +9 -9
- package/templates/docs/pipeline-orchestrator.md +3 -3
- package/templates/scripts/docs-refs-check.mjs +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -1207,8 +1207,8 @@ ${RECOMMENDED_NUDGE}`
|
|
|
1207
1207
|
};
|
|
1208
1208
|
}
|
|
1209
1209
|
const mentionsCanon = existingFiles.some((filename) => {
|
|
1210
|
-
const
|
|
1211
|
-
return /canon/i.test(readFileSync(
|
|
1210
|
+
const path15 = join(cwd, filename);
|
|
1211
|
+
return /canon/i.test(readFileSync(path15, "utf8"));
|
|
1212
1212
|
});
|
|
1213
1213
|
if (mentionsCanon) {
|
|
1214
1214
|
return { label: "canon discovery nudge", status: "pass" };
|
|
@@ -1245,7 +1245,7 @@ function checkTemplates(cwd) {
|
|
|
1245
1245
|
}
|
|
1246
1246
|
function checkCanonVersion(cwd) {
|
|
1247
1247
|
const versionPath = join(cwd, ".canon", "version");
|
|
1248
|
-
const installedVersion = "3.1.
|
|
1248
|
+
const installedVersion = "3.1.2";
|
|
1249
1249
|
if (!existsSync(versionPath)) {
|
|
1250
1250
|
return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
|
|
1251
1251
|
}
|
|
@@ -1419,10 +1419,10 @@ function checkCodexProjectTrust(cwd) {
|
|
|
1419
1419
|
trust_level = "trusted"`
|
|
1420
1420
|
};
|
|
1421
1421
|
}
|
|
1422
|
-
function readAllowFromSettings(
|
|
1423
|
-
if (!existsSync(
|
|
1422
|
+
function readAllowFromSettings(path15) {
|
|
1423
|
+
if (!existsSync(path15)) return { allow: /* @__PURE__ */ new Set(), status: "missing" };
|
|
1424
1424
|
try {
|
|
1425
|
-
const parsed = JSON.parse(readFileSync(
|
|
1425
|
+
const parsed = JSON.parse(readFileSync(path15, "utf8"));
|
|
1426
1426
|
const raw = parsed?.permissions?.allow;
|
|
1427
1427
|
const allow = new Set(
|
|
1428
1428
|
Array.isArray(raw) ? raw.filter((x) => typeof x === "string") : []
|
|
@@ -1639,6 +1639,31 @@ import {
|
|
|
1639
1639
|
} from "fs";
|
|
1640
1640
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1641
1641
|
import { dirname as dirname2, join as join2, relative as relative2 } from "path";
|
|
1642
|
+
|
|
1643
|
+
// src/lib/scaffold-paths.ts
|
|
1644
|
+
import { lstatSync } from "fs";
|
|
1645
|
+
import path7 from "path";
|
|
1646
|
+
function assertNoSymlinkDestinations(root, relativePaths) {
|
|
1647
|
+
for (const relativePath of relativePaths) {
|
|
1648
|
+
let current = root;
|
|
1649
|
+
for (const segment of relativePath.split(/[\\/]/)) {
|
|
1650
|
+
current = path7.join(current, segment);
|
|
1651
|
+
let entry;
|
|
1652
|
+
try {
|
|
1653
|
+
entry = lstatSync(current);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
const code = error.code;
|
|
1656
|
+
if (code === "ENOENT" || code === "ENOTDIR") break;
|
|
1657
|
+
throw error;
|
|
1658
|
+
}
|
|
1659
|
+
if (entry.isSymbolicLink()) {
|
|
1660
|
+
throw new Error(`Refusing to write ${relativePath}: ${current} is a symlink. Replace it with a regular file or directory before running canon init or upgrade; --force does not bypass this check.`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/cli/commands/init.ts
|
|
1642
1667
|
var packageDir = join2(dirname2(fileURLToPath2(import.meta.url)), "../..");
|
|
1643
1668
|
var templatesDir = join2(packageDir, "templates");
|
|
1644
1669
|
var AGENT_FILES = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
@@ -1665,6 +1690,7 @@ function walkDir(dir, base = dir) {
|
|
|
1665
1690
|
}
|
|
1666
1691
|
function scaffoldTemplates(cwd, srcTemplatesDir) {
|
|
1667
1692
|
const templateFiles = walkDir(srcTemplatesDir);
|
|
1693
|
+
assertNoSymlinkDestinations(cwd, templateFiles);
|
|
1668
1694
|
const scaffolded = [];
|
|
1669
1695
|
const skipped = [];
|
|
1670
1696
|
for (const rel of templateFiles) {
|
|
@@ -1682,6 +1708,7 @@ function scaffoldTemplates(cwd, srcTemplatesDir) {
|
|
|
1682
1708
|
function initCmd(_args) {
|
|
1683
1709
|
checkDeps();
|
|
1684
1710
|
const cwd = process.cwd();
|
|
1711
|
+
assertNoSymlinkDestinations(cwd, [".gitignore", ".canon/version"]);
|
|
1685
1712
|
const { scaffolded, skipped } = scaffoldTemplates(cwd, templatesDir);
|
|
1686
1713
|
const gitignorePath = join2(cwd, ".gitignore");
|
|
1687
1714
|
const existingGitignore = existsSync2(gitignorePath) ? readFileSync2(gitignorePath, "utf8") : "";
|
|
@@ -1715,7 +1742,7 @@ function initCmd(_args) {
|
|
|
1715
1742
|
}
|
|
1716
1743
|
function writeCanonVersion(cwd) {
|
|
1717
1744
|
const versionPath = join2(cwd, ".canon", "version");
|
|
1718
|
-
const version = "3.1.
|
|
1745
|
+
const version = "3.1.2";
|
|
1719
1746
|
mkdirSync(dirname2(versionPath), { recursive: true });
|
|
1720
1747
|
writeFileSync(versionPath, version + "\n");
|
|
1721
1748
|
}
|
|
@@ -1755,6 +1782,7 @@ function runCmd(args2) {
|
|
|
1755
1782
|
|
|
1756
1783
|
// src/cli/commands/watch.ts
|
|
1757
1784
|
import fs7 from "fs";
|
|
1785
|
+
import { StringDecoder } from "string_decoder";
|
|
1758
1786
|
|
|
1759
1787
|
// src/cli/commands/stop.ts
|
|
1760
1788
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -2362,25 +2390,39 @@ function primaryLogTaskId(ctx, fallbackTaskId) {
|
|
|
2362
2390
|
function tailRunLog(ctx, taskId, deps, tailState) {
|
|
2363
2391
|
const logTaskDir = tolerantTaskDir(primaryLogTaskId(ctx, taskId));
|
|
2364
2392
|
const logPath = runLogPathFor(logTaskDir);
|
|
2393
|
+
let fd;
|
|
2365
2394
|
try {
|
|
2366
|
-
|
|
2367
|
-
|
|
2395
|
+
fd = fs7.openSync(logPath, "r");
|
|
2396
|
+
const stat = fs7.fstatSync(fd);
|
|
2397
|
+
const identity = `${logPath}:${stat.dev}:${stat.ino}`;
|
|
2398
|
+
if (tailState.position == null || tailState.path !== logPath) {
|
|
2368
2399
|
tailState.position = stat.size;
|
|
2400
|
+
tailState.path = logPath;
|
|
2401
|
+
tailState.identity = identity;
|
|
2402
|
+
tailState.decoder = new StringDecoder("utf8");
|
|
2369
2403
|
return;
|
|
2370
2404
|
}
|
|
2371
|
-
if (stat.size < tailState.position) {
|
|
2405
|
+
if (identity !== tailState.identity || stat.size < tailState.position) {
|
|
2372
2406
|
tailState.position = 0;
|
|
2407
|
+
tailState.decoder = new StringDecoder("utf8");
|
|
2373
2408
|
}
|
|
2409
|
+
tailState.identity = identity;
|
|
2374
2410
|
if (stat.size === tailState.position) return;
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2411
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, stat.size - tailState.position));
|
|
2412
|
+
while (tailState.position < stat.size) {
|
|
2413
|
+
const bytes = fs7.readSync(fd, buffer, 0, Math.min(buffer.length, stat.size - tailState.position), tailState.position);
|
|
2414
|
+
if (bytes === 0) break;
|
|
2415
|
+
tailState.position += bytes;
|
|
2416
|
+
const chunk = tailState.decoder.write(buffer.subarray(0, bytes));
|
|
2417
|
+
if (chunk.length > 0) deps.stderr?.(chunk);
|
|
2418
|
+
}
|
|
2379
2419
|
} catch (error) {
|
|
2380
2420
|
const code = getErrnoCode2(error);
|
|
2381
2421
|
if (code === "ENOENT") return;
|
|
2382
2422
|
deps.stderr?.(`canon watch: failed to tail ${logPath}: ${errorMessage2(error)}
|
|
2383
2423
|
`);
|
|
2424
|
+
} finally {
|
|
2425
|
+
if (fd !== void 0) fs7.closeSync(fd);
|
|
2384
2426
|
}
|
|
2385
2427
|
}
|
|
2386
2428
|
function gatherContext(taskId, deps) {
|
|
@@ -2441,7 +2483,7 @@ function watchCmd(args2, deps = {}) {
|
|
|
2441
2483
|
}
|
|
2442
2484
|
const taskId = parsed.taskId;
|
|
2443
2485
|
const timeoutDeadline = parsed.timeoutMs == null ? null : now() + parsed.timeoutMs;
|
|
2444
|
-
const tailState = { position: null };
|
|
2486
|
+
const tailState = { position: null, decoder: new StringDecoder("utf8") };
|
|
2445
2487
|
const withinTimeout = () => timeoutDeadline != null && now() >= timeoutDeadline;
|
|
2446
2488
|
const remainingTimeoutMs = () => {
|
|
2447
2489
|
if (timeoutDeadline == null) return null;
|
|
@@ -2652,21 +2694,21 @@ function watchCmd(args2, deps = {}) {
|
|
|
2652
2694
|
|
|
2653
2695
|
// src/task/index.ts
|
|
2654
2696
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
2655
|
-
import
|
|
2656
|
-
import
|
|
2697
|
+
import fs13 from "fs";
|
|
2698
|
+
import path14 from "path";
|
|
2657
2699
|
|
|
2658
2700
|
// src/orchestrator/canon-snapshot.ts
|
|
2659
2701
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
2660
2702
|
import fs9 from "fs";
|
|
2661
|
-
import
|
|
2703
|
+
import path10 from "path";
|
|
2662
2704
|
|
|
2663
2705
|
// src/orchestrator/git.ts
|
|
2664
2706
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2665
|
-
import
|
|
2707
|
+
import path9 from "path";
|
|
2666
2708
|
|
|
2667
2709
|
// src/orchestrator/worktree.ts
|
|
2668
2710
|
import fs8 from "fs";
|
|
2669
|
-
import
|
|
2711
|
+
import path8 from "path";
|
|
2670
2712
|
var PIPELINE_TELEMETRY_FILES = [
|
|
2671
2713
|
"docs/pipeline-invocations.md",
|
|
2672
2714
|
"docs/task-quality-log.md",
|
|
@@ -2709,18 +2751,18 @@ function isInstalledSourcePath(sourcePath) {
|
|
|
2709
2751
|
return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
|
|
2710
2752
|
}
|
|
2711
2753
|
function resolveCanonVersion(explicit) {
|
|
2712
|
-
return explicit ?? "3.1.
|
|
2754
|
+
return explicit ?? "3.1.2" ?? "dev";
|
|
2713
2755
|
}
|
|
2714
2756
|
function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
|
|
2715
2757
|
const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
2716
2758
|
if (!ownToplevel) return upstreamCommit;
|
|
2717
|
-
const parentDir =
|
|
2759
|
+
const parentDir = path10.dirname(repoRoot);
|
|
2718
2760
|
const parentToplevel = captureGitOutput(parentDir, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
2719
2761
|
if (!parentToplevel) return upstreamCommit;
|
|
2720
|
-
if (
|
|
2762
|
+
if (path10.resolve(parentToplevel) === path10.resolve(ownToplevel)) {
|
|
2721
2763
|
return upstreamCommit;
|
|
2722
2764
|
}
|
|
2723
|
-
return captureGitOutput(
|
|
2765
|
+
return captureGitOutput(path10.resolve(parentToplevel), ["rev-parse", "HEAD"], runGitAt) || upstreamCommit;
|
|
2724
2766
|
}
|
|
2725
2767
|
function defaultRunCommand(command2, args2) {
|
|
2726
2768
|
const result = spawnSync6(command2, args2, {
|
|
@@ -2754,7 +2796,7 @@ function captureCanonSnapshot(repoRoot = REPO_ROOT, options = {}) {
|
|
|
2754
2796
|
const isInstalled = isInstalledSourcePath(canonSourcePath);
|
|
2755
2797
|
const superprojectWorkingTree = captureGitOutput(repoRoot, ["rev-parse", "--show-superproject-working-tree"], runGitAt);
|
|
2756
2798
|
const drivingCommit = captureGitOutput(repoRoot, ["rev-parse", "HEAD"], runGitAt) || "<unavailable>";
|
|
2757
|
-
const hostCommit = superprojectWorkingTree ? captureGitOutput(
|
|
2799
|
+
const hostCommit = superprojectWorkingTree ? captureGitOutput(path10.resolve(superprojectWorkingTree), ["rev-parse", "HEAD"], runGitAt) || "<unavailable>" : null;
|
|
2758
2800
|
let upstreamCommit;
|
|
2759
2801
|
let orchestratorCommit;
|
|
2760
2802
|
if (isInstalled) {
|
|
@@ -2801,12 +2843,50 @@ function refreshCanonSnapshotAtPath(statusFilePath, options = {}) {
|
|
|
2801
2843
|
}
|
|
2802
2844
|
|
|
2803
2845
|
// src/orchestrator/review-archive.ts
|
|
2804
|
-
import
|
|
2846
|
+
import fs12 from "fs";
|
|
2847
|
+
import path13 from "path";
|
|
2848
|
+
|
|
2849
|
+
// src/task/templates.ts
|
|
2850
|
+
import fs10 from "fs";
|
|
2805
2851
|
import path11 from "path";
|
|
2852
|
+
function tasksRoot() {
|
|
2853
|
+
return process.env.CANON_TASKS_DIR_OVERRIDE ?? "tasks";
|
|
2854
|
+
}
|
|
2855
|
+
function templatesRoot() {
|
|
2856
|
+
return path11.join(process.cwd(), ".canon", "templates");
|
|
2857
|
+
}
|
|
2858
|
+
function taskTemplateOverrideRoot() {
|
|
2859
|
+
return path11.join(tasksRoot(), "_templates");
|
|
2860
|
+
}
|
|
2861
|
+
function resolveTaskTemplateSource(basename2) {
|
|
2862
|
+
const override = path11.join(taskTemplateOverrideRoot(), basename2);
|
|
2863
|
+
if (fs10.existsSync(override)) return override;
|
|
2864
|
+
const managed = path11.join(templatesRoot(), basename2);
|
|
2865
|
+
return fs10.existsSync(managed) ? managed : null;
|
|
2866
|
+
}
|
|
2867
|
+
function renderTaskTemplate(source, taskId, title) {
|
|
2868
|
+
return fs10.readFileSync(source, "utf8").replaceAll("[TASK-ID]", taskId).replaceAll("[Title]", title);
|
|
2869
|
+
}
|
|
2870
|
+
function scaffoldTaskArtifact(taskDir, basename2, taskId, title) {
|
|
2871
|
+
const source = resolveTaskTemplateSource(basename2);
|
|
2872
|
+
if (!source) return null;
|
|
2873
|
+
fs10.writeFileSync(path11.join(taskDir, basename2), renderTaskTemplate(source, taskId, title), "utf8");
|
|
2874
|
+
return source;
|
|
2875
|
+
}
|
|
2876
|
+
function isPristineTaskArtifact(content, basename2, taskId, title) {
|
|
2877
|
+
if (content.includes("[TASK-ID]")) return true;
|
|
2878
|
+
const source = resolveTaskTemplateSource(basename2);
|
|
2879
|
+
if (!source) return false;
|
|
2880
|
+
try {
|
|
2881
|
+
return content === renderTaskTemplate(source, taskId, title);
|
|
2882
|
+
} catch {
|
|
2883
|
+
return false;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2806
2886
|
|
|
2807
2887
|
// src/orchestrator/validation.ts
|
|
2808
|
-
import
|
|
2809
|
-
import
|
|
2888
|
+
import fs11 from "fs";
|
|
2889
|
+
import path12 from "path";
|
|
2810
2890
|
function computeLatestValidationResults(handoffContent) {
|
|
2811
2891
|
const latest = /* @__PURE__ */ new Map();
|
|
2812
2892
|
const baseline = parseTable(handoffContent, "Validation Outcomes");
|
|
@@ -2937,7 +3017,7 @@ function isTemplateUnfilled(content) {
|
|
|
2937
3017
|
function isDoneMdTemplate(donePath) {
|
|
2938
3018
|
let content;
|
|
2939
3019
|
try {
|
|
2940
|
-
content =
|
|
3020
|
+
content = fs11.readFileSync(donePath, "utf8");
|
|
2941
3021
|
} catch {
|
|
2942
3022
|
return true;
|
|
2943
3023
|
}
|
|
@@ -2967,16 +3047,16 @@ var PHASE_GATE_CONFIG = {
|
|
|
2967
3047
|
human_review: {}
|
|
2968
3048
|
};
|
|
2969
3049
|
function resolveTaskDirForValidation(taskId, taskDirOverride) {
|
|
2970
|
-
return taskDirOverride ?
|
|
3050
|
+
return taskDirOverride ? path12.join(taskDirOverride, taskId) : taskDirFor(taskId);
|
|
2971
3051
|
}
|
|
2972
3052
|
function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
2973
3053
|
const config2 = PHASE_GATE_CONFIG[phase];
|
|
2974
3054
|
const taskDir = resolveTaskDirForValidation(taskId, taskDirOverride);
|
|
2975
3055
|
if (config2.artifactName) {
|
|
2976
|
-
const artifactPath =
|
|
3056
|
+
const artifactPath = path12.join(taskDir, config2.artifactName);
|
|
2977
3057
|
let content;
|
|
2978
3058
|
try {
|
|
2979
|
-
content =
|
|
3059
|
+
content = fs11.readFileSync(artifactPath, "utf8");
|
|
2980
3060
|
} catch {
|
|
2981
3061
|
return { ok: false, reason: `${config2.artifactName} is missing for phase '${phase}'` };
|
|
2982
3062
|
}
|
|
@@ -2988,7 +3068,7 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
2988
3068
|
if (phase === "spec_review" || phase === "plan") {
|
|
2989
3069
|
let statusRaw;
|
|
2990
3070
|
try {
|
|
2991
|
-
statusRaw =
|
|
3071
|
+
statusRaw = fs11.readFileSync(path12.join(taskDir, "status.json"), "utf8");
|
|
2992
3072
|
} catch {
|
|
2993
3073
|
return { ok: false, reason: `cannot determine reroute state for '${phase}': status.json in ${taskDir} is missing or unreadable` };
|
|
2994
3074
|
}
|
|
@@ -3023,19 +3103,19 @@ function checkPhaseGate(taskId, phase, verdict, taskDirOverride) {
|
|
|
3023
3103
|
}
|
|
3024
3104
|
}
|
|
3025
3105
|
if (phase === "human_review") {
|
|
3026
|
-
const handoffPath =
|
|
3106
|
+
const handoffPath = path12.join(taskDir, "handoff.md");
|
|
3027
3107
|
let handoffContent;
|
|
3028
3108
|
try {
|
|
3029
|
-
handoffContent =
|
|
3109
|
+
handoffContent = fs11.readFileSync(handoffPath, "utf8");
|
|
3030
3110
|
} catch {
|
|
3031
3111
|
return { ok: false, reason: `closing human_review requires a handoff.md \u2014 none found in ${taskDir}` };
|
|
3032
3112
|
}
|
|
3033
3113
|
const pending = countHumanPendingChecks(handoffContent);
|
|
3034
3114
|
if (pending.length === 0) return { ok: true };
|
|
3035
|
-
const donePath =
|
|
3115
|
+
const donePath = path12.join(taskDir, "done.md");
|
|
3036
3116
|
let doneContent = "";
|
|
3037
3117
|
try {
|
|
3038
|
-
doneContent =
|
|
3118
|
+
doneContent = fs11.readFileSync(donePath, "utf8");
|
|
3039
3119
|
} catch {
|
|
3040
3120
|
}
|
|
3041
3121
|
if (hasHumanPendingWaiver(doneContent)) return { ok: true };
|
|
@@ -3050,10 +3130,10 @@ ${list}
|
|
|
3050
3130
|
return { ok: true };
|
|
3051
3131
|
}
|
|
3052
3132
|
function parseHandoffChangesRows(taskId) {
|
|
3053
|
-
const handoffPath =
|
|
3133
|
+
const handoffPath = path12.join(taskDirFor(taskId), "handoff.md");
|
|
3054
3134
|
let content;
|
|
3055
3135
|
try {
|
|
3056
|
-
content =
|
|
3136
|
+
content = fs11.readFileSync(handoffPath, "utf8");
|
|
3057
3137
|
} catch {
|
|
3058
3138
|
return { files: [], malformed: [] };
|
|
3059
3139
|
}
|
|
@@ -3357,23 +3437,35 @@ var REVIEW_ARCHIVE_PREFIX = "review-prior-";
|
|
|
3357
3437
|
var REVIEW_ARCHIVE_RE = new RegExp(`^${REVIEW_ARCHIVE_PREFIX}(\\d+)\\.md$`);
|
|
3358
3438
|
function newestReviewArchiveNumber(taskDir) {
|
|
3359
3439
|
let newest = 0;
|
|
3360
|
-
for (const name of
|
|
3440
|
+
for (const name of fs12.readdirSync(taskDir)) {
|
|
3361
3441
|
const match = REVIEW_ARCHIVE_RE.exec(name);
|
|
3362
3442
|
if (match) newest = Math.max(newest, Number(match[1]));
|
|
3363
3443
|
}
|
|
3364
3444
|
return newest;
|
|
3365
3445
|
}
|
|
3366
3446
|
function archivePriorReview(taskDir, options = {}) {
|
|
3367
|
-
const reviewPath =
|
|
3368
|
-
if (!
|
|
3447
|
+
const reviewPath = path13.join(taskDir, "review.md");
|
|
3448
|
+
if (!fs12.existsSync(reviewPath)) return null;
|
|
3369
3449
|
if (options.skipUnfilledTemplate) {
|
|
3370
|
-
const content =
|
|
3450
|
+
const content = fs12.readFileSync(reviewPath, "utf8");
|
|
3371
3451
|
if (isTemplateUnfilled(content)) return null;
|
|
3452
|
+
if (options.scaffold && isPristineTaskArtifact(content, "review.md", options.scaffold.taskId, options.scaffold.title)) {
|
|
3453
|
+
return null;
|
|
3454
|
+
}
|
|
3372
3455
|
}
|
|
3373
3456
|
const archiveName = `${REVIEW_ARCHIVE_PREFIX}${newestReviewArchiveNumber(taskDir) + 1}.md`;
|
|
3374
|
-
|
|
3457
|
+
fs12.renameSync(reviewPath, path13.join(taskDir, archiveName));
|
|
3375
3458
|
return archiveName;
|
|
3376
3459
|
}
|
|
3460
|
+
function rescaffoldReview(taskDir, identity) {
|
|
3461
|
+
try {
|
|
3462
|
+
if (fs12.existsSync(path13.join(taskDir, "review.md"))) return { outcome: "exists" };
|
|
3463
|
+
const source = scaffoldTaskArtifact(taskDir, "review.md", identity.taskId, identity.title);
|
|
3464
|
+
return source ? { outcome: "written", source } : { outcome: "no-template" };
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
return { outcome: "error", message: error instanceof Error ? error.message : String(error) };
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3377
3469
|
|
|
3378
3470
|
// src/task/index.ts
|
|
3379
3471
|
var VALID_PHASES = new Set(PHASE_ORDER);
|
|
@@ -3421,35 +3513,26 @@ function validateTaskId(id) {
|
|
|
3421
3513
|
throw new Error(`Error: invalid task ID '${id}'. Must not contain '..'.`);
|
|
3422
3514
|
}
|
|
3423
3515
|
}
|
|
3424
|
-
function tasksRoot() {
|
|
3425
|
-
return process.env.CANON_TASKS_DIR_OVERRIDE ?? "tasks";
|
|
3426
|
-
}
|
|
3427
3516
|
function taskDirFromRoot(taskId) {
|
|
3428
|
-
return
|
|
3517
|
+
return path14.join(tasksRoot(), taskId);
|
|
3429
3518
|
}
|
|
3430
3519
|
function taskDirForCwd(_cwd, taskId) {
|
|
3431
3520
|
const root = tasksRoot();
|
|
3432
|
-
if (
|
|
3433
|
-
return
|
|
3521
|
+
if (path14.isAbsolute(root)) {
|
|
3522
|
+
return path14.join(root, taskId);
|
|
3434
3523
|
}
|
|
3435
|
-
return
|
|
3524
|
+
return path14.join(resolveTaskCwd(taskId), root, taskId);
|
|
3436
3525
|
}
|
|
3437
3526
|
function taskStatusFileForCwd(cwd, taskId) {
|
|
3438
|
-
return
|
|
3527
|
+
return path14.join(taskDirForCwd(cwd, taskId), "status.json");
|
|
3439
3528
|
}
|
|
3440
3529
|
function taskRootForGate(cwd) {
|
|
3441
3530
|
const root = tasksRoot();
|
|
3442
|
-
return
|
|
3443
|
-
}
|
|
3444
|
-
function templatesRoot() {
|
|
3445
|
-
return path12.join(process.cwd(), ".canon", "templates");
|
|
3446
|
-
}
|
|
3447
|
-
function taskTemplateOverrideRoot() {
|
|
3448
|
-
return path12.join(tasksRoot(), "_templates");
|
|
3531
|
+
return path14.isAbsolute(root) ? root : path14.join(cwd, root);
|
|
3449
3532
|
}
|
|
3450
3533
|
function readJsonFile(filePath) {
|
|
3451
3534
|
try {
|
|
3452
|
-
return JSON.parse(
|
|
3535
|
+
return JSON.parse(fs13.readFileSync(filePath, "utf8"));
|
|
3453
3536
|
} catch (error) {
|
|
3454
3537
|
const message = error instanceof Error ? error.message : String(error);
|
|
3455
3538
|
throw new Error(`Error: failed to read ${filePath}: ${message}`);
|
|
@@ -3457,9 +3540,9 @@ function readJsonFile(filePath) {
|
|
|
3457
3540
|
}
|
|
3458
3541
|
function writeJsonAtomic(filePath, data) {
|
|
3459
3542
|
const tmpFile = `${filePath}.tmp`;
|
|
3460
|
-
|
|
3543
|
+
fs13.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}
|
|
3461
3544
|
`, "utf8");
|
|
3462
|
-
|
|
3545
|
+
fs13.renameSync(tmpFile, filePath);
|
|
3463
3546
|
}
|
|
3464
3547
|
function writeStatusAtomic(filePath, status) {
|
|
3465
3548
|
status.status = deriveTopLevelStatus(status);
|
|
@@ -3498,21 +3581,17 @@ function currentBranchOrEmpty() {
|
|
|
3498
3581
|
if (result.error || result.status !== 0) return "";
|
|
3499
3582
|
return (result.stdout ?? "").trim();
|
|
3500
3583
|
}
|
|
3501
|
-
function copyTemplateFile(source, destination, taskId, title) {
|
|
3502
|
-
const content = fs12.readFileSync(source, "utf8").replaceAll("[TASK-ID]", taskId).replaceAll("[Title]", title);
|
|
3503
|
-
fs12.writeFileSync(destination, content, "utf8");
|
|
3504
|
-
}
|
|
3505
3584
|
function listTemplateFiles() {
|
|
3506
3585
|
const root = templatesRoot();
|
|
3507
|
-
if (!
|
|
3586
|
+
if (!fs13.existsSync(root)) {
|
|
3508
3587
|
throw new Error(`Error: templates directory not found at ${root}`);
|
|
3509
3588
|
}
|
|
3510
|
-
return
|
|
3589
|
+
return fs13.readdirSync(root).filter((name) => name.endsWith(".md") || name.endsWith(".json")).sort();
|
|
3511
3590
|
}
|
|
3512
3591
|
function printCreatedTask(taskDir, baseBranch) {
|
|
3513
3592
|
console.log(`Created task: ${taskDir}`);
|
|
3514
3593
|
console.log("Files:");
|
|
3515
|
-
for (const file of
|
|
3594
|
+
for (const file of fs13.readdirSync(taskDir).sort()) {
|
|
3516
3595
|
console.log(file);
|
|
3517
3596
|
}
|
|
3518
3597
|
console.log("");
|
|
@@ -3550,20 +3629,18 @@ function taskNew(args2) {
|
|
|
3550
3629
|
throw new Error("Error: title must be single-line (no embedded newlines).");
|
|
3551
3630
|
}
|
|
3552
3631
|
const taskDir = taskDirFromRoot(id);
|
|
3553
|
-
if (
|
|
3632
|
+
if (fs13.existsSync(taskDir)) {
|
|
3554
3633
|
throw new Error(`Error: Task directory ${taskDir} already exists.`);
|
|
3555
3634
|
}
|
|
3556
3635
|
if (!baseBranch) {
|
|
3557
3636
|
baseBranch = currentBranchOrEmpty() || "main";
|
|
3558
3637
|
}
|
|
3559
|
-
|
|
3560
|
-
const overrideRoot = taskTemplateOverrideRoot();
|
|
3638
|
+
fs13.mkdirSync(taskDir, { recursive: true });
|
|
3561
3639
|
for (const basename2 of listTemplateFiles()) {
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3564
|
-
copyTemplateFile(source, path12.join(taskDir, basename2), id, title);
|
|
3640
|
+
const source = resolveTaskTemplateSource(basename2) ?? path14.join(templatesRoot(), basename2);
|
|
3641
|
+
fs13.writeFileSync(path14.join(taskDir, basename2), renderTaskTemplate(source, id, title), "utf8");
|
|
3565
3642
|
}
|
|
3566
|
-
const statusPath =
|
|
3643
|
+
const statusPath = path14.join(taskDir, "status.json");
|
|
3567
3644
|
const status = readJsonFile(statusPath);
|
|
3568
3645
|
status.id = id;
|
|
3569
3646
|
status.title = title;
|
|
@@ -3584,31 +3661,31 @@ function derivePhase(status) {
|
|
|
3584
3661
|
}
|
|
3585
3662
|
function taskList() {
|
|
3586
3663
|
const root = tasksRoot();
|
|
3587
|
-
if (!
|
|
3664
|
+
if (!fs13.existsSync(root)) {
|
|
3588
3665
|
console.log("No tasks found.");
|
|
3589
3666
|
return;
|
|
3590
3667
|
}
|
|
3591
3668
|
const rows = [];
|
|
3592
3669
|
let invalidCount = 0;
|
|
3593
|
-
for (const entry of
|
|
3670
|
+
for (const entry of fs13.readdirSync(root).sort()) {
|
|
3594
3671
|
if (entry === "_archive" || entry === "_templates") continue;
|
|
3595
3672
|
if (isOrphanedWorktreeState(entry)) {
|
|
3596
3673
|
invalidCount += 1;
|
|
3597
3674
|
let title = "(untitled)";
|
|
3598
3675
|
try {
|
|
3599
|
-
const frozen = readJsonFile(
|
|
3676
|
+
const frozen = readJsonFile(path14.join(taskDirForRepoRoot(entry), "status.json"));
|
|
3600
3677
|
title = frozen.title ?? title;
|
|
3601
3678
|
} catch {
|
|
3602
3679
|
}
|
|
3603
3680
|
rows.push({
|
|
3604
3681
|
id: entry,
|
|
3605
3682
|
title,
|
|
3606
|
-
phase: `INVALID: worktree missing \u2014 restore ${
|
|
3683
|
+
phase: `INVALID: worktree missing \u2014 restore ${path14.join(effectiveWorktreesRoot(), entry)} or archive the task`
|
|
3607
3684
|
});
|
|
3608
3685
|
continue;
|
|
3609
3686
|
}
|
|
3610
|
-
const statusPath =
|
|
3611
|
-
if (!
|
|
3687
|
+
const statusPath = path14.join(taskDirForCwd(process.cwd(), entry), "status.json");
|
|
3688
|
+
if (!fs13.existsSync(statusPath)) continue;
|
|
3612
3689
|
try {
|
|
3613
3690
|
const status = readJsonFile(statusPath);
|
|
3614
3691
|
const phase = derivePhase(status);
|
|
@@ -3645,7 +3722,7 @@ function taskStatus(id) {
|
|
|
3645
3722
|
validateTaskId(id);
|
|
3646
3723
|
const cwd = resolveTaskCwd(id);
|
|
3647
3724
|
const statusPath = taskStatusFileForCwd(cwd, id);
|
|
3648
|
-
if (!
|
|
3725
|
+
if (!fs13.existsSync(statusPath)) {
|
|
3649
3726
|
throw new Error(`Error: No status.json found for task ${id}`);
|
|
3650
3727
|
}
|
|
3651
3728
|
const status = readJsonFile(statusPath);
|
|
@@ -3718,7 +3795,7 @@ function taskPhase(id, phaseArg, statusArg, verdictArg) {
|
|
|
3718
3795
|
assertValidVerdict(phaseArg, verdictArg);
|
|
3719
3796
|
const taskCwd = resolveTaskCwd(id);
|
|
3720
3797
|
const statusPath = taskStatusFileForCwd(taskCwd, id);
|
|
3721
|
-
if (!
|
|
3798
|
+
if (!fs13.existsSync(statusPath)) {
|
|
3722
3799
|
throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
|
|
3723
3800
|
}
|
|
3724
3801
|
const status = readJsonFile(statusPath);
|
|
@@ -3760,7 +3837,7 @@ function taskPhase(id, phaseArg, statusArg, verdictArg) {
|
|
|
3760
3837
|
writeQualityLogForTask(
|
|
3761
3838
|
id,
|
|
3762
3839
|
taskCwd,
|
|
3763
|
-
|
|
3840
|
+
path14.join(taskDirForCwd(taskCwd, id), "done.md"),
|
|
3764
3841
|
status
|
|
3765
3842
|
);
|
|
3766
3843
|
}
|
|
@@ -3784,7 +3861,7 @@ function taskAccept(ids, phaseArg, options = {}) {
|
|
|
3784
3861
|
for (const id of ids) {
|
|
3785
3862
|
const taskCwd = resolveTaskCwd(id);
|
|
3786
3863
|
const statusPath = taskStatusFileForCwd(taskCwd, id);
|
|
3787
|
-
if (!
|
|
3864
|
+
if (!fs13.existsSync(statusPath)) {
|
|
3788
3865
|
throw new Error(`Error: No status.json found for task ${id} (looked in ${taskDirForCwd(taskCwd, id)}/)`);
|
|
3789
3866
|
}
|
|
3790
3867
|
const status = readJsonFile(statusPath);
|
|
@@ -3886,7 +3963,7 @@ function taskAccept(ids, phaseArg, options = {}) {
|
|
|
3886
3963
|
const originalSnapshots2 = /* @__PURE__ */ new Map();
|
|
3887
3964
|
for (const ctx of ctxByTask.values()) {
|
|
3888
3965
|
try {
|
|
3889
|
-
originalSnapshots2.set(ctx.statusPath,
|
|
3966
|
+
originalSnapshots2.set(ctx.statusPath, fs13.readFileSync(ctx.statusPath, "utf8"));
|
|
3890
3967
|
} catch (error) {
|
|
3891
3968
|
const message = error instanceof Error ? error.message : String(error);
|
|
3892
3969
|
throw new Error(`Error: failed to read ${ctx.statusPath} for rollback snapshot: ${message}`);
|
|
@@ -3923,8 +4000,8 @@ function taskAccept(ids, phaseArg, options = {}) {
|
|
|
3923
4000
|
if (original === void 0) continue;
|
|
3924
4001
|
try {
|
|
3925
4002
|
const tmpFile = `${filePath}.rollback.tmp`;
|
|
3926
|
-
|
|
3927
|
-
|
|
4003
|
+
fs13.writeFileSync(tmpFile, original, "utf8");
|
|
4004
|
+
fs13.renameSync(tmpFile, filePath);
|
|
3928
4005
|
} catch (rollbackErr) {
|
|
3929
4006
|
const message = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
3930
4007
|
rollbackErrors.push(` ${filePath}: ${message}`);
|
|
@@ -3942,18 +4019,18 @@ ${rollbackErrors.join("\n")}`
|
|
|
3942
4019
|
throw new Error(`Error: bundled review accept failed; rolled back to pre-accept state. Original error: ${originalMessage}`);
|
|
3943
4020
|
}
|
|
3944
4021
|
for (const ctx of ctxByTask.values()) {
|
|
3945
|
-
const notesPath =
|
|
4022
|
+
const notesPath = path14.join(taskDirForCwd(ctx.taskCwd, ctx.id), "notes.md");
|
|
3946
4023
|
const entry = ctx.status.phases[phaseArg];
|
|
3947
4024
|
const sanctioned = entry?.verdict === "sanctioned";
|
|
3948
4025
|
const bundleNote = ids.length > 1 ? ` Bundle: ${ids.join(", ")}.` : "";
|
|
3949
4026
|
const noteLine = `[${today()}] Operator accepted ${phaseArg} via \`canon task accept\` \u2014 ${sanctioned ? "sanctioned (agent verdict overridden)" : "unblocked (advancing verdict preserved)"}. Reason: ${reason}.${bundleNote}`;
|
|
3950
4027
|
try {
|
|
3951
|
-
if (
|
|
3952
|
-
|
|
4028
|
+
if (fs13.existsSync(notesPath)) {
|
|
4029
|
+
fs13.appendFileSync(notesPath, `
|
|
3953
4030
|
${noteLine}
|
|
3954
4031
|
`, "utf8");
|
|
3955
4032
|
} else {
|
|
3956
|
-
|
|
4033
|
+
fs13.writeFileSync(notesPath, `${noteLine}
|
|
3957
4034
|
`, "utf8");
|
|
3958
4035
|
}
|
|
3959
4036
|
} catch (error) {
|
|
@@ -4076,7 +4153,7 @@ ${noteLine}
|
|
|
4076
4153
|
const originalSnapshots = /* @__PURE__ */ new Map();
|
|
4077
4154
|
for (const ctx of ctxByTask.values()) {
|
|
4078
4155
|
try {
|
|
4079
|
-
originalSnapshots.set(ctx.statusPath,
|
|
4156
|
+
originalSnapshots.set(ctx.statusPath, fs13.readFileSync(ctx.statusPath, "utf8"));
|
|
4080
4157
|
} catch (error) {
|
|
4081
4158
|
const message = error instanceof Error ? error.message : String(error);
|
|
4082
4159
|
throw new Error(`Error: failed to read ${ctx.statusPath} for rollback snapshot: ${message}`);
|
|
@@ -4101,8 +4178,8 @@ ${noteLine}
|
|
|
4101
4178
|
if (original === void 0) continue;
|
|
4102
4179
|
try {
|
|
4103
4180
|
const tmpFile = `${filePath}.rollback.tmp`;
|
|
4104
|
-
|
|
4105
|
-
|
|
4181
|
+
fs13.writeFileSync(tmpFile, original, "utf8");
|
|
4182
|
+
fs13.renameSync(tmpFile, filePath);
|
|
4106
4183
|
} catch (rollbackErr) {
|
|
4107
4184
|
const message = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
4108
4185
|
rollbackErrors.push(` ${filePath}: ${message}`);
|
|
@@ -4121,15 +4198,15 @@ ${noteLine}
|
|
|
4121
4198
|
throw new Error(`Error: bundled accept failed; rolled back to pre-accept state. Original error: ${originalMessage}`);
|
|
4122
4199
|
}
|
|
4123
4200
|
for (const ctx of ctxByTask.values()) {
|
|
4124
|
-
const notesPath =
|
|
4201
|
+
const notesPath = path14.join(taskDirForCwd(ctx.taskCwd, ctx.id), "notes.md");
|
|
4125
4202
|
const noteLine = `[${today()}] Operator accepted implement phase via \`canon task accept\` \u2014 auto-commit will be skipped.${options.force ? " (--force)" : ""}`;
|
|
4126
4203
|
try {
|
|
4127
|
-
if (
|
|
4128
|
-
|
|
4204
|
+
if (fs13.existsSync(notesPath)) {
|
|
4205
|
+
fs13.appendFileSync(notesPath, `
|
|
4129
4206
|
${noteLine}
|
|
4130
4207
|
`, "utf8");
|
|
4131
4208
|
} else {
|
|
4132
|
-
|
|
4209
|
+
fs13.writeFileSync(notesPath, `${noteLine}
|
|
4133
4210
|
`, "utf8");
|
|
4134
4211
|
}
|
|
4135
4212
|
} catch (error) {
|
|
@@ -4148,15 +4225,15 @@ function taskResetSpecReview(id) {
|
|
|
4148
4225
|
validateTaskId(id);
|
|
4149
4226
|
const taskCwd = resolveTaskCwd(id);
|
|
4150
4227
|
const taskDir = taskDirForCwd(taskCwd, id);
|
|
4151
|
-
const statusPath =
|
|
4152
|
-
if (!
|
|
4228
|
+
const statusPath = path14.join(taskDir, "status.json");
|
|
4229
|
+
if (!fs13.existsSync(statusPath)) {
|
|
4153
4230
|
throw new Error(`Error: no status.json at ${statusPath}`);
|
|
4154
4231
|
}
|
|
4155
|
-
const reviewPath =
|
|
4156
|
-
if (
|
|
4232
|
+
const reviewPath = path14.join(taskDir, "spec-review.md");
|
|
4233
|
+
if (fs13.existsSync(reviewPath)) {
|
|
4157
4234
|
let n = 1;
|
|
4158
|
-
while (
|
|
4159
|
-
|
|
4235
|
+
while (fs13.existsSync(path14.join(taskDir, `spec-review-prior-${n}.md`))) n += 1;
|
|
4236
|
+
fs13.renameSync(reviewPath, path14.join(taskDir, `spec-review-prior-${n}.md`));
|
|
4160
4237
|
console.log(`Archived prior spec-review.md \u2192 spec-review-prior-${n}.md`);
|
|
4161
4238
|
}
|
|
4162
4239
|
const status = readJsonFile(statusPath);
|
|
@@ -4179,8 +4256,8 @@ function taskResetCodeReview(id) {
|
|
|
4179
4256
|
validateTaskId(id);
|
|
4180
4257
|
const taskCwd = resolveTaskCwd(id);
|
|
4181
4258
|
const taskDir = taskDirForCwd(taskCwd, id);
|
|
4182
|
-
const statusPath =
|
|
4183
|
-
if (!
|
|
4259
|
+
const statusPath = path14.join(taskDir, "status.json");
|
|
4260
|
+
if (!fs13.existsSync(statusPath)) {
|
|
4184
4261
|
throw new Error(`Error: no status.json at ${statusPath}`);
|
|
4185
4262
|
}
|
|
4186
4263
|
const status = readJsonFile(statusPath);
|
|
@@ -4193,6 +4270,18 @@ function taskResetCodeReview(id) {
|
|
|
4193
4270
|
if (archivedReview) {
|
|
4194
4271
|
console.log(`Archived prior review.md \u2192 ${archivedReview}`);
|
|
4195
4272
|
}
|
|
4273
|
+
const rescaffold = rescaffoldReview(taskDir, { taskId: id, title: status.title ?? "" });
|
|
4274
|
+
if (rescaffold.outcome === "written") {
|
|
4275
|
+
console.log(`Re-scaffolded review.md from ${path14.relative(process.cwd(), rescaffold.source)}`);
|
|
4276
|
+
} else if (rescaffold.outcome === "no-template") {
|
|
4277
|
+
console.warn(
|
|
4278
|
+
"Warning: review.md is missing and no template was found to re-scaffold it (looked for a review.md override under tasks/_templates/ and .canon/templates/review.md); the next code review will write it from scratch."
|
|
4279
|
+
);
|
|
4280
|
+
} else if (rescaffold.outcome === "error") {
|
|
4281
|
+
console.warn(
|
|
4282
|
+
`Warning: review.md is missing and could not be re-scaffolded: ${rescaffold.message}; the next code review will write it from scratch.`
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4196
4285
|
const implement = ensurePhaseEntry(status, "implement");
|
|
4197
4286
|
const codeReview = ensurePhaseEntry(status, "code_review");
|
|
4198
4287
|
implement.status = "done";
|
|
@@ -4226,16 +4315,16 @@ function parsePorcelainPath(line) {
|
|
|
4226
4315
|
}
|
|
4227
4316
|
function isPipelineOwnedAcceptPath(filePath, taskId, gitCwd) {
|
|
4228
4317
|
const repoRootForPaths = resolveRepoRootForAccept(gitCwd);
|
|
4229
|
-
const dirtyAbsolute =
|
|
4318
|
+
const dirtyAbsolute = path14.isAbsolute(filePath) ? filePath : path14.resolve(repoRootForPaths, filePath);
|
|
4230
4319
|
const canonicalDirty = safeRealpath(dirtyAbsolute);
|
|
4231
4320
|
const root = tasksRoot();
|
|
4232
|
-
const rootAbsolute =
|
|
4321
|
+
const rootAbsolute = path14.isAbsolute(root) ? root : path14.resolve(repoRootForPaths, root);
|
|
4233
4322
|
const canonicalRoot = safeRealpath(rootAbsolute);
|
|
4234
|
-
const taskCanonical =
|
|
4323
|
+
const taskCanonical = path14.join(canonicalRoot, taskId);
|
|
4235
4324
|
if (canonicalDirty === taskCanonical) return true;
|
|
4236
|
-
if (canonicalDirty.startsWith(`${taskCanonical}${
|
|
4325
|
+
if (canonicalDirty.startsWith(`${taskCanonical}${path14.sep}`)) return true;
|
|
4237
4326
|
for (const telemetry of PIPELINE_TELEMETRY_FILES) {
|
|
4238
|
-
const telemetryAbsolute =
|
|
4327
|
+
const telemetryAbsolute = path14.resolve(repoRootForPaths, telemetry);
|
|
4239
4328
|
if (safeRealpath(telemetryAbsolute) === canonicalDirty) return true;
|
|
4240
4329
|
}
|
|
4241
4330
|
return false;
|
|
@@ -4250,16 +4339,16 @@ function resolveMainCheckoutRoot() {
|
|
|
4250
4339
|
if (out.error || out.status !== 0) return process.cwd();
|
|
4251
4340
|
const gitCommonDir = (out.stdout ?? "").trim();
|
|
4252
4341
|
if (!gitCommonDir) return process.cwd();
|
|
4253
|
-
return
|
|
4342
|
+
return path14.dirname(gitCommonDir);
|
|
4254
4343
|
}
|
|
4255
4344
|
function safeRealpath(target) {
|
|
4256
4345
|
try {
|
|
4257
|
-
return
|
|
4346
|
+
return fs13.realpathSync(target);
|
|
4258
4347
|
} catch {
|
|
4259
|
-
const parent =
|
|
4348
|
+
const parent = path14.dirname(target);
|
|
4260
4349
|
if (parent === target) return target;
|
|
4261
4350
|
try {
|
|
4262
|
-
return
|
|
4351
|
+
return path14.join(fs13.realpathSync(parent), path14.basename(target));
|
|
4263
4352
|
} catch {
|
|
4264
4353
|
return target;
|
|
4265
4354
|
}
|
|
@@ -4367,12 +4456,12 @@ function taskPostMergeSync(branchArg) {
|
|
|
4367
4456
|
}
|
|
4368
4457
|
function nudgeShippableTasks() {
|
|
4369
4458
|
const root = tasksRoot();
|
|
4370
|
-
if (!
|
|
4459
|
+
if (!fs13.existsSync(root)) return;
|
|
4371
4460
|
const shippable = [];
|
|
4372
|
-
for (const entry of
|
|
4461
|
+
for (const entry of fs13.readdirSync(root).sort()) {
|
|
4373
4462
|
if (entry === "_archive" || entry.startsWith("_")) continue;
|
|
4374
|
-
const statusPath =
|
|
4375
|
-
if (!
|
|
4463
|
+
const statusPath = path14.join(root, entry, "status.json");
|
|
4464
|
+
if (!fs13.existsSync(statusPath)) continue;
|
|
4376
4465
|
let status;
|
|
4377
4466
|
try {
|
|
4378
4467
|
status = readJsonFile(statusPath);
|
|
@@ -4447,7 +4536,7 @@ function taskSet(args2) {
|
|
|
4447
4536
|
validateTaskId(id);
|
|
4448
4537
|
const taskCwd = resolveTaskCwd(id);
|
|
4449
4538
|
const statusPath = taskStatusFileForCwd(taskCwd, id);
|
|
4450
|
-
if (!
|
|
4539
|
+
if (!fs13.existsSync(statusPath)) {
|
|
4451
4540
|
throw new Error(`Error: No status.json found for task ${id}`);
|
|
4452
4541
|
}
|
|
4453
4542
|
const status = readJsonFile(statusPath);
|
|
@@ -4788,7 +4877,7 @@ function currentPinFromManifest(manifest) {
|
|
|
4788
4877
|
return "unknown";
|
|
4789
4878
|
}
|
|
4790
4879
|
function bakedVersion() {
|
|
4791
|
-
return "3.1.
|
|
4880
|
+
return "3.1.2";
|
|
4792
4881
|
}
|
|
4793
4882
|
function ownPackageName(pkgDir) {
|
|
4794
4883
|
try {
|
|
@@ -5170,6 +5259,14 @@ function printUpgradeRefusals(refusals, prefix) {
|
|
|
5170
5259
|
}
|
|
5171
5260
|
}
|
|
5172
5261
|
function runUpgrade(cwd, pkgDir, options = {}) {
|
|
5262
|
+
assertNoSymlinkDestinations(cwd, [
|
|
5263
|
+
...CANON_OWNED,
|
|
5264
|
+
...DELIMITED,
|
|
5265
|
+
...HEADER_ONLY_SYNC,
|
|
5266
|
+
".canon/version",
|
|
5267
|
+
".gitignore",
|
|
5268
|
+
"scripts/docs-refs-config.mjs"
|
|
5269
|
+
]);
|
|
5173
5270
|
const upgraded = [];
|
|
5174
5271
|
const unchanged = [];
|
|
5175
5272
|
const skipped = [];
|
|
@@ -5254,7 +5351,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5254
5351
|
cutoverWarnings.push(docsRefsCheckRel);
|
|
5255
5352
|
}
|
|
5256
5353
|
const versionPath = join5(cwd, ".canon", "version");
|
|
5257
|
-
const newVersion = "3.1.
|
|
5354
|
+
const newVersion = "3.1.2";
|
|
5258
5355
|
const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
|
|
5259
5356
|
if (currentVersion !== newVersion) {
|
|
5260
5357
|
pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
|
|
@@ -5332,6 +5429,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5332
5429
|
const reportedWrites = options.force ? pending : clean;
|
|
5333
5430
|
const staleOverrides = getStaleOverrides(cwd, reportedWrites);
|
|
5334
5431
|
const toWrite = options.force ? pending : clean;
|
|
5432
|
+
assertNoSymlinkDestinations(cwd, toWrite.map((op) => op.rel));
|
|
5335
5433
|
for (const op of toWrite) {
|
|
5336
5434
|
mkdirSync3(dirname5(op.projectPath), { recursive: true });
|
|
5337
5435
|
writeFileSync3(op.projectPath, op.content);
|
|
@@ -5561,7 +5659,7 @@ Global:
|
|
|
5561
5659
|
`);
|
|
5562
5660
|
}
|
|
5563
5661
|
function printVersion() {
|
|
5564
|
-
console.log("3.1.
|
|
5662
|
+
console.log("3.1.2");
|
|
5565
5663
|
}
|
|
5566
5664
|
switch (command) {
|
|
5567
5665
|
case "doctor":
|