@gobing-ai/spur 0.3.73 → 0.3.75

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 (93) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +15 -4
  3. package/config/pipeline-budgets.json +4 -4
  4. package/config/rules/strict/runtime-boundaries.yaml +1 -1
  5. package/config/templates/AGENTS.md +5 -0
  6. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +2 -0
  7. package/config/workflows/basic.yaml +5 -2
  8. package/config/workflows/docs-pipeline.yaml +117 -55
  9. package/config/workflows/feature-dev.yaml +185 -121
  10. package/config/workflows/feature-lifecycle.yaml +1 -0
  11. package/config/workflows/history-anatomy.yaml +8 -4
  12. package/config/workflows/idea-pipeline.yaml +123 -43
  13. package/config/workflows/pr-review.yaml +31 -8
  14. package/config/workflows/task-lifecycle.yaml +1 -0
  15. package/config/workflows/task-pipeline.yaml +965 -905
  16. package/config/workflows/wayfinder-resolution.yaml +124 -90
  17. package/config/workflows/wrapup-pipeline.yaml +304 -107
  18. package/package.json +1 -1
  19. package/plugins/sp/agents/expert-spur.md +7 -4
  20. package/plugins/sp/commands/dev-idea.md +1 -1
  21. package/plugins/sp/plugin.json +1 -1
  22. package/plugins/sp/scripts/history-anatomy-cache.mjs +7 -1
  23. package/plugins/sp/scripts/history-anatomy-cache.ts +11 -2
  24. package/plugins/sp/scripts/pr-reviewing.mjs +5 -1
  25. package/plugins/sp/scripts/pr-reviewing.ts +7 -1
  26. package/plugins/sp/scripts/transition-shim-check.ts +6 -6
  27. package/plugins/sp/skills/pr-reviewing/SKILL.md +4 -1
  28. package/plugins/sp/skills/spur-cli/references/self.md +17 -3
  29. package/plugins/sp/skills/spur-cli/references/tasks.md +4 -2
  30. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +5 -5
  31. package/plugins/sp/skills/spur-cli/references/workflows.md +4 -4
  32. package/plugins/sp/skills/spur-dev/SKILL.md +17 -0
  33. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +23 -8
  34. package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
  35. package/plugins/sp/skills/spur-dev/references/execution-batch.md +3 -3
  36. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +5 -3
  37. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  38. package/spur.js +2999 -1010
  39. package/web/_astro/{BoardApp.BYCNkMOn.js → BoardApp.BOW1815F.js} +101 -100
  40. package/web/_astro/BoardApp.C4zimv1U.js +1 -0
  41. package/web/_astro/{TaskDetail.CgUreSP2.js → TaskDetail.CX6C7dSJ.js} +1 -1
  42. package/web/_astro/{arc.BySSh34M.js → arc.CwSvH1ji.js} +1 -1
  43. package/web/_astro/{architectureDiagram-3BPJPVTR.DM46TS_h.js → architectureDiagram-3BPJPVTR.Dxne1RvP.js} +1 -1
  44. package/web/_astro/{blockDiagram-GPEHLZMM.tZhvNUHA.js → blockDiagram-GPEHLZMM.D9Fb14rC.js} +1 -1
  45. package/web/_astro/{c4Diagram-AAUBKEIU.PT4Or4Nf.js → c4Diagram-AAUBKEIU.DmvWSFvv.js} +1 -1
  46. package/web/_astro/channel.CQHdDVp9.js +1 -0
  47. package/web/_astro/{chunk-2J33WTMH.J9r0_Bbe.js → chunk-2J33WTMH.BCCktIrE.js} +1 -1
  48. package/web/_astro/{chunk-4BX2VUAB.hzyeIvhR.js → chunk-4BX2VUAB.BzCk9q27.js} +1 -1
  49. package/web/_astro/{chunk-55IACEB6.B0rO7qVh.js → chunk-55IACEB6.CZXVgTk4.js} +1 -1
  50. package/web/_astro/{chunk-727SXJPM.wE_Uk5D4.js → chunk-727SXJPM.CJL8UIpX.js} +1 -1
  51. package/web/_astro/{chunk-AQP2D5EJ.DqEEjQw7.js → chunk-AQP2D5EJ.Dz8MDEdw.js} +1 -1
  52. package/web/_astro/{chunk-FMBD7UC4.CDoD9sBX.js → chunk-FMBD7UC4.CkQeYYUw.js} +1 -1
  53. package/web/_astro/{chunk-ND2GUHAM.CtX5nF9P.js → chunk-ND2GUHAM.KiC1QgzH.js} +1 -1
  54. package/web/_astro/{chunk-QZHKN3VN.CK_EwfaT.js → chunk-QZHKN3VN.AJ08mw2e.js} +1 -1
  55. package/web/_astro/{classDiagram-4FO5ZUOK.DLt5a8Lh.js → classDiagram-4FO5ZUOK.DRWRDKzK.js} +1 -1
  56. package/web/_astro/{classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js → classDiagram-v2-Q7XG4LA2.DRWRDKzK.js} +1 -1
  57. package/web/_astro/{cose-bilkent-S5V4N54A.CMCWP49h.js → cose-bilkent-S5V4N54A.CavydfLP.js} +1 -1
  58. package/web/_astro/{cynefin-OW5HDTMX.HyXw_vdS.js → cynefin-OW5HDTMX.D_7o_a0B.js} +1 -1
  59. package/web/_astro/{dagre-BM42HDAG.BTuAzh01.js → dagre-BM42HDAG.LWk2dKgg.js} +1 -1
  60. package/web/_astro/{diagram-2AECGRRQ.D9dr9wfT.js → diagram-2AECGRRQ.82lbq6aB.js} +1 -1
  61. package/web/_astro/{diagram-5GNKFQAL.C4Rot0hj.js → diagram-5GNKFQAL.hyNL1pwY.js} +1 -1
  62. package/web/_astro/{diagram-KO2AKTUF.B_TK5uWC.js → diagram-KO2AKTUF.BaLqVf-b.js} +1 -1
  63. package/web/_astro/{diagram-LMA3HP47.JkXKK7CO.js → diagram-LMA3HP47.DQJxj0oz.js} +1 -1
  64. package/web/_astro/{diagram-OG6HWLK6.BzMN8Bd6.js → diagram-OG6HWLK6.DhlL8cG2.js} +1 -1
  65. package/web/_astro/{erDiagram-TEJ5UH35.DVZaWGUd.js → erDiagram-TEJ5UH35.HoTnXwkF.js} +1 -1
  66. package/web/_astro/{flowDiagram-I6XJVG4X.rjEiWUfR.js → flowDiagram-I6XJVG4X.DkFKlZIZ.js} +1 -1
  67. package/web/_astro/{ganttDiagram-6RSMTGT7.C_EgAarK.js → ganttDiagram-6RSMTGT7.9qPfOTDb.js} +1 -1
  68. package/web/_astro/{gitGraphDiagram-PVQCEYII.B-QQSDsK.js → gitGraphDiagram-PVQCEYII.B4G18Dwc.js} +1 -1
  69. package/web/_astro/index.Dcr_8fiK.css +1 -0
  70. package/web/_astro/{infoDiagram-5YYISTIA.DlWesz7T.js → infoDiagram-5YYISTIA.e3KJkXAM.js} +1 -1
  71. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUMZOawi.js → ishikawaDiagram-YF4QCWOH.CY3yddhD.js} +1 -1
  72. package/web/_astro/{journeyDiagram-JHISSGLW.CWfkxfjY.js → journeyDiagram-JHISSGLW.DaAKIO1t.js} +1 -1
  73. package/web/_astro/{kanban-definition-UN3LZRKU.B-YpMwXf.js → kanban-definition-UN3LZRKU.DGkDddtc.js} +1 -1
  74. package/web/_astro/{linear.D7uqzENp.js → linear.DnDPcpd1.js} +1 -1
  75. package/web/_astro/{mermaid.core.CxrNppBD.js → mermaid.core.1uBmxa9t.js} +4 -4
  76. package/web/_astro/{mindmap-definition-RKZ34NQL.B4Qe7cM2.js → mindmap-definition-RKZ34NQL.XFJVayxw.js} +1 -1
  77. package/web/_astro/{pieDiagram-4H26LBE5.Ds-5j2ro.js → pieDiagram-4H26LBE5.BXm2OgHh.js} +1 -1
  78. package/web/_astro/{quadrantDiagram-W4KKPZXB.tBd38uNC.js → quadrantDiagram-W4KKPZXB.DgB2p5fc.js} +1 -1
  79. package/web/_astro/{requirementDiagram-4Y6WPE33.sFENkWl3.js → requirementDiagram-4Y6WPE33.jBR39bnS.js} +1 -1
  80. package/web/_astro/{sankeyDiagram-5OEKKPKP.BeB-Hk7C.js → sankeyDiagram-5OEKKPKP.N3u9OhE3.js} +1 -1
  81. package/web/_astro/{sequenceDiagram-3UESZ5HK.DnTeaSpx.js → sequenceDiagram-3UESZ5HK.CZhqZXNL.js} +1 -1
  82. package/web/_astro/{stateDiagram-AJRCARHV.B-8Jt5EJ.js → stateDiagram-AJRCARHV.DlNu1VEa.js} +1 -1
  83. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Br7xoqMW.js → stateDiagram-v2-BHNVJYJU.BEqi8NQV.js} +1 -1
  84. package/web/_astro/{timeline-definition-PNZ67QCA.C-3WdOyi.js → timeline-definition-PNZ67QCA.BPcexclc.js} +1 -1
  85. package/web/_astro/{vennDiagram-CIIHVFJN.DCIs7Lc6.js → vennDiagram-CIIHVFJN.BZalxKGQ.js} +1 -1
  86. package/web/_astro/{wardleyDiagram-YWT4CUSO.rGAL-bbz.js → wardleyDiagram-YWT4CUSO.BqOljod9.js} +1 -1
  87. package/web/_astro/{xychartDiagram-2RQKCTM6.hkfQKiRl.js → xychartDiagram-2RQKCTM6.tkO4ppN5.js} +1 -1
  88. package/web/index.html +2 -2
  89. package/config/corpus-baseline.json +0 -1805
  90. package/config/workflow-composition-baseline.json +0 -615
  91. package/web/_astro/BoardApp.E12MFjOS.js +0 -1
  92. package/web/_astro/channel.5cYKr5cs.js +0 -1
  93. package/web/_astro/index.B5MTfe7k.css +0 -1
@@ -46,8 +46,10 @@ CLI nouns for direct use, but they are not this corpus specialist's scope.
46
46
  3. Run the noun's read/check/validate path before mutation where available.
47
47
  4. Mutate only through `spur`; parse `--json` output when the verb advertises it.
48
48
  5. Inspect each result before the next dependent operation; stop on structural or validation failure.
49
- 6. Run the scoped check/validate/refresh path after mutation. After task/feature batch writes, run
50
- `spur task check --corpus --json` once.
49
+ 6. Run affected-input checks after mutation (constitution T11): after task/feature batch writes,
50
+ run `spur task check <wbs>` / `spur feature check <id>` for each changed document and its
51
+ required linked evidence — not a corpus sweep. The explicit unsuppressed audit
52
+ (`spur task check --corpus --json`) is reserved for checker-policy changes (T10).
51
53
 
52
54
  Workflow fit, mode selection, simplicity budgets, authoring, and tuning live in the workflow
53
55
  references under `plugins/sp/skills/spur-cli/references/workflows/`; load them rather than copying
@@ -59,7 +61,7 @@ their runbook here.
59
61
 
60
62
  - Use the source-local CLI when working in the Spur repository.
61
63
  - Use `spur task update --section --from-file` for task section writes.
62
- - Keep check-before/write/check-after evidence and the final scoped refresh result.
64
+ - Keep check-before/write/check-after evidence and the final scoped validation result.
63
65
  - Preserve declaration order and currently executing runs when changing workflows.
64
66
 
65
67
  ### Never
@@ -87,7 +89,8 @@ their runbook here.
87
89
  ### Gates
88
90
  - pre-check: <result>
89
91
  - post-check/validate: <result>
90
- - refresh/corpus sweep: <result or n/a>
92
+ - scoped validation: <affected task/feature checks + linked evidence, or n/a; explicit T10
93
+ corpus audit only when checker policy changed>
91
94
  ```
92
95
 
93
96
  ## Platform Notes
@@ -46,4 +46,4 @@ vars as subsets of `--approve-taste` (`idea_approved` / `design_approved`). Pref
46
46
  - `auto`/name: launch `spur workflow run idea-pipeline.yaml --async`, observe with one `workflow trace --follow`, and only report cancellation as stopped when `workflow cancel --json` returns `killed: true`.
47
47
  - `Skill(skill="sp:spur-dev", args="idea $ARGUMENTS")`
48
48
  - Stage contract (discovery → idea-eval → feature-create → AC → feature-check → system-design →
49
- decompose → batch-create → handoff): `plugins/sp/skills/spur-dev/references/dev-operations.md` § idea.
49
+ decompose → batch-create → ready-prepare → handoff): `plugins/sp/skills/spur-dev/references/dev-operations.md` § idea.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.73",
3
+ "version": "0.3.75",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -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'],
@@ -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,
@@ -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');
@@ -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` |
@@ -187,7 +187,9 @@ spur task record 0040 --verdict-file .spur/run/0040-verdict.json --solution-from
187
187
  - **`--solution-from-diff`** backfills `Solution` from `git diff -U0` hunk headers **only when the
188
188
  Solution section is still bare** — a safety net, not an overwrite.
189
189
  - **`--transition <status>`** optionally advances the lifecycle after writing (e.g. `testing`).
190
- `record` **never transitions to `done`** the `testing→done` gate stays in the workflow (0108).
190
+ `--transition done` is a **guarded** completion path: it requires a PASS verdict, auto-walks
191
+ `wip → testing → done`, and auto-creates the pipeline run-link (0436 R4); a non-PASS verdict to
192
+ `done` errors instead of transitioning, and the normal done gates still apply (0108).
191
193
 
192
194
  The verdict shape (`wbs`, `verdict`, `requirements[]`, `checks[]`) and the rendered tables are
193
195
  documented in [tasks/verbs.md](tasks/verbs.md).
@@ -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.
@@ -652,21 +652,35 @@ Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
652
652
  up once retention expires.
653
653
  - `source_commit` pins repository HEAD at write time; drift staleness is reported, not hidden.
654
654
  - `artifacts` lists referenced run files; a missing artifact makes the checkpoint stale.
655
- - Writers: write after every HITL gate decision, phase transition, and terminal state; overwrite
656
- the same file on resume (`session_id` = `<date>-<wbs-or-feature>`).
655
+
656
+ **Writer cadence (0784 R4).** There is exactly one canonical writer: the `task-pipeline` done
657
+ state's terminal checkpoint (`status: done`, real HEAD, run id from `$__runId`, and `$wbs`-expanded
658
+ artifact paths). It is a plain `shell` step — checkpoints are working memory, not CLI-gated corpus.
659
+ The `feature-dev`, `wrapup-pipeline`, and `idea-pipeline` pipelines used to echo pseudo-checkpoints
660
+ ("checkpoint: <workflow> done ...") that violated the canonical schema; those writers were removed
661
+ in 0784 — the persisted run row is the authoritative terminal record, and a non-canonical echo
662
+ cannot be resumed, routed, or reclaimed safely.
663
+
664
+ **Engine state vs checkpoint status (0784 R3).** The persisted run row's status is authoritative;
665
+ a checkpoint's `status` is an advisory projection written by consumers. On resume
666
+ (`workflow continue`), a checkpoint associated with the paused run must project a nonterminal
667
+ engine state — `pending`, `running`, or `approved` are accepted; `done`/`failed`/`cancelled`/
668
+ `skipped`, a missing status, or any unknown value refuse the resume with a named reason. The
669
+ engine owns no persisted `paused` checkpoint status.
657
670
 
658
671
  **Write checkpoints after:**
659
672
 
660
- - Every HITL gate decision (approved/rejected/deferred).
661
- - Every phase transition in `task-pipeline`, `feature-dev`, `idea-pipeline`, and
662
- `wrapup-pipeline`.
663
- - Every terminal state (`done`, `failed`, `cancelled`, `skipped`).
673
+ - Terminal task completion in `task-pipeline` (the canonical writer above).
674
+ - Any additional consumer-specific checkpoints you genuinely need for local resume; keep the
675
+ canonical schema — non-canonical files are ignored by routing and cleanup.
664
676
 
665
677
  **Read checkpoints when:**
666
678
 
667
679
  - `/sp:dev-run --continue` or `/sp:dev-runall --continue` is used.
668
680
  - The operator asks to resume a task or feature.
669
- - A workflow run is paused and later continued (`spur workflow continue <run-id>`).
681
+ - A workflow run is paused and later continued (`spur workflow continue <run-id>`). Resume
682
+ validates freshness in the run's recorded launch workdir (0784 R1/R3): artifacts resolve relative
683
+ to that workdir, and git HEAD is probed there — never in the ambient process cwd.
670
684
 
671
685
  **Rules:**
672
686
 
@@ -678,7 +692,8 @@ Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
678
692
  - **Retention.** Terminal checkpoints older than `workflowLogRetentionDays` with no active run are
679
693
  reclaimed by `spur workflow clean` (task 0711 R5–R8); non-terminal checkpoints are always kept.
680
694
  - **Not a validated corpus.** Checkpoints are working memory. They are overwritten when a session
681
- resumes and re-checkpoints. They are NOT authoritative task state — the task file is.
695
+ resumes and re-checkpoints. They are NOT authoritative task state — the task file and the
696
+ persisted run row are.
682
697
  - **One file per session.** The `session_id` is `<date>-<wbs-or-feature>`. A resumed session
683
698
  overwrites the same file.
684
699
  - **Operator-readable.** The YAML frontmatter is machine-parseable; the body is free-form markdown
@@ -341,7 +341,7 @@ must not be changed without updating the backing skill.
341
341
  - `--approve-taste` — with `--auto`, skip **all** remaining taste pauses this run (idea-eval + design-approval). Sets `idea_approved=true` and `design_approved=true`.
342
342
  Aliases (prefer `--approve-taste`): `--idea-approved` → `idea_approved`; `--design-approved` → `design_approved`. There is **no** `--design` force flag.
343
343
  - **Backing:** `idea-pipeline.yaml` through the inline driver for omitted/`inline`, or `spur workflow run idea-pipeline.yaml --async` for `auto`/name.
344
- - **Behavior:** Builds vars from the table above and drives the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create → handoff. STOPS at handoff — no task execution, no pipeline nesting. Headless runs use one `trace --follow`; cancellation is reported stopped only when `workflow cancel --json` returns `killed: true`.
344
+ - **Behavior:** Builds vars from the table above and drives the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create (`--skip-ready`) ready-prepare (ready checklist per created task + ready-evidence sidecar, 0788) → handoff. STOPS at handoff — no task execution, no pipeline nesting. Headless runs use one `trace --follow`; cancellation is reported stopped only when `workflow cancel --json` returns `killed: true`.
345
345
  - **Delegation:** Host-session inline driver by default; explicit executor selection uses the async workflow worker.
346
346
  - **Idea-evaluation gate:** After discovery, operator reviews `.spur/run/idea-eval-report.md` ([idea-evaluation.md](idea-evaluation.md)). Approve continues; reject/cancel → no feature. Under `--auto`, still pauses unless taste pre-cleared (`--approve-taste` / alias). Enhanced idea is a sidecar — `vars.idea` is not overwritten.
347
347
  - **Design package (`--skip-design` only):**
@@ -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
 
@@ -149,7 +149,10 @@ passed, so provenance denied first and Review L3 denied on the retry.
149
149
 
150
150
  | # | Gate layer | Triggers denial when | Remediation |
151
151
  |---|------------|----------------------|-------------|
152
- | 1 | **Strict-core + verdict artifact** (`spur task check <wbs> --strict-core` + `done-transition-guard.ts`) | The strict-core check fails, or `.spur/run/<wbs>-verdict.json` is **missing** or has a non-PASS aggregate. **Missing artifact is a deny** (not a silent allow — closes the 0349 "done without verdict" class). The aggregate is recomputed from requirement/AC rows; the harsher of stored and computed wins. | Re-run `/sp:dev-verify <wbs>` until PASS (writes the artifact), or explicitly override with `spur task update <wbs> done --force-done --reason "<why>"`. Docs-only pipelines write a docs PASS stub under `.spur/run/` before `done` (see `docs-pipeline.yaml`). |
152
+ | 1 | **Strict-core + verdict artifact** (`spur task check <wbs> --strict-core` + `done-transition-guard.ts`) | The strict-core check fails, or `.spur/run/<wbs>-verdict.json` is **missing** or has a non-PASS aggregate. **Missing artifact is a deny** (not a silent allow — closes the 0349 "done without verdict" class). The aggregate is recomputed from requirement/AC rows; the harsher of stored and computed wins. | Re-run `/sp:dev-verify <wbs>` until PASS (writes the artifact), or explicitly override with `spur task update <wbs> done --force-done --reason "<why>"`. Docs-only pipelines (`docs-pipeline.yaml`) meet the same layer: read-only measured verification
153
+ (answer file + `spur task verdict`) writes the standard `.spur/run/<wbs>-verdict.json` artifact
154
+ under proof-input digest bracketing; missing or non-PASS evidence is a refusal, never a synthetic
155
+ PASS stub. |
153
156
  | 2 | **Provenance guard** (`lifecycle-adapter.ts`) | No pipeline-kind run link exists for `<wbs>`. | Run `/sp:dev-run <wbs>` through the full pipeline, use `/sp:dev-run <wbs> --mode implement --auto --next` for the explicit step chain, or record the audited bypass with `SPUR_PROVENANCE_OVERRIDE=1`. |
154
157
  | 3 | **Review L3** (`task-check.ts`) | `### Review` is empty, placeholder-only, or lacks a populated P1–P4 findings table. | Run `/sp:dev-review <wbs>`; verify cannot write Review because of the Step 10 prohibition above. |
155
158
 
@@ -170,7 +173,7 @@ transition to `done`.
170
173
  > The verify skill writes this shape automatically — operators only need it when hand-authoring
171
174
  > an answer file or debugging an UNKNOWN verdict.
172
175
 
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**).
176
+ > **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
177
  >
175
178
  > **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
179
 
@@ -188,4 +191,3 @@ transition to `done`.
188
191
  > failure is not yours. This costs seconds and mutates nothing — do **not** `git stash` to get a
189
192
  > clean baseline on a shared working tree. Treat any failure that does *not* name a port/listen/`ps`
190
193
  > cause as yours until proven otherwise; the count is a moving baseline, not a constant.
191
-
@@ -147,7 +147,7 @@ Invoked when a map already exists (operator provides the feature ID). A ticket i
147
147
  — no task is claimed, and it does not consume the session's one ticket.
148
148
 
149
149
  5. **Record the resolution.** Post the answer in the task body, then `spur task update <wbs> done`. Append one line to the map's **## Decisions so far**: `- [<WBS> <title>](path) — <one-line gist of the answer>`.
150
- 6. **Graduate fog into new tickets.** Any fog the answer has made specifiable becomes fresh child tasks (create-then-wire). Clear each graduated patch from **### Not yet specified** so it lives only as its new ticket. Doing half of this — deleting the fog without creating the ticket — destroys the only record the work was ever identified, so `corpus-check` fails the branch (`corpus.ungraduated-fog`) unless the removal is matched by a new/re-parented ticket or a line in **### Out of scope**.
150
+ 6. **Graduate fog into new tickets.** Any fog the answer has made specifiable becomes fresh child tasks (create-then-wire). Clear each graduated patch from **### Not yet specified** so it lives only as its new ticket. Doing half of this — deleting the fog without creating the ticket — destroys the only record the work was ever identified; the `corpus.ungraduated-fog` sweep retired in 0775 enforcement is review-time discipline.
151
151
  7. **Rule out mis-scoped tickets.** If the answer reveals a ticket sits beyond the destination, close it and add one line to **### Out of scope** (the gist + why it's out of scope, linking the closed ticket). A scope boundary is not a step on the route — it stays out of **## Decisions so far**.
152
152
  8. **Stop after ONE ticket.** Never resolve more than one per session.
153
153