canon-ai 3.11.0 → 3.13.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 +32 -0
- package/README.md +1 -1
- package/dist/cli/index.js +69 -25
- package/dist/orchestrator/run-task.js +738 -212
- package/package.json +1 -1
- package/templates/.canon/templates/handoff.md +17 -1
- package/templates/.canon/templates/review.md +1 -0
- package/templates/.claude/agents/code-review-anchored.md +2 -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-invocations.md +1 -0
- package/templates/docs/pipeline-orchestrator.md +16 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.13.0] — 2026-10-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Bug-fix tasks record red-first evidence in the handoff, and code review checks for it.** The handoff template has a Red-First Evidence section with one row per red-first acceptance criterion: the test command, the commit the pre-fix code was at, the failure it showed there, and the post-fix result. A `[wrong-premise]` blocker can stand in when the test cannot be made to fail for the reason the spec states. The implement prompts point at the section and say which pre-fix state to run against. The anchored code-review lens flags a red-first criterion with no pre-fix run as a spec-compliance gap, and a revision round can close the gap by running the test against the pre-fix commit without redoing the fix.
|
|
12
|
+
- **Claude sub-agents get their own metrics rows.** When a pipeline phase's Claude session spawns sub-agents, each top-level sub-agent gets a row with Agent `claude:<agent type>`, its duration, and its own cumulative tokens. For `code_review` that separates the anchored and cold reviewers from the foreman. The session's own row is unchanged.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The spec_gap halt's fix hint names the amendment heading the next `--reroute` requires.** After a task's first reroute, it says `## Amendment Round N` instead of a bare `## Amendment` that `--reroute` refuses.
|
|
17
|
+
- **Implement auto-commit no longer fails on a gitignored generated file listed beside its generator.** The ignored file is not passed to `git add`. If staging fails partway, the index is restored to its state before the attempt.
|
|
18
|
+
- **A handoff row for one of the task's own artifacts no longer fails the coverage checks** when the pipeline rewrote it identical to the base. Another bundle member's artifact is still rejected.
|
|
19
|
+
- **The scaffold commit no longer fails when a telemetry file was removed with `git rm`**, and a committed rename's old path counts as deletion evidence at implement close.
|
|
20
|
+
- **The base-divergence and orphaned-branch messages name the actual branch** instead of a literal `<base>`, quoted when the name needs it, so the printed `git push` and `gh pr create` commands can be pasted as is.
|
|
21
|
+
|
|
22
|
+
## [3.12.0] — 2026-10-21
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **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.
|
|
27
|
+
- **`--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.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **`--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.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`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.
|
|
36
|
+
- **`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.
|
|
37
|
+
- **`--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.
|
|
38
|
+
|
|
7
39
|
## [3.11.0] — 2026-10-20
|
|
8
40
|
|
|
9
41
|
### 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.13.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.13.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.13.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) {
|
|
@@ -3683,6 +3716,7 @@ function verifyHandoffAgainstDiffFromData(taskIds, inputs) {
|
|
|
3683
3716
|
const files = handoffFilesByTask.get(taskId) ?? [];
|
|
3684
3717
|
for (const filePath of files) {
|
|
3685
3718
|
if (gitIgnored.has(filePath)) continue;
|
|
3719
|
+
if (isPipelineOwnedTaskArtifact(filePath, [taskId])) continue;
|
|
3686
3720
|
if (!coveredPaths.has(filePath)) {
|
|
3687
3721
|
issues.push(`[${taskId}] handoff\u2192diff: ${filePath} listed in handoff but not in diff \u2014 it has no committed change against the task base. If it was untouched or rewritten with identical content, drop the row`);
|
|
3688
3722
|
}
|
|
@@ -3852,13 +3886,13 @@ function writeStatusAtomic(filePath, status) {
|
|
|
3852
3886
|
}
|
|
3853
3887
|
function runGit(args2, options = {}) {
|
|
3854
3888
|
if (options.stdio === "inherit") {
|
|
3855
|
-
return
|
|
3889
|
+
return spawnSync9("git", args2, {
|
|
3856
3890
|
cwd: options.cwd ?? process.cwd(),
|
|
3857
3891
|
encoding: "utf8",
|
|
3858
3892
|
stdio: "inherit"
|
|
3859
3893
|
});
|
|
3860
3894
|
}
|
|
3861
|
-
return
|
|
3895
|
+
return spawnSync9("git", args2, {
|
|
3862
3896
|
cwd: options.cwd ?? process.cwd(),
|
|
3863
3897
|
encoding: "utf8",
|
|
3864
3898
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4562,9 +4596,16 @@ function taskResetSpecReview(id) {
|
|
|
4562
4596
|
if (status.sessions && Object.hasOwn(status.sessions, "claude_spec")) {
|
|
4563
4597
|
delete status.sessions.claude_spec;
|
|
4564
4598
|
}
|
|
4599
|
+
const rearmGate = status.full_send !== true && status.human_spec_gate !== true;
|
|
4600
|
+
if (rearmGate) status.human_spec_gate = true;
|
|
4565
4601
|
status.updated = today();
|
|
4566
4602
|
writeStatusAtomic(statusPath, status);
|
|
4567
4603
|
console.log(`Reset ${id}: spec \u2192 done, spec_review \u2192 pending (iter=0, verdict cleared, claude_spec session dropped)`);
|
|
4604
|
+
if (rearmGate) {
|
|
4605
|
+
console.log(
|
|
4606
|
+
`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.`
|
|
4607
|
+
);
|
|
4608
|
+
}
|
|
4568
4609
|
}
|
|
4569
4610
|
function taskResetCodeReview(id) {
|
|
4570
4611
|
if (!id) throw new Error("Error: usage: canon task reset-code-review <TASK-ID>");
|
|
@@ -4615,7 +4656,7 @@ function taskResetCodeReview(id) {
|
|
|
4615
4656
|
);
|
|
4616
4657
|
}
|
|
4617
4658
|
function ensureGitAvailable() {
|
|
4618
|
-
const result =
|
|
4659
|
+
const result = spawnSync9("git", ["--version"], { stdio: "ignore" });
|
|
4619
4660
|
if (result.error || result.status !== 0) {
|
|
4620
4661
|
throw new Error("Error: git is required.");
|
|
4621
4662
|
}
|
|
@@ -4996,7 +5037,7 @@ function taskCmd2(args2) {
|
|
|
4996
5037
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4997
5038
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4998
5039
|
import { dirname as dirname4, join as join4 } from "path";
|
|
4999
|
-
import { spawnSync as
|
|
5040
|
+
import { spawnSync as spawnSync10 } from "child_process";
|
|
5000
5041
|
var packageDir3 = join4(dirname4(fileURLToPath4(import.meta.url)), "../..");
|
|
5001
5042
|
function detectInstallType(pkgDirOverride) {
|
|
5002
5043
|
const dir = pkgDirOverride ?? packageDir3;
|
|
@@ -5049,7 +5090,7 @@ function nonInteractiveSshCommand(configuredCommand) {
|
|
|
5049
5090
|
return `${command2} -oBatchMode=yes`;
|
|
5050
5091
|
}
|
|
5051
5092
|
function defaultGitRunner(args2) {
|
|
5052
|
-
const result =
|
|
5093
|
+
const result = spawnSync10("git", args2, {
|
|
5053
5094
|
encoding: "utf8",
|
|
5054
5095
|
timeout: GIT_RESOLUTION_TIMEOUT_MS,
|
|
5055
5096
|
env: {
|
|
@@ -5234,7 +5275,7 @@ function currentPinFromManifest(manifest) {
|
|
|
5234
5275
|
return "unknown";
|
|
5235
5276
|
}
|
|
5236
5277
|
function bakedVersion() {
|
|
5237
|
-
return "3.
|
|
5278
|
+
return "3.13.0";
|
|
5238
5279
|
}
|
|
5239
5280
|
function ownPackageName(pkgDir) {
|
|
5240
5281
|
try {
|
|
@@ -5246,7 +5287,7 @@ function ownPackageName(pkgDir) {
|
|
|
5246
5287
|
}
|
|
5247
5288
|
var NPM_VIEW_TIMEOUT_MS = 3e4;
|
|
5248
5289
|
function defaultNpmViewRunner(args2, cwd) {
|
|
5249
|
-
const result =
|
|
5290
|
+
const result = spawnSync10("npm", args2, { encoding: "utf8", timeout: NPM_VIEW_TIMEOUT_MS, ...cwd ? { cwd } : {} });
|
|
5250
5291
|
if (result.error) return { status: null, stdout: "", stderr: result.error.message };
|
|
5251
5292
|
return { status: result.status, stdout: result.stdout ?? "", stderr: (result.stderr ?? "").trim() };
|
|
5252
5293
|
}
|
|
@@ -5302,7 +5343,7 @@ function updateCmd(args2, deps = {}) {
|
|
|
5302
5343
|
const pkgDir = deps.packageDir ?? packageDir3;
|
|
5303
5344
|
const pkgName = ownPackageName(pkgDir);
|
|
5304
5345
|
const cwd = deps.cwd ?? process.cwd();
|
|
5305
|
-
const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) =>
|
|
5346
|
+
const spawn2 = deps.spawnRunner ?? ((cmd, cmdArgs, opts) => spawnSync10(cmd, cmdArgs, { stdio: "inherit", cwd: opts.cwd }));
|
|
5306
5347
|
const runGit2 = deps.gitRunner ?? defaultGitRunner;
|
|
5307
5348
|
const nowIso = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
5308
5349
|
let options;
|
|
@@ -5433,7 +5474,7 @@ function updateCmd(args2, deps = {}) {
|
|
|
5433
5474
|
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5434
5475
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5435
5476
|
import { basename, dirname as dirname5, join as join5, relative as relative3, resolve } from "path";
|
|
5436
|
-
import { spawnSync as
|
|
5477
|
+
import { spawnSync as spawnSync11 } from "child_process";
|
|
5437
5478
|
|
|
5438
5479
|
// src/lib/canon-owned.ts
|
|
5439
5480
|
var CANON_OWNED = [
|
|
@@ -5542,7 +5583,7 @@ function classifyDestinations(cwd, relPaths) {
|
|
|
5542
5583
|
const classes = /* @__PURE__ */ new Map();
|
|
5543
5584
|
const uniqueRelPaths = [...new Set(relPaths)];
|
|
5544
5585
|
if (uniqueRelPaths.length === 0) return classes;
|
|
5545
|
-
const probe =
|
|
5586
|
+
const probe = spawnSync11("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
5546
5587
|
cwd,
|
|
5547
5588
|
encoding: "utf8",
|
|
5548
5589
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5554,12 +5595,12 @@ function classifyDestinations(cwd, relPaths) {
|
|
|
5554
5595
|
}
|
|
5555
5596
|
return classes;
|
|
5556
5597
|
}
|
|
5557
|
-
const lsFiles =
|
|
5598
|
+
const lsFiles = spawnSync11("git", ["ls-files", "-z", "--", ...uniqueRelPaths], {
|
|
5558
5599
|
cwd,
|
|
5559
5600
|
encoding: "utf8",
|
|
5560
5601
|
stdio: ["ignore", "pipe", "ignore"]
|
|
5561
5602
|
});
|
|
5562
|
-
const status =
|
|
5603
|
+
const status = spawnSync11("git", ["status", "--porcelain=v1", "-z", "--", ...uniqueRelPaths], {
|
|
5563
5604
|
cwd,
|
|
5564
5605
|
encoding: "utf8",
|
|
5565
5606
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5711,7 +5752,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5711
5752
|
cutoverWarnings.push(docsRefsCheckRel);
|
|
5712
5753
|
}
|
|
5713
5754
|
const versionPath = join5(cwd, ".canon", "version");
|
|
5714
|
-
const newVersion = "3.
|
|
5755
|
+
const newVersion = "3.13.0";
|
|
5715
5756
|
const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
|
|
5716
5757
|
if (currentVersion !== newVersion) {
|
|
5717
5758
|
pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
|
|
@@ -5864,7 +5905,7 @@ function upgradeCmd(args2) {
|
|
|
5864
5905
|
process.exit(2);
|
|
5865
5906
|
}
|
|
5866
5907
|
if (upgraded.length > 0 && !options.noStage) {
|
|
5867
|
-
const r =
|
|
5908
|
+
const r = spawnSync11("git", ["add", ...upgraded], { cwd: process.cwd(), stdio: "inherit" });
|
|
5868
5909
|
if (r.status !== 0) {
|
|
5869
5910
|
console.error("\nwarning: failed to stage changes \u2014 run `git add` manually.");
|
|
5870
5911
|
}
|
|
@@ -5992,14 +6033,17 @@ canon run options:
|
|
|
5992
6033
|
protected/configured docs listed in spec.md's "### Affected Files"
|
|
5993
6034
|
table (all admitted docs once QA is done). Dirty files outside
|
|
5994
6035
|
that set die with a remediation message.
|
|
5995
|
-
Aborts if
|
|
5996
|
-
spec's Affected Files (bypass with --force).
|
|
6036
|
+
Aborts if merging origin/<base> conflicts, or branch changes files
|
|
6037
|
+
outside spec's Affected Files (bypass with --force).
|
|
5997
6038
|
--push At human_review: push branch only, no PR (requires gh). Same allow-list
|
|
5998
|
-
as --pr. Aborts if
|
|
5999
|
-
|
|
6039
|
+
as --pr. Aborts if merging origin/<base> conflicts, or branch changes
|
|
6040
|
+
files outside spec's Affected Files (bypass with --force).
|
|
6000
6041
|
--full-send Skip the spec gate and auto-open a draft PR after clean QA
|
|
6001
|
-
--force Acknowledge high-commitment combinations
|
|
6002
|
-
|
|
6042
|
+
--force Acknowledge high-commitment combinations and bypass
|
|
6043
|
+
explicit safety gates where documented (currently:
|
|
6044
|
+
--full-send on delicate tasks, reroute amendment gate,
|
|
6045
|
+
base-drift gate, and dirty REPO_ROOT worktree-start gate)
|
|
6046
|
+
--ship Merge the open PR (gh pr merge --squash), delete its remote branch, tear
|
|
6003
6047
|
down the worktree, archive the task dir, and pull the base branch. Run
|
|
6004
6048
|
after the PR is approved \u2014 do NOT merge the PR manually first. If you
|
|
6005
6049
|
already merged externally, --ship detects the merged state and resumes
|
|
@@ -6020,7 +6064,7 @@ Global:
|
|
|
6020
6064
|
`);
|
|
6021
6065
|
}
|
|
6022
6066
|
function printVersion() {
|
|
6023
|
-
console.log("3.
|
|
6067
|
+
console.log("3.13.0");
|
|
6024
6068
|
}
|
|
6025
6069
|
switch (command) {
|
|
6026
6070
|
case "doctor":
|