@tekyzinc/gsd-t 3.29.11 → 4.0.12

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 (108) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t.js +48 -376
  4. package/bin/journey-coverage.cjs +6 -3
  5. package/bin/parallel-cli.cjs +13 -1
  6. package/commands/gsd-t-debug.md +47 -533
  7. package/commands/gsd-t-design-decompose.md +24 -497
  8. package/commands/gsd-t-doc-ripple.md +23 -139
  9. package/commands/gsd-t-execute.md +41 -958
  10. package/commands/gsd-t-help.md +0 -2
  11. package/commands/gsd-t-impact.md +26 -295
  12. package/commands/gsd-t-integrate.md +44 -369
  13. package/commands/gsd-t-milestone.md +25 -124
  14. package/commands/gsd-t-partition.md +28 -539
  15. package/commands/gsd-t-plan.md +26 -472
  16. package/commands/gsd-t-prd.md +25 -311
  17. package/commands/gsd-t-quick.md +1 -1
  18. package/commands/gsd-t-resume.md +0 -33
  19. package/commands/gsd-t-verify.md +52 -569
  20. package/commands/gsd-t-wave.md +41 -430
  21. package/package.json +1 -1
  22. package/scripts/gsd-t-calibration-hook.js +3 -1
  23. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  24. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  25. package/templates/CLAUDE-global.md +43 -173
  26. package/templates/CLAUDE-project.md +118 -104
  27. package/templates/prompts/design-verify-subagent.md +3 -0
  28. package/templates/prompts/qa-subagent.md +3 -0
  29. package/templates/prompts/red-team-subagent.md +5 -2
  30. package/templates/workflows/_lib.js +176 -0
  31. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  32. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  33. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  34. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  35. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  36. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  37. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  38. package/bin/check-headless-sessions.js +0 -140
  39. package/bin/context-budget-audit.cjs +0 -447
  40. package/bin/context-meter-config.cjs +0 -101
  41. package/bin/context-meter-config.test.cjs +0 -101
  42. package/bin/event-stream.cjs +0 -205
  43. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  44. package/bin/gsd-t-capture-lint.cjs +0 -440
  45. package/bin/gsd-t-economics.cjs +0 -315
  46. package/bin/gsd-t-in-session-usage.cjs +0 -213
  47. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  48. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  49. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  50. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  51. package/bin/gsd-t-orchestrator.js +0 -535
  52. package/bin/gsd-t-parallel-probe.cjs +0 -132
  53. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  54. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  55. package/bin/gsd-t-report-tokens.cjs +0 -549
  56. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  57. package/bin/gsd-t-token-backfill.cjs +0 -366
  58. package/bin/gsd-t-token-capture.cjs +0 -321
  59. package/bin/gsd-t-token-dashboard.cjs +0 -353
  60. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  61. package/bin/gsd-t-tool-attribution.cjs +0 -377
  62. package/bin/gsd-t-tool-cost.cjs +0 -195
  63. package/bin/gsd-t-transcript-tee.cjs +0 -246
  64. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  65. package/bin/gsd-t-unattended-platform.cjs +0 -551
  66. package/bin/gsd-t-unattended-platform.js +0 -381
  67. package/bin/gsd-t-unattended-safety.cjs +0 -773
  68. package/bin/gsd-t-unattended-safety.js +0 -788
  69. package/bin/gsd-t-unattended.cjs +0 -2109
  70. package/bin/gsd-t-unattended.js +0 -1367
  71. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  72. package/bin/handoff-lock.cjs +0 -249
  73. package/bin/handoff-lock.js +0 -249
  74. package/bin/headless-auto-spawn.cjs +0 -711
  75. package/bin/headless-auto-spawn.js +0 -373
  76. package/bin/headless-exit-codes.cjs +0 -67
  77. package/bin/live-activity-report.cjs +0 -615
  78. package/bin/log-tail.cjs +0 -81
  79. package/bin/m44-proof-measure.cjs +0 -285
  80. package/bin/m46-iter-proof.cjs +0 -149
  81. package/bin/m46-worker-proof.cjs +0 -201
  82. package/bin/m55-substrate-proof.cjs +0 -134
  83. package/bin/metrics-rollup.js +0 -200
  84. package/bin/model-windows.cjs +0 -99
  85. package/bin/model-windows.test.cjs +0 -75
  86. package/bin/parallelism-report.cjs +0 -535
  87. package/bin/runway-estimator.cjs +0 -242
  88. package/bin/spawn-plan-derive.cjs +0 -163
  89. package/bin/spawn-plan-status-updater.cjs +0 -292
  90. package/bin/spawn-plan-writer.cjs +0 -204
  91. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  92. package/bin/token-budget.cjs +0 -265
  93. package/bin/unattended-watch-format.cjs +0 -178
  94. package/bin/watch-progress.js +0 -155
  95. package/commands/gsd-t-unattended-stop.md +0 -85
  96. package/commands/gsd-t-unattended-watch.md +0 -465
  97. package/commands/gsd-t-unattended.md +0 -476
  98. package/commands/gsd-t-visualize.md +0 -116
  99. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  100. package/scripts/gsd-t-context-meter.js +0 -341
  101. package/scripts/gsd-t-context-meter.test.js +0 -471
  102. package/scripts/gsd-t-dashboard-server.js +0 -1203
  103. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  104. package/scripts/gsd-t-transcript.html +0 -1821
  105. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  106. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  107. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  108. package/templates/hooks/post-commit-spawn-plan.sh +0 -85
@@ -1,549 +0,0 @@
1
- 'use strict';
2
- /**
3
- * GSD-T Token-Usage Optimization Report Generator (M44)
4
- *
5
- * Emits a markdown report at `.gsd-t/reports/token-usage-{YYYY-MM-DD}.md`,
6
- * organized per the canonical 5-level hierarchy:
7
- *
8
- * Run → Iter → Context Window (CW) → Turn → Tool
9
- *
10
- * CW is the primary optimization unit (peer of Iter, not a partition of it).
11
- * The goal is to answer: "where am I burning tokens, and which CWs am I
12
- * getting full value out of vs. wasting?"
13
- *
14
- * Data sources (all read-only):
15
- * - `.gsd-t/metrics/token-usage.jsonl` — per-turn token rows (schema v2)
16
- * - `.gsd-t/metrics/compactions.jsonl` — compaction events (live + backfill)
17
- * - `.gsd-t/events/YYYY-MM-DD.jsonl` — tool_call events (for Section B)
18
- *
19
- * No cost columns (user is on Max subscription; tokens are the budget proxy).
20
- * V1 markdown only — no HTML, no JSON dump, no dashboard widget.
21
- *
22
- * Zero external deps. `.cjs` for ESM/CJS compat.
23
- *
24
- * Exports:
25
- * - generateReport({ projectDir, outPath?, date? }) → { path, summary }
26
- * - groupIntoCWs({ turnRows, compactionRows, sessionIds? }) → Array<CW>
27
- * - rollupCW(cw) → CW rollup
28
- * - topNExpensiveTurns(turnRows, n = 20) → Array<Turn>
29
- * - groupCompactionEvents(compactionRows, turnRows) → Array<CompactionRow>
30
- * - renderMarkdown({ cws, toolRollup, topTurns, compactions, meta }) → string
31
- */
32
-
33
- const fs = require('fs');
34
- const path = require('path');
35
-
36
- const attribution = require('./gsd-t-tool-attribution.cjs');
37
-
38
- // ── JSONL helpers ────────────────────────────────────────────────────
39
-
40
- function _safeParse(line) {
41
- const s = (line || '').trim();
42
- if (!s || s[0] !== '{') return null;
43
- try { return JSON.parse(s); } catch (_) { return null; }
44
- }
45
-
46
- function _readJsonl(p) {
47
- if (!p || !fs.existsSync(p)) return [];
48
- const text = fs.readFileSync(p, 'utf8');
49
- const out = [];
50
- for (const line of text.split('\n')) {
51
- const j = _safeParse(line);
52
- if (j) out.push(j);
53
- }
54
- return out;
55
- }
56
-
57
- function _parseMs(s) {
58
- if (!s) return NaN;
59
- if (/\dT\d/.test(s)) return Date.parse(s);
60
- if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(s)) return Date.parse(s.replace(' ', 'T') + ':00');
61
- const p = Date.parse(s);
62
- return Number.isFinite(p) ? p : NaN;
63
- }
64
-
65
- function _shortSid(sid) {
66
- return String(sid || '').slice(0, 8);
67
- }
68
-
69
- function _fmtNum(n) {
70
- if (n == null || !Number.isFinite(Number(n))) return '—';
71
- const v = Math.round(Number(n));
72
- return v.toLocaleString('en-US');
73
- }
74
-
75
- function _fmtFloat(n, digits = 1) {
76
- if (n == null || !Number.isFinite(Number(n))) return '—';
77
- return Number(n).toFixed(digits);
78
- }
79
-
80
- // ── CW grouping ──────────────────────────────────────────────────────
81
- //
82
- // Rule (V1):
83
- // - Each distinct session_id with ≥1 turn row = 1 CW.
84
- // - A compaction row `{session_id: Y, prior_session_id: X}` means CW X
85
- // ended (got compacted) and CW Y began. If X has turn rows, we mark
86
- // X's endedBy as 'compaction (auto|manual)'. Otherwise ignored.
87
- // - A CW with no matching compaction on `prior_session_id = CW.sid`
88
- // ended by 'iter end' (if a later CW exists by sort order) or
89
- // 'run end' (if it's the last CW in the data).
90
- // - Dedup compactions by (ts, session_id, prior_session_id) to collapse
91
- // any overlap between live `source=compact` and historical `compact-backfill`.
92
-
93
- function groupIntoCWs({ turnRows, compactionRows, sessionIds }) {
94
- const turns = Array.isArray(turnRows) ? turnRows : [];
95
- const comps = Array.isArray(compactionRows) ? compactionRows : [];
96
-
97
- // Bucket turns by session_id. Skip rows with no session_id.
98
- const bySid = new Map();
99
- for (const r of turns) {
100
- const sid = r.session_id;
101
- if (!sid) continue;
102
- if (!bySid.has(sid)) bySid.set(sid, []);
103
- bySid.get(sid).push({ ...r, _ms: _parseMs(r.ts || r.startedAt) });
104
- }
105
-
106
- // Optional filter: narrow to a specified sessionIds set.
107
- let sids = [...bySid.keys()];
108
- if (Array.isArray(sessionIds) && sessionIds.length) {
109
- const allow = new Set(sessionIds);
110
- sids = sids.filter((s) => allow.has(s));
111
- }
112
-
113
- // Dedup compactions by (ts|session_id|prior_session_id). Live wins over
114
- // backfill if both present for the same tuple.
115
- const compMap = new Map();
116
- for (const c of comps) {
117
- const key = `${c.ts || ''}|${c.session_id || ''}|${c.prior_session_id || ''}`;
118
- const prev = compMap.get(key);
119
- if (!prev) { compMap.set(key, c); continue; }
120
- // Prefer live (source=compact) over backfill.
121
- if (prev.source === 'compact-backfill' && c.source === 'compact') compMap.set(key, c);
122
- }
123
- const compsDedup = [...compMap.values()];
124
-
125
- // Index compactions by prior_session_id — that's "the CW this compaction ended."
126
- const compByPrior = new Map();
127
- for (const c of compsDedup) {
128
- const pid = c.prior_session_id;
129
- if (!pid) continue;
130
- if (!compByPrior.has(pid)) compByPrior.set(pid, []);
131
- compByPrior.get(pid).push(c);
132
- }
133
-
134
- // Sort each session's turns chronologically, then order CWs by their
135
- // earliest-turn timestamp.
136
- const cws = [];
137
- for (const sid of sids) {
138
- const trns = bySid.get(sid) || [];
139
- if (!trns.length) continue;
140
- trns.sort((a, b) => (a._ms || 0) - (b._ms || 0));
141
- const start = trns[0]._ms;
142
- const end = trns[trns.length - 1]._ms;
143
- cws.push({ sid, turns: trns, start, end });
144
- }
145
- cws.sort((a, b) => (a.start || 0) - (b.start || 0));
146
-
147
- // Assign endedBy per CW.
148
- for (let i = 0; i < cws.length; i++) {
149
- const cw = cws[i];
150
- const matches = compByPrior.get(cw.sid) || [];
151
- if (matches.length) {
152
- // Pick the earliest compaction following this CW's last turn.
153
- const afterLast = matches
154
- .map((c) => ({ ...c, _ms: _parseMs(c.ts) }))
155
- .filter((c) => !Number.isFinite(cw.end) || !Number.isFinite(c._ms) || c._ms >= cw.end)
156
- .sort((a, b) => (a._ms || 0) - (b._ms || 0));
157
- const chosen = afterLast[0] || matches[0];
158
- const trig = chosen.trigger || 'auto';
159
- cw.endedBy = trig === 'manual' ? 'compaction (manual)' : 'compaction (auto)';
160
- cw.endedByCompaction = chosen;
161
- cw.missingTrigger = !chosen.trigger;
162
- } else if (i < cws.length - 1) {
163
- cw.endedBy = 'iter end';
164
- } else {
165
- cw.endedBy = 'run end';
166
- }
167
- }
168
-
169
- return cws;
170
- }
171
-
172
- // ── Per-CW rollup ────────────────────────────────────────────────────
173
-
174
- function rollupCW(cw) {
175
- if (!cw) return null;
176
- const trns = Array.isArray(cw.turns) ? cw.turns : [];
177
- let input = 0, output = 0, cr = 0, cc = 0, peakCtxPct = null;
178
- for (const t of trns) {
179
- input += Number(t.inputTokens || 0);
180
- output += Number(t.outputTokens || 0);
181
- cr += Number(t.cacheReadInputTokens || 0);
182
- cc += Number(t.cacheCreationInputTokens || 0);
183
- const px = (typeof t.ctxPct === 'number' && Number.isFinite(t.ctxPct)) ? t.ctxPct : null;
184
- if (px != null) {
185
- if (peakCtxPct == null || px > peakCtxPct) peakCtxPct = px;
186
- }
187
- }
188
- const avgOutPerTurn = trns.length ? (output / trns.length) : 0;
189
- return {
190
- cwId: null, // caller assigns monotonic CW# in report-time numbering
191
- sid: cw.sid,
192
- iter: cw.sid, // in V1 each session_id is its own iter
193
- start: cw.start,
194
- end: cw.end,
195
- turns: trns.length,
196
- input, output, cacheRead: cr, cacheCreation: cc,
197
- avgOutPerTurn,
198
- peakCtxPct,
199
- endedBy: cw.endedBy || 'run end',
200
- endedByCompaction: cw.endedByCompaction || null,
201
- missingTrigger: !!cw.missingTrigger,
202
- };
203
- }
204
-
205
- // ── Top-N expensive turns ─────────────────────────────────────────────
206
-
207
- function topNExpensiveTurns(turnRows, n = 20) {
208
- const rows = Array.isArray(turnRows) ? turnRows : [];
209
- const ranked = rows.map((t) => {
210
- const i = Number(t.inputTokens || 0);
211
- const o = Number(t.outputTokens || 0);
212
- return {
213
- ts: t.ts || t.startedAt || null,
214
- command: t.command || '—',
215
- step: t.step || '—',
216
- domain: t.domain || '—',
217
- task: t.task || '—',
218
- input: i,
219
- output: o,
220
- cacheRead: Number(t.cacheReadInputTokens || 0),
221
- cacheCreation: Number(t.cacheCreationInputTokens || 0),
222
- total: i + o,
223
- ctxPct: (typeof t.ctxPct === 'number' && Number.isFinite(t.ctxPct)) ? t.ctxPct : null,
224
- };
225
- });
226
- ranked.sort((a, b) => b.total - a.total);
227
- return ranked.slice(0, Math.max(0, n | 0));
228
- }
229
-
230
- // ── Compaction event enrichment ──────────────────────────────────────
231
-
232
- function groupCompactionEvents(compactionRows, turnRows) {
233
- const comps = Array.isArray(compactionRows) ? compactionRows : [];
234
- const turns = Array.isArray(turnRows) ? turnRows : [];
235
-
236
- // Precompute turns sorted by ts (ascending) — used for "what was active"
237
- // lookup (the last turn's command/domain/task before the compaction ts).
238
- const sortedTurns = turns
239
- .map((t) => ({ ...t, _ms: _parseMs(t.ts || t.startedAt) }))
240
- .filter((t) => Number.isFinite(t._ms))
241
- .sort((a, b) => a._ms - b._ms);
242
-
243
- // Dedup (same key as groupIntoCWs) — report Section D shows each event once.
244
- const compMap = new Map();
245
- for (const c of comps) {
246
- const key = `${c.ts || ''}|${c.session_id || ''}|${c.prior_session_id || ''}`;
247
- const prev = compMap.get(key);
248
- if (!prev) { compMap.set(key, c); continue; }
249
- if (prev.source === 'compact-backfill' && c.source === 'compact') compMap.set(key, c);
250
- }
251
- const dedup = [...compMap.values()];
252
-
253
- const out = dedup.map((c) => {
254
- const cms = _parseMs(c.ts);
255
- // Find the last turn with _ms <= cms. Because sortedTurns is ordered,
256
- // a small linear scan is fine (V1 dataset is tiny).
257
- let active = null;
258
- if (Number.isFinite(cms)) {
259
- for (let i = sortedTurns.length - 1; i >= 0; i--) {
260
- if (sortedTurns[i]._ms <= cms) { active = sortedTurns[i]; break; }
261
- }
262
- }
263
- return {
264
- ts: c.ts || null,
265
- source: c.source || null,
266
- iter: _shortSid(c.prior_session_id || c.session_id),
267
- priorSid: c.prior_session_id || null,
268
- sid: c.session_id || null,
269
- trigger: c.trigger || null,
270
- preTokens: (typeof c.preTokens === 'number') ? c.preTokens : null,
271
- postTokens: (typeof c.postTokens === 'number') ? c.postTokens : null,
272
- durationMs: (typeof c.durationMs === 'number') ? c.durationMs : null,
273
- activeCommand: active ? (active.command || '—') : '—',
274
- activeDomain: active ? (active.domain || '—') : '—',
275
- activeTask: active ? (active.task || '—') : '—',
276
- };
277
- });
278
- out.sort((a, b) => String(a.ts || '').localeCompare(String(b.ts || '')));
279
- return out;
280
- }
281
-
282
- // ── Markdown rendering ────────────────────────────────────────────────
283
-
284
- function _section(title) { return `\n## ${title}\n\n`; }
285
-
286
- function _fmtDate(ms) {
287
- if (!Number.isFinite(ms)) return '—';
288
- const d = new Date(ms);
289
- const pad = (n) => String(n).padStart(2, '0');
290
- return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
291
- }
292
-
293
- function renderMarkdown({ cws, toolRollup, topTurns, compactions, meta }) {
294
- const m = meta || {};
295
- const lines = [];
296
-
297
- // ── Header ────────────────────────────────────────────────────────
298
- lines.push(`# Token Usage Optimization Report — ${m.date || 'unknown'}`);
299
- lines.push('');
300
- lines.push('Run → Iter → **CW** → Turn → Tool (CW is the primary optimization unit)');
301
- lines.push('');
302
- lines.push(`Generated: ${m.generatedAt || new Date().toISOString()}`);
303
- lines.push(`Source: ${m.turnCount || 0} turn rows, ${m.compactionCount || 0} compaction events, ${m.toolEventCount || 0} tool-call events`);
304
- lines.push(`Sessions covered: ${m.sessionCount || 0}`);
305
- lines.push('');
306
-
307
- // ── Section A — Per-CW Rollup ─────────────────────────────────────
308
- lines.push('## A — Per-CW Rollup (PRIMARY)');
309
- lines.push('');
310
- if (!cws || !cws.length) {
311
- lines.push('_No context windows found — `.gsd-t/metrics/token-usage.jsonl` has no rows with a `session_id`._');
312
- } else {
313
- lines.push('| CW# | Iter | Start | Turns | In | Out | CacheR | CacheC | Avg Out/turn | Peak Ctx% | Ended-by |');
314
- lines.push('|---|---|---|---:|---:|---:|---:|---:|---:|---:|---|');
315
- let hasMissingTrig = false;
316
- let totalIn = 0, totalOut = 0, totalCr = 0, totalCc = 0, totalTurns = 0;
317
- let autoCount = 0;
318
- cws.forEach((raw, i) => {
319
- const r = rollupCW(raw);
320
- if (!r) return;
321
- const num = i + 1;
322
- const ended = r.endedBy + (r.missingTrigger && r.endedBy.startsWith('compaction') ? ' †' : '');
323
- if (r.missingTrigger && r.endedBy.startsWith('compaction')) hasMissingTrig = true;
324
- if (r.endedBy === 'compaction (auto)') autoCount += 1;
325
- lines.push(`| CW-${num} | ${_shortSid(r.iter)} | ${_fmtDate(r.start)} | ${r.turns} | ${_fmtNum(r.input)} | ${_fmtNum(r.output)} | ${_fmtNum(r.cacheRead)} | ${_fmtNum(r.cacheCreation)} | ${_fmtFloat(r.avgOutPerTurn, 0)} | ${r.peakCtxPct == null ? '—' : _fmtFloat(r.peakCtxPct, 1)} | ${ended} |`);
326
- totalIn += r.input; totalOut += r.output; totalCr += r.cacheRead; totalCc += r.cacheCreation; totalTurns += r.turns;
327
- });
328
- lines.push('');
329
- lines.push(`**Total across ${cws.length} CW${cws.length === 1 ? '' : 's'}**: in=${_fmtNum(totalIn)} out=${_fmtNum(totalOut)} cacheR=${_fmtNum(totalCr)} cacheC=${_fmtNum(totalCc)} turns=${totalTurns}`);
330
- lines.push(`**Average turns per CW**: ${_fmtFloat(totalTurns / cws.length, 1)}`);
331
- lines.push(`**Compaction rate**: ${autoCount}/${cws.length} CW${cws.length === 1 ? '' : 's'} ended by auto-compaction`);
332
- if (hasMissingTrig) {
333
- lines.push('');
334
- lines.push('† Compaction had no `trigger` field in the source row — mapped to `compaction (auto)` as the safe default.');
335
- }
336
- }
337
- lines.push('');
338
-
339
- // ── Section B — Tool-Tokens Rollup ────────────────────────────────
340
- lines.push('## B — Tool-Tokens Rollup');
341
- lines.push('');
342
- if (!toolRollup || toolRollup.unavailable) {
343
- lines.push(`_Tool attribution unavailable: ${toolRollup && toolRollup.reason ? toolRollup.reason : 'no joined rows'}._`);
344
- } else {
345
- // B.1 — By tool
346
- lines.push('### B.1 — By tool');
347
- lines.push('');
348
- lines.push('| Tool | Calls | In | Out | CacheR | CacheC | Avg tokens/call |');
349
- lines.push('|---|---:|---:|---:|---:|---:|---:|');
350
- for (const row of toolRollup.byTool) {
351
- const total = row.total_input + row.total_output + row.total_cache_read + row.total_cache_creation;
352
- const avg = row.turn_count ? total / row.turn_count : 0;
353
- lines.push(`| ${row.key} | ${row.turn_count} | ${_fmtNum(row.total_input)} | ${_fmtNum(row.total_output)} | ${_fmtNum(row.total_cache_read)} | ${_fmtNum(row.total_cache_creation)} | ${_fmtNum(avg)} |`);
354
- }
355
- lines.push('');
356
-
357
- // B.2 — Tool × Command cross-tab
358
- lines.push('### B.2 — Tool × Command (top 10 tools × top 5 commands, total tokens)');
359
- lines.push('');
360
- const grid = toolRollup.toolByCommand || {};
361
- const topTools = (toolRollup.topTools || []).slice(0, 10);
362
- const topCmds = (toolRollup.topCommands || []).slice(0, 5);
363
- if (!topTools.length || !topCmds.length) {
364
- lines.push('_Not enough data for a cross-tab (need ≥1 tool and ≥1 command)._');
365
- } else {
366
- lines.push(`| Tool ╲ Command | ${topCmds.join(' | ')} |`);
367
- lines.push(`|---|${topCmds.map(() => '---:').join('|')}|`);
368
- for (const tool of topTools) {
369
- const cells = topCmds.map((cmd) => {
370
- const v = grid[tool] && grid[tool][cmd];
371
- return v ? _fmtNum(v) : '—';
372
- });
373
- lines.push(`| ${tool} | ${cells.join(' | ')} |`);
374
- }
375
- }
376
- }
377
- lines.push('');
378
-
379
- // ── Section C — Top 20 Expensive Turns ────────────────────────────
380
- lines.push('## C — Top 20 Expensive Turns');
381
- lines.push('');
382
- lines.push('Ranked by `input + output` tokens descending. _Input-heavy_ rows signal context bloat; _output-heavy_ rows signal generation cost. Both columns visible so the reader can re-sort mentally.');
383
- lines.push('');
384
- if (!topTurns || !topTurns.length) {
385
- lines.push('_No turn rows to rank._');
386
- } else {
387
- lines.push('| # | ts | Command | Step | Domain | Task | In | Out | CacheR | CacheC | Total | Ctx% |');
388
- lines.push('|---:|---|---|---|---|---|---:|---:|---:|---:|---:|---:|');
389
- topTurns.forEach((t, i) => {
390
- lines.push(`| ${i+1} | ${t.ts || '—'} | ${t.command} | ${t.step} | ${t.domain} | ${t.task} | ${_fmtNum(t.input)} | ${_fmtNum(t.output)} | ${_fmtNum(t.cacheRead)} | ${_fmtNum(t.cacheCreation)} | ${_fmtNum(t.total)} | ${t.ctxPct == null ? '—' : _fmtFloat(t.ctxPct, 1)} |`);
391
- });
392
- }
393
- lines.push('');
394
-
395
- // ── Section D — Compaction Events ─────────────────────────────────
396
- lines.push('## D — Compaction Events');
397
- lines.push('');
398
- if (!compactions || !compactions.length) {
399
- lines.push('_No compaction events recorded (hook not yet capturing, or no compactions during window)._');
400
- } else {
401
- lines.push('| ts | Source | Iter (prior) | Trigger | Pre-tokens | Post-tokens | Duration (ms) | Active Command | Active Domain/Task |');
402
- lines.push('|---|---|---|---|---:|---:|---:|---|---|');
403
- let autoC = 0, manC = 0, bfC = 0;
404
- for (const c of compactions) {
405
- if (c.source === 'compact-backfill') bfC += 1;
406
- if (c.trigger === 'manual') manC += 1; else autoC += 1;
407
- const dt = `${c.activeDomain}/${c.activeTask}`;
408
- lines.push(`| ${c.ts || '—'} | ${c.source || '—'} | ${c.iter || '—'} | ${c.trigger || '—'} | ${_fmtNum(c.preTokens)} | ${_fmtNum(c.postTokens)} | ${_fmtNum(c.durationMs)} | ${c.activeCommand} | ${dt} |`);
409
- }
410
- lines.push('');
411
- lines.push(`**Summary**: ${autoC} auto, ${manC} manual, ${bfC} backfilled.`);
412
- }
413
- lines.push('');
414
-
415
- return lines.join('\n');
416
- }
417
-
418
- // ── Tool rollup preparation ───────────────────────────────────────────
419
-
420
- function _prepareToolRollup({ projectDir }) {
421
- const turnsPath = path.join(projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
422
- const eventsDir = path.join(projectDir, '.gsd-t', 'events');
423
- if (!fs.existsSync(turnsPath)) {
424
- return { unavailable: true, reason: 'token-usage.jsonl not found' };
425
- }
426
- let joined;
427
- try {
428
- joined = attribution.joinTurnsAndEvents({ turnsPath, eventsGlob: eventsDir });
429
- } catch (e) {
430
- return { unavailable: true, reason: e.message || String(e) };
431
- }
432
- if (!joined || !joined.length) {
433
- return { unavailable: true, reason: 'no joined turn/event rows' };
434
- }
435
-
436
- const byTool = attribution.aggregateByTool(joined);
437
- if (!byTool.length) return { unavailable: true, reason: 'aggregateByTool returned 0 rows' };
438
-
439
- // Build tool × command cross-tab.
440
- const toolByCommand = {};
441
- const toolTotals = new Map();
442
- const cmdTotals = new Map();
443
- for (const turn of joined) {
444
- const attr = attribution.attributeTurn(turn);
445
- const cmd = (turn.command || 'unknown');
446
- for (const a of attr.attributions) {
447
- const tool = a.tool_name || 'unknown';
448
- const total = Number(a.input_tokens_share || 0)
449
- + Number(a.output_tokens_share || 0)
450
- + Number(a.cache_read_share || 0)
451
- + Number(a.cache_creation_share || 0);
452
- if (!toolByCommand[tool]) toolByCommand[tool] = {};
453
- toolByCommand[tool][cmd] = (toolByCommand[tool][cmd] || 0) + total;
454
- toolTotals.set(tool, (toolTotals.get(tool) || 0) + total);
455
- cmdTotals.set(cmd, (cmdTotals.get(cmd) || 0) + total);
456
- }
457
- }
458
- const topTools = [...toolTotals.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k);
459
- const topCommands = [...cmdTotals.entries()].sort((a, b) => b[1] - a[1]).map(([k]) => k);
460
-
461
- return {
462
- unavailable: false,
463
- byTool,
464
- toolByCommand,
465
- topTools,
466
- topCommands,
467
- joinedCount: joined.length,
468
- };
469
- }
470
-
471
- // ── Main entry ────────────────────────────────────────────────────────
472
-
473
- function generateReport({ projectDir, outPath, date }) {
474
- if (!projectDir) projectDir = process.cwd();
475
- const today = date || new Date().toISOString().slice(0, 10);
476
-
477
- const turnsPath = path.join(projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
478
- const compPath = path.join(projectDir, '.gsd-t', 'metrics', 'compactions.jsonl');
479
- const eventsDir = path.join(projectDir, '.gsd-t', 'events');
480
-
481
- const turnRows = _readJsonl(turnsPath);
482
- const compRows = _readJsonl(compPath);
483
-
484
- // Count tool-call events across all per-day files (approximation — informational).
485
- let toolEventCount = 0;
486
- if (fs.existsSync(eventsDir) && fs.statSync(eventsDir).isDirectory()) {
487
- for (const f of fs.readdirSync(eventsDir)) {
488
- if (!/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) continue;
489
- try {
490
- const txt = fs.readFileSync(path.join(eventsDir, f), 'utf8');
491
- for (const line of txt.split('\n')) {
492
- const j = _safeParse(line);
493
- if (j && j.event_type === 'tool_call') toolEventCount += 1;
494
- }
495
- } catch (_) { /* best effort */ }
496
- }
497
- }
498
-
499
- const cws = groupIntoCWs({ turnRows, compactionRows: compRows });
500
- const topTurns = topNExpensiveTurns(turnRows, 20);
501
- const compactions = groupCompactionEvents(compRows, turnRows);
502
- const toolRollup = _prepareToolRollup({ projectDir });
503
-
504
- const sessionIds = new Set();
505
- for (const r of turnRows) if (r.session_id) sessionIds.add(r.session_id);
506
-
507
- const md = renderMarkdown({
508
- cws, toolRollup, topTurns, compactions,
509
- meta: {
510
- date: today,
511
- generatedAt: new Date().toISOString(),
512
- turnCount: turnRows.length,
513
- compactionCount: compRows.length,
514
- toolEventCount,
515
- sessionCount: sessionIds.size,
516
- },
517
- });
518
-
519
- const reportsDir = path.join(projectDir, '.gsd-t', 'reports');
520
- if (!fs.existsSync(reportsDir)) fs.mkdirSync(reportsDir, { recursive: true });
521
- const finalPath = outPath || path.join(reportsDir, `token-usage-${today}.md`);
522
- fs.writeFileSync(finalPath, md, 'utf8');
523
-
524
- return {
525
- path: finalPath,
526
- summary: {
527
- date: today,
528
- turns: turnRows.length,
529
- compactions: compRows.length,
530
- toolEvents: toolEventCount,
531
- sessions: sessionIds.size,
532
- cws: cws.length,
533
- compactionEndedCWs: cws.filter((c) => (c.endedBy || '').startsWith('compaction')).length,
534
- topTool: (toolRollup && !toolRollup.unavailable && toolRollup.byTool[0]) ? toolRollup.byTool[0].key : null,
535
- },
536
- };
537
- }
538
-
539
- module.exports = {
540
- generateReport,
541
- groupIntoCWs,
542
- rollupCW,
543
- topNExpensiveTurns,
544
- groupCompactionEvents,
545
- renderMarkdown,
546
- // exposed for tests
547
- _readJsonl,
548
- _parseMs,
549
- };