@gobing-ai/spur 0.4.2 → 0.4.4

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 (168) 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/docs/verdict-confidence.yaml +68 -0
  7. package/config/rules/quality/coverage-gate.yaml +13 -0
  8. package/config/rules/recommended-pre-check.yaml +6 -2
  9. package/config/rules/strict/runtime-boundaries.yaml +10 -0
  10. package/config/rules/surface/check-cli-surface.yaml +1 -3
  11. package/config/script-placement-baseline.json +1 -19
  12. package/config/templates/AGENTS.md +7 -0
  13. package/config/workflows/feature-lifecycle.yaml +46 -1
  14. package/config/workflows/feature-verification.yaml +20 -1
  15. package/config/workflows/task-lifecycle.yaml +50 -0
  16. package/config/workflows/task-pipeline.yaml +15 -9
  17. package/package.json +10 -10
  18. package/plugins/sp/README.md +22 -16
  19. package/plugins/sp/agents/super-reviewer.md +46 -0
  20. package/plugins/sp/bunfig.toml +8 -5
  21. package/plugins/sp/commands/dev-changelog.md +1 -1
  22. package/plugins/sp/commands/dev-gitmsg.md +6 -2
  23. package/plugins/sp/commands/dev-handover.md +1 -0
  24. package/plugins/sp/commands/dev-job-dump.md +2 -1
  25. package/plugins/sp/commands/dev-refine.md +14 -2
  26. package/plugins/sp/commands/dev-refineall.md +7 -1
  27. package/plugins/sp/commands/dev-review.md +1 -0
  28. package/plugins/sp/hooks/careful-guard.ts +1 -1
  29. package/plugins/sp/hooks/context-post-tool.ts +7 -427
  30. package/plugins/sp/hooks/context-session-start.ts +5 -213
  31. package/plugins/sp/hooks/context-session-stop.ts +5 -191
  32. package/plugins/sp/hooks/pi/guard-extension.ts +6 -5
  33. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  34. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  35. package/plugins/sp/lib/context-post-tool.ts +432 -0
  36. package/plugins/sp/lib/context-session-start.ts +214 -0
  37. package/plugins/sp/lib/context-session-stop.ts +196 -0
  38. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  39. package/plugins/sp/lib/dogfood-report.ts +278 -0
  40. package/plugins/sp/lib/history-anatomy.generated.mjs +18 -18
  41. package/plugins/sp/lib/idea-handoff.generated.mjs +91 -91
  42. package/plugins/sp/lib/inline-run-app.ts +40 -0
  43. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  44. package/plugins/sp/lib/inline-run.generated.mjs +1425 -212
  45. package/plugins/sp/lib/plan-projection.generated.d.mts +150 -0
  46. package/plugins/sp/lib/plan-projection.generated.mjs +623 -9
  47. package/plugins/sp/lib/quality-gate.generated.d.mts +1 -0
  48. package/plugins/sp/lib/quality-gate.generated.mjs +13809 -13969
  49. package/plugins/sp/lib/residual-scan.generated.d.mts +20 -1
  50. package/plugins/sp/lib/residual-scan.generated.mjs +120 -25
  51. package/plugins/sp/lib/run-summary-core.ts +34 -370
  52. package/plugins/sp/lib/step-profile.generated.mjs +8 -8
  53. package/plugins/sp/lib/transcript.ts +37 -228
  54. package/plugins/sp/plugin.json +1 -1
  55. package/plugins/sp/scripts/batch-plan.mjs +619 -11
  56. package/plugins/sp/scripts/batch-preflight.mjs +57 -13
  57. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  58. package/plugins/sp/scripts/commit-guard.mjs +6 -6
  59. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +127 -108
  60. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  61. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +13 -12
  62. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  63. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +7 -4
  64. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  65. package/plugins/sp/scripts/feature-verification-steps.mjs +20 -125
  66. package/plugins/sp/scripts/feature-verification-steps.ts +57 -181
  67. package/plugins/sp/scripts/history-anatomy-cache.mjs +19 -19
  68. package/plugins/sp/scripts/idea-handoff.mjs +2 -2
  69. package/plugins/sp/scripts/inline-run-dispatch.mjs +25 -24
  70. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  71. package/plugins/sp/scripts/inline-run-setup.mjs +164 -137
  72. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  73. package/plugins/sp/scripts/persist-out-check.mjs +5 -5
  74. package/plugins/sp/scripts/pr-reviewing.mjs +13 -13
  75. package/plugins/sp/scripts/quality-gate.mjs +13730 -13887
  76. package/plugins/sp/scripts/residual-scan.mjs +129 -31
  77. package/plugins/sp/scripts/residual-scan.ts +16 -13
  78. package/plugins/sp/scripts/run-summary.mjs +166 -112
  79. package/plugins/sp/scripts/run-summary.ts +7 -5
  80. package/plugins/sp/scripts/script-root.mjs +6 -6
  81. package/plugins/sp/scripts/session-timeline.mjs +62 -35
  82. package/plugins/sp/scripts/task-diffstat.mjs +19 -9
  83. package/plugins/sp/scripts/task-diffstat.ts +8 -39
  84. package/plugins/sp/scripts/workflow-step-profile.mjs +12 -12
  85. package/plugins/sp/scripts/wrapup-drift-probe.mjs +9 -9
  86. package/plugins/sp/scripts/wrapup-steps.mjs +25 -25
  87. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  88. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  89. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  90. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  91. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  92. package/plugins/sp/skills/next-router/SKILL.md +3 -1
  93. package/plugins/sp/skills/next-router/references/routing-table.md +9 -3
  94. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  95. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  96. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  97. package/plugins/sp/skills/spur-cli/references/decision.md +32 -7
  98. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  99. package/plugins/sp/skills/spur-cli/references/tasks.md +24 -4
  100. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +8 -1
  101. package/plugins/sp/skills/spur-dev/references/dev-operations.md +37 -11
  102. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +1 -1
  103. package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -6
  104. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  105. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  106. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +4 -2
  107. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +49 -12
  108. package/schemas/section-matrix.schema.json +3 -2
  109. package/spur.js +66591 -73262
  110. package/web/_astro/{BoardApp.B3GQZvj8.js → BoardApp.Niubpe6z.js} +1 -1
  111. package/web/_astro/{BoardApp.BT7L2kSb.js → BoardApp.hF0RAIrg.js} +150 -150
  112. package/web/_astro/TaskDetail.DJpapt38.js +1 -0
  113. package/web/_astro/{arc.vaT7tPOB.js → arc.BgKcK0i8.js} +1 -1
  114. package/web/_astro/{architectureDiagram-3BPJPVTR.DRMmTjkN.js → architectureDiagram-3BPJPVTR.ChP-Ig0V.js} +1 -1
  115. package/web/_astro/{blockDiagram-GPEHLZMM.cla0LO_q.js → blockDiagram-GPEHLZMM.BVaCBoeN.js} +1 -1
  116. package/web/_astro/{c4Diagram-AAUBKEIU.D4YAIBKn.js → c4Diagram-AAUBKEIU.CHSDpEO5.js} +1 -1
  117. package/web/_astro/channel.DkpvN2tD.js +1 -0
  118. package/web/_astro/{chunk-2J33WTMH.BrSJbMRZ.js → chunk-2J33WTMH.MYRcYVRa.js} +1 -1
  119. package/web/_astro/{chunk-4BX2VUAB.CuLMgKF5.js → chunk-4BX2VUAB.S5j22Zqa.js} +1 -1
  120. package/web/_astro/{chunk-55IACEB6.zkvgu54F.js → chunk-55IACEB6.gciRPvmZ.js} +1 -1
  121. package/web/_astro/{chunk-727SXJPM.DxWkACh8.js → chunk-727SXJPM.CdqrSQkX.js} +1 -1
  122. package/web/_astro/{chunk-AQP2D5EJ.42XrmIlH.js → chunk-AQP2D5EJ.CyokdHT3.js} +1 -1
  123. package/web/_astro/{chunk-FMBD7UC4.Bao2wMK9.js → chunk-FMBD7UC4.Bg9KJhjW.js} +1 -1
  124. package/web/_astro/{chunk-ND2GUHAM.BhydojD1.js → chunk-ND2GUHAM.1gxxI-vD.js} +1 -1
  125. package/web/_astro/{chunk-QZHKN3VN.CmZ82g-H.js → chunk-QZHKN3VN.DDEYtCKU.js} +1 -1
  126. package/web/_astro/{classDiagram-4FO5ZUOK.Blr83Gt_.js → classDiagram-4FO5ZUOK.Ch3zi37T.js} +1 -1
  127. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Blr83Gt_.js → classDiagram-v2-Q7XG4LA2.Ch3zi37T.js} +1 -1
  128. package/web/_astro/{cose-bilkent-S5V4N54A.qmfv0SPn.js → cose-bilkent-S5V4N54A.D2q5AINI.js} +1 -1
  129. package/web/_astro/{cynefin-OW5HDTMX.BtELhXPR.js → cynefin-OW5HDTMX.BA9kC2YT.js} +1 -1
  130. package/web/_astro/{dagre-BM42HDAG.AWmaAvXo.js → dagre-BM42HDAG.CHO08nRp.js} +1 -1
  131. package/web/_astro/{diagram-2AECGRRQ.DdH34KDi.js → diagram-2AECGRRQ.BfFQt0LW.js} +1 -1
  132. package/web/_astro/{diagram-5GNKFQAL.BmNPix1k.js → diagram-5GNKFQAL.CHMwMTe4.js} +1 -1
  133. package/web/_astro/{diagram-KO2AKTUF.BNQd_Ppo.js → diagram-KO2AKTUF.BJ-7gK-w.js} +1 -1
  134. package/web/_astro/{diagram-LMA3HP47.BAPgDi0e.js → diagram-LMA3HP47.C5xqcc0t.js} +1 -1
  135. package/web/_astro/{diagram-OG6HWLK6.DZ4zyLdC.js → diagram-OG6HWLK6.C2pQjfrC.js} +1 -1
  136. package/web/_astro/{erDiagram-TEJ5UH35.Q1TySMmw.js → erDiagram-TEJ5UH35._FoNyxgC.js} +1 -1
  137. package/web/_astro/{flowDiagram-I6XJVG4X.BXhFBicG.js → flowDiagram-I6XJVG4X.B_ENkT2I.js} +1 -1
  138. package/web/_astro/{ganttDiagram-6RSMTGT7.vHSgcuo2.js → ganttDiagram-6RSMTGT7.B3q8h77C.js} +1 -1
  139. package/web/_astro/{gitGraphDiagram-PVQCEYII.Bi1f73C7.js → gitGraphDiagram-PVQCEYII.QWZFDboZ.js} +1 -1
  140. package/web/_astro/index.BFyW13JJ.css +1 -0
  141. package/web/_astro/{infoDiagram-5YYISTIA.DPvXZEWm.js → infoDiagram-5YYISTIA.Cs6eEc92.js} +1 -1
  142. package/web/_astro/{ishikawaDiagram-YF4QCWOH.Cz829fsV.js → ishikawaDiagram-YF4QCWOH.BU9DJMHA.js} +1 -1
  143. package/web/_astro/{journeyDiagram-JHISSGLW.enRnB9Me.js → journeyDiagram-JHISSGLW.D35k6Jpe.js} +1 -1
  144. package/web/_astro/{kanban-definition-UN3LZRKU.DnTNk6d2.js → kanban-definition-UN3LZRKU.DBqcAT4m.js} +1 -1
  145. package/web/_astro/{linear.BzGnBwNL.js → linear.D-LSNK8W.js} +1 -1
  146. package/web/_astro/{mermaid.core.CSbTpIVz.js → mermaid.core.CSruWVOa.js} +4 -4
  147. package/web/_astro/{mindmap-definition-RKZ34NQL.DGJDy8zT.js → mindmap-definition-RKZ34NQL.BiFaUsfj.js} +1 -1
  148. package/web/_astro/{pieDiagram-4H26LBE5.HSPdi5_8.js → pieDiagram-4H26LBE5.D1KTKqb9.js} +1 -1
  149. package/web/_astro/{quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js → quadrantDiagram-W4KKPZXB.DBHflsMG.js} +1 -1
  150. package/web/_astro/{requirementDiagram-4Y6WPE33.CnBCtFwa.js → requirementDiagram-4Y6WPE33.hwX9CNdk.js} +1 -1
  151. package/web/_astro/{sankeyDiagram-5OEKKPKP.5M8VWAu8.js → sankeyDiagram-5OEKKPKP.DkI8mJiE.js} +1 -1
  152. package/web/_astro/{sequenceDiagram-3UESZ5HK.DN41XN-U.js → sequenceDiagram-3UESZ5HK.DMz7jBDx.js} +1 -1
  153. package/web/_astro/{stateDiagram-AJRCARHV.ErOO3W8j.js → stateDiagram-AJRCARHV.B6Ef2xJ1.js} +1 -1
  154. package/web/_astro/{stateDiagram-v2-BHNVJYJU.B_ZHICM-.js → stateDiagram-v2-BHNVJYJU.CgoaufB9.js} +1 -1
  155. package/web/_astro/{timeline-definition-PNZ67QCA.CWdosr07.js → timeline-definition-PNZ67QCA.B3c8h1t-.js} +1 -1
  156. package/web/_astro/{vennDiagram-CIIHVFJN.BHzYjNV5.js → vennDiagram-CIIHVFJN.BdHPAxAb.js} +1 -1
  157. package/web/_astro/{wardleyDiagram-YWT4CUSO.h8x4IwPX.js → wardleyDiagram-YWT4CUSO.BrJXPPAj.js} +1 -1
  158. package/web/_astro/{xychartDiagram-2RQKCTM6.RTapW2X8.js → xychartDiagram-2RQKCTM6.CMH5yFsV.js} +1 -1
  159. package/web/index.html +2 -2
  160. package/web/spur_logo.svg +1 -1
  161. package/config/proportional-route-table.ts +0 -155
  162. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  163. package/web/_astro/TaskDetail.Cxov1_N_.js +0 -1
  164. package/web/_astro/channel.D4nG9YLf.js +0 -1
  165. package/web/_astro/index.BhSwN8yY.css +0 -1
  166. /package/plugins/sp/{hooks → lib}/context-owner.ts +0 -0
  167. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  168. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
- import { createRequire } from "node:module";
4
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
3
 
6
4
  // plugins/sp/scripts/daily-summary/daily-summary.ts
7
5
  import { spawn, spawnSync } from "child_process";
8
- import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
9
- import { dirname, isAbsolute, join, resolve } from "path";
6
+ import { existsSync as existsSync2, mkdirSync, readlinkSync, writeFileSync } from "fs";
7
+ import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
10
9
 
11
10
  // plugins/sp/lib/env.ts
12
11
  function getEnvVar(name, fallback) {
@@ -17,20 +16,54 @@ function getEnvVars() {
17
16
  return process.env;
18
17
  }
19
18
 
20
- // plugins/sp/scripts/daily-summary/logger.ts
21
- var state = { console: true, file: false };
22
- function emit(stream, message) {
23
- if (!state.console)
19
+ // plugins/sp/lib/inline-run-app.ts
20
+ import { existsSync } from "node:fs";
21
+ import { dirname, join, resolve } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ function resolveAppEntry(spurBin) {
24
+ const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
25
+ const candidates = spurBin !== "" ? [spurBin] : [fallback];
26
+ for (const candidate of candidates) {
27
+ const tokens = candidate.split(/\s+/).filter(Boolean);
28
+ const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
29
+ if (mainModule === undefined || !existsSync(mainModule))
30
+ continue;
31
+ const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
32
+ const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
33
+ if (existsSync(appEntry))
34
+ return { entry: appEntry, portable: false };
35
+ }
36
+ const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
37
+ if (!existsSync(entry)) {
38
+ throw new Error("inline application bundle is missing — rebuild/install the sp plugin before running inline");
39
+ }
40
+ return { entry, portable: true };
41
+ }
42
+ async function loadInlineApp(spurBin, resolveAppEntry, required) {
43
+ const { entry, portable } = resolveAppEntry(spurBin);
44
+ const app = await import(entry);
45
+ const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
46
+ if (missing.length > 0) {
47
+ throw new Error(`inline application bundle ${entry} is older than its scripts — missing ${missing.join(", ")}. Reinstall sp with a superskill release that stages lib/ (commit b42961b), or pass --spur-bin <spur checkout>/apps/cli/src/index.ts.`);
48
+ }
49
+ return { app, portable };
50
+ }
51
+
52
+ // plugins/sp/scripts/daily-summary/daily-summary.ts
53
+ var consoleEnabled = true;
54
+ function enableLogger(consoleOn) {
55
+ consoleEnabled = consoleOn;
56
+ }
57
+ function logAt(stream, message) {
58
+ if (!consoleEnabled)
24
59
  return;
25
60
  console[stream](message);
26
61
  }
27
62
  var logger = {
28
- info: (message) => emit("log", message),
29
- warn: (message) => emit("warn", message),
30
- error: (message) => emit("error", message)
63
+ info: (message) => logAt("log", message),
64
+ warn: (message) => logAt("warn", message),
65
+ error: (message) => logAt("error", message)
31
66
  };
32
-
33
- // plugins/sp/scripts/daily-summary/daily-summary.ts
34
67
  var DAILY_DIR = "docs/daily";
35
68
  var DEFAULT_DATE = "today";
36
69
  function parseArgs(argv = process.argv.slice(2)) {
@@ -108,7 +141,7 @@ function getDateRange(dateStr) {
108
141
  return { start, end };
109
142
  }
110
143
  var defaultProcessSpawner = (cmd, args, env) => {
111
- return new Promise((resolve2, reject) => {
144
+ return new Promise((resolve, reject) => {
112
145
  try {
113
146
  const proc = spawn(cmd, args, {
114
147
  stdio: ["ignore", "pipe", "pipe"],
@@ -119,7 +152,7 @@ var defaultProcessSpawner = (cmd, args, env) => {
119
152
  proc.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
120
153
  proc.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
121
154
  proc.on("close", (exitCode) => {
122
- resolve2({
155
+ resolve({
123
156
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
124
157
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
125
158
  exitCode: exitCode ?? 0
@@ -158,72 +191,49 @@ async function getCcusageData(date) {
158
191
  return null;
159
192
  }
160
193
  }
194
+ function composeHistoryHealth(reading) {
195
+ const loops = reading.loops.map((row) => ({
196
+ toolName: row.toolName,
197
+ argsDigest: row.argsDigest || "repeated execution",
198
+ repeats: row.repeats,
199
+ sessionId: row.sessionId,
200
+ fromSeq: row.firstSeq,
201
+ toSeq: row.lastSeq,
202
+ wastedTokens: row.repeats * 250
203
+ }));
204
+ const remediationProposals = [];
205
+ for (const lp of loops.slice(0, 5)) {
206
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
207
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
208
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
209
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
210
+ remediationProposals.push({ key, title, command });
211
+ }
212
+ if (reading.toolCalls > 0 && reading.errorRatePct > 10) {
213
+ remediationProposals.push({
214
+ key: "reliability:tooling:high-error-rate",
215
+ title: `Investigate high tool error rate (${reading.errorRatePct.toFixed(1)}%)`,
216
+ command: `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`
217
+ });
218
+ }
219
+ return {
220
+ toolCalls: reading.toolCalls,
221
+ toolErrors: reading.toolErrors,
222
+ errorRatePct: reading.errorRatePct,
223
+ loops,
224
+ redundantCalls: loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0),
225
+ wastedTokens: loops.reduce((acc, l) => acc + l.wastedTokens, 0),
226
+ remediationProposals
227
+ };
228
+ }
161
229
  async function getSpurHistoryHealth(date, dbPath = ".spur/spur.db") {
162
230
  try {
163
- const resolvedPath = resolve(process.cwd(), dbPath);
164
- if (!existsSync(resolvedPath)) {
165
- return null;
166
- }
167
- const { Database } = await import("bun:sqlite");
168
- const db = new Database(resolvedPath, { readonly: true });
169
- try {
170
- const loopTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_loop_findings");
171
- let loops = [];
172
- if (loopTable) {
173
- const rows = db.query(`SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
174
- FROM history_board_loop_findings
175
- WHERE started_at IS NULL OR started_at LIKE ?
176
- ORDER BY repeats DESC
177
- LIMIT 20`).all(`${date}%`);
178
- loops = rows.map((r) => ({
179
- toolName: r.tool_name,
180
- argsDigest: r.args_digest || "repeated execution",
181
- repeats: r.repeats,
182
- sessionId: r.session_id,
183
- fromSeq: r.first_seq,
184
- toSeq: r.last_seq,
185
- wastedTokens: r.repeats * 250
186
- }));
187
- }
188
- let toolCalls = 0;
189
- let toolErrors = 0;
190
- const toolTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_tool_5m");
191
- if (toolTable) {
192
- const stats = db.query(`SELECT SUM(calls) AS calls, SUM(errors) AS errors
193
- FROM history_board_tool_5m
194
- WHERE bucket_start LIKE ?`).get(`${date}%`);
195
- toolCalls = stats?.calls ?? 0;
196
- toolErrors = stats?.errors ?? 0;
197
- }
198
- const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
199
- const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
200
- const errorRatePct = toolCalls > 0 ? toolErrors / toolCalls * 100 : 0;
201
- const remediationProposals = [];
202
- for (const lp of loops.slice(0, 5)) {
203
- const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
204
- const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
205
- const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
206
- const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
207
- remediationProposals.push({ key, title, command });
208
- }
209
- if (toolCalls > 0 && errorRatePct > 10) {
210
- const key = "reliability:tooling:high-error-rate";
211
- const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
212
- const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
213
- remediationProposals.push({ key, title, command });
214
- }
215
- return {
216
- toolCalls,
217
- toolErrors,
218
- errorRatePct,
219
- loops,
220
- redundantCalls,
221
- wastedTokens,
222
- remediationProposals
223
- };
224
- } finally {
225
- db.close();
226
- }
231
+ const resolvedPath = resolve2(process.cwd(), dbPath);
232
+ const { app } = await loadInlineApp(getEnvVar("SPUR_BIN") ?? "", resolveAppEntry, [
233
+ "readHistoryHealth"
234
+ ]);
235
+ const reading = await app.readHistoryHealth({ date, dbPath: resolvedPath });
236
+ return reading === null ? null : composeHistoryHealth(reading);
227
237
  } catch {
228
238
  return null;
229
239
  }
@@ -307,12 +317,12 @@ Please provide the following (press Enter to skip):
307
317
  chunks.push(chunk);
308
318
  }
309
319
  const buffered = Buffer.concat(chunks).toString("utf-8");
310
- const [learnings2 = "", issuesFixed2 = "", pending2 = ""] = buffered.split(`
320
+ const [learnings = "", issuesFixed = "", pending = ""] = buffered.split(`
311
321
  `);
312
322
  return {
313
- learnings: learnings2.trim(),
314
- issuesFixed: issuesFixed2.trim(),
315
- pending: pending2.trim()
323
+ learnings: learnings.trim(),
324
+ issuesFixed: issuesFixed.trim(),
325
+ pending: pending.trim()
316
326
  };
317
327
  }
318
328
  const readline = await import("readline");
@@ -320,9 +330,9 @@ Please provide the following (press Enter to skip):
320
330
  input: process.stdin,
321
331
  output: process.stdout
322
332
  });
323
- const question = (prompt) => new Promise((resolve2) => {
333
+ const question = (prompt) => new Promise((resolve) => {
324
334
  rl.question(prompt, (answer) => {
325
- resolve2(answer.trim());
335
+ resolve(answer.trim());
326
336
  });
327
337
  });
328
338
  const learnings = await question(`1. What did you learn today? (optional)
@@ -468,26 +478,26 @@ function generateMarkdown(summary) {
468
478
  `);
469
479
  }
470
480
  function ensureDir(path) {
471
- if (!existsSync(path)) {
481
+ if (!existsSync2(path)) {
472
482
  mkdirSync(path, { recursive: true });
473
483
  }
474
484
  }
475
485
  function writeSummary(markdown, options) {
476
486
  const filename = `summary_${options.date.replace(/-/g, "")}.md`;
477
- const outputPath = options.outputPath || join(DAILY_DIR, filename);
478
- ensureDir(join(outputPath, ".."));
487
+ const outputPath = options.outputPath || join2(DAILY_DIR, filename);
488
+ ensureDir(join2(outputPath, ".."));
479
489
  writeFileSync(outputPath, markdown, "utf-8");
480
490
  return outputPath;
481
491
  }
482
492
  function resolveHistoryReportPath() {
483
- const pointer = resolve(process.cwd(), ".spur", "reports", "history", "latest.json");
484
- if (!existsSync(pointer)) {
493
+ const pointer = resolve2(process.cwd(), ".spur", "reports", "history", "latest.json");
494
+ if (!existsSync2(pointer)) {
485
495
  return;
486
496
  }
487
497
  try {
488
498
  const target = readlinkSync(pointer);
489
- const resolved = isAbsolute(target) ? target : resolve(dirname(pointer), target);
490
- return existsSync(resolved) ? resolved : undefined;
499
+ const resolved = isAbsolute(target) ? target : resolve2(dirname2(pointer), target);
500
+ return existsSync2(resolved) ? resolved : undefined;
491
501
  } catch {
492
502
  return;
493
503
  }
@@ -540,13 +550,21 @@ async function buildDailySummary(options) {
540
550
  result.gitActivity = gitActivity;
541
551
  }
542
552
  const historyHealth = await getSpurHistoryHealth(options.date);
543
- if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
553
+ if (historyHealth) {
544
554
  result.historyHealth = historyHealth;
545
555
  platforms.push("Spur History");
546
556
  }
547
557
  return result;
548
558
  }
549
559
  async function main() {
560
+ if (!process.versions.bun) {
561
+ const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
562
+ stdio: "inherit"
563
+ });
564
+ if (!child.error)
565
+ process.exit(child.status ?? 1);
566
+ console.error(`daily-summary: Bun not on PATH (${child.error.message}); history health unavailable`);
567
+ }
550
568
  const options = parseArgs();
551
569
  logger.info(`Generating daily summary for ${options.date}...`);
552
570
  try {
@@ -588,20 +606,21 @@ ${markdown}
588
606
  });
589
607
  }
590
608
  export {
591
- yesterdayLocal,
592
- writeSummary,
593
- todayLocal,
594
- setProcessSpawner,
595
- promptUser,
596
- printUsage,
597
- parseArgs,
598
- main,
599
- getSpurHistoryHealth,
600
- getGitCommits,
601
- getDateRange,
602
- getCcusageData,
603
- generateMarkdown,
604
- ensureDir,
609
+ buildDailySummary,
605
610
  defaultProcessSpawner,
606
- buildDailySummary
611
+ enableLogger,
612
+ ensureDir,
613
+ generateMarkdown,
614
+ getCcusageData,
615
+ getDateRange,
616
+ getGitCommits,
617
+ getSpurHistoryHealth,
618
+ main,
619
+ parseArgs,
620
+ printUsage,
621
+ promptUser,
622
+ setProcessSpawner,
623
+ todayLocal,
624
+ writeSummary,
625
+ yesterdayLocal
607
626
  };
@@ -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;
@@ -195,16 +196,16 @@ function runCli(argv) {
195
196
  process.exit(exitCode);
196
197
  }
197
198
  export {
198
- runCli,
199
- parseCliArgs,
200
- isImplementHeavyStep,
201
- hasMutatingFixMode,
202
- evaluateDogfoodGate,
203
- detectPipelineDriving,
204
- detectImplementHeavy,
205
- PIPELINE_TOKENS,
206
- PIPELINE_DRIVING_REFUSE_MESSAGE,
207
- MUTATING_FIX_REFUSE_MESSAGE,
199
+ CLI_USAGE,
208
200
  IMPLEMENT_HEAVY_ADVISORY_MESSAGE,
209
- CLI_USAGE
201
+ MUTATING_FIX_REFUSE_MESSAGE,
202
+ PIPELINE_DRIVING_REFUSE_MESSAGE,
203
+ PIPELINE_TOKENS,
204
+ detectImplementHeavy,
205
+ detectPipelineDriving,
206
+ evaluateDogfoodGate,
207
+ hasMutatingFixMode,
208
+ isImplementHeavyStep,
209
+ parseCliArgs,
210
+ runCli
210
211
  };