@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,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
- };