canon-ai 3.11.0 → 3.12.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 +17 -0
- package/README.md +1 -1
- package/dist/cli/index.js +68 -25
- package/dist/orchestrator/run-task.js +464 -71
- package/package.json +1 -1
- package/templates/.claude/skills/canon-pipeline/SKILL.md +1 -1
- package/templates/.claude/skills/canon-pipeline/recovery.md +1 -1
- package/templates/docs/pipeline-orchestrator.md +15 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.12.0] — 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Broken references in a task's handoff or QA summary are fixed by the agent that wrote them, before the commit.** After implement, and again after QA, `canon run` checks the task's own artifacts with the docs-refs checker. If it finds a broken reference, it resumes the same agent session to fix it, up to two attempts. A fix session may change only the task's own artifacts; if it touches anything else, the run stops and names the files. If a reference is still broken after two attempts, canon warns and continues as before. The implement and QA prompts also now ask agents to cite repository files by full path and deleted files as markdown links.
|
|
12
|
+
- **`--full-send` pushes its own scaffold commits to the base branch before opening the draft PR.** It pushes only commits that have the exact scaffold subject and change only task artifacts and pipeline telemetry, and it pushes the checked commit rather than the moving branch. If any other commit is unpushed on the base branch, nothing is pushed and the run stops at the divergence check as before. A rejected push (for example, a protected branch) stops with a hint to push or merge manually. The "Scaffold committed" hint now prints only when such commits exist.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`--pr` and `--push` block only on real merge conflicts with the base.** A base branch that moved on files the task never touched, such as another task's artifacts, no longer stops the run; canon prints one line saying those files merge cleanly. A conflict with `origin/<base>`, including in a file the spec declared, still aborts before the push and names the conflicted files. The task's own out-of-scope changes still abort as before. `--force` bypasses a conflict with a warning. On Git older than 2.38, canon warns that conflicts weren't checked and proceeds.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`canon watch` reports a full-send run that died after QA as a death, not a clean stop.** A run that crashed while opening its draft PR now shows `reason=death` and exits 4, with the reason on stderr, whether watch was attached during the run or started after it. A tail that is still running keeps watch blocking.
|
|
21
|
+
- **`canon task reset-spec-review` re-arms the spec gate.** The gate disarms itself when it halts, so a reset after that halt used to let the next run go straight through to human review. The reset now re-arms it and says so, unless the task is full-send.
|
|
22
|
+
- **`--ship` no longer warns that the branch delete failed.** It merges with `gh pr merge --squash`, deletes the remote branch itself, and deletes the local branch after the worktree is removed; the old order made `gh` try to delete a branch the worktree still held.
|
|
23
|
+
|
|
7
24
|
## [3.11.0] — 2026-10-20
|
|
8
25
|
|
|
9
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -257,7 +257,7 @@ Pipeline sessions are headless, so nobody is at the keyboard to answer a permiss
|
|
|
257
257
|
- **Worktrees separate branch state by convention, not by enforcement.** Each task, or each bundle when you pass several task ids together, gets its own worktree and branch, and the pipeline's dirty-tree gates inspect the active worktree at phase boundaries. But Claude sessions are also given the main checkout via `--add-dir` and run with permissions skipped, so nothing stops a session from writing there, and an unrelated file dropped into the main checkout is not something those gates look for; tasks with `worktree: false` run in the main checkout outright. Validation commands run with your normal privileges.
|
|
258
258
|
- **Budget caps bound cost, not access.** Each headless Claude phase runs with `--max-budget-usd`; hitting the cap stops the session. It does not narrow what the session may do beforehand, and interactive sessions (`--interactive`) run uncapped.
|
|
259
259
|
- **Your code goes to the model providers.** Each agent phase sends prompts and repository context to the provider running it, Anthropic for Claude phases and OpenAI for Codex phases, with `code_review` using both, all through the Claude Code and Codex CLIs under whatever data terms your accounts have. Treat a `canon run` like any other agent session in that respect.
|
|
260
|
-
- **Git side effects need a flag.** The pipeline never pushes, opens PRs, merges, or publishes on its own. `--pr` pushes the task branch and opens a draft PR, stopping at `human_review`. `--ship` squash-merges that PR; it checks the task's phase, not whether anyone approved, so PR approval is your prerequisite (or your branch protection's), not something canon enforces. `--full-send` skips the human spec gate and opens the draft PR on clean QA, still stopping before merge;
|
|
260
|
+
- **Git side effects need a flag.** The pipeline never pushes, opens PRs, merges, or publishes on its own. `--pr` pushes the task branch and opens a draft PR, stopping at `human_review`. `--ship` squash-merges that PR; it checks the task's phase, not whether anyone approved, so PR approval is your prerequisite (or your branch protection's), not something canon enforces. `--full-send` skips the human spec gate and opens the draft PR on clean QA, still stopping before merge; its tail also pushes unpushed base commits that carry only task artifacts and pipeline telemetry, and leaves other unpushed commits to the divergence gate unless `--allow-divergent-base` is set. On a `delicate` task it refuses without `--force`.
|
|
261
261
|
|
|
262
262
|
Run canon on repositories where you would be comfortable running your test suite unattended. If that is not your situation, keep it to a machine or container scoped for the purpose. Replacing the blanket permission skip with a narrower confinement is on the roadmap; the constraint to solve is that adopter validation commands legitimately need network and system access the sandbox would block. Security reports go through [SECURITY.md](SECURITY.md).
|
|
263
263
|
|
package/dist/cli/index.js
CHANGED
|
@@ -403,6 +403,11 @@ function filterGitIgnoredPaths(paths, cwd) {
|
|
|
403
403
|
const stdout = result.stdout ?? "";
|
|
404
404
|
return new Set(stdout.split("\0").filter((p) => p.length > 0));
|
|
405
405
|
}
|
|
406
|
+
var CANON_TASK_ID_PATTERN = "[a-z0-9][a-z0-9._-]*";
|
|
407
|
+
var SCAFFOLD_ARTIFACTS_SUBJECT = new RegExp(`^task\\(${CANON_TASK_ID_PATTERN}\\): commit artifacts pre-pipeline$`);
|
|
408
|
+
var SCAFFOLD_TELEMETRY_SUBJECT = new RegExp(
|
|
409
|
+
`^chore: absorb pre-implement telemetry into scaffold for ${CANON_TASK_ID_PATTERN}(?:, ${CANON_TASK_ID_PATTERN})*$`
|
|
410
|
+
);
|
|
406
411
|
|
|
407
412
|
// src/orchestrator/worktree.ts
|
|
408
413
|
var PIPELINE_TELEMETRY_FILES = [
|
|
@@ -1500,7 +1505,7 @@ function checkTemplates(cwd) {
|
|
|
1500
1505
|
}
|
|
1501
1506
|
function checkCanonVersion(cwd) {
|
|
1502
1507
|
const versionPath = join(cwd, ".canon", "version");
|
|
1503
|
-
const installedVersion = "3.
|
|
1508
|
+
const installedVersion = "3.12.0";
|
|
1504
1509
|
if (!existsSync(versionPath)) {
|
|
1505
1510
|
return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
|
|
1506
1511
|
}
|
|
@@ -2025,7 +2030,7 @@ function initCmd(_args) {
|
|
|
2025
2030
|
}
|
|
2026
2031
|
function writeCanonVersion(cwd) {
|
|
2027
2032
|
const versionPath = join2(cwd, ".canon", "version");
|
|
2028
|
-
const version = "3.
|
|
2033
|
+
const version = "3.12.0";
|
|
2029
2034
|
mkdirSync(dirname2(versionPath), { recursive: true });
|
|
2030
2035
|
writeFileSync(versionPath, version + "\n");
|
|
2031
2036
|
}
|
|
@@ -2382,6 +2387,9 @@ function findFirstBlockedPhase(status) {
|
|
|
2382
2387
|
}
|
|
2383
2388
|
return null;
|
|
2384
2389
|
}
|
|
2390
|
+
function isFullSendTailPending(status) {
|
|
2391
|
+
return status.full_send === true && status.status === "human_review" && (status.phases.human_review?.status ?? "pending") === "pending";
|
|
2392
|
+
}
|
|
2385
2393
|
function findPreviousDonePhase(status, beforePhase) {
|
|
2386
2394
|
const index = PHASE_ORDER.indexOf(beforePhase);
|
|
2387
2395
|
for (let i = index - 1; i >= 0; i -= 1) {
|
|
@@ -2518,6 +2526,8 @@ function parseWatchArgs(args2) {
|
|
|
2518
2526
|
}
|
|
2519
2527
|
function summarizeIdle(result) {
|
|
2520
2528
|
switch (result.kind) {
|
|
2529
|
+
case "live":
|
|
2530
|
+
throw new Error("summarizeIdle called for live result");
|
|
2521
2531
|
case "checkpoint":
|
|
2522
2532
|
return { state: result.state, reason: "checkpoint", phase: result.phase, verdict: result.verdict, pid: result.pid };
|
|
2523
2533
|
case "complete":
|
|
@@ -2572,6 +2582,15 @@ function classifyAttach(ctx, taskId, probeAlive, now) {
|
|
|
2572
2582
|
return { kind: "launch_window", state };
|
|
2573
2583
|
}
|
|
2574
2584
|
if (status) {
|
|
2585
|
+
if (isFullSendTailPending(status)) {
|
|
2586
|
+
if (orchestratorLive && ctx.resolvedPid != null) {
|
|
2587
|
+
return { kind: "live", pid: ctx.resolvedPid, state };
|
|
2588
|
+
}
|
|
2589
|
+
const heartbeatPrimary = ctx.heartbeatResult.kind === "found" ? ctx.heartbeatResult.record.task_ids[0] : void 0;
|
|
2590
|
+
const primaryId = typeof heartbeatPrimary === "string" && heartbeatPrimary.length > 0 ? heartbeatPrimary : taskId;
|
|
2591
|
+
const reason = findCrashEvidence(ctx, taskId, probeAlive, primaryId, orchestratorLive);
|
|
2592
|
+
if (reason) return { kind: "death", state, hint: `run \`canon run ${taskId}\` to resume`, reason };
|
|
2593
|
+
}
|
|
2575
2594
|
const currentPhase = PHASE_ORDER.find((phase) => (status.phases[phase]?.status ?? "pending") !== "done");
|
|
2576
2595
|
const currentStatus = currentPhase ? status.phases[currentPhase]?.status ?? "pending" : null;
|
|
2577
2596
|
if (currentPhase && currentPhase !== "human_review" && (currentStatus === "pending" || currentStatus === "in_progress")) {
|
|
@@ -2634,6 +2653,13 @@ function classifyIdle(ctx, taskId, probeAlive = () => false, now = Date.now(), p
|
|
|
2634
2653
|
return { kind: "auto_block", state: "blocked", phase: blockedPhase, pid: ctx.resolvedPid ?? void 0 };
|
|
2635
2654
|
}
|
|
2636
2655
|
if (state === "human_review") {
|
|
2656
|
+
if (isFullSendTailPending(status)) {
|
|
2657
|
+
if (orchestratorLive && ctx.resolvedPid != null) {
|
|
2658
|
+
return { kind: "live", state, pid: ctx.resolvedPid };
|
|
2659
|
+
}
|
|
2660
|
+
const crashReason = findCrashEvidence(ctx, taskId, probeAlive, primaryTaskId, orchestratorLive);
|
|
2661
|
+
if (crashReason) return { kind: "death", state, pid: ctx.resolvedPid ?? void 0, reason: crashReason };
|
|
2662
|
+
}
|
|
2637
2663
|
const verdict = status.phases.code_review?.verdict || void 0;
|
|
2638
2664
|
return {
|
|
2639
2665
|
kind: "checkpoint",
|
|
@@ -2689,6 +2715,7 @@ function orchestratorStillProgressing(ctx, probeAlive, now = Date.now()) {
|
|
|
2689
2715
|
const status = ctx.statusResult.kind === "ok" && isStatusJson(ctx.statusResult.status) ? ctx.statusResult.status : null;
|
|
2690
2716
|
if (status == null) return false;
|
|
2691
2717
|
if (findFirstBlockedPhase(status) && !blockedPhaseLivenessTrusted(ctx, orchestratorLive, now)) return false;
|
|
2718
|
+
if (isFullSendTailPending(status)) return true;
|
|
2692
2719
|
const state = status.status;
|
|
2693
2720
|
return state !== "human_review" && state !== "complete";
|
|
2694
2721
|
}
|
|
@@ -3007,6 +3034,11 @@ function watchCmd(args2, deps = {}) {
|
|
|
3007
3034
|
return exit(0);
|
|
3008
3035
|
}
|
|
3009
3036
|
const idleResult = classifyIdle(freshCtx, taskId, isOrchestratorAlive, now(), rememberedPrimaryTaskId ?? void 0);
|
|
3037
|
+
if (idleResult.kind === "live") {
|
|
3038
|
+
reportPhasePointer(freshCtx);
|
|
3039
|
+
if (parsed.follow) tailRunLog(freshCtx, taskId, { stderr }, tailState, rememberedPrimaryTaskId ?? void 0);
|
|
3040
|
+
continue;
|
|
3041
|
+
}
|
|
3010
3042
|
if (idleResult.kind === "death" && idleResult.reason) {
|
|
3011
3043
|
stderr(`canon watch: ${idleResult.reason}; run \`canon run ${taskId}\` to resume
|
|
3012
3044
|
`);
|
|
@@ -3036,7 +3068,7 @@ function watchCmd(args2, deps = {}) {
|
|
|
3036
3068
|
}
|
|
3037
3069
|
|
|
3038
3070
|
// src/task/index.ts
|
|
3039
|
-
import { spawnSync as
|
|
3071
|
+
import { spawnSync as spawnSync9 } from "child_process";
|
|
3040
3072
|
import fs16 from "fs";
|
|
3041
3073
|
import path16 from "path";
|
|
3042
3074
|
|
|
@@ -3049,7 +3081,7 @@ function isInstalledSourcePath(sourcePath) {
|
|
|
3049
3081
|
return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
|
|
3050
3082
|
}
|
|
3051
3083
|
function resolveCanonVersion(explicit) {
|
|
3052
|
-
return explicit ?? "3.
|
|
3084
|
+
return explicit ?? "3.12.0" ?? "dev";
|
|
3053
3085
|
}
|
|
3054
3086
|
function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
|
|
3055
3087
|
const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
@@ -3183,6 +3215,7 @@ function isPristineTaskArtifact(content, basename2, taskId, title) {
|
|
|
3183
3215
|
}
|
|
3184
3216
|
|
|
3185
3217
|
// src/orchestrator/validation.ts
|
|
3218
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
3186
3219
|
import fs14 from "fs";
|
|
3187
3220
|
import path14 from "path";
|
|
3188
3221
|
function computeLatestValidationResults(handoffContent) {
|
|
@@ -3852,13 +3885,13 @@ function writeStatusAtomic(filePath, status) {
|
|
|
3852
3885
|
}
|
|
3853
3886
|
function runGit(args2, options = {}) {
|
|
3854
3887
|
if (options.stdio === "inherit") {
|
|
3855
|
-
return
|
|
3888
|
+
return spawnSync9("git", args2, {
|
|
3856
3889
|
cwd: options.cwd ?? process.cwd(),
|
|
3857
3890
|
encoding: "utf8",
|
|
3858
3891
|
stdio: "inherit"
|
|
3859
3892
|
});
|
|
3860
3893
|
}
|
|
3861
|
-
return
|
|
3894
|
+
return spawnSync9("git", args2, {
|
|
3862
3895
|
cwd: options.cwd ?? process.cwd(),
|
|
3863
3896
|
encoding: "utf8",
|
|
3864
3897
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4562,9 +4595,16 @@ function taskResetSpecReview(id) {
|
|
|
4562
4595
|
if (status.sessions && Object.hasOwn(status.sessions, "claude_spec")) {
|
|
4563
4596
|
delete status.sessions.claude_spec;
|
|
4564
4597
|
}
|
|
4598
|
+
const rearmGate = status.full_send !== true && status.human_spec_gate !== true;
|
|
4599
|
+
if (rearmGate) status.human_spec_gate = true;
|
|
4565
4600
|
status.updated = today();
|
|
4566
4601
|
writeStatusAtomic(statusPath, status);
|
|
4567
4602
|
console.log(`Reset ${id}: spec \u2192 done, spec_review \u2192 pending (iter=0, verdict cleared, claude_spec session dropped)`);
|
|
4603
|
+
if (rearmGate) {
|
|
4604
|
+
console.log(
|
|
4605
|
+
`Re-armed the spec gate: the next \`canon run ${id}\` stops after spec_review approves. To continue past it instead, run \`canon task set ${id} human_spec_gate false\` first.`
|
|
4606
|
+
);
|
|
4607
|
+
}
|
|
4568
4608
|
}
|
|
4569
4609
|
function taskResetCodeReview(id) {
|
|
4570
4610
|
if (!id) throw new Error("Error: usage: canon task reset-code-review <TASK-ID>");
|
|
@@ -4615,7 +4655,7 @@ function taskResetCodeReview(id) {
|
|
|
4615
4655
|
);
|
|
4616
4656
|
}
|
|
4617
4657
|
function ensureGitAvailable() {
|
|
4618
|
-
const result =
|
|
4658
|
+
const result = spawnSync9("git", ["--version"], { stdio: "ignore" });
|
|
4619
4659
|
if (result.error || result.status !== 0) {
|
|
4620
4660
|
throw new Error("Error: git is required.");
|
|
4621
4661
|
}
|
|
@@ -4996,7 +5036,7 @@ function taskCmd2(args2) {
|
|
|
4996
5036
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4997
5037
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4998
5038
|
import { dirname as dirname4, join as join4 } from "path";
|
|
4999
|
-
import { spawnSync as
|
|
5039
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
5000
5040
|
var packageDir3 = join4(dirname4(fileURLToPath4(import.meta.url)), "../..");
|
|
5001
5041
|
function detectInstallType(pkgDirOverride) {
|
|
5002
5042
|
const dir = pkgDirOverride ?? packageDir3;
|
|
@@ -5049,7 +5089,7 @@ function nonInteractiveSshCommand(configuredCommand) {
|
|
|
5049
5089
|
return `${command2} -oBatchMode=yes`;
|
|
5050
5090
|
}
|
|
5051
5091
|
function defaultGitRunner(args2) {
|
|
5052
|
-
const result =
|
|
5092
|
+
const result = spawnSync10("git", args2, {
|
|
5053
5093
|
encoding: "utf8",
|
|
5054
5094
|
timeout: GIT_RESOLUTION_TIMEOUT_MS,
|
|
5055
5095
|
env: {
|
|
@@ -5234,7 +5274,7 @@ function currentPinFromManifest(manifest) {
|
|
|
5234
5274
|
return "unknown";
|
|
5235
5275
|
}
|
|
5236
5276
|
function bakedVersion() {
|
|
5237
|
-
return "3.
|
|
5277
|
+
return "3.12.0";
|
|
5238
5278
|
}
|
|
5239
5279
|
function ownPackageName(pkgDir) {
|
|
5240
5280
|
try {
|
|
@@ -5246,7 +5286,7 @@ function ownPackageName(pkgDir) {
|
|
|
5246
5286
|
}
|
|
5247
5287
|
var NPM_VIEW_TIMEOUT_MS = 3e4;
|
|
5248
5288
|
function defaultNpmViewRunner(args2, cwd) {
|
|
5249
|
-
const result =
|
|
5289
|
+
const result = spawnSync10("npm", args2, { encoding: "utf8", timeout: NPM_VIEW_TIMEOUT_MS, ...cwd ? { cwd } : {} });
|
|
5250
5290
|
if (result.error) return { status: null, stdout: "", stderr: result.error.message };
|
|
5251
5291
|
return { status: result.status, stdout: result.stdout ?? "", stderr: (result.stderr ?? "").trim() };
|
|
5252
5292
|
}
|
|
@@ -5302,7 +5342,7 @@ function updateCmd(args2, deps = {}) {
|
|
|
5302
5342
|
const pkgDir = deps.packageDir ?? packageDir3;
|
|
5303
5343
|
const pkgName = ownPackageName(pkgDir);
|
|
5304
5344
|
const cwd = deps.cwd ?? process.cwd();
|
|
5305
|
-
const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) =>
|
|
5345
|
+
const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) => spawnSync10(cmd, cmdArgs, { stdio: "inherit", cwd: opts.cwd }));
|
|
5306
5346
|
const runGit2 = deps.gitRunner ?? defaultGitRunner;
|
|
5307
5347
|
const nowIso = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
5308
5348
|
let options;
|
|
@@ -5433,7 +5473,7 @@ function updateCmd(args2, deps = {}) {
|
|
|
5433
5473
|
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5434
5474
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5435
5475
|
import { basename, dirname as dirname5, join as join5, relative as relative3, resolve } from "path";
|
|
5436
|
-
import { spawnSync as
|
|
5476
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
5437
5477
|
|
|
5438
5478
|
// src/lib/canon-owned.ts
|
|
5439
5479
|
var CANON_OWNED = [
|
|
@@ -5542,7 +5582,7 @@ function classifyDestinations(cwd, relPaths) {
|
|
|
5542
5582
|
const classes = /* @__PURE__ */ new Map();
|
|
5543
5583
|
const uniqueRelPaths = [...new Set(relPaths)];
|
|
5544
5584
|
if (uniqueRelPaths.length === 0) return classes;
|
|
5545
|
-
const probe =
|
|
5585
|
+
const probe = spawnSync11("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
5546
5586
|
cwd,
|
|
5547
5587
|
encoding: "utf8",
|
|
5548
5588
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5554,12 +5594,12 @@ function classifyDestinations(cwd, relPaths) {
|
|
|
5554
5594
|
}
|
|
5555
5595
|
return classes;
|
|
5556
5596
|
}
|
|
5557
|
-
const lsFiles =
|
|
5597
|
+
const lsFiles = spawnSync11("git", ["ls-files", "-z", "--", ...uniqueRelPaths], {
|
|
5558
5598
|
cwd,
|
|
5559
5599
|
encoding: "utf8",
|
|
5560
5600
|
stdio: ["ignore", "pipe", "ignore"]
|
|
5561
5601
|
});
|
|
5562
|
-
const status =
|
|
5602
|
+
const status = spawnSync11("git", ["status", "--porcelain=v1", "-z", "--", ...uniqueRelPaths], {
|
|
5563
5603
|
cwd,
|
|
5564
5604
|
encoding: "utf8",
|
|
5565
5605
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5711,7 +5751,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5711
5751
|
cutoverWarnings.push(docsRefsCheckRel);
|
|
5712
5752
|
}
|
|
5713
5753
|
const versionPath = join5(cwd, ".canon", "version");
|
|
5714
|
-
const newVersion = "3.
|
|
5754
|
+
const newVersion = "3.12.0";
|
|
5715
5755
|
const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
|
|
5716
5756
|
if (currentVersion !== newVersion) {
|
|
5717
5757
|
pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
|
|
@@ -5864,7 +5904,7 @@ function upgradeCmd(args2) {
|
|
|
5864
5904
|
process.exit(2);
|
|
5865
5905
|
}
|
|
5866
5906
|
if (upgraded.length > 0 && !options.noStage) {
|
|
5867
|
-
const r =
|
|
5907
|
+
const r = spawnSync11("git", ["add", ...upgraded], { cwd: process.cwd(), stdio: "inherit" });
|
|
5868
5908
|
if (r.status !== 0) {
|
|
5869
5909
|
console.error("\nwarning: failed to stage changes \u2014 run `git add` manually.");
|
|
5870
5910
|
}
|
|
@@ -5992,14 +6032,17 @@ canon run options:
|
|
|
5992
6032
|
protected/configured docs listed in spec.md's "### Affected Files"
|
|
5993
6033
|
table (all admitted docs once QA is done). Dirty files outside
|
|
5994
6034
|
that set die with a remediation message.
|
|
5995
|
-
Aborts if
|
|
5996
|
-
spec's Affected Files (bypass with --force).
|
|
6035
|
+
Aborts if merging origin/<base> conflicts, or branch changes files
|
|
6036
|
+
outside spec's Affected Files (bypass with --force).
|
|
5997
6037
|
--push At human_review: push branch only, no PR (requires gh). Same allow-list
|
|
5998
|
-
as --pr. Aborts if
|
|
5999
|
-
|
|
6038
|
+
as --pr. Aborts if merging origin/<base> conflicts, or branch changes
|
|
6039
|
+
files outside spec's Affected Files (bypass with --force).
|
|
6000
6040
|
--full-send Skip the spec gate and auto-open a draft PR after clean QA
|
|
6001
|
-
--force Acknowledge high-commitment combinations
|
|
6002
|
-
|
|
6041
|
+
--force Acknowledge high-commitment combinations and bypass
|
|
6042
|
+
explicit safety gates where documented (currently:
|
|
6043
|
+
--full-send on delicate tasks, reroute amendment gate,
|
|
6044
|
+
base-drift gate, and dirty REPO_ROOT worktree-start gate)
|
|
6045
|
+
--ship Merge the open PR (gh pr merge --squash), delete its remote branch, tear
|
|
6003
6046
|
down the worktree, archive the task dir, and pull the base branch. Run
|
|
6004
6047
|
after the PR is approved \u2014 do NOT merge the PR manually first. If you
|
|
6005
6048
|
already merged externally, --ship detects the merged state and resumes
|
|
@@ -6020,7 +6063,7 @@ Global:
|
|
|
6020
6063
|
`);
|
|
6021
6064
|
}
|
|
6022
6065
|
function printVersion() {
|
|
6023
|
-
console.log("3.
|
|
6066
|
+
console.log("3.12.0");
|
|
6024
6067
|
}
|
|
6025
6068
|
switch (command) {
|
|
6026
6069
|
case "doctor":
|