@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,439 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- /**
4
- * GSD-T Conversation Capture Hook (M45 D2)
5
- *
6
- * Captures the orchestrator session's conversational turns into
7
- * `.gsd-t/transcripts/in-session-{sessionId}.ndjson` so the visualizer
8
- * left rail can list the in-session conversation alongside spawn entries.
9
- *
10
- * Installed into `~/.claude/settings.json` (SessionStart, UserPromptSubmit,
11
- * Stop, optional PostToolUse). Reads the hook payload from stdin, dispatches
12
- * on `hook_event_name`, appends a typed NDJSON frame. Writes content-level
13
- * data (not just tokens — that's the job of `gsd-t-in-session-usage-hook.js`).
14
- *
15
- * Safety:
16
- * - Never throws to the caller — catches all errors, logs to stderr, exits 0.
17
- * - `content` is capped at 16 KB per frame; over-cap writes `truncated: true`.
18
- * - Append-only; never overwrites an existing in-session NDJSON file.
19
- * - Project-dir discovery: prefers `GSD_T_PROJECT_DIR`, then decodes
20
- * `payload.transcript_path`'s `~/.claude/projects/{slug}` to a real project
21
- * root (session-specific signal — required when multiple parallel Claude
22
- * Code sessions share one node-runtime hook process), then `payload.cwd`,
23
- * then walks up from `process.cwd()` looking for `.gsd-t/progress.md`.
24
- * Silent no-op if no project dir found.
25
- *
26
- * Contract: .gsd-t/contracts/conversation-capture-contract.md v1.2.0
27
- */
28
-
29
- const fs = require('fs');
30
- const path = require('path');
31
- const crypto = require('crypto');
32
- const os = require('os');
33
-
34
- const DEFAULT_SCRIPT_GUARD_MS = 5000;
35
- const CONTENT_CAP_BYTES = 16 * 1024; // 16 KB
36
- const MAX_STDIN = 1024 * 1024; // 1 MiB defense-in-depth
37
- const started = Date.now();
38
-
39
- function _readStdin() {
40
- return new Promise((resolve) => {
41
- let buf = '';
42
- let aborted = false;
43
- process.stdin.setEncoding('utf8');
44
- process.stdin.on('data', (chunk) => {
45
- if (aborted) return;
46
- buf += chunk;
47
- if (buf.length > MAX_STDIN) {
48
- aborted = true;
49
- try { process.stdin.destroy(); } catch (_) { /* noop */ }
50
- resolve('');
51
- }
52
- });
53
- process.stdin.on('end', () => { if (!aborted) resolve(buf); });
54
- process.stdin.on('error', () => resolve(buf));
55
- setTimeout(() => resolve(buf), DEFAULT_SCRIPT_GUARD_MS).unref();
56
- });
57
- }
58
-
59
- function _parsePayload(raw) {
60
- try { return JSON.parse(raw || '{}'); } catch (_) { return null; }
61
- }
62
-
63
- function _walkUpForProject(startDir) {
64
- try {
65
- let dir = path.resolve(startDir || '.');
66
- for (let i = 0; i < 10; i++) {
67
- if (fs.existsSync(path.join(dir, '.gsd-t', 'progress.md'))) return dir;
68
- const parent = path.dirname(dir);
69
- if (parent === dir) break;
70
- dir = parent;
71
- }
72
- } catch (_) { /* swallow */ }
73
- return null;
74
- }
75
-
76
- // Decode a Claude Code project slug (the directory name under
77
- // `~/.claude/projects/`) back to an absolute project root that contains a
78
- // `.gsd-t/` directory. The slug encoding is lossy — `/` and literal `-` both
79
- // map to `-` — so we DFS-walk the filesystem, greedily consuming token runs as
80
- // directory names. First match whose `.gsd-t/` exists wins. Returns null if
81
- // nothing matches or the slug is malformed.
82
- //
83
- // Why this exists: Claude Code Stop hook payloads carry `transcript_path` =
84
- // `~/.claude/projects/{slug}/{sessionId}.jsonl`. The hook runs as one shared
85
- // node-runtime process across parallel Claude Code sessions, so `process.cwd()`
86
- // can resolve to the wrong project. The slug is the only *session-specific*
87
- // signal we have for project routing.
88
- function _slugToProjectDir(slug) {
89
- if (typeof slug !== 'string' || slug.length === 0) return null;
90
- if (slug[0] !== '-') return null; // must encode leading '/'
91
- // Reject anything that could traversal-escape after decode.
92
- if (slug.includes('/') || slug.includes('\\') || slug.includes('\0')) return null;
93
- if (slug.includes('..')) return null;
94
- const tokens = slug.slice(1).split('-'); // strip leading '-' (the leading '/')
95
- if (tokens.length === 0 || tokens.some((t) => t.length === 0)) return null;
96
- // DFS over how many '-'-separated tokens form each directory segment.
97
- // Greedy preference: try fewest tokens first (most '/' separators) so deeper
98
- // paths win when both interpretations exist.
99
- function walk(prefix, idx) {
100
- if (idx >= tokens.length) {
101
- try {
102
- if (fs.existsSync(path.join(prefix, '.gsd-t'))) return prefix;
103
- } catch (_) { /* swallow */ }
104
- return null;
105
- }
106
- for (let k = 1; k <= tokens.length - idx; k++) {
107
- const seg = tokens.slice(idx, idx + k).join('-');
108
- const next = path.join(prefix, seg);
109
- // Re-validate after path.join — defense against weird inputs.
110
- if (next.includes('..')) continue;
111
- let exists = false;
112
- try { exists = fs.existsSync(next); } catch (_) { exists = false; }
113
- if (!exists) continue;
114
- const found = walk(next, idx + k);
115
- if (found) return found;
116
- }
117
- return null;
118
- }
119
- return walk('/', 0);
120
- }
121
-
122
- // Extract the slug ({dir-name} under `~/.claude/projects/`) from a
123
- // transcript_path. Returns null on malformed input.
124
- function _slugFromTranscriptPath(p) {
125
- if (typeof p !== 'string' || !path.isAbsolute(p)) return null;
126
- const home = process.env.HOME || os.homedir();
127
- if (!home) return null;
128
- const root = path.resolve(home, '.claude', 'projects') + path.sep;
129
- const resolved = path.resolve(p);
130
- if (!resolved.startsWith(root)) return null;
131
- const rest = resolved.slice(root.length);
132
- // First path segment after the projects/ root is the slug.
133
- const sep = rest.indexOf(path.sep);
134
- const slug = sep === -1 ? rest : rest.slice(0, sep);
135
- if (!slug) return null;
136
- return slug;
137
- }
138
-
139
- function _resolveProjectDir(payload) {
140
- // 1. Explicit env override (preserved for tests + operator overrides).
141
- const env = process.env.GSD_T_PROJECT_DIR;
142
- if (env && fs.existsSync(path.join(env, '.gsd-t'))) return env;
143
- // 2. Session-specific signal: decode the transcript_path slug. This is the
144
- // ONLY source that's per-session under parallel Claude Code instances —
145
- // cwd / walk-up resolve to whichever project the hook process happens to
146
- // inherit, which misroutes frames across projects.
147
- if (payload && typeof payload.transcript_path === 'string') {
148
- const slug = _slugFromTranscriptPath(payload.transcript_path);
149
- if (slug) {
150
- const fromSlug = _slugToProjectDir(slug);
151
- if (fromSlug) return fromSlug;
152
- }
153
- }
154
- // 3. payload.cwd (Claude Code may carry it on some events).
155
- if (payload && typeof payload.cwd === 'string' && path.isAbsolute(payload.cwd)
156
- && fs.existsSync(path.join(payload.cwd, '.gsd-t'))) {
157
- return payload.cwd;
158
- }
159
- // 4. Last resort: walk up from process.cwd(). Known-unreliable for parallel
160
- // sessions sharing a node-runtime hook process — emit a one-line warning
161
- // so misroutes are diagnosable from stderr.
162
- const walked = _walkUpForProject(process.cwd());
163
- if (walked) {
164
- try {
165
- process.stderr.write(
166
- 'gsd-t-conversation-capture: project-dir resolved via cwd walk-up (' +
167
- walked + ') — unreliable for parallel sessions\n'
168
- );
169
- } catch (_) { /* noop */ }
170
- return walked;
171
- }
172
- return null;
173
- }
174
-
175
- function _resolveSessionId(payload) {
176
- if (payload && typeof payload.session_id === 'string' && payload.session_id.length > 0
177
- && !/[\/\\\0]|\.\./.test(payload.session_id)) {
178
- return payload.session_id;
179
- }
180
- // Fallback: deterministic-ish per-process hash. Stable within one process,
181
- // different across processes. Keeps the filename non-empty when Claude Code
182
- // omits session_id (shouldn't happen in practice but we must not explode).
183
- // Also used as defense-in-depth for malformed session_ids containing path
184
- // separators / `..` that would let path.join collapse the `in-session-`
185
- // prefix (protects the filename-prefix discriminator contract with the
186
- // viewer + compact-detector).
187
- const seed = String(process.pid) + ':' + String(started);
188
- return 'pid-' + crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);
189
- }
190
-
191
- function _capContent(raw) {
192
- if (raw == null) return { content: null, truncated: false };
193
- let str;
194
- if (typeof raw === 'string') str = raw;
195
- else {
196
- try { str = JSON.stringify(raw); } catch (_) { str = String(raw); }
197
- }
198
- const byteLen = Buffer.byteLength(str, 'utf8');
199
- if (byteLen <= CONTENT_CAP_BYTES) return { content: str, truncated: false };
200
- // Truncate by bytes; slice then re-decode to avoid breaking a multi-byte char.
201
- const buf = Buffer.from(str, 'utf8').subarray(0, CONTENT_CAP_BYTES);
202
- return { content: buf.toString('utf8'), truncated: true };
203
- }
204
-
205
- function _appendFrame(projectDir, sessionId, frame) {
206
- const transcriptsDir = path.join(projectDir, '.gsd-t', 'transcripts');
207
- try { fs.mkdirSync(transcriptsDir, { recursive: true }); } catch (_) { /* noop */ }
208
- const outPath = path.join(transcriptsDir, 'in-session-' + sessionId + '.ndjson');
209
- // Path-traversal guard: resolved path must stay under transcriptsDir.
210
- const resolvedOut = path.resolve(outPath);
211
- const resolvedDir = path.resolve(transcriptsDir) + path.sep;
212
- if (!resolvedOut.startsWith(resolvedDir)) return;
213
- fs.appendFileSync(outPath, JSON.stringify(frame) + '\n', 'utf8');
214
- }
215
-
216
- function _extractUserContent(payload) {
217
- // Claude Code UserPromptSubmit payload carries the prompt text.
218
- if (payload && typeof payload.prompt === 'string') return payload.prompt;
219
- if (payload && payload.message && typeof payload.message.content === 'string') {
220
- return payload.message.content;
221
- }
222
- if (payload && payload.user_message && typeof payload.user_message === 'string') {
223
- return payload.user_message;
224
- }
225
- return null;
226
- }
227
-
228
- // Tail-read the last `bytes` of a file as UTF-8. Returns '' on any error.
229
- // Used so multi-MB transcripts don't get fully loaded into RAM.
230
- function _readFileTail(filePath, bytes) {
231
- let fd = -1;
232
- try {
233
- const st = fs.statSync(filePath);
234
- if (!st.isFile()) return '';
235
- const size = st.size;
236
- if (size === 0) return '';
237
- const want = Math.min(bytes, size);
238
- const start = size - want;
239
- fd = fs.openSync(filePath, 'r');
240
- const buf = Buffer.alloc(want);
241
- fs.readSync(fd, buf, 0, want, start);
242
- let str = buf.toString('utf8');
243
- // If we sliced mid-line, drop the (possibly malformed) leading partial.
244
- if (start > 0) {
245
- const nl = str.indexOf('\n');
246
- if (nl >= 0) str = str.slice(nl + 1);
247
- }
248
- return str;
249
- } catch (_) {
250
- return '';
251
- } finally {
252
- if (fd >= 0) { try { fs.closeSync(fd); } catch (_) { /* noop */ } }
253
- }
254
- }
255
-
256
- // Validate `transcript_path` from a hook payload before reading. Stop hooks
257
- // from Claude Code put the file under `~/.claude/projects/`; we lock to that
258
- // to defeat path-traversal attempts (BUG-1 sanitizer pattern). Fail open
259
- // (return null) on anything suspicious.
260
- function _safeTranscriptPath(p) {
261
- if (typeof p !== 'string' || p.length === 0) return null;
262
- if (!path.isAbsolute(p)) return null;
263
- const home = process.env.HOME || os.homedir();
264
- if (!home) return null;
265
- const allowedRoot = path.resolve(home, '.claude', 'projects') + path.sep;
266
- const resolved = path.resolve(p);
267
- if (!resolved.startsWith(allowedRoot)) return null;
268
- return resolved;
269
- }
270
-
271
- // Pull the assistant body out of a Claude Code transcript JSONL by scanning
272
- // from the tail. Each line is one event; the latest `type === 'assistant'`
273
- // row carries the message. Concatenate all `text`-type content blocks; ignore
274
- // tool_use / tool_result / thinking blocks. Returns null if no assistant row
275
- // is found or every candidate is body-less (tool_use only).
276
- function _readAssistantFromTranscript(transcriptPath) {
277
- const safe = _safeTranscriptPath(transcriptPath);
278
- if (!safe) return null;
279
- const tail = _readFileTail(safe, 64 * 1024);
280
- if (!tail) return null;
281
- const lines = tail.split('\n');
282
- for (let i = lines.length - 1; i >= 0; i--) {
283
- const line = lines[i];
284
- if (!line) continue;
285
- let row;
286
- try { row = JSON.parse(line); } catch (_) { continue; }
287
- if (!row || row.type !== 'assistant') continue;
288
- // Skip subagent turns — orchestrator transcripts record both, but only
289
- // the orchestrator's own assistant turn belongs in this in-session file.
290
- if (row.isSidechain === true) continue;
291
- const msg = row.message;
292
- if (!msg) continue;
293
- const blocks = msg.content;
294
- if (typeof blocks === 'string') return blocks;
295
- if (!Array.isArray(blocks)) continue;
296
- const texts = [];
297
- for (const b of blocks) {
298
- if (b && b.type === 'text' && typeof b.text === 'string') texts.push(b.text);
299
- }
300
- if (texts.length === 0) continue; // tool_use-only turn — keep scanning
301
- return texts.join('');
302
- }
303
- return null;
304
- }
305
-
306
- function _extractAssistantContent(payload) {
307
- // PRIMARY: Claude Code Stop hook payload carries `transcript_path` to the
308
- // orchestrator's JSONL. Read the most recent assistant row from the tail.
309
- if (payload && typeof payload.transcript_path === 'string') {
310
- const fromTranscript = _readAssistantFromTranscript(payload.transcript_path);
311
- if (fromTranscript != null) return fromTranscript;
312
- }
313
- // Fallback shapes — kept so older / non-Claude-Code payload shapes still
314
- // work (and so unit tests can exercise the hook without a real transcript).
315
- if (payload && typeof payload.assistant_message === 'string') return payload.assistant_message;
316
- if (payload && payload.message && typeof payload.message.content === 'string') {
317
- return payload.message.content;
318
- }
319
- if (payload && typeof payload.content === 'string') return payload.content;
320
- return null;
321
- }
322
-
323
- function _buildUserFrame(payload, sessionId, ts) {
324
- const { content, truncated } = _capContent(_extractUserContent(payload));
325
- const frame = {
326
- type: 'user_turn',
327
- ts,
328
- session_id: sessionId,
329
- };
330
- if (content != null) frame.content = content;
331
- if (truncated) frame.truncated = true;
332
- if (payload && typeof payload.message_id === 'string') frame.message_id = payload.message_id;
333
- return frame;
334
- }
335
-
336
- function _buildAssistantFrame(payload, sessionId, ts) {
337
- const { content, truncated } = _capContent(_extractAssistantContent(payload));
338
- const frame = {
339
- type: 'assistant_turn',
340
- ts,
341
- session_id: sessionId,
342
- };
343
- if (content != null) frame.content = content;
344
- if (truncated) frame.truncated = true;
345
- if (payload && typeof payload.message_id === 'string') frame.message_id = payload.message_id;
346
- return frame;
347
- }
348
-
349
- function _buildSessionStartFrame(sessionId, ts) {
350
- return { type: 'session_start', ts, session_id: sessionId };
351
- }
352
-
353
- function _buildToolUseFrame(payload, sessionId, ts) {
354
- const frame = {
355
- type: 'tool_use',
356
- ts,
357
- session_id: sessionId,
358
- };
359
- if (payload && typeof payload.tool_name === 'string') frame.name = payload.tool_name;
360
- else if (payload && payload.tool && typeof payload.tool.name === 'string') frame.name = payload.tool.name;
361
- if (payload && typeof payload.tool_use_id === 'string') frame.tool_use_id = payload.tool_use_id;
362
- if (payload && typeof payload.duration_ms === 'number') frame.duration_ms = payload.duration_ms;
363
- return frame;
364
- }
365
-
366
- function _handle(payload) {
367
- if (!payload || typeof payload !== 'object') return;
368
- const event = payload.hook_event_name;
369
- if (!event) return;
370
-
371
- const projectDir = _resolveProjectDir(payload);
372
- if (!projectDir) return; // not a GSD-T project — silent no-op
373
-
374
- const sessionId = _resolveSessionId(payload);
375
- const ts = new Date().toISOString();
376
-
377
- switch (event) {
378
- case 'SessionStart': {
379
- _appendFrame(projectDir, sessionId, _buildSessionStartFrame(sessionId, ts));
380
- return;
381
- }
382
- case 'UserPromptSubmit': {
383
- _appendFrame(projectDir, sessionId, _buildUserFrame(payload, sessionId, ts));
384
- return;
385
- }
386
- case 'Stop': {
387
- _appendFrame(projectDir, sessionId, _buildAssistantFrame(payload, sessionId, ts));
388
- return;
389
- }
390
- case 'PostToolUse': {
391
- // Opt-in: guarded to keep default writes small.
392
- if (process.env.GSD_T_CAPTURE_TOOL_USES !== '1') return;
393
- _appendFrame(projectDir, sessionId, _buildToolUseFrame(payload, sessionId, ts));
394
- return;
395
- }
396
- default:
397
- return;
398
- }
399
- }
400
-
401
- async function main() {
402
- try {
403
- const raw = await _readStdin();
404
- const payload = _parsePayload(raw);
405
- if (!payload) return;
406
- _handle(payload);
407
- } catch (err) {
408
- try { process.stderr.write('gsd-t-conversation-capture: ' + (err && err.message || err) + '\n'); } catch (_) { /* noop */ }
409
- } finally {
410
- const elapsed = Date.now() - started;
411
- if (elapsed > DEFAULT_SCRIPT_GUARD_MS) process.exitCode = 0;
412
- }
413
- }
414
-
415
- if (require.main === module) {
416
- main();
417
- }
418
-
419
- module.exports = {
420
- _internal: {
421
- _parsePayload,
422
- _resolveProjectDir,
423
- _resolveSessionId,
424
- _capContent,
425
- _buildUserFrame,
426
- _buildAssistantFrame,
427
- _buildSessionStartFrame,
428
- _buildToolUseFrame,
429
- _appendFrame,
430
- _handle,
431
- _extractAssistantContent,
432
- _readAssistantFromTranscript,
433
- _safeTranscriptPath,
434
- _readFileTail,
435
- _slugFromTranscriptPath,
436
- _slugToProjectDir,
437
- CONTENT_CAP_BYTES,
438
- },
439
- };
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- /**
4
- * GSD-T In-Session Usage Hook (M43 D1-T2)
5
- *
6
- * Installed into `~/.claude/settings.json` (Stop + SessionEnd) to capture
7
- * per-turn token usage for the dialog channel. Reads the hook payload from
8
- * stdin, resolves the project directory from `payload.cwd`, then delegates to
9
- * `bin/gsd-t-in-session-usage.cjs::processHookPayload` which reads the
10
- * transcript at `payload.transcript_path` and appends schema-v2 rows to
11
- * `.gsd-t/metrics/token-usage.jsonl`.
12
- *
13
- * Silent on success (hooks run async in Claude Code — stdout is ignored).
14
- * Swallows every error: a flaky capture must never break the user's session.
15
- */
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
-
20
- const DEFAULT_SCRIPT_GUARD_MS = 5000;
21
- const started = Date.now();
22
-
23
- function _readStdin() {
24
- return new Promise((resolve) => {
25
- let buf = '';
26
- process.stdin.setEncoding('utf8');
27
- process.stdin.on('data', chunk => { buf += chunk; });
28
- process.stdin.on('end', () => resolve(buf));
29
- process.stdin.on('error', () => resolve(buf));
30
- setTimeout(() => resolve(buf), DEFAULT_SCRIPT_GUARD_MS).unref();
31
- });
32
- }
33
-
34
- function _parsePayload(raw) {
35
- try {
36
- return JSON.parse(raw || '{}');
37
- } catch (_) {
38
- return null;
39
- }
40
- }
41
-
42
- function _resolveProjectDir(payload) {
43
- if (payload && payload.cwd && fs.existsSync(payload.cwd)) return payload.cwd;
44
- if (process.cwd) return process.cwd();
45
- return '.';
46
- }
47
-
48
- function _resolveInSessionModule(projectDir) {
49
- const candidates = [
50
- path.join(projectDir, 'bin', 'gsd-t-in-session-usage.cjs'),
51
- path.join(process.env.HOME || '', '.claude', 'gsd-t', 'bin', 'gsd-t-in-session-usage.cjs'),
52
- ];
53
- for (const p of candidates) {
54
- if (fs.existsSync(p)) return p;
55
- }
56
- return null;
57
- }
58
-
59
- async function main() {
60
- try {
61
- const raw = await _readStdin();
62
- const payload = _parsePayload(raw);
63
- if (!payload) return;
64
- if (payload.hook_event_name !== 'Stop' && payload.hook_event_name !== 'SessionEnd') return;
65
-
66
- const projectDir = _resolveProjectDir(payload);
67
- const modulePath = _resolveInSessionModule(projectDir);
68
- if (!modulePath) return;
69
-
70
- const mod = require(modulePath);
71
- mod.processHookPayload({ projectDir, payload });
72
- } catch (_) {
73
- // Intentionally swallow — hook must never interrupt the user session.
74
- } finally {
75
- const elapsed = Date.now() - started;
76
- if (elapsed > DEFAULT_SCRIPT_GUARD_MS) process.exitCode = 0;
77
- }
78
- }
79
-
80
- if (require.main === module) {
81
- main();
82
- }
83
-
84
- module.exports = { _internal: { _parsePayload, _resolveProjectDir } };
@@ -1,80 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * GSD-T Spawn Plan — token formatting + cumulative totals (M44 D8 T7)
5
- *
6
- * Pure helper module used by the transcript right-side spawn-plan panel.
7
- * Extracted from the inline script in `scripts/gsd-t-transcript.html` so the
8
- * same functions can be unit-tested under Node without loading a DOM.
9
- *
10
- * Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
11
- * - Token cell format: `in=12.5k out=1.7k $0.42` (k-suffix above 1000,
12
- * 2-decimal USD). Returns `—` when tokens are null/missing.
13
- * - Cumulative totals: sum `{in,out,cr,cc,cost_usd}` across done tasks.
14
- * Returns `null` when no done-task has tokens (panel renders `—`).
15
- *
16
- * Zero external deps. `.cjs` so it loads in both ESM-default and CJS projects.
17
- */
18
-
19
- /**
20
- * k-suffix number formatter. `1500` → `1.5k`, `999` → `999`, `null` → `0`.
21
- * Used by `fmtTokens` to compact large counts.
22
- *
23
- * @param {number|string|null|undefined} n
24
- * @returns {string}
25
- */
26
- function fmtK(n) {
27
- if (n == null || Number.isNaN(Number(n))) return '0';
28
- const num = Number(n);
29
- if (Math.abs(num) >= 1000) return (num / 1000).toFixed(1) + 'k';
30
- return String(num);
31
- }
32
-
33
- /**
34
- * Render a token-cell string from a `{in, out, cr, cc, cost_usd}` object.
35
- * When `tokens` is null/undefined/non-object, returns the em-dash marker
36
- * per the "zero is a measurement, dash is acknowledged gap" rule.
37
- *
38
- * @param {object|null|undefined} tokens
39
- * @returns {string}
40
- */
41
- function fmtTokens(tokens) {
42
- if (!tokens || typeof tokens !== 'object') return '—';
43
- const ins = fmtK(tokens.in);
44
- const out = fmtK(tokens.out);
45
- const cost = (typeof tokens.cost_usd === 'number')
46
- ? '$' + tokens.cost_usd.toFixed(2)
47
- : '';
48
- return ['in=' + ins, 'out=' + out, cost].filter(Boolean).join(' ');
49
- }
50
-
51
- /**
52
- * Sum the `tokens` field across every task whose `status === 'done'` and
53
- * whose `tokens` is a populated object. Returns null when no done-task has
54
- * a tokens object attached — the panel renders that state as `—`.
55
- *
56
- * @param {Array<{status?:string, tokens?:object}>|null|undefined} tasks
57
- * @returns {{in:number,out:number,cr:number,cc:number,cost_usd:number}|null}
58
- */
59
- function sumTokens(tasks) {
60
- const acc = { in: 0, out: 0, cr: 0, cc: 0, cost_usd: 0 };
61
- let hit = false;
62
- for (const t of (tasks || [])) {
63
- if (!t || t.status !== 'done' || !t.tokens) continue;
64
- acc.in += Number(t.tokens.in || 0);
65
- acc.out += Number(t.tokens.out || 0);
66
- acc.cr += Number(t.tokens.cr || 0);
67
- acc.cc += Number(t.tokens.cc || 0);
68
- acc.cost_usd += Number(t.tokens.cost_usd || 0);
69
- hit = true;
70
- }
71
- if (!hit) return null;
72
- acc.cost_usd = Math.round(acc.cost_usd * 100) / 100;
73
- return acc;
74
- }
75
-
76
- module.exports = {
77
- fmtK,
78
- fmtTokens,
79
- sumTokens,
80
- };
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # GSD-T Post-Commit Spawn-Plan Hook — installed by `gsd-t init`
3
- #
4
- # This is a thin shim that delegates to the canonical hook script
5
- # shipped with GSD-T. If your project doesn't yet have the canonical
6
- # script, this template copy handles commits by itself; once the
7
- # framework script is in place, the framework wins.
8
- #
9
- # Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
10
-
11
- set +e # silent-fail — never break the commit
12
-
13
- PROJECT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)"
14
- if [ -z "$PROJECT_DIR" ] || [ ! -d "$PROJECT_DIR/.gsd-t/spawns" ]; then
15
- exit 0
16
- fi
17
-
18
- CANONICAL="$PROJECT_DIR/scripts/gsd-t-post-commit-spawn-plan.sh"
19
- if [ -x "$CANONICAL" ]; then
20
- # Delegate to the framework-shipped hook.
21
- "$CANONICAL"
22
- exit 0
23
- fi
24
-
25
- UPDATER="$PROJECT_DIR/bin/spawn-plan-status-updater.cjs"
26
- if [ ! -f "$UPDATER" ]; then
27
- exit 0
28
- fi
29
-
30
- if ! command -v node >/dev/null 2>&1; then
31
- echo "[spawn-plan-hook] node not found — skipping" 1>&2
32
- exit 0
33
- fi
34
-
35
- COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null)"
36
- COMMIT_MSG="$(git log -1 --format=%B 2>/dev/null)"
37
- if [ -z "$COMMIT_SHA" ] || [ -z "$COMMIT_MSG" ]; then
38
- exit 0
39
- fi
40
-
41
- TASK_IDS="$(printf '%s' "$COMMIT_MSG" | grep -oE '\[M[0-9]+-D[0-9]+-T[0-9]+\]' | sed 's/[][]//g' | awk '!seen[$0]++')"
42
- if [ -z "$TASK_IDS" ]; then
43
- exit 0
44
- fi
45
-
46
- printf '%s\n' "$TASK_IDS" | node -e '
47
- "use strict";
48
- try {
49
- const path = require("path");
50
- const fs = require("fs");
51
- const projectDir = process.argv[1];
52
- const commit = process.argv[2];
53
- let raw = "";
54
- process.stdin.setEncoding("utf8");
55
- process.stdin.on("data", (c) => { raw += c; });
56
- process.stdin.on("end", () => {
57
- try {
58
- const taskIds = raw.split("\n").map((s) => s.trim()).filter(Boolean);
59
- if (!taskIds.length) process.exit(0);
60
- const updater = require(path.join(projectDir, "bin", "spawn-plan-status-updater.cjs"));
61
- const activePaths = updater.listActivePlans(projectDir);
62
- if (!activePaths.length) process.exit(0);
63
- for (const fp of activePaths) {
64
- let plan;
65
- try { plan = JSON.parse(fs.readFileSync(fp, "utf8")); } catch (_) { continue; }
66
- const spawnId = plan && plan.spawnId;
67
- const spawnStartedAt = plan && plan.startedAt;
68
- if (!spawnId) continue;
69
- const planTaskIds = new Set((plan.tasks || []).map((t) => t && t.id).filter(Boolean));
70
- for (const taskId of taskIds) {
71
- if (!planTaskIds.has(taskId)) continue;
72
- const tokens = updater.sumTokensForTask({ projectDir, taskId, spawnStartedAt });
73
- updater.markTaskDone({ spawnId, taskId, commit, tokens, projectDir });
74
- }
75
- }
76
- } catch (err) {
77
- try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
78
- }
79
- });
80
- } catch (err) {
81
- try { process.stderr.write("[spawn-plan-hook] " + String(err && err.message || err) + "\n"); } catch (_) { /* silent */ }
82
- }
83
- ' "$PROJECT_DIR" "$COMMIT_SHA" 2>/dev/null
84
-
85
- exit 0