@gobing-ai/spur 0.3.70 → 0.3.72

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 (74) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/workflow-composition-baseline.json +7 -7
  3. package/config/workflows/idea-pipeline.yaml +4 -3
  4. package/config/workflows/task-pipeline.yaml +31 -11
  5. package/config/workflows/wrapup-pipeline.yaml +10 -4
  6. package/package.json +9 -9
  7. package/plugins/sp/commands/dev-fixall.md +4 -3
  8. package/plugins/sp/plugin.json +1 -1
  9. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +121 -0
  10. package/plugins/sp/scripts/daily-summary/daily-summary.ts +204 -0
  11. package/plugins/sp/scripts/verify-answer-lint.ts +12 -3
  12. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  13. package/plugins/sp/skills/spec-decomposition/SKILL.md +1 -1
  14. package/plugins/sp/skills/spur-cli/references/history.md +7 -6
  15. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
  16. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +8 -1
  17. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  18. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +25 -0
  19. package/schemas/spur-config.schema.json +35 -0
  20. package/spur.js +3117 -1485
  21. package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
  22. package/web/_astro/BoardApp.E12MFjOS.js +1 -0
  23. package/web/_astro/{TaskDetail.DKzpkDj5.js → TaskDetail.CgUreSP2.js} +1 -1
  24. package/web/_astro/{arc.Bsa0gprH.js → arc.BySSh34M.js} +1 -1
  25. package/web/_astro/{architectureDiagram-3BPJPVTR.LOUZCDBC.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
  26. package/web/_astro/{blockDiagram-GPEHLZMM.CGee4isG.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
  27. package/web/_astro/{c4Diagram-AAUBKEIU.B2CrQJ_O.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
  28. package/web/_astro/channel.5cYKr5cs.js +1 -0
  29. package/web/_astro/{chunk-2J33WTMH.DJvf8RiP.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
  30. package/web/_astro/{chunk-4BX2VUAB.B_l35O-Y.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
  31. package/web/_astro/{chunk-55IACEB6.DqbxMswU.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
  32. package/web/_astro/{chunk-727SXJPM.D_7ZGNjE.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
  33. package/web/_astro/{chunk-AQP2D5EJ.Cq6yZ0s8.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
  34. package/web/_astro/{chunk-FMBD7UC4.BWrlUPYi.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
  35. package/web/_astro/{chunk-ND2GUHAM.BPLaXFZH.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
  36. package/web/_astro/{chunk-QZHKN3VN.BtmAEAwZ.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
  37. package/web/_astro/{classDiagram-4FO5ZUOK.CWycT7Ia.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
  38. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CWycT7Ia.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
  39. package/web/_astro/{cose-bilkent-S5V4N54A.DyxjSWon.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
  40. package/web/_astro/{cynefin-OW5HDTMX.DnNfLBpX.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
  41. package/web/_astro/{dagre-BM42HDAG.BFAvnjKD.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
  42. package/web/_astro/{diagram-2AECGRRQ.BAoNCFeB.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
  43. package/web/_astro/{diagram-5GNKFQAL.DiiKe7BP.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
  44. package/web/_astro/{diagram-KO2AKTUF.t_uOQWVP.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
  45. package/web/_astro/{diagram-LMA3HP47.SB9997cP.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
  46. package/web/_astro/{diagram-OG6HWLK6.Bgsxgf5G.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
  47. package/web/_astro/{erDiagram-TEJ5UH35.DO9OAOAc.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
  48. package/web/_astro/{flowDiagram-I6XJVG4X.CIb41ZyL.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
  49. package/web/_astro/{ganttDiagram-6RSMTGT7.Dc8Lk0fV.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
  50. package/web/_astro/{gitGraphDiagram-PVQCEYII.CpPmMWrl.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
  51. package/web/_astro/index.B5MTfe7k.css +1 -0
  52. package/web/_astro/{infoDiagram-5YYISTIA.DxBXfQSf.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
  53. package/web/_astro/{ishikawaDiagram-YF4QCWOH.gA6OKtXq.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
  54. package/web/_astro/{journeyDiagram-JHISSGLW.CMEyLMmm.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
  55. package/web/_astro/{kanban-definition-UN3LZRKU.CKXlUNCr.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
  56. package/web/_astro/{linear.CpTGdVx3.js → linear.D7uqzENp.js} +1 -1
  57. package/web/_astro/{mermaid.core.c8SSCsE5.js → mermaid.core.CxrNppBD.js} +4 -4
  58. package/web/_astro/{mindmap-definition-RKZ34NQL.BH59HDw6.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
  59. package/web/_astro/{pieDiagram-4H26LBE5.DmgFXkZR.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
  60. package/web/_astro/{quadrantDiagram-W4KKPZXB.DyfB7N4J.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
  61. package/web/_astro/{requirementDiagram-4Y6WPE33.DosszoFo.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
  62. package/web/_astro/{sankeyDiagram-5OEKKPKP.pBkHhvVN.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
  63. package/web/_astro/{sequenceDiagram-3UESZ5HK.BE8tmkNR.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
  64. package/web/_astro/{stateDiagram-AJRCARHV.C4n7vDiG.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
  65. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BpgwYlRy.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
  66. package/web/_astro/{timeline-definition-PNZ67QCA.DvcVKc-W.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
  67. package/web/_astro/{vennDiagram-CIIHVFJN.BLOk-UOl.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
  68. package/web/_astro/{wardleyDiagram-YWT4CUSO.CIzB7M2o.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
  69. package/web/_astro/{xychartDiagram-2RQKCTM6.CGC_lZ19.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
  70. package/web/index.html +2 -2
  71. package/web/_astro/BoardApp.DHj-03Dp.js +0 -1
  72. package/web/_astro/BoardApp.DOadeEJV.js +0 -178
  73. package/web/_astro/channel.BGL7KSHC.js +0 -1
  74. package/web/_astro/index.C-t8kB0T.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,122 @@ 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 body = `Finding: ${key}\\nObserved ${lp.repeats} redundant invocations in session ${lp.sessionId} (steps #${lp.fromSeq ?? 1}→#${lp.toSeq ?? lp.repeats}). Wasted tokens: ~${lp.wastedTokens}.`;
373
+ const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
374
+ remediationProposals.push({ key, title, command });
375
+ }
376
+
377
+ if (toolCalls > 0 && errorRatePct > 10) {
378
+ const key = 'reliability:tooling:high-error-rate';
379
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
380
+ const body = `Finding: ${key}\\nObserved ${toolErrors} errors across ${toolCalls} tool calls (${errorRatePct.toFixed(1)}% error rate) on ${date}.`;
381
+ const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
382
+ remediationProposals.push({ key, title, command });
383
+ }
384
+
385
+ return {
386
+ toolCalls,
387
+ toolErrors,
388
+ errorRatePct,
389
+ loops,
390
+ redundantCalls,
391
+ wastedTokens,
392
+ remediationProposals,
393
+ };
394
+ } finally {
395
+ db.close();
396
+ }
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+
260
402
  // ─── Git Integration ─────────────────────────────────────────────────────────
261
403
 
262
404
  export async function getGitCommits(date: string): Promise<GitCommit[]> {
@@ -477,6 +619,61 @@ export function generateMarkdown(summary: DailySummary): string {
477
619
  lines.push('');
478
620
  }
479
621
 
622
+ // Execution Loops & Health Findings
623
+ if (summary.historyHealth) {
624
+ const hh = summary.historyHealth;
625
+ lines.push('## Execution Loops & Health Findings');
626
+ lines.push('');
627
+
628
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
629
+ lines.push('- **Status:** ✅ Clean — No execution loops or tool calls recorded for this date.');
630
+ lines.push('');
631
+ } else {
632
+ lines.push('| Metric | Value |');
633
+ lines.push('|--------|-------|');
634
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
635
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
636
+ lines.push(`| Detected Loops (Repeats ≥ 3) | ${hh.loops.length} |`);
637
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
638
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
639
+ lines.push('');
640
+
641
+ if (hh.loops.length > 0) {
642
+ lines.push('### Detected Execution Loops');
643
+ lines.push('');
644
+ for (const lp of hh.loops.slice(0, 10)) {
645
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} → #${lp.toSeq})` : '';
646
+ const argsHint =
647
+ lp.argsDigest === '74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b'
648
+ ? 'empty/unrecorded arguments'
649
+ : lp.argsDigest.length > 28
650
+ ? `${lp.argsDigest.slice(0, 24)}...`
651
+ : lp.argsDigest;
652
+ lines.push(
653
+ `- \`${lp.toolName || 'unknown'}\` × **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`,
654
+ );
655
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
656
+ }
657
+ lines.push('');
658
+ }
659
+
660
+ if (hh.remediationProposals.length > 0) {
661
+ lines.push('### Auto-Healing Remediation Proposals');
662
+ lines.push('');
663
+ lines.push('To remediate root causes and prevent recurring token waste, execute:');
664
+ lines.push('');
665
+ lines.push('```bash');
666
+ for (const prop of hh.remediationProposals) {
667
+ lines.push(`# ${prop.title} [${prop.key}]`);
668
+ lines.push(prop.command);
669
+ lines.push('');
670
+ }
671
+ lines.push('```');
672
+ lines.push('');
673
+ }
674
+ }
675
+ }
676
+
480
677
  // History report path (R7 — surfaces the newest nightly-run artifact).
481
678
  if (summary.historyReportPath) {
482
679
  lines.push('## History Report');
@@ -598,6 +795,13 @@ export async function buildDailySummary(options: CliOptions): Promise<DailySumma
598
795
  result.gitActivity = gitActivity;
599
796
  }
600
797
 
798
+ // Query Spur history health (loops, tool errors, and auto-healing proposals)
799
+ const historyHealth = await getSpurHistoryHealth(options.date);
800
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
801
+ result.historyHealth = historyHealth;
802
+ platforms.push('Spur History');
803
+ }
804
+
601
805
  return result;
602
806
  }
603
807
 
@@ -245,15 +245,24 @@ function sectionBetween(text: string, heading: string): string {
245
245
  function extractRequirementIds(taskContent: string): string[] {
246
246
  const section = sectionBetween(taskContent, 'Requirements');
247
247
  const ids = new Set<string>();
248
- for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\s*\./g)) ids.add(m[1] ?? '');
249
- for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\*\*/g)) ids.add(m[1] ?? '');
248
+ // Corpus forms: bold-wrapped (`**R1. Title.**`, bare `**R1**`); right after a list
249
+ // marker with optional checkbox (`- [ ] R1.` — the dominant corpus form, `- R1. Title.`,
250
+ // `- R1:`) or a bare checkbox with the marker omitted (`[x] R1.`); line-start (`R1:`).
251
+ // The marker and the checkbox are never both optional — that would match bare prose
252
+ // (`R1 is …`) and fabricate declarations. Sub-IDs (`R1.1`) match in every form.
253
+ for (const m of section.matchAll(/\*\*(R\d+(?:\.\d+)*)\b/g)) ids.add(m[1] ?? '');
254
+ for (const m of section.matchAll(/^(?:[-*]\s+(?:\[[ xX]\]\s+)?|\[[ xX]\]\s+)(R\d+(?:\.\d+)*)/gm))
255
+ ids.add(m[1] ?? '');
256
+ for (const m of section.matchAll(/^(R\d+(?:\.\d+)*)\s*[.:]/gm)) ids.add(m[1] ?? '');
250
257
  return [...ids];
251
258
  }
252
259
 
253
260
  function extractAcIdentities(taskContent: string, featureContent: string | null): string[] {
254
261
  const identities = new Set<string>();
255
262
  const section = sectionBetween(taskContent, 'Acceptance Criteria');
256
- for (const m of section.matchAll(/^[-*]\s+\[[ x]\]\s+(.+?)\s*(?::|$)/gm)) {
263
+ // Checkbox labels (`- [x] AC1 (R1): …`, 0726) and plain bullets (`- AC1: Given …`,
264
+ // 0713/0727) both yield the label text up to `:` plus its leading token.
265
+ for (const m of section.matchAll(/^[-*]\s+(?:\[[ xX]\]\s+)?(.+?)\s*(?::|$)/gm)) {
257
266
  const label = (m[1] ?? '').trim();
258
267
  if (!label) continue;
259
268
  identities.add(label);
@@ -81,7 +81,7 @@ surfaces is what keeps that gate quiet.
81
81
  ## Implement scope: do not run the project quality gate
82
82
 
83
83
  During implement, the pipeline's `test` hop runs `${vars.qualityGateCmd}` (the full project gate:
84
- `bun run format && bun run spur-check`) immediately after this step and is the gate that actually
84
+ `bun run spur-check`) immediately after this step and is the gate that actually
85
85
  decides pass/fail. Running it inside implement is pure redundancy — it cannot change the outcome and
86
86
  only burns wall clock and context budget.
87
87
 
@@ -57,7 +57,7 @@ granularity knobs (min/target/force-split hours), and parent/umbrella-task conve
57
57
  spur task batch-create --file decomposition.json # bare JSON array; atomic, all-or-nothing
58
58
  ```
59
59
 
60
- Validate locally against `apps/cli/schemas/task-batch.schema.json` (runtime SSOT: the Zod
60
+ Validate locally against `task-batch.schema.json` (runtime SSOT: the Zod
61
61
  `taskBatchSchema`) before invoking the CLI — a single violation rejects the entire batch. The gate is
62
62
  the only proof the decomposition is well-formed; never hand-write task files to bypass it.
63
63
 
@@ -29,9 +29,9 @@ Every JSON-capable verb also advertises `--json-envelope`; use the facade's mach
29
29
  ## `import` - isolated fan-out
30
30
 
31
31
  ```bash
32
- bun run apps/cli/src/index.ts history import --source all --dry-run --json
33
- bun run apps/cli/src/index.ts history import --source codex --mode incremental --json
34
- bun run apps/cli/src/index.ts history import --source codex --file session.jsonl --mode force-file --json
32
+ spur history import --source all --dry-run --json
33
+ spur history import --source codex --mode incremental --json
34
+ spur history import --source codex --file session.jsonl --mode force-file --json
35
35
  ```
36
36
 
37
37
  - `--source all` and a single source use the same per-source fan-out path. A failed/timed-out source
@@ -39,9 +39,10 @@ bun run apps/cli/src/index.ts history import --source codex --file session.jsonl
39
39
  - Modes are `incremental`, `full`, and `force-file`. `--file` with the default `all` source is a
40
40
  usage error. `--file --mode full` requires `--dry-run`; use `force-file` for a real single-file
41
41
  write.
42
- - JSON contains `entries`, `warnings`, `exitCode`, and CLI/importer `provenance`. For real-data
43
- validation, invoke the source-local CLI and record that provenance; never trust a bare global
44
- `spur` that may be stale.
42
+ - JSON contains `entries`, `warnings`, `exitCode`, and CLI/importer `provenance`. Record that
43
+ provenance for any real-data validation. **When developing Spur itself**, invoke the source-local
44
+ CLI (`bun run apps/cli/src/index.ts history import …`) instead of a global `spur` that may be a
45
+ stale published bundle; in every other project the installed `spur` is the CLI.
45
46
  - Exit `0` when every source is clean/empty, `2` for a mixed failure or any degraded source, and `1`
46
47
  when all sources fail. CLI usage guards also exit `1` on this noun.
47
48
 
@@ -463,8 +463,8 @@ is the procedure. The backing is a combination of git CLI, `spur` CLI, and agent
463
463
  6. Run `bun run test`. Collect all failures.
464
464
  7. If tests are green, done.
465
465
  8. **Test fix loop:** for each failure, diagnose (test bug vs implementation bug), apply the fix, re-run the **failing test only** (`bun test <file> --test-name-pattern "<test>"`). Do NOT re-run the full suite per fix — it is the dominant loop cost (task 0436 R2).
466
- 9. **Confirming run (at most once).** After all fixes, run `bun run format && bun run lint && bun run test` **at most once** to confirm. If it passes, the hop is done.
467
- 10. **Pipeline-awareness (R4, task 0483).** When `/sp:dev-fixall` is invoked from the pipeline's `test-fix` hop, `test-recheck` runs the full `${vars.qualityGateCmd}` gate immediately after this hop returns — that is the **deciding** run that writes PASS to `.spur/run/<wbs>-test-gate.status`. Do NOT re-run the full gate beyond the single confirming run in step 9; the deciding run belongs to `test-recheck`. If your confirming run already passed, return immediately a second or third gate run inside this hop is pure redundancy (0482 ran the gate plus a standalone `bun run test`; all four were followed by `test-recheck` running it a 5th time). If your confirming run failed and you fixed more, re-run the full gate once more within `--max-retry` budget, then return let `test-recheck` judge.
466
+ 9. **Confirming run — standalone invocation only (at most once).** After all fixes, run `bun run format && bun run lint && bun run test` **at most once** to confirm. If it passes, the hop is done. **Skip this step entirely when `--gate-log` is set** (see step 10) — under the pipeline nothing consumes its verdict.
467
+ 10. **Pipeline-awareness (R4, task 0483; confirming run dropped 2026-08-31).** `--gate-log` is set only by the pipeline's `test-fix` hop, so treat it as the pipeline signal. There, `test-recheck` runs the full `${vars.qualityGateCmd}` gate immediately after this hop returns — that is the **deciding** run, the only one that writes PASS to `.spur/run/<wbs>-test-gate.status`. A confirming run inside this hop decides nothing and costs a second full gate (~105 s here) to answer a question `test-recheck` is about to answer anyway. So under `--gate-log`: **run no full gate at all.** Your self-check is what step 8 already gives you the failing tests you re-ran individually plus **one `bun run lint`** (~7 s: Biome + typecheck) to catch type or lint breakage your fixes introduced. Then return and let `test-recheck` judge. `test-recheck` opens with the same cheap `${vars.gateProbeCmd}` probe, so a still-red tree is caught in seconds rather than by a full suite. Historical anti-pattern this replaces: 0482 ran the gate 3× plus a standalone `bun run test`, and `test-recheck` then ran it a 5th time.
468
468
  11. Report: list what was fixed (file + one-line summary per fix). If any error could not be resolved, report it explicitly — do not suppress.
469
469
  - **Invariants:** Never bypass with `--no-verify`, `--force`, or new `biome-ignore`/`eslint-disable` suppressions. Never skip or `.skip` a test to make the suite green. Fix the root cause, not the symptom. Never claim green on `bun run lint` alone — a formatter-only diff passes `lint` but fails the formatter; run `bun run format` (or assert it produces no diff) before declaring the gate clean. **Never re-run the full gate more than once per confirming pass** (R4) — use targeted probes during the fix loops and let the pipeline's `test-recheck` state be the deciding run.
470
470
  - **MANDATORY Exit Condition.** The ONLY way to complete successfully:
@@ -46,7 +46,7 @@ one thing and yields, so the **pipeline (not the agent) owns the loop**.
46
46
  | Stage | Operation | Defined in |
47
47
  | ------- | ----------- | ------------ |
48
48
  | `implement` | `/sp:dev-run --mode implement <wbs>` — write the code that satisfies the task; author `## Solution`. | [dev-operations.md §4 run](dev-operations.md) → `sp:code-implementation` |
49
- | `test` → (`test-fix` ↔ `test-recheck`) → `review` \| `failed` | **Project quality gate** (not `/sp:dev-unit`). Soft shell probe of `${vars.qualityGateCmd}` (default `bun run autofix && bun run spur-check`) — green path pays **one** full gate run. On FAIL: bounded `/sp:dev-fixall` loop (`qualityGateMaxFixAttempts`, default 2) with soft recheck; exhausted attempts route to pipeline `failed`. `/sp:dev-unit` remains **coverage gap-fill** (router C3/C5 / standalone). | [dev-operations.md §10 fixall](dev-operations.md); unit op still §1 |
49
+ | `test` → (`test-fix` ↔ `test-recheck`) → `review` \| `failed` | **Project quality gate** (not `/sp:dev-unit`). Soft shell probe of `${vars.qualityGateCmd}` (default `bun run spur-check`) — green path pays **one** full gate run. On FAIL: bounded `/sp:dev-fixall` loop (`qualityGateMaxFixAttempts`, default 2) with soft recheck; exhausted attempts route to pipeline `failed`. `/sp:dev-unit` remains **coverage gap-fill** (router C3/C5 / standalone). | [dev-operations.md §10 fixall](dev-operations.md); unit op still §1 |
50
50
  | `review` | `/sp:dev-review <wbs>` — SECUA-framework review of the diff. | [dev-operations.md §2 review](dev-operations.md) |
51
51
  | `verify` | `sp:code-verification` — requirements traceability + verdict. | [dev-operations.md §3 verify](dev-operations.md) |
52
52
 
@@ -97,6 +97,7 @@ cache-conservation discipline (`plugins/sp/skills/dogfood-testing/references/mon
97
97
  ## Step 2: Pipeline run
98
98
 
99
99
  > **Pre-launch size-gate pre-check (R1 / 0478).** Before launching `spur workflow run task-pipeline.yaml`, probe the task's `## Plan` checklist item count (`spur task show <wbs> --json`). The default cap is 8 items (`maxImplementPlanItems: 8`). If the plan item count exceeds 8:
100
+ >
100
101
  > - Without `--auto`: warn the operator before calling `spur workflow run` and prompt for confirmation or a plan-item override via `--vars '{"maxImplementPlanItems":"<count>"}'`.
101
102
  > - With `--auto`: automatically append `"maxImplementPlanItems": "<count>"` to `--vars` and log a single-line notice (e.g. `Notice: task <wbs> has N plan items (>8 default cap); injecting maxImplementPlanItems override`).
102
103
 
@@ -313,6 +314,12 @@ the partial work still in the working tree. The failure output names the partial
313
314
  [`done-housekeeping.md`](done-housekeeping.md) F6 — it carries the provenance obligations
314
315
  (honest `done_reason`, verdict regeneration).
315
316
 
317
+ **Inline path (task 0727).** There is no `<runId>-implement-partial.md` artifact on the inline
318
+ driver path — it is written only by the subprocess `agent.run` action. The inline equivalent is
319
+ the dispatch-timeout contract in [`inline-pipeline-driver.md`](inline-pipeline-driver.md):
320
+ resume from the partial tree, never restart the stage inline; the partial working tree is the
321
+ recovery input.
322
+
316
323
  **3. Match the executor to the size (task 0487 R3).** Size and executor capability are one
317
324
  decision, not two. **≥ 6 requirements or ≥ 9 Plan items → a `reviewer`-role executor (the
318
325
  `reviewer` row of [`roles.md`](../../../references/roles.md) declares the floor), or split the
@@ -52,7 +52,7 @@ Entered before `spur task batch-create --file <json>` (idea-pipeline `batch-crea
52
52
  decomposition gate `/sp:dev-plan` also routes through since D5-K).
53
53
 
54
54
  - [ ] The batch JSON is a bare array (not an object with a `tasks` key).
55
- - [ ] Each entry validates locally against `apps/cli/schemas/task-batch.schema.json` (`additionalProperties: false` — no unknown keys).
55
+ - [ ] Each entry validates locally against `task-batch.schema.json` (`additionalProperties: false` — no unknown keys).
56
56
  - [ ] Each entry has a non-empty `name` (required).
57
57
  - [ ] `feature_id` matches an existing feature (or is intentionally deferred with operator awareness).
58
58
  - [ ] `parent_wbs` is set when the task is a child of a decomposition parent.
@@ -41,6 +41,12 @@ command, skill, script, or second workflow.
41
41
  the YAML parsed in step 1, shown only for the active state.
42
42
  - **Refresh cadence** = stage boundaries only (when the current state changes after a transition),
43
43
  never per action.
44
+ - **Transition reconciliation (task 0727)** = at every stage boundary the host must
45
+ **mark the finished stage completed and the next stage in_progress** in the host todo list.
46
+ This reconciliation is **host-owned and execution-surface-independent**: it fires identically
47
+ whether the stage ran via native subagent, host-inline execution, or the post-dispatch host
48
+ fallback, so a run can never terminate with earlier stages stuck `in_progress` (task 0726
49
+ ended 0/11 with precheck and implement still open).
44
50
  - **Source of truth** = the CLI projection for layer 1; the YAML parsed in step 1 for layer 2.
45
51
  Never hand-copy or hand-derive the state list into the driver, a command, a skill, or a script.
46
52
  5. For task execution only, record lifecycle provenance before entering the FSM:
@@ -119,6 +125,19 @@ before the subagent starts, log the reason and use host fallback. If a started s
119
125
  leaves invalid artifacts, do **not** replay the stage in the host — follow the YAML error policy so
120
126
  partial mutations are not duplicated.
121
127
 
128
+ **Timeout boundary (task 0727):** a dispatched subagent is governed by
129
+ **the host platform's subagent limit, not the YAML timeoutMs** — `timeoutMs` stays not-applicable
130
+ for host execution only — and before dispatch the driver must
131
+ **record the governing timeout boundary and its source before dispatch** in the run log
132
+ (e.g. `host timeout <ms> (<platform subagent limit|yaml timeoutMs>)`). If the dispatch reaches that
133
+ boundary, **a dispatch timeout is a started-subagent failure**: the no-replay rule above and the
134
+ stage's declared YAML error policy govern (implement's default `fail` policy routes the run to
135
+ `failed`); it is never a host re-execution. Recovery follows the
136
+ [timed-out implement runbook](execution-workflow.md)'s inline-path equivalent:
137
+ **resume from the partial tree, never restart the stage inline** — no
138
+ `<runId>-implement-partial.md` artifact is written on this path, so the partial working tree
139
+ itself is the recovery input.
140
+
122
141
  **Host-owned interaction:** the host alone executes operator-confirmation actions, owns
123
142
  `pause: true`, and surfaces approve/taste/ask decisions. A subagent that discovers missing authority
124
143
  or an operator decision returns a blocker; the host pauses at the current state and presents it. The
@@ -129,6 +148,12 @@ subagent form above) to `.spur/run/<run-id>.log`, where `<id>` is the current YA
129
148
  log start/failure and the ignored timeout value so an inline run remains auditable without
130
149
  fabricating an `AgentRunTracedResult`.
131
150
 
151
+ Run-log stamps (task 0727): every appended line is prefixed with an **ISO-8601 UTC** timestamp
152
+ (`YYYY-MM-DDTHH:MM:SSZ`, e.g. `2026-08-31T17:51:11Z`); the exact-template provenance lines above
153
+ keep their exact content after the stamp prefix. This normalization is contractual:
154
+ **bare local-clock stamps are prohibited** — a hand-appended `[stage 12:31]` form mixes timezones
155
+ in one file and makes the run unauditable (task 0726 mixed both forms).
156
+
132
157
  Transition guards are not advisory. Execute the declared guard exactly, in order, with the same
133
158
  resolved variables and artifacts. `--no-lifecycle` remains bookkeeping only; the YAML's task checks,
134
159
  verdict gate, record step, and done guard all remain authoritative.
@@ -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
  }