@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +9 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +121 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +204 -0
- package/schemas/spur-config.schema.json +35 -0
- package/spur.js +3008 -1468
- package/web/_astro/BoardApp.BYCNkMOn.js +185 -0
- package/web/_astro/BoardApp.E12MFjOS.js +1 -0
- package/web/_astro/{TaskDetail.BvkKvo57.js → TaskDetail.CgUreSP2.js} +1 -1
- package/web/_astro/{arc.DuEIzPMi.js → arc.BySSh34M.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.D0dUxCjA.js → architectureDiagram-3BPJPVTR.DM46TS_h.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Cv60iOpM.js → blockDiagram-GPEHLZMM.tZhvNUHA.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ddx7WGhb.js → c4Diagram-AAUBKEIU.PT4Or4Nf.js} +1 -1
- package/web/_astro/channel.5cYKr5cs.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BGKzU_1t.js → chunk-2J33WTMH.J9r0_Bbe.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.FlApjIIH.js → chunk-4BX2VUAB.hzyeIvhR.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.CDfmvDeW.js → chunk-55IACEB6.B0rO7qVh.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.T9bc-xir.js → chunk-727SXJPM.wE_Uk5D4.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BiTc-MQo.js → chunk-AQP2D5EJ.DqEEjQw7.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.he4KrHni.js → chunk-FMBD7UC4.CDoD9sBX.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.D05XuUuJ.js → chunk-ND2GUHAM.CtX5nF9P.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CA2NThIE.js → chunk-QZHKN3VN.CK_EwfaT.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Couj-zYZ.js → classDiagram-4FO5ZUOK.DLt5a8Lh.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Couj-zYZ.js → classDiagram-v2-Q7XG4LA2.DLt5a8Lh.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.B8YYW7NG.js → cose-bilkent-S5V4N54A.CMCWP49h.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BExFdiin.js → cynefin-OW5HDTMX.HyXw_vdS.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BybKbz3q.js → dagre-BM42HDAG.BTuAzh01.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.UyRTSl9n.js → diagram-2AECGRRQ.D9dr9wfT.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BrxCucBf.js → diagram-5GNKFQAL.C4Rot0hj.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CdE5oy5J.js → diagram-KO2AKTUF.B_TK5uWC.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BJLgdosK.js → diagram-LMA3HP47.JkXKK7CO.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CUynieTU.js → diagram-OG6HWLK6.BzMN8Bd6.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.C0vS6DJv.js → erDiagram-TEJ5UH35.DVZaWGUd.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.T3QLi_en.js → flowDiagram-I6XJVG4X.rjEiWUfR.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BNsk3w9Z.js → ganttDiagram-6RSMTGT7.C_EgAarK.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Mwe2I4V6.js → gitGraphDiagram-PVQCEYII.B-QQSDsK.js} +1 -1
- package/web/_astro/index.B5MTfe7k.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BlcjLmtc.js → infoDiagram-5YYISTIA.DlWesz7T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.js8qeS0h.js → ishikawaDiagram-YF4QCWOH.BUMZOawi.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CM6UK0a4.js → journeyDiagram-JHISSGLW.CWfkxfjY.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.a6ihOzMd.js → kanban-definition-UN3LZRKU.B-YpMwXf.js} +1 -1
- package/web/_astro/{linear.CsIB2jFu.js → linear.D7uqzENp.js} +1 -1
- package/web/_astro/{mermaid.core.Br2Fo22q.js → mermaid.core.CxrNppBD.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.29inC1Mk.js → mindmap-definition-RKZ34NQL.B4Qe7cM2.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C9CxG_Kf.js → pieDiagram-4H26LBE5.Ds-5j2ro.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.6qo9MOJM.js → quadrantDiagram-W4KKPZXB.tBd38uNC.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DN07zrP5.js → requirementDiagram-4Y6WPE33.sFENkWl3.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BSw5o173.js → sankeyDiagram-5OEKKPKP.BeB-Hk7C.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.LJPzySKw.js → sequenceDiagram-3UESZ5HK.DnTeaSpx.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.ClNjEiKV.js → stateDiagram-AJRCARHV.B-8Jt5EJ.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.c6Z-_WfX.js → stateDiagram-v2-BHNVJYJU.Br7xoqMW.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CIMR-87j.js → timeline-definition-PNZ67QCA.C-3WdOyi.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.BnRSRI9I.js → vennDiagram-CIIHVFJN.DCIs7Lc6.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.uN08C3gv.js → wardleyDiagram-YWT4CUSO.rGAL-bbz.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.BjbEvfuq.js → xychartDiagram-2RQKCTM6.hkfQKiRl.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BnjsI80-.js +0 -178
- package/web/_astro/BoardApp.SJcrHBZp.js +0 -1
- package/web/_astro/channel.tWfETQvX.js +0 -1
- package/web/_astro/index.9npdrEIr.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
57
|
-
"@gobing-ai/ts-ai-runner": "^0.4.
|
|
58
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.4.
|
|
59
|
-
"@gobing-ai/ts-infra": "^0.4.
|
|
60
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.
|
|
61
|
-
"@gobing-ai/ts-rule-engine": "^0.4.
|
|
62
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
63
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
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",
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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
|
}
|