canon-ai 3.6.0 → 3.7.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 CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.7.0] — 2026-10-06
8
+
9
+ ### Added
10
+
11
+ - **`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`.
12
+
13
+ ### Changed
14
+
15
+ - **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.
16
+ - **The npm package's homepage now links to <https://timothystraub.com/canon>.**
17
+
7
18
  ## [3.6.0] — 2026-10-04
8
19
 
9
20
  ### 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.6.0";
1227
+ const installedVersion = "3.7.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.6.0";
1724
+ const version = "3.7.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.6.0" ?? "dev";
2734
+ return explicit ?? "3.7.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: "a per-run stance, not durable metadata. Use `canon run --full-send <id>`, which also clears the spec gate and enforces the delicate\u2192`--force` guard.",
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(` Edit ${taskDir}/status.json to adjust before running the pipeline.`);
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
- if (taskHasStarted(status)) {
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.6.0";
4896
+ return "3.7.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.6.0";
5373
+ const newVersion = "3.7.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.6.0");
5681
+ console.log("3.7.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.6.0" ?? "dev";
5208
+ return explicit ?? "3.7.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.6.0",
3
+ "version": "3.7.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://github.com/tstraub89/canon-ai#readme",
44
+ "homepage": "https://timothystraub.com/canon",
45
45
  "repository": {
46
46
  "type": "git",
47
47
  "url": "git+https://github.com/tstraub89/canon-ai.git"
@@ -128,13 +128,13 @@ After scope is approved:
128
128
  canon task new TASK-ID "Title"
129
129
  ```
130
130
 
131
- 2. Edit `tasks/TASK-ID/status.json`: set `task_size`, `delicate`, and `human_spec_gate: true`.
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. Set `spec.status` to `"done"` in `status.json`.
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 `human_spec_gate: false` in `status.json` (already cleared by the human's conversational approval).
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
@@ -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 `delicate` remain settable whenever the task is reachable. 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`; `human_spec_gate` → re-run `canon run`; `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`. |
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* — so `false` means "the gate already fired (or was pre-cleared)," not "review was skipped." See [Spec gate is a single-use latch](#spec-gate-is-a-single-use-latch). |
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
- The flip to `false` *is the gate firing* — it happens at the halt, **before** the human has reviewed, not after. Concretely, 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)`. The reason it burns the latch *at* the halt is 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.
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
- So when you inspect `status.json` mid-pipeline and see `human_spec_gate: false`, read it as **"the gate already fired (or was pre-cleared) — review was *not* skipped"**, not "the halt was lost." There is no separate "gate satisfied" field; the consumed latch *is* the record that the gate fired. Re-running `canon run <id>` after the banner is the intended next move — it does not re-arm or re-fire the gate.
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` without re-arming the human spec gate; 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.
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