@gobing-ai/spur 0.4.2 → 0.4.3

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 (133) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/decisions/maker-liveness.yaml +39 -0
  3. package/config/plugin-scripts.json +0 -4
  4. package/config/rules/README.md +1 -0
  5. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  6. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  7. package/config/script-placement-baseline.json +1 -19
  8. package/config/workflows/feature-lifecycle.yaml +46 -1
  9. package/config/workflows/feature-verification.yaml +11 -0
  10. package/package.json +9 -9
  11. package/plugins/sp/README.md +22 -16
  12. package/plugins/sp/agents/super-reviewer.md +43 -0
  13. package/plugins/sp/bunfig.toml +8 -5
  14. package/plugins/sp/hooks/careful-guard.ts +1 -1
  15. package/plugins/sp/hooks/context-post-tool.ts +7 -427
  16. package/plugins/sp/hooks/context-session-start.ts +5 -213
  17. package/plugins/sp/hooks/context-session-stop.ts +5 -191
  18. package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
  19. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  20. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  21. package/plugins/sp/lib/context-post-tool.ts +432 -0
  22. package/plugins/sp/lib/context-session-start.ts +214 -0
  23. package/plugins/sp/lib/context-session-stop.ts +196 -0
  24. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  25. package/plugins/sp/lib/dogfood-report.ts +278 -0
  26. package/plugins/sp/lib/idea-handoff.generated.mjs +3 -3
  27. package/plugins/sp/lib/inline-run-app.ts +40 -0
  28. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  29. package/plugins/sp/lib/inline-run.generated.mjs +1347 -90
  30. package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
  31. package/plugins/sp/lib/plan-projection.generated.mjs +591 -1
  32. package/plugins/sp/lib/quality-gate.generated.mjs +38 -25
  33. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  34. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  35. package/plugins/sp/lib/run-summary-core.ts +34 -370
  36. package/plugins/sp/lib/transcript.ts +37 -228
  37. package/plugins/sp/plugin.json +1 -1
  38. package/plugins/sp/scripts/batch-plan.mjs +586 -0
  39. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  40. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  41. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  42. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  43. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  44. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  45. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  46. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  47. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  48. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  49. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  50. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  51. package/plugins/sp/scripts/inline-run-setup.mjs +149 -122
  52. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  53. package/plugins/sp/scripts/quality-gate.mjs +14 -2
  54. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  55. package/plugins/sp/scripts/residual-scan.ts +8 -5
  56. package/plugins/sp/scripts/run-summary.mjs +151 -97
  57. package/plugins/sp/scripts/run-summary.ts +7 -5
  58. package/plugins/sp/scripts/session-timeline.mjs +46 -19
  59. package/plugins/sp/scripts/task-diffstat.mjs +11 -1
  60. package/plugins/sp/scripts/task-diffstat.ts +8 -39
  61. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  62. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  63. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  64. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  65. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  66. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  67. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  68. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  69. package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
  70. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  71. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
  72. package/plugins/sp/skills/spur-dev/references/execution-batch.md +13 -3
  73. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  74. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  75. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -12
  76. package/spur.js +80681 -79296
  77. package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.5IIl2O-z.js} +1 -1
  78. package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.Drnx-KeM.js} +150 -150
  79. package/web/_astro/{TaskDetail.Cxov1_N_.js → TaskDetail.Ci4LNn9S.js} +1 -1
  80. package/web/_astro/{arc.vaT7tPOB.js → arc.j2EVkYVd.js} +1 -1
  81. package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  82. package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  83. package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  84. package/web/_astro/channel.DmdDxGsq.js +1 -0
  85. package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  86. package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  87. package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  88. package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  89. package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  90. package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  91. package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  92. package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  93. package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  94. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  95. package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  96. package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  97. package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  98. package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  99. package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  100. package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  101. package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  102. package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  103. package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  104. package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  105. package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  106. package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  107. package/web/_astro/index.Do2lLdgo.css +1 -0
  108. package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  109. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  110. package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  111. package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  112. package/web/_astro/{linear.BzGnBwNL.js → linear.CI6djNi7.js} +1 -1
  113. package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CuQPxS8b.js} +4 -4
  114. package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  115. package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  116. package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  117. package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  118. package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  119. package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  120. package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  121. package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  122. package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  123. package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  124. package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  125. package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  126. package/web/index.html +2 -2
  127. package/config/proportional-route-table.ts +0 -155
  128. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  129. package/web/_astro/channel.D4nG9YLf.js +0 -1
  130. package/web/_astro/index.BhSwN8yY.css +0 -1
  131. /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
  132. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  133. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -11,9 +11,31 @@
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 { fileURLToPath } from 'node:url';
14
15
  import { getEnvVar, getEnvVars } from '../../lib/env';
16
+ import { loadInlineApp, resolveAppEntry } from '../../lib/inline-run-app';
15
17
 
16
- import { logger } from './logger';
18
+ // ─── In-script logger ────────────────────────────────────────────────────────
19
+
20
+ // The former `logger.ts` helper exposed a file channel nothing ever implemented, so the used part
21
+ // — a console toggle the tests need — lives here instead of a one-caller module (task 1173, plan P7).
22
+ let consoleEnabled = true;
23
+
24
+ /** Silence or restore this script's console output (test hook). */
25
+ export function enableLogger(consoleOn: boolean): void {
26
+ consoleEnabled = consoleOn;
27
+ }
28
+
29
+ function logAt(stream: 'log' | 'warn' | 'error', message: string): void {
30
+ if (!consoleEnabled) return;
31
+ console[stream](message);
32
+ }
33
+
34
+ const logger = {
35
+ info: (message: string): void => logAt('log', message),
36
+ warn: (message: string): void => logAt('warn', message),
37
+ error: (message: string): void => logAt('error', message),
38
+ };
17
39
 
18
40
  // ─── Types ───────────────────────────────────────────────────────────────────
19
41
 
@@ -287,113 +309,85 @@ export async function getCcusageData(date: string): Promise<CcusageData | null>
287
309
 
288
310
  // ─── Spur History Health Integration ──────────────────────────────────────────
289
311
 
312
+ /** The day's raw history health read back across the generated inline app entry (task 1178 R3). */
313
+ interface HistoryHealthReading {
314
+ toolCalls: number;
315
+ toolErrors: number;
316
+ errorRatePct: number;
317
+ loops: Array<{
318
+ toolName: string;
319
+ argsDigest: string;
320
+ repeats: number;
321
+ sessionId: string;
322
+ firstSeq: number;
323
+ lastSeq: number;
324
+ startedAt: string | null;
325
+ }>;
326
+ }
327
+
328
+ /** The generated inline app entry's history-health export. */
329
+ interface InlineHistoryApp {
330
+ readHistoryHealth(input: { date: string; dbPath: string }): Promise<HistoryHealthReading | null>;
331
+ }
332
+
333
+ /**
334
+ * Compose the daily report's health summary from the app reading. `wastedTokens` stays an
335
+ * explicit `repeats * 250` estimate; remediation proposals stay plain text (task 1178 R3).
336
+ */
337
+ function composeHistoryHealth(reading: HistoryHealthReading): HistoryHealthSummary {
338
+ const loops: HistoryLoopFinding[] = reading.loops.map((row) => ({
339
+ toolName: row.toolName,
340
+ argsDigest: row.argsDigest || 'repeated execution',
341
+ repeats: row.repeats,
342
+ sessionId: row.sessionId,
343
+ fromSeq: row.firstSeq,
344
+ toSeq: row.lastSeq,
345
+ wastedTokens: row.repeats * 250,
346
+ }));
347
+
348
+ const remediationProposals: Array<{ key: string; title: string; command: string }> = [];
349
+ for (const lp of loops.slice(0, 5)) {
350
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
351
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
352
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
353
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
354
+ remediationProposals.push({ key, title, command });
355
+ }
356
+ if (reading.toolCalls > 0 && reading.errorRatePct > 10) {
357
+ remediationProposals.push({
358
+ key: 'reliability:tooling:high-error-rate',
359
+ title: `Investigate high tool error rate (${reading.errorRatePct.toFixed(1)}%)`,
360
+ command: `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`,
361
+ });
362
+ }
363
+
364
+ return {
365
+ toolCalls: reading.toolCalls,
366
+ toolErrors: reading.toolErrors,
367
+ errorRatePct: reading.errorRatePct,
368
+ loops,
369
+ redundantCalls: loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0),
370
+ wastedTokens: loops.reduce((acc, l) => acc + l.wastedTokens, 0),
371
+ remediationProposals,
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Read the day's history health through the app owner (task 1178 R3): this adapter opens no
377
+ * SQLite and carries no SQL. A missing database file or required table returns `null`
378
+ * (unavailable); an existing empty table returns a measured-zero summary, never `n/a`.
379
+ */
290
380
  export async function getSpurHistoryHealth(
291
381
  date: string,
292
382
  dbPath = '.spur/spur.db',
293
383
  ): Promise<HistoryHealthSummary | null> {
294
384
  try {
295
385
  const resolvedPath = resolve(process.cwd(), dbPath);
296
- if (!existsSync(resolvedPath)) {
297
- return null;
298
- }
299
-
300
- const { Database } = await import('bun:sqlite');
301
- const db = new Database(resolvedPath, { readonly: true });
302
-
303
- try {
304
- // 1. Query execution loop findings
305
- const loopTable = db
306
- .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
307
- .get('history_board_loop_findings');
308
-
309
- let loops: HistoryLoopFinding[] = [];
310
- if (loopTable) {
311
- const rows = db
312
- .query<
313
- {
314
- tool_name: string;
315
- args_digest: string;
316
- repeats: number;
317
- session_id: string;
318
- first_seq: number;
319
- last_seq: number;
320
- started_at: string | null;
321
- },
322
- [string]
323
- >(
324
- `SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
325
- FROM history_board_loop_findings
326
- WHERE started_at IS NULL OR started_at LIKE ?
327
- ORDER BY repeats DESC
328
- LIMIT 20`,
329
- )
330
- .all(`${date}%`);
331
-
332
- loops = rows.map((r) => ({
333
- toolName: r.tool_name,
334
- argsDigest: r.args_digest || 'repeated execution',
335
- repeats: r.repeats,
336
- sessionId: r.session_id,
337
- fromSeq: r.first_seq,
338
- toSeq: r.last_seq,
339
- wastedTokens: r.repeats * 250,
340
- }));
341
- }
342
-
343
- // 2. Query tool calls and errors
344
- let toolCalls = 0;
345
- let toolErrors = 0;
346
- const toolTable = db
347
- .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
348
- .get('history_board_tool_5m');
349
-
350
- if (toolTable) {
351
- const stats = db
352
- .query<{ calls: number | null; errors: number | null }, [string]>(
353
- `SELECT SUM(calls) AS calls, SUM(errors) AS errors
354
- FROM history_board_tool_5m
355
- WHERE bucket_start LIKE ?`,
356
- )
357
- .get(`${date}%`);
358
-
359
- toolCalls = stats?.calls ?? 0;
360
- toolErrors = stats?.errors ?? 0;
361
- }
362
-
363
- const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
364
- const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
365
- const errorRatePct = toolCalls > 0 ? (toolErrors / toolCalls) * 100 : 0;
366
-
367
- // 3. Generate auto-healing remediation proposals
368
- const remediationProposals: Array<{ key: string; title: string; command: string }> = [];
369
-
370
- for (const lp of loops.slice(0, 5)) {
371
- const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
372
- const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
373
- const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
374
- const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
375
- remediationProposals.push({ key, title, command });
376
- }
377
-
378
- if (toolCalls > 0 && errorRatePct > 10) {
379
- const key = 'reliability:tooling:high-error-rate';
380
- const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
381
- const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
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
- }
386
+ const { app } = await loadInlineApp<InlineHistoryApp>(getEnvVar('SPUR_BIN') ?? '', resolveAppEntry, [
387
+ 'readHistoryHealth',
388
+ ]);
389
+ const reading = await app.readHistoryHealth({ date, dbPath: resolvedPath });
390
+ return reading === null ? null : composeHistoryHealth(reading);
397
391
  } catch {
398
392
  return null;
399
393
  }
@@ -795,9 +789,11 @@ export async function buildDailySummary(options: CliOptions): Promise<DailySumma
795
789
  result.gitActivity = gitActivity;
796
790
  }
797
791
 
798
- // Query Spur history health (loops, tool errors, and auto-healing proposals)
792
+ // Query Spur history health (loops, tool errors, and auto-healing proposals). A measured
793
+ // empty day is a real zero and keeps its "Clean" section; an unavailable database (missing
794
+ // file/table) stays absent — absence and zero never collapse (task 1178 R3).
799
795
  const historyHealth = await getSpurHistoryHealth(options.date);
800
- if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
796
+ if (historyHealth) {
801
797
  result.historyHealth = historyHealth;
802
798
  platforms.push('Spur History');
803
799
  }
@@ -806,6 +802,17 @@ export async function buildDailySummary(options: CliOptions): Promise<DailySumma
806
802
  }
807
803
 
808
804
  export async function main(): Promise<void> {
805
+ // The generated app entry read by getSpurHistoryHealth needs Bun (SQLite); the installed
806
+ // `.mjs` twin may be launched with plain node, so re-enter Bun before any app import.
807
+ // Without Bun on PATH, keep running under node: the history reader degrades to unavailable
808
+ // (n/a) instead of failing the whole report (task 1178 review P4).
809
+ if (!process.versions.bun) {
810
+ const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
811
+ stdio: 'inherit',
812
+ });
813
+ if (!child.error) process.exit(child.status ?? 1);
814
+ console.error(`daily-summary: Bun not on PATH (${child.error.message}); history health unavailable`);
815
+ }
809
816
  const options = parseArgs();
810
817
 
811
818
  logger.info(`Generating daily summary for ${options.date}...`);
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
- // plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts
5
+ // plugins/sp/lib/dogfood-pipeline.ts
6
6
  var PIPELINE_TOKENS = [
7
7
  "--next",
8
8
  "dev-runall",
@@ -125,6 +125,7 @@ function evaluateDogfoodGate(testee, options = {}) {
125
125
  exitCode: 0
126
126
  };
127
127
  }
128
+ // plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts
128
129
  function parseCliArgs(argv) {
129
130
  let testee = null;
130
131
  let maxRetryPresent = false;
@@ -1,12 +1,10 @@
1
1
  /**
2
- * detect-pipeline-driving — @1.2 pipeline-driving testee detector (task 0277, W7).
2
+ * detect-pipeline-driving — live Phase 1.0 CLI gate over the @1.2 detector (task 0277 W7).
3
3
  *
4
- * Pure function over the raw testee string → boolean. Used by the dogfood driver
5
- * (Phase 1.0 refuse-ambiguous gate) and by unit tests so the matcher contract is
6
- * machine-checked rather than agent-interpreted.
7
- *
8
- * Also ships a **CLI gate** (import.meta.main) so Phase 1.0 is a live shell call,
9
- * not agent-only prose interpretation:
4
+ * The detector and its policy helpers are application-owned in
5
+ * `plugins/sp/lib/dogfood-pipeline.ts` (task 1173); this script keeps the shell entry the dogfood
6
+ * driver calls, with argv, usage, exit codes and refusal messages unchanged. Callers and tests may
7
+ * keep importing this path — every former export is re-exported.
10
8
  *
11
9
  * bun plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts \
12
10
  * --testee "/sp:dev-run 0125 --auto --next" [--max-retry-present] \
@@ -16,267 +14,11 @@
16
14
  * 0 — proceed (may print implement-heavy advisory on stderr)
17
15
  * 2 — refuse: pipeline-driving without explicit --max-retry
18
16
  * 1 — usage / argument error
19
- *
20
- * Contract (0274 §3 dogfood-pipeline-detect, 0277 R1):
21
- * - Word-boundary matchers with `-` treated as a word char (NOT a boundary).
22
- * A token counts only when it is a distinct hyphen-word, not when it is a
23
- * substring of a longer alphanumeric or hyphen run. This removes the
24
- * leading-space dependency of the @1.1 prose detector.
25
- * - Two token shapes, both with strict `[^\w-]` boundaries:
26
- * (a) complete tokens — `--next`, `dev-run`, `dev-runall`, `dev-wrap`,
27
- * `dev-wrapall`, `dev-idea` — matched as whole hyphen-words so the
28
- * slash form `/sp:dev-run` matches on its `dev-run` tail whether or
29
- * not it is preceded by a space, but `--next` does NOT match inside
30
- * `--next-gen` (the trailing `-gen` breaks the boundary).
31
- * (b) bare nouns — `run`, `runall`, `wrap`, `wrapall`, `idea` — matched
32
- * only as standalone words, so `run` matches in
33
- * `bun ... task run 0042` but NOT inside `runaway`/`prerun`, and
34
- * `idea` does NOT match inside `idealist`/`ideal`.
35
- * - Listing both shapes is what makes the detector leading-space invariant
36
- * (the slash form is caught by its `dev-*` tail; the bare noun is caught
37
- * when it appears as its own word) without letting `run` leak into every
38
- * `-run-` identifier.
39
- *
40
- * Non-goals of detectPipelineDriving: decides ambiguity only. It does NOT decide
41
- * observe vs fix — any explicit `--max-retry` value proceeds regardless.
42
- * Implement-heavy advisory is a separate helper (detectImplementHeavy).
43
- */
44
-
45
- /**
46
- * Tokens whose presence makes a testee pipeline-driving. Ordered for stable
47
- * diagnostics. Two shapes per the contract above: complete `dev-*` / `--next`
48
- * tokens first, then bare nouns.
49
- */
50
- const PIPELINE_TOKENS = [
51
- '--next',
52
- 'dev-runall',
53
- 'dev-wrapall',
54
- 'dev-refineall',
55
- 'dev-verifyall',
56
- 'dev-run',
57
- 'dev-wrap',
58
- 'dev-idea',
59
- 'refineall',
60
- 'verifyall',
61
- 'runall',
62
- 'wrapall',
63
- 'run',
64
- 'wrap',
65
- 'idea',
66
- ] as const;
67
-
68
- /** Refuse message when pipeline-driving and `--max-retry` was not passed (exact string). */
69
- export const PIPELINE_DRIVING_REFUSE_MESSAGE =
70
- '⚠ pipeline-driving testee detected; pass --max-retry 0 (observe-only) or --max-retry N (fix mode, tree mutation acknowledged)';
71
-
72
- /**
73
- * Refuse message when the testee carries a mutating `--fix` mode (`--fix all` /
74
- * `--fix blockers-first`) and `--max-retry` was not passed. A mutating fix mode
75
- * mutates the working tree independent of pipeline-driving (task 0293 R1/R2):
76
- * `--max-retry 0` bounds the **driver** only — the testee's own `--fix` pass
77
- * still mutates the tree — so the message must not imply otherwise.
78
- */
79
- export const MUTATING_FIX_REFUSE_MESSAGE =
80
- '⚠ mutating --fix mode detected (--fix all | --fix blockers-first); pass --max-retry 0 (observe-only for the driver; the testee still mutates the tree) or --max-retry N (fix mode, driver + testee both mutate)';
81
-
82
- /** Advisory when pipeline-driving + implement-heavy derived steps (exact string). */
83
- export const IMPLEMENT_HEAVY_ADVISORY_MESSAGE =
84
- '⚠ implement-heavy pipeline dogfood: prefer --max-retry 0 (observe-only) or step-split; operator --max-retry N overrides';
85
-
86
- /**
87
- * Tokens that mark a derived step as implement-heavy (mutates product/code or
88
- * drives a mutating pipeline leg). Deliberately excludes verify/review/unit-only
89
- * surfaces even when they carry `--next`.
90
- */
91
- const IMPLEMENT_HEAVY_TOKENS = [
92
- 'dev-runall',
93
- 'dev-wrapall',
94
- 'dev-run',
95
- 'dev-wrap',
96
- 'dev-idea',
97
- 'runall',
98
- 'wrapall',
99
- // bare `run` / `wrap` / `idea` — only when not clearly a non-mutating surface
100
- 'run',
101
- 'wrap',
102
- 'idea',
103
- 'implement',
104
- ] as const;
105
-
106
- /**
107
- * A verify/review surface stops being non-mutating the moment it carries a
108
- * repair mode: `--fix all` / `--fix blockers-first` applies Edit/Write repairs
109
- * to the working tree (0280 dogfood, finding P2). `--fix none` stays
110
- * observational. Boundary-guarded so `--prefix all` / `--focus all` never match.
111
- * Exported so the Phase 1.0 gate and tests share one matcher (task 0293 R1/R4).
112
- */
113
- export function hasMutatingFixMode(step: string): boolean {
114
- return /(?<![\w-])--fix[=\s]+(all|blockers-first)(?![\w-])/i.test(step);
115
- }
116
-
117
- /**
118
- * Match a token at a word boundary. A "word boundary" here is the position
119
- * between a non-`[\w-]` char (or string start) and the token, and between the
120
- * token and a non-`[\w-]` char (or string end). Treating `-` as a word char
121
- * is what makes `--next` reject `--next-gen` and `dev-run` reject `dev-runner`.
122
- */
123
- function tokenMatches(testee: string, token: string): boolean {
124
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
125
- // (?<![\w-]) — not preceded by a word char or hyphen.
126
- // (?![\w-]) — not followed by a word char or hyphen.
127
- const re = new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, 'i');
128
- return re.test(testee);
129
- }
130
-
131
- /**
132
- * Returns whether the testee string is pipeline-driving (contains any of the
133
- * pipeline-driving tokens as a distinct hyphen-word). Machine-checked
134
- * counterpart of the prose list in SKILL.md §Pipeline-driving detection and
135
- * dev-dogfood.md.
136
- */
137
- export function detectPipelineDriving(testee: string): boolean {
138
- if (typeof testee !== 'string' || testee.length === 0) {
139
- return false;
140
- }
141
- return PIPELINE_TOKENS.some((token) => tokenMatches(testee, token));
142
- }
143
-
144
- /**
145
- * True when a single step label (or the whole testee) is implement-heavy —
146
- * it chains into real implementation / wrap / idea work, not verify-only.
147
- *
148
- * Non-mutating surfaces (`dev-verify`, `dev-review`, `dev-unit`, plain
149
- * `dev-refine` without a further run) are never implement-heavy even if
150
- * pipeline-driving via `--next` — UNLESS they carry a mutating repair mode
151
- * (`--fix all` / `--fix blockers-first`), which turns the verify/review leg
152
- * into a tree-mutating fix pass (0280 dogfood, finding P2).
153
- */
154
- export function isImplementHeavyStep(step: string): boolean {
155
- if (typeof step !== 'string' || step.length === 0) return false;
156
- // Explicit non-mutating surfaces win unless a mutating token co-occurs.
157
- const nonMutatingOnly =
158
- tokenMatches(step, 'dev-verify') || tokenMatches(step, 'dev-review') || tokenMatches(step, 'dev-unit');
159
- const hasMutating = IMPLEMENT_HEAVY_TOKENS.some((token) => tokenMatches(step, token)) || hasMutatingFixMode(step);
160
- if (nonMutatingOnly && !hasMutating) return false;
161
- // refine alone is planning, not implement-heavy; refine+run/--next chain is.
162
- if (tokenMatches(step, 'dev-refine') && !hasMutating && !tokenMatches(step, '--next')) {
163
- return false;
164
- }
165
- if (tokenMatches(step, 'dev-refine') && tokenMatches(step, '--next')) {
166
- // refine --next chains into run → implement-heavy
167
- return true;
168
- }
169
- return hasMutating;
170
- }
171
-
172
- /**
173
- * True when the testee is implement-heavy: it carries a pipeline-driving token,
174
- * a mutating `--fix` mode, OR at least one derived step (or the testee itself
175
- * when no steps given) is implement-heavy. Used for the W8 Phase 1 advisory
176
- * after step derivation.
177
- *
178
- * Task 0293 R3: a mutating `--fix` mode alone is implement-heavy even with no
179
- * pipeline token — the verify/review leg itself becomes a tree-mutating fix
180
- * pass.
181
- */
182
- export function detectImplementHeavy(testee: string, derivedSteps: string[] = []): boolean {
183
- if (isImplementHeavyStep(testee)) return true;
184
- if (!detectPipelineDriving(testee)) return false;
185
- return derivedSteps.some((step) => isImplementHeavyStep(step));
186
- }
187
-
188
- export interface GateResult {
189
- pipelineDriving: boolean;
190
- mutatingFix: boolean;
191
- maxRetryPresent: boolean;
192
- implementHeavy: boolean;
193
- refuse: boolean;
194
- advisory: boolean;
195
- message: string | null;
196
- exitCode: 0 | 1 | 2;
197
- }
198
-
199
- /**
200
- * Phase 1.0 + W8 gate decision. Pure — no I/O. The CLI wrapper prints and exits.
201
- *
202
- * Refuse condition (task 0293 R1) is the union of two **independent** mutation
203
- * sources, each gated on `!maxRetryPresent`:
204
- *
205
- * 1. pipeline-driving testee (`--next`, `dev-run`, …) — refuses because the
206
- * testee chains lifecycle legs whose cumulative blast radius the driver
207
- * cannot pre-attribute.
208
- * 2. mutating `--fix` mode (`--fix all` / `--fix blockers-first`) — refuses
209
- * because the testee itself applies Edit/Write repairs even with no
210
- * pipeline token. `--max-retry 0` bounds the **driver** only; the testee
211
- * still mutates (honesty note in MUTATING_FIX_REFUSE_MESSAGE).
212
- *
213
- * Pipeline-driving is reported first when both co-occur (its refuse message is
214
- * the superset — chain + tree mutation).
215
17
  */
216
- export function evaluateDogfoodGate(
217
- testee: string,
218
- options: { maxRetryPresent?: boolean; steps?: string[] } = {},
219
- ): GateResult {
220
- const maxRetryPresent = options.maxRetryPresent === true;
221
- const steps = options.steps ?? [];
222
- const pipelineDriving = detectPipelineDriving(testee);
223
- const mutatingFix = hasMutatingFixMode(testee);
224
- const implementHeavy = detectImplementHeavy(testee, steps);
225
-
226
- if (pipelineDriving && !maxRetryPresent) {
227
- return {
228
- pipelineDriving,
229
- mutatingFix,
230
- maxRetryPresent,
231
- implementHeavy,
232
- refuse: true,
233
- advisory: false,
234
- message: PIPELINE_DRIVING_REFUSE_MESSAGE,
235
- exitCode: 2,
236
- };
237
- }
238
-
239
- if (mutatingFix && !maxRetryPresent) {
240
- return {
241
- pipelineDriving,
242
- mutatingFix,
243
- maxRetryPresent,
244
- implementHeavy,
245
- refuse: true,
246
- advisory: false,
247
- message: MUTATING_FIX_REFUSE_MESSAGE,
248
- exitCode: 2,
249
- };
250
- }
251
-
252
- if (implementHeavy) {
253
- return {
254
- pipelineDriving,
255
- mutatingFix,
256
- maxRetryPresent,
257
- implementHeavy,
258
- refuse: false,
259
- advisory: true,
260
- message: IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
261
- exitCode: 0,
262
- };
263
- }
264
-
265
- return {
266
- pipelineDriving,
267
- mutatingFix,
268
- maxRetryPresent,
269
- implementHeavy,
270
- refuse: false,
271
- advisory: false,
272
- message: null,
273
- exitCode: 0,
274
- };
275
- }
276
18
 
277
- export { PIPELINE_TOKENS };
19
+ import { evaluateDogfoodGate } from '../../lib/dogfood-pipeline';
278
20
 
279
- // ── CLI entry (live Phase 1.0 gate) ──────────────────────────────────────────
21
+ export * from '../../lib/dogfood-pipeline';
280
22
 
281
23
  export interface CliArgs {
282
24
  testee: string | null;
@@ -4,6 +4,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // plugins/sp/scripts/dogfood-testing/validate-report.ts
6
6
  import { readFileSync } from "node:fs";
7
+
8
+ // plugins/sp/lib/dogfood-report.ts
7
9
  var REQUIRED_SECTIONS = [1, 2, 3, 4, 5, 6];
8
10
  var CANONICAL_PROTOCOL = "sp:dogfood-testing@1.2";
9
11
  function countSectionHeadings(markdown, section) {
@@ -188,6 +190,7 @@ function validateReport(markdown) {
188
190
  validateCostEvidence(markdown, errors);
189
191
  return { ok: errors.length === 0, errors };
190
192
  }
193
+ // plugins/sp/scripts/dogfood-testing/validate-report.ts
191
194
  function parseValidateCliArgs(argv) {
192
195
  let file = null;
193
196
  let json = false;