@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,129 +0,0 @@
1
- 'use strict';
2
- /**
3
- * GSD-T Token Log Regenerator (M43 D3)
4
- *
5
- * Reads `.gsd-t/metrics/token-usage.jsonl` end-to-end and writes
6
- * `.gsd-t/token-log.md` deterministically. Per metrics-schema-contract v2
7
- * §Derived Artifact, `token-log.md` is a regenerated view post-v2.
8
- *
9
- * Sort (v2 §5): startedAt asc → session_id asc → turn_id asc.
10
- * Numeric turn_ids sort numerically; mixed/non-numeric falls back to lex.
11
- *
12
- * Idempotent and deterministic: running twice produces byte-identical output.
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
-
18
- const capture = require('./gsd-t-token-capture.cjs');
19
- const { NEW_HEADER, NEW_SEP, _formatTokensCell } = capture;
20
-
21
- function _readJsonl(jsonlPath) {
22
- if (!fs.existsSync(jsonlPath)) return [];
23
- const text = fs.readFileSync(jsonlPath, 'utf8');
24
- const rows = [];
25
- for (const line of text.split('\n')) {
26
- const s = line.trim();
27
- if (!s) continue;
28
- try { rows.push(JSON.parse(s)); }
29
- catch (_) { /* skip malformed line */ }
30
- }
31
- return rows;
32
- }
33
-
34
- function _tokenCellFromRow(row) {
35
- if (!row.hasUsage) return '—';
36
- const u = {
37
- input_tokens: row.inputTokens || 0,
38
- output_tokens: row.outputTokens || 0,
39
- cache_read_input_tokens: row.cacheReadInputTokens || 0,
40
- cache_creation_input_tokens: row.cacheCreationInputTokens || 0,
41
- total_cost_usd: (typeof row.costUSD === 'number') ? row.costUSD : undefined,
42
- };
43
- return _formatTokensCell(u);
44
- }
45
-
46
- function _cmpStart(a, b) {
47
- const av = a.startedAt || '';
48
- const bv = b.startedAt || '';
49
- if (av < bv) return -1;
50
- if (av > bv) return 1;
51
- return 0;
52
- }
53
-
54
- function _cmpSession(a, b) {
55
- const av = a.session_id == null ? '' : String(a.session_id);
56
- const bv = b.session_id == null ? '' : String(b.session_id);
57
- if (av < bv) return -1;
58
- if (av > bv) return 1;
59
- return 0;
60
- }
61
-
62
- function _cmpTurn(a, b) {
63
- const av = a.turn_id == null ? '' : String(a.turn_id);
64
- const bv = b.turn_id == null ? '' : String(b.turn_id);
65
- const an = Number(av), bn = Number(bv);
66
- if (av !== '' && bv !== '' && Number.isFinite(an) && Number.isFinite(bn)) {
67
- return an - bn;
68
- }
69
- if (av < bv) return -1;
70
- if (av > bv) return 1;
71
- return 0;
72
- }
73
-
74
- function sortRows(rows) {
75
- return rows.slice().sort((a, b) =>
76
- _cmpStart(a, b) || _cmpSession(a, b) || _cmpTurn(a, b)
77
- );
78
- }
79
-
80
- function _durationCell(row) {
81
- const ms = Number(row.durationMs || 0);
82
- return `${Math.max(0, Math.round(ms / 1000))}s`;
83
- }
84
-
85
- function _renderRow(row) {
86
- const tokensCell = _tokenCellFromRow(row);
87
- const notes = (row.notes == null || row.notes === '') ? '-' : String(row.notes).replace(/\|/g, '\\|');
88
- const domain = (row.domain == null || row.domain === '') ? '-' : String(row.domain);
89
- const task = (row.task == null || row.task === '') ? '-' : String(row.task);
90
- const ctxPct = (row.ctxPct == null) ? 'N/A' : String(row.ctxPct);
91
- return `| ${row.startedAt || ''} | ${row.endedAt || ''} | ${row.command || ''} | ${row.step || ''} | ${row.model || ''} | ${_durationCell(row)} | ${tokensCell} | ${notes} | ${domain} | ${task} | ${ctxPct} |`;
92
- }
93
-
94
- function renderMarkdown(rows) {
95
- const sorted = sortRows(rows);
96
- const lines = ['# GSD-T Token Log', '', NEW_HEADER, NEW_SEP];
97
- for (const r of sorted) lines.push(_renderRow(r));
98
- lines.push('');
99
- return lines.join('\n');
100
- }
101
-
102
- /**
103
- * Regenerate token-log.md from token-usage.jsonl.
104
- * @param {object} [opts]
105
- * @param {string} [opts.projectDir='.']
106
- * @param {string} [opts.jsonlPath] override input path
107
- * @param {string} [opts.tokenLogPath] override output path
108
- * @returns {{ wrote: string, rowCount: number }}
109
- */
110
- function regenerateLog(opts = {}) {
111
- const projectDir = opts.projectDir || '.';
112
- const jsonlPath = opts.jsonlPath || path.join(projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
113
- const tokenLogPath = opts.tokenLogPath || path.join(projectDir, '.gsd-t', 'token-log.md');
114
- const rows = _readJsonl(jsonlPath);
115
- const markdown = renderMarkdown(rows);
116
- const dir = path.dirname(tokenLogPath);
117
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
118
- fs.writeFileSync(tokenLogPath, markdown);
119
- return { wrote: tokenLogPath, rowCount: rows.length };
120
- }
121
-
122
- module.exports = {
123
- regenerateLog,
124
- renderMarkdown,
125
- sortRows,
126
- _readJsonl,
127
- _renderRow,
128
- _tokenCellFromRow,
129
- };
@@ -1,377 +0,0 @@
1
- 'use strict';
2
- /**
3
- * GSD-T Per-Tool Attribution Library (M43 D2)
4
- *
5
- * Joins per-turn usage rows (`.gsd-t/metrics/token-usage.jsonl`) with tool_call
6
- * events (`.gsd-t/events/YYYY-MM-DD.jsonl`) and attributes each turn's tokens
7
- * to the tools called during that turn via the output-byte ratio algorithm.
8
- *
9
- * Contract: `.gsd-t/contracts/tool-attribution-contract.md` v1.0.0.
10
- *
11
- * Zero external deps. `.cjs` for ESM/CJS compat.
12
- *
13
- * Exports:
14
- * - joinTurnsAndEvents({ turnsPath, eventsGlob, since?, milestone? })
15
- * - attributeTurn(turn)
16
- * - aggregateByTool(rows)
17
- * - aggregateByCommand(rows)
18
- * - aggregateByDomain(rows)
19
- */
20
-
21
- const fs = require('fs');
22
- const path = require('path');
23
-
24
- // ── JSONL helpers ────────────────────────────────────────────────────
25
-
26
- function _safeParse(line) {
27
- const s = (line || '').trim();
28
- if (!s || s[0] !== '{') return null;
29
- try { return JSON.parse(s); } catch (_) { return null; }
30
- }
31
-
32
- function _readJsonl(p) {
33
- if (!p || !fs.existsSync(p)) return [];
34
- const text = fs.readFileSync(p, 'utf8');
35
- const out = [];
36
- for (const line of text.split('\n')) {
37
- const j = _safeParse(line);
38
- if (j) out.push(j);
39
- }
40
- return out;
41
- }
42
-
43
- // ── Event-glob resolution ────────────────────────────────────────────
44
-
45
- function _resolveEventFiles(eventsGlob, since) {
46
- if (Array.isArray(eventsGlob)) return eventsGlob.filter((p) => p && fs.existsSync(p));
47
- if (!eventsGlob) return [];
48
- let stat;
49
- try { stat = fs.statSync(eventsGlob); } catch (_) { return []; }
50
- if (stat.isFile()) return [eventsGlob];
51
- if (stat.isDirectory()) {
52
- const entries = fs.readdirSync(eventsGlob).filter((f) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
53
- const sinceDay = since && /^\d{4}-\d{2}-\d{2}$/.test(since) ? since : null;
54
- const filtered = sinceDay ? entries.filter((f) => f.slice(0, 10) >= sinceDay) : entries;
55
- return filtered.sort().map((f) => path.join(eventsGlob, f));
56
- }
57
- return [];
58
- }
59
-
60
- // ── Turn-row normalization ───────────────────────────────────────────
61
-
62
- function _normalizeTurn(row) {
63
- const usage = {
64
- input_tokens: Number(row.inputTokens || 0),
65
- output_tokens: Number(row.outputTokens || 0),
66
- cache_read: Number(row.cacheReadInputTokens || 0),
67
- cache_creation: Number(row.cacheCreationInputTokens || 0),
68
- cost_usd: (typeof row.costUSD === 'number' && Number.isFinite(row.costUSD)) ? row.costUSD : null,
69
- };
70
- const hasUsage = !!row.hasUsage;
71
- const anyTokens = usage.input_tokens > 0 || usage.output_tokens > 0 ||
72
- usage.cache_read > 0 || usage.cache_creation > 0;
73
- if (!hasUsage && !anyTokens) return null;
74
- return {
75
- turn_id: row.turn_id != null ? String(row.turn_id) : null,
76
- session_id: row.session_id != null ? String(row.session_id) : null,
77
- ts: row.ts || row.startedAt || null,
78
- startedAt: row.startedAt || null,
79
- command: row.command || null,
80
- domain: row.domain || null,
81
- milestone: row.milestone || null,
82
- usage,
83
- };
84
- }
85
-
86
- // ── Turn-window matching ─────────────────────────────────────────────
87
-
88
- function _assignToolCallsToTurns(turnsBySession, eventsBySession) {
89
- const assignments = new Map();
90
- for (const [sid, turns] of turnsBySession.entries()) {
91
- if (!turns.length) continue;
92
- const events = eventsBySession.get(sid) || [];
93
- events.sort((a, b) => (a._ms - b._ms));
94
- let ti = 0;
95
- for (const ev of events) {
96
- while (ti + 1 < turns.length && turns[ti + 1]._ms <= ev._ms) ti += 1;
97
- const currentTurn = turns[ti];
98
- if (ti === 0 && currentTurn._ms > ev._ms) continue;
99
- const key = `${sid} ${currentTurn.turn_id}`;
100
- if (!assignments.has(key)) assignments.set(key, []);
101
- assignments.get(key).push({
102
- tool_name: ev.tool_name || null,
103
- ts: ev.ts,
104
- bytes: Number.isFinite(ev.bytes) ? Math.max(0, Number(ev.bytes)) : 0,
105
- });
106
- }
107
- }
108
- return assignments;
109
- }
110
-
111
- function _parseMs(s) {
112
- if (!s) return NaN;
113
- if (/\dT\d/.test(s)) return Date.parse(s);
114
- if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(s)) return Date.parse(s.replace(' ', 'T') + ':00');
115
- const p = Date.parse(s);
116
- return Number.isFinite(p) ? p : NaN;
117
- }
118
-
119
- // ── Public API: joinTurnsAndEvents ───────────────────────────────────
120
-
121
- function joinTurnsAndEvents(opts) {
122
- opts = opts || {};
123
- const turnsPath = opts.turnsPath;
124
- if (!turnsPath) throw new Error('joinTurnsAndEvents: turnsPath required');
125
-
126
- const rawTurns = _readJsonl(turnsPath);
127
- const turns = [];
128
- for (const row of rawTurns) {
129
- if (!row.session_id || !row.turn_id) continue;
130
- const n = _normalizeTurn(row);
131
- if (!n) continue;
132
- if (opts.since) {
133
- const day = String(n.startedAt || '').slice(0, 10);
134
- if (day && day < opts.since) continue;
135
- }
136
- if (opts.milestone) {
137
- if (!n.milestone || n.milestone !== opts.milestone) continue;
138
- }
139
- // Prefer ts (ISO UTC — exact write time of the row) over startedAt
140
- // ('YYYY-MM-DD HH:MM' local minute-precision). Events carry ISO UTC, so
141
- // ts → ts comparison is timezone-safe. startedAt remains the display
142
- // field but isn't the join key.
143
- n._ms = _parseMs(n.ts || n.startedAt);
144
- turns.push(n);
145
- }
146
-
147
- // Dedup by (session_id, turn_id).
148
- const seen = new Set();
149
- const turnsDedup = [];
150
- for (const t of turns) {
151
- const k = `${t.session_id} ${t.turn_id}`;
152
- if (seen.has(k)) continue;
153
- seen.add(k);
154
- turnsDedup.push(t);
155
- }
156
-
157
- const turnsBySession = new Map();
158
- for (const t of turnsDedup) {
159
- if (!turnsBySession.has(t.session_id)) turnsBySession.set(t.session_id, []);
160
- turnsBySession.get(t.session_id).push(t);
161
- }
162
- for (const arr of turnsBySession.values()) {
163
- arr.sort((a, b) => (a._ms || 0) - (b._ms || 0));
164
- }
165
-
166
- // Build a valid (session_id, turn_id) set so we can route events that carry
167
- // a turn_id straight to their turn without timestamp-window matching.
168
- const turnKeys = new Set();
169
- for (const t of turnsDedup) turnKeys.add(`${t.session_id} ${t.turn_id}`);
170
-
171
- const eventFiles = _resolveEventFiles(opts.eventsGlob, opts.since);
172
- const eventsBySession = new Map();
173
- // Direct assignments (M43 D2-fix): events that carry turn_id bypass the
174
- // timestamp-window heuristic and map straight to (session_id, turn_id).
175
- const directAssignments = new Map();
176
- for (const f of eventFiles) {
177
- const rows = _readJsonl(f);
178
- for (const e of rows) {
179
- if (e.event_type !== 'tool_call') continue;
180
- const sid = e.agent_id;
181
- if (!sid || !turnsBySession.has(sid)) continue;
182
-
183
- const call = {
184
- tool_name: e.reasoning || null,
185
- ts: e.ts,
186
- bytes: Number.isFinite(e.bytes) ? e.bytes :
187
- Number.isFinite(e.result_bytes) ? e.result_bytes : 0,
188
- };
189
-
190
- // Prefer direct turn_id join when the event carries it and the turn
191
- // exists in the dataset. Events written before this fix won't have
192
- // turn_id — they fall through to the timestamp-window matcher below.
193
- if (e.turn_id) {
194
- const key = `${sid} ${e.turn_id}`;
195
- if (turnKeys.has(key)) {
196
- if (!directAssignments.has(key)) directAssignments.set(key, []);
197
- directAssignments.get(key).push(call);
198
- continue;
199
- }
200
- // turn_id set but no matching turn row — fall through so we still
201
- // attribute this event via the window matcher (defensive, rare).
202
- }
203
-
204
- if (!eventsBySession.has(sid)) eventsBySession.set(sid, []);
205
- eventsBySession.get(sid).push({ ...call, _ms: _parseMs(e.ts) });
206
- }
207
- }
208
-
209
- const assignments = _assignToolCallsToTurns(turnsBySession, eventsBySession);
210
- // Merge direct assignments on top of the window-matched ones. Direct wins
211
- // (authoritative) — both shouldn't fire for the same event.
212
- for (const [key, calls] of directAssignments.entries()) {
213
- if (!assignments.has(key)) assignments.set(key, []);
214
- for (const c of calls) assignments.get(key).push(c);
215
- }
216
-
217
- const out = [];
218
- for (const [sid, arr] of turnsBySession.entries()) {
219
- for (const t of arr) {
220
- const key = `${sid} ${t.turn_id}`;
221
- const calls = assignments.get(key) || [];
222
- out.push({
223
- turn_id: t.turn_id,
224
- session_id: t.session_id,
225
- ts: t.startedAt || t.ts,
226
- command: t.command,
227
- domain: t.domain,
228
- milestone: t.milestone,
229
- usage: t.usage,
230
- tool_calls: calls,
231
- });
232
- }
233
- }
234
- return out;
235
- }
236
-
237
- // ── Public API: attributeTurn ────────────────────────────────────────
238
-
239
- function attributeTurn(turn) {
240
- if (!turn || typeof turn !== 'object') {
241
- throw new Error('attributeTurn: turn object required');
242
- }
243
- const calls = Array.isArray(turn.tool_calls) ? turn.tool_calls : [];
244
- const usage = turn.usage || {};
245
- const inT = Number(usage.input_tokens || 0);
246
- const outT = Number(usage.output_tokens || 0);
247
- const crT = Number(usage.cache_read || 0);
248
- const ccT = Number(usage.cache_creation || 0);
249
- const cost = (typeof usage.cost_usd === 'number' && Number.isFinite(usage.cost_usd)) ? usage.cost_usd : null;
250
-
251
- const base = {
252
- turn_id: turn.turn_id || null,
253
- session_id: turn.session_id || null,
254
- command: turn.command || null,
255
- domain: turn.domain || null,
256
- milestone: turn.milestone || null,
257
- attributions: [],
258
- };
259
-
260
- if (calls.length === 0) {
261
- base.attributions.push(_mkAttr('no-tool', 0, 1.0, inT, outT, crT, ccT, cost, false));
262
- return base;
263
- }
264
-
265
- let totalBytes = 0;
266
- const calls2 = calls.map((c) => {
267
- const b = Number.isFinite(c.bytes) ? Math.max(0, Number(c.bytes)) : 0;
268
- totalBytes += b;
269
- return { ...c, bytes: b };
270
- });
271
-
272
- if (totalBytes === 0) {
273
- const share = 1 / calls2.length;
274
- for (const c of calls2) {
275
- base.attributions.push(_mkAttr(
276
- c.tool_name || 'unknown',
277
- c.bytes,
278
- share,
279
- share * inT, share * outT, share * crT, share * ccT,
280
- cost == null ? null : share * cost,
281
- !!c.missing_tool_result,
282
- ));
283
- }
284
- return base;
285
- }
286
-
287
- for (const c of calls2) {
288
- const share = c.bytes / totalBytes;
289
- const missingTR = c.bytes === 0;
290
- base.attributions.push(_mkAttr(
291
- c.tool_name || 'unknown',
292
- c.bytes,
293
- share,
294
- share * inT, share * outT, share * crT, share * ccT,
295
- cost == null ? null : share * cost,
296
- missingTR,
297
- ));
298
- }
299
- return base;
300
- }
301
-
302
- function _mkAttr(name, bytes, share, inShare, outShare, crShare, ccShare, costShare, missing) {
303
- return {
304
- tool_name: name,
305
- bytes_attributed: bytes,
306
- share,
307
- input_tokens_share: inShare,
308
- output_tokens_share: outShare,
309
- cache_read_share: crShare,
310
- cache_creation_share: ccShare,
311
- cost_usd_share: costShare,
312
- missing_tool_result: !!missing,
313
- };
314
- }
315
-
316
- // ── Public API: aggregators ──────────────────────────────────────────
317
-
318
- function _aggregateBy(keyFn, joinedRows) {
319
- const rows = Array.isArray(joinedRows) ? joinedRows : [];
320
- const acc = new Map();
321
- for (const turn of rows) {
322
- const attr = attributeTurn(turn);
323
- for (const a of attr.attributions) {
324
- const k = keyFn(attr, a);
325
- if (k == null) continue;
326
- if (!acc.has(k)) {
327
- acc.set(k, {
328
- key: k,
329
- total_input: 0,
330
- total_output: 0,
331
- total_cache_read: 0,
332
- total_cache_creation: 0,
333
- total_cost_usd: 0,
334
- turn_count: 0,
335
- _turnIds: new Set(),
336
- });
337
- }
338
- const a2 = acc.get(k);
339
- a2.total_input += Number(a.input_tokens_share || 0);
340
- a2.total_output += Number(a.output_tokens_share || 0);
341
- a2.total_cache_read += Number(a.cache_read_share || 0);
342
- a2.total_cache_creation += Number(a.cache_creation_share || 0);
343
- a2.total_cost_usd += Number(a.cost_usd_share || 0);
344
- a2._turnIds.add(`${attr.session_id} ${attr.turn_id}`);
345
- }
346
- }
347
- const out = [];
348
- for (const v of acc.values()) {
349
- v.turn_count = v._turnIds.size;
350
- delete v._turnIds;
351
- out.push(v);
352
- }
353
- out.sort((a, b) => {
354
- if (b.total_cost_usd !== a.total_cost_usd) return b.total_cost_usd - a.total_cost_usd;
355
- if (b.total_output !== a.total_output) return b.total_output - a.total_output;
356
- if (a.key < b.key) return -1;
357
- if (a.key > b.key) return 1;
358
- return 0;
359
- });
360
- return out;
361
- }
362
-
363
- function aggregateByTool(joinedRows) { return _aggregateBy((_attr, a) => a.tool_name || 'unknown', joinedRows); }
364
- function aggregateByCommand(joinedRows) { return _aggregateBy((attr) => attr.command || 'unknown', joinedRows); }
365
- function aggregateByDomain(joinedRows) { return _aggregateBy((attr) => attr.domain || 'unknown', joinedRows); }
366
-
367
- module.exports = {
368
- joinTurnsAndEvents,
369
- attributeTurn,
370
- aggregateByTool,
371
- aggregateByCommand,
372
- aggregateByDomain,
373
- _readJsonl,
374
- _normalizeTurn,
375
- _resolveEventFiles,
376
- _parseMs,
377
- };
@@ -1,195 +0,0 @@
1
- 'use strict';
2
- /**
3
- * GSD-T Tool-Cost CLI (M43 D2)
4
- *
5
- * `gsd-t tool-cost [--group-by tool|command|domain] [--since YYYY-MM-DD]
6
- * [--milestone Mxx] [--format table|json]`
7
- *
8
- * Consumer of `bin/gsd-t-tool-attribution.cjs`. Zero deps.
9
- *
10
- * Exit codes: 0 success, 2 arg parse error, 3 data access error.
11
- */
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
-
16
- const attribution = require('./gsd-t-tool-attribution.cjs');
17
-
18
- function _defaultTurnsPath(projectDir) {
19
- return path.join(projectDir, '.gsd-t', 'metrics', 'token-usage.jsonl');
20
- }
21
- function _defaultEventsDir(projectDir) {
22
- return path.join(projectDir, '.gsd-t', 'events');
23
- }
24
-
25
- function parseArgs(argv) {
26
- const opts = {
27
- groupBy: 'tool',
28
- since: null,
29
- milestone: null,
30
- format: 'table',
31
- projectDir: process.cwd(),
32
- turnsPath: null,
33
- eventsGlob: null,
34
- help: false,
35
- };
36
- for (let i = 0; i < argv.length; i++) {
37
- const a = argv[i];
38
- const take = () => argv[++i];
39
- if (a === '--group-by' || a === '-g') { opts.groupBy = take(); }
40
- else if (a.startsWith('--group-by=')) { opts.groupBy = a.slice('--group-by='.length); }
41
- else if (a === '--since') { opts.since = take(); }
42
- else if (a.startsWith('--since=')) { opts.since = a.slice('--since='.length); }
43
- else if (a === '--milestone') { opts.milestone = take(); }
44
- else if (a.startsWith('--milestone=')) { opts.milestone = a.slice('--milestone='.length); }
45
- else if (a === '--format' || a === '-f') { opts.format = take(); }
46
- else if (a.startsWith('--format=')) { opts.format = a.slice('--format='.length); }
47
- else if (a === '--project-dir') { opts.projectDir = take(); }
48
- else if (a.startsWith('--project-dir=')) { opts.projectDir = a.slice('--project-dir='.length); }
49
- else if (a === '--turns-path') { opts.turnsPath = take(); }
50
- else if (a === '--events-glob') { opts.eventsGlob = take(); }
51
- else if (a === '--help' || a === '-h') { opts.help = true; }
52
- else {
53
- const err = new Error(`tool-cost: unknown arg: ${a}`);
54
- err.exitCode = 2;
55
- throw err;
56
- }
57
- }
58
- if (!['tool', 'command', 'domain'].includes(opts.groupBy)) {
59
- const err = new Error(`tool-cost: --group-by must be tool|command|domain (got: ${opts.groupBy})`);
60
- err.exitCode = 2;
61
- throw err;
62
- }
63
- if (!['table', 'json'].includes(opts.format)) {
64
- const err = new Error(`tool-cost: --format must be table|json (got: ${opts.format})`);
65
- err.exitCode = 2;
66
- throw err;
67
- }
68
- return opts;
69
- }
70
-
71
- function helpText() {
72
- return [
73
- 'Usage: gsd-t tool-cost [options]',
74
- '',
75
- 'Attribute per-turn tokens/cost across the tools used in each turn.',
76
- '',
77
- 'Options:',
78
- ' --group-by tool|command|domain Aggregation key (default: tool)',
79
- ' --since YYYY-MM-DD Only include turns on or after this day',
80
- ' --milestone Mxx Only include turns tagged with this milestone',
81
- ' --format table|json Output format (default: table)',
82
- ' --project-dir PATH Project root (default: cwd)',
83
- ' --turns-path PATH Override token-usage.jsonl path',
84
- ' --events-glob PATH Override events dir/file',
85
- ' -h, --help Show this help',
86
- '',
87
- ].join('\n');
88
- }
89
-
90
- // ── Rendering ────────────────────────────────────────────────────────
91
-
92
- function _fmtMoney(n) {
93
- if (n == null || !Number.isFinite(n)) return '-';
94
- if (n === 0) return '$0.00';
95
- if (Math.abs(n) < 0.01) return `$${n.toFixed(4)}`;
96
- return `$${n.toFixed(2)}`;
97
- }
98
-
99
- function _fmtInt(n) {
100
- if (!Number.isFinite(n)) return '0';
101
- return String(Math.round(Number(n))).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
102
- }
103
-
104
- function renderTable(agg, opts) {
105
- const top = agg.slice(0, 20);
106
- const header = opts.groupBy === 'tool' ? 'Tool' :
107
- opts.groupBy === 'command' ? 'Command' :
108
- 'Domain';
109
- const lines = [];
110
- lines.push(`═══ Tool Cost (group-by ${opts.groupBy}) ═══`);
111
- if (opts.since) lines.push(`Since: ${opts.since}`);
112
- if (opts.milestone) lines.push(`Milestone: ${opts.milestone}`);
113
- lines.push('');
114
- if (top.length === 0) {
115
- lines.push(' (no data)');
116
- return lines.join('\n');
117
- }
118
- lines.push(`${header.padEnd(22)} ${'Turns'.padStart(6)} ${'Input'.padStart(12)} ${'Output'.padStart(10)} ${'CacheR'.padStart(12)} ${'CacheC'.padStart(10)} ${'Cost'.padStart(10)}`);
119
- lines.push('─'.repeat(22 + 1 + 6 + 1 + 12 + 1 + 10 + 1 + 12 + 1 + 10 + 1 + 10));
120
- for (const r of top) {
121
- lines.push(
122
- `${String(r.key).padEnd(22).slice(0, 22)} ` +
123
- `${String(r.turn_count).padStart(6)} ` +
124
- `${_fmtInt(r.total_input).padStart(12)} ` +
125
- `${_fmtInt(r.total_output).padStart(10)} ` +
126
- `${_fmtInt(r.total_cache_read).padStart(12)} ` +
127
- `${_fmtInt(r.total_cache_creation).padStart(10)} ` +
128
- `${_fmtMoney(r.total_cost_usd).padStart(10)}`
129
- );
130
- }
131
- return lines.join('\n');
132
- }
133
-
134
- function renderJson(agg) {
135
- // Newline-delimited JSON, one ranker row per line (contract §consumer).
136
- return agg.map((r) => JSON.stringify(r)).join('\n');
137
- }
138
-
139
- // ── Main ─────────────────────────────────────────────────────────────
140
-
141
- function compute(opts) {
142
- const projectDir = opts.projectDir || '.';
143
- const turnsPath = opts.turnsPath || _defaultTurnsPath(projectDir);
144
- const eventsGlob = opts.eventsGlob || _defaultEventsDir(projectDir);
145
- if (!fs.existsSync(turnsPath)) {
146
- // Empty-sink case per contract — not an error.
147
- return [];
148
- }
149
- const joined = attribution.joinTurnsAndEvents({
150
- turnsPath,
151
- eventsGlob,
152
- since: opts.since || undefined,
153
- milestone: opts.milestone || undefined,
154
- });
155
- let agg;
156
- if (opts.groupBy === 'command') agg = attribution.aggregateByCommand(joined);
157
- else if (opts.groupBy === 'domain') agg = attribution.aggregateByDomain(joined);
158
- else agg = attribution.aggregateByTool(joined);
159
- return agg;
160
- }
161
-
162
- function run(argv) {
163
- let opts;
164
- try { opts = parseArgs(argv); }
165
- catch (e) {
166
- process.stderr.write(String(e.message || e) + '\n');
167
- return e.exitCode || 2;
168
- }
169
- if (opts.help) {
170
- process.stdout.write(helpText());
171
- return 0;
172
- }
173
- let agg;
174
- try { agg = compute(opts); }
175
- catch (e) {
176
- process.stderr.write(`tool-cost: data access error: ${e.message || e}\n`);
177
- return 3;
178
- }
179
- const out = (opts.format === 'json') ? renderJson(agg) : renderTable(agg, opts);
180
- process.stdout.write(out + '\n');
181
- return 0;
182
- }
183
-
184
- if (require.main === module) {
185
- process.exit(run(process.argv.slice(2)));
186
- }
187
-
188
- module.exports = {
189
- parseArgs,
190
- helpText,
191
- renderTable,
192
- renderJson,
193
- compute,
194
- run,
195
- };