@gobing-ai/spur 0.3.97 → 0.3.99

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.
Files changed (90) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +5 -0
  3. package/config/templates/task/brainstorm.md +1 -1
  4. package/config/templates/task/feature-impl.md +1 -1
  5. package/config/templates/task/issue.md +1 -1
  6. package/config/templates/task/meta.md +1 -1
  7. package/config/templates/task/review.md +1 -1
  8. package/config/templates/task/standard.md +1 -1
  9. package/package.json +9 -9
  10. package/plugins/sp/README.md +9 -9
  11. package/plugins/sp/lib/idea-handoff.generated.mjs +37 -4
  12. package/plugins/sp/lib/inline-run.generated.mjs +54 -14
  13. package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
  14. package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
  15. package/plugins/sp/plugin.json +1 -1
  16. package/plugins/sp/references/roles.md +3 -3
  17. package/plugins/sp/scripts/inline-run-setup.mjs +4 -2
  18. package/plugins/sp/scripts/inline-run-setup.ts +12 -12
  19. package/plugins/sp/scripts/persist-out-check.mjs +164 -0
  20. package/plugins/sp/scripts/persist-out-check.ts +185 -0
  21. package/plugins/sp/scripts/residual-scan.mjs +50 -8
  22. package/plugins/sp/scripts/residual-scan.ts +4 -3
  23. package/plugins/sp/scripts/task-diffstat.mjs +17 -5
  24. package/plugins/sp/scripts/task-diffstat.ts +25 -4
  25. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  26. package/plugins/sp/skills/code-verification/SKILL.md +3 -0
  27. package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
  28. package/plugins/sp/skills/conflict-finding/SKILL.md +1 -1
  29. package/plugins/sp/skills/conflict-finding/references/authority-resolution.md +5 -3
  30. package/plugins/sp/skills/conflict-finding/references/remediation-routing.md +9 -8
  31. package/plugins/sp/skills/doc-evolve/SKILL.md +4 -3
  32. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +7 -4
  33. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +7 -4
  34. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
  35. package/plugins/sp/skills/spur-dev/references/dev-operations.md +16 -13
  36. package/plugins/sp/skills/spur-dev/references/execution-batch.md +217 -38
  37. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -1
  38. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +7 -8
  39. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +110 -3
  40. package/spur.js +2142 -637
  41. package/web/_astro/{BoardApp.vveLOdDq.js → BoardApp.BVp-LYi6.js} +101 -101
  42. package/web/_astro/{BoardApp.Bmv5WkJ9.js → BoardApp.i4eh_fHA.js} +1 -1
  43. package/web/_astro/{TaskDetail.IKc3uDYV.js → TaskDetail.C5Cu8LdB.js} +1 -1
  44. package/web/_astro/{arc.C63Ufg35.js → arc.Bu5XpBEz.js} +1 -1
  45. package/web/_astro/{architectureDiagram-3BPJPVTR.CP8Hyldk.js → architectureDiagram-3BPJPVTR.9vHQTvhF.js} +1 -1
  46. package/web/_astro/{blockDiagram-GPEHLZMM.Bo_48MRA.js → blockDiagram-GPEHLZMM.Cn2Xl7D6.js} +1 -1
  47. package/web/_astro/{c4Diagram-AAUBKEIU.Ch61sw-O.js → c4Diagram-AAUBKEIU.7o0P_QDo.js} +1 -1
  48. package/web/_astro/channel.6IFIKBhu.js +1 -0
  49. package/web/_astro/{chunk-2J33WTMH.CHpzS7xf.js → chunk-2J33WTMH.C3zZFsNt.js} +1 -1
  50. package/web/_astro/{chunk-4BX2VUAB.DzMNNWIw.js → chunk-4BX2VUAB.CNy_M7fO.js} +1 -1
  51. package/web/_astro/{chunk-55IACEB6.BqPrDw_Q.js → chunk-55IACEB6.2TiPnLBL.js} +1 -1
  52. package/web/_astro/{chunk-727SXJPM.ZzBHhNw-.js → chunk-727SXJPM.5CGSyKAp.js} +1 -1
  53. package/web/_astro/{chunk-AQP2D5EJ.1_O_LLkx.js → chunk-AQP2D5EJ.w-ZIgfsK.js} +1 -1
  54. package/web/_astro/{chunk-FMBD7UC4.GS4UHg7_.js → chunk-FMBD7UC4.BkcEyhKa.js} +1 -1
  55. package/web/_astro/{chunk-ND2GUHAM.BnqotN4g.js → chunk-ND2GUHAM.mxKKbBG4.js} +1 -1
  56. package/web/_astro/{chunk-QZHKN3VN.Ghuh8zoJ.js → chunk-QZHKN3VN.cyA0z9aT.js} +1 -1
  57. package/web/_astro/{classDiagram-4FO5ZUOK.BW9vC3zg.js → classDiagram-4FO5ZUOK.CqtUzwhX.js} +1 -1
  58. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BW9vC3zg.js → classDiagram-v2-Q7XG4LA2.CqtUzwhX.js} +1 -1
  59. package/web/_astro/{cose-bilkent-S5V4N54A.D6jNC0ti.js → cose-bilkent-S5V4N54A.BExPTopI.js} +1 -1
  60. package/web/_astro/{cynefin-OW5HDTMX.BOGoCs4L.js → cynefin-OW5HDTMX.Bq2i10Cn.js} +1 -1
  61. package/web/_astro/{dagre-BM42HDAG.8iU5VcWi.js → dagre-BM42HDAG.By_HsOuv.js} +1 -1
  62. package/web/_astro/{diagram-2AECGRRQ.CVt88HwH.js → diagram-2AECGRRQ.BtU0b7hz.js} +1 -1
  63. package/web/_astro/{diagram-5GNKFQAL.C9HxUUMl.js → diagram-5GNKFQAL.D00ztMdT.js} +1 -1
  64. package/web/_astro/{diagram-KO2AKTUF.4ywkQN3t.js → diagram-KO2AKTUF.DMVJWaUN.js} +1 -1
  65. package/web/_astro/{diagram-LMA3HP47.C48A6gpz.js → diagram-LMA3HP47.Cxqrjg5-.js} +1 -1
  66. package/web/_astro/{diagram-OG6HWLK6.DxFieQW4.js → diagram-OG6HWLK6.2lz_1B5h.js} +1 -1
  67. package/web/_astro/{erDiagram-TEJ5UH35.CtjtQqYs.js → erDiagram-TEJ5UH35.BVQEuJ3T.js} +1 -1
  68. package/web/_astro/{flowDiagram-I6XJVG4X.C5zAiahU.js → flowDiagram-I6XJVG4X.Qlyo90ky.js} +1 -1
  69. package/web/_astro/{ganttDiagram-6RSMTGT7.D_L6zsny.js → ganttDiagram-6RSMTGT7.DRwb8HHZ.js} +1 -1
  70. package/web/_astro/{gitGraphDiagram-PVQCEYII.Ignnrk3V.js → gitGraphDiagram-PVQCEYII.YfoPthfS.js} +1 -1
  71. package/web/_astro/{infoDiagram-5YYISTIA.DLv8-P5u.js → infoDiagram-5YYISTIA.DvFREck1.js} +1 -1
  72. package/web/_astro/{ishikawaDiagram-YF4QCWOH.p0OTpwFO.js → ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js} +1 -1
  73. package/web/_astro/{journeyDiagram-JHISSGLW.CIaBt-DD.js → journeyDiagram-JHISSGLW.BDMT_AFb.js} +1 -1
  74. package/web/_astro/{kanban-definition-UN3LZRKU.DGySIntq.js → kanban-definition-UN3LZRKU.D6Fh3gj9.js} +1 -1
  75. package/web/_astro/{linear.BATV4RXu.js → linear.Dxw00fGQ.js} +1 -1
  76. package/web/_astro/{mermaid.core.DzkwM3VX.js → mermaid.core.CsoxUWa0.js} +4 -4
  77. package/web/_astro/{mindmap-definition-RKZ34NQL.3piBRRzA.js → mindmap-definition-RKZ34NQL.CN8oeHLu.js} +1 -1
  78. package/web/_astro/{pieDiagram-4H26LBE5.DUNueLub.js → pieDiagram-4H26LBE5.C7ADqQ_7.js} +1 -1
  79. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bihVbrt.js → quadrantDiagram-W4KKPZXB.Cw9lKyHP.js} +1 -1
  80. package/web/_astro/{requirementDiagram-4Y6WPE33.CkjsC-YT.js → requirementDiagram-4Y6WPE33.B1ni-z4q.js} +1 -1
  81. package/web/_astro/{sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js → sankeyDiagram-5OEKKPKP.BhexdjVr.js} +1 -1
  82. package/web/_astro/{sequenceDiagram-3UESZ5HK.BivrH99P.js → sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js} +1 -1
  83. package/web/_astro/{stateDiagram-AJRCARHV.CqLQ1Vzz.js → stateDiagram-AJRCARHV.CkuMt27u.js} +1 -1
  84. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DvvoLD3b.js → stateDiagram-v2-BHNVJYJU.Dc_aazUA.js} +1 -1
  85. package/web/_astro/{timeline-definition-PNZ67QCA.B-KR78gv.js → timeline-definition-PNZ67QCA.ja7sUFhH.js} +1 -1
  86. package/web/_astro/{vennDiagram-CIIHVFJN.Ck0Eieb5.js → vennDiagram-CIIHVFJN.BxIshX_K.js} +1 -1
  87. package/web/_astro/{wardleyDiagram-YWT4CUSO.CRfdd_do.js → wardleyDiagram-YWT4CUSO.DfHlC0sh.js} +1 -1
  88. package/web/_astro/{xychartDiagram-2RQKCTM6.Ck_Jxk9C.js → xychartDiagram-2RQKCTM6.DSnhHwCh.js} +1 -1
  89. package/web/index.html +1 -1
  90. package/web/_astro/channel.BKCmJpWM.js +0 -1
@@ -3,7 +3,7 @@
3
3
 
4
4
  // plugins/sp/scripts/task-diffstat.ts
5
5
  import { spawnSync } from "child_process";
6
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
7
7
  import { join } from "path";
8
8
 
9
9
  // plugins/sp/lib/env.ts
@@ -85,10 +85,14 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
85
85
  process.stderr.write(`task-diffstat: ${reason} \u2014 failing safe (sensitive)
86
86
  `);
87
87
  const row2 = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
88
- writeFileSync(abs(relResult), `${JSON.stringify(row2)}
89
- `);
88
+ writeArtifact(abs(relResult), row2);
90
89
  return { ...row2, resultFile: relResult, exitCode: 0 };
91
90
  };
91
+ let stdoutIsArtifact = false;
92
+ try {
93
+ const [out, art] = [fstatSync(1), statSync(abs(relResult))];
94
+ stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
95
+ } catch {}
92
96
  let base = "";
93
97
  try {
94
98
  base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), "utf8").trim();
@@ -125,9 +129,17 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
125
129
  const all = [...paths].filter((p) => p.length > 0).sort();
126
130
  const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
127
131
  const row = { files: all.length, insertions, deletions, paths: all, sensitive };
128
- writeFileSync(abs(relResult), `${JSON.stringify(row)}
132
+ writeArtifact(abs(relResult), row);
133
+ if (stdoutIsArtifact)
134
+ process.stderr.write(`task-diffstat: stdout redirected onto the artifact \u2014 kept; redirect stdout elsewhere
135
+ `);
136
+ return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
137
+ }
138
+ function writeArtifact(absPath, row) {
139
+ const tmp = `${absPath}.tmp`;
140
+ writeFileSync(tmp, `${JSON.stringify(row)}
129
141
  `);
130
- return { ...row, resultFile: relResult, exitCode: 0 };
142
+ renameSync(tmp, absPath);
131
143
  }
132
144
  var TASK_DIFFSTAT_USAGE = "usage: task-diffstat (env: wbs)";
133
145
  function main(argv, env = getEnvVars(), options = {}) {
@@ -19,13 +19,17 @@
19
19
  * other probe problem writes `sensitive: true` (and exits 0) so the lane degrades to
20
20
  * safety, never to fast.
21
21
  *
22
+ * The artifact is written atomically (tmp + rename), so redirecting stdout onto the artifact path
23
+ * cannot corrupt it — the rename swaps in the real JSON after the shell's redirect truncates the
24
+ * old inode (1067 R3); that redirect is still refused with a nonzero exit. Normal output is silent.
25
+ *
22
26
  * Node-builtin imports only; the git lookup is a spawnable `GitRunner` so tests can fake
23
27
  * it; `main(argv, env, options)` is the same injection point the workflow wrapper and its
24
28
  * node twin exercise. Only an empty `wbs` (mis-invocation) exits nonzero.
25
29
  */
26
30
 
27
31
  import { spawnSync } from 'node:child_process';
28
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
32
+ import { fstatSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
29
33
  import { join } from 'node:path';
30
34
  import { getEnvVars } from '../lib/env';
31
35
 
@@ -166,10 +170,18 @@ export function runDiffstat(
166
170
  const failSafe = (reason: string): DiffstatResult => {
167
171
  process.stderr.write(`task-diffstat: ${reason} — failing safe (sensitive)\n`);
168
172
  const row: Diffstat = { files: 0, insertions: 0, deletions: 0, paths: [], sensitive: true };
169
- writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
173
+ writeArtifact(abs(relResult), row);
170
174
  return { ...row, resultFile: relResult, exitCode: 0 };
171
175
  };
172
176
 
177
+ let stdoutIsArtifact = false; // 1067 R3: stdout redirected onto the artifact itself clobbered it once
178
+ try {
179
+ const [out, art] = [fstatSync(1), statSync(abs(relResult))];
180
+ stdoutIsArtifact = out.dev === art.dev && out.ino === art.ino;
181
+ } catch {
182
+ /* artifact absent or stdout not a regular file — nothing to compare */
183
+ }
184
+
173
185
  let base = '';
174
186
  try {
175
187
  base = readFileSync(abs(join(relRun, `${wbs}-base.sha`)), 'utf8').trim();
@@ -203,8 +215,17 @@ export function runDiffstat(
203
215
  const all = [...paths].filter((p) => p.length > 0).sort();
204
216
  const sensitive = all.some((p) => sensitiveReasonForPath(p) !== null);
205
217
  const row: Diffstat = { files: all.length, insertions, deletions, paths: all, sensitive };
206
- writeFileSync(abs(relResult), `${JSON.stringify(row)}\n`);
207
- return { ...row, resultFile: relResult, exitCode: 0 };
218
+ writeArtifact(abs(relResult), row);
219
+ if (stdoutIsArtifact)
220
+ process.stderr.write('task-diffstat: stdout redirected onto the artifact — kept; redirect stdout elsewhere\n');
221
+ return { ...row, resultFile: relResult, exitCode: stdoutIsArtifact ? 1 : 0 };
222
+ }
223
+
224
+ /** Atomic artifact write (tmp + rename): a stdout redirect onto the path cannot interleave (1067 R3). */
225
+ function writeArtifact(absPath: string, row: Diffstat): void {
226
+ const tmp = `${absPath}.tmp`;
227
+ writeFileSync(tmp, `${JSON.stringify(row)}\n`);
228
+ renameSync(tmp, absPath);
208
229
  }
209
230
 
210
231
  export const TASK_DIFFSTAT_USAGE = 'usage: task-diffstat (env: wbs)';
@@ -184,7 +184,7 @@ reproduce → isolate → minimal fix → regression guard.
184
184
 
185
185
  - Writing code without having read the immediate callers of what you're changing.
186
186
  - A `## Solution` section that lists files but not what changed or why.
187
- - A `## Solution` section with file references that are not in backtick `` `path:line` `` form (L3 requirement: `` `packages/app/src/foo.ts:123` `` or `` `packages/app/src/bar.ts:10-20` ``; paths from repo root).
187
+ - A `## Solution` section with file references that are not in backtick `` `path:line` `` form (L3 requirement: `` `packages/app/src/foo.ts:123` `` or `` `packages/app/src/bar.ts:10-20` ``; paths from repo root). Non-repo paths (external packages, `@scope/...`) can never resolve repo-relative — cite them in the frozen external-evidence form instead: `Evidence: @gobing-ai/ts-x \`src/persistence.ts\` line 108` (origin and line number OUTSIDE the backticks). Wrong: `` `@gobing-ai/ts-x/src/persistence.ts:108` `` — the L4 gate fails it as an unresolvable anchor (task 1055).
188
188
  - A new abstraction with exactly one caller and no second use in sight.
189
189
  - The diff touches files unrelated to the task's scope.
190
190
  - The diff touches another WBS's surfaces, or the pass reads sibling `todo` / `wip` task files.
@@ -336,6 +336,7 @@ Verdict: PASS (or PARTIAL / FAIL)
336
336
 
337
337
  ```markdown
338
338
  Verdict: PASS
339
+ Confidence: HIGH
339
340
 
340
341
  ### Per-Requirement Traceability
341
342
  | Req | Status | Evidence |
@@ -354,6 +355,8 @@ Verdict: PASS
354
355
  ```
355
356
 
356
357
  The per-requirement traceability table MUST use `| Req | Status | Evidence |` (exactly this header, no `R#`/`R`/`Requirement` variant — `Status` in column 2 — and no extra columns between Req and Status). The Acceptance Criteria table MUST use `| AC | Status | Evidence Type | Evidence |`.
358
+ The answer MUST carry exactly one `Confidence: HIGH|MEDIUM|LOW` line (task 1068): the verifier's stated confidence in the verdict — HIGH = fresh executable evidence + re-read anchors this run; MEDIUM = partial reliance on prior evidence, external/delegated outcomes, or an unstable tree; LOW = key evidence not re-executed. A missing or out-of-vocabulary line fails lint (`confidence-missing` / `confidence-value`) and blocks derivation; the value lands in the verdict artifact's `confidence` field and the recorded `## Testing` section.
359
+ The answer MUST cite a source for external API/library claims in Requirement/AC evidence cells: a cell asserting a scoped-package (`@scope/name`), `node_modules/`, semver-version (`x.y.z`), or backticked function-call (`` `name(args)` ``) claim is rejected with `evidence-citation` unless the SAME cell carries a citation — a repo `path:line(-end)` (backticked or bare), the external named-origin form (@origin `path` line N), or a URL. Cross-cell references ("receipt above") do not count; marker-free receipts ("bun test 398 pass / 0 fail") need no citation.
357
360
  **MUST NOT:** place a `Severity` column between `Req` and `Status` in the authoring contract.
358
361
  The parser is tolerant of these variants (defense-in-depth), but the authoring contract is
359
362
  canonical.
@@ -115,7 +115,9 @@ AC ids that do not resolve to one accepted identity — a task AC checklist labe
115
115
  (exact/bare title, `Scenario:` prefix, bracket tags, `AC-N`); paraphrases and ambiguous aliases
116
116
  fail — invalid status (`MET | PARTIAL | UNMET` for requirements;
117
117
  `N/A` additionally allowed for AC), invalid evidence type (`test | command | static-ref |
118
- manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Interrupted runs keep the
118
+ manual-review | llm-judge | n/a`, or a `+` compound), and empty evidence. Evidence cells that assert an
119
+ external API/library claim without a same-cell citation are rejected with `evidence-citation` (see
120
+ "Source citations for external claims" below). Interrupted runs keep the
119
121
  rows that pass the lint and complete only the missing IDs on retry.
120
122
 
121
123
  **Feature-credited AC ids are narrower than the lint's accepted set (task 0966 finding).** A row keyed
@@ -136,6 +138,20 @@ the run actually verified. Since 0804 R9 the checker ignores complete parsed cit
136
138
  scanning for subjects, so a citation's filename (including snake_case paths) can never become a
137
139
  false subject — a real absent symbol, nonexistent file, or invalid range still reports.
138
140
 
141
+ ## Source citations for external claims (task 1070)
142
+
143
+ A Requirement/AC evidence cell that asserts an external API/library claim must carry a verifiable
144
+ citation in the SAME cell, or lint rejects the row with `evidence-citation` (blocking derivation).
145
+
146
+ - Claim markers (closed set): a scoped-package mention (`@scope/name`), a `node_modules/` reference,
147
+ a semver version claim (`x.y.z`), or a backticked function-call-shaped token (`` `name(args)` ``).
148
+ - Accepted citation forms (any one clears the cell): a repo `path:line(-end)` (backticked or bare),
149
+ the external named-origin form (@origin `path` line N — line number outside the backticks), or an
150
+ http(s) URL.
151
+ - The rule is cell-local: a citation in another row or a "receipt above" cross-reference does not
152
+ satisfy it. Marker-free receipts (test counts, reviewed-log notes) need no citation. The check is
153
+ presence-only — which citation proves which claim is not deterministically decidable.
154
+
139
155
  ## Checks evidence
140
156
 
141
157
  Wave C verification can emit the following additive `checks[]` rows:
@@ -249,7 +249,7 @@ Thin wrapper: `Skill(skill="sp:conflict-finding", args="$ARGUMENTS")`.
249
249
  - Silently omitting a selected pillar or a tool failure (no coverage accounting).
250
250
  - Claiming "comprehensive" when `coverage.complete` is false.
251
251
  - Any mutation in audit mode, or automatic mutation merely because `--resolve` is present.
252
- - Editing a derived projection before its authority.
252
+ - Editing a derived projection before correcting a defective authority, or changing a correct authority merely to repair its stale projection.
253
253
  - Adding a production analyzer, index/cache/database, dependency, or dedicated subagent in v1.
254
254
 
255
255
  ## Reference files
@@ -206,13 +206,15 @@ authority it governs:
206
206
  | Owner | Repair route | Governs (claim types) |
207
207
  |-------|--------------|----------------------|
208
208
  | Task/feature corpus | `sp:spur-cli` / `spur task` / `spur feature`; never direct file writes | Task obligation/status, feature goal and AC |
209
- | Numbered docs and AGENTS projections | `sp:doc-evolve`; authority first, derived projections second | Process rules, structural decision, product scope, architecture, command surface |
209
+ | Numbered docs and AGENTS projections | `sp:doc-evolve`; inspect authority first, correct it only if defective, then repair affected projections | Process rules, structural decision, product scope, architecture, command surface |
210
210
  | Source/tests | create/use a Spur task and route through `sp:spur-dev` / build competencies | Implementation behavior, architecture mechanism |
211
211
  | Command/skill capability source | Superskill command/skill lifecycle in the owning plugin source | Command/API/schema surface |
212
212
  | Ambiguous authority | **stop** for an operator decision; do not mutate either side | (all, when unresolved) |
213
213
 
214
- Never edit a derived projection before its authority. The authority column of §2 is always the
215
- owner surface's source of truth; a projection is brought into line with it, not the reverse.
214
+ Inspect the governing authority before repairing a derived projection. Correct the authority first
215
+ only when it is defective and the correction is authorized; otherwise leave it unchanged and repair
216
+ the projection. The authority column of §2 remains the owner surface's source of truth; a projection
217
+ is brought into line with it, not the reverse.
216
218
 
217
219
  ## 7. Worked examples
218
220
 
@@ -113,13 +113,13 @@ between "the audit said X" and "the file now says Y."
113
113
  Each approved repair is dispatched through the artifact's **owner surface** — the harness path that
114
114
  owns writes to that artifact — not a direct file write. Authority for a repair is the same
115
115
  authority model as the audit ([./authority-resolution.md](./authority-resolution.md)): for a given
116
- subject + claim type, one artifact is normative; derived projections are updated only after their
117
- authority.
116
+ subject + claim type, one artifact is normative. Inspect it first; correct it only if defective and
117
+ authorized, then update affected projections. A correct authority needs no edit for a stale projection.
118
118
 
119
119
  | Owner | Repair route | Never |
120
120
  |-------|--------------|-------|
121
121
  | Task/feature corpus | `sp:spur-cli` — `spur task` / `spur feature` (with `--section`, `--from-file`, `--json` as needed) | Direct file writes to task/feature files. |
122
- | Numbered docs and AGENTS projections | `sp:doc-evolve` — **authority first, derived projections second** (see below) | Editing `docs/05_FEATURES.md` or an `AGENTS.md` projection before its authoritative source is fixed. |
122
+ | Numbered docs and AGENTS projections | `sp:doc-evolve` — inspect authority first; correct it only if defective, then repair affected projections (see below) | Changing correct authority to repair a stale projection, or projecting a defective authority before its authorized correction. |
123
123
  | Source / tests | Create or use a **Spur task** and route through `sp:spur-dev` / build competencies (`sp:code-implementation`, `sp:code-testing`), **unless** the active session already has explicit implementation authority for the change. | Bare in-place edits to source/tests without a task or explicit authority. |
124
124
  | Command/skill capability source | Superskill command/skill **lifecycle** (`superskill command …` / `superskill skill …` scaffold/validate/evaluate/refine) in the owning plugin source. | Hand-editing capability source outside the Superskill lifecycle. |
125
125
  | Ambiguous authority | **Stop for an operator decision.** Do not mutate either side. | Choosing a winner, editing both sides, or forcing a global ranking. |
@@ -148,12 +148,13 @@ item; never route around it with a direct write.
148
148
 
149
149
  ### 3.2 Numbered docs and AGENTS projections
150
150
 
151
- Documentation repairs follow **authority first, derived projections second**. If a derived
151
+ Documentation repairs **inspect authority first**. If a derived
152
152
  projection (`docs/04_DESIGN.md`, `docs/05_FEATURES.md`, `AGENTS.md`, templates) has drifted from its
153
153
  authority (`docs/00_ADR.md`, `docs/01_PRD.md`, `docs/03_ARCHITECTURE.md`,
154
- `docs/99_PROJECT_CONSTITUTION.md`), the repair fixes the **authority** first, then re-derives the
155
- projection via `sp:doc-evolve`. Editing a projection without its authority is a prohibited
156
- anti-pattern (see §6).
154
+ `docs/99_PROJECT_CONSTITUTION.md`), leave a correct authority unchanged and repair the stale
155
+ projection via `sp:doc-evolve`. If the authority itself is defective, correct it first within the
156
+ approved scope, then update affected projections. Incomparable authority still requires an operator
157
+ decision; projection drift alone never authorizes changing the governing contract (see §6).
157
158
 
158
159
  `sp:doc-evolve` owns the numbered-doc/projection lifecycle: sync checks, contract verification,
159
160
  frontmatter checks, and derived-doc refresh. A doc repair is dispatched to it; it is not hand-applied.
@@ -244,7 +245,7 @@ These are the explicit boundaries for remediation. Each is a hard violation, not
244
245
  | Anti-pattern | Why it is prohibited |
245
246
  |--------------|----------------------|
246
247
  | **Automatic mutation merely because `--resolve` is present** | `--resolve` only opens the workflow; it never authorizes automatic repair. Confirmation + freshness + owner surface are all required. |
247
- | **Editing a derived projection before its authority** | A projection (`docs/04`/`docs/05`, `AGENTS.md`, templates) is not the source of truth; fixing it without its authority (`ADR`/`PRD`/constitution) leaves the real conflict in place. Authority first. |
248
+ | **Changing correct authority for projection drift** | A stale projection is repaired against its governing contract. Correct authority only when defective and authorized; then update affected projections. |
248
249
  | **Broad cleanup outside confirmed findings** | Only the artifacts named by confirmed, freshness-checked findings are touched. No opportunistic refactors or doc "improvements" while repairing. |
249
250
  | **Ambiguity silently resolved** | Ambiguous/missing authority stops for an operator decision; it is never forced through a global ranking or a guessed owner. |
250
251
  | **Partial failure silently continued** | A failed repair is reported, not papered over; siblings are not marked successful and the run does not roll forward silently. |
@@ -100,9 +100,10 @@ rg --files apps packages # real modules
100
100
  git log -1 --format='%ci' -- docs/04_DESIGN.md # last touch vs. recent surface changes
101
101
  ```
102
102
 
103
- **Repair protocol (§7):** fix the authoritative statement first, then affected detail/index/entry
104
- files. Preserve ADR numbers, original titles/dates and decision history; editorial condensation
105
- follows §6.1, while actual reversals need a superseding decision. Record findings in a task/report.
103
+ **Repair protocol (§7):** inspect the governing statement first. Correct it only if defective and
104
+ authorized, then update affected detail/index/entry files; otherwise leave the authority unchanged
105
+ and repair its stale projections. Preserve ADR numbers, original titles/dates and decision history;
106
+ editorial condensation follows §6.1, while actual reversals need a superseding decision. Record findings in a task/report.
106
107
  Routine lessons go to existing learning/context storage (§8); no automatic constitution edits.
107
108
 
108
109
  Output a **drift report**: per finding, `{ doc, what code says, what the doc says, authority, repair
@@ -42,13 +42,16 @@ for `Plan`, `Acceptance Criteria`, hand-authored `Solution`, and any narrative s
42
42
  spur task update 0040 --section Review --from-file /tmp/review.md
43
43
  ```
44
44
 
45
- 3. The whole `### Review` body is now that file's contents. To amend rather than overwrite, read
46
- the current body (`spur task show 0040`), edit the temp file to the full desired state, and
47
- replace again — there is no append mode for ordinary sections.
45
+ 3. The whole `### Review` body is now that file's contents. To amend rather than overwrite, either
46
+ read the current body (`spur task show 0040`), edit the temp file to the full desired state, and
47
+ replace again — or pass `--append` (task 1057) to write the file's body after the existing
48
+ section content (existing content + one blank line + new body; surrounding whitespace trimmed; an empty/missing section is
49
+ created). `--append` requires `--section` and never replaces on its own.
48
50
 
49
51
  **`Q&A` is the exception.** `--section "Q&A"` APPENDS a timestamped `#### Q&A entry — <ISO>`
50
52
  block rather than replacing the section. Start the body with `<!-- qa:replace -->` to replace it
51
- wholesale.
53
+ wholesale. Explicit `--append` on `Q&A` uses the generic append (no timestamped wrapper); the
54
+ `<!-- qa:replace -->` marker still wins and forces wholesale replace.
52
55
 
53
56
  `--section` **requires** `--from-file` (exit `2` otherwise). Section names match the DD-08 headings
54
57
  exactly: `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Root Cause`, `Testing`, `Review`, `References`, `History`, `Notes` (universal sections are `History`, `References`, `Notes`; `Root Cause` is carried by the `issue` template variant).
@@ -70,9 +70,12 @@ frontmatter scalar.
70
70
  `testing→done` `check` gates above still run; the CLI evaluates them inline when the FSM guard
71
71
  does not. `--force-done` waives the verify **verdict** only, never the section matrix.
72
72
  - **Section** (`--section` **requires** `--from-file`): writes the named section body from the file.
73
- Most sections replace wholesale. Exception: `--section "Q&A"` APPENDS a timestamped
74
- `#### Q&A entry — <ISO>` block; start the body with `<!-- qa:replace -->` to replace the section
75
- wholesale. No inline-body flag. Section names: `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Testing`, `Review`, `References`, `History`, `Notes`.
73
+ Most sections replace wholesale. `--append` extends instead: existing body + one blank line + the
74
+ file's body (task 1057; empty/missing section is created; still requires `--section`). Exception:
75
+ `--section "Q&A"` APPENDS a timestamped `#### Q&A entry — <ISO>` block; start the body with
76
+ `<!-- qa:replace -->` to replace the section wholesale (explicit `--append` on `Q&A` uses the
77
+ generic append, marker still wins). No inline-body flag. Section names (case-sensitive):
78
+ `Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Testing`, `Review`, `References`, `History`, `Notes`.
76
79
  - **Frontmatter** (`--feature <id>`, `--priority <p>`): sets the scalar frontmatter field on an
77
80
  existing task — the only post-create path, allow-listed to `feature_id` / `parent_wbs` / `priority`.
78
81
  - **Estimate** (`--estimate-hours <n>`): sets the `estimate_hours` frontmatter field (positive number) —
@@ -113,7 +116,7 @@ Flags: `--folder <path>`, `--json`. Exit codes: `0` success, `1` error, `2` usag
113
116
 
114
117
  ## `sections <wbs> <op> [name]`
115
118
 
116
- CLI-safe, matrix-enforced task section mutation. Section names are validated against canonical sections (`Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Root Cause`, `Testing`, `Review`, `References`, `History`, `Notes`). Universal sections (`History`, `References`, `Notes`) are always allowed; `Root Cause` is carried by the `issue` template variant. `Q&A` on `update --section` appends rather than replacing (see the Section bullet above); `<!-- qa:replace -->` forces a wholesale replace.
119
+ CLI-safe, matrix-enforced task section mutation. Section names are validated against canonical sections (`Background`, `Requirements`, `Acceptance Criteria`, `Q&A`, `Design`, `Plan`, `Solution`, `Root Cause`, `Testing`, `Review`, `References`, `History`, `Notes`). Universal sections (`History`, `References`, `Notes`) are always allowed; `Root Cause` is carried by the `issue` template variant. `Q&A` on `update --section` appends rather than replacing (see the Section bullet above); `<!-- qa:replace -->` forces a wholesale replace. `--append` is the generic extend-after-existing path (task 1057).
117
120
 
118
121
  | Op | Usage | Description |
119
122
  | --- | --- | --- |
@@ -30,10 +30,28 @@ Rules:
30
30
  ## Task-side numbering (`AC<n>`)
31
31
 
32
32
  `R<n>` is the **feature** scenario key and the **task Requirements** key. Task `### Acceptance
33
- Criteria` items therefore use their own namespace: `- [ ] AC1 — <title>` or `Scenario: AC1 — <title>`,
34
- numbered task-locally. Carry a feature scenario by copying its title after the prefix (`normalizeTitle`
35
- strips both `AC<n>` and `R<n>`, so DD-09 matching is unaffected); bind a task requirement with
36
- `(req: R<n>)`. Legacy tasks that wrote `- [ ] R<n> —` / `Scenario: R<n> —` keep working unchanged.
33
+ Criteria` items therefore use their own namespace: `AC1, AC2, …`. Two frontmatter fields split the
34
+ gate work, and neither implies the other:
35
+
36
+ - **`ac_altitude`** owns the **feature subset** (DD-09). `graduating` (or absent) keeps
37
+ normalized-title matching against the linked feature (`normalizeTitle` strips both `AC<n>` and
38
+ `R<n>` prefixes, so copied titles match); `task-local` skips that check — and nothing else.
39
+ - **`ac_numbering: task-local`** owns **task requirement bindings**. It opts the AC section into the
40
+ `L3.ac-requirement-coverage` loop, which reads **`Scenario:` titles only**: bind requirements with
41
+ `(req: R1; R2)` in the title, or ship a legacy `Scenario: R1 — …` title, which still binds by its
42
+ own prefix.
43
+
44
+ **Preferred task-local form** (regression/fix tasks): `Scenario: AC1 — <concrete outcome> (req: R1)`
45
+ with Given/When/Then, declaring **both** `ac_altitude: task-local` and `ac_numbering: task-local`.
46
+
47
+ Parsed checkbox rows (`- [ ] AC1 — <title>`) are a supported form: they join checklist parsing, the
48
+ feature-subset match, and the terminal-status unchecked-box count — but they do **not** enter the
49
+ Scenario-only requirement-binding loop, so a checkbox row never substitutes for a `(req: …)` binding.
50
+
51
+ Raw freeform bullets (`- AC1 — …`: no checkbox, no `Scenario:`) parse as nothing — no subset match,
52
+ no requirement binding, no box counting. They survive in the corpus as **legacy unparsed records**:
53
+ read them, don't author them, and don't reach for them to dodge traceability. Completed tasks keep
54
+ their records; new work uses one of the two parsed forms above.
37
55
 
38
56
  ## Two AC tiers (authoring convention)
39
57
 
@@ -145,9 +163,11 @@ The `spur task verdict` answer lint declares the bold span of a single-line crit
145
163
  (`- **AC2 — The roster runtime is gone (R3).** Given …, when …, then …`) and its head before the
146
164
  first ` — ` or `:` — so answer rows may key `AC2` or `AC2 — The roster runtime is gone (R3).`. Keep
147
165
  at least one answer row keyed to the verbatim feature scenario title the task graduates, and keep
148
- requirement ids in the corpus form they are checked against (`- **R1** — …`): `L3.requirements-format`
149
- matches `R\d+` followed by a space, so a colon after the bold span silently drops the whole section
150
- to a warning.
166
+ requirement ids in the template-taught corpus form, `- [ ] R1. <text>`: that (or an unbolded `R1.`
167
+ variant) is what the task-local binding loop parses — bold wrapping (`**R1**`) passes the
168
+ `L3.requirements-format` ratio but binds nothing — and a colon after the bold span still silently
169
+ drops the whole section to a warning. Bold AC heads are likewise a verdict-table id form, not the AC
170
+ authoring form: author new AC rows in the plain `- [ ] AC1 —` / `Scenario: AC1 —` shapes.
151
171
 
152
172
  ### The id is exactly the scenario title — no Gherkin body appended
153
173
 
@@ -276,3 +296,16 @@ Use `task-local` for a fix/refactor task whose regression criteria are not the f
276
296
  contract. Keep a `graduating` task's scenario titles identical to the feature's so DD-09 stays
277
297
  satisfied. Absent-altitude is `graduating` — set `task-local` explicitly only where the subset rule
278
298
  truly does not apply (do not silently default new tasks to it).
299
+
300
+ ### Choosing an AC form (decision table)
301
+
302
+ | Task situation | Author AC as | Frontmatter | Gate effect |
303
+ | --- | --- | --- | --- |
304
+ | Criteria that graduate the feature's ship contract | `Scenario: AC1 — <feature scenario title>` | `ac_altitude: graduating` (or absent) | DD-09 subset enforced — drifted titles report `L4.uncovered-task-scenario`; `(req: …)` binding only when `ac_numbering: task-local` is also declared |
305
+ | Task-local regression/fix criteria | `Scenario: AC1 — <concrete outcome> (req: R1)` + Given/When/Then | `ac_altitude: task-local` + `ac_numbering: task-local` | subset skipped; every `- [ ] R<n>.` requirement must be bound by some scenario's `(req: …)` |
306
+ | Simple verifiable checklist rows | `- [ ] AC1 — <title>` | either altitude, per the rows above | joins checklist parsing and the subset match; **no** requirement binding (`Scenario:`-only) |
307
+ | Legacy raw bullets | `- AC1 …` (no checkbox, no `Scenario:`) | n/a | unparsed: invisible to subset, binding, and box checks — preserve in place, never author new |
308
+
309
+ `task-local` altitude skips only the feature-subset check. It does not waive the unchecked-box check,
310
+ required sections, the proof-bound verdict, line anchors, or any other done check — AC syntax alone,
311
+ at any altitude, never makes a task done.
@@ -534,7 +534,8 @@ is the procedure. The backing is a combination of git CLI, `spur` CLI, and agent
534
534
  - **Coverage** — bullet count equals the commit count in `<since>..<until>`, and every short hash appears exactly once.
535
535
  - **Mapping** — per-category bullet counts match the conventional-type counts (`feat`→`Added`, `fix`→`Fixed`, other recognized types→`Changed`, unrecognized→`Other`).
536
536
  - **Header** — `[<version>] - <date>` matches the resolved `--version` (or detected tag) and the header date.
537
- - **HIGH** — all three checks pass. **MEDIUM** — coverage intact but mapping or header deviates. **LOW** — coverage broken (missing/duplicate hashes) or the range cannot be reconciled against git.
537
+ - **Citations** — a bullet that asserts API/library behavior or a version/dependency claim beyond its commit summary carries a source citation: an official docs URL, or a local `path:line` verified against the installed manifest/lockfile (`package.json`, lockfile, or the cited file). A commit hash is provenance for what shipped, not a citation for what the API/library does; an uncited claim is flagged inline on the bullet (`(uncited: <claim>)`) rather than silently dropped.
538
+ - **HIGH** — all checks pass with every API/library claim cited. **MEDIUM** — coverage intact but mapping or header deviates, or at least one API/library claim lacks a source citation. **LOW** — coverage broken (missing/duplicate hashes), the range cannot be reconciled against git, or a citation fails verification (dead URL, or a `path:line` that does not support the claim).
538
539
  - **Invariants:** Never mutates `CHANGELOG.md` directly — the command surface is stdout-only; writing it to a file (e.g. appending to `CHANGELOG.md`) is the operator's redirect choice, never the command's.
539
540
 
540
541
  ### 9. gitmsg
@@ -624,25 +625,27 @@ is the procedure. The backing is a combination of git CLI, `spur` CLI, and agent
624
625
  - **Inputs:** `--scope <path>` (default: entire working tree) — limits fixes to a file or directory. `--gate-log <path>` (R3, task 0482) — before fixing, read this captured validation-run log and start at the `file:line` anchors its findings name, rather than re-deriving the failure from a fresh gate run. `--findings <anchors>` (R3, task 0482) — space-separated `file:line` anchors already extracted from that log by the `test` hop; when present these ARE the failing set, so fix them in order and do not run the gate to discover what broke.
625
626
  - **Backing:** `inline` — lint + test fix loop.
626
627
  - **Behavior:** 0. **Start at the anchors, never at a discovery run.** If `--findings <anchors>` is given, that space-separated `file:line` list IS the failing set — open those locations first, in order, and fix them; do not run the gate to find out what broke. If only `--gate-log <path>` is given, read that log first and identify the failing findings (lint/test errors with their `file:line` anchors) before running the loop — the captured log is the authoritative source. Either way the first action of this hop is a read, not a gate run (R3, task 0482).
627
- 1. Run `bun run format` (add `-- <path>` if `--scope` is given) to settle formatter-only diffs first — `bun run lint` asserts `--error-on-warnings` + typecheck but does **not** rewrite formatting, so a formatter-only change (e.g. a multi-line import reflow) can pass `lint` locally yet still be unformatted. Formatting before linting removes that class of false-green.
628
- 2. Run `bun run lint` (add `-- <path>` if `--scope` is given). Collect all errors.
629
- 3. **Surface-change sweep (before any fix).** If the failing set is a **surface/parity** change — CLI noun/verb/flag surface, reference-file structure, or a renamed/moved symbol — do one upfront `rg` sweep across ALL test dirs + docs for stale references to the changed symbols (e.g. `rg -l "<old-noun>|<old-file>.md|Tier C" apps/*/tests plugins/sp/tests plugins/sp/skills docs`), and fix every pinned test/docs file in that sweep. A surface change is pinned in **multiple independent test files** (each with its own hardcoded noun→reference map); discovering them one-by-one via full-suite probes is the dominant waste — each wave costs a suite run and a fresh edit loop (task 0436 R2 lesson). One sweep produces the complete edit set upfront.
628
+ 1. Inspect the project's formatter command, then settle formatter-only diffs. Without `--scope`, use the project formatter (here `bun run format`). With `--scope`, use a formatter invocation whose only path operands are inside the selected scope — for Biome, `bunx biome check --write "$SCOPE"`. Never append a path to a script already containing `.`: `biome check . --write <path>` still writes across the repository. Preserve quoted paths and apply the same scope to every later formatting pass. If no scoped formatter is available, report that limitation and leave out-of-scope files untouched. Here `bun run lint` checks formatting without rewriting it and also runs typecheck; formatting errors fail that check.
629
+ 2. Run the project's read-only lint/type check (here `bun run lint`). Collect all errors. A repository-wide check may report outside-scope errors; report them without widening the authorized fix scope.
630
+ 3. **Surface-change sweep (before any fix).** If the failing set is a **surface/parity** change — CLI noun/verb/flag surface, reference-file structure, or a renamed/moved symbol — do one upfront `rg` sweep across ALL test dirs + docs for stale references to the changed symbols (e.g. `rg -l "<old-noun>|<old-file>.md|Tier C" apps/*/tests plugins/sp/tests plugins/sp/skills docs`), and fix every in-scope pinned test/docs file in that sweep; report outside-scope matches without editing them. A surface change is pinned in **multiple independent test files** (each with its own hardcoded noun→reference map); discovering them one-by-one via full-suite probes is the dominant waste — each wave costs a suite run and a fresh edit loop (task 0436 R2 lesson). One sweep produces the complete edit set upfront.
630
631
  4. If lint is clean, skip to step 6.
631
- 5. **Lint fix loop:** for each error, diagnose the root cause and apply the smallest fix. **Use targeted probes to verify each fix** (`bunx tsc --noEmit` on the affected package, `bun run lint -- <file>`) — do NOT re-run the full gate after every batch. Loop until lint is green.
632
- 6. Run `bun run test`. Collect all failures.
633
- 7. If tests are green, done.
634
- 8. **Test fix loop:** for each failure, diagnose (test bug vs implementation bug), apply the fix, re-run the **failing test only** (`bun test <file> --test-name-pattern "<test>"`). Do NOT re-run the full suite per fix — it is the dominant loop cost (task 0436 R2).
635
- 9. **Confirming run — standalone invocation only (at most once).** After all fixes, run `bun run format && bun run lint && bun run test` **at most once** to confirm. If it passes, the hop is done. **Skip this step entirely when `--gate-log` is set** (see step 10) — under the pipeline nothing consumes its verdict.
632
+ 5. **Lint fix loop:** for each in-scope error, diagnose the root cause and apply the smallest fix. **Use targeted probes to verify each fix** (`bunx tsc --noEmit` on the affected package, `bunx biome check <file>`) — do NOT re-run the full gate after every batch. Loop until lint is green, or report outside-scope blockers without editing them.
633
+ 6. For standalone execution, run `bun run test` and collect all failures. With `--gate-log`, use its failing set and targeted tests instead of running the full suite here.
634
+ 7. If tests are green, proceed to the applicable standalone confirmation or pipeline handoff below.
635
+ 8. **Test fix loop:** for each in-scope failure, diagnose (test bug vs implementation bug), apply the fix, re-run the **failing test only** (`bun test <file> --test-name-pattern "<test>"`). Report outside-scope blockers without editing them. Do NOT re-run the full suite per fix — it is the dominant loop cost (task 0436 R2).
636
+ 9. **Confirming run — standalone invocation only (at most once).** After all fixes, repeat the scope-safe formatting invocation from step 1, then run the selected validation command **at most once** to confirm (here the default project gate is `bun run spur-check`). If it passes, the hop is done. **Skip this step entirely when `--gate-log` is set** (see step 10) — under the pipeline nothing consumes its verdict.
636
637
  10. **Pipeline-awareness (R4, task 0483; confirming run dropped 2026-08-31).** `--gate-log` is set only by the pipeline's `test-fix` hop, so treat it as the pipeline signal. There, `test-recheck` runs the full `${vars.qualityGateCmd}` gate immediately after this hop returns — that is the **deciding** run, the only one that writes PASS to `.spur/run/<wbs>-test-gate.status`. A confirming run inside this hop decides nothing and costs a second full gate (~105 s here) to answer a question `test-recheck` is about to answer anyway. So under `--gate-log`: **run no full gate at all.** Your self-check is what step 8 already gives you — the failing tests you re-ran individually — plus **one `bun run lint`** (~7 s: Biome + typecheck) to catch type or lint breakage your fixes introduced. Then return and let `test-recheck` judge. `test-recheck` opens with the same cheap `${vars.gateProbeCmd}` probe, so a still-red tree is caught in seconds rather than by a full suite. Historical anti-pattern this replaces: 0482 ran the gate 3× plus a standalone `bun run test`, and `test-recheck` then ran it a 5th time.
637
638
  11. Report: list what was fixed (file + one-line summary per fix). If any error could not be resolved, report it explicitly — do not suppress.
638
- - **Invariants:** Never bypass with `--no-verify`, `--force`, or new `biome-ignore`/`eslint-disable` suppressions. Never skip or `.skip` a test to make the suite green. Fix the root cause, not the symptom. Never claim green on `bun run lint` alone — a formatter-only diff passes `lint` but fails the formatter; run `bun run format` (or assert it produces no diff) before declaring the gate clean. **Never re-run the full gate more than once per confirming pass** (R4) — use targeted probes during the fix loops and let the pipeline's `test-recheck` state be the deciding run.
639
- - **MANDATORY Exit Condition.** The ONLY way to complete successfully:
639
+ - **Invariants:** Never bypass with `--no-verify`, `--force`, or new `biome-ignore`/`eslint-disable` suppressions. Never skip or `.skip` a test to make the suite green. Fix the root cause, not the symptom. Here lint checks formatting and types; it does not prove that the full test gate passed. Keep every write inside `--scope`. **Never re-run the full gate more than once per confirming pass** (R4) — use targeted probes during the fix loops and let the pipeline's `test-recheck` state be the deciding run.
640
+ - **MANDATORY Exit Condition — standalone only.** At step 9, execute the following once; do not add another confirming run:
640
641
  1. Run validation command: `eval "$VALIDATION_CMD"`
641
642
  2. Capture exit code: `EXIT_CODE=$?`
642
643
  3. Output: `echo "EXIT_CODE=$EXIT_CODE"`
643
644
  4. **EXIT_CODE must equal 0**
644
645
 
645
- If EXIT_CODE != 0: NOT completed. MUST continue fixing.
646
+ If EXIT_CODE != 0: NOT completed. Continue the bounded fix loop for in-scope failures; report outside-scope blockers.
647
+
648
+ **Pipeline exit condition (`--gate-log` set):** return the fixes and the targeted-test/lint evidence from step 10 to `test-recheck`. Do not run the standalone validation command or claim a full-gate PASS; `test-recheck` owns that verdict.
646
649
 
647
650
  **Hallucination Red Flags — STOP if you think:**
648
651
  - "The errors look fixed" — check exit code, not appearance
@@ -671,7 +674,7 @@ is the procedure. The backing is a combination of git CLI, `spur` CLI, and agent
671
674
  └─────────────────────────────────────────────────┘
672
675
  ```
673
676
 
674
- The 7 phases map onto the format→lint→test behavior loop above: Phases 1–2 capture the gate, Phase 3 settles auto-fixable formatting, Phases 4–6 are the per-group root-cause fix loops, Phase 7 is the final verification re-run.
677
+ The 7 phases describe standalone execution: Phases 1–2 capture the gate, Phase 3 settles auto-fixable formatting within scope, Phases 4–6 are the per-group root-cause fix loops, Phase 7 is the final verification run. With `--gate-log`, use the supplied evidence instead of capturing the gate again, and hand off to `test-recheck` instead of running Phase 7's full validation.
675
678
 
676
679
  - **Fix Priority:**
677
680