@gobing-ai/spur 0.3.71 → 0.3.73

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 (81) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +33 -23
  3. package/config/corpus-baseline.json +168 -0
  4. package/config/pipeline-budgets.json +7 -3
  5. package/config/plugin-scripts.json +4 -0
  6. package/config/proportional-route-table.ts +155 -0
  7. package/config/rules/structure/protected-files.yaml +3 -0
  8. package/config/task-pipeline-proportional-migration-plan.md +79 -0
  9. package/config/workflow-composition-baseline.json +124 -85
  10. package/config/workflows/docs-pipeline.yaml +11 -1
  11. package/config/workflows/feature-dev.yaml +27 -20
  12. package/config/workflows/task-lifecycle.yaml +27 -15
  13. package/config/workflows/task-pipeline.yaml +72 -11
  14. package/config/workflows/wrapup-pipeline.yaml +100 -42
  15. package/package.json +9 -9
  16. package/plugins/sp/plugin.json +1 -1
  17. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +119 -0
  18. package/plugins/sp/scripts/daily-summary/daily-summary.ts +202 -0
  19. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  20. package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
  21. package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
  22. package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
  23. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
  24. package/schemas/spur-config.schema.json +35 -0
  25. package/schemas/state-machine-workflow.schema.json +3 -1
  26. package/schemas/transition-flow-workflow.schema.json +3 -1
  27. package/spur.js +12391 -8659
  28. package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
  29. package/web/_astro/BoardApp.E12MFjOS.js +1 -0
  30. package/web/_astro/{TaskDetail.BvkKvo57.js → TaskDetail.CgUreSP2.js} +1 -1
  31. package/web/_astro/{arc.DuEIzPMi.js → arc.BySSh34M.js} +1 -1
  32. package/web/_astro/{architectureDiagram-3BPJPVTR.D0dUxCjA.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
  33. package/web/_astro/{blockDiagram-GPEHLZMM.Cv60iOpM.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
  34. package/web/_astro/{c4Diagram-AAUBKEIU.Ddx7WGhb.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
  35. package/web/_astro/channel.5cYKr5cs.js +1 -0
  36. package/web/_astro/{chunk-2J33WTMH.BGKzU_1t.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
  37. package/web/_astro/{chunk-4BX2VUAB.FlApjIIH.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
  38. package/web/_astro/{chunk-55IACEB6.CDfmvDeW.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
  39. package/web/_astro/{chunk-727SXJPM.T9bc-xir.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
  40. package/web/_astro/{chunk-AQP2D5EJ.BiTc-MQo.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
  41. package/web/_astro/{chunk-FMBD7UC4.he4KrHni.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
  42. package/web/_astro/{chunk-ND2GUHAM.D05XuUuJ.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
  43. package/web/_astro/{chunk-QZHKN3VN.CA2NThIE.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
  44. package/web/_astro/{classDiagram-4FO5ZUOK.Couj-zYZ.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
  45. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Couj-zYZ.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
  46. package/web/_astro/{cose-bilkent-S5V4N54A.B8YYW7NG.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
  47. package/web/_astro/{cynefin-OW5HDTMX.BExFdiin.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
  48. package/web/_astro/{dagre-BM42HDAG.BybKbz3q.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
  49. package/web/_astro/{diagram-2AECGRRQ.UyRTSl9n.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
  50. package/web/_astro/{diagram-5GNKFQAL.BrxCucBf.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
  51. package/web/_astro/{diagram-KO2AKTUF.CdE5oy5J.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
  52. package/web/_astro/{diagram-LMA3HP47.BJLgdosK.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
  53. package/web/_astro/{diagram-OG6HWLK6.CUynieTU.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
  54. package/web/_astro/{erDiagram-TEJ5UH35.C0vS6DJv.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
  55. package/web/_astro/{flowDiagram-I6XJVG4X.T3QLi_en.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
  56. package/web/_astro/{ganttDiagram-6RSMTGT7.BNsk3w9Z.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
  57. package/web/_astro/{gitGraphDiagram-PVQCEYII.Mwe2I4V6.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
  58. package/web/_astro/index.B5MTfe7k.css +1 -0
  59. package/web/_astro/{infoDiagram-5YYISTIA.BlcjLmtc.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
  60. package/web/_astro/{ishikawaDiagram-YF4QCWOH.js8qeS0h.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
  61. package/web/_astro/{journeyDiagram-JHISSGLW.CM6UK0a4.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
  62. package/web/_astro/{kanban-definition-UN3LZRKU.a6ihOzMd.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
  63. package/web/_astro/{linear.CsIB2jFu.js → linear.D7uqzENp.js} +1 -1
  64. package/web/_astro/{mermaid.core.Br2Fo22q.js → mermaid.core.CxrNppBD.js} +4 -4
  65. package/web/_astro/{mindmap-definition-RKZ34NQL.29inC1Mk.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
  66. package/web/_astro/{pieDiagram-4H26LBE5.C9CxG_Kf.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
  67. package/web/_astro/{quadrantDiagram-W4KKPZXB.6qo9MOJM.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
  68. package/web/_astro/{requirementDiagram-4Y6WPE33.DN07zrP5.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
  69. package/web/_astro/{sankeyDiagram-5OEKKPKP.BSw5o173.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
  70. package/web/_astro/{sequenceDiagram-3UESZ5HK.LJPzySKw.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
  71. package/web/_astro/{stateDiagram-AJRCARHV.ClNjEiKV.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
  72. package/web/_astro/{stateDiagram-v2-BHNVJYJU.c6Z-_WfX.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
  73. package/web/_astro/{timeline-definition-PNZ67QCA.CIMR-87j.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
  74. package/web/_astro/{vennDiagram-CIIHVFJN.BnRSRI9I.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
  75. package/web/_astro/{wardleyDiagram-YWT4CUSO.uN08C3gv.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
  76. package/web/_astro/{xychartDiagram-2RQKCTM6.BjbEvfuq.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
  77. package/web/index.html +2 -2
  78. package/web/_astro/BoardApp.BnjsI80-.js +0 -178
  79. package/web/_astro/BoardApp.SJcrHBZp.js +0 -1
  80. package/web/_astro/channel.tWfETQvX.js +0 -1
  81. package/web/_astro/index.9npdrEIr.css +0 -1
@@ -59,6 +59,30 @@ export interface UserAnnotations {
59
59
  pending: string;
60
60
  }
61
61
 
62
+ export interface HistoryLoopFinding {
63
+ toolName: string;
64
+ argsDigest: string;
65
+ repeats: number;
66
+ sessionId: string;
67
+ fromSeq?: number;
68
+ toSeq?: number;
69
+ wastedTokens: number;
70
+ }
71
+
72
+ export interface HistoryHealthSummary {
73
+ toolCalls: number;
74
+ toolErrors: number;
75
+ errorRatePct: number;
76
+ loops: HistoryLoopFinding[];
77
+ redundantCalls: number;
78
+ wastedTokens: number;
79
+ remediationProposals: Array<{
80
+ key: string;
81
+ title: string;
82
+ command: string;
83
+ }>;
84
+ }
85
+
62
86
  export interface DailySummary {
63
87
  date: string;
64
88
  platforms: string[];
@@ -77,6 +101,8 @@ export interface DailySummary {
77
101
  };
78
102
  commits: GitCommit[];
79
103
  annotations: UserAnnotations;
104
+ /** Health metrics and loop findings from Spur history analytics. */
105
+ historyHealth?: HistoryHealthSummary;
80
106
  /** Path to the newest history report artifact (R7), resolved from the
81
107
  * `.spur/reports/history/latest.json` pointer. Omitted when no report exists. */
82
108
  historyReportPath?: string;
@@ -257,6 +283,120 @@ export async function getCcusageData(date: string): Promise<CcusageData | null>
257
283
  }
258
284
  }
259
285
 
286
+ // ─── Spur History Health Integration ──────────────────────────────────────────
287
+
288
+ export async function getSpurHistoryHealth(
289
+ date: string,
290
+ dbPath = '.spur/spur.db',
291
+ ): Promise<HistoryHealthSummary | null> {
292
+ try {
293
+ const resolvedPath = resolve(process.cwd(), dbPath);
294
+ if (!existsSync(resolvedPath)) {
295
+ return null;
296
+ }
297
+
298
+ const { Database } = await import('bun:sqlite');
299
+ const db = new Database(resolvedPath, { readonly: true });
300
+
301
+ try {
302
+ // 1. Query execution loop findings
303
+ const loopTable = db
304
+ .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
305
+ .get('history_board_loop_findings');
306
+
307
+ let loops: HistoryLoopFinding[] = [];
308
+ if (loopTable) {
309
+ const rows = db
310
+ .query<
311
+ {
312
+ tool_name: string;
313
+ args_digest: string;
314
+ repeats: number;
315
+ session_id: string;
316
+ first_seq: number;
317
+ last_seq: number;
318
+ started_at: string | null;
319
+ },
320
+ [string]
321
+ >(
322
+ `SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
323
+ FROM history_board_loop_findings
324
+ WHERE started_at IS NULL OR started_at LIKE ?
325
+ ORDER BY repeats DESC
326
+ LIMIT 20`,
327
+ )
328
+ .all(`${date}%`);
329
+
330
+ loops = rows.map((r) => ({
331
+ toolName: r.tool_name,
332
+ argsDigest: r.args_digest || 'repeated execution',
333
+ repeats: r.repeats,
334
+ sessionId: r.session_id,
335
+ fromSeq: r.first_seq,
336
+ toSeq: r.last_seq,
337
+ wastedTokens: r.repeats * 250,
338
+ }));
339
+ }
340
+
341
+ // 2. Query tool calls and errors
342
+ let toolCalls = 0;
343
+ let toolErrors = 0;
344
+ const toolTable = db
345
+ .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
346
+ .get('history_board_tool_5m');
347
+
348
+ if (toolTable) {
349
+ const stats = db
350
+ .query<{ calls: number | null; errors: number | null }, [string]>(
351
+ `SELECT SUM(calls) AS calls, SUM(errors) AS errors
352
+ FROM history_board_tool_5m
353
+ WHERE bucket_start LIKE ?`,
354
+ )
355
+ .get(`${date}%`);
356
+
357
+ toolCalls = stats?.calls ?? 0;
358
+ toolErrors = stats?.errors ?? 0;
359
+ }
360
+
361
+ const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
362
+ const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
363
+ const errorRatePct = toolCalls > 0 ? (toolErrors / toolCalls) * 100 : 0;
364
+
365
+ // 3. Generate auto-healing remediation proposals
366
+ const remediationProposals: Array<{ key: string; title: string; command: string }> = [];
367
+
368
+ for (const lp of loops.slice(0, 5)) {
369
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
370
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
371
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
372
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
373
+ remediationProposals.push({ key, title, command });
374
+ }
375
+
376
+ if (toolCalls > 0 && errorRatePct > 10) {
377
+ const key = 'reliability:tooling:high-error-rate';
378
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
379
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
380
+ remediationProposals.push({ key, title, command });
381
+ }
382
+
383
+ return {
384
+ toolCalls,
385
+ toolErrors,
386
+ errorRatePct,
387
+ loops,
388
+ redundantCalls,
389
+ wastedTokens,
390
+ remediationProposals,
391
+ };
392
+ } finally {
393
+ db.close();
394
+ }
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+
260
400
  // ─── Git Integration ─────────────────────────────────────────────────────────
261
401
 
262
402
  export async function getGitCommits(date: string): Promise<GitCommit[]> {
@@ -477,6 +617,61 @@ export function generateMarkdown(summary: DailySummary): string {
477
617
  lines.push('');
478
618
  }
479
619
 
620
+ // Execution Loops & Health Findings
621
+ if (summary.historyHealth) {
622
+ const hh = summary.historyHealth;
623
+ lines.push('## Execution Loops & Health Findings');
624
+ lines.push('');
625
+
626
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
627
+ lines.push('- **Status:** ✅ Clean — No execution loops or tool calls recorded for this date.');
628
+ lines.push('');
629
+ } else {
630
+ lines.push('| Metric | Value |');
631
+ lines.push('|--------|-------|');
632
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
633
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
634
+ lines.push(`| Detected Loops (Repeats ≥ 3) | ${hh.loops.length} |`);
635
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
636
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
637
+ lines.push('');
638
+
639
+ if (hh.loops.length > 0) {
640
+ lines.push('### Detected Execution Loops');
641
+ lines.push('');
642
+ for (const lp of hh.loops.slice(0, 10)) {
643
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} → #${lp.toSeq})` : '';
644
+ const argsHint =
645
+ lp.argsDigest === '74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b'
646
+ ? 'empty/unrecorded arguments'
647
+ : lp.argsDigest.length > 28
648
+ ? `${lp.argsDigest.slice(0, 24)}...`
649
+ : lp.argsDigest;
650
+ lines.push(
651
+ `- \`${lp.toolName || 'unknown'}\` × **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`,
652
+ );
653
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
654
+ }
655
+ lines.push('');
656
+ }
657
+
658
+ if (hh.remediationProposals.length > 0) {
659
+ lines.push('### Auto-Healing Remediation Proposals');
660
+ lines.push('');
661
+ lines.push('To remediate root causes and prevent recurring token waste, execute:');
662
+ lines.push('');
663
+ lines.push('```bash');
664
+ for (const prop of hh.remediationProposals) {
665
+ lines.push(`# ${prop.title} [${prop.key}]`);
666
+ lines.push(prop.command);
667
+ lines.push('');
668
+ }
669
+ lines.push('```');
670
+ lines.push('');
671
+ }
672
+ }
673
+ }
674
+
480
675
  // History report path (R7 — surfaces the newest nightly-run artifact).
481
676
  if (summary.historyReportPath) {
482
677
  lines.push('## History Report');
@@ -598,6 +793,13 @@ export async function buildDailySummary(options: CliOptions): Promise<DailySumma
598
793
  result.gitActivity = gitActivity;
599
794
  }
600
795
 
796
+ // Query Spur history health (loops, tool errors, and auto-healing proposals)
797
+ const historyHealth = await getSpurHistoryHealth(options.date);
798
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
799
+ result.historyHealth = historyHealth;
800
+ platforms.push('Spur History');
801
+ }
802
+
601
803
  return result;
602
804
  }
603
805
 
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * inline-pipeline-parity-check — two-sided gate between the inline pipeline
4
+ * driver's documented action/guard set and the resolved action/guard sets in
5
+ * `.spur/workflows/task-pipeline.yaml` and `.spur/workflows/idea-pipeline.yaml`
6
+ * (task 0755 R2/R3).
7
+ *
8
+ * The driver reference at `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`
9
+ * documents the set of action and guard kinds it implements. The two runtime
10
+ * pipelines are the only consumers the driver needs to keep in step with. The
11
+ * check is a symmetric set diff: an element present in one and absent in the
12
+ * other fails the check and names the element.
13
+ *
14
+ * The set is defined in {@link DOCUMENTED} below; the driver's markdown list is
15
+ * the human mirror. Update both when the driver adds or drops a kind.
16
+ *
17
+ * Usage:
18
+ * bun plugins/sp/scripts/inline-pipeline-parity-check.ts
19
+ * [--root <path>] default: repo root
20
+ *
21
+ * Exit code: 0 when the sets agree; 1 on any divergence. Violations are printed
22
+ * to stderr; a summary to stdout.
23
+ */
24
+
25
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
26
+ import { join, resolve } from 'node:path';
27
+ import { parse as parseYaml } from 'yaml';
28
+
29
+ /** Documented action and guard set. Must stay in lockstep with the
30
+ * "Supported action and guard set (0755 R2 parity contract)" section in
31
+ * `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`. The
32
+ * driver supports a kind if ANY workflow in `.spur/workflows/*.yaml` uses
33
+ * it (the driver applies to any selected pipeline per its reference doc). */
34
+ const DOCUMENTED = {
35
+ actions: new Set([
36
+ 'shell',
37
+ 'note',
38
+ 'doctor.probe',
39
+ 'file.read.into-var',
40
+ 'hitl.confirm',
41
+ 'agent.run',
42
+ 'proof.fingerprint',
43
+ 'run.artifact',
44
+ 'command.gate',
45
+ ]),
46
+ guards: new Set(['always', 'shell']),
47
+ } as const;
48
+
49
+ /** Directory of workflow definitions the driver is responsible for. */
50
+ const WORKFLOW_DIR = join('config', 'workflows');
51
+
52
+ /** Walk a state list and yield every `kind:` value found in `onEnter` action
53
+ * lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
54
+ function collectActionKinds(states: unknown): Set<string> {
55
+ const out = new Set<string>();
56
+ if (!Array.isArray(states)) return out;
57
+ for (const state of states) {
58
+ if (typeof state !== 'object' || state === null) continue;
59
+ const onEnter = (state as { onEnter?: unknown }).onEnter;
60
+ if (!Array.isArray(onEnter)) continue;
61
+ for (const action of onEnter) {
62
+ if (typeof action !== 'object' || action === null) continue;
63
+ const kind = (action as { kind?: unknown }).kind;
64
+ if (typeof kind === 'string') out.add(kind);
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Walk a transition list and yield every `guard.kind` value. */
71
+ function collectGuardKinds(transitions: unknown): Set<string> {
72
+ const out = new Set<string>();
73
+ if (!Array.isArray(transitions)) return out;
74
+ for (const transition of transitions) {
75
+ if (typeof transition !== 'object' || transition === null) continue;
76
+ const guard = (transition as { guard?: { kind?: unknown } }).guard;
77
+ const kind = guard?.kind;
78
+ if (typeof kind === 'string') out.add(kind);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /** Symmetric set diff. Returns elements in `a` but not in `b`, and vice versa. */
84
+ function diff<T>(a: Set<T>, b: Set<T>): { onlyInA: T[]; onlyInB: T[] } {
85
+ const onlyInA: T[] = [];
86
+ const onlyInB: T[] = [];
87
+ for (const x of a) if (!b.has(x)) onlyInA.push(x);
88
+ for (const x of b) if (!a.has(x)) onlyInB.push(x);
89
+ return { onlyInA, onlyInB };
90
+ }
91
+
92
+ function parseArgs(argv: string[]): { root: string } {
93
+ let root = resolve('.');
94
+ for (let i = 0; i < argv.length; i += 1) {
95
+ const arg = argv[i];
96
+ if (arg === '--root' && i + 1 < argv.length) {
97
+ root = resolve(argv[++i] ?? '.');
98
+ } else if (arg === '--help' || arg === '-h') {
99
+ process.stdout.write('Usage: bun inline-pipeline-parity-check.ts [--root <path>]\n');
100
+ process.exit(0);
101
+ }
102
+ }
103
+ return { root };
104
+ }
105
+
106
+ function listWorkflowFiles(dir: string): string[] {
107
+ let entries: string[];
108
+ try {
109
+ entries = readdirSync(dir);
110
+ } catch {
111
+ return [];
112
+ }
113
+ return entries.filter((e) => e.endsWith('.yaml')).map((e) => join(dir, e));
114
+ }
115
+
116
+ async function main(): Promise<number> {
117
+ const { root } = parseArgs(process.argv.slice(2));
118
+ const errors: string[] = [];
119
+
120
+ const workflowDir = join(root, WORKFLOW_DIR);
121
+ if (!statSync(workflowDir, { throwIfNoEntry: false })) {
122
+ process.stderr.write(`inline-pipeline-parity-check: workflow directory not found: ${workflowDir}\n`);
123
+ return 1;
124
+ }
125
+
126
+ const files = listWorkflowFiles(workflowDir);
127
+ if (files.length === 0) {
128
+ process.stderr.write(`inline-pipeline-parity-check: no .yaml workflows found in ${workflowDir}\n`);
129
+ return 1;
130
+ }
131
+
132
+ const unionActions = new Set<string>();
133
+ const unionGuards = new Set<string>();
134
+ const perFileKinds: { path: string; actions: Set<string>; guards: Set<string> }[] = [];
135
+
136
+ for (const path of files) {
137
+ let parsed: unknown;
138
+ try {
139
+ parsed = parseYaml(readFileSync(path, 'utf8'));
140
+ } catch (err) {
141
+ errors.push(`${path}: failed to parse (${err instanceof Error ? err.message : String(err)})`);
142
+ continue;
143
+ }
144
+ if (typeof parsed !== 'object' || parsed === null) {
145
+ continue;
146
+ }
147
+ const def = parsed as { states?: unknown; transitions?: unknown };
148
+ const actions = collectActionKinds(def.states);
149
+ const guards = collectGuardKinds(def.transitions);
150
+ perFileKinds.push({ path, actions, guards });
151
+ for (const a of actions) unionActions.add(a);
152
+ for (const g of guards) unionGuards.add(g);
153
+ }
154
+
155
+ const actionDiff = diff(unionActions, DOCUMENTED.actions);
156
+ const guardDiff = diff(unionGuards, DOCUMENTED.guards);
157
+
158
+ for (const x of actionDiff.onlyInA) {
159
+ const usedIn = perFileKinds.filter((f) => f.actions.has(x)).map((f) => f.path);
160
+ errors.push(`action kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
161
+ }
162
+ for (const x of actionDiff.onlyInB) {
163
+ errors.push(`action kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
164
+ }
165
+ for (const x of guardDiff.onlyInA) {
166
+ const usedIn = perFileKinds.filter((f) => f.guards.has(x)).map((f) => f.path);
167
+ errors.push(`guard kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
168
+ }
169
+ for (const x of guardDiff.onlyInB) {
170
+ errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
171
+ }
172
+
173
+ if (errors.length > 0) {
174
+ process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
175
+ for (const e of errors) process.stderr.write(` - ${e}\n`);
176
+ return 1;
177
+ }
178
+
179
+ process.stdout.write(
180
+ `inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
181
+ );
182
+ return 0;
183
+ }
184
+
185
+ process.exit(await main());
@@ -145,7 +145,7 @@ function main(): void {
145
145
 
146
146
  const dbPath = join(process.cwd(), '.spur', 'spur.db');
147
147
  if (!existsSync(dbPath)) {
148
- fail(wbs, [`spur database not found at ${dbPath} — run a real history import first`]);
148
+ fail(wbs, [`local spur database not found at ${dbPath} — run a real history import first`]);
149
149
  }
150
150
 
151
151
  let count: number;
@@ -269,6 +269,10 @@ function extractAcIdentities(taskContent: string, featureContent: string | null)
269
269
  const leading = label.split(/\s+/)[0] ?? '';
270
270
  if (leading && leading !== label) identities.add(leading);
271
271
  }
272
+ for (const m of section.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
273
+ const title = (m[1] ?? '').trim();
274
+ if (title) identities.add(title);
275
+ }
272
276
  if (featureContent !== null) {
273
277
  for (const m of featureContent.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
274
278
  const title = (m[1] ?? '').trim();
@@ -229,3 +229,26 @@ verifying shape, or stand in a `note` action until the path is proven, then swap
229
229
  `agent.run` input referencing a command rather than a raw prompt, guards a single predicate
230
230
  ([workflow-fit-and-tuning.md](workflow-fit-and-tuning.md#3-node-simplicity-budget)).
231
231
  - [ ] Validates clean AND dry-run reaches the expected terminal state.
232
+
233
+ ## Optional version literal (task 0756)
234
+
235
+ Both dialects accept an optional root `version` field. The literal is **behavior-neutral** — it
236
+ exists as an identity tag, not a routing key. The contract:
237
+
238
+ - **Absent** → reported as `unversioned`. The default for all 11 shipped definitions.
239
+ - **Present, non-empty string** → reported as `explicit(<literal>)`. The literal is wrapped in
240
+ parentheses verbatim — no parsing, no ordering, no compatibility check.
241
+ - **Present, empty string (`version: ""`)** → **rejected** with a diagnostic naming the empty
242
+ value. The rejection lives in the resolve/preflight seam
243
+ (`packages/app/src/workflow/workflow-resolver.ts`), not in the dialect JSON schemas: those carry
244
+ `minLength: 1` for editors and Ajv consumers, but the load path validates against the engine's
245
+ Zod schema, which has no minimum. Move the check upstream once
246
+ `@gobing-ai/ts-dual-workflow-engine` ships `z.string().min(1)` on the root version.
247
+
248
+ The literal folds into the definition digest (`packages/app/src/workflow/composition-baseline.ts`),
249
+ so a version-only edit changes the digest with zero behavior change. `show` and `trace` do **not**
250
+ surface the literal by default — the digest stays the rendered run identity (D8 decision D5).
251
+
252
+ **No registry, no semver parser, no compatibility engine.** A future-major requirement needs
253
+ objective evidence: a consumer that branches on version, or a real drift incident the digest
254
+ diagnostic could not disambiguate. Neither exists today.
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: inline-pipeline-driver
3
3
  description: "Interactive host-session interpreter for Spur state-machine pipelines: execute the existing FSM without a workflow agent subprocess while preserving actions, guards, artifacts, and provenance."
4
+ owner: spur-dev-maintainers
5
+ retirement-criterion: "The per-task interpreter retires once the engine covers per-task execution for /sp:dev-runall with real terminal runs and the parity check (plugins/sp/scripts/inline-pipeline-parity-check.ts) is green (D8 decision D7). Batch orchestration wrapper may remain."
4
6
  see_also:
5
7
  - spur-dev
6
8
  - execution-workflow
@@ -9,6 +11,24 @@ see_also:
9
11
 
10
12
  # Inline Pipeline Driver
11
13
 
14
+ **Owner:** `spur-dev-maintainers` (per task 0755 R1). Reach the named owner via the frontmatter; no need to read the originating task.
15
+
16
+ **Retirement criterion (0755 R5, D8 decision D7):** the per-task interpreter retires once the engine covers per-task execution for `/sp:dev-runall` with real terminal runs **and** the parity check (this doc's documented action/guard set ≡ the resolved action/guard set of every `.spur/workflows/*.yaml`) is green. Recording the criterion is part of this task; acting on it is not — that is a separate A3-gate decision.
17
+
18
+ ## Supported action and guard set (0755 R2 parity contract)
19
+
20
+ The action and guard kinds this driver implements. The parity check
21
+ (`plugins/sp/scripts/inline-pipeline-parity-check.ts`) compares this set against
22
+ the resolved actions and guards of every `.spur/workflows/*.yaml`; any element present
23
+ in one and absent in the other fails the check. Add a new kind here when the driver
24
+ implements it; remove the entry when the corresponding kind is dropped from the YAML.
25
+
26
+ **Actions:** `shell` · `note` · `doctor.probe` · `file.read.into-var` · `hitl.confirm` · `agent.run` · `proof.fingerprint` · `run.artifact` · `command.gate`
27
+
28
+ **Guards (transitions):** `always` · `shell`
29
+
30
+ ## What this driver is
31
+
12
32
  This driver is the interactive control-inversion path granted by ADR-047. It applies when an
13
33
  interactive `/sp:dev-run --mode full`, sequential `/sp:dev-runall`, `/sp:dev-idea`, or
14
34
  `/sp:dev-plan` invocation omits `--agent` or passes `--agent inline`. A named executor,
@@ -92,6 +92,41 @@
92
92
  "enabled": {
93
93
  "type": "boolean",
94
94
  "description": "Enable the scheduled-task runner. OFF by default for CLI (run-once)."
95
+ },
96
+ "jobs": {
97
+ "type": "array",
98
+ "description": "Declarative scheduled commands (task 0734). Each entry registers one scheduler tick that enqueues a `scheduler.custom` queue job running `command` through `/bin/sh -c`. Normalized and validated by @gobing-ai/ts-infra runNodeApplication before the server starts.",
99
+ "items": {
100
+ "type": "object",
101
+ "required": ["name", "command"],
102
+ "properties": {
103
+ "name": {
104
+ "type": "string",
105
+ "minLength": 1,
106
+ "description": "Job name, unique across the list after trimming. Reported as the `scheduler.custom:<name>` display name on scheduler.job.executed."
107
+ },
108
+ "command": {
109
+ "type": "string",
110
+ "minLength": 1,
111
+ "description": "Shell command run by `/bin/sh -c` with the project root as cwd. Trusted operator input \u2014 never logged."
112
+ },
113
+ "intervalMinutes": {
114
+ "type": "integer",
115
+ "minimum": 1,
116
+ "maximum": 35791,
117
+ "description": "Fixed interval in minutes. Mutually exclusive with `cron`. The upper bound is the Node timer ceiling (2147483647 ms)."
118
+ },
119
+ "cron": {
120
+ "type": "string",
121
+ "minLength": 1,
122
+ "description": "Five-field cron expression (minute hour day-of-month month day-of-week) evaluated in local time. Mutually exclusive with `intervalMinutes`."
123
+ }
124
+ },
125
+ "oneOf": [
126
+ { "required": ["intervalMinutes"], "not": { "required": ["cron"] } },
127
+ { "required": ["cron"], "not": { "required": ["intervalMinutes"] } }
128
+ ]
129
+ }
95
130
  }
96
131
  }
97
132
  }
@@ -17,7 +17,9 @@
17
17
  "minLength": 1
18
18
  },
19
19
  "version": {
20
- "type": "string"
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "description": "Optional behavior-neutral identity tag (0756). Absent = unversioned; present = a non-empty opaque literal surfaced as explicit(<literal>). Not parsed, ordered, or compatibility-checked; no registry."
21
23
  },
22
24
  "description": {
23
25
  "type": "string"
@@ -17,7 +17,9 @@
17
17
  "minLength": 1
18
18
  },
19
19
  "version": {
20
- "type": "string"
20
+ "type": "string",
21
+ "minLength": 1,
22
+ "description": "Optional behavior-neutral identity tag (0756). Absent = unversioned; present = a non-empty opaque literal surfaced as explicit(<literal>). Not parsed, ordered, or compatibility-checked; no registry."
21
23
  },
22
24
  "description": {
23
25
  "type": "string"