@tekyzinc/gsd-t 3.16.12 → 3.18.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 (53) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +13 -3
  3. package/bin/gsd-t-depgraph-validate.cjs +140 -0
  4. package/bin/gsd-t-economics.cjs +287 -0
  5. package/bin/gsd-t-file-disjointness.cjs +227 -0
  6. package/bin/gsd-t-in-session-usage.cjs +213 -0
  7. package/bin/gsd-t-orchestrator-config.cjs +100 -3
  8. package/bin/gsd-t-orchestrator.js +2 -1
  9. package/bin/gsd-t-parallel.cjs +382 -0
  10. package/bin/gsd-t-report-tokens.cjs +549 -0
  11. package/bin/gsd-t-task-graph.cjs +366 -0
  12. package/bin/gsd-t-token-capture.cjs +29 -14
  13. package/bin/gsd-t-token-dashboard.cjs +35 -0
  14. package/bin/gsd-t-tool-attribution.cjs +377 -0
  15. package/bin/gsd-t-tool-cost.cjs +195 -0
  16. package/bin/gsd-t-unattended-platform.cjs +7 -1
  17. package/bin/gsd-t-unattended.cjs +2 -0
  18. package/bin/gsd-t.js +155 -5
  19. package/bin/headless-auto-spawn.cjs +69 -49
  20. package/bin/headless-auto-spawn.js +18 -24
  21. package/bin/runway-estimator.cjs +212 -0
  22. package/bin/spawn-plan-derive.cjs +163 -0
  23. package/bin/spawn-plan-status-updater.cjs +292 -0
  24. package/bin/spawn-plan-writer.cjs +204 -0
  25. package/commands/gsd-t-debug.md +26 -7
  26. package/commands/gsd-t-execute.md +36 -28
  27. package/commands/gsd-t-help.md +11 -0
  28. package/commands/gsd-t-integrate.md +27 -7
  29. package/commands/gsd-t-quick.md +30 -13
  30. package/commands/gsd-t-scan.md +5 -5
  31. package/commands/gsd-t-unattended-watch.md +4 -3
  32. package/commands/gsd-t-unattended.md +9 -3
  33. package/commands/gsd-t-verify.md +5 -5
  34. package/commands/gsd-t-wave.md +21 -8
  35. package/commands/gsd.md +45 -3
  36. package/docs/GSD-T-README.md +43 -5
  37. package/docs/architecture.md +423 -3
  38. package/docs/requirements.md +203 -0
  39. package/package.json +1 -1
  40. package/scripts/gsd-t-calibration-hook.js +256 -0
  41. package/scripts/gsd-t-compact-detector.js +223 -0
  42. package/scripts/gsd-t-compaction-scanner.js +305 -0
  43. package/scripts/gsd-t-dashboard-autostart.cjs +172 -0
  44. package/scripts/gsd-t-dashboard-server.js +179 -0
  45. package/scripts/gsd-t-dashboard.html +3 -3
  46. package/scripts/gsd-t-heartbeat.js +50 -2
  47. package/scripts/gsd-t-post-commit-spawn-plan.sh +86 -0
  48. package/scripts/gsd-t-transcript.html +546 -43
  49. package/scripts/hooks/gsd-t-in-session-usage-hook.js +84 -0
  50. package/scripts/spawn-plan-fmt-tokens.cjs +80 -0
  51. package/templates/CLAUDE-global.md +8 -3
  52. package/templates/CLAUDE-project.md +17 -14
  53. package/templates/hooks/post-commit-spawn-plan.sh +85 -0
@@ -0,0 +1,377 @@
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
+ };
@@ -0,0 +1,195 @@
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
+ };
@@ -151,7 +151,13 @@ function isAlive(pid) {
151
151
  */
152
152
  function spawnWorker(projectDir, timeoutMs, opts = {}) {
153
153
  const bin = opts.bin || resolveClaudePath();
154
- const args = opts.args || ["-p", "/gsd-t-resume"];
154
+ const args = opts.args || [
155
+ "-p",
156
+ "/gsd-t-resume",
157
+ "--output-format", "stream-json",
158
+ "--verbose",
159
+ "--dangerously-skip-permissions",
160
+ ];
155
161
  const env = opts.env || process.env;
156
162
 
157
163
  if (typeof opts.onHeartbeatCheck === "function") {
@@ -1378,6 +1378,8 @@ function _spawnWorker(state, opts) {
1378
1378
  "",
1379
1379
  "Your job: run /gsd-t-resume but skip the unattended supervisor auto-reattach check in Step 0.",
1380
1380
  ].join("\n"),
1381
+ "--output-format", "stream-json",
1382
+ "--verbose",
1381
1383
  "--dangerously-skip-permissions",
1382
1384
  ],
1383
1385
  env: workerEnv,