@gobing-ai/spur 0.3.99 → 0.4.1

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 (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/gates.yaml +36 -0
  4. package/config/decisions/history-anatomy.yaml +35 -0
  5. package/config/decisions/idea-pipeline.yaml +38 -0
  6. package/config/decisions/task-pipeline.yaml +55 -0
  7. package/config/plugin-scripts.json +25 -0
  8. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  9. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  10. package/config/rules/quality/coverage-gate.yaml +5 -3
  11. package/config/rules/recommended-pre-check.yaml +8 -3
  12. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  13. package/config/rules/structure/protected-files.yaml +3 -0
  14. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  15. package/config/script-placement-baseline.json +2 -2
  16. package/config/workflows/feature-lifecycle.yaml +9 -2
  17. package/config/workflows/history-anatomy.yaml +22 -0
  18. package/config/workflows/idea-pipeline.yaml +75 -0
  19. package/config/workflows/task-pipeline.yaml +368 -49
  20. package/config/workflows/wrapup-pipeline.yaml +106 -13
  21. package/package.json +9 -9
  22. package/plugins/sp/README.md +25 -2
  23. package/plugins/sp/agents/super-planner.md +11 -0
  24. package/plugins/sp/agents/super-reviewer.md +39 -11
  25. package/plugins/sp/bunfig.toml +8 -0
  26. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  27. package/plugins/sp/commands/dev-changelog.md +5 -4
  28. package/plugins/sp/commands/dev-find-way.md +80 -0
  29. package/plugins/sp/commands/dev-idea.md +6 -0
  30. package/plugins/sp/commands/dev-parallel.md +8 -2
  31. package/plugins/sp/commands/dev-plan.md +6 -0
  32. package/plugins/sp/commands/dev-review-session.md +14 -7
  33. package/plugins/sp/commands/dev-run.md +17 -4
  34. package/plugins/sp/commands/dev-runall.md +21 -10
  35. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  36. package/plugins/sp/hooks/context-session-start.ts +1 -1
  37. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  38. package/plugins/sp/hooks/hooks.json +45 -1
  39. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +72 -11
  42. package/plugins/sp/lib/inline-run-app.ts +28 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +6 -0
  44. package/plugins/sp/lib/inline-run.generated.mjs +364 -46
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +28 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +255 -0
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +47 -1
  48. package/plugins/sp/lib/quality-gate.generated.mjs +30489 -82
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  50. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  51. package/plugins/sp/lib/run-summary-core.ts +561 -0
  52. package/plugins/sp/lib/transcript.ts +275 -0
  53. package/plugins/sp/plugin.json +1 -1
  54. package/plugins/sp/references/roles.md +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +335 -0
  56. package/plugins/sp/scripts/batch-plan.ts +100 -0
  57. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  58. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  59. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  60. package/plugins/sp/scripts/commit-guard.ts +249 -0
  61. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  62. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  63. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  64. package/plugins/sp/scripts/inline-run-setup.mjs +563 -91
  65. package/plugins/sp/scripts/inline-run-setup.ts +65 -97
  66. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  67. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  68. package/plugins/sp/scripts/quality-gate.mjs +30407 -73
  69. package/plugins/sp/scripts/quality-gate.ts +21 -5
  70. package/plugins/sp/scripts/residual-scan.mjs +88 -38
  71. package/plugins/sp/scripts/residual-scan.ts +35 -38
  72. package/plugins/sp/scripts/run-summary.mjs +693 -0
  73. package/plugins/sp/scripts/run-summary.ts +105 -0
  74. package/plugins/sp/scripts/session-timeline.mjs +331 -0
  75. package/plugins/sp/scripts/session-timeline.ts +204 -0
  76. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  77. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  78. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  79. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  80. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  81. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  82. package/plugins/sp/skills/code-verification/SKILL.md +8 -4
  83. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  84. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  85. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  86. package/plugins/sp/skills/session-review/SKILL.md +77 -24
  87. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  88. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  89. package/plugins/sp/skills/spur-cli/SKILL.md +6 -2
  90. package/plugins/sp/skills/spur-cli/references/agent.md +124 -17
  91. package/plugins/sp/skills/spur-cli/references/decision.md +98 -0
  92. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  93. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +41 -3
  94. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  95. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  96. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  97. package/plugins/sp/skills/spur-composer/SKILL.md +28 -2
  98. package/plugins/sp/skills/spur-dev/SKILL.md +18 -14
  99. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +6 -3
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +51 -18
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  103. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +194 -0
  104. package/plugins/sp/skills/spur-dev/references/execution-batch.md +113 -963
  105. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +130 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +20 -0
  107. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  109. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +51 -9
  110. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  111. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +361 -156
  112. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  113. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  114. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  115. package/schemas/decision-catalog.schema.json +226 -0
  116. package/schemas/spur-config.schema.json +25 -0
  117. package/schemas/state-machine-workflow.schema.json +23 -0
  118. package/spur.js +28165 -19051
  119. package/web/_astro/{BoardApp.i4eh_fHA.js → BoardApp.D66k_s_T.js} +1 -1
  120. package/web/_astro/BoardApp.D6sjAJr_.js +324 -0
  121. package/web/_astro/{TaskDetail.C5Cu8LdB.js → TaskDetail.BynLnMNc.js} +1 -1
  122. package/web/_astro/{arc.Bu5XpBEz.js → arc._BUadrdH.js} +1 -1
  123. package/web/_astro/{architectureDiagram-3BPJPVTR.9vHQTvhF.js → architectureDiagram-3BPJPVTR.Dh7xS1Pp.js} +1 -1
  124. package/web/_astro/{blockDiagram-GPEHLZMM.Cn2Xl7D6.js → blockDiagram-GPEHLZMM.DFDVpfXm.js} +1 -1
  125. package/web/_astro/{c4Diagram-AAUBKEIU.7o0P_QDo.js → c4Diagram-AAUBKEIU.V7_3_glh.js} +1 -1
  126. package/web/_astro/channel.DZVfto-r.js +1 -0
  127. package/web/_astro/{chunk-2J33WTMH.C3zZFsNt.js → chunk-2J33WTMH.De1yx7Jz.js} +1 -1
  128. package/web/_astro/{chunk-4BX2VUAB.CNy_M7fO.js → chunk-4BX2VUAB.BDhHFuxx.js} +1 -1
  129. package/web/_astro/{chunk-55IACEB6.2TiPnLBL.js → chunk-55IACEB6.DDLkWnmC.js} +1 -1
  130. package/web/_astro/{chunk-727SXJPM.5CGSyKAp.js → chunk-727SXJPM.CwfiLnA7.js} +1 -1
  131. package/web/_astro/{chunk-AQP2D5EJ.w-ZIgfsK.js → chunk-AQP2D5EJ.CtGDmUnM.js} +1 -1
  132. package/web/_astro/{chunk-FMBD7UC4.BkcEyhKa.js → chunk-FMBD7UC4.BOLjpw73.js} +1 -1
  133. package/web/_astro/{chunk-ND2GUHAM.mxKKbBG4.js → chunk-ND2GUHAM.BDhQls6O.js} +1 -1
  134. package/web/_astro/{chunk-QZHKN3VN.cyA0z9aT.js → chunk-QZHKN3VN.76qBynDF.js} +1 -1
  135. package/web/_astro/{classDiagram-4FO5ZUOK.CqtUzwhX.js → classDiagram-4FO5ZUOK.N0mpPHT0.js} +1 -1
  136. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CqtUzwhX.js → classDiagram-v2-Q7XG4LA2.N0mpPHT0.js} +1 -1
  137. package/web/_astro/{cose-bilkent-S5V4N54A.BExPTopI.js → cose-bilkent-S5V4N54A.CKknFfjB.js} +1 -1
  138. package/web/_astro/{cynefin-OW5HDTMX.Bq2i10Cn.js → cynefin-OW5HDTMX.Cw3ZWBAh.js} +1 -1
  139. package/web/_astro/{dagre-BM42HDAG.By_HsOuv.js → dagre-BM42HDAG.D3yIKCRD.js} +1 -1
  140. package/web/_astro/{diagram-2AECGRRQ.BtU0b7hz.js → diagram-2AECGRRQ.D51dO83Z.js} +1 -1
  141. package/web/_astro/{diagram-5GNKFQAL.D00ztMdT.js → diagram-5GNKFQAL.Bb-FViDj.js} +1 -1
  142. package/web/_astro/{diagram-KO2AKTUF.DMVJWaUN.js → diagram-KO2AKTUF.D3mN8S6x.js} +1 -1
  143. package/web/_astro/{diagram-LMA3HP47.Cxqrjg5-.js → diagram-LMA3HP47.dMMyPcbJ.js} +1 -1
  144. package/web/_astro/{diagram-OG6HWLK6.2lz_1B5h.js → diagram-OG6HWLK6.AzbvzY4C.js} +1 -1
  145. package/web/_astro/{erDiagram-TEJ5UH35.BVQEuJ3T.js → erDiagram-TEJ5UH35.BR5I4OZb.js} +1 -1
  146. package/web/_astro/{flowDiagram-I6XJVG4X.Qlyo90ky.js → flowDiagram-I6XJVG4X.DFqvhGkn.js} +1 -1
  147. package/web/_astro/{ganttDiagram-6RSMTGT7.DRwb8HHZ.js → ganttDiagram-6RSMTGT7.ByJbhH6J.js} +1 -1
  148. package/web/_astro/{gitGraphDiagram-PVQCEYII.YfoPthfS.js → gitGraphDiagram-PVQCEYII.CInHjQR3.js} +1 -1
  149. package/web/_astro/index.COh_kBxN.css +1 -0
  150. package/web/_astro/{infoDiagram-5YYISTIA.DvFREck1.js → infoDiagram-5YYISTIA.Cm0dGVjJ.js} +1 -1
  151. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js → ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js} +1 -1
  152. package/web/_astro/{journeyDiagram-JHISSGLW.BDMT_AFb.js → journeyDiagram-JHISSGLW.4IvLa2MW.js} +1 -1
  153. package/web/_astro/{kanban-definition-UN3LZRKU.D6Fh3gj9.js → kanban-definition-UN3LZRKU.DS87Cl-k.js} +1 -1
  154. package/web/_astro/{linear.Dxw00fGQ.js → linear.MzmRrNER.js} +1 -1
  155. package/web/_astro/{mermaid.core.CsoxUWa0.js → mermaid.core.fK-WNswS.js} +4 -4
  156. package/web/_astro/{mindmap-definition-RKZ34NQL.CN8oeHLu.js → mindmap-definition-RKZ34NQL.BqipOHhY.js} +1 -1
  157. package/web/_astro/{pieDiagram-4H26LBE5.C7ADqQ_7.js → pieDiagram-4H26LBE5.0nBiPqFG.js} +1 -1
  158. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cw9lKyHP.js → quadrantDiagram-W4KKPZXB.Szj7Zpx5.js} +1 -1
  159. package/web/_astro/{requirementDiagram-4Y6WPE33.B1ni-z4q.js → requirementDiagram-4Y6WPE33.D4UR6TWo.js} +1 -1
  160. package/web/_astro/{sankeyDiagram-5OEKKPKP.BhexdjVr.js → sankeyDiagram-5OEKKPKP.DAE7BdOs.js} +1 -1
  161. package/web/_astro/{sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js → sequenceDiagram-3UESZ5HK.BP9ZVq25.js} +1 -1
  162. package/web/_astro/{stateDiagram-AJRCARHV.CkuMt27u.js → stateDiagram-AJRCARHV.BQW7mIxk.js} +1 -1
  163. package/web/_astro/{stateDiagram-v2-BHNVJYJU.Dc_aazUA.js → stateDiagram-v2-BHNVJYJU.PbK4nDih.js} +1 -1
  164. package/web/_astro/{timeline-definition-PNZ67QCA.ja7sUFhH.js → timeline-definition-PNZ67QCA.Ci_GvUpo.js} +1 -1
  165. package/web/_astro/{vennDiagram-CIIHVFJN.BxIshX_K.js → vennDiagram-CIIHVFJN.DNw1Y5qx.js} +1 -1
  166. package/web/_astro/{wardleyDiagram-YWT4CUSO.DfHlC0sh.js → wardleyDiagram-YWT4CUSO.CwlkaaNe.js} +1 -1
  167. package/web/_astro/{xychartDiagram-2RQKCTM6.DSnhHwCh.js → xychartDiagram-2RQKCTM6.MI6ZuiH_.js} +1 -1
  168. package/web/board-runtime.json +1 -1
  169. package/web/index.html +2 -2
  170. package/web/_astro/BoardApp.BVp-LYi6.js +0 -322
  171. package/web/_astro/channel.6IFIKBhu.js +0 -1
  172. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -1,21 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1).
3
+ * persist-out-check — WT-4 pre-removal assertion for E71 persist-out (task 1067 R1, 1148 R1).
4
4
  *
5
- * Before `git worktree remove`, verify the worktree's durable evidence would not be
6
- * abandoned: every file the worktree owns under `.spur/run/` (`<wbs>-*` per forwarded
7
- * task file, `<runId>-*` per forwarded run id) and the whole `.spur/memory/evidence/`
8
- * tree must exist in the invoking tree with identical bytes. Missing files BLOCK the
9
- * removal (persist-out was skipped or stale); divergent files block too (persist-out
10
- * never overwrites — reconcile by hand per execution-batch.md). No new public CLI verb:
11
- * this is a driver-side assertion step, not a corpus tool.
12
- *
13
- * ponytail: file-level comparison only — DB run rows and nested-run deep walks stay
14
- * persist-out's job (`inline-run-setup --persist-out`); this checks the planes E71
15
- * names as files. Listing caps (64/prefix, 256 evidence) refuse runaways by name.
16
- *
17
- * Node-builtin imports only; pure helpers are exported for tests. Exit 0 ok, 1
18
- * blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
5
+ * Before `git worktree remove`, verify the worktree's OWNED durable evidence would not be abandoned:
6
+ * every `.spur/run/` and `.spur/memory/evidence/` file starting with a forwarded `<wbs>-`/`<runId>-`
7
+ * prefix must exist in the invoking tree with identical bytes. Unowned evidence is counted
8
+ * (`unowned: N`) and does not block (1148 R1). Node builtins only; helpers are exported for tests.
9
+ * Exit 0 ok, 1 blocked/divergent, 2 usage error. Normal output is the finding lines themselves.
19
10
  */
20
11
 
21
12
  import { spawnSync } from 'node:child_process';
@@ -26,7 +17,6 @@ const EVIDENCE_DIR = join('.spur', 'memory', 'evidence');
26
17
  const RUN_DIR = join('.spur', 'run');
27
18
  const NAMED_CAP = 32;
28
19
  const PREFIX_CAP = 64;
29
- const EVIDENCE_CAP = 256;
30
20
 
31
21
  /** Task files own their `<wbs>-*` evidence: leading digits before `_` in the basename. */
32
22
  export function wbsOfTaskFile(path: string): string | null {
@@ -34,49 +24,64 @@ export function wbsOfTaskFile(path: string): string | null {
34
24
  return m?.[1] ?? null;
35
25
  }
36
26
 
37
- function listFilesUnder(root: string, rel: string, cap: number): string[] | null {
38
- const dir = join(root, rel);
27
+ export interface ListObligationsResult {
28
+ files: string[];
29
+ unowned: number;
30
+ }
31
+
32
+ /** Recursively list files under `dir`, relative to `root`. */
33
+ function walkFiles(dir: string, root: string): string[] {
39
34
  if (!existsSync(dir)) return [];
40
35
  const out: string[] = [];
41
- const walk = (cur: string): boolean => {
42
- for (const entry of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
43
- const p = join(cur, entry.name);
44
- if (entry.isDirectory()) {
45
- if (!walk(p)) return false;
46
- } else {
47
- out.push(relative(root, p));
48
- if (out.length > cap) return false;
49
- }
36
+ const walk = (cur: string): void => {
37
+ for (const e of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
38
+ const p = join(cur, e.name);
39
+ if (e.isDirectory()) walk(p);
40
+ else if (e.isFile()) out.push(relative(root, p));
50
41
  }
51
- return true;
52
42
  };
53
- return walk(dir) ? out : null;
43
+ walk(dir);
44
+ return out;
45
+ }
46
+
47
+ /** Owned evidence (prefix-matched) under one plane; null once a prefix exceeds PREFIX_CAP. */
48
+ function collectOwned(rels: string[], prefixes: string[], counts: Map<string, number>, out: string[]): boolean {
49
+ for (const rel of rels) {
50
+ const prefix = prefixes.find((p) => basename(rel).startsWith(`${p}-`));
51
+ if (prefix === undefined) continue;
52
+ const count = (counts.get(prefix) ?? 0) + 1;
53
+ if (count > PREFIX_CAP) return false;
54
+ counts.set(prefix, count);
55
+ out.push(rel);
56
+ }
57
+ return true;
54
58
  }
55
59
 
56
60
  /**
57
- * Evidence files the worktree owns: `.spur/memory/evidence/**` plus `.spur/run/<prefix>-*`
58
- * direct children per task-file WBS / run id. Returns null on a cap overrun or a listing
59
- * failure (not a directory, permission denied) — the caller refuses either way.
61
+ * Evidence the worktree owns: `.spur/memory/evidence/` (recursive) and `.spur/run/` (flat) files
62
+ * whose basename starts with a forwarded `<wbs>-`/`<runId>-` prefix. Evidence matching no prefix is
63
+ * counted as `unowned` (1148 R1). Null when no prefix is forwarded or a prefix cap is exceeded.
60
64
  */
61
- export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): string[] | null {
62
- const evidence = listFilesUnder(wtRoot, EVIDENCE_DIR, EVIDENCE_CAP);
63
- if (evidence === null) return null;
64
- const out = [...evidence];
65
- const prefixes = [...wbsList, ...runIds];
66
- for (const prefix of prefixes) {
67
- const dir = join(wtRoot, RUN_DIR);
68
- if (!existsSync(dir)) continue;
69
- let entries: string[];
70
- try {
71
- entries = readdirSync(dir).sort();
72
- } catch {
73
- return null;
74
- }
75
- const owned = entries.filter((n) => n.startsWith(`${prefix}-`));
76
- if (owned.length > PREFIX_CAP) return null;
77
- out.push(...owned.map((n) => join(RUN_DIR, n)));
78
- }
79
- return [...new Set(out)].sort();
65
+ export function listObligations(wtRoot: string, wbsList: string[], runIds: string[]): ListObligationsResult | null {
66
+ const prefixes = [...new Set([...wbsList, ...runIds].filter(Boolean))];
67
+ if (prefixes.length === 0) return null;
68
+
69
+ const evDir = join(wtRoot, EVIDENCE_DIR);
70
+ const runDir = join(wtRoot, RUN_DIR);
71
+ const evFiles = walkFiles(evDir, wtRoot);
72
+ const runFiles = existsSync(runDir)
73
+ ? readdirSync(runDir, { withFileTypes: true })
74
+ .filter((e) => e.isFile())
75
+ .map((e) => join(RUN_DIR, e.name))
76
+ .sort()
77
+ : [];
78
+
79
+ const files: string[] = [];
80
+ const counts = new Map<string, number>();
81
+ if (!collectOwned(evFiles, prefixes, counts, files)) return null;
82
+ if (!collectOwned(runFiles, prefixes, counts, files)) return null;
83
+ const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
84
+ return { files: [...new Set(files)].sort(), unowned };
80
85
  }
81
86
 
82
87
  /** Classify obligations against the invoking tree: missing, divergent (byte-differs), or ok. */
@@ -84,11 +89,15 @@ export function compareTrees(
84
89
  wtRoot: string,
85
90
  invokeRoot: string,
86
91
  files: string[],
92
+ foreignDivergent = new Set<string>(),
87
93
  ): { missing: string[]; divergent: string[]; ok: number } {
88
94
  const missing: string[] = [];
89
95
  const divergent: string[] = [];
90
96
  let ok = 0;
91
97
  for (const rel of files) {
98
+ if (foreignDivergent.has(rel) || foreignDivergent.has(basename(rel))) {
99
+ continue;
100
+ }
92
101
  const wtPath = join(wtRoot, rel);
93
102
  const invPath = join(invokeRoot, rel);
94
103
  if (!existsSync(invPath)) {
@@ -107,7 +116,7 @@ export function compareTrees(
107
116
  }
108
117
 
109
118
  export function persistOutCheckUsage(): string {
110
- return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>]';
119
+ return 'usage: persist-out-check --from <worktree> [--task-file <path>]… [--run-id <id>]… [--root <invoke-tree>] [--success-json <path>]';
111
120
  }
112
121
 
113
122
  /**
@@ -129,6 +138,7 @@ export function defaultInvokeRoot(cwd: string): string {
129
138
 
130
139
  export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd())): number {
131
140
  let wtRoot = '';
141
+ let successJsonPath = '';
132
142
  const wbsList: string[] = [];
133
143
  const runIds: string[] = [];
134
144
  for (let i = 0; i < argv.length; i++) {
@@ -145,25 +155,64 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
145
155
  } else if (arg === '--run-id') {
146
156
  const id = argv[++i] ?? '';
147
157
  if (id) runIds.push(id);
158
+ } else if (arg === '--success-json') {
159
+ successJsonPath = argv[++i] ?? '';
148
160
  } else {
149
161
  process.stderr.write(`${persistOutCheckUsage()}\n`);
150
162
  return 2;
151
163
  }
152
164
  }
165
+ if (wbsList.length === 0 && runIds.length === 0) {
166
+ process.stderr.write(
167
+ `${persistOutCheckUsage()}\npersist-out-check: at least one --task-file or --run-id prefix required\n`,
168
+ );
169
+ return 2;
170
+ }
153
171
  if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
154
172
  process.stderr.write(
155
173
  `${persistOutCheckUsage()}\npersist-out-check: --from must be an existing worktree directory\n`,
156
174
  );
157
175
  return 2;
158
176
  }
159
- const files = listObligations(wtRoot, wbsList, runIds);
160
- if (files === null) {
177
+ const foreignDivergent = new Set<string>();
178
+ const skips: Array<{ id: string; reason: string }> = [];
179
+ const candidates = [
180
+ successJsonPath,
181
+ join(invokeRoot, '.spur', 'run', 'persist-out.json'),
182
+ join(wtRoot, '.spur', 'run', 'persist-out.json'),
183
+ ].filter(Boolean);
184
+ for (const cand of candidates) {
185
+ if (existsSync(cand)) {
186
+ try {
187
+ const parsed = JSON.parse(readFileSync(cand, 'utf8'));
188
+ if (Array.isArray(parsed.evidenceSkipped)) {
189
+ for (const item of parsed.evidenceSkipped) {
190
+ if (item.reason === 'foreign-divergent' && typeof item.name === 'string') {
191
+ foreignDivergent.add(item.name);
192
+ foreignDivergent.add(join(EVIDENCE_DIR, item.name));
193
+ }
194
+ }
195
+ }
196
+ if (Array.isArray(parsed.skipped)) {
197
+ for (const item of parsed.skipped) {
198
+ if (typeof item.id === 'string' && typeof item.reason === 'string') {
199
+ skips.push({ id: item.id, reason: item.reason });
200
+ }
201
+ }
202
+ }
203
+ break;
204
+ } catch {}
205
+ }
206
+ }
207
+ const obligations = listObligations(wtRoot, wbsList, runIds);
208
+ if (obligations === null) {
161
209
  process.stderr.write(
162
- `persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (64/prefix, ${EVIDENCE_CAP} evidence files) — inspect by hand\n`,
210
+ `persist-out-check: BLOCKED — worktree evidence listing failed or exceeded caps (${PREFIX_CAP}/prefix) — inspect by hand\n`,
163
211
  );
164
212
  return 1;
165
213
  }
166
- const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files);
214
+ const { files, unowned } = obligations;
215
+ const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
167
216
  const findings = [
168
217
  ...missing.map((f) => `MISSING ${f}`),
169
218
  ...divergent.map((f) => `DIVERGENT ${f} — reconcile by hand (persist-out never overwrites)`),
@@ -171,7 +220,13 @@ export function main(argv: string[], invokeRoot = defaultInvokeRoot(process.cwd(
171
220
  for (const line of findings.slice(0, NAMED_CAP)) process.stdout.write(`${line}\n`);
172
221
  if (findings.length > NAMED_CAP) process.stdout.write(`… +${findings.length - NAMED_CAP} more\n`);
173
222
  if (findings.length === 0) {
174
- process.stdout.write(`persist-out-check: ok — ${ok} evidence file(s) persisted, nothing abandoned\n`);
223
+ for (const skip of skips) {
224
+ process.stdout.write(`SKIP ${skip.id}: ${skip.reason}\n`);
225
+ }
226
+ const abandonedMsg = skips.length > 0 ? `${skips.length} skipped` : 'nothing abandoned';
227
+ process.stdout.write(
228
+ `persist-out-check: ok — ${ok} evidence file(s) persisted, unowned: ${unowned}, ${abandonedMsg}\n`,
229
+ );
175
230
  return 0;
176
231
  }
177
232
  process.stderr.write(