@tekyzinc/gsd-t 3.18.13 → 3.19.0
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.
- package/CHANGELOG.md +114 -0
- package/bin/gsd-t-parallel-probe.cjs +132 -0
- package/bin/gsd-t-parallel.cjs +422 -9
- package/bin/gsd-t-task-graph.cjs +80 -19
- package/bin/gsd-t-unattended.cjs +634 -229
- package/bin/gsd-t-worker-dispatch.cjs +211 -0
- package/bin/headless-auto-spawn.cjs +44 -1
- package/bin/headless-exit-codes.cjs +36 -18
- package/bin/m44-proof-measure.cjs +285 -0
- package/bin/m46-iter-proof.cjs +149 -0
- package/bin/m46-worker-proof.cjs +201 -0
- package/bin/parallelism-report.cjs +535 -0
- package/bin/spawn-plan-writer.cjs +1 -1
- package/commands/gsd-t-debug.md +10 -14
- package/commands/gsd-t-execute.md +10 -16
- package/commands/gsd-t-help.md +1 -0
- package/commands/gsd-t-integrate.md +8 -14
- package/commands/gsd-t-quick.md +10 -14
- package/commands/gsd-t-resume.md +32 -0
- package/commands/gsd-t-status.md +10 -0
- package/commands/gsd-t-unattended-watch.md +58 -1
- package/commands/gsd-t-visualize.md +15 -12
- package/commands/gsd-t-wave.md +2 -11
- package/docs/architecture.md +82 -0
- package/docs/requirements.md +20 -0
- package/package.json +1 -1
- package/scripts/gsd-t-compact-detector.js +51 -8
- package/scripts/gsd-t-dashboard-server.js +138 -85
- package/scripts/gsd-t-transcript.html +152 -1
- package/scripts/gsd-t-update-check.js +13 -4
- package/scripts/hooks/gsd-t-conversation-capture.js +258 -0
- package/templates/CLAUDE-global.md +54 -0
|
@@ -0,0 +1,258 @@
|
|
|
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 `payload.cwd`,
|
|
20
|
+
* then walks up from `process.cwd()` looking for `.gsd-t/progress.md`.
|
|
21
|
+
* Silent no-op if no project dir found.
|
|
22
|
+
*
|
|
23
|
+
* Contract: .gsd-t/contracts/conversation-capture-contract.md v1.0.0
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const crypto = require('crypto');
|
|
29
|
+
|
|
30
|
+
const DEFAULT_SCRIPT_GUARD_MS = 5000;
|
|
31
|
+
const CONTENT_CAP_BYTES = 16 * 1024; // 16 KB
|
|
32
|
+
const MAX_STDIN = 1024 * 1024; // 1 MiB defense-in-depth
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
|
|
35
|
+
function _readStdin() {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let buf = '';
|
|
38
|
+
let aborted = false;
|
|
39
|
+
process.stdin.setEncoding('utf8');
|
|
40
|
+
process.stdin.on('data', (chunk) => {
|
|
41
|
+
if (aborted) return;
|
|
42
|
+
buf += chunk;
|
|
43
|
+
if (buf.length > MAX_STDIN) {
|
|
44
|
+
aborted = true;
|
|
45
|
+
try { process.stdin.destroy(); } catch (_) { /* noop */ }
|
|
46
|
+
resolve('');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
process.stdin.on('end', () => { if (!aborted) resolve(buf); });
|
|
50
|
+
process.stdin.on('error', () => resolve(buf));
|
|
51
|
+
setTimeout(() => resolve(buf), DEFAULT_SCRIPT_GUARD_MS).unref();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _parsePayload(raw) {
|
|
56
|
+
try { return JSON.parse(raw || '{}'); } catch (_) { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _walkUpForProject(startDir) {
|
|
60
|
+
try {
|
|
61
|
+
let dir = path.resolve(startDir || '.');
|
|
62
|
+
for (let i = 0; i < 10; i++) {
|
|
63
|
+
if (fs.existsSync(path.join(dir, '.gsd-t', 'progress.md'))) return dir;
|
|
64
|
+
const parent = path.dirname(dir);
|
|
65
|
+
if (parent === dir) break;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
} catch (_) { /* swallow */ }
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function _resolveProjectDir(payload) {
|
|
73
|
+
const env = process.env.GSD_T_PROJECT_DIR;
|
|
74
|
+
if (env && fs.existsSync(path.join(env, '.gsd-t'))) return env;
|
|
75
|
+
if (payload && typeof payload.cwd === 'string' && path.isAbsolute(payload.cwd)
|
|
76
|
+
&& fs.existsSync(path.join(payload.cwd, '.gsd-t'))) {
|
|
77
|
+
return payload.cwd;
|
|
78
|
+
}
|
|
79
|
+
const walked = _walkUpForProject(process.cwd());
|
|
80
|
+
if (walked) return walked;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _resolveSessionId(payload) {
|
|
85
|
+
if (payload && typeof payload.session_id === 'string' && payload.session_id.length > 0
|
|
86
|
+
&& !/[\/\\\0]|\.\./.test(payload.session_id)) {
|
|
87
|
+
return payload.session_id;
|
|
88
|
+
}
|
|
89
|
+
// Fallback: deterministic-ish per-process hash. Stable within one process,
|
|
90
|
+
// different across processes. Keeps the filename non-empty when Claude Code
|
|
91
|
+
// omits session_id (shouldn't happen in practice but we must not explode).
|
|
92
|
+
// Also used as defense-in-depth for malformed session_ids containing path
|
|
93
|
+
// separators / `..` that would let path.join collapse the `in-session-`
|
|
94
|
+
// prefix (protects the filename-prefix discriminator contract with the
|
|
95
|
+
// viewer + compact-detector).
|
|
96
|
+
const seed = String(process.pid) + ':' + String(started);
|
|
97
|
+
return 'pid-' + crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function _capContent(raw) {
|
|
101
|
+
if (raw == null) return { content: null, truncated: false };
|
|
102
|
+
let str;
|
|
103
|
+
if (typeof raw === 'string') str = raw;
|
|
104
|
+
else {
|
|
105
|
+
try { str = JSON.stringify(raw); } catch (_) { str = String(raw); }
|
|
106
|
+
}
|
|
107
|
+
const byteLen = Buffer.byteLength(str, 'utf8');
|
|
108
|
+
if (byteLen <= CONTENT_CAP_BYTES) return { content: str, truncated: false };
|
|
109
|
+
// Truncate by bytes; slice then re-decode to avoid breaking a multi-byte char.
|
|
110
|
+
const buf = Buffer.from(str, 'utf8').subarray(0, CONTENT_CAP_BYTES);
|
|
111
|
+
return { content: buf.toString('utf8'), truncated: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _appendFrame(projectDir, sessionId, frame) {
|
|
115
|
+
const transcriptsDir = path.join(projectDir, '.gsd-t', 'transcripts');
|
|
116
|
+
try { fs.mkdirSync(transcriptsDir, { recursive: true }); } catch (_) { /* noop */ }
|
|
117
|
+
const outPath = path.join(transcriptsDir, 'in-session-' + sessionId + '.ndjson');
|
|
118
|
+
// Path-traversal guard: resolved path must stay under transcriptsDir.
|
|
119
|
+
const resolvedOut = path.resolve(outPath);
|
|
120
|
+
const resolvedDir = path.resolve(transcriptsDir) + path.sep;
|
|
121
|
+
if (!resolvedOut.startsWith(resolvedDir)) return;
|
|
122
|
+
fs.appendFileSync(outPath, JSON.stringify(frame) + '\n', 'utf8');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _extractUserContent(payload) {
|
|
126
|
+
// Claude Code UserPromptSubmit payload carries the prompt text.
|
|
127
|
+
if (payload && typeof payload.prompt === 'string') return payload.prompt;
|
|
128
|
+
if (payload && payload.message && typeof payload.message.content === 'string') {
|
|
129
|
+
return payload.message.content;
|
|
130
|
+
}
|
|
131
|
+
if (payload && payload.user_message && typeof payload.user_message === 'string') {
|
|
132
|
+
return payload.user_message;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _extractAssistantContent(payload) {
|
|
138
|
+
// Stop hook payloads vary. Try the common shapes; fall back to null so we
|
|
139
|
+
// still emit a stub frame (ts + session_id only).
|
|
140
|
+
if (payload && typeof payload.assistant_message === 'string') return payload.assistant_message;
|
|
141
|
+
if (payload && payload.message && typeof payload.message.content === 'string') {
|
|
142
|
+
return payload.message.content;
|
|
143
|
+
}
|
|
144
|
+
if (payload && typeof payload.content === 'string') return payload.content;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _buildUserFrame(payload, sessionId, ts) {
|
|
149
|
+
const { content, truncated } = _capContent(_extractUserContent(payload));
|
|
150
|
+
const frame = {
|
|
151
|
+
type: 'user_turn',
|
|
152
|
+
ts,
|
|
153
|
+
session_id: sessionId,
|
|
154
|
+
};
|
|
155
|
+
if (content != null) frame.content = content;
|
|
156
|
+
if (truncated) frame.truncated = true;
|
|
157
|
+
if (payload && typeof payload.message_id === 'string') frame.message_id = payload.message_id;
|
|
158
|
+
return frame;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _buildAssistantFrame(payload, sessionId, ts) {
|
|
162
|
+
const { content, truncated } = _capContent(_extractAssistantContent(payload));
|
|
163
|
+
const frame = {
|
|
164
|
+
type: 'assistant_turn',
|
|
165
|
+
ts,
|
|
166
|
+
session_id: sessionId,
|
|
167
|
+
};
|
|
168
|
+
if (content != null) frame.content = content;
|
|
169
|
+
if (truncated) frame.truncated = true;
|
|
170
|
+
if (payload && typeof payload.message_id === 'string') frame.message_id = payload.message_id;
|
|
171
|
+
return frame;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _buildSessionStartFrame(sessionId, ts) {
|
|
175
|
+
return { type: 'session_start', ts, session_id: sessionId };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _buildToolUseFrame(payload, sessionId, ts) {
|
|
179
|
+
const frame = {
|
|
180
|
+
type: 'tool_use',
|
|
181
|
+
ts,
|
|
182
|
+
session_id: sessionId,
|
|
183
|
+
};
|
|
184
|
+
if (payload && typeof payload.tool_name === 'string') frame.name = payload.tool_name;
|
|
185
|
+
else if (payload && payload.tool && typeof payload.tool.name === 'string') frame.name = payload.tool.name;
|
|
186
|
+
if (payload && typeof payload.tool_use_id === 'string') frame.tool_use_id = payload.tool_use_id;
|
|
187
|
+
if (payload && typeof payload.duration_ms === 'number') frame.duration_ms = payload.duration_ms;
|
|
188
|
+
return frame;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _handle(payload) {
|
|
192
|
+
if (!payload || typeof payload !== 'object') return;
|
|
193
|
+
const event = payload.hook_event_name;
|
|
194
|
+
if (!event) return;
|
|
195
|
+
|
|
196
|
+
const projectDir = _resolveProjectDir(payload);
|
|
197
|
+
if (!projectDir) return; // not a GSD-T project — silent no-op
|
|
198
|
+
|
|
199
|
+
const sessionId = _resolveSessionId(payload);
|
|
200
|
+
const ts = new Date().toISOString();
|
|
201
|
+
|
|
202
|
+
switch (event) {
|
|
203
|
+
case 'SessionStart': {
|
|
204
|
+
_appendFrame(projectDir, sessionId, _buildSessionStartFrame(sessionId, ts));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
case 'UserPromptSubmit': {
|
|
208
|
+
_appendFrame(projectDir, sessionId, _buildUserFrame(payload, sessionId, ts));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
case 'Stop': {
|
|
212
|
+
_appendFrame(projectDir, sessionId, _buildAssistantFrame(payload, sessionId, ts));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
case 'PostToolUse': {
|
|
216
|
+
// Opt-in: guarded to keep default writes small.
|
|
217
|
+
if (process.env.GSD_T_CAPTURE_TOOL_USES !== '1') return;
|
|
218
|
+
_appendFrame(projectDir, sessionId, _buildToolUseFrame(payload, sessionId, ts));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
try {
|
|
228
|
+
const raw = await _readStdin();
|
|
229
|
+
const payload = _parsePayload(raw);
|
|
230
|
+
if (!payload) return;
|
|
231
|
+
_handle(payload);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
try { process.stderr.write('gsd-t-conversation-capture: ' + (err && err.message || err) + '\n'); } catch (_) { /* noop */ }
|
|
234
|
+
} finally {
|
|
235
|
+
const elapsed = Date.now() - started;
|
|
236
|
+
if (elapsed > DEFAULT_SCRIPT_GUARD_MS) process.exitCode = 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (require.main === module) {
|
|
241
|
+
main();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
_internal: {
|
|
246
|
+
_parsePayload,
|
|
247
|
+
_resolveProjectDir,
|
|
248
|
+
_resolveSessionId,
|
|
249
|
+
_capContent,
|
|
250
|
+
_buildUserFrame,
|
|
251
|
+
_buildAssistantFrame,
|
|
252
|
+
_buildSessionStartFrame,
|
|
253
|
+
_buildToolUseFrame,
|
|
254
|
+
_appendFrame,
|
|
255
|
+
_handle,
|
|
256
|
+
CONTENT_CAP_BYTES,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
@@ -253,6 +253,60 @@ 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
|
+
## In-Session Conversation Capture (M45 D2)
|
|
257
|
+
|
|
258
|
+
The orchestrator session's user↔assistant dialog is captured into
|
|
259
|
+
`.gsd-t/transcripts/in-session-{sessionId}.ndjson` via a dedicated hook
|
|
260
|
+
script (`scripts/hooks/gsd-t-conversation-capture.js`). The viewer's left
|
|
261
|
+
rail labels these entries `💬 conversation` (front-end-only discriminator
|
|
262
|
+
— the `in-session-` filename prefix is the contract).
|
|
263
|
+
|
|
264
|
+
This hook captures **content** (user prompts + assistant replies). It is
|
|
265
|
+
complementary to `scripts/hooks/gsd-t-in-session-usage-hook.js` (M43 D1),
|
|
266
|
+
which captures per-turn **token usage** into
|
|
267
|
+
`.gsd-t/metrics/token-usage.jsonl`. Both hooks coexist on the same events.
|
|
268
|
+
|
|
269
|
+
**Install block** (append to `~/.claude/settings.json` alongside the existing
|
|
270
|
+
context-meter, version-check, compact-detector, and in-session-usage hooks):
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"hooks": {
|
|
275
|
+
"SessionStart": [
|
|
276
|
+
{ "matcher": "",
|
|
277
|
+
"hooks": [{ "type": "command",
|
|
278
|
+
"command": "node \"$HOME/.claude/scripts/hooks/gsd-t-conversation-capture.js\"",
|
|
279
|
+
"async": true }] }
|
|
280
|
+
],
|
|
281
|
+
"UserPromptSubmit": [
|
|
282
|
+
{ "matcher": "",
|
|
283
|
+
"hooks": [{ "type": "command",
|
|
284
|
+
"command": "node \"$HOME/.claude/scripts/hooks/gsd-t-conversation-capture.js\"",
|
|
285
|
+
"async": true }] }
|
|
286
|
+
],
|
|
287
|
+
"Stop": [
|
|
288
|
+
{ "matcher": "",
|
|
289
|
+
"hooks": [{ "type": "command",
|
|
290
|
+
"command": "node \"$HOME/.claude/scripts/hooks/gsd-t-conversation-capture.js\"",
|
|
291
|
+
"async": true }] }
|
|
292
|
+
],
|
|
293
|
+
"PostToolUse": [
|
|
294
|
+
{ "matcher": "",
|
|
295
|
+
"hooks": [{ "type": "command",
|
|
296
|
+
"command": "GSD_T_CAPTURE_TOOL_USES=1 node \"$HOME/.claude/scripts/hooks/gsd-t-conversation-capture.js\"",
|
|
297
|
+
"async": true }] }
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The `PostToolUse` entry is **opt-in** via `GSD_T_CAPTURE_TOOL_USES=1`. Leave it
|
|
304
|
+
unset unless you want per-tool frames in the NDJSON (full tool payloads are
|
|
305
|
+
already recorded in `events/*.jsonl`).
|
|
306
|
+
|
|
307
|
+
Contract: `.gsd-t/contracts/conversation-capture-contract.md` v1.0.0. Frame
|
|
308
|
+
schema, file-naming, and session-id resolution rules are locked there.
|
|
309
|
+
|
|
256
310
|
## Observability Logging (MANDATORY)
|
|
257
311
|
|
|
258
312
|
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.
|