@gobing-ai/spur 0.4.1 → 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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/maker-liveness.yaml +39 -0
  4. package/config/plugin-scripts.json +0 -4
  5. package/config/rules/README.md +1 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  7. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  8. package/config/script-placement-baseline.json +1 -19
  9. package/config/templates/AGENTS.md +1 -1
  10. package/config/workflows/feature-lifecycle.yaml +46 -1
  11. package/config/workflows/feature-verification.yaml +11 -0
  12. package/config/workflows/wrapup-pipeline.yaml +1 -1
  13. package/package.json +10 -10
  14. package/plugins/sp/README.md +23 -18
  15. package/plugins/sp/agents/super-planner.md +5 -6
  16. package/plugins/sp/agents/super-reviewer.md +43 -0
  17. package/plugins/sp/bunfig.toml +8 -5
  18. package/plugins/sp/commands/dev-review-session.md +2 -1
  19. package/plugins/sp/commands/dev-runall.md +18 -9
  20. package/plugins/sp/hooks/careful-guard.ts +1 -1
  21. package/plugins/sp/hooks/context-post-tool.ts +7 -404
  22. package/plugins/sp/hooks/context-session-start.ts +6 -192
  23. package/plugins/sp/hooks/context-session-stop.ts +6 -91
  24. package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
  25. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  26. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  27. package/plugins/sp/lib/context-owner.ts +56 -0
  28. package/plugins/sp/lib/context-post-tool.ts +432 -0
  29. package/plugins/sp/lib/context-session-start.ts +214 -0
  30. package/plugins/sp/lib/context-session-stop.ts +196 -0
  31. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  32. package/plugins/sp/lib/dogfood-report.ts +278 -0
  33. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -4
  34. package/plugins/sp/lib/inline-run-app.ts +40 -0
  35. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  36. package/plugins/sp/lib/inline-run.generated.mjs +1361 -87
  37. package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
  38. package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  40. package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  42. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  43. package/plugins/sp/lib/run-summary-core.ts +34 -370
  44. package/plugins/sp/lib/transcript.ts +37 -200
  45. package/plugins/sp/plugin.json +1 -1
  46. package/plugins/sp/references/roles.md +1 -1
  47. package/plugins/sp/scripts/batch-plan.mjs +740 -1
  48. package/plugins/sp/scripts/batch-plan.ts +48 -3
  49. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  50. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  51. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  52. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  53. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  54. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  55. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  56. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  57. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  58. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  59. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  60. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  61. package/plugins/sp/scripts/inline-run-setup.mjs +158 -120
  62. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  63. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  64. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  65. package/plugins/sp/scripts/quality-gate.mjs +38 -208
  66. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  67. package/plugins/sp/scripts/residual-scan.ts +8 -5
  68. package/plugins/sp/scripts/run-summary.mjs +160 -95
  69. package/plugins/sp/scripts/run-summary.ts +7 -5
  70. package/plugins/sp/scripts/session-timeline.mjs +57 -29
  71. package/plugins/sp/scripts/session-timeline.ts +4 -18
  72. package/plugins/sp/scripts/task-diffstat.mjs +43 -30
  73. package/plugins/sp/scripts/task-diffstat.ts +40 -71
  74. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  75. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  76. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  77. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  78. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  79. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  81. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
  82. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  83. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  84. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  85. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  86. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  87. package/plugins/sp/skills/spur-cli/references/decision.md +40 -8
  88. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  89. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  90. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  91. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  93. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
  95. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  96. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  97. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  98. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  99. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
  100. package/spur.js +89792 -76992
  101. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
  102. package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
  103. package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
  104. package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
  105. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  106. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  107. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  108. package/web/_astro/channel.DmdDxGsq.js +1 -0
  109. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  110. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  111. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  112. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  113. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  114. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  115. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  116. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  117. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  118. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  119. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  120. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  121. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  122. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  123. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  124. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  125. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  126. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  127. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  128. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  129. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  130. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  131. package/web/_astro/index.Do2lLdgo.css +1 -0
  132. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  133. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  134. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  135. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  136. package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
  137. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
  138. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  139. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  140. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  141. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  142. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  143. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  144. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  145. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  146. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  147. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  148. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  149. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  150. package/web/index.html +2 -2
  151. package/config/proportional-route-table.ts +0 -155
  152. package/plugins/sp/commands/dev-parallel.md +0 -37
  153. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  154. package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
  155. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  156. package/web/_astro/channel.DZVfto-r.js +0 -1
  157. package/web/_astro/index.COh_kBxN.css +0 -1
  158. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  159. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -5,8 +5,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
 
6
6
  // plugins/sp/scripts/daily-summary/daily-summary.ts
7
7
  import { spawn, spawnSync } from "child_process";
8
- import { existsSync, mkdirSync, readlinkSync, writeFileSync } from "fs";
9
- import { dirname, isAbsolute, join, resolve } from "path";
8
+ import { existsSync as existsSync2, mkdirSync, readlinkSync, writeFileSync } from "fs";
9
+ import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } from "path";
10
+ import { fileURLToPath as fileURLToPath2 } from "url";
10
11
 
11
12
  // plugins/sp/lib/env.ts
12
13
  function getEnvVar(name, fallback) {
@@ -17,20 +18,54 @@ function getEnvVars() {
17
18
  return process.env;
18
19
  }
19
20
 
20
- // plugins/sp/scripts/daily-summary/logger.ts
21
- var state = { console: true, file: false };
22
- function emit(stream, message) {
23
- if (!state.console)
21
+ // plugins/sp/lib/inline-run-app.ts
22
+ import { existsSync } from "node:fs";
23
+ import { dirname, join, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ function resolveAppEntry(spurBin) {
26
+ const fallback = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
27
+ const candidates = spurBin !== "" ? [spurBin] : [fallback];
28
+ for (const candidate of candidates) {
29
+ const tokens = candidate.split(/\s+/).filter(Boolean);
30
+ const mainModule = [...tokens].reverse().find((t) => t.endsWith(".ts"));
31
+ if (mainModule === undefined || !existsSync(mainModule))
32
+ continue;
33
+ const repoRoot = resolve(dirname(mainModule), "..", "..", "..");
34
+ const appEntry = join(repoRoot, "packages", "app", "src", "index.ts");
35
+ if (existsSync(appEntry))
36
+ return { entry: appEntry, portable: false };
37
+ }
38
+ const entry = fileURLToPath(new URL("../lib/inline-run.generated.mjs", import.meta.url));
39
+ if (!existsSync(entry)) {
40
+ throw new Error("inline application bundle is missing — rebuild/install the sp plugin before running inline");
41
+ }
42
+ return { entry, portable: true };
43
+ }
44
+ async function loadInlineApp(spurBin, resolveAppEntry2, required) {
45
+ const { entry, portable } = resolveAppEntry2(spurBin);
46
+ const app = await import(entry);
47
+ const missing = portable ? required.filter((name) => typeof app[name] !== "function") : [];
48
+ if (missing.length > 0) {
49
+ 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.`);
50
+ }
51
+ return { app, portable };
52
+ }
53
+
54
+ // plugins/sp/scripts/daily-summary/daily-summary.ts
55
+ var consoleEnabled = true;
56
+ function enableLogger(consoleOn) {
57
+ consoleEnabled = consoleOn;
58
+ }
59
+ function logAt(stream, message) {
60
+ if (!consoleEnabled)
24
61
  return;
25
62
  console[stream](message);
26
63
  }
27
64
  var logger = {
28
- info: (message) => emit("log", message),
29
- warn: (message) => emit("warn", message),
30
- error: (message) => emit("error", message)
65
+ info: (message) => logAt("log", message),
66
+ warn: (message) => logAt("warn", message),
67
+ error: (message) => logAt("error", message)
31
68
  };
32
-
33
- // plugins/sp/scripts/daily-summary/daily-summary.ts
34
69
  var DAILY_DIR = "docs/daily";
35
70
  var DEFAULT_DATE = "today";
36
71
  function parseArgs(argv = process.argv.slice(2)) {
@@ -108,7 +143,7 @@ function getDateRange(dateStr) {
108
143
  return { start, end };
109
144
  }
110
145
  var defaultProcessSpawner = (cmd, args, env) => {
111
- return new Promise((resolve2, reject) => {
146
+ return new Promise((resolve3, reject) => {
112
147
  try {
113
148
  const proc = spawn(cmd, args, {
114
149
  stdio: ["ignore", "pipe", "pipe"],
@@ -119,7 +154,7 @@ var defaultProcessSpawner = (cmd, args, env) => {
119
154
  proc.stdout.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
120
155
  proc.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
121
156
  proc.on("close", (exitCode) => {
122
- resolve2({
157
+ resolve3({
123
158
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
124
159
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
125
160
  exitCode: exitCode ?? 0
@@ -158,72 +193,49 @@ async function getCcusageData(date) {
158
193
  return null;
159
194
  }
160
195
  }
196
+ function composeHistoryHealth(reading) {
197
+ const loops = reading.loops.map((row) => ({
198
+ toolName: row.toolName,
199
+ argsDigest: row.argsDigest || "repeated execution",
200
+ repeats: row.repeats,
201
+ sessionId: row.sessionId,
202
+ fromSeq: row.firstSeq,
203
+ toSeq: row.lastSeq,
204
+ wastedTokens: row.repeats * 250
205
+ }));
206
+ const remediationProposals = [];
207
+ for (const lp of loops.slice(0, 5)) {
208
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
209
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
210
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
211
+ const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
212
+ remediationProposals.push({ key, title, command });
213
+ }
214
+ if (reading.toolCalls > 0 && reading.errorRatePct > 10) {
215
+ remediationProposals.push({
216
+ key: "reliability:tooling:high-error-rate",
217
+ title: `Investigate high tool error rate (${reading.errorRatePct.toFixed(1)}%)`,
218
+ command: `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`
219
+ });
220
+ }
221
+ return {
222
+ toolCalls: reading.toolCalls,
223
+ toolErrors: reading.toolErrors,
224
+ errorRatePct: reading.errorRatePct,
225
+ loops,
226
+ redundantCalls: loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0),
227
+ wastedTokens: loops.reduce((acc, l) => acc + l.wastedTokens, 0),
228
+ remediationProposals
229
+ };
230
+ }
161
231
  async function getSpurHistoryHealth(date, dbPath = ".spur/spur.db") {
162
232
  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
- }
233
+ const resolvedPath = resolve2(process.cwd(), dbPath);
234
+ const { app } = await loadInlineApp(getEnvVar("SPUR_BIN") ?? "", resolveAppEntry, [
235
+ "readHistoryHealth"
236
+ ]);
237
+ const reading = await app.readHistoryHealth({ date, dbPath: resolvedPath });
238
+ return reading === null ? null : composeHistoryHealth(reading);
227
239
  } catch {
228
240
  return null;
229
241
  }
@@ -320,9 +332,9 @@ Please provide the following (press Enter to skip):
320
332
  input: process.stdin,
321
333
  output: process.stdout
322
334
  });
323
- const question = (prompt) => new Promise((resolve2) => {
335
+ const question = (prompt) => new Promise((resolve3) => {
324
336
  rl.question(prompt, (answer) => {
325
- resolve2(answer.trim());
337
+ resolve3(answer.trim());
326
338
  });
327
339
  });
328
340
  const learnings = await question(`1. What did you learn today? (optional)
@@ -468,26 +480,26 @@ function generateMarkdown(summary) {
468
480
  `);
469
481
  }
470
482
  function ensureDir(path) {
471
- if (!existsSync(path)) {
483
+ if (!existsSync2(path)) {
472
484
  mkdirSync(path, { recursive: true });
473
485
  }
474
486
  }
475
487
  function writeSummary(markdown, options) {
476
488
  const filename = `summary_${options.date.replace(/-/g, "")}.md`;
477
- const outputPath = options.outputPath || join(DAILY_DIR, filename);
478
- ensureDir(join(outputPath, ".."));
489
+ const outputPath = options.outputPath || join2(DAILY_DIR, filename);
490
+ ensureDir(join2(outputPath, ".."));
479
491
  writeFileSync(outputPath, markdown, "utf-8");
480
492
  return outputPath;
481
493
  }
482
494
  function resolveHistoryReportPath() {
483
- const pointer = resolve(process.cwd(), ".spur", "reports", "history", "latest.json");
484
- if (!existsSync(pointer)) {
495
+ const pointer = resolve2(process.cwd(), ".spur", "reports", "history", "latest.json");
496
+ if (!existsSync2(pointer)) {
485
497
  return;
486
498
  }
487
499
  try {
488
500
  const target = readlinkSync(pointer);
489
- const resolved = isAbsolute(target) ? target : resolve(dirname(pointer), target);
490
- return existsSync(resolved) ? resolved : undefined;
501
+ const resolved = isAbsolute(target) ? target : resolve2(dirname2(pointer), target);
502
+ return existsSync2(resolved) ? resolved : undefined;
491
503
  } catch {
492
504
  return;
493
505
  }
@@ -540,13 +552,21 @@ async function buildDailySummary(options) {
540
552
  result.gitActivity = gitActivity;
541
553
  }
542
554
  const historyHealth = await getSpurHistoryHealth(options.date);
543
- if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
555
+ if (historyHealth) {
544
556
  result.historyHealth = historyHealth;
545
557
  platforms.push("Spur History");
546
558
  }
547
559
  return result;
548
560
  }
549
561
  async function main() {
562
+ if (!process.versions.bun) {
563
+ const child = spawnSync("bun", [fileURLToPath2(import.meta.url), ...process.argv.slice(2)], {
564
+ stdio: "inherit"
565
+ });
566
+ if (!child.error)
567
+ process.exit(child.status ?? 1);
568
+ console.error(`daily-summary: Bun not on PATH (${child.error.message}); history health unavailable`);
569
+ }
550
570
  const options = parseArgs();
551
571
  logger.info(`Generating daily summary for ${options.date}...`);
552
572
  try {
@@ -602,6 +622,7 @@ export {
602
622
  getCcusageData,
603
623
  generateMarkdown,
604
624
  ensureDir,
625
+ enableLogger,
605
626
  defaultProcessSpawner,
606
627
  buildDailySummary
607
628
  };
@@ -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;