@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,615 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * GSD-T Live Activity Report (M54 D1 T1)
5
- *
6
- * Pure read-only observer that answers:
7
- *
8
- * "What subprocesses, tool calls, and watches is the orchestrator
9
- * currently running RIGHT NOW?"
10
- *
11
- * Contract: .gsd-t/contracts/live-activity-contract.md v1.0.0
12
- *
13
- * Hard rules:
14
- * 1. NEVER writes a file.
15
- * 2. NEVER calls an LLM or spawns a subprocess.
16
- * 3. Silent-fail on malformed inputs — skip the bad line, note it, continue.
17
- * 4. Zero external deps. `.cjs` so it loads in both ESM and CJS projects.
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const os = require('os');
23
- const crypto = require('crypto');
24
-
25
- const SCHEMA_VERSION = 1;
26
-
27
- // Tool-use threshold for `tool` kind detection (30 seconds)
28
- const TOOL_THRESHOLD_MS = 30_000;
29
-
30
- // Source-file mtime threshold — files older than this are considered stale (60 seconds)
31
- const MTIME_STALE_MS = 60_000;
32
-
33
- // ── Public API ──────────────────────────────────────────────────────────────
34
-
35
- /**
36
- * Compute live activities for the given project directory.
37
- *
38
- * @param {object} [opts]
39
- * @param {string} [opts.projectDir='.']
40
- * @param {Date} [opts.now] injection for tests
41
- * @returns {{ schemaVersion: number, generatedAt: string, activities: object[], notes: string[] }}
42
- */
43
- function computeLiveActivities(opts) {
44
- opts = opts || {};
45
- const projectDir = opts.projectDir || '.';
46
- const now = opts.now instanceof Date ? opts.now : new Date();
47
- const notes = [];
48
-
49
- // Source 1: events JSONL (today only)
50
- const today = now.toISOString().slice(0, 10);
51
- const eventsFile = path.join(projectDir, '.gsd-t', 'events', today + '.jsonl');
52
- const eventsActivities = _readEventsActivities(eventsFile, now, notes);
53
- const eventsMtime = _safeFileMtime(eventsFile);
54
-
55
- // Source 2: orchestrator JSONL (~/.claude/projects/<slug>/<sid>.jsonl)
56
- const orchestratorActivities = _readOrchestratorActivities(projectDir, now, notes);
57
-
58
- // UNION + dedup
59
- const allActivities = _dedup([...eventsActivities, ...orchestratorActivities]);
60
-
61
- // Source 3: spawn plan files (.gsd-t/spawns/*.json)
62
- const spawnActivities = _readSpawnActivities(projectDir, now, notes);
63
- const allWithSpawns = _dedup([...allActivities, ...spawnActivities]);
64
-
65
- // Apply liveness falsifiers
66
- const live = allWithSpawns.filter((a) => _isLive(a, eventsFile, eventsMtime, now, notes));
67
-
68
- return {
69
- schemaVersion: SCHEMA_VERSION,
70
- generatedAt: now.toISOString(),
71
- activities: live,
72
- notes,
73
- };
74
- }
75
-
76
- // ── Internal helpers ────────────────────────────────────────────────────────
77
-
78
- /**
79
- * Read activities from .gsd-t/events/<today>.jsonl.
80
- * Detects bash (run_in_background) and monitor (Monitor tool_use without stop).
81
- * Also detects tool kind (any tool_use > 30s without tool_result).
82
- */
83
- function _readEventsActivities(eventsFile, now, notes) {
84
- const activities = [];
85
- let content;
86
- try {
87
- if (!fs.existsSync(eventsFile)) {
88
- notes.push('no events file for today');
89
- return activities;
90
- }
91
- content = fs.readFileSync(eventsFile, 'utf8');
92
- } catch (err) {
93
- notes.push('could not read events file: ' + (err && err.message || String(err)));
94
- return activities;
95
- }
96
-
97
- const lines = content.split(/\r?\n/);
98
- const toolUseById = new Map(); // tool_use_id -> event object
99
- const terminatedIds = new Set(); // tool_use_ids with tool_result or monitor_stopped
100
- const monitorStoppedIds = new Set();
101
-
102
- // First pass: collect all events
103
- const allEvents = [];
104
- for (let i = 0; i < lines.length; i++) {
105
- const line = lines[i].trim();
106
- if (!line) continue;
107
- let obj;
108
- try {
109
- obj = JSON.parse(line);
110
- } catch (_) {
111
- notes.push('skipped malformed JSONL line at ' + eventsFile + ':' + (i + 1));
112
- continue;
113
- }
114
- if (!obj || typeof obj !== 'object') continue;
115
- allEvents.push({ obj, lineno: i + 1 });
116
- }
117
-
118
- // Second pass: build lookup maps
119
- for (const { obj } of allEvents) {
120
- const type = obj.type || obj.event_type;
121
-
122
- // tool_result terminates a tool_use
123
- if (type === 'tool_result' && obj.tool_use_id) {
124
- terminatedIds.add(obj.tool_use_id);
125
- }
126
- // monitor_stopped terminates a monitor
127
- if (type === 'monitor_stopped' && obj.tool_use_id) {
128
- terminatedIds.add(obj.tool_use_id);
129
- monitorStoppedIds.add(obj.tool_use_id);
130
- }
131
- // spawn_completed terminates a spawn
132
- if (type === 'spawn_completed' && obj.tool_use_id) {
133
- terminatedIds.add(obj.tool_use_id);
134
- }
135
- }
136
-
137
- // Third pass: detect activities
138
- for (const { obj } of allEvents) {
139
- const type = obj.type || obj.event_type;
140
-
141
- // bash: run_in_background sentinel
142
- if (obj.run_in_background === true) {
143
- const id = obj.tool_use_id || _makeId('bash', obj.command || obj.label || '', obj.startedAt || obj.ts || now.toISOString());
144
- if (!terminatedIds.has(id)) {
145
- activities.push(_makeActivity({
146
- id,
147
- kind: 'bash',
148
- label: obj.command || obj.label || 'bash',
149
- startedAt: obj.startedAt || obj.ts || now.toISOString(),
150
- now,
151
- pid: obj.pid,
152
- toolUseId: obj.tool_use_id,
153
- }));
154
- }
155
- continue;
156
- }
157
-
158
- // Monitor and tool: tool_use events
159
- const toolName = obj.name || (obj.type === 'tool_use' && obj.name) || (obj.event_type === 'tool_use_started' && obj.name);
160
- if (type === 'tool_use' || type === 'tool_use_started') {
161
- const toolUseId = obj.tool_use_id || obj.id;
162
- if (!toolUseId) continue;
163
- if (terminatedIds.has(toolUseId)) continue;
164
-
165
- const name = obj.name || toolName || '';
166
- const startedAt = obj.startedAt || obj.start_time || obj.ts || now.toISOString();
167
- const startMs = _safeDate(startedAt);
168
- const ageMs = startMs ? now.getTime() - startMs.getTime() : 0;
169
-
170
- if (name === 'Monitor') {
171
- // Monitor kind
172
- activities.push(_makeActivity({
173
- id: toolUseId,
174
- kind: 'monitor',
175
- label: (obj.input && (obj.input.command || obj.input.path)) || name,
176
- startedAt,
177
- now,
178
- pid: obj.pid,
179
- toolUseId,
180
- }));
181
- } else if (ageMs > TOOL_THRESHOLD_MS) {
182
- // Tool kind: any tool_use > 30s without result
183
- activities.push(_makeActivity({
184
- id: toolUseId,
185
- kind: 'tool',
186
- label: name || 'tool',
187
- startedAt,
188
- now,
189
- pid: obj.pid,
190
- toolUseId,
191
- }));
192
- }
193
- }
194
- }
195
-
196
- return activities;
197
- }
198
-
199
- /**
200
- * Read activities from the orchestrator JSONL (~/.claude/projects/<slug>/<sid>.jsonl).
201
- * Uses slug discovery via _slugFromTranscriptPath / _slugToProjectDir from
202
- * scripts/hooks/gsd-t-conversation-capture.js.
203
- */
204
- function _readOrchestratorActivities(projectDir, now, notes) {
205
- const activities = [];
206
-
207
- // Lazy-load the slug helpers from conversation-capture.js
208
- let slugHelpers;
209
- try {
210
- slugHelpers = require(path.join(__dirname, '..', 'scripts', 'hooks', 'gsd-t-conversation-capture.js'));
211
- } catch (err) {
212
- notes.push('could not load slug helpers: ' + (err && err.message || String(err)));
213
- return activities;
214
- }
215
-
216
- const { _slugFromTranscriptPath, _slugToProjectDir } = slugHelpers._internal || slugHelpers;
217
-
218
- // Derive slug from project dir: reverse the _slugToProjectDir logic
219
- // by looking for a slug in ~/.claude/projects/ that decodes to projectDir
220
- const home = process.env.HOME || os.homedir();
221
- if (!home) {
222
- notes.push('could not determine HOME directory for slug discovery');
223
- return activities;
224
- }
225
-
226
- const projectsDir = path.join(home, '.claude', 'projects');
227
- let slugDir;
228
- try {
229
- if (!fs.existsSync(projectsDir)) {
230
- notes.push('no ~/.claude/projects/ directory found');
231
- return activities;
232
- }
233
- // Enumerate slugs and find one that maps to projectDir
234
- const slugs = fs.readdirSync(projectsDir, { withFileTypes: true })
235
- .filter((e) => e.isDirectory())
236
- .map((e) => e.name);
237
-
238
- const resolvedProject = path.resolve(projectDir);
239
- for (const slug of slugs) {
240
- const decoded = _slugToProjectDir(slug);
241
- if (decoded && path.resolve(decoded) === resolvedProject) {
242
- slugDir = path.join(projectsDir, slug);
243
- break;
244
- }
245
- }
246
- } catch (err) {
247
- notes.push('slug discovery error: ' + (err && err.message || String(err)));
248
- return activities;
249
- }
250
-
251
- if (!slugDir) {
252
- notes.push('orchestrator slug unresolvable for cwd=' + projectDir);
253
- return activities;
254
- }
255
-
256
- // Find most recent JSONL file in the slug directory
257
- let jsonlFile;
258
- try {
259
- const files = fs.readdirSync(slugDir)
260
- .filter((f) => f.endsWith('.jsonl'))
261
- .map((f) => {
262
- const full = path.join(slugDir, f);
263
- let mtime = 0;
264
- try { mtime = fs.statSync(full).mtimeMs; } catch (_) { /* noop */ }
265
- return { f, full, mtime };
266
- })
267
- .sort((a, b) => b.mtime - a.mtime);
268
- if (files.length > 0) jsonlFile = files[0].full;
269
- } catch (err) {
270
- notes.push('could not list slug dir ' + slugDir + ': ' + (err && err.message || String(err)));
271
- return activities;
272
- }
273
-
274
- if (!jsonlFile) {
275
- notes.push('no JSONL files in slug dir ' + slugDir);
276
- return activities;
277
- }
278
-
279
- // Parse orchestrator JSONL
280
- let content;
281
- try {
282
- content = fs.readFileSync(jsonlFile, 'utf8');
283
- } catch (err) {
284
- notes.push('could not read orchestrator JSONL ' + jsonlFile + ': ' + (err && err.message || String(err)));
285
- return activities;
286
- }
287
-
288
- const lines = content.split(/\r?\n/);
289
- const terminatedIds = new Set();
290
- const allEvents = [];
291
-
292
- // First pass: collect
293
- for (let i = 0; i < lines.length; i++) {
294
- const line = lines[i].trim();
295
- if (!line) continue;
296
- let obj;
297
- try {
298
- obj = JSON.parse(line);
299
- } catch (_) {
300
- // Orchestrator JSONL lines can be complex; skip silently
301
- continue;
302
- }
303
- if (!obj || typeof obj !== 'object') continue;
304
- allEvents.push(obj);
305
- }
306
-
307
- // The orchestrator JSONL stores conversation turns. Each line is an object with
308
- // a `message` field containing a Claude API message (role: "assistant" | "user").
309
- // Tool uses are in assistant messages: message.content[].type === "tool_use".
310
- // Tool results are in user messages: message.content[].type === "tool_result".
311
-
312
- // First pass: collect all termination signals from tool_result blocks
313
- for (const obj of allEvents) {
314
- // Direct termination events (from GSD-T event hooks)
315
- if (obj.type === 'monitor_stopped' && obj.tool_use_id) {
316
- terminatedIds.add(obj.tool_use_id);
317
- }
318
- if (obj.type === 'spawn_completed' && obj.tool_use_id) {
319
- terminatedIds.add(obj.tool_use_id);
320
- }
321
-
322
- // Claude API user turns: message.content[].type === "tool_result"
323
- const msg = obj.message;
324
- if (msg && msg.role === 'user' && Array.isArray(msg.content)) {
325
- for (const block of msg.content) {
326
- if (block && block.type === 'tool_result' && block.tool_use_id) {
327
- terminatedIds.add(block.tool_use_id);
328
- }
329
- }
330
- }
331
- }
332
-
333
- // Second pass: collect tool_use blocks from assistant messages
334
- const toolUseBlocks = []; // { block, timestamp }
335
- for (const obj of allEvents) {
336
- const msg = obj.message;
337
- if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
338
- // Timestamp: prefer the message-level timestamp if available
339
- const timestamp = obj.timestamp || (msg && msg.created_at) || null;
340
- for (const block of msg.content) {
341
- if (block && block.type === 'tool_use' && block.id) {
342
- toolUseBlocks.push({ block, timestamp });
343
- }
344
- }
345
- }
346
-
347
- // Third pass: emit activities for non-terminated tool uses
348
- for (const { block, timestamp } of toolUseBlocks) {
349
- const toolUseId = block.id;
350
- if (terminatedIds.has(toolUseId)) continue;
351
-
352
- const name = block.name || '';
353
- const startedAt = timestamp || now.toISOString();
354
- const startMs = _safeDate(startedAt);
355
- const ageMs = startMs ? now.getTime() - startMs.getTime() : 0;
356
-
357
- if (name === 'Bash') {
358
- const isBg = block.input && block.input.run_in_background === true;
359
- if (isBg) {
360
- const cmd = (block.input && block.input.command) || name;
361
- activities.push(_makeActivity({
362
- id: toolUseId,
363
- kind: 'bash',
364
- label: cmd,
365
- startedAt,
366
- now,
367
- toolUseId,
368
- }));
369
- }
370
- } else if (name === 'Monitor') {
371
- activities.push(_makeActivity({
372
- id: toolUseId,
373
- kind: 'monitor',
374
- label: (block.input && (block.input.command || block.input.path)) || name,
375
- startedAt,
376
- now,
377
- toolUseId,
378
- }));
379
- } else if (ageMs > TOOL_THRESHOLD_MS) {
380
- activities.push(_makeActivity({
381
- id: toolUseId,
382
- kind: 'tool',
383
- label: name || 'tool',
384
- startedAt,
385
- now,
386
- toolUseId,
387
- }));
388
- }
389
- }
390
-
391
- return activities;
392
- }
393
-
394
- /**
395
- * Read spawn kind activities from .gsd-t/spawns/*.json plan files.
396
- * Delegates to the same plan-file reader shape as parallelism-report.cjs.
397
- */
398
- function _readSpawnActivities(projectDir, now, notes) {
399
- const activities = [];
400
- const spawnPlans = _readSpawnPlans(projectDir, notes);
401
-
402
- for (const plan of spawnPlans) {
403
- // Only include active (not ended) plans
404
- if (plan.endedAt !== null && plan.endedAt !== undefined) continue;
405
-
406
- const startedAt = plan.startedAt || now.toISOString();
407
- const label = (plan.spawnId || plan.kind || 'spawn') + (plan.command ? ' ' + String(plan.command).slice(0, 30) : '');
408
- const id = plan.spawnId || _makeId('spawn', label, startedAt);
409
-
410
- activities.push(_makeActivity({
411
- id,
412
- kind: 'spawn',
413
- label,
414
- startedAt,
415
- now,
416
- pid: plan.pid || plan.workerPid,
417
- toolUseId: plan.tool_use_id,
418
- }));
419
- }
420
-
421
- return activities;
422
- }
423
-
424
- /**
425
- * Read .gsd-t/spawns/*.json plan files. Mirrors parallelism-report.cjs's _readSpawnPlans.
426
- */
427
- function _readSpawnPlans(projectDir, notes) {
428
- const dir = path.join(projectDir, '.gsd-t', 'spawns');
429
- let files;
430
- try {
431
- if (!fs.existsSync(dir)) return [];
432
- files = fs.readdirSync(dir).filter((n) => n.endsWith('.json'));
433
- } catch (err) {
434
- notes.push('spawns dir unreadable: ' + (err && err.message || err));
435
- return [];
436
- }
437
- const plans = [];
438
- for (const f of files) {
439
- const full = path.join(dir, f);
440
- let raw;
441
- try {
442
- raw = fs.readFileSync(full, 'utf8');
443
- } catch (err) {
444
- notes.push('could not read spawn plan ' + full + ': ' + (err && err.message || err));
445
- continue;
446
- }
447
- let parsed;
448
- try {
449
- parsed = JSON.parse(raw);
450
- } catch (_) {
451
- notes.push('spawn-plan malformed JSON: ' + f);
452
- continue;
453
- }
454
- if (!parsed || typeof parsed !== 'object') {
455
- notes.push('spawn-plan not an object: ' + f);
456
- continue;
457
- }
458
- plans.push(parsed);
459
- }
460
- return plans;
461
- }
462
-
463
- /**
464
- * Deduplicate activities by tool_use_id (priority 1) then (kind, label, startedAt) tuple (priority 2).
465
- */
466
- function _dedup(activities) {
467
- const byToolUseId = new Map();
468
- const byTuple = new Map();
469
- const result = [];
470
-
471
- for (const a of activities) {
472
- // Priority 1: deduplicate by tool_use_id
473
- if (a.toolUseId) {
474
- if (byToolUseId.has(a.toolUseId)) {
475
- // Merge: prefer orchestrator pid when present
476
- const existing = byToolUseId.get(a.toolUseId);
477
- if (a.pid && !existing.pid) existing.pid = a.pid;
478
- continue;
479
- }
480
- byToolUseId.set(a.toolUseId, a);
481
- result.push(a);
482
- continue;
483
- }
484
-
485
- // Priority 2: deduplicate by (kind, label, startedAt) tuple
486
- const tupleKey = a.kind + '|' + a.label + '|' + a.startedAt;
487
- if (byTuple.has(tupleKey)) {
488
- const existing = byTuple.get(tupleKey);
489
- if (a.pid && !existing.pid) existing.pid = a.pid;
490
- continue;
491
- }
492
- byTuple.set(tupleKey, a);
493
- result.push(a);
494
- }
495
-
496
- return result;
497
- }
498
-
499
- /**
500
- * Apply all 3 liveness falsifiers to an activity.
501
- * Returns true if the activity should remain in activities[].
502
- * Returns false if any falsifier fires.
503
- */
504
- function _isLive(activity, eventsFile, eventsMtime, now, notes) {
505
- // F1: explicit terminating event (already handled during parsing — entries with
506
- // matching tool_result/monitor_stopped/spawn_completed were never added)
507
- // This is a double-check for spawn kind which reads from a different source.
508
- if (activity.kind === 'spawn') {
509
- // For spawn kind, F1 is already handled in _readSpawnActivities (endedAt check).
510
- // No additional F1 check needed here for spawns.
511
- }
512
-
513
- // F2: PID check — process.kill(pid, 0) throws ESRCH
514
- if (activity.pid) {
515
- try {
516
- process.kill(activity.pid, 0);
517
- // Process is alive (no error thrown)
518
- } catch (err) {
519
- if (err && err.code === 'ESRCH') {
520
- // Process not found — remove entry
521
- return false;
522
- }
523
- // EPERM or other: conservative, treat as dead
524
- notes.push('PID check error for pid ' + activity.pid + ': ' + (err && err.code || String(err)));
525
- return false;
526
- }
527
- }
528
-
529
- // F3: Source-file mtime > 60s old
530
- if (activity.kind === 'spawn') {
531
- // Spawn activities use spawn plan file mtime, not events file
532
- // Spawn plan mtime > 60s → stale
533
- const spawnMtime = activity._spawnPlanMtime;
534
- if (spawnMtime) {
535
- const ageMs = now.getTime() - spawnMtime;
536
- if (ageMs > MTIME_STALE_MS) return false;
537
- }
538
- } else {
539
- // Events-sourced activities: check events file mtime
540
- if (eventsMtime) {
541
- const ageMs = now.getTime() - eventsMtime;
542
- if (ageMs > MTIME_STALE_MS) return false;
543
- }
544
- }
545
-
546
- return true;
547
- }
548
-
549
- /**
550
- * Safely stat a file and return its mtime in milliseconds, or null on error.
551
- */
552
- function _safeFileMtime(filePath) {
553
- try {
554
- return fs.statSync(filePath).mtimeMs;
555
- } catch (_) {
556
- return null;
557
- }
558
- }
559
-
560
- /**
561
- * Construct an Activity object.
562
- */
563
- function _makeActivity({ id, kind, label, startedAt, now, pid, toolUseId, _spawnPlanMtime }) {
564
- const startMs = _safeDate(startedAt);
565
- const durationMs = startMs ? Math.max(0, now.getTime() - startMs.getTime()) : 0;
566
- const activity = {
567
- id: String(id),
568
- kind,
569
- label: String(label),
570
- startedAt: startMs ? startMs.toISOString() : new Date(now).toISOString(),
571
- durationMs,
572
- tailUrl: '/api/live-activity/' + encodeURIComponent(String(id)) + '/tail',
573
- alive: true,
574
- };
575
- if (pid != null) activity.pid = pid;
576
- if (toolUseId) activity.toolUseId = toolUseId;
577
- if (_spawnPlanMtime != null) activity._spawnPlanMtime = _spawnPlanMtime;
578
- return activity;
579
- }
580
-
581
- /**
582
- * Generate a deterministic ID from kind + label + startedAt when no tool_use_id is available.
583
- */
584
- function _makeId(kind, label, startedAt) {
585
- const hash = crypto.createHash('sha1').update(label + startedAt).digest('hex').slice(0, 12);
586
- return kind + ':' + hash;
587
- }
588
-
589
- /**
590
- * Safely parse a date string. Returns null for invalid inputs.
591
- */
592
- function _safeDate(v) {
593
- if (!v || typeof v !== 'string') return null;
594
- const d = new Date(v);
595
- if (isNaN(d.getTime())) return null;
596
- return d;
597
- }
598
-
599
- // ── Module exports ──────────────────────────────────────────────────────────
600
-
601
- module.exports = {
602
- computeLiveActivities,
603
- SCHEMA_VERSION,
604
- // Exposed for unit tests only; not part of the public contract.
605
- _readEventsActivities,
606
- _readOrchestratorActivities,
607
- _readSpawnActivities,
608
- _readSpawnPlans,
609
- _dedup,
610
- _isLive,
611
- _makeActivity,
612
- _makeId,
613
- _safeDate,
614
- _safeFileMtime,
615
- };
package/bin/log-tail.cjs DELETED
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env node
2
- // Log Tail — print the last N lines of a log file. Used by GSD-T command files to
3
- // truncate test/build output before forwarding it into the conversation context.
4
- //
5
- // Usage:
6
- // node bin/log-tail.js <logfile> # print last 100 lines
7
- // node bin/log-tail.js <logfile> 500 # print last 500 lines
8
- // node bin/log-tail.js <logfile> --on-fail # print 500 lines if the log contains
9
- // "FAIL", "ERROR", or non-zero exit;
10
- // 100 lines otherwise
11
- //
12
- // Why: piping `npm test` or `playwright test` directly into a Bash tool result
13
- // dumps the entire stdout (often 5K-50K tokens) into context. This helper writes
14
- // the full log to disk and prints only the tail, with a header showing the path
15
- // to the full log so the agent can read more if needed.
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
-
20
- function parseArgs(argv) {
21
- const opts = { logFile: null, lines: 100, onFail: false };
22
- for (let i = 0; i < argv.length; i++) {
23
- const a = argv[i];
24
- if (a === '--on-fail') opts.onFail = true;
25
- else if (a === '--help' || a === '-h') {
26
- console.log('Usage: node bin/log-tail.js <logfile> [N=100] [--on-fail]');
27
- process.exit(0);
28
- } else if (!opts.logFile) {
29
- opts.logFile = a;
30
- } else if (/^\d+$/.test(a)) {
31
- opts.lines = parseInt(a, 10);
32
- }
33
- }
34
- return opts;
35
- }
36
-
37
- function detectFailure(content) {
38
- return /\b(FAIL|FAILED|ERROR|Exception|Traceback|Test Failed|✗|❌)\b/i.test(content);
39
- }
40
-
41
- function tail(content, n) {
42
- const lines = content.split('\n');
43
- if (lines.length <= n) return lines;
44
- return lines.slice(-n);
45
- }
46
-
47
- function main() {
48
- const opts = parseArgs(process.argv.slice(2));
49
- if (!opts.logFile) {
50
- console.error('Usage: node bin/log-tail.js <logfile> [N=100] [--on-fail]');
51
- process.exit(2);
52
- }
53
-
54
- const abs = path.resolve(opts.logFile);
55
- if (!fs.existsSync(abs)) {
56
- console.error(`log-tail: file not found — ${abs}`);
57
- process.exit(2);
58
- }
59
-
60
- const content = fs.readFileSync(abs, 'utf8');
61
- let n = opts.lines;
62
- if (opts.onFail) {
63
- n = detectFailure(content) ? 500 : 100;
64
- }
65
-
66
- const total = content.split('\n').length;
67
- const tailLines = tail(content, n);
68
- const truncated = total > n;
69
-
70
- console.log(`─── log tail: ${abs} ───`);
71
- console.log(` total lines: ${total} showing: ${tailLines.length} truncated: ${truncated}`);
72
- if (truncated) {
73
- console.log(` full log: cat ${abs}`);
74
- }
75
- console.log('─────────────────────────────────────────────────────────────');
76
- console.log(tailLines.join('\n'));
77
- }
78
-
79
- if (require.main === module) main();
80
-
81
- module.exports = { tail, detectFailure };