canon-ai 3.6.0 → 3.8.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/dist/cli/index.js +57 -21
- package/dist/orchestrator/run-task.js +2 -2
- package/package.json +2 -2
- package/templates/.claude/agents/spec-review-lens.md +1 -1
- package/templates/.claude/skills/canon-spec/SKILL.md +3 -3
- package/templates/.claude/skills/canon-spec-review/SKILL.md +1 -1
- package/templates/docs/pipeline-orchestrator.md +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.0] — 2026-10-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`/canon-spec-review`'s factual reviewer now runs on Haiku.** The reviewer that checks a spec's paths, symbols, and signatures against the code now runs on `haiku`. Its work is lookups, so the smaller model is enough and costs less. The shape and spec-quality reviewers still use your session's model. Run `canon upgrade`, then start a new Claude Code session to pick it up.
|
|
12
|
+
|
|
13
|
+
## [3.7.0] — 2026-10-06
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`canon task set <id> human_spec_gate true|false` arms or disarms the human spec gate.** You no longer need to hand-edit `status.json`. Arming the gate on a full-send task also clears `full_send`, because the gate never fires on a full-send task, and prints how to turn full-send back on (`canon run --full-send <id>`). If the task's `spec_review` is already done, it prints a note that the gate fires again only after a full-tier `canon run --reroute <id>`. Disarming is refused on delicate tasks; `canon run --full-send --force <id>` is still the only way to skip the gate there. `canon task new` now points at `canon task set` instead of telling you to edit `status.json`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **The `full_send` refusal message now says how the flag clears.** It stays in `status.json` until `--reroute` or arming the spec gate clears it.
|
|
22
|
+
- **The npm package's homepage now links to <https://timothystraub.com/canon>.**
|
|
23
|
+
|
|
7
24
|
## [3.6.0] — 2026-10-04
|
|
8
25
|
|
|
9
26
|
### Changed
|
package/dist/cli/index.js
CHANGED
|
@@ -1224,7 +1224,7 @@ function checkTemplates(cwd) {
|
|
|
1224
1224
|
}
|
|
1225
1225
|
function checkCanonVersion(cwd) {
|
|
1226
1226
|
const versionPath = join(cwd, ".canon", "version");
|
|
1227
|
-
const installedVersion = "3.
|
|
1227
|
+
const installedVersion = "3.8.0";
|
|
1228
1228
|
if (!existsSync(versionPath)) {
|
|
1229
1229
|
return { label: ".canon/version", status: "warn", detail: "missing \u2014 run `canon upgrade`" };
|
|
1230
1230
|
}
|
|
@@ -1721,7 +1721,7 @@ function initCmd(_args) {
|
|
|
1721
1721
|
}
|
|
1722
1722
|
function writeCanonVersion(cwd) {
|
|
1723
1723
|
const versionPath = join2(cwd, ".canon", "version");
|
|
1724
|
-
const version = "3.
|
|
1724
|
+
const version = "3.8.0";
|
|
1725
1725
|
mkdirSync(dirname2(versionPath), { recursive: true });
|
|
1726
1726
|
writeFileSync(versionPath, version + "\n");
|
|
1727
1727
|
}
|
|
@@ -2731,7 +2731,7 @@ function isInstalledSourcePath(sourcePath) {
|
|
|
2731
2731
|
return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
|
|
2732
2732
|
}
|
|
2733
2733
|
function resolveCanonVersion(explicit) {
|
|
2734
|
-
return explicit ?? "3.
|
|
2734
|
+
return explicit ?? "3.8.0" ?? "dev";
|
|
2735
2735
|
}
|
|
2736
2736
|
function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
|
|
2737
2737
|
const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
@@ -3447,17 +3447,21 @@ function rescaffoldReview(taskDir, identity) {
|
|
|
3447
3447
|
}
|
|
3448
3448
|
}
|
|
3449
3449
|
|
|
3450
|
+
// src/lib/pipeline-policy.ts
|
|
3451
|
+
function isPlanCombined(task) {
|
|
3452
|
+
return task.task_size === "XS" && !(task.delicate ?? false);
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3450
3455
|
// src/task/index.ts
|
|
3451
3456
|
var VALID_PHASES = new Set(PHASE_ORDER);
|
|
3452
3457
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "done", "changes_requested", "blocked"]);
|
|
3453
3458
|
var VALID_VERDICTS = /* @__PURE__ */ new Set(["approved", "approved_with_nits", "changes_requested", "needs_re_review", "spec_gap", "sanctioned"]);
|
|
3454
3459
|
var REVIEW_PHASES = /* @__PURE__ */ new Set(["spec_review", "code_review"]);
|
|
3455
|
-
var SETTABLE_FIELDS = ["title", "task_size", "delicate", "worktree", "base_branch"];
|
|
3460
|
+
var SETTABLE_FIELDS = ["title", "task_size", "delicate", "worktree", "base_branch", "human_spec_gate"];
|
|
3456
3461
|
var SETTABLE_FIELD_SET = new Set(SETTABLE_FIELDS);
|
|
3457
3462
|
var IMMUTABLE_FIELDS = /* @__PURE__ */ new Set(["id", "created", "updated"]);
|
|
3458
3463
|
var REDIRECT_MESSAGES = {
|
|
3459
|
-
full_send: "
|
|
3460
|
-
human_spec_gate: "the spec gate is self-clearing. Re-run `canon run <id>` to proceed past it, or use `canon run --full-send <id>` to skip it entirely.",
|
|
3464
|
+
full_send: "persisted in status.json until `canon run --reroute` clears it or arming the gate with `canon task set <id> human_spec_gate true` does. Use `canon run --full-send <id>` to enable it; the delicate-task guard requires `--force`.",
|
|
3461
3465
|
status: "derived from phase states. Use `canon task phase <id> <phase> <status>`.",
|
|
3462
3466
|
branch: "load-bearing git identity; retargeting it desyncs the worktree. Not settable via `canon task set`.",
|
|
3463
3467
|
phases: "nested orchestrator-owned state. Use `canon task phase`, `canon task reset-spec-review`, `canon task reset-code-review`, or `canon task accept` instead.",
|
|
@@ -3568,7 +3572,7 @@ function listTemplateFiles() {
|
|
|
3568
3572
|
}
|
|
3569
3573
|
return fs14.readdirSync(root).filter((name) => name.endsWith(".md") || name.endsWith(".json")).sort();
|
|
3570
3574
|
}
|
|
3571
|
-
function printCreatedTask(taskDir, baseBranch) {
|
|
3575
|
+
function printCreatedTask(taskId, taskDir, baseBranch) {
|
|
3572
3576
|
console.log(`Created task: ${taskDir}`);
|
|
3573
3577
|
console.log("Files:");
|
|
3574
3578
|
for (const file of fs14.readdirSync(taskDir).sort()) {
|
|
@@ -3578,7 +3582,7 @@ function printCreatedTask(taskDir, baseBranch) {
|
|
|
3578
3582
|
console.log(`Next: Write the spec in ${taskDir}/spec.md`);
|
|
3579
3583
|
console.log("");
|
|
3580
3584
|
console.log(` Defaults: task_size=M, delicate=false, human_spec_gate=true, base_branch=${baseBranch}`);
|
|
3581
|
-
console.log(`
|
|
3585
|
+
console.log(` Adjust with: canon task set ${taskId} <field> <value>`);
|
|
3582
3586
|
}
|
|
3583
3587
|
function taskNew(args2) {
|
|
3584
3588
|
let id = "";
|
|
@@ -3634,7 +3638,7 @@ function taskNew(args2) {
|
|
|
3634
3638
|
const message = error instanceof Error ? error.message : String(error);
|
|
3635
3639
|
console.error(`Warning: created task without canon snapshot refresh: ${message}`);
|
|
3636
3640
|
}
|
|
3637
|
-
printCreatedTask(taskDir, baseBranch);
|
|
3641
|
+
printCreatedTask(id, taskDir, baseBranch);
|
|
3638
3642
|
}
|
|
3639
3643
|
function derivePhase(status) {
|
|
3640
3644
|
return deriveTopLevelStatus(status);
|
|
@@ -4470,13 +4474,13 @@ function taskSetValue(taskId, field, value, status) {
|
|
|
4470
4474
|
throw new Error("Error: title must be single-line (no embedded newlines).");
|
|
4471
4475
|
}
|
|
4472
4476
|
status.title = value;
|
|
4473
|
-
return;
|
|
4477
|
+
return [];
|
|
4474
4478
|
case "task_size":
|
|
4475
4479
|
if (!TASK_SIZE_VALUES.has(value)) {
|
|
4476
4480
|
throw new Error(`Error: invalid task_size '${value}'. Must be one of: XS, S, M, L, XL.`);
|
|
4477
4481
|
}
|
|
4478
4482
|
status.task_size = value;
|
|
4479
|
-
return;
|
|
4483
|
+
return [];
|
|
4480
4484
|
case "delicate":
|
|
4481
4485
|
case "worktree": {
|
|
4482
4486
|
const normalized = value.toLowerCase();
|
|
@@ -4484,7 +4488,26 @@ function taskSetValue(taskId, field, value, status) {
|
|
|
4484
4488
|
throw new Error(`Error: invalid ${field} '${value}'. Must be true or false.`);
|
|
4485
4489
|
}
|
|
4486
4490
|
status[field] = normalized === "true";
|
|
4487
|
-
return;
|
|
4491
|
+
return [];
|
|
4492
|
+
}
|
|
4493
|
+
case "human_spec_gate": {
|
|
4494
|
+
const normalized = value.toLowerCase();
|
|
4495
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
4496
|
+
throw new Error(`Error: invalid ${field} '${value}'. Must be true or false.`);
|
|
4497
|
+
}
|
|
4498
|
+
if (normalized === "false") {
|
|
4499
|
+
if (status.delicate === true) {
|
|
4500
|
+
throw new Error(`Error: human_spec_gate cannot be disarmed on delicate task ${taskId}. Use \`canon run --full-send --force ${taskId}\` to skip the spec gate on a delicate task, or re-run \`canon run ${taskId}\` to proceed past a gate that already halted.`);
|
|
4501
|
+
}
|
|
4502
|
+
status.human_spec_gate = false;
|
|
4503
|
+
return [];
|
|
4504
|
+
}
|
|
4505
|
+
status.human_spec_gate = true;
|
|
4506
|
+
if (status.full_send === true) {
|
|
4507
|
+
status.full_send = false;
|
|
4508
|
+
return [`Note: full-send was cleared on task ${taskId} because arming the spec gate re-introduces a human checkpoint. Run \`canon run --full-send ${taskId}\` to re-enable it.`];
|
|
4509
|
+
}
|
|
4510
|
+
return [];
|
|
4488
4511
|
}
|
|
4489
4512
|
case "base_branch": {
|
|
4490
4513
|
const trimmed = value.trim();
|
|
@@ -4493,7 +4516,7 @@ function taskSetValue(taskId, field, value, status) {
|
|
|
4493
4516
|
}
|
|
4494
4517
|
validateBranchField(trimmed, taskId, "base_branch");
|
|
4495
4518
|
status.base_branch = trimmed;
|
|
4496
|
-
return;
|
|
4519
|
+
return [];
|
|
4497
4520
|
}
|
|
4498
4521
|
default:
|
|
4499
4522
|
throw new Error(`Error: internal error \u2014 unsupported settable field '${field}'.`);
|
|
@@ -4502,6 +4525,16 @@ function taskSetValue(taskId, field, value, status) {
|
|
|
4502
4525
|
function taskSetRedirectMessage(field) {
|
|
4503
4526
|
return REDIRECT_MESSAGES[field] ?? "nested orchestrator-owned state. Use the owning canon task command instead.";
|
|
4504
4527
|
}
|
|
4528
|
+
var REROUTE_ADMITTED_PHASES = /* @__PURE__ */ new Set(["code_review", "qa", "human_review"]);
|
|
4529
|
+
function specGateDoneNote(id, status) {
|
|
4530
|
+
if (isPlanCombined(status)) {
|
|
4531
|
+
return `Note: spec_review is already done on task ${id}. A fast-tier reroute skips spec_review, so the armed gate fires again only if this task is rerouted in a bundle with a full-tier task.`;
|
|
4532
|
+
}
|
|
4533
|
+
if (REROUTE_ADMITTED_PHASES.has(deriveTopLevelStatus(status))) {
|
|
4534
|
+
return `Note: spec_review is already done on task ${id}. The armed gate fires after the next spec_review, which a full-tier reroute runs: add an amendment to the spec, then run \`canon run --reroute ${id}\`.`;
|
|
4535
|
+
}
|
|
4536
|
+
return `Note: spec_review is already done on task ${id}, so the armed gate will not fire on this pass. On a full-tier task it can fire again only if an amendment is rerouted once the task reaches code_review, qa, or human_review.`;
|
|
4537
|
+
}
|
|
4505
4538
|
function taskHasStarted(status) {
|
|
4506
4539
|
return Object.values(status.phases).some((entry) => (entry?.status ?? "pending") !== "pending");
|
|
4507
4540
|
}
|
|
@@ -4527,10 +4560,16 @@ function taskSet(args2) {
|
|
|
4527
4560
|
);
|
|
4528
4561
|
}
|
|
4529
4562
|
if (SETTABLE_FIELD_SET.has(field)) {
|
|
4530
|
-
taskSetValue(id, field, value, status);
|
|
4563
|
+
const notes = taskSetValue(id, field, value, status);
|
|
4531
4564
|
status.updated = today();
|
|
4532
4565
|
writeStatusAtomic(statusPath, status);
|
|
4533
|
-
|
|
4566
|
+
for (const note of notes) console.log(note);
|
|
4567
|
+
const specReviewDone = status.phases.spec_review?.status === "done";
|
|
4568
|
+
if (field === "human_spec_gate" && specReviewDone) {
|
|
4569
|
+
if (status.human_spec_gate === true) {
|
|
4570
|
+
console.log(specGateDoneNote(id, status));
|
|
4571
|
+
}
|
|
4572
|
+
} else if (taskHasStarted(status)) {
|
|
4534
4573
|
console.log(`Warning: ${field} on task ${id} takes effect on the next canon run.`);
|
|
4535
4574
|
}
|
|
4536
4575
|
return;
|
|
@@ -4538,9 +4577,6 @@ function taskSet(args2) {
|
|
|
4538
4577
|
if (field === "full_send") {
|
|
4539
4578
|
throw new Error(`Error: full_send is ${taskSetRedirectMessage(field)}`);
|
|
4540
4579
|
}
|
|
4541
|
-
if (field === "human_spec_gate") {
|
|
4542
|
-
throw new Error(`Error: human_spec_gate is ${taskSetRedirectMessage(field)}`);
|
|
4543
|
-
}
|
|
4544
4580
|
if (field === "status") {
|
|
4545
4581
|
throw new Error(`Error: status is ${taskSetRedirectMessage(field)}`);
|
|
4546
4582
|
}
|
|
@@ -4857,7 +4893,7 @@ function currentPinFromManifest(manifest) {
|
|
|
4857
4893
|
return "unknown";
|
|
4858
4894
|
}
|
|
4859
4895
|
function bakedVersion() {
|
|
4860
|
-
return "3.
|
|
4896
|
+
return "3.8.0";
|
|
4861
4897
|
}
|
|
4862
4898
|
function ownPackageName(pkgDir) {
|
|
4863
4899
|
try {
|
|
@@ -5334,7 +5370,7 @@ function runUpgrade(cwd, pkgDir, options = {}) {
|
|
|
5334
5370
|
cutoverWarnings.push(docsRefsCheckRel);
|
|
5335
5371
|
}
|
|
5336
5372
|
const versionPath = join5(cwd, ".canon", "version");
|
|
5337
|
-
const newVersion = "3.
|
|
5373
|
+
const newVersion = "3.8.0";
|
|
5338
5374
|
const currentVersion = existsSync5(versionPath) ? readFileSync4(versionPath, "utf8").trim() : null;
|
|
5339
5375
|
if (currentVersion !== newVersion) {
|
|
5340
5376
|
pending.push({ rel: ".canon/version", projectPath: versionPath, content: newVersion + "\n" });
|
|
@@ -5642,7 +5678,7 @@ Global:
|
|
|
5642
5678
|
`);
|
|
5643
5679
|
}
|
|
5644
5680
|
function printVersion() {
|
|
5645
|
-
console.log("3.
|
|
5681
|
+
console.log("3.8.0");
|
|
5646
5682
|
}
|
|
5647
5683
|
switch (command) {
|
|
5648
5684
|
case "doctor":
|
|
@@ -5205,7 +5205,7 @@ function isInstalledSourcePath(sourcePath) {
|
|
|
5205
5205
|
return sourcePath.includes("/node_modules/") || sourcePath.includes("\\node_modules\\") || sourcePath.includes("/_npx/") || sourcePath.includes("\\_npx\\");
|
|
5206
5206
|
}
|
|
5207
5207
|
function resolveCanonVersion(explicit) {
|
|
5208
|
-
return explicit ?? "3.
|
|
5208
|
+
return explicit ?? "3.8.0" ?? "dev";
|
|
5209
5209
|
}
|
|
5210
5210
|
function resolveOrchestratorCommit(repoRoot, upstreamCommit, runGitAt) {
|
|
5211
5211
|
const ownToplevel = captureGitOutput(repoRoot, ["rev-parse", "--show-toplevel"], runGitAt);
|
|
@@ -5592,7 +5592,7 @@ var VALID_PHASES = new Set(PHASE_ORDER);
|
|
|
5592
5592
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "done", "changes_requested", "blocked"]);
|
|
5593
5593
|
var VALID_VERDICTS = /* @__PURE__ */ new Set(["approved", "approved_with_nits", "changes_requested", "needs_re_review", "spec_gap", "sanctioned"]);
|
|
5594
5594
|
var REVIEW_PHASES = /* @__PURE__ */ new Set(["spec_review", "code_review"]);
|
|
5595
|
-
var SETTABLE_FIELDS = ["title", "task_size", "delicate", "worktree", "base_branch"];
|
|
5595
|
+
var SETTABLE_FIELDS = ["title", "task_size", "delicate", "worktree", "base_branch", "human_spec_gate"];
|
|
5596
5596
|
var SETTABLE_FIELD_SET = new Set(SETTABLE_FIELDS);
|
|
5597
5597
|
function today() {
|
|
5598
5598
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canon-ai",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A spec-first, multi-agent AI coding pipeline you drop into any repo. The law your agents work under.",
|
|
6
6
|
"engines": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"author": "Tim Straub",
|
|
44
|
-
"homepage": "https://
|
|
44
|
+
"homepage": "https://timothystraub.com/canon",
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
|
47
47
|
"url": "git+https://github.com/tstraub89/canon-ai.git"
|
|
@@ -10,7 +10,7 @@ Review the spec yourself and spawn no sub-agents.
|
|
|
10
10
|
Invoke no skills.
|
|
11
11
|
Write no files. Run no `canon` commands and ask the user nothing.
|
|
12
12
|
Bash is read-only: search, list, print file ranges, and inspect read-only git history. Use no redirects, in-place edits, or state-changing git.
|
|
13
|
-
Inherit the session's model and effort.
|
|
13
|
+
Inherit the session's model and effort unless the dispatching call sets a model.
|
|
14
14
|
|
|
15
15
|
Every finding cites `file:line` or an AC number. Re-open the current file before making any claim about code; do not rely on memory.
|
|
16
16
|
|
|
@@ -128,13 +128,13 @@ After scope is approved:
|
|
|
128
128
|
canon task new TASK-ID "Title"
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
2.
|
|
131
|
+
2. Set metadata with `canon task set TASK-ID task_size <XS|S|M|L|XL>` and `canon task set TASK-ID delicate <true|false>`. The spec gate defaults to armed for new tasks.
|
|
132
132
|
|
|
133
133
|
3. Write `tasks/TASK-ID/spec.md` using `.canon/templates/spec.md` as structure. Fill every section — no placeholders, no "TBD".
|
|
134
134
|
- If full-send mode is active, prepend this line immediately after the title block and before `## Problem`:
|
|
135
135
|
`> **Full-send mode**: This spec was produced in full-send mode.`
|
|
136
136
|
|
|
137
|
-
4.
|
|
137
|
+
4. Run `canon task phase TASK-ID spec done`.
|
|
138
138
|
|
|
139
139
|
Self-check before presenting:
|
|
140
140
|
- [ ] Every AC states exactly how to verify it (not just "it works")
|
|
@@ -184,7 +184,7 @@ Self-check before presenting:
|
|
|
184
184
|
canon task phase TASK-ID spec_review done approved
|
|
185
185
|
canon task phase TASK-ID plan done
|
|
186
186
|
```
|
|
187
|
-
Then set
|
|
187
|
+
Then run `canon task set TASK-ID human_spec_gate false` (already cleared by the human's conversational approval).
|
|
188
188
|
4. Invoke the pipeline:
|
|
189
189
|
```bash
|
|
190
190
|
canon run TASK-ID
|
|
@@ -39,7 +39,7 @@ State briefly: task ID, size, delicate flag, "dispatching 3 parallel sub-agents.
|
|
|
39
39
|
|
|
40
40
|
### 2. Dispatch 3 sub-agents in one message
|
|
41
41
|
|
|
42
|
-
Send a SINGLE message with three Agent tool calls so they run concurrently. Every call uses `subagent_type: spec-review-lens` and carries the full spec text inline, its angle, scope and rubric. Each lens returns cited findings in its own format. If the Agent tool rejects `spec-review-lens`, stop and tell the human to run `canon upgrade`, then start a new Claude Code session; do not fall back to another agent type.
|
|
42
|
+
Send a SINGLE message with three Agent tool calls so they run concurrently. Every call uses `subagent_type: spec-review-lens` and carries the full spec text inline, its angle, scope and rubric. Agent B's call also passes `model: haiku`; its lookups are mechanical, while A and C need judgment and inherit the session model. Each lens returns cited findings in its own format. If the Agent tool rejects `spec-review-lens`, stop and tell the human to run `canon upgrade`, then start a new Claude Code session; do not fall back to another agent type.
|
|
43
43
|
|
|
44
44
|
#### Agent A — Structural / shape
|
|
45
45
|
|
|
@@ -117,7 +117,7 @@ canon task <subcommand> [args]
|
|
|
117
117
|
| `status` | `<id>` | Print full `status.json` detail for a task. |
|
|
118
118
|
| `phase` | `<id> <phase> <status> [verdict]` | Update a task phase and re-derive the top-level `status` pointer. Phases: `spec spec_review plan implement code_review qa human_review`. Status: `pending in_progress done changes_requested blocked`. |
|
|
119
119
|
| `accept` | `<id...> <phase> [--reason "<text>"] [--force]` | Operator accept supports `implement`, `spec_review`, and `code_review`. For `implement`, it is the escape hatch for already-committed work: it marks implement done and sets `operator_accepted` so post-implement auto-commit is skipped when the recorded HEAD still matches. For `spec_review` and `code_review`, `--reason` is required; accept writes a `sanctioned` verdict for non-advancing review verdicts, preserves already-advancing verdicts in a blocked bundle, records `operator_accepted*`, and appends the audit reason to `notes.md`. Review accept intentionally skips the implement-only diff/handoff guards. |
|
|
120
|
-
| `set` | `<id> <field> <value>` | Set flat task metadata with validation. Metadata fields `title`, `task_size`, and `
|
|
120
|
+
| `set` | `<id> <field> <value>` | Set flat task metadata with validation. Metadata fields `title`, `task_size`, `delicate`, and `human_spec_gate` remain settable whenever the task is reachable; setting `human_spec_gate true` clears `full_send` with a note, while setting it `false` is refused on delicate tasks (use `canon run --full-send --force`). Topology fields `worktree` and `base_branch` are only settable before `status.branch` is recorded; once branched, `set` rejects them and points operators at recreating the task or migrating `status.json` manually. Redirected: `full_send` → `canon run --full-send` to enable it (it persists until `--reroute` clears it or arming the spec gate does); `status` → `canon task phase`; `branch` → git identity, not `canon task set`; `phases`/`sessions`/`canon`/`escalations` → their owning canon task commands. Immutable: `id`, `created`, `updated`, and `_`-prefixed inline-doc keys. Writes warn after dispatch has started because metadata changes take effect on the next `canon run`. |
|
|
121
121
|
| `reset-spec-review` | `<id>` | Clear router-relevant state for a fresh spec-review pass after an auto-block. Zeroes iterations, clears verdict, archives the prior `spec-review.md`, and drops the stored `sessions.claude_spec` ID so the next pass runs against a fresh Claude spec session. |
|
|
122
122
|
| `reset-code-review` | `<id>` | Clear router-relevant state for a fresh code-review pass after an auto-block. Zeroes the loop-local review counters, clears verdict, archives the prior `review.md` and re-scaffolds a fresh one from the task templates, and drops the stored `sessions.claude_review` ID so the next pass runs against a fresh Claude review session. |
|
|
123
123
|
| `post-merge-sync` | `[<branch>]` | After a squash-merge PR, reconcile local branch with origin. Hard-resets if the only divergence is pipeline telemetry; refuses if real new work exists. |
|
|
@@ -185,7 +185,7 @@ Set in `status.json` at task creation:
|
|
|
185
185
|
|---|---|---|
|
|
186
186
|
| `task_size` | `XS \| S \| M \| L \| XL` | Drives the pipeline tier and effective-size model/effort cells for Codex and Claude. XS is fast-tier; S+ runs the full pipeline (including Codex spec review). |
|
|
187
187
|
| `delicate` | `true \| false` | Forces the XL bucket (full Codex model and high Claude spec/plan/code-review effort) regardless of nominal size. Set when an undetected bug has materially harder-to-recover blast radius than a normal bug — common examples: auth, payments, premium gating, persistent storage migrations, security-sensitive cryptography. Project-specific surfaces also qualify (medical PHI, scientific reproducibility, regulated data). The bar is *blast radius*, not difficulty. |
|
|
188
|
-
| `human_spec_gate` | `true \| false` | **Single-use latch**, not a persistent toggle. `true` arms a one-time halt after `spec_review`, before planning (default: `true`). The orchestrator flips it to `false` *at the moment it halts
|
|
188
|
+
| `human_spec_gate` | `true \| false` | **Single-use latch**, not a persistent toggle. `true` arms a one-time halt after `spec_review`, before planning (default: `true`). A human can re-arm it with `canon task set <id> human_spec_gate true`; setting it `false` on a delicate task is refused. The orchestrator flips it to `false` *at the moment it halts*, but a human can also disarm it, so `false` means "fired, pre-cleared, or disarmed," not proof the gate fired. See [Spec gate is a single-use latch](#spec-gate-is-a-single-use-latch). |
|
|
189
189
|
| `worktree` | `true \| false` | Worktree isolation. `canon task new` scaffolds this to `true`, so worktree mode is the effective default for every scaffolded task — set it to `false` to opt out and run in the main checkout. The orchestrator treats an *absent* field as `false`, but that fallback only applies to hand-rolled `status.json` files. See Worktree Isolation below. |
|
|
190
190
|
| `base_branch` | string (default `"main"`) | Branch the task branches off and PRs against. Auto-set by `canon task new` from the current git checkout at task creation. |
|
|
191
191
|
|
|
@@ -356,7 +356,7 @@ For projects that version their releases, changelog and version bump remain a ma
|
|
|
356
356
|
|
|
357
357
|
`human_spec_gate` confuses operators because it looks like a persistent on/off setting and isn't. It is a **one-shot latch the orchestrator consumes the first time the pipeline reaches the gate.**
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
When the pipeline reaches the gate point with the latch armed, the orchestrator does three things in one step: (1) sets `human_spec_gate = false`, (2) prints the `✋ SPEC GATE` banner, (3) `process.exit(0)`. A human can also re-arm or disarm the latch with `canon task set`; disarming it on a delicate task is refused. The orchestrator consumes the latch at the halt so the **next** `canon run <id>` — the one the human types after approving the spec — passes straight through instead of halting on the same gate forever.
|
|
360
360
|
|
|
361
361
|
Where the gate fires by tier:
|
|
362
362
|
|
|
@@ -364,7 +364,9 @@ Where the gate fires by tier:
|
|
|
364
364
|
- **Full tier (S/M/L/XL/delicate)**: after Codex `spec_review` completes, before planning.
|
|
365
365
|
- **`--full-send`**: pre-clears the latch to `false` at launch, so the gate never fires.
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
On a full-tier reroute, an armed latch halts after the amendment's `spec_review`, unless that `spec_review` is closed with `canon task accept`, which is itself the human sign-off and skips the gate. A fast-tier reroute re-enters at `implement`, so it never reaches the gate. A reroute does not re-arm a consumed latch by itself; to halt after the amendment's review, run `canon task set <id> human_spec_gate true` before rerouting.
|
|
368
|
+
|
|
369
|
+
So when you inspect `status.json` mid-pipeline and see `human_spec_gate: false`, read it as **"fired, pre-cleared, or disarmed"**; the value alone does not prove the gate fired or that review was not skipped. Re-running `canon run <id>` after the banner proceeds past the consumed latch.
|
|
368
370
|
|
|
369
371
|
**Bundle rule**: the gate skip is all-or-nothing. One task in the bundle with the latch still armed re-engages the halt for the whole invocation; `--full-send` must apply to every task in the bundle to skip it.
|
|
370
372
|
|
|
@@ -470,7 +472,7 @@ The Stage 1 AC table is redone on round 2+. Earlier AC tables were snapshots of
|
|
|
470
472
|
|
|
471
473
|
Write the new requirements into **`tasks/<id>/spec.md` in the active task directory** for every named task as an Amendment section — except on a spec_gap-entry reroute (every named task blocked at `code_review` with at least one `spec_gap` verdict), where only the `spec_gap` tasks need an Amendment section; approved or other non-gap siblings in the same bundle do not. If a worktree exists for the task, edit the worktree copy; edit REPO_ROOT only before the task has a worktree. `review.md` alone is not sufficient — Codex reads `spec.md` as the contract.
|
|
472
474
|
|
|
473
|
-
Full-tier reroute (any S/M/L/XL task or any `delicate` task) re-enters at the same review altitude as the original spec — `spec_review` → `plan` → `implement` — regardless of which admitted phase (`code_review`, `qa`, or `human_review`) the task was rerouted from. Codex reviews the amendment in the context of the previously approved ACs and prior `spec-review.md`, without auditing `handoff.md`, `review.md`, or `done.md`. If the amendment is approved, the pipeline flows through to `plan`
|
|
475
|
+
Full-tier reroute (any S/M/L/XL task or any `delicate` task) re-enters at the same review altitude as the original spec — `spec_review` → `plan` → `implement` — regardless of which admitted phase (`code_review`, `qa`, or `human_review`) the task was rerouted from. Codex reviews the amendment in the context of the previously approved ACs and prior `spec-review.md`, without auditing `handoff.md`, `review.md`, or `done.md`. If the amendment is approved, the pipeline flows through to `plan` unless the operator re-armed the human spec gate first, in which case it halts at the gate. Once past `spec_review`, Claude appends a reroute plan section (`## Reroute Plan` or `## Reroute Plan Round N`) to `plan.md`; Codex then implements from the amendment plus that reroute plan.
|
|
474
476
|
|
|
475
477
|
Rerouting from `code_review` or a `qa` `pending`/`in_progress` entry happens before the QA-end commit, so task artifacts may still be uncommitted in the worktree. `--reroute` performs no git operation itself, so nothing is destroyed by the reroute. Do not run a manual git command that can discard uncommitted state (`reset --hard`, `checkout HEAD~N`, `stash drop`, or equivalent) in that window.
|
|
476
478
|
|