@tekyzinc/gsd-t 3.29.11 → 4.0.14

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 (114) hide show
  1. package/CHANGELOG.md +171 -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-complete-milestone.md +6 -12
  7. package/commands/gsd-t-debug.md +47 -533
  8. package/commands/gsd-t-design-decompose.md +24 -497
  9. package/commands/gsd-t-doc-ripple.md +23 -139
  10. package/commands/gsd-t-execute.md +41 -958
  11. package/commands/gsd-t-feature.md +2 -6
  12. package/commands/gsd-t-gap-analysis.md +3 -6
  13. package/commands/gsd-t-help.md +2 -4
  14. package/commands/gsd-t-impact.md +26 -295
  15. package/commands/gsd-t-init-scan-setup.md +7 -7
  16. package/commands/gsd-t-integrate.md +44 -369
  17. package/commands/gsd-t-milestone.md +25 -124
  18. package/commands/gsd-t-partition.md +28 -539
  19. package/commands/gsd-t-plan.md +26 -472
  20. package/commands/gsd-t-prd.md +25 -311
  21. package/commands/gsd-t-quick.md +1 -1
  22. package/commands/gsd-t-resume.md +0 -33
  23. package/commands/gsd-t-scan.md +45 -652
  24. package/commands/gsd-t-verify.md +52 -569
  25. package/commands/gsd-t-wave.md +41 -430
  26. package/package.json +1 -1
  27. package/scripts/gsd-t-calibration-hook.js +3 -1
  28. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  29. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  30. package/templates/CLAUDE-global.md +44 -173
  31. package/templates/CLAUDE-project.md +118 -104
  32. package/templates/prompts/design-verify-subagent.md +3 -0
  33. package/templates/prompts/qa-subagent.md +3 -0
  34. package/templates/prompts/red-team-subagent.md +5 -2
  35. package/templates/workflows/_lib.js +176 -0
  36. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  37. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  38. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  39. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  40. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  41. package/templates/workflows/gsd-t-scan.workflow.js +711 -0
  42. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  43. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  44. package/bin/check-headless-sessions.js +0 -140
  45. package/bin/context-budget-audit.cjs +0 -447
  46. package/bin/context-meter-config.cjs +0 -101
  47. package/bin/context-meter-config.test.cjs +0 -101
  48. package/bin/event-stream.cjs +0 -205
  49. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  50. package/bin/gsd-t-capture-lint.cjs +0 -440
  51. package/bin/gsd-t-economics.cjs +0 -315
  52. package/bin/gsd-t-in-session-usage.cjs +0 -213
  53. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  54. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  55. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  56. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  57. package/bin/gsd-t-orchestrator.js +0 -535
  58. package/bin/gsd-t-parallel-probe.cjs +0 -132
  59. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  60. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  61. package/bin/gsd-t-report-tokens.cjs +0 -549
  62. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  63. package/bin/gsd-t-token-backfill.cjs +0 -366
  64. package/bin/gsd-t-token-capture.cjs +0 -321
  65. package/bin/gsd-t-token-dashboard.cjs +0 -353
  66. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  67. package/bin/gsd-t-tool-attribution.cjs +0 -377
  68. package/bin/gsd-t-tool-cost.cjs +0 -195
  69. package/bin/gsd-t-transcript-tee.cjs +0 -246
  70. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  71. package/bin/gsd-t-unattended-platform.cjs +0 -551
  72. package/bin/gsd-t-unattended-platform.js +0 -381
  73. package/bin/gsd-t-unattended-safety.cjs +0 -773
  74. package/bin/gsd-t-unattended-safety.js +0 -788
  75. package/bin/gsd-t-unattended.cjs +0 -2109
  76. package/bin/gsd-t-unattended.js +0 -1367
  77. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  78. package/bin/handoff-lock.cjs +0 -249
  79. package/bin/handoff-lock.js +0 -249
  80. package/bin/headless-auto-spawn.cjs +0 -711
  81. package/bin/headless-auto-spawn.js +0 -373
  82. package/bin/headless-exit-codes.cjs +0 -67
  83. package/bin/live-activity-report.cjs +0 -615
  84. package/bin/log-tail.cjs +0 -81
  85. package/bin/m44-proof-measure.cjs +0 -285
  86. package/bin/m46-iter-proof.cjs +0 -149
  87. package/bin/m46-worker-proof.cjs +0 -201
  88. package/bin/m55-substrate-proof.cjs +0 -134
  89. package/bin/metrics-rollup.js +0 -200
  90. package/bin/model-windows.cjs +0 -99
  91. package/bin/model-windows.test.cjs +0 -75
  92. package/bin/parallelism-report.cjs +0 -535
  93. package/bin/runway-estimator.cjs +0 -242
  94. package/bin/spawn-plan-derive.cjs +0 -163
  95. package/bin/spawn-plan-status-updater.cjs +0 -292
  96. package/bin/spawn-plan-writer.cjs +0 -204
  97. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  98. package/bin/token-budget.cjs +0 -265
  99. package/bin/unattended-watch-format.cjs +0 -178
  100. package/bin/watch-progress.js +0 -155
  101. package/commands/gsd-t-unattended-stop.md +0 -85
  102. package/commands/gsd-t-unattended-watch.md +0 -465
  103. package/commands/gsd-t-unattended.md +0 -476
  104. package/commands/gsd-t-visualize.md +0 -116
  105. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  106. package/scripts/gsd-t-context-meter.js +0 -341
  107. package/scripts/gsd-t-context-meter.test.js +0 -471
  108. package/scripts/gsd-t-dashboard-server.js +0 -1203
  109. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  110. package/scripts/gsd-t-transcript.html +0 -1821
  111. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  112. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  113. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  114. 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
- };