@gobing-ai/spur 0.4.7 → 0.4.9

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/pipeline-budgets.json +2 -2
  3. package/config/rules/boundary/config-loading-ownership.yaml +1 -0
  4. package/config/rules/boundary/sp-plugin-standalone.yaml +1 -1
  5. package/config/rules/boundary/sp-script-placement.yaml +1 -1
  6. package/config/rules/docs/sourced-api-claims.yaml +45 -7
  7. package/config/rules/strict/runtime-boundaries.yaml +40 -1
  8. package/config/rules/structure/protected-files.yaml +1 -1
  9. package/config/rules/surface/check-cli-surface.yaml +33 -0
  10. package/config/rules/typescript/no-debugger.yaml +3 -0
  11. package/config/workflow-candidates.json +2 -2
  12. package/config/workflows/feature-verification.yaml +6 -11
  13. package/config/workflows/history-anatomy.yaml +14 -28
  14. package/config/workflows/idea-pipeline.yaml +6 -19
  15. package/config/workflows/pr-review.yaml +9 -16
  16. package/config/workflows/task-pipeline.yaml +16 -23
  17. package/config/workflows/wrapup-pipeline.yaml +21 -63
  18. package/package.json +3 -3
  19. package/plugins/sp/README.md +70 -37
  20. package/plugins/sp/agents/super-planner.md +5 -5
  21. package/plugins/sp/agents/super-reviewer.md +2 -2
  22. package/plugins/sp/commands/dev-changelog.md +1 -1
  23. package/plugins/sp/commands/dev-daily.md +1 -1
  24. package/plugins/sp/commands/dev-pr-review.md +1 -1
  25. package/plugins/sp/commands/dev-review-session.md +1 -1
  26. package/plugins/sp/commands/dev-runall.md +4 -4
  27. package/plugins/sp/commands/dev-verify.md +4 -6
  28. package/plugins/sp/commands/dev-verifyall.md +4 -5
  29. package/plugins/sp/hooks/hooks.json +11 -1
  30. package/plugins/sp/lib/context-post-tool.ts +1 -1
  31. package/plugins/sp/lib/context-session-start.ts +1 -1
  32. package/plugins/sp/lib/context-session-stop.ts +1 -1
  33. package/plugins/sp/lib/destructive-policy.ts +65 -1
  34. package/plugins/sp/plugin.json +2 -5
  35. package/plugins/sp/{hooks → scripts/hooks}/agent-lifecycle.ts +1 -1
  36. package/plugins/sp/scripts/hooks/careful-guard.ts +94 -0
  37. package/plugins/sp/{hooks → scripts/hooks}/context-post-tool.ts +2 -2
  38. package/plugins/sp/{hooks → scripts/hooks}/context-session-start.ts +1 -1
  39. package/plugins/sp/{hooks → scripts/hooks}/context-session-stop.ts +1 -1
  40. package/plugins/sp/{hooks → scripts/hooks}/fleet-guest-stop.ts +6 -4
  41. package/plugins/sp/{hooks → scripts/hooks}/task-write-guard.ts +38 -21
  42. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  43. package/plugins/sp/skills/code-verification/SKILL.md +8 -8
  44. package/plugins/sp/skills/code-verification/references/secu-review.md +4 -3
  45. package/plugins/sp/skills/code-verification/references/verdict-schema.md +9 -1
  46. package/plugins/sp/skills/daily-summary/SKILL.md +4 -5
  47. package/plugins/sp/skills/dogfood-testing/SKILL.md +9 -9
  48. package/plugins/sp/skills/dogfood-testing/references/report-template.md +1 -1
  49. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -2
  50. package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
  51. package/plugins/sp/skills/parallel-execution/SKILL.md +1 -1
  52. package/plugins/sp/skills/pr-reviewing/SKILL.md +14 -15
  53. package/plugins/sp/skills/session-review/SKILL.md +3 -3
  54. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  55. package/plugins/sp/skills/spur-check/SKILL.md +14 -13
  56. package/plugins/sp/skills/spur-cli/SKILL.md +1 -1
  57. package/plugins/sp/skills/spur-cli/references/agent.md +12 -0
  58. package/plugins/sp/skills/spur-cli/references/builder.md +27 -6
  59. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  60. package/plugins/sp/skills/spur-cli/references/history.md +3 -0
  61. package/plugins/sp/skills/spur-cli/references/self.md +5 -3
  62. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +3 -0
  63. package/plugins/sp/skills/spur-cli/references/tasks.md +13 -3
  64. package/plugins/sp/skills/spur-cli/references/workflows.md +36 -1
  65. package/plugins/sp/skills/spur-dev/SKILL.md +1 -1
  66. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +1 -1
  67. package/plugins/sp/skills/spur-dev/references/dev-operations.md +12 -12
  68. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +10 -11
  69. package/plugins/sp/skills/spur-dev/references/execution-batch.md +16 -17
  70. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +1 -1
  71. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +8 -11
  72. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +6 -8
  73. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +1 -1
  74. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +33 -40
  75. package/plugins/sp/skills/spur-dev/references/structured-trace-emission.md +24 -24
  76. package/plugins/sp/skills/spur-doctor/SKILL.md +5 -4
  77. package/spur.js +41901 -30598
  78. package/config/plugin-scripts.json +0 -116
  79. package/config/script-placement-baseline.json +0 -33
  80. package/plugins/sp/hooks/careful-guard.ts +0 -74
  81. package/plugins/sp/hooks/pi/guard-extension.ts +0 -304
  82. package/plugins/sp/lib/dogfood-pipeline.ts +0 -265
  83. package/plugins/sp/lib/dogfood-report.ts +0 -278
  84. package/plugins/sp/lib/history-anatomy.generated.d.mts +0 -112
  85. package/plugins/sp/lib/history-anatomy.generated.mjs +0 -691
  86. package/plugins/sp/lib/idea-handoff.generated.d.mts +0 -17
  87. package/plugins/sp/lib/idea-handoff.generated.mjs +0 -1446
  88. package/plugins/sp/lib/inline-run-app.ts +0 -68
  89. package/plugins/sp/lib/inline-run.generated.d.mts +0 -37
  90. package/plugins/sp/lib/inline-run.generated.mjs +0 -3103
  91. package/plugins/sp/lib/plan-projection.generated.d.mts +0 -197
  92. package/plugins/sp/lib/plan-projection.generated.mjs +0 -982
  93. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -139
  94. package/plugins/sp/lib/quality-gate.generated.mjs +0 -30510
  95. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -93
  96. package/plugins/sp/lib/residual-scan.generated.mjs +0 -383
  97. package/plugins/sp/lib/run-summary-core.ts +0 -225
  98. package/plugins/sp/lib/spur-bin.ts +0 -36
  99. package/plugins/sp/lib/step-profile.generated.d.mts +0 -71
  100. package/plugins/sp/lib/step-profile.generated.mjs +0 -174
  101. package/plugins/sp/lib/transcript.ts +0 -112
  102. package/plugins/sp/scripts/batch-plan.mjs +0 -1096
  103. package/plugins/sp/scripts/batch-plan.ts +0 -145
  104. package/plugins/sp/scripts/batch-preflight.mjs +0 -423
  105. package/plugins/sp/scripts/batch-preflight.ts +0 -270
  106. package/plugins/sp/scripts/commit-guard.mjs +0 -226
  107. package/plugins/sp/scripts/commit-guard.ts +0 -248
  108. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +0 -626
  109. package/plugins/sp/scripts/daily-summary/daily-summary.ts +0 -855
  110. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +0 -211
  111. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +0 -110
  112. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +0 -282
  113. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +0 -98
  114. package/plugins/sp/scripts/feature-verification-steps.mjs +0 -82
  115. package/plugins/sp/scripts/feature-verification-steps.ts +0 -164
  116. package/plugins/sp/scripts/history-anatomy-cache.mjs +0 -931
  117. package/plugins/sp/scripts/history-anatomy-cache.ts +0 -247
  118. package/plugins/sp/scripts/idea-handoff.mjs +0 -32
  119. package/plugins/sp/scripts/idea-handoff.ts +0 -47
  120. package/plugins/sp/scripts/inline-run-dispatch.mjs +0 -131
  121. package/plugins/sp/scripts/inline-run-dispatch.ts +0 -115
  122. package/plugins/sp/scripts/inline-run-setup.mjs +0 -746
  123. package/plugins/sp/scripts/inline-run-setup.ts +0 -198
  124. package/plugins/sp/scripts/persist-out-check.mjs +0 -217
  125. package/plugins/sp/scripts/persist-out-check.ts +0 -249
  126. package/plugins/sp/scripts/pr-reviewing.mjs +0 -712
  127. package/plugins/sp/scripts/pr-reviewing.ts +0 -952
  128. package/plugins/sp/scripts/quality-gate.mjs +0 -30491
  129. package/plugins/sp/scripts/quality-gate.ts +0 -80
  130. package/plugins/sp/scripts/residual-scan.mjs +0 -634
  131. package/plugins/sp/scripts/residual-scan.ts +0 -248
  132. package/plugins/sp/scripts/run-summary.mjs +0 -758
  133. package/plugins/sp/scripts/run-summary.ts +0 -107
  134. package/plugins/sp/scripts/script-root.mjs +0 -129
  135. package/plugins/sp/scripts/script-root.ts +0 -199
  136. package/plugins/sp/scripts/session-timeline.mjs +0 -359
  137. package/plugins/sp/scripts/session-timeline.ts +0 -190
  138. package/plugins/sp/scripts/task-diffstat.mjs +0 -181
  139. package/plugins/sp/scripts/task-diffstat.ts +0 -219
  140. package/plugins/sp/scripts/workflow-step-profile.mjs +0 -338
  141. package/plugins/sp/scripts/workflow-step-profile.ts +0 -165
  142. package/plugins/sp/scripts/wrapup-drift-probe.mjs +0 -190
  143. package/plugins/sp/scripts/wrapup-drift-probe.ts +0 -261
  144. package/plugins/sp/scripts/wrapup-steps.mjs +0 -627
  145. package/plugins/sp/scripts/wrapup-steps.ts +0 -891
@@ -1,855 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * sp:daily-summary — Daily Summary Report Generator
4
- *
5
- * Generates structured markdown summaries from:
6
- * - Token usage data (via ccusage CLI)
7
- * - Git history (commits, changes)
8
- * - User annotations (learnings, issues, pending)
9
- */
10
-
11
- import { spawn, spawnSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
13
- import { dirname, isAbsolute, join, resolve } from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
- import { getEnvVar, getEnvVars } from '../../lib/env';
16
- import { loadInlineApp, resolveAppEntry } from '../../lib/inline-run-app';
17
-
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
- };
39
-
40
- // ─── Types ───────────────────────────────────────────────────────────────────
41
-
42
- export interface CliOptions {
43
- date: string;
44
- dryRun: boolean;
45
- outputPath?: string;
46
- skipGit: boolean;
47
- skipCcusage: boolean;
48
- }
49
-
50
- interface CcusageData {
51
- daily?: Array<{
52
- date: string;
53
- inputTokens: number;
54
- outputTokens: number;
55
- cacheCreationTokens: number;
56
- cacheReadTokens: number;
57
- totalTokens: number;
58
- totalCost: number;
59
- modelsUsed: string[];
60
- }>;
61
- totals: {
62
- inputTokens: number;
63
- outputTokens: number;
64
- cacheCreationTokens: number;
65
- cacheReadTokens: number;
66
- totalTokens: number;
67
- totalCost: number;
68
- };
69
- }
70
-
71
- export interface GitCommit {
72
- hash: string;
73
- date: string;
74
- message: string;
75
- filesChanged: number;
76
- insertions: number;
77
- deletions: number;
78
- }
79
-
80
- export interface UserAnnotations {
81
- learnings: string;
82
- issuesFixed: string;
83
- pending: string;
84
- }
85
-
86
- export interface HistoryLoopFinding {
87
- toolName: string;
88
- argsDigest: string;
89
- repeats: number;
90
- sessionId: string;
91
- fromSeq?: number;
92
- toSeq?: number;
93
- wastedTokens: number;
94
- }
95
-
96
- export interface HistoryHealthSummary {
97
- toolCalls: number;
98
- toolErrors: number;
99
- errorRatePct: number;
100
- loops: HistoryLoopFinding[];
101
- redundantCalls: number;
102
- wastedTokens: number;
103
- remediationProposals: Array<{
104
- key: string;
105
- title: string;
106
- command: string;
107
- }>;
108
- }
109
-
110
- export interface DailySummary {
111
- date: string;
112
- platforms: string[];
113
- tokenUsage?: {
114
- inputTokens: number;
115
- outputTokens: number;
116
- cacheTokens: number;
117
- totalTokens: number;
118
- costUsd: number;
119
- };
120
- gitActivity?: {
121
- commitCount: number;
122
- filesChanged: number;
123
- insertions: number;
124
- deletions: number;
125
- };
126
- commits: GitCommit[];
127
- annotations: UserAnnotations;
128
- /** Health metrics and loop findings from Spur history analytics. */
129
- historyHealth?: HistoryHealthSummary;
130
- /** Path to the newest history report artifact (R7), resolved from the
131
- * `.spur/reports/history/latest.json` pointer. Omitted when no report exists. */
132
- historyReportPath?: string;
133
- generatedAt: string;
134
- }
135
-
136
- // ─── Constants ────────────────────────────────────────────────────────────────
137
-
138
- const DAILY_DIR = 'docs/daily';
139
- const DEFAULT_DATE = 'today';
140
-
141
- // ─── CLI Argument Parsing ────────────────────────────────────────────────────
142
-
143
- export function parseArgs(argv: string[] = process.argv.slice(2)): CliOptions {
144
- const args = argv;
145
- const options: CliOptions = {
146
- date: DEFAULT_DATE,
147
- dryRun: false,
148
- skipGit: false,
149
- skipCcusage: false,
150
- };
151
-
152
- for (let i = 0; i < args.length; i++) {
153
- const arg = args[i];
154
- if (arg === '--date' && i + 1 < args.length) {
155
- options.date = args[++i] ?? options.date;
156
- } else if (arg === '--dry-run') {
157
- options.dryRun = true;
158
- } else if (arg === '--output' && i + 1 < args.length) {
159
- options.outputPath = args[++i];
160
- } else if (arg === '--no-git') {
161
- options.skipGit = true;
162
- } else if (arg === '--no-ccusage') {
163
- options.skipCcusage = true;
164
- } else if (arg === '--help' || arg === '-h') {
165
- printUsage();
166
- process.exit(0);
167
- }
168
- }
169
-
170
- // Resolve date
171
- if (options.date === 'today') {
172
- options.date = todayLocal();
173
- } else if (options.date === 'yesterday') {
174
- options.date = yesterdayLocal();
175
- }
176
-
177
- return options;
178
- }
179
-
180
- export function printUsage(): void {
181
- console.log(`
182
- sp:daily-summary — Generate daily summary reports
183
-
184
- Usage: daily-summary.ts [options]
185
-
186
- Options:
187
- --date YYYY-MM-DD Date for summary (default: today, also: yesterday)
188
- --dry-run Show summary without writing file
189
- --output <path> Write to custom path
190
- --no-git Skip git history collection
191
- --no-ccusage Skip token usage collection
192
- --help, -h Show this help
193
-
194
- Examples:
195
- daily-summary.ts # Today's summary
196
- daily-summary.ts --date yesterday # Yesterday's summary
197
- daily-summary.ts --dry-run # Preview without writing
198
- `);
199
- }
200
-
201
- // ─── Date Helpers ─────────────────────────────────────────────────────────────
202
-
203
- // ─── Date Helpers ─────────────────────────────────────────────────────────────
204
-
205
- /** Return today's date as YYYY-MM-DD in the system (git) timezone. */
206
- export function todayLocal(): string {
207
- // Spawn 'date' to get system timezone date, since JS runtime
208
- // may have a different TZ (e.g. bun test forces UTC).
209
- const proc = spawnSync('date', ['+%Y-%m-%d'], { encoding: 'utf8' });
210
- return (proc.stdout ?? '').trim();
211
- }
212
-
213
- /** Return yesterday's date as YYYY-MM-DD in the system (git) timezone. */
214
- export function yesterdayLocal(): string {
215
- // Use portable epoch math via date command
216
- const epochProc = spawnSync('date', ['+%s'], { encoding: 'utf8' });
217
- const epoch = parseInt((epochProc.stdout ?? '').trim(), 10);
218
- const yesterdayEpoch = epoch - 86400;
219
- // Try BSD -r first, then GNU -d @
220
- let proc = spawnSync('date', ['-r', String(yesterdayEpoch), '+%Y-%m-%d'], { encoding: 'utf8' });
221
- if ((proc.status ?? 1) !== 0) {
222
- proc = spawnSync('date', ['-d', `@${yesterdayEpoch}`, '+%Y-%m-%d'], { encoding: 'utf8' });
223
- }
224
- return (proc.stdout ?? '').trim();
225
- }
226
-
227
- export function getDateRange(dateStr: string): { start: string; end: string } {
228
- // Date is YYYY-MM-DD format
229
- const date = new Date(`${dateStr}T00:00:00`);
230
- const start = `${dateStr} 00:00:00`;
231
- const endDate = new Date(date);
232
- endDate.setDate(endDate.getDate() + 1);
233
- const end = `${endDate.toISOString().slice(0, 10)} 00:00:00`;
234
- return { start, end };
235
- }
236
-
237
- // ─── Subprocess Spawner ──────────────────────────────────────────────────────
238
-
239
- export interface ProcessSpawnResult {
240
- stdout: string;
241
- stderr: string;
242
- exitCode: number;
243
- }
244
-
245
- export type ProcessSpawner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => Promise<ProcessSpawnResult>;
246
-
247
- export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
248
- return new Promise((resolve, reject) => {
249
- try {
250
- const proc = spawn(cmd, args, {
251
- stdio: ['ignore', 'pipe', 'pipe'],
252
- env: env ?? getEnvVars(),
253
- });
254
- const stdoutChunks: Buffer[] = [];
255
- const stderrChunks: Buffer[] = [];
256
- proc.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
257
- proc.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
258
- proc.on('close', (exitCode) => {
259
- resolve({
260
- stdout: Buffer.concat(stdoutChunks).toString('utf8'),
261
- stderr: Buffer.concat(stderrChunks).toString('utf8'),
262
- exitCode: exitCode ?? 0,
263
- });
264
- });
265
- proc.on('error', (err) => {
266
- reject(err);
267
- });
268
- } catch (err) {
269
- reject(err);
270
- }
271
- });
272
- };
273
-
274
- let processSpawner: ProcessSpawner = defaultProcessSpawner;
275
-
276
- export function setProcessSpawner(next?: ProcessSpawner): void {
277
- processSpawner = next ?? defaultProcessSpawner;
278
- }
279
-
280
- // ─── Ccusage Integration ─────────────────────────────────────────────────────
281
-
282
- export async function getCcusageData(date: string): Promise<CcusageData | null> {
283
- try {
284
- // Check if ccusage is available
285
- const env = { ...getEnvVars() };
286
- const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
287
- if (ccusageCheck.exitCode !== 0) {
288
- return null;
289
- }
290
-
291
- // Get daily data for the date
292
- const since = `${date}T00:00:00`;
293
- const until = `${date}T23:59:59`;
294
-
295
- const proc = await processSpawner('ccusage', ['daily', '--since', since, '--until', until, '--json'], env);
296
-
297
- if (proc.exitCode !== 0) {
298
- logger.warn(`ccusage error: ${proc.stderr}`);
299
- return null;
300
- }
301
-
302
- const data = JSON.parse(proc.stdout) as CcusageData;
303
- return data;
304
- } catch (error) {
305
- logger.warn(`Failed to get ccusage data: ${error}`);
306
- return null;
307
- }
308
- }
309
-
310
- // ─── Spur History Health Integration ──────────────────────────────────────────
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
- */
380
- export async function getSpurHistoryHealth(
381
- date: string,
382
- dbPath = '.spur/spur.db',
383
- ): Promise<HistoryHealthSummary | null> {
384
- try {
385
- const resolvedPath = resolve(process.cwd(), dbPath);
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);
391
- } catch {
392
- return null;
393
- }
394
- }
395
-
396
- // ─── Git Integration ─────────────────────────────────────────────────────────
397
-
398
- export async function getGitCommits(date: string): Promise<GitCommit[]> {
399
- try {
400
- const { start, end } = getDateRange(date);
401
-
402
- const proc = await processSpawner('git', [
403
- 'log',
404
- '--since',
405
- start,
406
- '--until',
407
- end,
408
- '--pretty=format:%H|%ad|%s',
409
- '--date=iso',
410
- '--numstat',
411
- ]);
412
-
413
- if (proc.exitCode !== 0) {
414
- logger.warn('Failed to get git commits');
415
- return [];
416
- }
417
-
418
- const commits: GitCommit[] = [];
419
- const lines = proc.stdout.trim().split('\n');
420
-
421
- let currentCommit: Partial<GitCommit> | null = null;
422
-
423
- for (const line of lines) {
424
- if (!line.trim()) continue;
425
-
426
- // Check if this is a commit line (contains | separator)
427
- if (line.includes('|')) {
428
- const parts = line.split('|');
429
- if (parts.length >= 3) {
430
- // Save previous commit if exists
431
- if (currentCommit?.hash) {
432
- commits.push(currentCommit as GitCommit);
433
- }
434
-
435
- currentCommit = {
436
- hash: parts[0],
437
- date: parts[1],
438
- message: parts[2],
439
- filesChanged: 0,
440
- insertions: 0,
441
- deletions: 0,
442
- };
443
- }
444
- } else if (currentCommit && line.includes('\t')) {
445
- // This is a numstat line (files changed)
446
- const parts = line.split('\t');
447
- if (parts.length >= 3) {
448
- const insertions = parseInt(parts[0] ?? '', 10) || 0;
449
- const deletions = parseInt(parts[1] ?? '', 10) || 0;
450
- currentCommit.filesChanged = (currentCommit.filesChanged ?? 0) + 1;
451
- currentCommit.insertions = (currentCommit.insertions ?? 0) + insertions;
452
- currentCommit.deletions = (currentCommit.deletions ?? 0) + deletions;
453
- }
454
- }
455
- }
456
-
457
- // Don't forget the last commit
458
- if (currentCommit?.hash) {
459
- commits.push(currentCommit as GitCommit);
460
- }
461
-
462
- return commits;
463
- } catch (error) {
464
- logger.warn(`Failed to get git commits: ${error}`);
465
- return [];
466
- }
467
- }
468
-
469
- // ─── User Input ─────────────────────────────────────────────────────────────
470
-
471
- export async function promptUser(): Promise<UserAnnotations> {
472
- if (getEnvVar('SP_DAILY_SUMMARY_NO_PROMPT') === '1') {
473
- return { learnings: '', issuesFixed: '', pending: '' };
474
- }
475
- if (getEnvVar('RD3_DAILY_SUMMARY_NO_PROMPT') === '1') {
476
- logger.warn('[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT');
477
- return { learnings: '', issuesFixed: '', pending: '' };
478
- }
479
-
480
- console.log(`\n📊 Daily Summary — ${todayLocal()}`);
481
- console.log('═'.repeat(50));
482
- console.log('\nPlease provide the following (press Enter to skip):\n');
483
-
484
- if (!process.stdin.isTTY) {
485
- const chunks: Buffer[] = [];
486
- for await (const chunk of process.stdin) {
487
- chunks.push(chunk as Buffer);
488
- }
489
- const buffered = Buffer.concat(chunks).toString('utf-8');
490
- const [learnings = '', issuesFixed = '', pending = ''] = buffered.split('\n');
491
- return {
492
- learnings: learnings.trim(),
493
- issuesFixed: issuesFixed.trim(),
494
- pending: pending.trim(),
495
- };
496
- }
497
-
498
- const readline = await import('node:readline');
499
- const rl = readline.createInterface({
500
- input: process.stdin,
501
- output: process.stdout,
502
- });
503
-
504
- const question = (prompt: string): Promise<string> =>
505
- new Promise((resolve) => {
506
- rl.question(prompt, (answer) => {
507
- resolve(answer.trim());
508
- });
509
- });
510
-
511
- const learnings = await question('1. What did you learn today? (optional)\n > ');
512
- const issuesFixed = await question('\n2. What issues did you fix? (optional)\n > ');
513
- const pending = await question("\n3. What's pending for tomorrow? (optional)\n > ");
514
-
515
- rl.close();
516
-
517
- return {
518
- learnings,
519
- issuesFixed,
520
- pending,
521
- };
522
- }
523
-
524
- // ─── Markdown Generation ─────────────────────────────────────────────────────
525
-
526
- export function generateMarkdown(summary: DailySummary): string {
527
- const lines: string[] = [];
528
-
529
- // Header
530
- lines.push(`# Daily Summary — ${summary.date}`);
531
- lines.push('');
532
- lines.push(`**Generated:** ${summary.generatedAt}`);
533
- lines.push('');
534
-
535
- // Meta section
536
- lines.push('## Meta');
537
- lines.push('');
538
- lines.push(`- **Date:** ${summary.date}`);
539
- lines.push(`- **Platforms:** ${summary.platforms.join(', ') || 'unknown'}`);
540
- lines.push('');
541
-
542
- // Token Usage
543
- if (summary.tokenUsage) {
544
- const tu = summary.tokenUsage;
545
- lines.push('## Token Usage');
546
- lines.push('');
547
- lines.push(`| Metric | Value |`);
548
- lines.push(`|--------|-------|`);
549
- lines.push(`| Input Tokens | ${tu.inputTokens.toLocaleString()} |`);
550
- lines.push(`| Output Tokens | ${tu.outputTokens.toLocaleString()} |`);
551
- lines.push(`| Cache Tokens | ${tu.cacheTokens.toLocaleString()} |`);
552
- lines.push(`| Total Tokens | ${tu.totalTokens.toLocaleString()} |`);
553
- lines.push(`| Estimated Cost | $${tu.costUsd.toFixed(4)} |`);
554
- lines.push('');
555
-
556
- // Calculate cache hit rate
557
- if (tu.inputTokens > 0) {
558
- const cacheHitRate = (tu.cacheTokens / (tu.inputTokens + tu.cacheTokens)) * 100;
559
- lines.push(`**Cache Hit Rate:** ${cacheHitRate.toFixed(1)}%`);
560
- lines.push('');
561
- }
562
- }
563
-
564
- // Git Activity
565
- if (summary.gitActivity) {
566
- const ga = summary.gitActivity;
567
- lines.push('## Git Activity');
568
- lines.push('');
569
- lines.push(`| Metric | Value |`);
570
- lines.push(`|--------|-------|`);
571
- lines.push(`| Commits | ${ga.commitCount} |`);
572
- lines.push(`| Files Changed | ${ga.filesChanged} |`);
573
- lines.push(`| Insertions | +${ga.insertions} |`);
574
- lines.push(`| Deletions | -${ga.deletions} |`);
575
- lines.push('');
576
- }
577
-
578
- // Commits
579
- if (summary.commits.length > 0) {
580
- lines.push('## Commits');
581
- lines.push('');
582
- for (const commit of summary.commits.slice(0, 10)) {
583
- const shortHash = commit.hash.slice(0, 7);
584
- lines.push(`- \`${shortHash}\` ${commit.message}`);
585
- }
586
- if (summary.commits.length > 10) {
587
- lines.push(`- ... and ${summary.commits.length - 10} more commits`);
588
- }
589
- lines.push('');
590
- }
591
-
592
- // Annotations
593
- const { learnings, issuesFixed, pending } = summary.annotations;
594
-
595
- if (learnings) {
596
- lines.push('## Learnings');
597
- lines.push('');
598
- lines.push(learnings);
599
- lines.push('');
600
- }
601
-
602
- if (issuesFixed) {
603
- lines.push('## Issues Fixed');
604
- lines.push('');
605
- lines.push(issuesFixed);
606
- lines.push('');
607
- }
608
-
609
- if (pending) {
610
- lines.push('## Pending');
611
- lines.push('');
612
- lines.push(pending);
613
- lines.push('');
614
- }
615
-
616
- // Execution Loops & Health Findings
617
- if (summary.historyHealth) {
618
- const hh = summary.historyHealth;
619
- lines.push('## Execution Loops & Health Findings');
620
- lines.push('');
621
-
622
- if (hh.loops.length === 0 && hh.toolCalls === 0) {
623
- lines.push('- **Status:** ✅ Clean — No execution loops or tool calls recorded for this date.');
624
- lines.push('');
625
- } else {
626
- lines.push('| Metric | Value |');
627
- lines.push('|--------|-------|');
628
- lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
629
- lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
630
- lines.push(`| Detected Loops (Repeats ≥ 3) | ${hh.loops.length} |`);
631
- lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
632
- lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
633
- lines.push('');
634
-
635
- if (hh.loops.length > 0) {
636
- lines.push('### Detected Execution Loops');
637
- lines.push('');
638
- for (const lp of hh.loops.slice(0, 10)) {
639
- const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} → #${lp.toSeq})` : '';
640
- const argsHint =
641
- lp.argsDigest === '74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b'
642
- ? 'empty/unrecorded arguments'
643
- : lp.argsDigest.length > 28
644
- ? `${lp.argsDigest.slice(0, 24)}...`
645
- : lp.argsDigest;
646
- lines.push(
647
- `- \`${lp.toolName || 'unknown'}\` × **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`,
648
- );
649
- lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
650
- }
651
- lines.push('');
652
- }
653
-
654
- if (hh.remediationProposals.length > 0) {
655
- lines.push('### Auto-Healing Remediation Proposals');
656
- lines.push('');
657
- lines.push('To remediate root causes and prevent recurring token waste, execute:');
658
- lines.push('');
659
- lines.push('```bash');
660
- for (const prop of hh.remediationProposals) {
661
- lines.push(`# ${prop.title} [${prop.key}]`);
662
- lines.push(prop.command);
663
- lines.push('');
664
- }
665
- lines.push('```');
666
- lines.push('');
667
- }
668
- }
669
- }
670
-
671
- // History report path (R7 — surfaces the newest nightly-run artifact).
672
- if (summary.historyReportPath) {
673
- lines.push('## History Report');
674
- lines.push('');
675
- lines.push(`- **Newest artifact:** ${summary.historyReportPath}`);
676
- lines.push('');
677
- }
678
-
679
- // Footer
680
- lines.push('---');
681
- lines.push('');
682
- lines.push(`*Generated by sp:daily-summary at ${summary.generatedAt}*`);
683
-
684
- return lines.join('\n');
685
- }
686
-
687
- // ─── File Output ─────────────────────────────────────────────────────────────
688
-
689
- export function ensureDir(path: string): void {
690
- if (!existsSync(path)) {
691
- mkdirSync(path, { recursive: true });
692
- }
693
- }
694
-
695
- export function writeSummary(markdown: string, options: CliOptions): string {
696
- const filename = `summary_${options.date.replace(/-/g, '')}.md`;
697
- const outputPath = options.outputPath || join(DAILY_DIR, filename);
698
-
699
- ensureDir(join(outputPath, '..'));
700
-
701
- writeFileSync(outputPath, markdown, 'utf-8');
702
-
703
- return outputPath;
704
- }
705
-
706
- // ─── Main ────────────────────────────────────────────────────────────────────
707
-
708
- /**
709
- * Resolve the newest history report artifact path by following the
710
- * `.spur/reports/history/latest.json` symlink (task 0471 R7). Returns
711
- * `undefined` when no report exists — the daily summary simply omits the
712
- * section rather than failing.
713
- */
714
- function resolveHistoryReportPath(): string | undefined {
715
- const pointer = resolve(process.cwd(), '.spur', 'reports', 'history', 'latest.json');
716
- if (!existsSync(pointer)) {
717
- return undefined;
718
- }
719
- try {
720
- const target = readlinkSync(pointer);
721
- const resolved = isAbsolute(target) ? target : resolve(dirname(pointer), target);
722
- return existsSync(resolved) ? resolved : undefined;
723
- } catch {
724
- // Not a symlink or unreadable — treat as absent.
725
- return undefined;
726
- }
727
- }
728
-
729
- export async function buildDailySummary(options: CliOptions): Promise<DailySummary> {
730
- const platforms: string[] = [];
731
-
732
- // Get token usage from ccusage
733
- let tokenUsage: DailySummary['tokenUsage'];
734
-
735
- if (!options.skipCcusage) {
736
- const ccusageData = await getCcusageData(options.date);
737
- if (ccusageData?.totals) {
738
- const totals = ccusageData.totals;
739
- tokenUsage = {
740
- inputTokens: totals.inputTokens,
741
- outputTokens: totals.outputTokens,
742
- cacheTokens: totals.cacheCreationTokens + totals.cacheReadTokens,
743
- totalTokens: totals.totalTokens,
744
- costUsd: totals.totalCost,
745
- };
746
- platforms.push('Claude Code');
747
- }
748
- }
749
-
750
- // Get git history
751
- let gitActivity: DailySummary['gitActivity'];
752
- let commits: GitCommit[] = [];
753
-
754
- if (!options.skipGit) {
755
- commits = await getGitCommits(options.date);
756
- if (commits.length > 0) {
757
- gitActivity = commits.reduce(
758
- (acc, commit) => ({
759
- commitCount: acc.commitCount + 1,
760
- filesChanged: acc.filesChanged + (commit.filesChanged || 0),
761
- insertions: acc.insertions + (commit.insertions || 0),
762
- deletions: acc.deletions + (commit.deletions || 0),
763
- }),
764
- { commitCount: 0, filesChanged: 0, insertions: 0, deletions: 0 },
765
- );
766
- platforms.push('Git');
767
- }
768
- }
769
-
770
- // R7 — surface the newest history report artifact path (if present).
771
- const historyReportPath = resolveHistoryReportPath();
772
-
773
- // Get user annotations
774
- const annotations = await promptUser();
775
-
776
- const result: DailySummary = {
777
- date: options.date,
778
- platforms,
779
- commits,
780
- annotations,
781
- historyReportPath,
782
- generatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
783
- };
784
-
785
- if (tokenUsage !== undefined) {
786
- result.tokenUsage = tokenUsage;
787
- }
788
- if (gitActivity !== undefined) {
789
- result.gitActivity = gitActivity;
790
- }
791
-
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).
795
- const historyHealth = await getSpurHistoryHealth(options.date);
796
- if (historyHealth) {
797
- result.historyHealth = historyHealth;
798
- platforms.push('Spur History');
799
- }
800
-
801
- return result;
802
- }
803
-
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
- }
816
- const options = parseArgs();
817
-
818
- logger.info(`Generating daily summary for ${options.date}...`);
819
-
820
- try {
821
- const summary = await buildDailySummary(options);
822
- const markdown = generateMarkdown(summary);
823
-
824
- if (options.dryRun) {
825
- console.log(`\n${markdown}\n`);
826
- logger.info('(dry-run) Summary not written to file');
827
- } else {
828
- const outputPath = writeSummary(markdown, options);
829
- console.log(`\n${markdown}\n`);
830
- console.log(`\n✅ Summary written to: ${outputPath}`);
831
- }
832
-
833
- // Print summary stats
834
- console.log('\n📊 Summary Statistics:');
835
- console.log(` Date: ${summary.date}`);
836
- console.log(` Platforms: ${summary.platforms.join(', ') || 'none'}`);
837
- if (summary.tokenUsage) {
838
- console.log(` Tokens: ${summary.tokenUsage.totalTokens.toLocaleString()}`);
839
- console.log(` Cost: $${summary.tokenUsage.costUsd.toFixed(4)}`);
840
- }
841
- if (summary.gitActivity) {
842
- console.log(` Commits: ${summary.gitActivity.commitCount}`);
843
- }
844
- } catch (error) {
845
- logger.error(`Failed to generate summary: ${error}`);
846
- process.exit(1);
847
- }
848
- }
849
-
850
- if (import.meta.main) {
851
- main().catch((error) => {
852
- logger.error(`Daily summary failed: ${error}`);
853
- process.exit(1);
854
- });
855
- }