@gobing-ai/spur 0.3.91 → 0.3.92

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 (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/plugin-scripts.json +17 -1
  4. package/config/workflows/feature-lifecycle.yaml +14 -5
  5. package/config/workflows/feature-verification.yaml +35 -27
  6. package/config/workflows/history-anatomy.yaml +14 -25
  7. package/config/workflows/idea-pipeline.yaml +9 -5
  8. package/config/workflows/task-pipeline.yaml +185 -4
  9. package/config/workflows/wrapup-pipeline.yaml +47 -5
  10. package/package.json +9 -9
  11. package/plugins/sp/agents/super-planner.md +14 -5
  12. package/plugins/sp/commands/dev-dogfood.md +4 -4
  13. package/plugins/sp/commands/dev-fixall.md +8 -5
  14. package/plugins/sp/commands/dev-run.md +6 -0
  15. package/plugins/sp/commands/dev-runall.md +12 -6
  16. package/plugins/sp/commands/dev-verify.md +9 -0
  17. package/plugins/sp/commands/dev-verifyall.md +5 -0
  18. package/plugins/sp/lib/idea-handoff.generated.mjs +301 -300
  19. package/plugins/sp/lib/inline-run.generated.d.mts +17 -0
  20. package/plugins/sp/lib/inline-run.generated.mjs +1460 -0
  21. package/plugins/sp/plugin.json +1 -1
  22. package/plugins/sp/references/environment-lens.md +1 -1
  23. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
  24. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
  25. package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
  26. package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
  27. package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
  28. package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
  29. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -0
  30. package/plugins/sp/scripts/inline-run-setup.mjs +349 -0
  31. package/plugins/sp/scripts/inline-run-setup.ts +192 -75
  32. package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
  33. package/plugins/sp/scripts/record-feature-sync.ts +84 -0
  34. package/plugins/sp/scripts/residual-scan.mjs +476 -0
  35. package/plugins/sp/scripts/residual-scan.ts +614 -0
  36. package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
  37. package/plugins/sp/scripts/task-size-precheck.ts +8 -3
  38. package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
  39. package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
  40. package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
  41. package/plugins/sp/skills/code-verification/SKILL.md +21 -0
  42. package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
  43. package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
  44. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
  45. package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
  46. package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
  47. package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
  48. package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
  49. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  50. package/plugins/sp/skills/session-review/SKILL.md +12 -2
  51. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  52. package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
  53. package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
  54. package/plugins/sp/skills/spur-dev/SKILL.md +11 -4
  55. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -1
  56. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  57. package/plugins/sp/skills/spur-dev/references/execution-batch.md +285 -57
  58. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
  59. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +19 -4
  60. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +43 -16
  61. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -0
  62. package/spur.js +21827 -19615
  63. package/web/_astro/BoardApp.CerSBgis.js +192 -0
  64. package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
  65. package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
  66. package/web/_astro/arc.CPwg6Rw0.js +1 -0
  67. package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
  68. package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
  69. package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
  70. package/web/_astro/channel.MYZLKNwy.js +1 -0
  71. package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
  72. package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
  73. package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
  74. package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
  75. package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
  76. package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
  77. package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
  78. package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
  79. package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
  80. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
  81. package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
  82. package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
  83. package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
  84. package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
  85. package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
  86. package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
  87. package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
  88. package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
  89. package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
  90. package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
  91. package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
  92. package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
  93. package/web/_astro/index.Hjbr15fG.css +1 -0
  94. package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
  95. package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
  96. package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
  97. package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
  98. package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
  99. package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
  100. package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
  101. package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
  102. package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
  103. package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
  104. package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
  105. package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
  106. package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
  107. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
  108. package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
  109. package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
  110. package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
  111. package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
  112. package/web/index.html +2 -2
  113. package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
  114. package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
  115. package/web/_astro/arc.C0rrflm_.js +0 -1
  116. package/web/_astro/channel.SRrg1P-w.js +0 -1
  117. package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
  118. package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
  119. package/web/_astro/index.Bx6GY4RH.css +0 -1
  120. package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
@@ -5,27 +5,27 @@
5
5
  * Unlike the subprocess path (`spur workflow run`), the interactive inline driver
6
6
  * allocated a run id but never persisted an authoritative `runs` row, so bound
7
7
  * `run.artifact` registration (0785 R3) correctly refused every inline record. This
8
- * script is the thin delegate the driver now runs at Run setup: it resolves the spur
9
- * repo checkout from the SPUR_BIN chain, imports the real app service
8
+ * script is the thin delegate the driver now runs at Run setup: it loads the source
9
+ * app service when the SPUR_BIN chain identifies a checkout, otherwise its generated bundle
10
10
  * (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
11
- * resolve the SAME project-or-bundled definition the engine would launch, compute the
11
+ * resolve the SAME project/registered/shared definition the engine would launch, compute the
12
12
  * canonical definition digest with the exported hash machinery, and create-or-attach the
13
13
  * run row through the existing engine persistence adapter.
14
14
  *
15
15
  * The script itself contains NO direct SQL, NO second hasher and NO persistence policy —
16
- * every rule lives in packages/app (0804 D1). On a bundle-only install there is no repo
17
- * checkout to import the app service from, so the setup fails closed with actionable
18
- * remediation guidance (point SPUR_BIN at a repo checkout); it never falls back to an
19
- * unbound run (0804 R1 failure policy).
16
+ * every rule lives in packages/app (0804 D1). On a bundle-only install, the existing
17
+ * workflow-show projection selects the CLI's definition; the bundled app revalidates its
18
+ * schema and digest before recording the selected layer. It never creates an unbound run.
20
19
  *
21
- * Outcome JSON is written to `.spur/run/<run-id>-inline-setup.json` so the driver can
22
- * seed the inline var overlay (`__runId`, `__definitionDigest`) that proof capture and
23
- * bound registration verify against. Exit 0 = authoritative identity ready (created or
24
- * idempotently attached); exit 1 = fail closed, the driver must stop.
20
+ * The setup outcome is written into the two-file run record (task 0927 R1) — machine state
21
+ * at `.spur/run/<run-id>.state.json` (atomic replace) plus the `.spur/run/<run-id>.md`
22
+ * run-start header — so the driver can seed the inline var overlay (`__runId`,
23
+ * `__definitionDigest`) that proof capture and bound registration verify against. The
24
+ * pre-0927 `<run-id>-inline-setup.json` sidecar is no longer written. Exit 0 = authoritative
25
+ * identity ready (created or idempotently attached); exit 1 = fail closed, the driver must stop.
25
26
  *
26
- * Repo-only script (ADR-065): it imports the app workspace source, so it runs under bun
27
- * against a monorepo checkout only — the same posture as task-size-precheck.ts and
28
- * task-evidence-precheck.ts.
27
+ * Standard script (ADR-065): the generated Node-runnable twin re-enters Bun for the
28
+ * existing SQLite runtime. Bun on PATH is required; a monorepo checkout is not.
29
29
  *
30
30
  * Usage:
31
31
  * bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
@@ -41,7 +41,7 @@
41
41
  * 0868): `--action` records one completed action boundary as an `action_runs` row through the
42
42
  * shared `WorkflowActionTraceWriter`, `--close` marks the run row terminal through the same
43
43
  * writer. `--action` is best-effort: a persistence failure is appended to
44
- * `.spur/run/<run-id>.log` and the script still exits 0 with `{"ok":false}` on stdout, so
44
+ * `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
45
45
  * observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
46
46
  * bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
47
47
  * Exit 2 is reserved for usage errors.
@@ -49,12 +49,24 @@
49
49
  * Env: SPUR_BIN
50
50
  */
51
51
 
52
- import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
52
+ import { spawnSync } from 'node:child_process';
53
+ import {
54
+ appendFileSync,
55
+ closeSync,
56
+ existsSync,
57
+ mkdirSync,
58
+ openSync,
59
+ readFileSync,
60
+ renameSync,
61
+ unlinkSync,
62
+ writeFileSync,
63
+ writeSync,
64
+ } from 'node:fs';
53
65
  import { dirname, join, resolve } from 'node:path';
54
66
  import { fileURLToPath } from 'node:url';
55
67
  import { getEnvVar } from '../lib/env';
56
68
 
57
- /** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
69
+ /** Setup outcome projected into the run-record state `.spur/run/<run-id>.state.json` (0927 R1). */
58
70
  interface SetupOutcome {
59
71
  readonly ok: boolean;
60
72
  readonly runId: string;
@@ -87,8 +99,8 @@ function usage(): never {
87
99
  }
88
100
 
89
101
  /**
90
- * The run id becomes a filename under `.spur/run/` (`<run-id>-inline-setup.json`), so it must be a
91
- * single safe filename component before anything is written — the same guard class the
102
+ * The run id becomes filenames under `.spur/run/` (`<run-id>.state.json` + `<run-id>.md`),
103
+ * so it must be a single safe filename component before anything is written — the same guard class the
92
104
  * task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
93
105
  * refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
94
106
  * every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
@@ -97,7 +109,7 @@ const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
97
109
 
98
110
  function refuseUnsafeRunId(runId: string): never {
99
111
  // Refuse BEFORE any outcome write: an unsafe id must never reach
100
- // `.spur/run/<run-id>-inline-setup.json` (no traversal, no unintended file).
112
+ // `.spur/run/<run-id>.state.json` / `.md` (no traversal, no unintended file).
101
113
  console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
102
114
  console.error(
103
115
  ' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
@@ -112,12 +124,9 @@ function refuseUnsafeRunId(runId: string): never {
112
124
  * (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
113
125
  * root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
114
126
  * three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
115
- * binary without the app workspace) has no app entry to import — the caller fails that
116
- * closed with remediation guidance.
127
+ * binary without the app workspace) uses the adjacent generated application bundle.
117
128
  */
118
- function resolveAppEntry(
119
- spurBin: string,
120
- ): { entry: string; repoRoot: string } | { entry: null; repoRoot: null; chain: string } {
129
+ function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
121
130
  let candidates: string[] = [];
122
131
  if (spurBin !== '') {
123
132
  candidates = [spurBin];
@@ -137,16 +146,119 @@ function resolveAppEntry(
137
146
  const srcDir = dirname(mainModule);
138
147
  const repoRoot = resolve(srcDir, '..', '..', '..');
139
148
  const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
140
- if (existsSync(appEntry)) return { entry: appEntry, repoRoot };
141
- return { entry: null, repoRoot: null, chain: `${candidate} (no ${appEntry})` };
149
+ if (existsSync(appEntry)) return { entry: appEntry, portable: false };
142
150
  }
143
- return { entry: null, repoRoot: null, chain: spurBin === '' ? 'PATH spur (bundle-only install)' : spurBin };
151
+ const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
152
+ if (!existsSync(entry)) {
153
+ throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
154
+ }
155
+ return { entry, portable: true };
156
+ }
157
+
158
+ /** Let the selected CLI own config and layer resolution, then revalidate its snapshot in the app. */
159
+ async function readInstalledInventory(file: string, spurBin: string): Promise<unknown> {
160
+ const localCli = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
161
+ const bundle = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
162
+ const { splitLaunchCommand } = (await import(bundle)) as typeof import('../lib/inline-run.generated.mjs');
163
+ const launch = spurBin
164
+ ? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"')
165
+ : existsSync(localCli)
166
+ ? { command: 'bun', leadingArgs: [localCli] }
167
+ : { command: 'spur', leadingArgs: [] };
168
+ if ('error' in launch) throw new Error(launch.error);
169
+ const result = spawnSync(
170
+ launch.command,
171
+ [...launch.leadingArgs, 'workflow', 'show', file, '--format', 'todo', '--json'],
172
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
173
+ );
174
+ if (result.status !== 0) {
175
+ throw new Error(
176
+ `could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`,
177
+ );
178
+ }
179
+ const value: unknown = JSON.parse(result.stdout);
180
+ // Honor the existing optional JSON envelope without inventing another projection format.
181
+ if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) {
182
+ return value.data;
183
+ }
184
+ return value;
144
185
  }
145
186
 
187
+ /**
188
+ * Project the setup outcome into the two-file run record (task 0927 R1): the machine state
189
+ * merges into `.spur/run/<run-id>.state.json` (atomic same-directory temp + rename, the
190
+ * 0925 R1 pattern) and `.spur/run/<run-id>.md` receives its run-start header exactly once.
191
+ * A re-setup of the same run id rewrites the state from the current outcome but keeps the
192
+ * prior `startedAt` instead of resurrecting setup time. Identity/provenance fields only —
193
+ * never prompt bodies — so the record carries no secret-bearing content by construction
194
+ * (0927 AC2). The retired `<run-id>-inline-setup.json` sidecar is not written (0927 R4);
195
+ * its readers were migrated to the pair state in the same change.
196
+ */
146
197
  function writeOutcome(runId: string, outcome: SetupOutcome): void {
147
198
  const runDir = join(process.cwd(), '.spur', 'run');
148
199
  if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
149
- writeFileSync(join(runDir, `${runId}-inline-setup.json`), `${JSON.stringify(outcome, null, 4)}\n`);
200
+ const statePath = join(runDir, `${runId}.state.json`);
201
+ const markdownPath = join(runDir, `${runId}.md`);
202
+ let prior: Record<string, unknown> = {};
203
+ try {
204
+ const parsed: unknown = JSON.parse(readFileSync(statePath, 'utf8'));
205
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
206
+ prior = parsed as Record<string, unknown>;
207
+ }
208
+ } catch {
209
+ // No prior state (new run) or unreadable file — the outcome alone defines the state.
210
+ }
211
+ const at = new Date().toISOString();
212
+ const state = {
213
+ schemaVersion: 1 as const,
214
+ runId,
215
+ ...(outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {}),
216
+ ...(outcome.status !== undefined ? { status: outcome.status } : {}),
217
+ startedAt: typeof prior.startedAt === 'string' ? prior.startedAt : at,
218
+ updatedAt: at,
219
+ ...(outcome.attached !== undefined ? { attached: outcome.attached } : {}),
220
+ ...(outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {}),
221
+ ...(outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {}),
222
+ ...(outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {}),
223
+ ...(outcome.layer !== undefined ? { layer: outcome.layer } : {}),
224
+ ...(outcome.workdir !== undefined ? { workdir: outcome.workdir } : {}),
225
+ // 0948 R7: project the setup outcome itself, so the retired `-inline-setup.json`
226
+ // sidecar's `ok` is still readable from the pair, and a SUCCESSFUL re-setup
227
+ // explicitly drops any prior `error` — a stale failure message must not outlive
228
+ // the failure it described (the state object is rebuilt, never merged with `prior`).
229
+ ok: outcome.ok,
230
+ ...(outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}),
231
+ };
232
+ const temp = `${statePath}.tmp`;
233
+ try {
234
+ writeFileSync(temp, `${JSON.stringify(state, null, 4)}\n`);
235
+ renameSync(temp, statePath);
236
+ } catch {
237
+ // Best-effort: a failing record write must not wedge setup reporting — and never
238
+ // leaves `.tmp` residue behind (0926 R1).
239
+ try {
240
+ unlinkSync(temp);
241
+ } catch {
242
+ // Nothing to clean (temp was never created).
243
+ }
244
+ }
245
+ // 0948 R7: create the header with `wx` (O_EXCL) instead of `existsSync` → `appendFileSync`.
246
+ // The check-then-append pair could append a SECOND header when two setups raced, and it
247
+ // kept "header present" as a separate fact from the identity write. One exclusive create
248
+ // makes exactly-one-header atomic; EEXIST just means it is already there.
249
+ let headerFd: number | undefined;
250
+ try {
251
+ headerFd = openSync(markdownPath, 'wx');
252
+ writeSync(
253
+ headerFd,
254
+ `# spur inline run ${runId} — ${outcome.workflowName ?? 'unknown workflow'} — setup ${at}\n`,
255
+ );
256
+ } catch {
257
+ // EEXIST (already written) or an unwritable path — the human header is not the setup
258
+ // identity, so this stays best-effort exactly like the state write above.
259
+ } finally {
260
+ if (headerFd !== undefined) closeSync(headerFd);
261
+ }
150
262
  }
151
263
 
152
264
  /**
@@ -159,14 +271,7 @@ function writeOutcome(runId: string, outcome: SetupOutcome): void {
159
271
  * Returns the process exit code: 0 printed, 1 read/resolve failure.
160
272
  */
161
273
  async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
162
- const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
163
- if (entry === null || repoRoot === null) {
164
- console.error(
165
- `inline-run-setup: FAIL — no monorepo checkout of spur is reachable via ${chain}. ` +
166
- 'The proof-input digest must be computed by the app service; a bundle-only install cannot do it.',
167
- );
168
- return 1;
169
- }
274
+ const { entry } = resolveAppEntry(spurBin);
170
275
  const app = (await import(entry)) as {
171
276
  computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
172
277
  readProofInputContents: (
@@ -204,7 +309,7 @@ const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
204
309
  const ACTION_STATUSES = new Set(['done', 'failed']);
205
310
 
206
311
  /** Input for the ADR-117 emission modes (`--action` / `--close`). */
207
- import type { WorkflowActionTraceWriter } from '@gobing-ai/app';
312
+ import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
208
313
 
209
314
  interface TraceModeInput {
210
315
  readonly runId: string;
@@ -218,9 +323,23 @@ interface TraceModeInput {
218
323
  }
219
324
 
220
325
  /**
221
- * Append one emission-failure line to `.spur/run/<run-id>.log` — the run log the inline
222
- * driver already owns. Best-effort and synchronous (the process may exit immediately
223
- * after), and never throws: an unwritable log must not wedge the run (ADR-117 R3).
326
+ * The run-record file for appended driver lines (task 0927 R1): `.spur/run/<run-id>.md`
327
+ * for pair-based runs. A legacy run that predates the pair keeps appending to its declared
328
+ * `.spur/run/<run-id>.log` — the same precedence as `readWorkflowRunRecord` (the pair wins,
329
+ * legacy stays readable in place).
330
+ */
331
+ function runRecordLogPath(runDir: string, runId: string): string {
332
+ const markdownPath = join(runDir, `${runId}.md`);
333
+ const legacyLogPath = join(runDir, `${runId}.log`);
334
+ if (existsSync(legacyLogPath) && !existsSync(markdownPath)) return legacyLogPath;
335
+ return markdownPath;
336
+ }
337
+
338
+ /**
339
+ * Append one emission-failure line to the run record markdown — `.spur/run/<run-id>.md`,
340
+ * the run log the inline driver already owns. Best-effort and synchronous (the process may
341
+ * exit immediately after), and never throws: an unwritable log must not wedge the run
342
+ * (ADR-117 R3).
224
343
  */
225
344
  function appendTraceFailureLine(runId: string, detail: string): void {
226
345
  try {
@@ -228,7 +347,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
228
347
  if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
229
348
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
230
349
  const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
231
- appendFileSync(join(runDir, `${safeRunId}.log`), `[${stamp}] ${detail}\n`);
350
+ appendFileSync(runRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}\n`);
232
351
  } catch {
233
352
  // Best-effort (R3): the run continues even when the failure cannot be recorded.
234
353
  }
@@ -254,27 +373,20 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
254
373
  return input.close ? 1 : 0;
255
374
  };
256
375
 
257
- const { entry, repoRoot, chain } = resolveAppEntry(input.spurBin);
258
- if (entry === null || repoRoot === null) {
259
- return fail(
260
- `no monorepo checkout of spur is reachable via ${chain} — the shared trace writer ` +
261
- 'lives in packages/app (point SPUR_BIN at a repo checkout)',
262
- );
263
- }
264
-
265
376
  // Compile-time link (0868 finding #5): the writer half is typed by the real packages/app
266
377
  // export (type-only import, erased at runtime) so a signature drift breaks THIS file's
267
378
  // typecheck instead of hiding behind the hand-declared cast.
268
- const app = (await import(entry)) as {
269
- openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
270
- createWorkflowActionTraceWriter: (
271
- db: unknown,
272
- recordFailure?: (failure: unknown) => void,
273
- ) => WorkflowActionTraceWriter;
274
- };
275
-
276
379
  let projectDb: { adapter: unknown; close: () => void } | undefined;
277
380
  try {
381
+ const { entry } = resolveAppEntry(input.spurBin);
382
+ const app = (await import(entry)) as {
383
+ openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
384
+ createWorkflowActionTraceWriter: (
385
+ db: unknown,
386
+ recordFailure?: (failure: unknown) => void,
387
+ ) => WorkflowActionTraceWriter;
388
+ };
389
+
278
390
  projectDb = await app.openInlineRunProjectDb(process.cwd());
279
391
  const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
280
392
  const detail = failure as { operation?: string; error?: string };
@@ -321,6 +433,14 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
321
433
  }
322
434
 
323
435
  async function main(): Promise<void> {
436
+ // The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
437
+ if (!process.versions.bun) {
438
+ const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
439
+ stdio: 'inherit',
440
+ });
441
+ if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
442
+ process.exit(child.status ?? 1);
443
+ }
324
444
  let runId = '';
325
445
  let file = '';
326
446
  let fingerprint = false;
@@ -396,23 +516,7 @@ async function main(): Promise<void> {
396
516
  if (runId.trim() === '' || file.trim() === '') usage();
397
517
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
398
518
 
399
- const { entry, repoRoot, chain } = resolveAppEntry(spurBin);
400
- if (entry === null || repoRoot === null) {
401
- const outcome: SetupOutcome = {
402
- ok: false,
403
- runId,
404
- error:
405
- `inline run setup failed closed: no monorepo checkout of spur is reachable via ${chain}. ` +
406
- 'The authoritative run identity must be persisted by the app service ' +
407
- '(packages/app/src/services/inline-run-setup.ts); a bundle-only install cannot do this. ' +
408
- 'Remediation: point SPUR_BIN at a repo checkout, e.g. ' +
409
- 'SPUR_BIN="bun /path/to/spur/apps/cli/src/index.ts". The pipeline must not run unbound.',
410
- };
411
- writeOutcome(runId, outcome);
412
- console.error(`inline-run-setup: FAIL for run ${runId}`);
413
- console.error(` ${outcome.error}`);
414
- process.exit(1);
415
- }
519
+ const { entry, portable } = resolveAppEntry(spurBin);
416
520
 
417
521
  // Dynamic import by absolute path: the app source graph resolves its own workspace
418
522
  // dependencies from the repo checkout, never from this plugin script's location.
@@ -422,11 +526,22 @@ async function main(): Promise<void> {
422
526
  getDb: () => Promise<unknown>;
423
527
  file: string;
424
528
  runId: string;
529
+ inventory?: unknown;
530
+ embeddedSchemas?: ReadonlyMap<string, string>;
425
531
  }) => Promise<SetupOutcome & { ok: boolean }>;
426
532
  openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
533
+ EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string>;
427
534
  };
428
535
 
429
536
  const workdir = process.cwd();
537
+ let inventory: unknown;
538
+ try {
539
+ inventory = await readInstalledInventory(file, spurBin);
540
+ } catch (error) {
541
+ const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
542
+ writeOutcome(runId, { ok: false, runId, error: message });
543
+ throw new Error(message);
544
+ }
430
545
  const projectDb = await app.openInlineRunProjectDb(workdir);
431
546
  let exitCode = 0;
432
547
  try {
@@ -435,6 +550,8 @@ async function main(): Promise<void> {
435
550
  getDb: async () => projectDb.adapter,
436
551
  file,
437
552
  runId,
553
+ inventory,
554
+ ...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
438
555
  });
439
556
  writeOutcome(runId, result);
440
557
  if (!result.ok) {
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // plugins/sp/scripts/record-feature-sync.ts
3
+ import { spawnSync } from "node:child_process";
4
+ import { appendFileSync, existsSync } from "node:fs";
5
+
6
+ // plugins/sp/lib/env.ts
7
+ function getEnvVar(name, fallback) {
8
+ const raw = process.env[name];
9
+ return raw === undefined ? fallback : raw;
10
+ }
11
+
12
+ // plugins/sp/scripts/record-feature-sync.ts
13
+ function resolveSpurBin(argv) {
14
+ const flag = argv.indexOf("--spur-bin");
15
+ const value = flag !== -1 ? argv[flag + 1] : undefined;
16
+ return value ?? getEnvVar("spurBin") ?? "spur";
17
+ }
18
+ function main() {
19
+ const argv = process.argv.slice(2);
20
+ const spurBin = resolveSpurBin(argv);
21
+ const wbs = getEnvVar("wbs") ?? "";
22
+ if (wbs.length === 0) {
23
+ console.error("record-feature-sync: env `wbs` is required");
24
+ process.exit(0);
25
+ }
26
+ const reportPath = `.spur/run/${wbs}-report.txt`;
27
+ const report = (line) => {
28
+ appendFileSync(reportPath, `${line}
29
+ `);
30
+ };
31
+ const show = spawnSync(spurBin, ["task", "show", wbs, "--json"], {
32
+ encoding: "utf8",
33
+ stdio: ["ignore", "pipe", "ignore"]
34
+ });
35
+ let featureId = "";
36
+ try {
37
+ const parsed = JSON.parse(show.stdout ?? "");
38
+ featureId = String(parsed.feature_id ?? parsed.frontmatter?.feature_id ?? "");
39
+ } catch {
40
+ featureId = "";
41
+ }
42
+ if (featureId.length === 0) {
43
+ report(`Orphan task ${wbs} — no feature_id linked — proposal: consider linking to a parent feature.`);
44
+ return;
45
+ }
46
+ if (existsSync("plugins/sp/scripts/feature-sync-bounded.ts")) {
47
+ spawnSync("bun", ["plugins/sp/scripts/feature-sync-bounded.ts", featureId, "--spur-bin", spurBin, "--json"], {
48
+ stdio: "inherit"
49
+ });
50
+ return;
51
+ }
52
+ const staged = spawnSync("superskill", ["script", "path", "sp", "feature-sync-bounded.mjs"], {
53
+ encoding: "utf8",
54
+ stdio: ["ignore", "pipe", "ignore"]
55
+ });
56
+ const stagedPath = (staged.stdout ?? "").trim();
57
+ if (staged.status === 0 && stagedPath.length > 0 && existsSync(stagedPath)) {
58
+ spawnSync("node", [stagedPath, featureId, "--spur-bin", spurBin, "--json"], { stdio: "inherit" });
59
+ return;
60
+ }
61
+ spawnSync(spurBin, ["feature", "sync", featureId, "--json"], { stdio: "inherit" });
62
+ }
63
+ main();
@@ -0,0 +1,84 @@
1
+ /**
2
+ * record-feature-sync — record-step post-record feature sync owner (tasks 0328 / 0411 / 0931).
3
+ *
4
+ * Extracted from the record onEnter shell (0931 R5): ADR-115 caps one workflow shell at 10
5
+ * logical commands and 800 chars, and the deferFeatureSync guard plus the sync chain can no
6
+ * longer share it. The pipeline shell resolves this script (in-repo first, superskill-staged
7
+ * .mjs fallback), notes a deferral, and delegates the rest here:
8
+ *
9
+ * - A task with no feature_id gets the orphan proposal note (0328).
10
+ * - The sync routes through feature-sync-bounded.ts (0411 retry suppression), then the
11
+ * superskill-staged module, then bare `spur feature sync` (0328).
12
+ * - Best-effort by contract: a sync failure never fails the record step.
13
+ *
14
+ * Node-builtin imports only (ADR-065): this file is transpiled to a node-runnable .mjs twin
15
+ * by `superskill script convert`. Env: wbs (required), spurBin (default "spur"); the
16
+ * `--spur-bin <bin>` argument wins over the env var.
17
+ */
18
+
19
+ import { spawnSync } from 'node:child_process';
20
+ import { appendFileSync, existsSync } from 'node:fs';
21
+ import { getEnvVar } from '../lib/env';
22
+
23
+ function resolveSpurBin(argv: string[]): string {
24
+ const flag = argv.indexOf('--spur-bin');
25
+ const value = flag !== -1 ? argv[flag + 1] : undefined;
26
+ return value ?? getEnvVar('spurBin') ?? 'spur';
27
+ }
28
+
29
+ function main(): void {
30
+ const argv = process.argv.slice(2);
31
+ const spurBin = resolveSpurBin(argv);
32
+ const wbs = getEnvVar('wbs') ?? '';
33
+ if (wbs.length === 0) {
34
+ console.error('record-feature-sync: env `wbs` is required');
35
+ process.exit(0); // best-effort contract — never fail the record step
36
+ }
37
+
38
+ const reportPath = `.spur/run/${wbs}-report.txt`;
39
+ const report = (line: string): void => {
40
+ appendFileSync(reportPath, `${line}\n`);
41
+ };
42
+
43
+ const show = spawnSync(spurBin, ['task', 'show', wbs, '--json'], {
44
+ encoding: 'utf8',
45
+ stdio: ['ignore', 'pipe', 'ignore'],
46
+ });
47
+ let featureId = '';
48
+ try {
49
+ const parsed = JSON.parse(show.stdout ?? '') as {
50
+ feature_id?: string;
51
+ frontmatter?: { feature_id?: string };
52
+ };
53
+ featureId = String(parsed.feature_id ?? parsed.frontmatter?.feature_id ?? '');
54
+ } catch {
55
+ featureId = '';
56
+ }
57
+
58
+ if (featureId.length === 0) {
59
+ report(`Orphan task ${wbs} — no feature_id linked — proposal: consider linking to a parent feature.`);
60
+ return;
61
+ }
62
+
63
+ if (existsSync('plugins/sp/scripts/feature-sync-bounded.ts')) {
64
+ spawnSync('bun', ['plugins/sp/scripts/feature-sync-bounded.ts', featureId, '--spur-bin', spurBin, '--json'], {
65
+ stdio: 'inherit',
66
+ });
67
+ return;
68
+ }
69
+
70
+ const staged = spawnSync('superskill', ['script', 'path', 'sp', 'feature-sync-bounded.mjs'], {
71
+ encoding: 'utf8',
72
+ stdio: ['ignore', 'pipe', 'ignore'],
73
+ });
74
+ const stagedPath = (staged.stdout ?? '').trim();
75
+ if (staged.status === 0 && stagedPath.length > 0 && existsSync(stagedPath)) {
76
+ spawnSync('node', [stagedPath, featureId, '--spur-bin', spurBin, '--json'], { stdio: 'inherit' });
77
+ return;
78
+ }
79
+
80
+ spawnSync(spurBin, ['feature', 'sync', featureId, '--json'], { stdio: 'inherit' });
81
+ // Falls through with exit 0: a sync failure never fails the record step (0411).
82
+ }
83
+
84
+ main();