@gobing-ai/spur 0.4.0 → 0.4.2

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 +4 -2
  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/plugin-scripts.json +15 -0
  7. package/config/rules/docs/sourced-api-claims.yaml +60 -0
  8. package/config/rules/recommended-pre-check.yaml +8 -3
  9. package/config/rules/structure/scratch-evidence-pointer.yaml +35 -0
  10. package/config/script-placement-baseline.json +2 -2
  11. package/config/templates/AGENTS.md +1 -1
  12. package/config/workflows/feature-lifecycle.yaml +9 -2
  13. package/config/workflows/history-anatomy.yaml +22 -0
  14. package/config/workflows/idea-pipeline.yaml +75 -0
  15. package/config/workflows/task-pipeline.yaml +216 -57
  16. package/config/workflows/wrapup-pipeline.yaml +107 -14
  17. package/package.json +10 -10
  18. package/plugins/sp/README.md +12 -3
  19. package/plugins/sp/agents/super-planner.md +16 -6
  20. package/plugins/sp/agents/super-reviewer.md +14 -0
  21. package/plugins/sp/bunfig.toml +8 -0
  22. package/plugins/sp/commands/dev-brainstorm.md +29 -16
  23. package/plugins/sp/commands/dev-find-way.md +80 -0
  24. package/plugins/sp/commands/dev-idea.md +6 -0
  25. package/plugins/sp/commands/dev-plan.md +6 -0
  26. package/plugins/sp/commands/dev-review-session.md +4 -1
  27. package/plugins/sp/commands/dev-run.md +12 -2
  28. package/plugins/sp/commands/dev-runall.md +32 -15
  29. package/plugins/sp/hooks/context-owner.ts +56 -0
  30. package/plugins/sp/hooks/context-post-tool.ts +23 -0
  31. package/plugins/sp/hooks/context-session-start.ts +49 -27
  32. package/plugins/sp/hooks/context-session-stop.ts +131 -30
  33. package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
  34. package/plugins/sp/lib/idea-handoff.generated.mjs +43 -5
  35. package/plugins/sp/lib/inline-run-app.ts +28 -0
  36. package/plugins/sp/lib/inline-run.generated.d.mts +5 -0
  37. package/plugins/sp/lib/inline-run.generated.mjs +351 -46
  38. package/plugins/sp/lib/plan-projection.generated.d.mts +47 -0
  39. package/plugins/sp/lib/plan-projection.generated.mjs +368 -0
  40. package/plugins/sp/lib/quality-gate.generated.d.mts +35 -1
  41. package/plugins/sp/lib/quality-gate.generated.mjs +30296 -71
  42. package/plugins/sp/lib/residual-scan.generated.d.mts +19 -0
  43. package/plugins/sp/lib/residual-scan.generated.mjs +66 -0
  44. package/plugins/sp/lib/run-summary-core.ts +561 -0
  45. package/plugins/sp/lib/transcript.ts +150 -7
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/batch-plan.mjs +488 -0
  49. package/plugins/sp/scripts/batch-plan.ts +145 -0
  50. package/plugins/sp/scripts/batch-preflight.mjs +51 -3
  51. package/plugins/sp/scripts/batch-preflight.ts +72 -1
  52. package/plugins/sp/scripts/commit-guard.mjs +216 -0
  53. package/plugins/sp/scripts/commit-guard.ts +249 -0
  54. package/plugins/sp/scripts/inline-run-dispatch.mjs +130 -0
  55. package/plugins/sp/scripts/inline-run-dispatch.ts +135 -0
  56. package/plugins/sp/scripts/inline-run-setup.mjs +562 -80
  57. package/plugins/sp/scripts/inline-run-setup.ts +52 -78
  58. package/plugins/sp/scripts/persist-out-check.mjs +90 -41
  59. package/plugins/sp/scripts/persist-out-check.ts +112 -57
  60. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  61. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  62. package/plugins/sp/scripts/quality-gate.mjs +30213 -61
  63. package/plugins/sp/scripts/quality-gate.ts +21 -5
  64. package/plugins/sp/scripts/residual-scan.mjs +86 -31
  65. package/plugins/sp/scripts/residual-scan.ts +28 -30
  66. package/plugins/sp/scripts/run-summary.mjs +432 -14
  67. package/plugins/sp/scripts/run-summary.ts +41 -177
  68. package/plugins/sp/scripts/session-timeline.mjs +109 -11
  69. package/plugins/sp/scripts/session-timeline.ts +57 -19
  70. package/plugins/sp/scripts/task-diffstat.mjs +32 -29
  71. package/plugins/sp/scripts/task-diffstat.ts +34 -34
  72. package/plugins/sp/scripts/wrapup-steps.mjs +150 -2
  73. package/plugins/sp/scripts/wrapup-steps.ts +203 -1
  74. package/plugins/sp/skills/brainstorm/SKILL.md +9 -47
  75. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  76. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +1 -0
  77. package/plugins/sp/skills/code-implementation/SKILL.md +48 -1
  78. package/plugins/sp/skills/code-verification/SKILL.md +3 -4
  79. package/plugins/sp/skills/code-verification/references/secu-review.md +3 -0
  80. package/plugins/sp/skills/code-verification/references/verdict-schema.md +68 -0
  81. package/plugins/sp/skills/fleet-join/SKILL.md +1 -1
  82. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  83. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  84. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  85. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  86. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  87. package/plugins/sp/skills/session-review/SKILL.md +40 -7
  88. package/plugins/sp/skills/source-driven-development/SKILL.md +5 -0
  89. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +5 -3
  90. package/plugins/sp/skills/spur-cli/SKILL.md +5 -2
  91. package/plugins/sp/skills/spur-cli/references/agent.md +20 -15
  92. package/plugins/sp/skills/spur-cli/references/decision.md +16 -8
  93. package/plugins/sp/skills/spur-cli/references/message.md +7 -7
  94. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  95. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +19 -3
  96. package/plugins/sp/skills/spur-cli/references/tasks.md +10 -7
  97. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +7 -0
  98. package/plugins/sp/skills/spur-cli/references/workflows.md +7 -2
  99. package/plugins/sp/skills/spur-composer/SKILL.md +1 -1
  100. package/plugins/sp/skills/spur-dev/SKILL.md +20 -14
  101. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +33 -3
  102. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +49 -14
  103. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -17
  104. package/plugins/sp/skills/spur-dev/references/execution-batch-continuation.md +68 -0
  105. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +199 -0
  106. package/plugins/sp/skills/spur-dev/references/execution-batch.md +156 -1024
  107. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +133 -0
  108. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +6 -0
  109. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +471 -0
  110. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +379 -0
  111. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +44 -12
  112. package/plugins/sp/skills/spur-dev/references/glossary.md +2 -2
  113. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +340 -165
  114. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +178 -0
  115. package/plugins/sp/skills/wayfinder/SKILL.md +10 -14
  116. package/schemas/state-machine-workflow.schema.json +23 -0
  117. package/spur.js +42402 -26889
  118. package/web/_astro/{BoardApp.VeoxxLBy.js → BoardApp.B3GQZvj8.js} +1 -1
  119. package/web/_astro/BoardApp.BT7L2kSb.js +324 -0
  120. package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
  121. package/web/_astro/{arc.f4e0OYLg.js → arc.vaT7tPOB.js} +1 -1
  122. package/web/_astro/{architectureDiagram-3BPJPVTR.SSIvxi-Z.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
  123. package/web/_astro/{blockDiagram-GPEHLZMM.DSXIHOFf.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
  124. package/web/_astro/{c4Diagram-AAUBKEIU.DG9Ubu85.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
  125. package/web/_astro/channel.D4nG9YLf.js +1 -0
  126. package/web/_astro/{chunk-2J33WTMH.C7QU88Qh.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
  127. package/web/_astro/{chunk-4BX2VUAB.26Z94xBj.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
  128. package/web/_astro/{chunk-55IACEB6.DYRiBkqw.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
  129. package/web/_astro/{chunk-727SXJPM.D5Vfv5pe.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
  130. package/web/_astro/{chunk-AQP2D5EJ.DXHV47bN.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
  131. package/web/_astro/{chunk-FMBD7UC4.D1suAfOs.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
  132. package/web/_astro/{chunk-ND2GUHAM.D6BmdHEY.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
  133. package/web/_astro/{chunk-QZHKN3VN.rnPU-Ak-.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
  134. package/web/_astro/{classDiagram-4FO5ZUOK.CvEfXIo1.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
  135. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CvEfXIo1.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
  136. package/web/_astro/{cose-bilkent-S5V4N54A.DkMisLSm.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
  137. package/web/_astro/{cynefin-OW5HDTMX.C77Q-pF6.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
  138. package/web/_astro/{dagre-BM42HDAG.YaV8ce27.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
  139. package/web/_astro/{diagram-2AECGRRQ.CUyGEKge.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
  140. package/web/_astro/{diagram-5GNKFQAL.CRgd8dgj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
  141. package/web/_astro/{diagram-KO2AKTUF.Bmzys9hq.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
  142. package/web/_astro/{diagram-LMA3HP47.BvmJY4Lw.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
  143. package/web/_astro/{diagram-OG6HWLK6.0hEhD30d.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
  144. package/web/_astro/{erDiagram-TEJ5UH35.Bjt8lrnm.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
  145. package/web/_astro/{flowDiagram-I6XJVG4X.CJZTjRDV.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
  146. package/web/_astro/{ganttDiagram-6RSMTGT7._VqoVafc.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
  147. package/web/_astro/{gitGraphDiagram-PVQCEYII.CEDb7tVo.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
  148. package/web/_astro/index.BhSwN8yY.css +1 -0
  149. package/web/_astro/{infoDiagram-5YYISTIA.CgwifPpK.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
  150. package/web/_astro/{ishikawaDiagram-YF4QCWOH.B99wx2J1.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
  151. package/web/_astro/{journeyDiagram-JHISSGLW.2hRwJbEo.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
  152. package/web/_astro/{kanban-definition-UN3LZRKU.BahJkKNC.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
  153. package/web/_astro/{linear.DguUUe6V.js → linear.BzGnBwNL.js} +1 -1
  154. package/web/_astro/{mermaid.core.CvgfmqVr.js → mermaid.core.CSbTpIVz.js} +4 -4
  155. package/web/_astro/{mindmap-definition-RKZ34NQL.B3qEA3x9.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
  156. package/web/_astro/{pieDiagram-4H26LBE5.FlWA-_ru.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
  157. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bVn35X7.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
  158. package/web/_astro/{requirementDiagram-4Y6WPE33.ChykIhct.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
  159. package/web/_astro/{sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
  160. package/web/_astro/{sequenceDiagram-3UESZ5HK.J7JsHWDt.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
  161. package/web/_astro/{stateDiagram-AJRCARHV.DAPbiCSS.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
  162. package/web/_astro/{stateDiagram-v2-BHNVJYJU.D2jvSi0q.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
  163. package/web/_astro/{timeline-definition-PNZ67QCA.psW2ci29.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
  164. package/web/_astro/{vennDiagram-CIIHVFJN.DlILYVmg.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
  165. package/web/_astro/{wardleyDiagram-YWT4CUSO.xNWwLJnn.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
  166. package/web/_astro/{xychartDiagram-2RQKCTM6.EU_BuH7V.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
  167. package/web/index.html +2 -2
  168. package/plugins/sp/commands/dev-parallel.md +0 -31
  169. package/web/_astro/BoardApp.Bzr4OXrW.js +0 -324
  170. package/web/_astro/TaskDetail.LAyR_Pvn.js +0 -1
  171. package/web/_astro/channel.CUFdtWhm.js +0 -1
  172. package/web/_astro/index.Dg_3bOaJ.css +0 -1
@@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url';
9
9
  // Type-only namespace import (erased before bundling; the smoke scanner exempts `import type`).
10
10
  import type * as spurApp from '@gobing-ai/spur-app';
11
11
  import { getEnvVar } from '../lib/env';
12
+ import { loadInlineApp } from '../lib/inline-run-app';
13
+ import { writeCloseSummary } from './run-summary';
12
14
 
13
15
  // EMBEDDED_SPUR_SCHEMAS: declared by the bundle twin, never exported by the barrel — optional
14
16
  // intersection member, exactly what the previous structural cast expressed.
@@ -18,9 +20,12 @@ function usage(): never {
18
20
  const text = [
19
21
  'Usage: bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]',
20
22
  ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
21
- ' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> --duration-ms <n> [--estimated] [--spur-bin <path>]',
22
- ' bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--spur-bin <path>] (1007 R5 batch trace emission)',
23
- ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--spur-bin <path>]',
23
+ ' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> --status <done|failed> --ok <true|false> [--duration-ms <n>] [--estimated] [--project-root <path>] [--spur-bin <path>]',
24
+ ' bun plugins/sp/scripts/inline-run-setup.ts --node-enter --run-id <id> --node <state> [--project-root <path>] [--spur-bin <path>] (1136 R4: stamps the node enter time)',
25
+ ' bun plugins/sp/scripts/inline-run-setup.ts --actions-file <json-file> --run-id <id> [--project-root <path>] [--spur-bin <path>] (1007 R5 batch trace emission)',
26
+ ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--reason <terminal-reason>] [--no-summary] [--project-root <path>] [--spur-bin <path>]',
27
+ ' --project-root (1136 R1) names the tree that OWNS the run row; without it the run row is read from the cwd tree, and a miss exits 1 with RUN_NOT_FOUND',
28
+ ' --duration-ms is optional on --action (1136 R4): omitted, the duration is measured from the calling --node-enter; supplied, it records provenance host-reported',
24
29
  ' bun plugins/sp/scripts/inline-run-setup.ts --persist-out --from <worktree-path> [--task-file <path>]... [--spur-bin <path>]',
25
30
  ' terminal-reason is a closed enum (0937 R2): done, paused-operator, failed-check, failed-agent, failed-timeout, failed-guard, cancelled, interrupted, retry-exhausted',
26
31
  ' close defaults (1051 AC1): --status done → reason done, --status paused → reason paused-operator; --status failed requires an explicit --reason',
@@ -61,19 +66,7 @@ function resolveAppEntry(spurBin: string): { entry: string; portable: boolean }
61
66
  return { entry, portable: true };
62
67
  }
63
68
 
64
- // 0937 R2 closed terminal-reason vocabulary for `--close`. COPIED from
65
- // packages/app/src/workflow/terminal-reason.ts (no value import); parity test asserts equality.
66
- const TERMINAL_REASONS = new Set([
67
- 'done',
68
- 'paused-operator',
69
- 'failed-check',
70
- 'failed-agent',
71
- 'failed-timeout',
72
- 'failed-guard',
73
- 'cancelled',
74
- 'interrupted',
75
- 'retry-exhausted',
76
- ]);
69
+ const SETUP_EXPORTS = ['readInstalledInventory', 'runInlineRunSetup', 'writeInlineRunOutcome'] as const;
77
70
 
78
71
  async function main(): Promise<void> {
79
72
  // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
@@ -90,7 +83,9 @@ async function main(): Promise<void> {
90
83
  close = false,
91
84
  decide = false,
92
85
  persistOut = false,
93
- estimated = false;
86
+ nodeEnter = false,
87
+ estimated = false,
88
+ noSummary = false;
94
89
  const taskFiles: string[] = [];
95
90
  let spurBin = getEnvVar('SPUR_BIN') ?? '';
96
91
  const argv = process.argv.slice(2);
@@ -100,8 +95,10 @@ async function main(): Promise<void> {
100
95
  else if (flag === '--action') action = true;
101
96
  else if (flag === '--close') close = true;
102
97
  else if (flag === '--decide') decide = true;
98
+ else if (flag === '--node-enter') nodeEnter = true;
103
99
  else if (flag === '--persist-out') persistOut = true;
104
100
  else if (flag === '--estimated') estimated = true;
101
+ else if (flag === '--no-summary') noSummary = true;
105
102
  else if (flag === '--task-file') taskFiles.push(argv[++i] ?? '');
106
103
  else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
107
104
  else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
@@ -114,6 +111,7 @@ async function main(): Promise<void> {
114
111
  const optionsJson = flags.get('--options-json') ?? '';
115
112
  const node = flags.get('--node') ?? '';
116
113
  const kind = flags.get('--kind') ?? '';
114
+ const projectRoot = flags.get('--project-root') ?? '';
117
115
  const status = flags.get('--status') ?? '';
118
116
  const reason = flags.get('--reason') ?? '';
119
117
  const okRaw = flags.get('--ok') ?? '';
@@ -123,37 +121,33 @@ async function main(): Promise<void> {
123
121
  // `--estimated` labels a host-reported duration the driver did NOT time around the action
124
122
  // (1070 R2). Only `--action` carries one: every other mode is refused here, before any write.
125
123
  if (estimated && !action) usage();
124
+ // 1146 AC3: `--no-summary` belongs to the close step; any other mode is a usage error.
125
+ if (noSummary && !close) usage();
126
126
 
127
127
  if (fingerprint) {
128
128
  if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
129
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
129
+ const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunFingerprint']);
130
130
  process.exit(await app.runInlineRunFingerprint({ taskFile: taskFiles[0] ?? '', featureFile }));
131
131
  }
132
132
 
133
- if (actionsFile !== '') {
134
- if (action || close || fingerprint || decide || persistOut || file !== '' || taskFiles.length > 0) usage();
135
- if (runId.trim() === '' || status !== '' || node !== '' || kind !== '') usage();
136
- if (okRaw !== '' || durationRaw !== '') usage();
137
- if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
138
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
139
- process.exit(await app.runInlineRunTraceBatch({ runId, actionsFile }));
140
- }
141
-
142
133
  if (decide) {
143
134
  if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
144
135
  if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
145
136
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
146
137
  try {
147
- const { entry, portable } = resolveAppEntry(spurBin);
148
- const app = (await import(entry)) as InlineApp;
149
- const bundlePath = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
150
- const lib = (await import(bundlePath)) as typeof import('../lib/inline-run.generated.mjs');
151
- // Decide-enabled switch resolved at the driver boundary (ADR-082), passed as a param.
152
- const enabled = await lib.resolveDecideDecisionMakerEnabled(
153
- process.cwd(),
154
- portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined,
138
+ const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunDecide']);
139
+ // Decide switch + catalog config, resolved once (1094, ADR-082), behind the skew guard (1120 R2).
140
+ const { app: lib } = await loadInlineApp<typeof import('../lib/inline-run.generated.mjs')>(
141
+ spurBin,
142
+ () => ({ entry: fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url)), portable }),
143
+ ['resolveDecideDecisionMakerEnabled', 'loadSpurConfig'],
155
144
  );
156
- process.exit(await app.runInlineRunDecide({ runId, node, optionsFile: optionsJson, enabled }));
145
+ const loadOpts = portable ? { embeddedSchemas: lib.EMBEDDED_SPUR_SCHEMAS } : undefined;
146
+ const [enabled, spurConfig] = await Promise.all([
147
+ lib.resolveDecideDecisionMakerEnabled(process.cwd(), loadOpts),
148
+ lib.loadSpurConfig(process.cwd(), loadOpts),
149
+ ]);
150
+ process.exit(await app.runInlineRunDecide({ runId, node, optionsFile: optionsJson, enabled, spurConfig }));
157
151
  } catch (error) {
158
152
  const message = error instanceof Error ? error.message : String(error);
159
153
  process.stdout.write(`${JSON.stringify({ ok: false, runId, error: message })}\n`);
@@ -164,56 +158,36 @@ async function main(): Promise<void> {
164
158
  if (persistOut) {
165
159
  if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
166
160
  if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
167
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
161
+ const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunPersistOut']);
168
162
  process.exit(await app.runInlineRunPersistOut({ from, taskFiles }));
169
163
  }
170
164
 
171
- if (action || close) {
172
- if (action && close) usage();
173
- if (runId.trim() === '' || status.trim() === '') usage();
165
+ if (nodeEnter || actionsFile !== '' || action || close) {
174
166
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
175
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
176
- if (close) {
177
- if (!app.isInlineRunCloseStatus(status)) usage();
178
- // 0937 R2: failed close needs a declared closed-enum reason — before any write.
179
- if (reason.trim() === '' ? status === 'failed' : !TERMINAL_REASONS.has(reason)) usage();
180
- process.exit(
181
- await app.runInlineRunTrace({
182
- runId,
183
- close: true,
184
- node: '',
185
- kind: '',
186
- status,
187
- ok: true,
188
- durationMs: 0,
189
- ...(reason.trim() === '' ? {} : { reason }),
190
- }),
191
- );
192
- }
193
- if (node.trim() === '' || kind.trim() === '') usage();
194
- if (!app.isInlineRunActionStatus(status)) usage();
195
- // `--ok` and `--duration-ms` are required and exact (0868 #2): no silent defaults.
196
- if (okRaw !== 'true' && okRaw !== 'false') usage();
197
- const durationMs = Number(durationRaw);
198
- if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
199
- process.exit(
200
- await app.runInlineRunTrace({
201
- runId,
202
- close: false,
203
- node,
204
- kind,
205
- status,
206
- ok: okRaw === 'true',
207
- durationMs,
208
- ...(estimated ? { estimated: true } : {}),
209
- }),
210
- );
167
+ const mode = nodeEnter ? 'node-enter' : actionsFile !== '' ? 'actions-file' : close ? 'close' : 'action';
168
+ const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunTraceMode']);
169
+ const code = await app.runInlineRunTraceMode({
170
+ mode,
171
+ runId,
172
+ node,
173
+ kind,
174
+ status,
175
+ reason,
176
+ ok: okRaw,
177
+ durationMs: durationRaw,
178
+ actionsFile,
179
+ projectRoot,
180
+ estimated,
181
+ noSummary,
182
+ ...(noSummary ? {} : { summarize: async (run) => writeCloseSummary(run) }),
183
+ });
184
+ if (code === 2) usage();
185
+ process.exit(code);
211
186
  }
212
187
 
213
188
  if (runId.trim() === '' || file.trim() === '') usage();
214
189
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
215
- const { entry, portable } = resolveAppEntry(spurBin);
216
- const app = (await import(entry)) as InlineApp;
190
+ const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, SETUP_EXPORTS);
217
191
  let inventory: unknown;
218
192
  try {
219
193
  // The CLI walk lives in the app service (ADR-130 glue budget): the script passes only the
@@ -9,60 +9,64 @@ var EVIDENCE_DIR = join(".spur", "memory", "evidence");
9
9
  var RUN_DIR = join(".spur", "run");
10
10
  var NAMED_CAP = 32;
11
11
  var PREFIX_CAP = 64;
12
- var EVIDENCE_CAP = 256;
13
12
  function wbsOfTaskFile(path) {
14
13
  const m = /^(\d+)_/.exec(basename(path));
15
14
  return m?.[1] ?? null;
16
15
  }
17
- function listFilesUnder(root, rel, cap) {
18
- const dir = join(root, rel);
16
+ function walkFiles(dir, root) {
19
17
  if (!existsSync(dir))
20
18
  return [];
21
19
  const out = [];
22
20
  const walk = (cur) => {
23
- for (const entry of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
24
- const p = join(cur, entry.name);
25
- if (entry.isDirectory()) {
26
- if (!walk(p))
27
- return false;
28
- } else {
21
+ for (const e of readdirSync(cur, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
22
+ const p = join(cur, e.name);
23
+ if (e.isDirectory())
24
+ walk(p);
25
+ else if (e.isFile())
29
26
  out.push(relative(root, p));
30
- if (out.length > cap)
31
- return false;
32
- }
33
27
  }
34
- return true;
35
28
  };
36
- return walk(dir) ? out : null;
29
+ walk(dir);
30
+ return out;
37
31
  }
38
- function listObligations(wtRoot, wbsList, runIds) {
39
- const evidence = listFilesUnder(wtRoot, EVIDENCE_DIR, EVIDENCE_CAP);
40
- if (evidence === null)
41
- return null;
42
- const out = [...evidence];
43
- const prefixes = [...wbsList, ...runIds];
44
- for (const prefix of prefixes) {
45
- const dir = join(wtRoot, RUN_DIR);
46
- if (!existsSync(dir))
32
+ function collectOwned(rels, prefixes, counts, out) {
33
+ for (const rel of rels) {
34
+ const prefix = prefixes.find((p) => basename(rel).startsWith(`${p}-`));
35
+ if (prefix === undefined)
47
36
  continue;
48
- let entries;
49
- try {
50
- entries = readdirSync(dir).sort();
51
- } catch {
52
- return null;
53
- }
54
- const owned = entries.filter((n) => n.startsWith(`${prefix}-`));
55
- if (owned.length > PREFIX_CAP)
56
- return null;
57
- out.push(...owned.map((n) => join(RUN_DIR, n)));
37
+ const count = (counts.get(prefix) ?? 0) + 1;
38
+ if (count > PREFIX_CAP)
39
+ return false;
40
+ counts.set(prefix, count);
41
+ out.push(rel);
58
42
  }
59
- return [...new Set(out)].sort();
43
+ return true;
44
+ }
45
+ function listObligations(wtRoot, wbsList, runIds) {
46
+ const prefixes = [...new Set([...wbsList, ...runIds].filter(Boolean))];
47
+ if (prefixes.length === 0)
48
+ return null;
49
+ const evDir = join(wtRoot, EVIDENCE_DIR);
50
+ const runDir = join(wtRoot, RUN_DIR);
51
+ const evFiles = walkFiles(evDir, wtRoot);
52
+ const runFiles = existsSync(runDir) ? readdirSync(runDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => join(RUN_DIR, e.name)).sort() : [];
53
+ const files = [];
54
+ const counts = new Map;
55
+ if (!collectOwned(evFiles, prefixes, counts, files))
56
+ return null;
57
+ if (!collectOwned(runFiles, prefixes, counts, files))
58
+ return null;
59
+ const unowned = evFiles.length - files.filter((f) => f.startsWith(EVIDENCE_DIR)).length;
60
+ return { files: [...new Set(files)].sort(), unowned };
60
61
  }
61
- function compareTrees(wtRoot, invokeRoot, files) {
62
+ function compareTrees(wtRoot, invokeRoot, files, foreignDivergent = new Set) {
62
63
  const missing = [];
63
64
  const divergent = [];
64
65
  let ok = 0;
65
66
  for (const rel of files) {
67
+ if (foreignDivergent.has(rel) || foreignDivergent.has(basename(rel))) {
68
+ continue;
69
+ }
66
70
  const wtPath = join(wtRoot, rel);
67
71
  const invPath = join(invokeRoot, rel);
68
72
  if (!existsSync(invPath)) {
@@ -82,7 +86,7 @@ function compareTrees(wtRoot, invokeRoot, files) {
82
86
  return { missing, divergent, ok };
83
87
  }
84
88
  function persistOutCheckUsage() {
85
- return "usage: persist-out-check --from <worktree> [--task-file <path>]\u2026 [--run-id <id>]\u2026 [--root <invoke-tree>]";
89
+ return "usage: persist-out-check --from <worktree> [--task-file <path>]\u2026 [--run-id <id>]\u2026 [--root <invoke-tree>] [--success-json <path>]";
86
90
  }
87
91
  function defaultInvokeRoot(cwd) {
88
92
  try {
@@ -95,6 +99,7 @@ function defaultInvokeRoot(cwd) {
95
99
  }
96
100
  function main(argv, invokeRoot = defaultInvokeRoot(process.cwd())) {
97
101
  let wtRoot = "";
102
+ let successJsonPath = "";
98
103
  const wbsList = [];
99
104
  const runIds = [];
100
105
  for (let i = 0;i < argv.length; i++) {
@@ -113,25 +118,64 @@ function main(argv, invokeRoot = defaultInvokeRoot(process.cwd())) {
113
118
  const id = argv[++i] ?? "";
114
119
  if (id)
115
120
  runIds.push(id);
121
+ } else if (arg === "--success-json") {
122
+ successJsonPath = argv[++i] ?? "";
116
123
  } else {
117
124
  process.stderr.write(`${persistOutCheckUsage()}
118
125
  `);
119
126
  return 2;
120
127
  }
121
128
  }
129
+ if (wbsList.length === 0 && runIds.length === 0) {
130
+ process.stderr.write(`${persistOutCheckUsage()}
131
+ persist-out-check: at least one --task-file or --run-id prefix required
132
+ `);
133
+ return 2;
134
+ }
122
135
  if (!wtRoot || !existsSync(wtRoot) || !statSync(wtRoot).isDirectory()) {
123
136
  process.stderr.write(`${persistOutCheckUsage()}
124
137
  persist-out-check: --from must be an existing worktree directory
125
138
  `);
126
139
  return 2;
127
140
  }
128
- const files = listObligations(wtRoot, wbsList, runIds);
129
- if (files === null) {
130
- process.stderr.write(`persist-out-check: BLOCKED \u2014 worktree evidence listing failed or exceeded caps (64/prefix, ${EVIDENCE_CAP} evidence files) \u2014 inspect by hand
141
+ const foreignDivergent = new Set;
142
+ const skips = [];
143
+ const candidates = [
144
+ successJsonPath,
145
+ join(invokeRoot, ".spur", "run", "persist-out.json"),
146
+ join(wtRoot, ".spur", "run", "persist-out.json")
147
+ ].filter(Boolean);
148
+ for (const cand of candidates) {
149
+ if (existsSync(cand)) {
150
+ try {
151
+ const parsed = JSON.parse(readFileSync(cand, "utf8"));
152
+ if (Array.isArray(parsed.evidenceSkipped)) {
153
+ for (const item of parsed.evidenceSkipped) {
154
+ if (item.reason === "foreign-divergent" && typeof item.name === "string") {
155
+ foreignDivergent.add(item.name);
156
+ foreignDivergent.add(join(EVIDENCE_DIR, item.name));
157
+ }
158
+ }
159
+ }
160
+ if (Array.isArray(parsed.skipped)) {
161
+ for (const item of parsed.skipped) {
162
+ if (typeof item.id === "string" && typeof item.reason === "string") {
163
+ skips.push({ id: item.id, reason: item.reason });
164
+ }
165
+ }
166
+ }
167
+ break;
168
+ } catch {}
169
+ }
170
+ }
171
+ const obligations = listObligations(wtRoot, wbsList, runIds);
172
+ if (obligations === null) {
173
+ process.stderr.write(`persist-out-check: BLOCKED \u2014 worktree evidence listing failed or exceeded caps (${PREFIX_CAP}/prefix) \u2014 inspect by hand
131
174
  `);
132
175
  return 1;
133
176
  }
134
- const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files);
177
+ const { files, unowned } = obligations;
178
+ const { missing, divergent, ok } = compareTrees(wtRoot, invokeRoot, files, foreignDivergent);
135
179
  const findings = [
136
180
  ...missing.map((f) => `MISSING ${f}`),
137
181
  ...divergent.map((f) => `DIVERGENT ${f} \u2014 reconcile by hand (persist-out never overwrites)`)
@@ -143,7 +187,12 @@ persist-out-check: --from must be an existing worktree directory
143
187
  process.stdout.write(`\u2026 +${findings.length - NAMED_CAP} more
144
188
  `);
145
189
  if (findings.length === 0) {
146
- process.stdout.write(`persist-out-check: ok \u2014 ${ok} evidence file(s) persisted, nothing abandoned
190
+ for (const skip of skips) {
191
+ process.stdout.write(`SKIP ${skip.id}: ${skip.reason}
192
+ `);
193
+ }
194
+ const abandonedMsg = skips.length > 0 ? `${skips.length} skipped` : "nothing abandoned";
195
+ process.stdout.write(`persist-out-check: ok \u2014 ${ok} evidence file(s) persisted, unowned: ${unowned}, ${abandonedMsg}
147
196
  `);
148
197
  return 0;
149
198
  }