@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
@@ -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> [--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,46 +66,7 @@ function resolveAppEntry(spurBin: string): { entry: string; portable: boolean }
61
66
  return { entry, portable: true };
62
67
  }
63
68
 
64
- /** Let the selected CLI own config and layer resolution, then revalidate its snapshot in the app. */
65
- async function readInstalledInventory(file: string, spurBin: string): Promise<unknown> {
66
- const localCli = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
67
- const bundle = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
68
- const { splitLaunchCommand } = (await import(bundle)) as typeof import('../lib/inline-run.generated.mjs');
69
- const launch = spurBin
70
- ? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"')
71
- : existsSync(localCli)
72
- ? { command: 'bun', leadingArgs: [localCli] }
73
- : { command: 'spur', leadingArgs: [] };
74
- if ('error' in launch) throw new Error(launch.error);
75
- const result = spawnSync(
76
- launch.command,
77
- [...launch.leadingArgs, 'workflow', 'show', file, '--format', 'todo', '--json'],
78
- { cwd: process.cwd(), encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
79
- );
80
- if (result.status !== 0) {
81
- throw new Error(
82
- `could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`,
83
- );
84
- }
85
- const value: unknown = JSON.parse(result.stdout);
86
- // Honor the existing optional JSON envelope without inventing another projection format.
87
- if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) return value.data;
88
- return value;
89
- }
90
-
91
- // 0937 R2 closed terminal-reason vocabulary for `--close`. COPIED from
92
- // packages/app/src/workflow/terminal-reason.ts (no value import); parity test asserts equality.
93
- const TERMINAL_REASONS = new Set([
94
- 'done',
95
- 'paused-operator',
96
- 'failed-check',
97
- 'failed-agent',
98
- 'failed-timeout',
99
- 'failed-guard',
100
- 'cancelled',
101
- 'interrupted',
102
- 'retry-exhausted',
103
- ]);
69
+ const SETUP_EXPORTS = ['readInstalledInventory', 'runInlineRunSetup', 'writeInlineRunOutcome'] as const;
104
70
 
105
71
  async function main(): Promise<void> {
106
72
  // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
@@ -116,7 +82,10 @@ async function main(): Promise<void> {
116
82
  action = false,
117
83
  close = false,
118
84
  decide = false,
119
- persistOut = false;
85
+ persistOut = false,
86
+ nodeEnter = false,
87
+ estimated = false,
88
+ noSummary = false;
120
89
  const taskFiles: string[] = [];
121
90
  let spurBin = getEnvVar('SPUR_BIN') ?? '';
122
91
  const argv = process.argv.slice(2);
@@ -126,7 +95,10 @@ async function main(): Promise<void> {
126
95
  else if (flag === '--action') action = true;
127
96
  else if (flag === '--close') close = true;
128
97
  else if (flag === '--decide') decide = true;
98
+ else if (flag === '--node-enter') nodeEnter = true;
129
99
  else if (flag === '--persist-out') persistOut = true;
100
+ else if (flag === '--estimated') estimated = true;
101
+ else if (flag === '--no-summary') noSummary = true;
130
102
  else if (flag === '--task-file') taskFiles.push(argv[++i] ?? '');
131
103
  else if (flag === '--spur-bin') spurBin = argv[++i] ?? spurBin;
132
104
  else if (flag !== undefined) flags.set(flag, argv[++i] ?? '');
@@ -139,42 +111,43 @@ async function main(): Promise<void> {
139
111
  const optionsJson = flags.get('--options-json') ?? '';
140
112
  const node = flags.get('--node') ?? '';
141
113
  const kind = flags.get('--kind') ?? '';
114
+ const projectRoot = flags.get('--project-root') ?? '';
142
115
  const status = flags.get('--status') ?? '';
143
116
  const reason = flags.get('--reason') ?? '';
144
117
  const okRaw = flags.get('--ok') ?? '';
145
118
  const durationRaw = flags.get('--duration-ms') ?? '';
146
119
  const actionsFile = flags.get('--actions-file') ?? '';
147
120
 
121
+ // `--estimated` labels a host-reported duration the driver did NOT time around the action
122
+ // (1070 R2). Only `--action` carries one: every other mode is refused here, before any write.
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
+
148
127
  if (fingerprint) {
149
128
  if (runId !== '' || file !== '' || taskFiles.length !== 1 || (taskFiles[0] ?? '').trim() === '') usage();
150
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
129
+ const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunFingerprint']);
151
130
  process.exit(await app.runInlineRunFingerprint({ taskFile: taskFiles[0] ?? '', featureFile }));
152
131
  }
153
132
 
154
- if (actionsFile !== '') {
155
- if (action || close || fingerprint || decide || persistOut || file !== '' || taskFiles.length > 0) usage();
156
- if (runId.trim() === '' || status !== '' || node !== '' || kind !== '') usage();
157
- if (okRaw !== '' || durationRaw !== '') usage();
158
- if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
159
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
160
- process.exit(await app.runInlineRunTraceBatch({ runId, actionsFile }));
161
- }
162
-
163
133
  if (decide) {
164
134
  if (action || close || fingerprint || file !== '' || taskFiles.length > 0) usage();
165
135
  if (runId.trim() === '' || node.trim() === '' || optionsJson.trim() === '') usage();
166
136
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
167
137
  try {
168
- const { entry, portable } = resolveAppEntry(spurBin);
169
- const app = (await import(entry)) as InlineApp;
170
- const bundlePath = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
171
- const lib = (await import(bundlePath)) as typeof import('../lib/inline-run.generated.mjs');
172
- // Decide-enabled switch resolved at the driver boundary (ADR-082), passed as a param.
173
- const enabled = await lib.resolveDecideDecisionMakerEnabled(
174
- process.cwd(),
175
- 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'],
176
144
  );
177
- 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 }));
178
151
  } catch (error) {
179
152
  const message = error instanceof Error ? error.message : String(error);
180
153
  process.stdout.write(`${JSON.stringify({ ok: false, runId, error: message })}\n`);
@@ -185,50 +158,45 @@ async function main(): Promise<void> {
185
158
  if (persistOut) {
186
159
  if (fingerprint || decide || action || close || runId !== '' || file !== '') usage();
187
160
  if (from.trim() === '' || taskFiles.some((taskFile) => taskFile.trim() === '')) usage();
188
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
161
+ const { app } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, ['runInlineRunPersistOut']);
189
162
  process.exit(await app.runInlineRunPersistOut({ from, taskFiles }));
190
163
  }
191
164
 
192
- if (action || close) {
193
- if (action && close) usage();
194
- if (runId.trim() === '' || status.trim() === '') usage();
165
+ if (nodeEnter || actionsFile !== '' || action || close) {
195
166
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
196
- const app = (await import(resolveAppEntry(spurBin).entry)) as InlineApp;
197
- if (close) {
198
- if (!app.isInlineRunCloseStatus(status)) usage();
199
- // 0937 R2: failed close needs a declared closed-enum reason — before any write.
200
- if (reason.trim() === '' ? status === 'failed' : !TERMINAL_REASONS.has(reason)) usage();
201
- process.exit(
202
- await app.runInlineRunTrace({
203
- runId,
204
- close: true,
205
- node: '',
206
- kind: '',
207
- status,
208
- ok: true,
209
- durationMs: 0,
210
- ...(reason.trim() === '' ? {} : { reason }),
211
- }),
212
- );
213
- }
214
- if (node.trim() === '' || kind.trim() === '') usage();
215
- if (!app.isInlineRunActionStatus(status)) usage();
216
- // `--ok` and `--duration-ms` are required and exact (0868 #2): no silent defaults.
217
- if (okRaw !== 'true' && okRaw !== 'false') usage();
218
- const durationMs = Number(durationRaw);
219
- if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
220
- process.exit(
221
- await app.runInlineRunTrace({ runId, close: false, node, kind, status, ok: okRaw === 'true', durationMs }),
222
- );
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);
223
186
  }
224
187
 
225
188
  if (runId.trim() === '' || file.trim() === '') usage();
226
189
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
227
- const { entry, portable } = resolveAppEntry(spurBin);
228
- const app = (await import(entry)) as InlineApp;
190
+ const { app, portable } = await loadInlineApp<InlineApp>(spurBin, resolveAppEntry, SETUP_EXPORTS);
229
191
  let inventory: unknown;
230
192
  try {
231
- inventory = await readInstalledInventory(file, spurBin);
193
+ // The CLI walk lives in the app service (ADR-130 glue budget): the script passes only the
194
+ // paths it derives from its own module URL.
195
+ inventory = await app.readInstalledInventory({
196
+ file,
197
+ spurBin,
198
+ localCli: fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url)),
199
+ });
232
200
  } catch (error) {
233
201
  const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
234
202
  app.writeInlineRunOutcome(runId, { ok: false, runId, error: message });
@@ -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
  }