@gobing-ai/spur 0.3.71 → 0.3.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/package.json +9 -9
  3. package/plugins/sp/plugin.json +1 -1
  4. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +121 -0
  5. package/plugins/sp/scripts/daily-summary/daily-summary.ts +204 -0
  6. package/schemas/spur-config.schema.json +35 -0
  7. package/spur.js +3008 -1468
  8. package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
  9. package/web/_astro/BoardApp.E12MFjOS.js +1 -0
  10. package/web/_astro/{TaskDetail.BvkKvo57.js → TaskDetail.CgUreSP2.js} +1 -1
  11. package/web/_astro/{arc.DuEIzPMi.js → arc.BySSh34M.js} +1 -1
  12. package/web/_astro/{architectureDiagram-3BPJPVTR.D0dUxCjA.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
  13. package/web/_astro/{blockDiagram-GPEHLZMM.Cv60iOpM.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
  14. package/web/_astro/{c4Diagram-AAUBKEIU.Ddx7WGhb.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
  15. package/web/_astro/channel.5cYKr5cs.js +1 -0
  16. package/web/_astro/{chunk-2J33WTMH.BGKzU_1t.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
  17. package/web/_astro/{chunk-4BX2VUAB.FlApjIIH.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
  18. package/web/_astro/{chunk-55IACEB6.CDfmvDeW.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
  19. package/web/_astro/{chunk-727SXJPM.T9bc-xir.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
  20. package/web/_astro/{chunk-AQP2D5EJ.BiTc-MQo.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
  21. package/web/_astro/{chunk-FMBD7UC4.he4KrHni.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
  22. package/web/_astro/{chunk-ND2GUHAM.D05XuUuJ.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
  23. package/web/_astro/{chunk-QZHKN3VN.CA2NThIE.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
  24. package/web/_astro/{classDiagram-4FO5ZUOK.Couj-zYZ.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
  25. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Couj-zYZ.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
  26. package/web/_astro/{cose-bilkent-S5V4N54A.B8YYW7NG.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
  27. package/web/_astro/{cynefin-OW5HDTMX.BExFdiin.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
  28. package/web/_astro/{dagre-BM42HDAG.BybKbz3q.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
  29. package/web/_astro/{diagram-2AECGRRQ.UyRTSl9n.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
  30. package/web/_astro/{diagram-5GNKFQAL.BrxCucBf.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
  31. package/web/_astro/{diagram-KO2AKTUF.CdE5oy5J.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
  32. package/web/_astro/{diagram-LMA3HP47.BJLgdosK.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
  33. package/web/_astro/{diagram-OG6HWLK6.CUynieTU.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
  34. package/web/_astro/{erDiagram-TEJ5UH35.C0vS6DJv.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
  35. package/web/_astro/{flowDiagram-I6XJVG4X.T3QLi_en.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
  36. package/web/_astro/{ganttDiagram-6RSMTGT7.BNsk3w9Z.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
  37. package/web/_astro/{gitGraphDiagram-PVQCEYII.Mwe2I4V6.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
  38. package/web/_astro/index.B5MTfe7k.css +1 -0
  39. package/web/_astro/{infoDiagram-5YYISTIA.BlcjLmtc.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
  40. package/web/_astro/{ishikawaDiagram-YF4QCWOH.js8qeS0h.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
  41. package/web/_astro/{journeyDiagram-JHISSGLW.CM6UK0a4.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
  42. package/web/_astro/{kanban-definition-UN3LZRKU.a6ihOzMd.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
  43. package/web/_astro/{linear.CsIB2jFu.js → linear.D7uqzENp.js} +1 -1
  44. package/web/_astro/{mermaid.core.Br2Fo22q.js → mermaid.core.CxrNppBD.js} +4 -4
  45. package/web/_astro/{mindmap-definition-RKZ34NQL.29inC1Mk.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
  46. package/web/_astro/{pieDiagram-4H26LBE5.C9CxG_Kf.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
  47. package/web/_astro/{quadrantDiagram-W4KKPZXB.6qo9MOJM.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
  48. package/web/_astro/{requirementDiagram-4Y6WPE33.DN07zrP5.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
  49. package/web/_astro/{sankeyDiagram-5OEKKPKP.BSw5o173.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
  50. package/web/_astro/{sequenceDiagram-3UESZ5HK.LJPzySKw.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
  51. package/web/_astro/{stateDiagram-AJRCARHV.ClNjEiKV.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
  52. package/web/_astro/{stateDiagram-v2-BHNVJYJU.c6Z-_WfX.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
  53. package/web/_astro/{timeline-definition-PNZ67QCA.CIMR-87j.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
  54. package/web/_astro/{vennDiagram-CIIHVFJN.BnRSRI9I.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
  55. package/web/_astro/{wardleyDiagram-YWT4CUSO.uN08C3gv.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
  56. package/web/_astro/{xychartDiagram-2RQKCTM6.BjbEvfuq.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
  57. package/web/index.html +2 -2
  58. package/web/_astro/BoardApp.BnjsI80-.js +0 -178
  59. package/web/_astro/BoardApp.SJcrHBZp.js +0 -1
  60. package/web/_astro/channel.tWfETQvX.js +0 -1
  61. package/web/_astro/index.9npdrEIr.css +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.71",
10
+ "version": "0.3.72",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.71",
3
+ "version": "0.3.72",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -53,14 +53,14 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@commander-js/extra-typings": "^14.0.0",
56
- "@gobing-ai/ts-db": "^0.4.50",
57
- "@gobing-ai/ts-ai-runner": "^0.4.50",
58
- "@gobing-ai/ts-dual-workflow-engine": "^0.4.50",
59
- "@gobing-ai/ts-infra": "^0.4.50",
60
- "@gobing-ai/ts-llm-jsonl-importer": "^0.4.50",
61
- "@gobing-ai/ts-rule-engine": "^0.4.50",
62
- "@gobing-ai/ts-runtime": "^0.4.50",
63
- "@gobing-ai/ts-utils": "^0.4.50",
56
+ "@gobing-ai/ts-db": "^0.4.54",
57
+ "@gobing-ai/ts-ai-runner": "^0.4.54",
58
+ "@gobing-ai/ts-dual-workflow-engine": "^0.4.54",
59
+ "@gobing-ai/ts-infra": "^0.4.54",
60
+ "@gobing-ai/ts-llm-jsonl-importer": "^0.4.54",
61
+ "@gobing-ai/ts-rule-engine": "^0.4.54",
62
+ "@gobing-ai/ts-runtime": "^0.4.54",
63
+ "@gobing-ai/ts-utils": "^0.4.54",
64
64
  "@types/bun": "1.3.14",
65
65
  "@types/figlet": "^1.7.0",
66
66
  "@types/node-notifier": "8.0.5",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.71",
3
+ "version": "0.3.72",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -149,6 +149,78 @@ async function getCcusageData(date) {
149
149
  return null;
150
150
  }
151
151
  }
152
+ async function getSpurHistoryHealth(date, dbPath = ".spur/spur.db") {
153
+ try {
154
+ const resolvedPath = resolve(process.cwd(), dbPath);
155
+ if (!existsSync(resolvedPath)) {
156
+ return null;
157
+ }
158
+ const { Database } = await import("bun:sqlite");
159
+ const db = new Database(resolvedPath, { readonly: true });
160
+ try {
161
+ const loopTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_loop_findings");
162
+ let loops = [];
163
+ if (loopTable) {
164
+ const rows = db.query(`SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
165
+ FROM history_board_loop_findings
166
+ WHERE started_at IS NULL OR started_at LIKE ?
167
+ ORDER BY repeats DESC
168
+ LIMIT 20`).all(`${date}%`);
169
+ loops = rows.map((r) => ({
170
+ toolName: r.tool_name,
171
+ argsDigest: r.args_digest || "repeated execution",
172
+ repeats: r.repeats,
173
+ sessionId: r.session_id,
174
+ fromSeq: r.first_seq,
175
+ toSeq: r.last_seq,
176
+ wastedTokens: r.repeats * 250
177
+ }));
178
+ }
179
+ let toolCalls = 0;
180
+ let toolErrors = 0;
181
+ const toolTable = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get("history_board_tool_5m");
182
+ if (toolTable) {
183
+ const stats = db.query(`SELECT SUM(calls) AS calls, SUM(errors) AS errors
184
+ FROM history_board_tool_5m
185
+ WHERE bucket_start LIKE ?`).get(`${date}%`);
186
+ toolCalls = stats?.calls ?? 0;
187
+ toolErrors = stats?.errors ?? 0;
188
+ }
189
+ const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
190
+ const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
191
+ const errorRatePct = toolCalls > 0 ? toolErrors / toolCalls * 100 : 0;
192
+ const remediationProposals = [];
193
+ for (const lp of loops.slice(0, 5)) {
194
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, "_");
195
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
196
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
197
+ const body = `Finding: ${key}\\nObserved ${lp.repeats} redundant invocations in session ${lp.sessionId} (steps #${lp.fromSeq ?? 1}\u2192#${lp.toSeq ?? lp.repeats}). Wasted tokens: ~${lp.wastedTokens}.`;
198
+ const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
199
+ remediationProposals.push({ key, title, command });
200
+ }
201
+ if (toolCalls > 0 && errorRatePct > 10) {
202
+ const key = "reliability:tooling:high-error-rate";
203
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
204
+ const body = `Finding: ${key}\\nObserved ${toolErrors} errors across ${toolCalls} tool calls (${errorRatePct.toFixed(1)}% error rate) on ${date}.`;
205
+ const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
206
+ remediationProposals.push({ key, title, command });
207
+ }
208
+ return {
209
+ toolCalls,
210
+ toolErrors,
211
+ errorRatePct,
212
+ loops,
213
+ redundantCalls,
214
+ wastedTokens,
215
+ remediationProposals
216
+ };
217
+ } finally {
218
+ db.close();
219
+ }
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
152
224
  async function getGitCommits(date) {
153
225
  try {
154
226
  const { start, end } = getDateRange(date);
@@ -333,6 +405,49 @@ function generateMarkdown(summary) {
333
405
  lines.push(pending);
334
406
  lines.push("");
335
407
  }
408
+ if (summary.historyHealth) {
409
+ const hh = summary.historyHealth;
410
+ lines.push("## Execution Loops & Health Findings");
411
+ lines.push("");
412
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
413
+ lines.push("- **Status:** \u2705 Clean \u2014 No execution loops or tool calls recorded for this date.");
414
+ lines.push("");
415
+ } else {
416
+ lines.push("| Metric | Value |");
417
+ lines.push("|--------|-------|");
418
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
419
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
420
+ lines.push(`| Detected Loops (Repeats \u2265 3) | ${hh.loops.length} |`);
421
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
422
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
423
+ lines.push("");
424
+ if (hh.loops.length > 0) {
425
+ lines.push("### Detected Execution Loops");
426
+ lines.push("");
427
+ for (const lp of hh.loops.slice(0, 10)) {
428
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} \u2192 #${lp.toSeq})` : "";
429
+ const argsHint = lp.argsDigest === "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b" ? "empty/unrecorded arguments" : lp.argsDigest.length > 28 ? `${lp.argsDigest.slice(0, 24)}...` : lp.argsDigest;
430
+ lines.push(`- \`${lp.toolName || "unknown"}\` \xD7 **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`);
431
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
432
+ }
433
+ lines.push("");
434
+ }
435
+ if (hh.remediationProposals.length > 0) {
436
+ lines.push("### Auto-Healing Remediation Proposals");
437
+ lines.push("");
438
+ lines.push("To remediate root causes and prevent recurring token waste, execute:");
439
+ lines.push("");
440
+ lines.push("```bash");
441
+ for (const prop of hh.remediationProposals) {
442
+ lines.push(`# ${prop.title} [${prop.key}]`);
443
+ lines.push(prop.command);
444
+ lines.push("");
445
+ }
446
+ lines.push("```");
447
+ lines.push("");
448
+ }
449
+ }
450
+ }
336
451
  if (summary.historyReportPath) {
337
452
  lines.push("## History Report");
338
453
  lines.push("");
@@ -417,6 +532,11 @@ async function buildDailySummary(options) {
417
532
  if (gitActivity !== undefined) {
418
533
  result.gitActivity = gitActivity;
419
534
  }
535
+ const historyHealth = await getSpurHistoryHealth(options.date);
536
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
537
+ result.historyHealth = historyHealth;
538
+ platforms.push("Spur History");
539
+ }
420
540
  return result;
421
541
  }
422
542
  async function main() {
@@ -469,6 +589,7 @@ export {
469
589
  printUsage,
470
590
  parseArgs,
471
591
  main,
592
+ getSpurHistoryHealth,
472
593
  getGitCommits,
473
594
  getDateRange,
474
595
  getCcusageData,
@@ -59,6 +59,30 @@ export interface UserAnnotations {
59
59
  pending: string;
60
60
  }
61
61
 
62
+ export interface HistoryLoopFinding {
63
+ toolName: string;
64
+ argsDigest: string;
65
+ repeats: number;
66
+ sessionId: string;
67
+ fromSeq?: number;
68
+ toSeq?: number;
69
+ wastedTokens: number;
70
+ }
71
+
72
+ export interface HistoryHealthSummary {
73
+ toolCalls: number;
74
+ toolErrors: number;
75
+ errorRatePct: number;
76
+ loops: HistoryLoopFinding[];
77
+ redundantCalls: number;
78
+ wastedTokens: number;
79
+ remediationProposals: Array<{
80
+ key: string;
81
+ title: string;
82
+ command: string;
83
+ }>;
84
+ }
85
+
62
86
  export interface DailySummary {
63
87
  date: string;
64
88
  platforms: string[];
@@ -77,6 +101,8 @@ export interface DailySummary {
77
101
  };
78
102
  commits: GitCommit[];
79
103
  annotations: UserAnnotations;
104
+ /** Health metrics and loop findings from Spur history analytics. */
105
+ historyHealth?: HistoryHealthSummary;
80
106
  /** Path to the newest history report artifact (R7), resolved from the
81
107
  * `.spur/reports/history/latest.json` pointer. Omitted when no report exists. */
82
108
  historyReportPath?: string;
@@ -257,6 +283,122 @@ export async function getCcusageData(date: string): Promise<CcusageData | null>
257
283
  }
258
284
  }
259
285
 
286
+ // ─── Spur History Health Integration ──────────────────────────────────────────
287
+
288
+ export async function getSpurHistoryHealth(
289
+ date: string,
290
+ dbPath = '.spur/spur.db',
291
+ ): Promise<HistoryHealthSummary | null> {
292
+ try {
293
+ const resolvedPath = resolve(process.cwd(), dbPath);
294
+ if (!existsSync(resolvedPath)) {
295
+ return null;
296
+ }
297
+
298
+ const { Database } = await import('bun:sqlite');
299
+ const db = new Database(resolvedPath, { readonly: true });
300
+
301
+ try {
302
+ // 1. Query execution loop findings
303
+ const loopTable = db
304
+ .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
305
+ .get('history_board_loop_findings');
306
+
307
+ let loops: HistoryLoopFinding[] = [];
308
+ if (loopTable) {
309
+ const rows = db
310
+ .query<
311
+ {
312
+ tool_name: string;
313
+ args_digest: string;
314
+ repeats: number;
315
+ session_id: string;
316
+ first_seq: number;
317
+ last_seq: number;
318
+ started_at: string | null;
319
+ },
320
+ [string]
321
+ >(
322
+ `SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
323
+ FROM history_board_loop_findings
324
+ WHERE started_at IS NULL OR started_at LIKE ?
325
+ ORDER BY repeats DESC
326
+ LIMIT 20`,
327
+ )
328
+ .all(`${date}%`);
329
+
330
+ loops = rows.map((r) => ({
331
+ toolName: r.tool_name,
332
+ argsDigest: r.args_digest || 'repeated execution',
333
+ repeats: r.repeats,
334
+ sessionId: r.session_id,
335
+ fromSeq: r.first_seq,
336
+ toSeq: r.last_seq,
337
+ wastedTokens: r.repeats * 250,
338
+ }));
339
+ }
340
+
341
+ // 2. Query tool calls and errors
342
+ let toolCalls = 0;
343
+ let toolErrors = 0;
344
+ const toolTable = db
345
+ .query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
346
+ .get('history_board_tool_5m');
347
+
348
+ if (toolTable) {
349
+ const stats = db
350
+ .query<{ calls: number | null; errors: number | null }, [string]>(
351
+ `SELECT SUM(calls) AS calls, SUM(errors) AS errors
352
+ FROM history_board_tool_5m
353
+ WHERE bucket_start LIKE ?`,
354
+ )
355
+ .get(`${date}%`);
356
+
357
+ toolCalls = stats?.calls ?? 0;
358
+ toolErrors = stats?.errors ?? 0;
359
+ }
360
+
361
+ const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
362
+ const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
363
+ const errorRatePct = toolCalls > 0 ? (toolErrors / toolCalls) * 100 : 0;
364
+
365
+ // 3. Generate auto-healing remediation proposals
366
+ const remediationProposals: Array<{ key: string; title: string; command: string }> = [];
367
+
368
+ for (const lp of loops.slice(0, 5)) {
369
+ const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
370
+ const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
371
+ const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
372
+ const body = `Finding: ${key}\\nObserved ${lp.repeats} redundant invocations in session ${lp.sessionId} (steps #${lp.fromSeq ?? 1}→#${lp.toSeq ?? lp.repeats}). Wasted tokens: ~${lp.wastedTokens}.`;
373
+ const command = `spur task create --section "Fix ${cleanTool} repetition loop" --body "${body}"`;
374
+ remediationProposals.push({ key, title, command });
375
+ }
376
+
377
+ if (toolCalls > 0 && errorRatePct > 10) {
378
+ const key = 'reliability:tooling:high-error-rate';
379
+ const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
380
+ const body = `Finding: ${key}\\nObserved ${toolErrors} errors across ${toolCalls} tool calls (${errorRatePct.toFixed(1)}% error rate) on ${date}.`;
381
+ const command = `spur task create --section "Investigate tool error rate spikes" --body "${body}"`;
382
+ remediationProposals.push({ key, title, command });
383
+ }
384
+
385
+ return {
386
+ toolCalls,
387
+ toolErrors,
388
+ errorRatePct,
389
+ loops,
390
+ redundantCalls,
391
+ wastedTokens,
392
+ remediationProposals,
393
+ };
394
+ } finally {
395
+ db.close();
396
+ }
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+
260
402
  // ─── Git Integration ─────────────────────────────────────────────────────────
261
403
 
262
404
  export async function getGitCommits(date: string): Promise<GitCommit[]> {
@@ -477,6 +619,61 @@ export function generateMarkdown(summary: DailySummary): string {
477
619
  lines.push('');
478
620
  }
479
621
 
622
+ // Execution Loops & Health Findings
623
+ if (summary.historyHealth) {
624
+ const hh = summary.historyHealth;
625
+ lines.push('## Execution Loops & Health Findings');
626
+ lines.push('');
627
+
628
+ if (hh.loops.length === 0 && hh.toolCalls === 0) {
629
+ lines.push('- **Status:** ✅ Clean — No execution loops or tool calls recorded for this date.');
630
+ lines.push('');
631
+ } else {
632
+ lines.push('| Metric | Value |');
633
+ lines.push('|--------|-------|');
634
+ lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
635
+ lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
636
+ lines.push(`| Detected Loops (Repeats ≥ 3) | ${hh.loops.length} |`);
637
+ lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
638
+ lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
639
+ lines.push('');
640
+
641
+ if (hh.loops.length > 0) {
642
+ lines.push('### Detected Execution Loops');
643
+ lines.push('');
644
+ for (const lp of hh.loops.slice(0, 10)) {
645
+ const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} → #${lp.toSeq})` : '';
646
+ const argsHint =
647
+ lp.argsDigest === '74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b'
648
+ ? 'empty/unrecorded arguments'
649
+ : lp.argsDigest.length > 28
650
+ ? `${lp.argsDigest.slice(0, 24)}...`
651
+ : lp.argsDigest;
652
+ lines.push(
653
+ `- \`${lp.toolName || 'unknown'}\` × **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`,
654
+ );
655
+ lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
656
+ }
657
+ lines.push('');
658
+ }
659
+
660
+ if (hh.remediationProposals.length > 0) {
661
+ lines.push('### Auto-Healing Remediation Proposals');
662
+ lines.push('');
663
+ lines.push('To remediate root causes and prevent recurring token waste, execute:');
664
+ lines.push('');
665
+ lines.push('```bash');
666
+ for (const prop of hh.remediationProposals) {
667
+ lines.push(`# ${prop.title} [${prop.key}]`);
668
+ lines.push(prop.command);
669
+ lines.push('');
670
+ }
671
+ lines.push('```');
672
+ lines.push('');
673
+ }
674
+ }
675
+ }
676
+
480
677
  // History report path (R7 — surfaces the newest nightly-run artifact).
481
678
  if (summary.historyReportPath) {
482
679
  lines.push('## History Report');
@@ -598,6 +795,13 @@ export async function buildDailySummary(options: CliOptions): Promise<DailySumma
598
795
  result.gitActivity = gitActivity;
599
796
  }
600
797
 
798
+ // Query Spur history health (loops, tool errors, and auto-healing proposals)
799
+ const historyHealth = await getSpurHistoryHealth(options.date);
800
+ if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
801
+ result.historyHealth = historyHealth;
802
+ platforms.push('Spur History');
803
+ }
804
+
601
805
  return result;
602
806
  }
603
807
 
@@ -92,6 +92,41 @@
92
92
  "enabled": {
93
93
  "type": "boolean",
94
94
  "description": "Enable the scheduled-task runner. OFF by default for CLI (run-once)."
95
+ },
96
+ "jobs": {
97
+ "type": "array",
98
+ "description": "Declarative scheduled commands (task 0734). Each entry registers one scheduler tick that enqueues a `scheduler.custom` queue job running `command` through `/bin/sh -c`. Normalized and validated by @gobing-ai/ts-infra runNodeApplication before the server starts.",
99
+ "items": {
100
+ "type": "object",
101
+ "required": ["name", "command"],
102
+ "properties": {
103
+ "name": {
104
+ "type": "string",
105
+ "minLength": 1,
106
+ "description": "Job name, unique across the list after trimming. Reported as the `scheduler.custom:<name>` display name on scheduler.job.executed."
107
+ },
108
+ "command": {
109
+ "type": "string",
110
+ "minLength": 1,
111
+ "description": "Shell command run by `/bin/sh -c` with the project root as cwd. Trusted operator input \u2014 never logged."
112
+ },
113
+ "intervalMinutes": {
114
+ "type": "integer",
115
+ "minimum": 1,
116
+ "maximum": 35791,
117
+ "description": "Fixed interval in minutes. Mutually exclusive with `cron`. The upper bound is the Node timer ceiling (2147483647 ms)."
118
+ },
119
+ "cron": {
120
+ "type": "string",
121
+ "minLength": 1,
122
+ "description": "Five-field cron expression (minute hour day-of-month month day-of-week) evaluated in local time. Mutually exclusive with `intervalMinutes`."
123
+ }
124
+ },
125
+ "oneOf": [
126
+ { "required": ["intervalMinutes"], "not": { "required": ["cron"] } },
127
+ { "required": ["cron"], "not": { "required": ["intervalMinutes"] } }
128
+ ]
129
+ }
95
130
  }
96
131
  }
97
132
  }