@tekyzinc/gsd-t 3.13.16 → 3.16.11

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 (54) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -0
  3. package/bin/gsd-t-benchmark-orchestrator.js +437 -0
  4. package/bin/gsd-t-capture-lint.cjs +276 -0
  5. package/bin/gsd-t-completion-check.cjs +106 -0
  6. package/bin/gsd-t-orchestrator-config.cjs +64 -0
  7. package/bin/gsd-t-orchestrator-queue.cjs +180 -0
  8. package/bin/gsd-t-orchestrator-recover.cjs +231 -0
  9. package/bin/gsd-t-orchestrator-worker.cjs +219 -0
  10. package/bin/gsd-t-orchestrator.js +534 -0
  11. package/bin/gsd-t-stream-feed-client.cjs +151 -0
  12. package/bin/gsd-t-task-brief-compactor.cjs +89 -0
  13. package/bin/gsd-t-task-brief-template.cjs +96 -0
  14. package/bin/gsd-t-task-brief.js +249 -0
  15. package/bin/gsd-t-token-backfill.cjs +366 -0
  16. package/bin/gsd-t-token-capture.cjs +306 -0
  17. package/bin/gsd-t-token-dashboard.cjs +318 -0
  18. package/bin/gsd-t-token-regenerate-log.cjs +129 -0
  19. package/bin/gsd-t-transcript-tee.cjs +246 -0
  20. package/bin/gsd-t-unattended-heartbeat.cjs +188 -0
  21. package/bin/gsd-t-unattended-platform.cjs +191 -27
  22. package/bin/gsd-t-unattended-safety.cjs +8 -1
  23. package/bin/gsd-t-unattended.cjs +192 -31
  24. package/bin/gsd-t.js +329 -2
  25. package/bin/supervisor-pid-fingerprint.cjs +126 -0
  26. package/commands/gsd-t-debug.md +63 -51
  27. package/commands/gsd-t-design-decompose.md +2 -7
  28. package/commands/gsd-t-doc-ripple.md +20 -11
  29. package/commands/gsd-t-execute.md +82 -50
  30. package/commands/gsd-t-integrate.md +43 -16
  31. package/commands/gsd-t-plan.md +20 -7
  32. package/commands/gsd-t-prd.md +19 -12
  33. package/commands/gsd-t-quick.md +64 -29
  34. package/commands/gsd-t-resume.md +51 -4
  35. package/commands/gsd-t-unattended.md +19 -20
  36. package/commands/gsd-t-verify.md +48 -32
  37. package/commands/gsd-t-visualize.md +19 -17
  38. package/commands/gsd-t-wave.md +29 -27
  39. package/docs/architecture.md +16 -0
  40. package/docs/m40-benchmark-report.md +35 -0
  41. package/docs/requirements.md +20 -0
  42. package/package.json +1 -1
  43. package/scripts/gsd-t-dashboard-server.js +291 -4
  44. package/scripts/gsd-t-dashboard.html +31 -1
  45. package/scripts/gsd-t-design-review-server.js +3 -1
  46. package/scripts/gsd-t-stream-feed-server.js +428 -0
  47. package/scripts/gsd-t-stream-feed.html +1168 -0
  48. package/scripts/gsd-t-token-aggregator.js +373 -0
  49. package/scripts/gsd-t-transcript.html +422 -0
  50. package/scripts/hooks/gsd-t-in-session-probe.js +62 -0
  51. package/scripts/hooks/pre-commit-capture-lint +26 -0
  52. package/templates/CLAUDE-global.md +69 -0
  53. package/scripts/gsd-t-agent-dashboard-server.js +0 -424
  54. package/scripts/gsd-t-agent-dashboard.html +0 -1043
@@ -0,0 +1,422 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GSD-T Transcript</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117;
10
+ --bg-raised: #161b22;
11
+ --bg-soft: #1f2937;
12
+ --fg: #c9d1d9;
13
+ --fg-dim: #8b949e;
14
+ --fg-xdim: #6e7681;
15
+ --accent: #58a6ff;
16
+ --accent-warm: #f0883e;
17
+ --border: #30363d;
18
+ --green: #3fb950;
19
+ --yellow: #d29922;
20
+ --red: #f85149;
21
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
22
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
23
+ }
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); font-size: 14px; line-height: 1.55; display: grid; grid-template-columns: 280px 1fr; grid-template-rows: auto 1fr; min-height: 100vh; }
26
+ body > header { grid-column: 1 / -1; }
27
+ body > aside { grid-column: 1; grid-row: 2; background: var(--bg-raised); border-right: 1px solid var(--border); padding: 12px 0; overflow-y: auto; max-height: calc(100vh - 50px); position: sticky; top: 49px; align-self: start; }
28
+ body > main { grid-column: 2; grid-row: 2; }
29
+ aside h3 { margin: 0 12px 8px 12px; font-size: 11px; text-transform: uppercase; color: var(--fg-xdim); letter-spacing: 0.08em; }
30
+ aside .tree { font-size: 13px; }
31
+ aside .node { display: flex; align-items: center; padding: 5px 12px; cursor: pointer; gap: 6px; }
32
+ aside .node:hover { background: var(--bg-soft); }
33
+ aside .node.active { background: var(--bg-soft); border-left: 2px solid var(--accent); }
34
+ aside .node .dot { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--fg-xdim); }
35
+ aside .node.running .dot { background: var(--green); animation: pulse 2s ease-in-out infinite; }
36
+ aside .node.stopped .dot { background: var(--yellow); }
37
+ aside .node.failed .dot { background: var(--red); }
38
+ aside .node .name { flex: 1; font-family: var(--mono); font-size: 11px; color: var(--fg-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
39
+ aside .node.active .name { color: var(--fg); }
40
+ aside .node .kill { flex: 0 0 auto; background: transparent; border: 1px solid var(--border); color: var(--fg-xdim); border-radius: 3px; padding: 1px 6px; font-size: 10px; cursor: pointer; font-family: var(--mono); }
41
+ aside .node.running .kill { color: var(--red); border-color: var(--red); }
42
+ aside .node .kill:hover:not([disabled]) { background: var(--red); color: #fff; }
43
+ aside .node .kill[disabled] { opacity: 0.3; cursor: not-allowed; }
44
+ aside .empty { padding: 12px; color: var(--fg-xdim); font-size: 12px; font-style: italic; }
45
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
46
+ header { position: sticky; top: 0; z-index: 10; background: var(--bg-raised); border-bottom: 1px solid var(--border); padding: 10px 16px; display: flex; align-items: center; gap: 12px; }
47
+ header .title { font-weight: 600; }
48
+ header .spawn-id { font-family: var(--mono); color: var(--fg-dim); font-size: 12px; }
49
+ header .status { margin-left: auto; font-size: 12px; color: var(--fg-dim); }
50
+ header .status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--yellow); margin-right: 6px; vertical-align: middle; }
51
+ header .status.connected .dot { background: var(--green); }
52
+ header .status.ended .dot { background: var(--fg-xdim); }
53
+ header .status.error .dot { background: var(--red); }
54
+
55
+ main { padding: 16px; padding-bottom: 120px; max-width: 960px; margin: 0 auto; width: 100%; }
56
+ .frame { margin: 8px 0; }
57
+ .frame.system { color: var(--fg-xdim); font-style: italic; font-size: 12px; padding: 2px 0; }
58
+ .frame.user { border-left: 3px solid var(--accent); padding: 6px 12px; background: var(--bg-raised); border-radius: 0 4px 4px 0; }
59
+ .frame.user .prefix { color: var(--accent); font-weight: 600; margin-right: 6px; }
60
+ .frame.assistant-text { font-family: var(--mono); white-space: pre-wrap; word-break: break-word; }
61
+ .frame.thinking { color: var(--fg-xdim); font-style: italic; font-size: 13px; padding: 4px 8px; border-left: 2px dashed var(--border); margin: 6px 0; }
62
+ .frame.thinking summary { cursor: pointer; color: var(--fg-dim); }
63
+ .frame.tool-use { margin: 8px 0; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-raised); }
64
+ .frame.tool-use summary { padding: 8px 12px; cursor: pointer; color: var(--accent-warm); font-family: var(--mono); font-size: 13px; list-style: none; }
65
+ .frame.tool-use summary::before { content: "⎿ "; color: var(--fg-dim); }
66
+ .frame.tool-use[open] summary { border-bottom: 1px solid var(--border); }
67
+ .frame.tool-use pre { margin: 0; padding: 10px 12px; font-family: var(--mono); font-size: 12px; color: var(--fg); white-space: pre-wrap; word-break: break-word; overflow-x: auto; }
68
+ .frame.tool-result { margin: 4px 0 8px 24px; padding: 8px 12px; border-left: 2px solid var(--border); background: var(--bg-soft); border-radius: 0 4px 4px 0; }
69
+ .frame.tool-result pre { margin: 0; font-family: var(--mono); font-size: 12px; color: var(--fg-dim); white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: hidden; }
70
+ .frame.tool-result.expanded pre { max-height: none; }
71
+ .frame.tool-result .show-more { display: block; margin-top: 6px; color: var(--accent); cursor: pointer; font-size: 11px; font-family: var(--mono); }
72
+ .frame.raw { color: var(--fg-xdim); font-family: var(--mono); font-size: 11px; white-space: pre-wrap; padding: 2px 0; }
73
+ .frame.boundary { margin: 12px 0; padding: 8px 12px; background: var(--bg-soft); border-left: 3px solid var(--accent-warm); border-radius: 0 4px 4px 0; font-size: 13px; }
74
+ .frame.boundary.done { border-left-color: var(--green); }
75
+ .frame.boundary.failed { border-left-color: var(--red); }
76
+ .frame.boundary .label { color: var(--accent-warm); font-weight: 600; font-family: var(--mono); font-size: 12px; margin-right: 8px; }
77
+ .frame.boundary.done .label { color: var(--green); }
78
+ .frame.boundary.failed .label { color: var(--red); }
79
+ .frame.boundary .meta { color: var(--fg-dim); font-family: var(--mono); font-size: 12px; }
80
+
81
+ .jump-to-live { position: fixed; bottom: 24px; right: 24px; background: var(--accent); color: #fff; border: none; padding: 10px 16px; border-radius: 20px; cursor: pointer; font-size: 13px; font-family: var(--sans); font-weight: 600; box-shadow: 0 4px 12px rgba(0,0,0,0.4); display: none; z-index: 20; }
82
+ .jump-to-live.visible { display: block; }
83
+ .jump-to-live:hover { background: #79c0ff; }
84
+ </style>
85
+ </head>
86
+ <body data-spawn-id="__SPAWN_ID__">
87
+ <header>
88
+ <div class="title">GSD-T Transcript</div>
89
+ <div class="spawn-id" id="hdr-spawn-id"></div>
90
+ <div class="status" id="hdr-status"><span class="dot"></span><span class="label">connecting…</span></div>
91
+ </header>
92
+ <aside>
93
+ <h3>Spawns</h3>
94
+ <div class="tree" id="tree"></div>
95
+ </aside>
96
+ <main id="stream"></main>
97
+ <button class="jump-to-live" id="jump-btn">↓ Jump to live</button>
98
+
99
+ <script>
100
+ (function () {
101
+ 'use strict';
102
+
103
+ const spawnId = document.body.getAttribute('data-spawn-id');
104
+ document.getElementById('hdr-spawn-id').textContent = spawnId;
105
+
106
+ const stream = document.getElementById('stream');
107
+ const statusEl = document.getElementById('hdr-status');
108
+ const statusLabel = statusEl.querySelector('.label');
109
+ const jumpBtn = document.getElementById('jump-btn');
110
+
111
+ // Pair tool_result → tool_use by tool_use_id so the renderer can place
112
+ // the result next to the call even when they come as separate frames.
113
+ const toolUseById = new Map();
114
+
115
+ let autoScroll = true;
116
+
117
+ function setStatus(cls, text) {
118
+ statusEl.className = 'status ' + cls;
119
+ statusLabel.textContent = text;
120
+ }
121
+
122
+ function atBottom() {
123
+ return window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
124
+ }
125
+
126
+ window.addEventListener('scroll', () => {
127
+ if (atBottom()) { autoScroll = true; jumpBtn.classList.remove('visible'); }
128
+ else { autoScroll = false; jumpBtn.classList.add('visible'); }
129
+ });
130
+
131
+ jumpBtn.addEventListener('click', () => {
132
+ autoScroll = true;
133
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
134
+ jumpBtn.classList.remove('visible');
135
+ });
136
+
137
+ function appendFrame(el) {
138
+ stream.appendChild(el);
139
+ if (autoScroll) {
140
+ requestAnimationFrame(() => window.scrollTo(0, document.body.scrollHeight));
141
+ }
142
+ }
143
+
144
+ function renderSystem(frame) {
145
+ const div = document.createElement('div');
146
+ div.className = 'frame system';
147
+ const sid = frame.session_id ? ' session=' + frame.session_id.slice(0, 8) : '';
148
+ const model = frame.model ? ' model=' + frame.model : '';
149
+ const subtype = frame.subtype ? ' ' + frame.subtype : '';
150
+ div.textContent = 'system' + subtype + sid + model;
151
+ appendFrame(div);
152
+ }
153
+
154
+ function renderUserMessage(content) {
155
+ const div = document.createElement('div');
156
+ div.className = 'frame user';
157
+ const p = document.createElement('span');
158
+ p.className = 'prefix';
159
+ p.textContent = '>';
160
+ div.appendChild(p);
161
+ const span = document.createElement('span');
162
+ span.textContent = typeof content === 'string' ? content : JSON.stringify(content);
163
+ div.appendChild(span);
164
+ appendFrame(div);
165
+ }
166
+
167
+ function renderAssistantText(text) {
168
+ const div = document.createElement('div');
169
+ div.className = 'frame assistant-text';
170
+ div.textContent = text;
171
+ appendFrame(div);
172
+ }
173
+
174
+ function renderThinking(text) {
175
+ const d = document.createElement('details');
176
+ d.className = 'frame thinking';
177
+ const s = document.createElement('summary');
178
+ s.textContent = '✻ Thinking (' + text.length + ' chars)';
179
+ d.appendChild(s);
180
+ const pre = document.createElement('div');
181
+ pre.textContent = text;
182
+ d.appendChild(pre);
183
+ appendFrame(d);
184
+ }
185
+
186
+ function toolUsePreview(input) {
187
+ if (!input || typeof input !== 'object') return '';
188
+ // Surface the most useful single arg: file_path, command, pattern, path, description
189
+ const keys = ['file_path', 'command', 'pattern', 'path', 'description', 'prompt'];
190
+ for (const k of keys) {
191
+ if (typeof input[k] === 'string') {
192
+ const v = input[k];
193
+ return k + '="' + (v.length > 80 ? v.slice(0, 77) + '…' : v) + '"';
194
+ }
195
+ }
196
+ const first = Object.keys(input)[0];
197
+ if (first) return first + '=…';
198
+ return '';
199
+ }
200
+
201
+ function renderToolUse(block) {
202
+ const d = document.createElement('details');
203
+ d.className = 'frame tool-use';
204
+ const s = document.createElement('summary');
205
+ s.textContent = block.name + '(' + toolUsePreview(block.input) + ')';
206
+ d.appendChild(s);
207
+ const pre = document.createElement('pre');
208
+ try { pre.textContent = JSON.stringify(block.input, null, 2); }
209
+ catch { pre.textContent = String(block.input); }
210
+ d.appendChild(pre);
211
+ toolUseById.set(block.id, { name: block.name, el: d });
212
+ appendFrame(d);
213
+ }
214
+
215
+ function renderToolResult(block) {
216
+ const div = document.createElement('div');
217
+ div.className = 'frame tool-result';
218
+ const pre = document.createElement('pre');
219
+ let text;
220
+ if (typeof block.content === 'string') text = block.content;
221
+ else if (Array.isArray(block.content)) {
222
+ text = block.content.map(c => typeof c === 'string' ? c : (c.text || JSON.stringify(c))).join('\n');
223
+ } else text = JSON.stringify(block.content || '');
224
+ pre.textContent = text;
225
+ div.appendChild(pre);
226
+ if (text.length > 800) {
227
+ const more = document.createElement('span');
228
+ more.className = 'show-more';
229
+ more.textContent = 'show more';
230
+ more.addEventListener('click', () => {
231
+ div.classList.toggle('expanded');
232
+ more.textContent = div.classList.contains('expanded') ? 'show less' : 'show more';
233
+ });
234
+ div.appendChild(more);
235
+ }
236
+ appendFrame(div);
237
+ }
238
+
239
+ function renderBoundary(frame) {
240
+ const div = document.createElement('div');
241
+ div.className = 'frame boundary ' + (frame.state || '');
242
+ const label = document.createElement('span');
243
+ label.className = 'label';
244
+ label.textContent = '▸ task ' + (frame.state || '?');
245
+ div.appendChild(label);
246
+ const meta = document.createElement('span');
247
+ meta.className = 'meta';
248
+ const bits = [];
249
+ if (frame.taskId) bits.push('id=' + frame.taskId);
250
+ if (frame.domain) bits.push('domain=' + frame.domain);
251
+ if (frame.wave != null) bits.push('wave=' + frame.wave);
252
+ if (frame.exitCode != null) bits.push('exit=' + frame.exitCode);
253
+ if (frame.durationMs != null) bits.push('duration=' + Math.round(frame.durationMs/1000) + 's');
254
+ meta.textContent = bits.join(' ');
255
+ div.appendChild(meta);
256
+ appendFrame(div);
257
+ }
258
+
259
+ function renderRaw(line) {
260
+ const div = document.createElement('div');
261
+ div.className = 'frame raw';
262
+ div.textContent = line;
263
+ appendFrame(div);
264
+ }
265
+
266
+ function renderFrame(frame) {
267
+ if (!frame || typeof frame !== 'object') return;
268
+ const type = frame.type;
269
+ if (type === 'system') { renderSystem(frame); return; }
270
+ if (type === 'task-boundary') { renderBoundary(frame); return; }
271
+ if (type === 'raw') { renderRaw(frame.line || ''); return; }
272
+ if (type === 'assistant' && frame.message && Array.isArray(frame.message.content)) {
273
+ for (const b of frame.message.content) {
274
+ if (b.type === 'text') renderAssistantText(b.text || '');
275
+ else if (b.type === 'thinking') renderThinking(b.thinking || b.text || '');
276
+ else if (b.type === 'tool_use') renderToolUse(b);
277
+ else renderRaw(JSON.stringify(b));
278
+ }
279
+ return;
280
+ }
281
+ if (type === 'user' && frame.message && Array.isArray(frame.message.content)) {
282
+ for (const b of frame.message.content) {
283
+ if (b.type === 'tool_result') renderToolResult(b);
284
+ else if (b.type === 'text') renderUserMessage(b.text || '');
285
+ else renderRaw(JSON.stringify(b));
286
+ }
287
+ return;
288
+ }
289
+ if (type === 'user' && typeof frame.message === 'string') {
290
+ renderUserMessage(frame.message);
291
+ return;
292
+ }
293
+ if (type === 'result') {
294
+ const d = document.createElement('div');
295
+ d.className = 'frame system';
296
+ d.textContent = 'result subtype=' + (frame.subtype || '?') + ' duration=' + (frame.duration_ms || 0) + 'ms';
297
+ appendFrame(d);
298
+ return;
299
+ }
300
+ renderRaw(JSON.stringify(frame));
301
+ }
302
+
303
+ // ── Sidebar ─────────────────────────────────────────────────────────
304
+ //
305
+ // Poll /transcripts every 3s, build a parent-indented tree, render.
306
+ // Exposed as window.__gsdtBuildTree for unit-testable tree-build logic.
307
+
308
+ function buildTree(spawns) {
309
+ // Map spawnId → node, then link children to parents. Orphans float
310
+ // to the root level.
311
+ const byId = new Map();
312
+ const roots = [];
313
+ for (const s of spawns) byId.set(s.spawnId, { ...s, children: [] });
314
+ for (const node of byId.values()) {
315
+ if (node.parentId && byId.has(node.parentId)) {
316
+ byId.get(node.parentId).children.push(node);
317
+ } else {
318
+ roots.push(node);
319
+ }
320
+ }
321
+ // Sort each level newest-first
322
+ const ts = (a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0);
323
+ roots.sort(ts);
324
+ for (const n of byId.values()) n.children.sort(ts);
325
+ return roots;
326
+ }
327
+ window.__gsdtBuildTree = buildTree;
328
+
329
+ function statusClass(s) {
330
+ if (s.status === 'running') return 'running';
331
+ if (s.status === 'failed') return 'failed';
332
+ if (s.status === 'stopped') return 'stopped';
333
+ return '';
334
+ }
335
+
336
+ function renderTree(roots) {
337
+ const tree = document.getElementById('tree');
338
+ tree.innerHTML = '';
339
+ if (!roots.length) {
340
+ const e = document.createElement('div');
341
+ e.className = 'empty';
342
+ e.textContent = 'No spawns yet.';
343
+ tree.appendChild(e);
344
+ return;
345
+ }
346
+ const currentId = (location.hash || '#').slice(1) || spawnId;
347
+ function render(node, depth) {
348
+ const el = document.createElement('div');
349
+ el.className = 'node ' + statusClass(node);
350
+ if (node.spawnId === currentId) el.classList.add('active');
351
+ el.style.paddingLeft = (12 + depth * 14) + 'px';
352
+ const dot = document.createElement('span'); dot.className = 'dot';
353
+ const name = document.createElement('span'); name.className = 'name';
354
+ name.textContent = (node.command || 'spawn') + ' · ' + node.spawnId.slice(-8);
355
+ name.title = (node.description || node.spawnId) + '\n' + (node.startedAt || '');
356
+ const kill = document.createElement('button');
357
+ kill.className = 'kill';
358
+ kill.textContent = 'kill';
359
+ kill.disabled = node.status !== 'running' || !node.workerPid;
360
+ kill.addEventListener('click', (ev) => {
361
+ ev.stopPropagation();
362
+ if (!confirm('SIGTERM spawn ' + node.spawnId + ' (pid ' + node.workerPid + ')?')) return;
363
+ fetch('/transcript/' + encodeURIComponent(node.spawnId) + '/kill', { method: 'POST' })
364
+ .then((r) => r.json())
365
+ .then((j) => { kill.textContent = j.status || 'killed'; })
366
+ .catch(() => { kill.textContent = 'err'; });
367
+ });
368
+ el.appendChild(dot); el.appendChild(name); el.appendChild(kill);
369
+ el.addEventListener('click', () => {
370
+ if (node.spawnId === currentId) return;
371
+ location.hash = node.spawnId;
372
+ });
373
+ document.getElementById('tree').appendChild(el);
374
+ for (const c of node.children) render(c, depth + 1);
375
+ }
376
+ for (const r of roots) render(r, 0);
377
+ }
378
+
379
+ let pollTimer = null;
380
+ function pollSpawns() {
381
+ fetch('/transcripts')
382
+ .then((r) => r.json())
383
+ .then((j) => renderTree(buildTree(j.spawns || [])))
384
+ .catch(() => { /* keep last render */ });
385
+ }
386
+
387
+ // ── SSE connection (reconnectable on hash change) ───────────────────
388
+
389
+ let src = null;
390
+ function connect(id) {
391
+ if (src) { try { src.close(); } catch { /* gone */ } src = null; }
392
+ stream.innerHTML = '';
393
+ toolUseById.clear();
394
+ autoScroll = true;
395
+ jumpBtn.classList.remove('visible');
396
+ document.getElementById('hdr-spawn-id').textContent = id;
397
+ setStatus('', 'connecting…');
398
+ src = new EventSource('/transcript/' + encodeURIComponent(id) + '/stream');
399
+ src.onopen = () => setStatus('connected', 'live');
400
+ src.onerror = () => setStatus('error', 'disconnected');
401
+ src.onmessage = (ev) => {
402
+ if (!ev.data) return;
403
+ try { renderFrame(JSON.parse(ev.data)); }
404
+ catch { renderRaw(ev.data); }
405
+ };
406
+ }
407
+
408
+ window.addEventListener('hashchange', () => {
409
+ const id = (location.hash || '').slice(1);
410
+ if (id) { connect(id); pollSpawns(); }
411
+ });
412
+
413
+ const initialId = (location.hash || '').slice(1) || spawnId;
414
+ if (!location.hash && spawnId) location.hash = spawnId;
415
+ connect(initialId);
416
+
417
+ pollSpawns();
418
+ pollTimer = setInterval(pollSpawns, 3000);
419
+ })();
420
+ </script>
421
+ </body>
422
+ </html>
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * M43 D1-T1 Probe — In-Session Usage Capture Branch Selection
4
+ *
5
+ * Captures the RAW Claude Code hook payload for Stop / SessionEnd / PostToolUse
6
+ * into .gsd-t/.hook-probe/{event}-{ts}.json so D1-T1 can decide:
7
+ * Branch A (hook-based) — if payload carries a `usage` object.
8
+ * Branch B (transcript tee) — otherwise.
9
+ *
10
+ * Behavior:
11
+ * - Zero-dep. Silent failure on any error (never interferes with Claude Code).
12
+ * - Writes at most 10 files per event type (rotating) to avoid growth.
13
+ * - Only active when .gsd-t/.hook-probe/ exists in the cwd; creating/deleting
14
+ * that directory is the on/off switch.
15
+ */
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const MAX_STDIN = 1024 * 1024;
20
+ const MAX_PER_EVENT = 10;
21
+
22
+ let input = "";
23
+ let aborted = false;
24
+ process.stdin.setEncoding("utf8");
25
+ process.stdin.on("data", (d) => {
26
+ input += d;
27
+ if (input.length > MAX_STDIN) { aborted = true; process.stdin.destroy(); }
28
+ });
29
+ process.stdin.on("end", () => {
30
+ if (aborted) return;
31
+ try {
32
+ const hook = JSON.parse(input);
33
+ const cwd = hook.cwd || process.cwd();
34
+ if (!path.isAbsolute(cwd)) return;
35
+ const probeDir = path.join(cwd, ".gsd-t", ".hook-probe");
36
+ if (!fs.existsSync(probeDir)) return; // disabled unless dir exists
37
+ const event = hook.hook_event_name || "unknown";
38
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
39
+ const sid = (hook.session_id || "nosid").slice(0, 12);
40
+ const file = path.join(probeDir, `${event}-${ts}-${sid}.json`);
41
+ const resolved = path.resolve(file);
42
+ if (!resolved.startsWith(path.resolve(probeDir) + path.sep)) return;
43
+ fs.writeFileSync(file, JSON.stringify(hook, null, 2) + "\n");
44
+ rotate(probeDir, event);
45
+ } catch {
46
+ // silent
47
+ }
48
+ });
49
+
50
+ function rotate(dir, event) {
51
+ try {
52
+ const files = fs.readdirSync(dir)
53
+ .filter((f) => f.startsWith(event + "-") && f.endsWith(".json"))
54
+ .map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
55
+ .sort((a, b) => b.t - a.t);
56
+ for (const { f } of files.slice(MAX_PER_EVENT)) {
57
+ try { fs.unlinkSync(path.join(dir, f)); } catch { /* noop */ }
58
+ }
59
+ } catch {
60
+ // silent
61
+ }
62
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # GSD-T capture lint (M41 D5)
3
+ # Rejects commits that introduce bare Task(…) / claude -p / spawn('claude', …)
4
+ # calls bypassing bin/gsd-t-token-capture.cjs.
5
+ #
6
+ # Install (opt-in): gsd-t init --install-hooks
7
+ # Remove: rm .git/hooks/pre-commit (or delete this block if merged
8
+ # into an existing hook)
9
+
10
+ set -e
11
+
12
+ if command -v gsd-t >/dev/null 2>&1; then
13
+ GSD_T_BIN=gsd-t
14
+ elif [ -x "./bin/gsd-t.js" ]; then
15
+ GSD_T_BIN="node ./bin/gsd-t.js"
16
+ else
17
+ echo "[capture-lint] gsd-t CLI not found — skipping lint" >&2
18
+ exit 0
19
+ fi
20
+
21
+ if ! $GSD_T_BIN capture-lint --staged; then
22
+ echo ""
23
+ echo "Token capture lint failed — see paths above."
24
+ echo "Wrap spawns with captureSpawn({…, spawnFn}) or add GSD-T-CAPTURE-LINT: skip" >&2
25
+ exit 1
26
+ fi
@@ -253,6 +253,75 @@ This gives the user real-time visibility into which model is handling each opera
253
253
 
254
254
  **Context Meter (M34/M38, v3.12.10+)** — The real context-window measurement feeding the headless-default spawn decision. A PostToolUse hook (`scripts/gsd-t-context-meter.js`) runs after every tool call, uses local token estimation to write the current input-token count into `.gsd-t/.context-meter-state.json`. `getSessionStatus()` reads that state file (fresh window = 5 minutes) with a historical heuristic fallback when the file is missing or stale. Command files consume the signal via a small bash shim (`CTX_PCT=$(node -e "…tb.getSessionStatus('.').pct")`). **Single-band model** (context-meter-contract v1.3.0): there's one threshold (default 85%) and one action — hand off to a detached headless spawn. No three-band routing, no silent downgrades, no MANDATORY STOP prose. The meter exists to inform spawn-time routing, not to pause work in-flight.
255
255
 
256
+ ## Observability Logging (MANDATORY)
257
+
258
+ Every command that spawns a Task subagent, invokes `claude -p`, or calls `spawn('claude', ...)` MUST route the spawn through `bin/gsd-t-token-capture.cjs` so the real token-usage envelope is parsed and recorded. This is the M41 canonical pattern — the pre-M41 bash block that wrote `| N/A |` is retired.
259
+
260
+ ### Pattern A — wrap a spawn callable with `captureSpawn`
261
+
262
+ Preferred for new spawn sites. The wrapper owns the before/after timing, model banner, envelope parse, row write, and JSONL record.
263
+
264
+ ```
265
+ node -e "
266
+ const { captureSpawn } = require('./bin/gsd-t-token-capture.cjs');
267
+ (async () => {
268
+ await captureSpawn({
269
+ command: 'gsd-t-execute',
270
+ step: 'Step 4',
271
+ model: 'sonnet',
272
+ description: 'domain: auth-service',
273
+ projectDir: '.',
274
+ domain: 'auth-service',
275
+ task: 'T-3',
276
+ spawnFn: async () => { /* actual Task(...) or spawn('claude', ...) call */ },
277
+ });
278
+ })();
279
+ "
280
+ ```
281
+
282
+ ### Pattern B — record after the result envelope is already in hand
283
+
284
+ For command files where the Task subagent already ran and the caller has the result object. Identical row format, no timing wrap.
285
+
286
+ ```
287
+ node -e "
288
+ const { recordSpawnRow } = require('./bin/gsd-t-token-capture.cjs');
289
+ recordSpawnRow({
290
+ projectDir: '.',
291
+ command: 'gsd-t-verify',
292
+ step: 'Step 4',
293
+ model: 'haiku',
294
+ startedAt: '2026-04-21 10:00',
295
+ endedAt: '2026-04-21 10:02',
296
+ usage: result.usage, // may be undefined — wrapper handles with '—'
297
+ domain: '-', task: '-',
298
+ ctxPct: 42,
299
+ notes: 'test audit + contract review',
300
+ });
301
+ "
302
+ ```
303
+
304
+ ### Canonical `.gsd-t/token-log.md` header
305
+
306
+ ```
307
+ | Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Tokens | Notes | Domain | Task | Ctx% |
308
+ ```
309
+
310
+ The wrapper detects old headers (no `Tokens` column) and upgrades in place, preserving existing rows. The **Tokens** cell renders as `in=N out=N cr=N cc=N $X.XX` when usage is present, or `—` when absent. Never `0`. Never `N/A`. A zero is a measurement; a dash is an acknowledged gap.
311
+
312
+ For QA/validation subagents, append findings to `.gsd-t/qa-issues.md`:
313
+ ```
314
+ | Date | Command | Step | Model | Duration(s) | Severity | Finding |
315
+ ```
316
+
317
+ ## Token Capture Rule (MANDATORY)
318
+
319
+ Every `Task(...)` subagent spawn, every `claude -p` child process, and every `spawn('claude', ...)` call MUST flow through `bin/gsd-t-token-capture.cjs`. Either wrap with `captureSpawn({..., spawnFn})` or record explicitly with `recordSpawnRow({...})` after the call returns.
320
+
321
+ No command file ships a bare `Task(...)` or `claude -p` line outside of a wrapper call. `gsd-t capture-lint` (D5) enforces this mechanically; violations fail the opt-in pre-commit hook.
322
+
323
+ Rationale: the pre-M41 convention silently wrote `N/A` tokens because no caller parsed the `usage` envelope. The wrapper is the single place that parses it. Bypassing the wrapper re-introduces blind spots.
324
+
256
325
  ## Headless-by-Default Spawn (M38, v3.12.10+)
257
326
 
258
327
  Long-running work (execute, wave, integrate, debug repair loops) spawns detached by default. Interactive session shows a banner, event-stream path, then exits — no mid-session `/compact` wall. `--watch` keeps a ScheduleWakeup-driven status block in the caller; events stream JSONL to `.gsd-t/events/YYYY-MM-DD.jsonl`. Router mode (`/gsd`) answers exploratory requests inline without a command spawn — see `commands/gsd.md` Step 2.5.