@gobing-ai/spur 0.3.72 → 0.3.74

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 (101) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +48 -27
  3. package/config/pipeline-budgets.json +9 -5
  4. package/config/plugin-scripts.json +4 -0
  5. package/config/proportional-route-table.ts +155 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +1 -1
  7. package/config/rules/structure/protected-files.yaml +3 -0
  8. package/config/task-pipeline-proportional-migration-plan.md +79 -0
  9. package/config/templates/AGENTS.md +5 -0
  10. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
  11. package/config/workflows/basic.yaml +5 -2
  12. package/config/workflows/docs-pipeline.yaml +101 -54
  13. package/config/workflows/feature-dev.yaml +57 -27
  14. package/config/workflows/feature-lifecycle.yaml +1 -0
  15. package/config/workflows/history-anatomy.yaml +8 -4
  16. package/config/workflows/idea-pipeline.yaml +66 -33
  17. package/config/workflows/pr-review.yaml +31 -8
  18. package/config/workflows/task-lifecycle.yaml +28 -15
  19. package/config/workflows/task-pipeline.yaml +929 -844
  20. package/config/workflows/wayfinder-resolution.yaml +124 -90
  21. package/config/workflows/wrapup-pipeline.yaml +290 -107
  22. package/package.json +9 -9
  23. package/plugins/sp/plugin.json +1 -1
  24. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +2 -4
  25. package/plugins/sp/scripts/daily-summary/daily-summary.ts +2 -4
  26. package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
  27. package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
  28. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  29. package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
  30. package/plugins/sp/scripts/pr-reviewing.ts +7 -1
  31. package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
  32. package/plugins/sp/scripts/transition-shim-check.ts +6 -6
  33. package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
  34. package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
  35. package/plugins/sp/skills/spur-cli/references/self.md +17 -3
  36. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  37. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
  38. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
  39. package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
  40. package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
  41. package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
  42. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -2
  43. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
  44. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  45. package/schemas/state-machine-workflow.schema.json +3 -1
  46. package/schemas/transition-flow-workflow.schema.json +3 -1
  47. package/spur.js +12639 -10106
  48. package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.C60RJZRj.js} +78 -77
  49. package/web/_astro/BoardApp.ymfj1EHA.js +1 -0
  50. package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.Guk8VuNu.js} +1 -1
  51. package/web/_astro/{arc.BySSh34M.js → arc.CAZtlWJM.js} +1 -1
  52. package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.9-XbF_Tz.js} +1 -1
  53. package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.DC4WLx3L.js} +1 -1
  54. package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.JgjUQMgG.js} +1 -1
  55. package/web/_astro/channel.Bxxe2Byc.js +1 -0
  56. package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.D_JFiXa-.js} +1 -1
  57. package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.Bk_RxoeT.js} +1 -1
  58. package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.a_Rj2VxL.js} +1 -1
  59. package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.C2NoS1U6.js} +1 -1
  60. package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.D1Xn6CNa.js} +1 -1
  61. package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.BhJrSBsX.js} +1 -1
  62. package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.ByVGduYQ.js} +1 -1
  63. package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.B47Paunq.js} +1 -1
  64. package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.BERMSD5C.js} +1 -1
  65. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.BERMSD5C.js} +1 -1
  66. package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.O_rHGlhU.js} +1 -1
  67. package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.qCW2GdNp.js} +1 -1
  68. package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.C5Y4lH_u.js} +1 -1
  69. package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.DxZmRXxF.js} +1 -1
  70. package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.qSxZeYSK.js} +1 -1
  71. package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.-7vl3bXs.js} +1 -1
  72. package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.BHVV-UH3.js} +1 -1
  73. package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DHAuW9sK.js} +1 -1
  74. package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.DMRLQwPN.js} +1 -1
  75. package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.B3EpPp_8.js} +1 -1
  76. package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.BgUrExXM.js} +1 -1
  77. package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.CU-9yVN4.js} +1 -1
  78. package/web/_astro/{index.B5MTfe7k.css → index.BhvM3djt.css} +1 -1
  79. package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.CvVTCRLe.js} +1 -1
  80. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.BUzOfKPf.js} +1 -1
  81. package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.BBTb7ziz.js} +1 -1
  82. package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.CTQQr70i.js} +1 -1
  83. package/web/_astro/{linear.D7uqzENp.js → linear.CLKlJPiS.js} +1 -1
  84. package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.BA4wFhLP.js} +4 -4
  85. package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.t1vG2l5e.js} +1 -1
  86. package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.C8zJhyTu.js} +1 -1
  87. package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DqYYvb7T.js} +1 -1
  88. package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.lzfDZ9nI.js} +1 -1
  89. package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.lm6N5ORM.js} +1 -1
  90. package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.E6_IV4Dj.js} +1 -1
  91. package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.Bnv6Ok2p.js} +1 -1
  92. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js} +1 -1
  93. package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BhRgqSLa.js} +1 -1
  94. package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.pjb5DMC9.js} +1 -1
  95. package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.CW20KUng.js} +1 -1
  96. package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.vX9_yuMl.js} +1 -1
  97. package/web/index.html +2 -2
  98. package/config/corpus-baseline.json +0 -1637
  99. package/config/workflow-composition-baseline.json +0 -576
  100. package/web/_astro/BoardApp.E12MFjOS.js +0 -1
  101. package/web/_astro/channel.5cYKr5cs.js +0 -1
@@ -369,16 +369,14 @@ export async function getSpurHistoryHealth(
369
369
  const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
370
370
  const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
371
371
  const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
372
- const body = `Finding: ${key}\\nObserved ${lp.repeats} redundant invocations in session ${lp.sessionId} (steps #${lp.fromSeq ?? 1}→#${lp.toSeq ?? lp.repeats}). Wasted tokens: ~${lp.wastedTokens}.`;
373
- const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
372
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
374
373
  remediationProposals.push({ key, title, command });
375
374
  }
376
375
 
377
376
  if (toolCalls > 0 && errorRatePct > 10) {
378
377
  const key = 'reliability:tooling:high-error-rate';
379
378
  const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
380
- const body = `Finding: ${key}\\nObserved ${toolErrors} errors across ${toolCalls} tool calls (${errorRatePct.toFixed(1)}% error rate) on ${date}.`;
381
- const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
379
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
382
380
  remediationProposals.push({ key, title, command });
383
381
  }
384
382
 
@@ -198,6 +198,7 @@ function parseProvenance(reportMarkdown) {
198
198
  contractDigest: String(obj.contractDigest ?? ""),
199
199
  skillDigest: String(obj.skillDigest ?? ""),
200
200
  workflowDigest: String(obj.workflowDigest ?? ""),
201
+ helperDigest: String(obj.helperDigest ?? ""),
201
202
  coverage: coverage.map((c) => ({
202
203
  source: String(c.source ?? ""),
203
204
  status: String(c.status ?? ""),
@@ -244,6 +245,8 @@ function decideCache(cached, current, opts) {
244
245
  reasons.push("logic-changed:skill");
245
246
  if (cached.workflowDigest !== current.workflowDigest)
246
247
  reasons.push("logic-changed:workflow");
248
+ if (cached.helperDigest !== current.helperDigest)
249
+ reasons.push("logic-changed:helper");
247
250
  const currentSources = new Set(current.coverage.map((c) => c.source));
248
251
  if (cached.coverage.some((c) => !currentSources.has(c.source)))
249
252
  reasons.push("coverage-degraded");
@@ -473,6 +476,7 @@ function buildProvenance(opts) {
473
476
  contractDigest: logicDigest(opts.contractFile),
474
477
  skillDigest: logicDigest(opts.skillDir),
475
478
  workflowDigest: logicDigest(opts.workflowFile),
479
+ helperDigest: logicDigest(opts.helperFile),
476
480
  coverage,
477
481
  runId: opts.runId,
478
482
  currentArtifactPath: opts.artifact,
@@ -503,6 +507,7 @@ var YAML_KEYS = [
503
507
  "contractDigest",
504
508
  "skillDigest",
505
509
  "workflowDigest",
510
+ "helperDigest",
506
511
  "runId",
507
512
  "currentArtifactPath",
508
513
  "baselineArtifactPath",
@@ -595,7 +600,7 @@ function diffPorcelain(before, now, expects) {
595
600
  return [...porcelainPaths(now)].filter((p) => !beforePaths.has(p) && !expects.has(p)).sort();
596
601
  }
597
602
  var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh, publish";
598
- var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]";
603
+ var PROBE_USAGE = "<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
599
604
  function parseFlags(args) {
600
605
  const out = {};
601
606
  for (let i = 0;i < args.length; i++) {
@@ -729,6 +734,7 @@ ${result.problems.map((p) => `- ${p}
729
734
  skillDir: f["skill-dir"],
730
735
  contractFile: f.contract,
731
736
  workflowFile: f.workflow,
737
+ helperFile: f.helper,
732
738
  contractVersion: f["contract-version"],
733
739
  runId: f["run-id"],
734
740
  spurVersion: f["spur-version"]
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * ADR-079 makes cache validity a *derived* fact, not a stored claim: a cached report is reusable
6
6
  * only for its model-authored half, and only when a freshly derived semantic digest of the analyze
7
- * artifact plus the contract/skill/workflow logic digests all match what the cache recorded.
7
+ * artifact plus the contract/skill/workflow/helper logic digests all match what the cache
8
+ * recorded (helper digest: task 0771 — the deterministic half is part of cache identity).
8
9
  *
9
10
  * This script performs deterministic file, hash, and schema work only — no finding, remediation,
10
11
  * severity, or ranking logic (that is judgment, owned by the sp:history-anatomy skill). Jobs:
@@ -68,6 +69,8 @@ export interface CacheProvenance {
68
69
  contractDigest: string;
69
70
  skillDigest: string;
70
71
  workflowDigest: string;
72
+ /** Digest of the executing helper twin itself (0771): a changed deterministic half invalidates. */
73
+ helperDigest: string;
71
74
  coverage: Array<{ source: string; status: string; lastImportedAt: string | null }>;
72
75
  // 0660 R7 audit fields. Recorded in the published frontmatter for provenance; deliberately
73
76
  // NOT part of the invalidation matrix — a changed run id or executor is not stale evidence.
@@ -243,6 +246,7 @@ export function parseProvenance(reportMarkdown: string): CacheProvenance | null
243
246
  contractDigest: String(obj.contractDigest ?? ''),
244
247
  skillDigest: String(obj.skillDigest ?? ''),
245
248
  workflowDigest: String(obj.workflowDigest ?? ''),
249
+ helperDigest: String(obj.helperDigest ?? ''),
246
250
  coverage: coverage.map((c) => ({
247
251
  source: String(c.source ?? ''),
248
252
  status: String(c.status ?? ''),
@@ -300,6 +304,7 @@ export function decideCache(
300
304
  if (cached.contractDigest !== current.contractDigest) reasons.push('logic-changed:contract');
301
305
  if (cached.skillDigest !== current.skillDigest) reasons.push('logic-changed:skill');
302
306
  if (cached.workflowDigest !== current.workflowDigest) reasons.push('logic-changed:workflow');
307
+ if (cached.helperDigest !== current.helperDigest) reasons.push('logic-changed:helper');
303
308
 
304
309
  // Coverage cannot degrade: the cache must not claim broader coverage than the current
305
310
  // analyze covers. If the cached report covered a source the current analyze no longer does,
@@ -601,6 +606,7 @@ export interface ProbeOptions {
601
606
  skillDir?: string;
602
607
  contractFile?: string;
603
608
  workflowFile?: string;
609
+ helperFile?: string;
604
610
  contractVersion?: string;
605
611
  runId?: string;
606
612
  spurVersion?: string;
@@ -660,6 +666,7 @@ export function buildProvenance(opts: ProbeOptions): CacheProvenance {
660
666
  contractDigest: logicDigest(opts.contractFile),
661
667
  skillDigest: logicDigest(opts.skillDir),
662
668
  workflowDigest: logicDigest(opts.workflowFile),
669
+ helperDigest: logicDigest(opts.helperFile),
663
670
  coverage,
664
671
  runId: opts.runId,
665
672
  currentArtifactPath: opts.artifact,
@@ -697,6 +704,7 @@ const YAML_KEYS: Array<keyof CacheProvenance> = [
697
704
  'contractDigest',
698
705
  'skillDigest',
699
706
  'workflowDigest',
707
+ 'helperDigest',
700
708
  'runId',
701
709
  'currentArtifactPath',
702
710
  'baselineArtifactPath',
@@ -823,7 +831,7 @@ export function diffPorcelain(before: string, now: string, expects: Set<string>)
823
831
  const VALID_COMMANDS = 'digest, check, paths, assert-clean, probe, stamp, refresh, publish';
824
832
  const PROBE_USAGE =
825
833
  '<script> probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] ' +
826
- '[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>]';
834
+ '[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]';
827
835
 
828
836
  /** `--key value` / `--flag` → record. Bare flags become `"true"` so `--recompute` needs no value. */
829
837
  function parseFlags(args: string[]): Record<string, string | undefined> {
@@ -956,6 +964,7 @@ export function runCacheCli(argv: string[]): CacheCliResult {
956
964
  skillDir: f['skill-dir'],
957
965
  contractFile: f.contract,
958
966
  workflowFile: f.workflow,
967
+ helperFile: f.helper,
959
968
  contractVersion: f['contract-version'],
960
969
  runId: f['run-id'],
961
970
  spurVersion: f['spur-version'],
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * inline-pipeline-parity-check — two-sided gate between the inline pipeline
4
+ * driver's documented action/guard set and the resolved action/guard sets in
5
+ * `.spur/workflows/task-pipeline.yaml` and `.spur/workflows/idea-pipeline.yaml`
6
+ * (task 0755 R2/R3).
7
+ *
8
+ * The driver reference at `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`
9
+ * documents the set of action and guard kinds it implements. The two runtime
10
+ * pipelines are the only consumers the driver needs to keep in step with. The
11
+ * check is a symmetric set diff: an element present in one and absent in the
12
+ * other fails the check and names the element.
13
+ *
14
+ * The set is defined in {@link DOCUMENTED} below; the driver's markdown list is
15
+ * the human mirror. Update both when the driver adds or drops a kind.
16
+ *
17
+ * Usage:
18
+ * bun plugins/sp/scripts/inline-pipeline-parity-check.ts
19
+ * [--root <path>] default: repo root
20
+ *
21
+ * Exit code: 0 when the sets agree; 1 on any divergence. Violations are printed
22
+ * to stderr; a summary to stdout.
23
+ */
24
+
25
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
26
+ import { join, resolve } from 'node:path';
27
+ import { parse as parseYaml } from 'yaml';
28
+
29
+ /** Documented action and guard set. Must stay in lockstep with the
30
+ * "Supported action and guard set (0755 R2 parity contract)" section in
31
+ * `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`. The
32
+ * driver supports a kind if ANY workflow in `.spur/workflows/*.yaml` uses
33
+ * it (the driver applies to any selected pipeline per its reference doc). */
34
+ const DOCUMENTED = {
35
+ actions: new Set([
36
+ 'shell',
37
+ 'note',
38
+ 'doctor.probe',
39
+ 'file.read.into-var',
40
+ 'hitl.confirm',
41
+ 'agent.run',
42
+ 'proof.fingerprint',
43
+ 'run.artifact',
44
+ 'command.gate',
45
+ ]),
46
+ guards: new Set(['always', 'shell']),
47
+ } as const;
48
+
49
+ /** Directory of workflow definitions the driver is responsible for. */
50
+ const WORKFLOW_DIR = join('config', 'workflows');
51
+
52
+ /** Walk a state list and yield every `kind:` value found in `onEnter` action
53
+ * lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
54
+ function collectActionKinds(states: unknown): Set<string> {
55
+ const out = new Set<string>();
56
+ if (!Array.isArray(states)) return out;
57
+ for (const state of states) {
58
+ if (typeof state !== 'object' || state === null) continue;
59
+ const onEnter = (state as { onEnter?: unknown }).onEnter;
60
+ if (!Array.isArray(onEnter)) continue;
61
+ for (const action of onEnter) {
62
+ if (typeof action !== 'object' || action === null) continue;
63
+ const kind = (action as { kind?: unknown }).kind;
64
+ if (typeof kind === 'string') out.add(kind);
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Walk a transition list and yield every `guard.kind` value. */
71
+ function collectGuardKinds(transitions: unknown): Set<string> {
72
+ const out = new Set<string>();
73
+ if (!Array.isArray(transitions)) return out;
74
+ for (const transition of transitions) {
75
+ if (typeof transition !== 'object' || transition === null) continue;
76
+ const guard = (transition as { guard?: { kind?: unknown } }).guard;
77
+ const kind = guard?.kind;
78
+ if (typeof kind === 'string') out.add(kind);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /** Symmetric set diff. Returns elements in `a` but not in `b`, and vice versa. */
84
+ function diff<T>(a: Set<T>, b: Set<T>): { onlyInA: T[]; onlyInB: T[] } {
85
+ const onlyInA: T[] = [];
86
+ const onlyInB: T[] = [];
87
+ for (const x of a) if (!b.has(x)) onlyInA.push(x);
88
+ for (const x of b) if (!a.has(x)) onlyInB.push(x);
89
+ return { onlyInA, onlyInB };
90
+ }
91
+
92
+ function parseArgs(argv: string[]): { root: string } {
93
+ let root = resolve('.');
94
+ for (let i = 0; i < argv.length; i += 1) {
95
+ const arg = argv[i];
96
+ if (arg === '--root' && i + 1 < argv.length) {
97
+ root = resolve(argv[++i] ?? '.');
98
+ } else if (arg === '--help' || arg === '-h') {
99
+ process.stdout.write('Usage: bun inline-pipeline-parity-check.ts [--root <path>]\n');
100
+ process.exit(0);
101
+ }
102
+ }
103
+ return { root };
104
+ }
105
+
106
+ function listWorkflowFiles(dir: string): string[] {
107
+ let entries: string[];
108
+ try {
109
+ entries = readdirSync(dir);
110
+ } catch {
111
+ return [];
112
+ }
113
+ return entries.filter((e) => e.endsWith('.yaml')).map((e) => join(dir, e));
114
+ }
115
+
116
+ async function main(): Promise<number> {
117
+ const { root } = parseArgs(process.argv.slice(2));
118
+ const errors: string[] = [];
119
+
120
+ const workflowDir = join(root, WORKFLOW_DIR);
121
+ if (!statSync(workflowDir, { throwIfNoEntry: false })) {
122
+ process.stderr.write(`inline-pipeline-parity-check: workflow directory not found: ${workflowDir}\n`);
123
+ return 1;
124
+ }
125
+
126
+ const files = listWorkflowFiles(workflowDir);
127
+ if (files.length === 0) {
128
+ process.stderr.write(`inline-pipeline-parity-check: no .yaml workflows found in ${workflowDir}\n`);
129
+ return 1;
130
+ }
131
+
132
+ const unionActions = new Set<string>();
133
+ const unionGuards = new Set<string>();
134
+ const perFileKinds: { path: string; actions: Set<string>; guards: Set<string> }[] = [];
135
+
136
+ for (const path of files) {
137
+ let parsed: unknown;
138
+ try {
139
+ parsed = parseYaml(readFileSync(path, 'utf8'));
140
+ } catch (err) {
141
+ errors.push(`${path}: failed to parse (${err instanceof Error ? err.message : String(err)})`);
142
+ continue;
143
+ }
144
+ if (typeof parsed !== 'object' || parsed === null) {
145
+ continue;
146
+ }
147
+ const def = parsed as { states?: unknown; transitions?: unknown };
148
+ const actions = collectActionKinds(def.states);
149
+ const guards = collectGuardKinds(def.transitions);
150
+ perFileKinds.push({ path, actions, guards });
151
+ for (const a of actions) unionActions.add(a);
152
+ for (const g of guards) unionGuards.add(g);
153
+ }
154
+
155
+ const actionDiff = diff(unionActions, DOCUMENTED.actions);
156
+ const guardDiff = diff(unionGuards, DOCUMENTED.guards);
157
+
158
+ for (const x of actionDiff.onlyInA) {
159
+ const usedIn = perFileKinds.filter((f) => f.actions.has(x)).map((f) => f.path);
160
+ errors.push(`action kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
161
+ }
162
+ for (const x of actionDiff.onlyInB) {
163
+ errors.push(`action kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
164
+ }
165
+ for (const x of guardDiff.onlyInA) {
166
+ const usedIn = perFileKinds.filter((f) => f.guards.has(x)).map((f) => f.path);
167
+ errors.push(`guard kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
168
+ }
169
+ for (const x of guardDiff.onlyInB) {
170
+ errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
171
+ }
172
+
173
+ if (errors.length > 0) {
174
+ process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
175
+ for (const e of errors) process.stderr.write(` - ${e}\n`);
176
+ return 1;
177
+ }
178
+
179
+ process.stdout.write(
180
+ `inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
181
+ );
182
+ return 0;
183
+ }
184
+
185
+ process.exit(await main());
@@ -325,7 +325,11 @@ function hasPendingRequest(comments, head, login) {
325
325
  }
326
326
  function requireExpectedHead(args, pr) {
327
327
  const expected = args.flags.get("--head");
328
- if (expected && expected !== pr.headRefOid) {
328
+ if (!expected) {
329
+ writeStatus(args, "FAIL");
330
+ fail(args, "--head <sha> is required and must match the reviewed request HEAD", 2);
331
+ }
332
+ if (expected !== pr.headRefOid) {
329
333
  writeStatus(args, "FAIL");
330
334
  fail(args, `PR HEAD moved from ${expected.slice(0, 7)} to ${pr.headRefOid.slice(0, 7)} \u2014 request a new review`, 2);
331
335
  }
@@ -519,8 +519,14 @@ function hasPendingRequest(comments: readonly GhIssueComment[], head: string, lo
519
519
  }
520
520
 
521
521
  function requireExpectedHead(args: ParsedArgs, pr: GhPr): void {
522
+ // 0771: head pinning only protects when the flag actually carries a SHA — an empty or
523
+ // missing --head must fail loud instead of silently reviewing whatever HEAD is current.
522
524
  const expected = args.flags.get('--head');
523
- if (expected && expected !== pr.headRefOid) {
525
+ if (!expected) {
526
+ writeStatus(args, 'FAIL');
527
+ fail(args, '--head <sha> is required and must match the reviewed request HEAD', 2);
528
+ }
529
+ if (expected !== pr.headRefOid) {
524
530
  writeStatus(args, 'FAIL');
525
531
  fail(
526
532
  args,
@@ -145,7 +145,7 @@ function main(): void {
145
145
 
146
146
  const dbPath = join(process.cwd(), '.spur', 'spur.db');
147
147
  if (!existsSync(dbPath)) {
148
- fail(wbs, [`spur database not found at ${dbPath} — run a real history import first`]);
148
+ fail(wbs, [`local spur database not found at ${dbPath} — run a real history import first`]);
149
149
  }
150
150
 
151
151
  let count: number;
@@ -8,11 +8,11 @@
8
8
  * so it is a grep target and a review signal, and a manifest entry in
9
9
  * `config/transition-shims.json` records who owns it and when it can be removed.
10
10
  *
11
- * The gate is deliberately two-sided, matching `config/corpus-baseline.json`
12
- * semantics (corpus-check.ts): a marker with no manifest entry FAILS, and a
13
- * manifest entry whose marker no longer appears in source FAILS. Without the
14
- * second half the manifest would rot into a permanent suppression list — the
15
- * exact invisible-debt pattern this gate exists to end.
11
+ * The gate is deliberately two-sided: a marker with no manifest entry
12
+ * FAILS, and a manifest entry whose marker no longer appears in source
13
+ * FAILS. Without the second half the manifest would rot into a permanent
14
+ * suppression list the exact invisible-debt pattern this gate exists to
15
+ * end.
16
16
  *
17
17
  * Marker convention (docs/04_DESIGN.md §2.5):
18
18
  * // @transition-shim(<id>) — <one line on what this keeps working>
@@ -134,7 +134,7 @@ function scanMarkers(cwd: string, roots: string[]): Map<string, Set<string>> {
134
134
  return found;
135
135
  }
136
136
 
137
- /** Load the manifest; a missing file degrades to no entries (corpus-check parity). */
137
+ /** Load the manifest; a missing file degrades to no entries (fail-open). */
138
138
  function loadManifest(path: string): { entries: ManifestEntry[]; raw: string | null } {
139
139
  if (!existsSync(path)) return { entries: [], raw: null };
140
140
  const raw = readFileSync(path, 'utf8');
@@ -269,6 +269,10 @@ function extractAcIdentities(taskContent: string, featureContent: string | null)
269
269
  const leading = label.split(/\s+/)[0] ?? '';
270
270
  if (leading && leading !== label) identities.add(leading);
271
271
  }
272
+ for (const m of section.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
273
+ const title = (m[1] ?? '').trim();
274
+ if (title) identities.add(title);
275
+ }
272
276
  if (featureContent !== null) {
273
277
  for (const m of featureContent.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
274
278
  const title = (m[1] ?? '').trim();
@@ -164,12 +164,15 @@ superskill script contract.
164
164
  8. **Collect** — `<script> collect --since "$requestedAt" --head "$requestHead" --json`. Normalize
165
165
  only the requested HEAD and request window; then invoke `status` with the same bounds for
166
166
  PR/base/CI/local state. Never turn an absent, stale, or moved-HEAD result into `clean`.
167
+ `--head` is mandatory on wait/collect/status (0771): an empty or missing pin fails loud
168
+ instead of silently reviewing whatever HEAD is current.
167
169
 
168
170
  ## Fix mode
169
171
 
170
172
  Only in `fix` mode:
171
173
 
172
- 1. **Collect before editing.** Invoke direct `collect --json` plus `status --json` and require a
174
+ 1. **Collect before editing.** Invoke direct `collect --json --head "$(git rev-parse HEAD)"` plus
175
+ `status --json --head "$(git rev-parse HEAD)"` and require a
173
176
  completed result for the current pushed HEAD. If the result is pending, stale, or unavailable,
174
177
  report it and stop without source edits. If it is explicitly clean, report no fixes and stop.
175
178
  2. **Validate each finding.** Open the referenced code; trace callers/callees; verify the problem
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: spur-cli-self
3
- description: "spur-cli noun reference for `spur self`: self-management verbs — scaffold (`init`), schema migrations (`migrate`), local web server (`serve`), and status overview (`status`). Each verb mounts the same command builder as its legacy top-level noun, which remains a hidden alias over the identical command."
3
+ description: "spur-cli noun reference for `spur self`: self-management verbs — scaffold (`init`), database maintenance (`maintain`), schema migrations (`migrate`), local web server (`serve`), and status overview (`status`). Each verb mounts the same command builder as its legacy top-level noun, which remains a hidden alias over the identical command."
4
4
  see_also:
5
5
  - spur-cli
6
6
  ---
7
7
 
8
8
  # spur self - self-management verbs
9
9
 
10
- `spur self` hosts the four self-management verbs. Each verb is the canonical path for a command
11
- that also remains registered as a legacy top-level **hidden alias** (`spur init`, `spur migrate`,
10
+ `spur self` hosts the self-management verbs. Each verb is the canonical path for a command
11
+ that also remains registered as a legacy top-level **hidden alias** (`spur init`, `spur maintain`, `spur migrate`,
12
12
  `spur serve`, `spur status`) so existing scripts, workflow YAML, and habits keep working unchanged.
13
13
  Both paths share the same command builder: identical flags, output, and exit codes. The legacy
14
14
  top-level forms are omitted from `spur --help`, leaving `self` as the visible surface.
@@ -18,6 +18,7 @@ top-level forms are omitted from `spur --help`, leaving `self` as the visible su
18
18
  | Verb | Purpose | Key flags |
19
19
  | ---- | ------- | --------- |
20
20
  | `init` | Scaffold a new Spur project in the current directory | `--name <name>` `--force` `--minimal` `--json` |
21
+ | `maintain` | Run database maintenance: PRAGMA optimize, WAL truncation, optional VACUUM | `--vacuum` `--json` |
21
22
  | `migrate` | Apply CLI-owned schema migrations | `--json` |
22
23
  | `serve` | Start the Spur web server (local fallback) | `--port <n>` `--host <addr>` `--no-open` `--cwd <path>` `--json` |
23
24
  | `status [path]` | Show project and git status for a Spur project | `--json` |
@@ -43,6 +44,19 @@ Materializes the `.spur/` directory tree with config, docs, rules, and workflow
43
44
  (Phase 1.5 / 1.6) run immediately after this verb completes — see **[init.md](init.md)** for the
44
45
  probe protocol and rule-glob adaptation procedure.
45
46
 
47
+ ## `self maintain` - run database maintenance
48
+
49
+ ```bash
50
+ spur self maintain # run PRAGMA optimize + wal_checkpoint(TRUNCATE)
51
+ spur self maintain --vacuum # deep maintenance: VACUUM + optimize + checkpoint
52
+ spur self maintain --json # machine-readable
53
+ ```
54
+
55
+ Executes SQLite database maintenance operations on `.spur/spur.db`:
56
+ - `PRAGMA optimize;` to refresh query planner statistics in `sqlite_stat1`.
57
+ - `PRAGMA wal_checkpoint(TRUNCATE);` to checkpoint committed WAL frames and truncate WAL to 0 bytes.
58
+ - `--vacuum` runs `VACUUM;` first for page compaction and B-tree defragmentation (requires 2x free disk space).
59
+
46
60
  ## `self migrate` - apply CLI-owned schema migrations
47
61
 
48
62
  ```bash
@@ -51,7 +51,7 @@ re-reading or re-tokenizing the task.
51
51
  | `batch-create` | Create many tasks from a validated JSON array | `--file <path>` `--folder` `--json` |
52
52
  | `record <wbs>` | Write `Testing` from a verify verdict (deterministic); bare-`## Review` fallback only; optional Solution + transition | `--verdict-file <path>` `--solution-from-diff` `--transition <status>` `--folder` `--json` |
53
53
  | `verdict <wbs>` | Derive PASS/PARTIAL/FAIL/UNKNOWN from verify answer text → verdict JSON; see [answer-file shape](tasks/verbs.md#answer-file-shape-what---from-answer-parses) | `--from-answer <path>` `--folder` `--json` |
54
- | `check [wbs]` | Four-layer validation; the readiness matrix; `--fix` repairs structural findings in place | `--strict` `--as <status>` `--strict-core` `--fix` `--folder` `--json` |
54
+ | `check [wbs]` | Four-layer validation; `--fix` repairs structure; `--corpus` is the explicit unsuppressed audit | `--strict` `--as <status>` `--strict-core` `--fix` `--folder` `--corpus` `--since <ref>` `--json` |
55
55
  | `resolve <file-path>` | Map a file path to its owning task WBS | `--strict` `--folder` `--json` |
56
56
  | `path <wbs>` | Map a WBS to its absolute task file path (inverse of `resolve`) | `--folder` `--json` |
57
57
  | `run-link <wbs>` | Record pipeline run provenance link for task | `--source <src>` `--run-id <id>` `--json` |
@@ -229,3 +229,26 @@ verifying shape, or stand in a `note` action until the path is proven, then swap
229
229
  `agent.run` input referencing a command rather than a raw prompt, guards a single predicate
230
230
  ([workflow-fit-and-tuning.md](workflow-fit-and-tuning.md#3-node-simplicity-budget)).
231
231
  - [ ] Validates clean AND dry-run reaches the expected terminal state.
232
+
233
+ ## Optional version literal (task 0756)
234
+
235
+ Both dialects accept an optional root `version` field. The literal is **behavior-neutral** — it
236
+ exists as an identity tag, not a routing key. The contract:
237
+
238
+ - **Absent** → reported as `unversioned`. The default for all 11 shipped definitions.
239
+ - **Present, non-empty string** → reported as `explicit(<literal>)`. The literal is wrapped in
240
+ parentheses verbatim — no parsing, no ordering, no compatibility check.
241
+ - **Present, empty string (`version: ""`)** → **rejected** with a diagnostic naming the empty
242
+ value. The rejection lives in the resolve/preflight seam
243
+ (`packages/app/src/workflow/workflow-resolver.ts`), not in the dialect JSON schemas: those carry
244
+ `minLength: 1` for editors and Ajv consumers, but the load path validates against the engine's
245
+ Zod schema, which has no minimum. Move the check upstream once
246
+ `@gobing-ai/ts-dual-workflow-engine` ships `z.string().min(1)` on the root version.
247
+
248
+ The literal folds into the definition digest (`packages/app/src/workflow/composition-baseline.ts`),
249
+ so a version-only edit changes the digest with zero behavior change. `show` and `trace` do **not**
250
+ surface the literal by default — the digest stays the rendered run identity (D8 decision D5).
251
+
252
+ **No registry, no semver parser, no compatibility engine.** A future-major requirement needs
253
+ objective evidence: a consumer that branches on version, or a real drift incident the digest
254
+ diagnostic could not disambiguate. Neither exists today.
@@ -140,10 +140,10 @@ reports it. Do not invent a second threshold; author to the one that is frozen (
140
140
  | Node count | Every node earns its transition round-trip | A node that always runs immediately after another, with no guard between them, is one node |
141
141
 
142
142
  **When a node breaches the budget, do not reformat to dodge the measure.** Joining five lines with
143
- `&&` moves the complexity, not the ownership. Pick an owner from the five recorded options in
143
+ `&&` moves the complexity, not the ownership. Pick one of the four remaining owners from
144
144
  `docs/design/workflow-shell-ownership.md`: public `spur` verb (consent-gated), application service,
145
- least-privilege built-in action kind, workflow-relative external extension, or a recorded
146
- stays-shell exception with its reason in `config/workflow-composition-baseline.json`.
145
+ least-privilege built-in action kind, or workflow-relative external extension. (0775 retired the
146
+ recorded stays-shell exception along with the suppression snapshot.)
147
147
 
148
148
  **Advisory posture is binding.** Composition findings never block a run, never change a `validate`
149
149
  exit status, and are never a reason to hot-edit an executing pipeline. Surface them; fix on operator
@@ -193,8 +193,8 @@ Steps:
193
193
  2. **Return each node's work to its owner** — judgment nodes to the command or skill they should
194
194
  have been calling; a genuinely useful shell sequence to one script under its owning surface.
195
195
  3. **Rewrite the entry surface as the procedure**, in the order the graph ran.
196
- 4. **Delete the YAML and its `config/workflow-composition-baseline.json` entries** in the same
197
- change. A baseline entry whose action no longer exists fails the two-sided check.
196
+ 4. **Delete the YAML** in the same change. (0775 retired the composition-baseline entries that
197
+ used to be deleted alongside it; the live definition is the only artifact left.)
198
198
  5. **Record the demotion** and its trigger, so the next author does not re-promote it by reflex.
199
199
 
200
200
  ### optimize — refine an accepted workflow in place
@@ -231,10 +231,10 @@ agent prompts where a first-class surface would own it better?" — nothing more
231
231
  raw prompt length only sets the reported severity (<200 low / ≤1000 medium / >1000 high).
232
232
 
233
233
  **How to run it:** it is not a separate command — `spur workflow validate <file> --json` returns
234
- `composition: {findings[], suppressed}` on the valid path; human mode prints the advisory to stderr
235
- with exit 0. Actions with a recorded disposition in
236
- `config/workflow-composition-baseline.json` (resolved by walking up from the workflow file) are
237
- counted in `suppressed`, not `findings`.
234
+ `composition: {findings[]}` on the valid path; human mode prints the advisory to stderr
235
+ with exit 0. Findings are derived from the workflow definition itself
236
+ (`extractResolvedWorkflowFacts`, guarded by `composition-baseline.test.ts`); 0775 retired the
237
+ suppression snapshot, so every finding is reported — none are suppressed.
238
238
 
239
239
  **How to read it, and the fix path per defect class:**
240
240
 
@@ -153,6 +153,23 @@ CLI does.
153
153
 
154
154
  ## Gotchas
155
155
 
156
+ ### Bounded execution and recovery (0777)
157
+
158
+ - During planning/refinement of corpus-scale work, record an execution budget, persisted-artifact
159
+ paths and requireDiff guidance in the task Design. For classification-only work, declare
160
+ `mutationPolicy: none`; its task/evidence change is the deliverable, not invented source edits.
161
+ Preserve partial artifacts at the budget boundary; resume only against the current task contract.
162
+ - Before accepting a child/watcher result, compare its run ID with the dispatched run ID and check
163
+ current trace state through Spur. If using a run log as evidence, require its mtime to be at least
164
+ the dispatch time. A mismatch or stale timestamp is not completion evidence. Do not scrape terminals.
165
+ Bound each watch invocation to 10 minutes or 20 polls, whichever comes first; persist the last
166
+ confirmed identity/state and report a checkpoint before continuing. A watcher timeout does not
167
+ cancel the owned run or authorize launching a replacement.
168
+ - Mark superseded scratch with `SUPERSEDED` and a pointer to the authoritative task Design. Never
169
+ let a scratch instruction override the live task, even when its old run is still readable.
170
+ - Checker-policy changes require one explicit unsuppressed audit (T10); ordinary corpus commit
171
+ prep checks affected documents and linked evidence (T11), not the corpus. No acceptance snapshots.
172
+
156
173
  1. **Never skip a gate.** A clean `feature check` is the only proof the AC is valid; a
157
174
  passing `batch-create` is the only proof the decomposition is well-formed. Skip either and
158
175
  you ship corrupted corpus.
@@ -887,9 +887,9 @@ time. Before launching a full `spur-check-new`:
887
887
  `console.*`), spawn processes via `NodeProcessExecutor` (no `Bun.spawnSync`), get a
888
888
  `runtime-boundaries` fs-io exemption for sync reads (mirrors `task.ts`), and a non-command helper
889
889
  must not live in `apps/cli/src/commands/` (the noun scan treats every file there as a noun).
890
- 3. **Doc/TSDoc edits shift `file:line` anchors** cited by other tasks — `corpus-check` surfaces them
891
- as `L4.anchor-subject-mismatch`. Repoint the shifted citations (via `spur task update --section`)
892
- in the same commit, and reconcile new/stale baseline entries before the gate run.
890
+ 3. **Doc/TSDoc edits shift `file:line` anchors** cited by other tasks — the per-task gate surfaces
891
+ them as `L4.anchor-subject-mismatch` (0775: the corpus sweep retired; run `spur task check <wbs>`
892
+ on touched tasks). Repoint the shifted citations (via `spur task update --section`)
893
893
 
894
894
  ## AC traceability
895
895
 
@@ -170,7 +170,7 @@ transition to `done`.
170
170
  > The verify skill writes this shape automatically — operators only need it when hand-authoring
171
171
  > an answer file or debugging an UNKNOWN verdict.
172
172
 
173
- > **Corpus baseline discipline (`corpus-check`).** `corpus-check` sweeps every task/feature and fails on any structural finding outside `config/corpus-baseline.json`. The baseline is two-sided: an unlisted finding fails, **and** a listed entry that no longer reproduces fails. When introducing or tightening finding rules, reconcile the fallout in the same commit (constitution **T10**).
173
+ > **Finding-rule discipline (ADR-108).** When checker policy changes, run focused tests and one explicit unsuppressed `bun run corpus-check` audit; record/reconcile essential failures without waivers (T10). Ordinary corpus commit prep checks changed tasks/features and their required linked evidence, not the corpus (T11). The baseline and regenerators remain deleted; tests are not a substitute for the explicit policy-change audit.
174
174
  >
175
175
  > **Transition-shim discipline (`transition-shim-check`, task 0541 / ADR-058).** Wired as an early step of `spur-check` / `spur-check-new` (second, after `link-check` and before `lint`) so a shim violation fails in ~0.3 s rather than after the test run. Two-sided against `config/transition-shims.json`: an `@transition-shim(<id>)` marker with no manifest entry fails as a **new unregistered shim**, **and** a listed entry whose marker is gone from source fails as a **stale entry**. Emptying the manifest is the definition of the agent-role transition being complete. A removal condition must be objectively checkable against the repository. Shapes: `docs/04_DESIGN.md` §2.5.
176
176
 
@@ -188,4 +188,3 @@ transition to `done`.
188
188
  > failure is not yours. This costs seconds and mutates nothing — do **not** `git stash` to get a
189
189
  > clean baseline on a shared working tree. Treat any failure that does *not* name a port/listen/`ps`
190
190
  > cause as yours until proven otherwise; the count is a moving baseline, not a constant.
191
-