@gobing-ai/spur 0.3.85 → 0.3.86

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 (106) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +0 -25
  3. package/config/plugin-scripts.json +0 -5
  4. package/config/rules/boundary/env-var-hygiene.yaml +41 -0
  5. package/config/transition-shims.json +1 -1
  6. package/config/workflow-candidates.json +41 -0
  7. package/config/workflows/feature-lifecycle.yaml +4 -2
  8. package/config/workflows/feature-verification.yaml +70 -0
  9. package/config/workflows/idea-pipeline.yaml +24 -12
  10. package/config/workflows/task-pipeline.yaml +43 -12
  11. package/config/workflows/wrapup-pipeline.yaml +70 -7
  12. package/package.json +9 -9
  13. package/plugins/sp/README.md +0 -3
  14. package/plugins/sp/hooks/agent-hint.ts +5 -4
  15. package/plugins/sp/hooks/careful-guard.ts +3 -1
  16. package/plugins/sp/hooks/context-post-tool.ts +2 -1
  17. package/plugins/sp/hooks/context-session-start.ts +4 -3
  18. package/plugins/sp/hooks/context-session-stop.ts +2 -1
  19. package/plugins/sp/hooks/pi/guard-extension.ts +4 -3
  20. package/plugins/sp/hooks/task-write-guard.ts +4 -3
  21. package/plugins/sp/lib/idea-handoff.generated.mjs +260 -260
  22. package/plugins/sp/plugin.json +1 -1
  23. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +13 -4
  24. package/plugins/sp/scripts/daily-summary/daily-summary.ts +6 -4
  25. package/plugins/sp/scripts/feature-sync-bounded.mjs +10 -2
  26. package/plugins/sp/scripts/feature-sync-bounded.ts +2 -1
  27. package/plugins/sp/scripts/idea-handoff.mjs +6 -1
  28. package/plugins/sp/scripts/idea-handoff.ts +4 -1
  29. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -1
  30. package/plugins/sp/scripts/inline-run-setup.ts +185 -2
  31. package/plugins/sp/scripts/pr-reviewing.mjs +8 -1
  32. package/plugins/sp/scripts/pr-reviewing.ts +2 -1
  33. package/plugins/sp/scripts/quality-gate.mjs +8 -1
  34. package/plugins/sp/scripts/quality-gate.ts +2 -1
  35. package/plugins/sp/scripts/surface-drift-inventory.ts +1 -4
  36. package/plugins/sp/scripts/task-evidence-precheck.ts +2 -1
  37. package/plugins/sp/scripts/task-size-precheck.ts +6 -4
  38. package/plugins/sp/scripts/verify-answer-lint.ts +2 -1
  39. package/plugins/sp/scripts/workflow-step-profile.mjs +10 -2
  40. package/plugins/sp/scripts/workflow-step-profile.ts +2 -1
  41. package/plugins/sp/scripts/wrapup-steps.mjs +8 -1
  42. package/plugins/sp/scripts/wrapup-steps.ts +2 -1
  43. package/plugins/sp/skills/spur-cli/references/workflows.md +8 -7
  44. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -5
  45. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +1 -1
  46. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -2
  47. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +59 -1
  48. package/schemas/state-machine-workflow.schema.json +5 -0
  49. package/schemas/transition-flow-workflow.schema.json +5 -0
  50. package/spur.js +20988 -20652
  51. package/web/_astro/BoardApp.CJiqp5pS.js +1 -0
  52. package/web/_astro/{BoardApp.yW425dRZ.js → BoardApp.yBBcFXWP.js} +4 -4
  53. package/web/_astro/{TaskDetail.CGwJAinW.js → TaskDetail.DqFJbRFc.js} +1 -1
  54. package/web/_astro/{arc.B-qNXzSO.js → arc.DL-BpHoi.js} +1 -1
  55. package/web/_astro/{architectureDiagram-3BPJPVTR.Bdg-xiji.js → architectureDiagram-3BPJPVTR.9IdQYyDq.js} +1 -1
  56. package/web/_astro/{blockDiagram-GPEHLZMM.DFArg3Kp.js → blockDiagram-GPEHLZMM.BKsFCqTl.js} +1 -1
  57. package/web/_astro/{c4Diagram-AAUBKEIU.BkOLcjyx.js → c4Diagram-AAUBKEIU.DhwI0dh1.js} +1 -1
  58. package/web/_astro/channel.CX5453qQ.js +1 -0
  59. package/web/_astro/{chunk-2J33WTMH.DN3c20V4.js → chunk-2J33WTMH.B3QVmQ9S.js} +1 -1
  60. package/web/_astro/{chunk-4BX2VUAB.B64K0Ttd.js → chunk-4BX2VUAB.DBSuqs9F.js} +1 -1
  61. package/web/_astro/{chunk-55IACEB6.C-AJvNwa.js → chunk-55IACEB6.BSYTWAYD.js} +1 -1
  62. package/web/_astro/{chunk-727SXJPM.DMkr46uS.js → chunk-727SXJPM.cWVuxXfS.js} +1 -1
  63. package/web/_astro/{chunk-AQP2D5EJ.4_WGbI64.js → chunk-AQP2D5EJ.DpU_Ob3d.js} +1 -1
  64. package/web/_astro/{chunk-FMBD7UC4.cbFW_lVE.js → chunk-FMBD7UC4.BykFkyji.js} +1 -1
  65. package/web/_astro/{chunk-ND2GUHAM.CDW_-2O1.js → chunk-ND2GUHAM.DwgHlMdY.js} +1 -1
  66. package/web/_astro/{chunk-QZHKN3VN.BoYOt_yg.js → chunk-QZHKN3VN.CusXUGWM.js} +1 -1
  67. package/web/_astro/{classDiagram-4FO5ZUOK.BNT6XpHQ.js → classDiagram-4FO5ZUOK.fx0ObzkN.js} +1 -1
  68. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BNT6XpHQ.js → classDiagram-v2-Q7XG4LA2.fx0ObzkN.js} +1 -1
  69. package/web/_astro/{cose-bilkent-S5V4N54A.CI1DQxR3.js → cose-bilkent-S5V4N54A.Z4HgOlsd.js} +1 -1
  70. package/web/_astro/{cynefin-OW5HDTMX.A8ARH61p.js → cynefin-OW5HDTMX.B5dIZHJu.js} +1 -1
  71. package/web/_astro/{dagre-BM42HDAG.u28ZmqwR.js → dagre-BM42HDAG.DT70Q_Yw.js} +1 -1
  72. package/web/_astro/{diagram-2AECGRRQ.Dx8JcrGc.js → diagram-2AECGRRQ.DqHA3XBF.js} +1 -1
  73. package/web/_astro/{diagram-5GNKFQAL.BXJUG9GY.js → diagram-5GNKFQAL.BmCem957.js} +1 -1
  74. package/web/_astro/{diagram-KO2AKTUF.BtlMuWqW.js → diagram-KO2AKTUF.sn0-hrE0.js} +1 -1
  75. package/web/_astro/{diagram-LMA3HP47.C4T7qaAy.js → diagram-LMA3HP47.BSHe9tVc.js} +1 -1
  76. package/web/_astro/{diagram-OG6HWLK6.D5HECE10.js → diagram-OG6HWLK6.DHIc-86k.js} +1 -1
  77. package/web/_astro/{erDiagram-TEJ5UH35.B_hMR3yw.js → erDiagram-TEJ5UH35.Bxayrs7v.js} +1 -1
  78. package/web/_astro/{flowDiagram-I6XJVG4X.BAmpcYl3.js → flowDiagram-I6XJVG4X.BkzoE_5I.js} +1 -1
  79. package/web/_astro/{ganttDiagram-6RSMTGT7.DV1bSWK-.js → ganttDiagram-6RSMTGT7.okT6CvTo.js} +1 -1
  80. package/web/_astro/{gitGraphDiagram-PVQCEYII.DxYKkKrJ.js → gitGraphDiagram-PVQCEYII.CJuYbhC7.js} +1 -1
  81. package/web/_astro/{infoDiagram-5YYISTIA.BAWY2xMb.js → infoDiagram-5YYISTIA.RqLy7nBo.js} +1 -1
  82. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CZssRn6V.js → ishikawaDiagram-YF4QCWOH.BwIcoagw.js} +1 -1
  83. package/web/_astro/{journeyDiagram-JHISSGLW.C--muARd.js → journeyDiagram-JHISSGLW.UB1VbWtH.js} +1 -1
  84. package/web/_astro/{kanban-definition-UN3LZRKU.D_QCK4et.js → kanban-definition-UN3LZRKU.AaxMKpTk.js} +1 -1
  85. package/web/_astro/{linear.DzrmTtZ0.js → linear.Nv_xOUjP.js} +1 -1
  86. package/web/_astro/{mermaid.core.Da03W3iu.js → mermaid.core.Bc4LqQgX.js} +4 -4
  87. package/web/_astro/{mindmap-definition-RKZ34NQL.6cy-8hR_.js → mindmap-definition-RKZ34NQL.oKUvU_qi.js} +1 -1
  88. package/web/_astro/{pieDiagram-4H26LBE5.C5rS1pdU.js → pieDiagram-4H26LBE5.DQk0oo03.js} +1 -1
  89. package/web/_astro/{quadrantDiagram-W4KKPZXB.w56GZZ6Q.js → quadrantDiagram-W4KKPZXB.BdDjESDa.js} +1 -1
  90. package/web/_astro/{requirementDiagram-4Y6WPE33.CipX3Pwu.js → requirementDiagram-4Y6WPE33.C2u9hUeH.js} +1 -1
  91. package/web/_astro/{sankeyDiagram-5OEKKPKP.C0VVzgJm.js → sankeyDiagram-5OEKKPKP.CDEoiJST.js} +1 -1
  92. package/web/_astro/{sequenceDiagram-3UESZ5HK.BX2dUUbF.js → sequenceDiagram-3UESZ5HK.D_hT_GAT.js} +1 -1
  93. package/web/_astro/{stateDiagram-AJRCARHV.ypCdgODQ.js → stateDiagram-AJRCARHV.DI8RYG0b.js} +1 -1
  94. package/web/_astro/{stateDiagram-v2-BHNVJYJU.In0baEtg.js → stateDiagram-v2-BHNVJYJU.Bkxz4DnP.js} +1 -1
  95. package/web/_astro/{timeline-definition-PNZ67QCA.CJN4Vkvl.js → timeline-definition-PNZ67QCA.DSY-kH3-.js} +1 -1
  96. package/web/_astro/{vennDiagram-CIIHVFJN.Cf8KkIPY.js → vennDiagram-CIIHVFJN.CpaDtuGr.js} +1 -1
  97. package/web/_astro/{wardleyDiagram-YWT4CUSO.DCZBo9xw.js → wardleyDiagram-YWT4CUSO.DujQWvo8.js} +1 -1
  98. package/web/_astro/{xychartDiagram-2RQKCTM6.Cm4v_MiJ.js → xychartDiagram-2RQKCTM6.DcM5Y4b9.js} +1 -1
  99. package/web/index.html +1 -1
  100. package/config/workflows/basic.yaml +0 -146
  101. package/config/workflows/docs-pipeline.yaml +0 -350
  102. package/config/workflows/feature-dev.yaml +0 -288
  103. package/plugins/sp/scripts/feature-dev-precheck.mjs +0 -146
  104. package/plugins/sp/scripts/feature-dev-precheck.ts +0 -238
  105. package/web/_astro/BoardApp.CHenHFia.js +0 -1
  106. package/web/_astro/channel.CbDHK5UQ.js +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.85",
3
+ "version": "0.3.86",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -8,6 +8,15 @@ import { spawn, spawnSync } from "child_process";
8
8
  import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
9
9
  import { dirname, isAbsolute, join, resolve } from "path";
10
10
 
11
+ // ../ts-libs/packages/utils/dist/env.js
12
+ function getEnvVar(name, fallback) {
13
+ const raw = process.env[name];
14
+ return raw === undefined ? fallback : raw;
15
+ }
16
+ function getEnvVars() {
17
+ return process.env;
18
+ }
19
+
11
20
  // plugins/sp/scripts/daily-summary/logger.ts
12
21
  var state = { console: true, file: false };
13
22
  function emit(stream, message) {
@@ -103,7 +112,7 @@ var defaultProcessSpawner = (cmd, args, env) => {
103
112
  try {
104
113
  const proc = spawn(cmd, args, {
105
114
  stdio: ["ignore", "pipe", "pipe"],
106
- env: env ?? process.env
115
+ env: env ?? getEnvVars()
107
116
  });
108
117
  const stdoutChunks = [];
109
118
  const stderrChunks = [];
@@ -130,7 +139,7 @@ function setProcessSpawner(next) {
130
139
  }
131
140
  async function getCcusageData(date) {
132
141
  try {
133
- const env = { ...process.env };
142
+ const env = { ...getEnvVars() };
134
143
  const ccusageCheck = await processSpawner("ccusage", ["--version"], env);
135
144
  if (ccusageCheck.exitCode !== 0) {
136
145
  return null;
@@ -279,10 +288,10 @@ async function getGitCommits(date) {
279
288
  }
280
289
  }
281
290
  async function promptUser() {
282
- if (process.env.SP_DAILY_SUMMARY_NO_PROMPT === "1") {
291
+ if (getEnvVar("SP_DAILY_SUMMARY_NO_PROMPT") === "1") {
283
292
  return { learnings: "", issuesFixed: "", pending: "" };
284
293
  }
285
- if (process.env.RD3_DAILY_SUMMARY_NO_PROMPT === "1") {
294
+ if (getEnvVar("RD3_DAILY_SUMMARY_NO_PROMPT") === "1") {
286
295
  logger.warn("[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT");
287
296
  return { learnings: "", issuesFixed: "", pending: "" };
288
297
  }
@@ -11,6 +11,8 @@
11
11
  import { spawn, spawnSync } from 'node:child_process';
12
12
  import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
13
13
  import { dirname, isAbsolute, join, resolve } from 'node:path';
14
+ import { getEnvVar, getEnvVars } from '@gobing-ai/ts-utils';
15
+
14
16
  import { logger } from './logger';
15
17
 
16
18
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -225,7 +227,7 @@ export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
225
227
  try {
226
228
  const proc = spawn(cmd, args, {
227
229
  stdio: ['ignore', 'pipe', 'pipe'],
228
- env: env ?? process.env,
230
+ env: env ?? getEnvVars(),
229
231
  });
230
232
  const stdoutChunks: Buffer[] = [];
231
233
  const stderrChunks: Buffer[] = [];
@@ -258,7 +260,7 @@ export function setProcessSpawner(next?: ProcessSpawner): void {
258
260
  export async function getCcusageData(date: string): Promise<CcusageData | null> {
259
261
  try {
260
262
  // Check if ccusage is available
261
- const env = { ...process.env };
263
+ const env = { ...getEnvVars() };
262
264
  const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
263
265
  if (ccusageCheck.exitCode !== 0) {
264
266
  return null;
@@ -473,10 +475,10 @@ export async function getGitCommits(date: string): Promise<GitCommit[]> {
473
475
  // ─── User Input ─────────────────────────────────────────────────────────────
474
476
 
475
477
  export async function promptUser(): Promise<UserAnnotations> {
476
- if (process.env.SP_DAILY_SUMMARY_NO_PROMPT === '1') {
478
+ if (getEnvVar('SP_DAILY_SUMMARY_NO_PROMPT') === '1') {
477
479
  return { learnings: '', issuesFixed: '', pending: '' };
478
480
  }
479
- if (process.env.RD3_DAILY_SUMMARY_NO_PROMPT === '1') {
481
+ if (getEnvVar('RD3_DAILY_SUMMARY_NO_PROMPT') === '1') {
480
482
  logger.warn('[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT');
481
483
  return { learnings: '', issuesFixed: '', pending: '' };
482
484
  }
@@ -8,6 +8,14 @@ import { createHash } from "node:crypto";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
9
  import { dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+
12
+ // ../ts-libs/packages/utils/dist/env.js
13
+ function getEnvVar(name, fallback) {
14
+ const raw = process.env[name];
15
+ return raw === undefined ? fallback : raw;
16
+ }
17
+
18
+ // plugins/sp/scripts/feature-sync-bounded.ts
11
19
  function classifySyncResult(result) {
12
20
  if (result.proposal.gateBlocked === true)
13
21
  return "blocked";
@@ -95,8 +103,8 @@ through unchanged.
95
103
 
96
104
  Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
97
105
  function defaultSpurBin() {
98
- if (process.env.SPUR_BIN)
99
- return process.env.SPUR_BIN;
106
+ if (getEnvVar("SPUR_BIN"))
107
+ return getEnvVar("SPUR_BIN");
100
108
  const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
101
109
  if (existsSync(local))
102
110
  return `bun ${local}`;
@@ -24,6 +24,7 @@ import { createHash } from 'node:crypto';
24
24
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
25
25
  import { dirname } from 'node:path';
26
26
  import { fileURLToPath } from 'node:url';
27
+ import { getEnvVar } from '@gobing-ai/ts-utils';
27
28
 
28
29
  // ── Local types (match packages/app FeatureService shapes; no package import) ───────────
29
30
 
@@ -250,7 +251,7 @@ Exit: 0 = sync handled (applied / no-op / suppressed-blocked / live-blocked).`;
250
251
  * keeps ad-hoc invocations from silently hitting a stale PATH install.
251
252
  */
252
253
  export function defaultSpurBin(): string {
253
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
254
+ if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
254
255
  // scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
255
256
  // on %-encoded paths, e.g. spaces in the checkout directory)
256
257
  const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
3
 
4
+ // ../ts-libs/packages/utils/dist/env.js
5
+ function getEnvVars() {
6
+ return process.env;
7
+ }
8
+
4
9
  // plugins/sp/scripts/idea-handoff.ts
5
10
  var libUrl = new URL("../lib/idea-handoff.generated.mjs", import.meta.url).href;
6
11
  async function loadHandoffLib() {
7
12
  return await import(libUrl);
8
13
  }
9
14
  var IDEA_HANDOFF_USAGE = "usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) \u2014 no subcommands";
10
- async function main(argv, env = process.env) {
15
+ async function main(argv, env = getEnvVars()) {
11
16
  if (argv.length > 0) {
12
17
  process.stderr.write(`${IDEA_HANDOFF_USAGE}
13
18
  `);
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env bun
2
+
3
+ import { getEnvVars } from '@gobing-ai/ts-utils';
4
+
2
5
  /**
3
6
  * idea-handoff — portable entrypoint for the idea-pipeline finalization (task 0824, feature I21).
4
7
  *
@@ -29,7 +32,7 @@ export async function loadHandoffLib(): Promise<typeof import('../lib/idea-hando
29
32
  export const IDEA_HANDOFF_USAGE =
30
33
  'usage: idea-handoff.ts (env: __runId, featureId, optional spurBin) — no subcommands';
31
34
 
32
- export async function main(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
35
+ export async function main(argv: string[], env: NodeJS.ProcessEnv = getEnvVars()): Promise<number> {
33
36
  if (argv.length > 0) {
34
37
  process.stderr.write(`${IDEA_HANDOFF_USAGE}\n`);
35
38
  return 2;
@@ -43,7 +43,7 @@ const DOCUMENTED = {
43
43
  'run.artifact',
44
44
  'command.gate',
45
45
  ]),
46
- guards: new Set(['always', 'shell']),
46
+ guards: new Set(['always', 'shell', 'action-ok', 'contract-violation']),
47
47
  } as const;
48
48
 
49
49
  /** Directory of workflow definitions the driver is responsible for. */
@@ -30,16 +30,29 @@
30
30
  * Usage:
31
31
  * bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
32
32
  * bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]
33
+ * bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> \
34
+ * --status <done|failed|running|paused> --ok <true|false> --duration-ms <n> [--spur-bin <path>]
35
+ * bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]
33
36
  *
34
37
  * The `--fingerprint` mode prints the engine's proof-input digest for the given spec files and
35
38
  * creates nothing (task 0862 R5).
36
39
  *
40
+ * The `--action` / `--close` modes are the inline driver's ADR-117 emission boundary (task
41
+ * 0868): `--action` records one completed action boundary as an `action_runs` row through the
42
+ * shared `WorkflowActionTraceWriter`, `--close` marks the run row terminal through the same
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
45
+ * observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
46
+ * bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
47
+ * Exit 2 is reserved for usage errors.
48
+ *
37
49
  * Env: SPUR_BIN
38
50
  */
39
51
 
40
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
52
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
41
53
  import { dirname, join, resolve } from 'node:path';
42
54
  import { fileURLToPath } from 'node:url';
55
+ import { getEnvVar } from '@gobing-ai/ts-utils';
43
56
 
44
57
  /** Outcome document written to `.spur/run/<run-id>-inline-setup.json`. */
45
58
  interface SetupOutcome {
@@ -63,6 +76,13 @@ function usage(): never {
63
76
  console.error(
64
77
  ' bun plugins/sp/scripts/inline-run-setup.ts --fingerprint --task-file <path> [--feature-file <path>] [--spur-bin <path>]',
65
78
  );
79
+ console.error(
80
+ ' bun plugins/sp/scripts/inline-run-setup.ts --action --run-id <id> --node <state> --kind <kind> ' +
81
+ '--status <done|failed|running|paused> --ok <true|false> --duration-ms <n> [--spur-bin <path>]',
82
+ );
83
+ console.error(
84
+ ' bun plugins/sp/scripts/inline-run-setup.ts --close --run-id <id> --status <done|failed|paused> [--spur-bin <path>]',
85
+ );
66
86
  process.exit(2);
67
87
  }
68
88
 
@@ -177,13 +197,133 @@ async function printFingerprint(taskFile: string, featureFile: string, spurBin:
177
197
  return 0;
178
198
  }
179
199
 
200
+ /** Terminal statuses the inline driver may declare when closing its run row. */
201
+ const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
202
+
203
+ /** Action-row statuses the engine's `action_runs.status` column accepts. */
204
+ const ACTION_STATUSES = new Set(['running', 'done', 'failed', 'paused']);
205
+
206
+ /** Input for the ADR-117 emission modes (`--action` / `--close`). */
207
+ interface TraceModeInput {
208
+ readonly runId: string;
209
+ readonly close: boolean;
210
+ readonly node: string;
211
+ readonly kind: string;
212
+ readonly status: string;
213
+ readonly ok: boolean;
214
+ readonly durationMs: number;
215
+ readonly spurBin: string;
216
+ }
217
+
218
+ /**
219
+ * Append one emission-failure line to `.spur/run/<run-id>.log` — the run log the inline
220
+ * driver already owns. Best-effort and synchronous (the process may exit immediately
221
+ * after), and never throws: an unwritable log must not wedge the run (ADR-117 R3).
222
+ */
223
+ function appendTraceFailureLine(runId: string, detail: string): void {
224
+ try {
225
+ const runDir = join(process.cwd(), '.spur', 'run');
226
+ if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
227
+ const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
228
+ const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
229
+ appendFileSync(join(runDir, `${safeRunId}.log`), `[${stamp}] ${detail}\n`);
230
+ } catch {
231
+ // Best-effort (R3): the run continues even when the failure cannot be recorded.
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Emit one trace write through the SHARED `WorkflowActionTraceWriter` (task 0868 R5/R7).
237
+ * `--action` is best-effort: an emission failure is recorded to the run log and reported
238
+ * on stdout as `{"ok":false}`, and the script exits 0 so the run still reaches its declared
239
+ * terminal state (R3/R12). `--close` is bookkeeping, so a missing run row or a persistence
240
+ * failure fails loudly with exit 1 (review findings #1/#4).
241
+ */
242
+ async function runTraceMode(input: TraceModeInput): Promise<number> {
243
+ const operation = input.close ? 'run.close' : 'action.finish';
244
+ const fail = (error: string): number => {
245
+ appendTraceFailureLine(
246
+ input.runId,
247
+ `trace-emission-failed operation=${operation} run=${input.runId}` +
248
+ `${input.node === '' ? '' : ` node=${input.node}`}${input.kind === '' ? '' : ` kind=${input.kind}`}: ${error}`,
249
+ );
250
+ process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error })}\n`);
251
+ // The action boundary is best-effort (exit 0); the run-row closure fails loudly (exit 1).
252
+ return input.close ? 1 : 0;
253
+ };
254
+
255
+ const { entry, repoRoot, chain } = resolveAppEntry(input.spurBin);
256
+ if (entry === null || repoRoot === null) {
257
+ return fail(
258
+ `no monorepo checkout of spur is reachable via ${chain} — the shared trace writer ` +
259
+ 'lives in packages/app (point SPUR_BIN at a repo checkout)',
260
+ );
261
+ }
262
+
263
+ const app = (await import(entry)) as {
264
+ openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
265
+ createWorkflowActionTraceWriter: (
266
+ db: unknown,
267
+ recordFailure?: (failure: unknown) => void,
268
+ ) => {
269
+ recordAction: (boundary: Record<string, unknown>) => Promise<{ ok: boolean; actionId?: string }>;
270
+ closeRun: (runId: string, status: string) => Promise<{ ok: boolean }>;
271
+ };
272
+ };
273
+
274
+ let projectDb: { adapter: unknown; close: () => void } | undefined;
275
+ try {
276
+ projectDb = await app.openInlineRunProjectDb(process.cwd());
277
+ const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
278
+ const detail = failure as { operation?: string; error?: string };
279
+ appendTraceFailureLine(
280
+ input.runId,
281
+ `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? 'unknown error'}`,
282
+ );
283
+ });
284
+ const result = input.close
285
+ ? await writer.closeRun(input.runId, input.status)
286
+ : await writer.recordAction({
287
+ runId: input.runId,
288
+ node: input.node,
289
+ kind: input.kind,
290
+ status: input.status,
291
+ ok: input.ok,
292
+ durationMs: input.durationMs,
293
+ });
294
+ process.stdout.write(`${JSON.stringify({ ...result, runId: input.runId })}\n`);
295
+ return 0;
296
+ } catch (error) {
297
+ if (input.close && (error as { name?: string }).name === 'RunRowNotFoundError') {
298
+ // The run row must exist before --close can mark it terminal (R6); a missing row
299
+ // is a loud correctness failure, not a best-effort emission failure (finding #4).
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ appendTraceFailureLine(input.runId, `trace-close-failed run=${input.runId}: ${message}`);
302
+ process.stdout.write(
303
+ `${JSON.stringify({ ok: false, runId: input.runId, error: message, code: 'RUN_NOT_FOUND' })}\n`,
304
+ );
305
+ return 1;
306
+ }
307
+ return fail(error instanceof Error ? error.message : String(error));
308
+ } finally {
309
+ projectDb?.close();
310
+ }
311
+ }
312
+
180
313
  async function main(): Promise<void> {
181
314
  let runId = '';
182
315
  let file = '';
183
316
  let fingerprint = false;
184
317
  let taskFile = '';
185
318
  let featureFile = '';
186
- let spurBin = process.env.SPUR_BIN ?? '';
319
+ let action = false;
320
+ let close = false;
321
+ let node = '';
322
+ let kind = '';
323
+ let status = '';
324
+ let okRaw = '';
325
+ let durationRaw = '';
326
+ let spurBin = getEnvVar('SPUR_BIN') ?? '';
187
327
  const argv = process.argv.slice(2);
188
328
  for (let i = 0; i < argv.length; i++) {
189
329
  if (argv[i] === '--run-id') runId = argv[++i] ?? '';
@@ -191,6 +331,13 @@ async function main(): Promise<void> {
191
331
  else if (argv[i] === '--fingerprint') fingerprint = true;
192
332
  else if (argv[i] === '--task-file') taskFile = argv[++i] ?? '';
193
333
  else if (argv[i] === '--feature-file') featureFile = argv[++i] ?? '';
334
+ else if (argv[i] === '--action') action = true;
335
+ else if (argv[i] === '--close') close = true;
336
+ else if (argv[i] === '--node') node = argv[++i] ?? '';
337
+ else if (argv[i] === '--kind') kind = argv[++i] ?? '';
338
+ else if (argv[i] === '--status') status = argv[++i] ?? '';
339
+ else if (argv[i] === '--ok') okRaw = argv[++i] ?? '';
340
+ else if (argv[i] === '--duration-ms') durationRaw = argv[++i] ?? '';
194
341
  else if (argv[i] === '--spur-bin') spurBin = argv[++i] ?? spurBin;
195
342
  }
196
343
 
@@ -200,6 +347,42 @@ async function main(): Promise<void> {
200
347
  if (runId !== '' || file !== '' || taskFile.trim() === '') usage();
201
348
  process.exit(await printFingerprint(taskFile, featureFile, spurBin));
202
349
  }
350
+
351
+ // ADR-117 emission modes (task 0868): the inline driver reports one completed action
352
+ // boundary, or closes its run row at the declared terminal state. Both share the run-id
353
+ // filename guard, the app-entry resolution chain and the best-effort failure contract.
354
+ if (action || close) {
355
+ if (action && close) usage();
356
+ if (runId.trim() === '' || status.trim() === '') usage();
357
+ if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
358
+ if (close) {
359
+ if (!CLOSE_STATUSES.has(status)) usage();
360
+ process.exit(
361
+ await runTraceMode({
362
+ runId,
363
+ close: true,
364
+ node: '',
365
+ kind: '',
366
+ status,
367
+ ok: true,
368
+ durationMs: 0,
369
+ spurBin,
370
+ }),
371
+ );
372
+ }
373
+ if (node.trim() === '' || kind.trim() === '') usage();
374
+ if (!ACTION_STATUSES.has(status)) usage();
375
+ // `--ok` and `--duration-ms` are required and exact for the action mode
376
+ // (review finding #2): a miscased `--ok True` or an omitted `--duration-ms`
377
+ // must be a loud usage error, never a silently-defaulted `ok=0` /
378
+ // `duration_ms=0` row.
379
+ if (okRaw !== 'true' && okRaw !== 'false') usage();
380
+ const ok = okRaw === 'true';
381
+ const durationMs = Number(durationRaw);
382
+ if (durationRaw.trim() === '' || !Number.isFinite(durationMs) || durationMs < 0) usage();
383
+ process.exit(await runTraceMode({ runId, close: false, node, kind, status, ok, durationMs, spurBin }));
384
+ }
385
+
203
386
  if (runId.trim() === '' || file.trim() === '') usage();
204
387
  if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
205
388
 
@@ -5,8 +5,15 @@
5
5
  import { spawnSync } from "child_process";
6
6
  import { existsSync, readFileSync, writeFileSync } from "fs";
7
7
  import { join } from "path";
8
+
9
+ // ../ts-libs/packages/utils/dist/env.js
10
+ function getEnvVars() {
11
+ return process.env;
12
+ }
13
+
14
+ // plugins/sp/scripts/pr-reviewing.ts
8
15
  var spawnRunner = (cmd) => {
9
- const proc = spawnSync(cmd[0] ?? "", [...cmd.slice(1)], { encoding: "utf8", env: process.env });
16
+ const proc = spawnSync(cmd[0] ?? "", [...cmd.slice(1)], { encoding: "utf8", env: getEnvVars() });
10
17
  return {
11
18
  code: proc.status ?? 1,
12
19
  stdout: proc.stdout ?? "",
@@ -22,6 +22,7 @@
22
22
  import { spawnSync } from 'node:child_process';
23
23
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
24
24
  import { join } from 'node:path';
25
+ import { getEnvVars } from '@gobing-ai/ts-utils';
25
26
 
26
27
  // ─── Types ──────────────────────────────────────────────────────────────────
27
28
 
@@ -118,7 +119,7 @@ export type CommandRunner = (cmd: readonly string[]) => CmdResult;
118
119
  /** Real execution: resolve the binary from PATH and capture its output. The default runner. */
119
120
  export const spawnRunner: CommandRunner = (cmd) => {
120
121
  // Pass env explicitly so both Node and Bun resolve git/gh against the caller's PATH.
121
- const proc = spawnSync(cmd[0] ?? '', [...cmd.slice(1)], { encoding: 'utf8', env: process.env });
122
+ const proc = spawnSync(cmd[0] ?? '', [...cmd.slice(1)], { encoding: 'utf8', env: getEnvVars() });
122
123
  return {
123
124
  code: proc.status ?? 1,
124
125
  stdout: proc.stdout ?? '',
@@ -17,6 +17,13 @@ import {
17
17
  } from "fs";
18
18
  import { tmpdir } from "os";
19
19
  import { join } from "path";
20
+
21
+ // ../ts-libs/packages/utils/dist/env.js
22
+ function getEnvVars() {
23
+ return process.env;
24
+ }
25
+
26
+ // plugins/sp/scripts/quality-gate.ts
20
27
  var MAX_GATE_ATTEMPTS = 5;
21
28
  var MAX_FINDINGS = 20;
22
29
  var RETRY_DELAY_MS_DEFAULT = 1e4;
@@ -178,7 +185,7 @@ function runQualityGate(mode, env, options = {}) {
178
185
  return { status, attempts: gateAttempt, logFile, findingsFile, statusFile, attemptFile };
179
186
  }
180
187
  var QUALITY_GATE_USAGE = "usage: quality-gate.ts <run|recheck> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest)";
181
- function main(argv, env = process.env) {
188
+ function main(argv, env = getEnvVars()) {
182
189
  const mode = argv[0];
183
190
  if (mode !== "run" && mode !== "recheck") {
184
191
  process.stderr.write(`${QUALITY_GATE_USAGE}
@@ -41,6 +41,7 @@ import {
41
41
  } from 'node:fs';
42
42
  import { tmpdir } from 'node:os';
43
43
  import { join } from 'node:path';
44
+ import { getEnvVars } from '@gobing-ai/ts-utils';
44
45
 
45
46
  export const MAX_GATE_ATTEMPTS = 5;
46
47
  export const MAX_FINDINGS = 20;
@@ -268,7 +269,7 @@ export function runQualityGate(
268
269
  export const QUALITY_GATE_USAGE =
269
270
  'usage: quality-gate.ts <run|recheck> (env: wbs, qualityGateCmd, gateProbeCmd, proofDigest)';
270
271
 
271
- export function main(argv: string[], env: QualityGateEnv = process.env): number {
272
+ export function main(argv: string[], env: QualityGateEnv = getEnvVars()): number {
272
273
  const mode = argv[0];
273
274
  if (mode !== 'run' && mode !== 'recheck') {
274
275
  process.stderr.write(`${QUALITY_GATE_USAGE}\n`);
@@ -179,9 +179,6 @@ export function parseInvocation(spanRaw: string): Parsed | null {
179
179
  }
180
180
  }
181
181
  if (nouns.length === 0 && verbs.length === 0 && flags.length === 0) return null;
182
- if (process.env.DEBUG_SPANS === '1') {
183
- process.stderr.write(`SPAN ${JSON.stringify({ spanRaw, nouns, verbs, flags })}\n`);
184
- }
185
182
  return { nouns, verbs, flags };
186
183
  }
187
184
 
@@ -638,7 +635,7 @@ const JSON_PROBES: string[][] = [
638
635
  ['agent', 'list', '--json'],
639
636
  ['agent', 'doctor', 'pi', '--json'],
640
637
  ['workflow', 'list', '--json'],
641
- ['workflow', 'validate', 'basic.yaml', '--json'],
638
+ ['workflow', 'validate', 'task-pipeline.yaml', '--json'],
642
639
  ['status', '--json'],
643
640
  ['projects', 'list', '--json'],
644
641
  ];
@@ -37,6 +37,7 @@ import { execFileSync } from 'node:child_process';
37
37
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
38
38
  import { join } from 'node:path';
39
39
  import { fileURLToPath } from 'node:url';
40
+ import { getEnvVar } from '@gobing-ai/ts-utils';
40
41
 
41
42
  /** Exact task-content declaration that activates the live-evidence gate (0726 R2). */
42
43
  const DECLARATION_PREFIX = 'evidence-channel:';
@@ -58,7 +59,7 @@ function usage(): never {
58
59
  }
59
60
 
60
61
  function defaultSpurBin(): string {
61
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
62
+ if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
62
63
  const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
63
64
  if (existsSync(local)) return `bun ${local}`;
64
65
  return 'spur';
@@ -19,13 +19,14 @@
19
19
  * bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>]
20
20
  * [--max-reqs <n>] [--max-plan-items <n>]
21
21
  *
22
- * Env: SPUR_BIN, MAX_IMPLEMENT_REQS, MAX_IMPLEMENT_PLAN_ITEMS
22
+ * Env: SPUR_BIN (flags own the size ceilings — env overrides removed, task 0902)
23
23
  */
24
24
 
25
25
  import { execFileSync } from 'node:child_process';
26
26
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
27
27
  import { join } from 'node:path';
28
28
  import { fileURLToPath } from 'node:url';
29
+ import { getEnvVar } from '@gobing-ai/ts-utils';
29
30
 
30
31
  // ─── Counting (sync with packages/app/src/services/task-size-precheck.ts) ────
31
32
 
@@ -88,7 +89,7 @@ function usage(): never {
88
89
  * keeps ad-hoc invocations from silently hitting a stale PATH install.
89
90
  */
90
91
  function defaultSpurBin(): string {
91
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
92
+ if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
92
93
  // scripts/ -> plugins/sp/ -> <repo>/apps/cli/src/index.ts (fileURLToPath — raw pathname breaks
93
94
  // on %-encoded paths, e.g. spaces in the checkout directory)
94
95
  const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
@@ -107,8 +108,9 @@ function parseArgs(argv: string[]): {
107
108
  // Doubled deterministic ceiling (0723 operator decision): 10 R-items / 16
108
109
  // Plan items — keep in sync with DEFAULT_TASK_SIZE_LIMITS in
109
110
  // packages/app/src/services/task-size-precheck.ts (asserted by test).
110
- let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 10;
111
- let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 16;
111
+ // Overridable per invocation via --max-reqs / --max-plan-items (no env fallback).
112
+ let maxReqs = 10;
113
+ let maxPlanItems = 16;
112
114
 
113
115
  let i = 0;
114
116
  while (i < argv.length) {
@@ -32,6 +32,7 @@
32
32
  import { execFileSync } from 'node:child_process';
33
33
  import { existsSync, readFileSync } from 'node:fs';
34
34
  import { fileURLToPath } from 'node:url';
35
+ import { getEnvVar } from '@gobing-ai/ts-utils';
35
36
 
36
37
  // ─── CLI (same spur-bin chain as task-evidence-precheck.ts) ─────────────────
37
38
 
@@ -41,7 +42,7 @@ function usage(): never {
41
42
  }
42
43
 
43
44
  function defaultSpurBin(): string {
44
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
45
+ if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
45
46
  const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
46
47
  if (existsSync(local)) return `bun ${local}`;
47
48
  return 'spur';
@@ -5,6 +5,14 @@
5
5
  import { spawnSync } from "child_process";
6
6
  import { existsSync } from "fs";
7
7
  import { fileURLToPath } from "url";
8
+
9
+ // ../ts-libs/packages/utils/dist/env.js
10
+ function getEnvVar(name, fallback) {
11
+ const raw = process.env[name];
12
+ return raw === undefined ? fallback : raw;
13
+ }
14
+
15
+ // plugins/sp/scripts/workflow-step-profile.ts
8
16
  var DEFAULT_LAST = 20;
9
17
  var DEFAULT_WINDOW_SEC = 300;
10
18
  function nearestRankP50(values) {
@@ -168,8 +176,8 @@ function formatStepProfileHuman(profile) {
168
176
  `;
169
177
  }
170
178
  function defaultSpurBin() {
171
- if (process.env.SPUR_BIN)
172
- return process.env.SPUR_BIN;
179
+ if (getEnvVar("SPUR_BIN"))
180
+ return getEnvVar("SPUR_BIN");
173
181
  const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
174
182
  if (existsSync(local))
175
183
  return `bun ${local}`;
@@ -25,6 +25,7 @@
25
25
  import { spawnSync } from 'node:child_process';
26
26
  import { existsSync } from 'node:fs';
27
27
  import { fileURLToPath } from 'node:url';
28
+ import { getEnvVar } from '@gobing-ai/ts-utils';
28
29
 
29
30
  // ── Input shapes (a subset of the live `spur workflow trace --json` payload) ─────────────
30
31
 
@@ -321,7 +322,7 @@ export interface StepProfileCliArgs {
321
322
  * --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
322
323
  */
323
324
  export function defaultSpurBin(): string {
324
- if (process.env.SPUR_BIN) return process.env.SPUR_BIN;
325
+ if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
325
326
  const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
326
327
  if (existsSync(local)) return `bun ${local}`;
327
328
  return 'spur';