@tekyzinc/gsd-t 3.20.12 → 3.20.13

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.20.13] - 2026-05-05
6
+
7
+ ### Fixed — visualizer: surface in-session NDJSONs when `.index.json` is empty
8
+
9
+ The dashboard's `/transcripts` endpoint only read `.gsd-t/transcripts/.index.json` to populate the left-rail spawn list. The M45 D2 conversation-capture hook writes `in-session-{sessionId}.ndjson` directly to `transcripts/` but does NOT update the index — the index is owned by the spawn lifecycle, not the in-session hook. Result: the visualizer's left rail showed "no spawns yet" even when the in-session conversation was actively being captured to disk. Discovered when the M43 D1 + M45 D2 hooks were installed (v3.20.12) and the conversation NDJSONs were appearing on disk but invisible in the UI.
10
+
11
+ **Changes:**
12
+ - `scripts/gsd-t-dashboard-server.js`: new `listInSessionTranscripts(projectDir)` function scans `transcripts/` for `in-session-*.ndjson` files and returns spawn-shaped entries with `spawnId: in-session-{sessionId}`. Filenames are validated through `isValidSpawnId` for path-traversal safety. `handleTranscriptsList` merges these with the index entries (index takes precedence on dup `spawnId`). The viewer's existing `in-session-` prefix detection then applies the `💬 conversation` badge.
13
+ - `test/dashboard-server.test.js`: 5 new regression tests covering the empty-dir case, missing-dir case, find/filter behavior, mixed file types, and malformed-filename rejection.
14
+
15
+ **Migration:** existing dashboards picking up the new code will surface in-session conversations automatically on next refresh of `/transcripts`. No state migration needed; the index is read-only here.
16
+
17
+ **Suite:** 2047/2047 pass.
18
+
19
+ **Side cleanup:** killed 144 + 20 orphan `gsd-t-dashboard-server.js` processes (164 total) accumulated from prior detached spawns whose parents had exited and been reparented to launchd. 3 stale pidfiles cleaned. Per-project `transcripts/` directories pre-created in 15 GSD-T projects so the M45 D2 hook can write without first-run delay.
20
+
5
21
  ## [3.20.12] - 2026-05-05
6
22
 
7
23
  ### Fixed — install: auto-configure M43 D1 + M45 D2 in-session hooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.20.12",
3
+ "version": "3.20.13",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -176,9 +176,47 @@ function isValidSpawnId(id) {
176
176
  return typeof id === "string" && /^[a-zA-Z0-9._-]+$/.test(id) && id.length <= 200;
177
177
  }
178
178
 
179
+ // M45 D2 follow-up: filesystem-walk fallback for in-session conversation NDJSONs.
180
+ // The conversation-capture hook writes `in-session-{sessionId}.ndjson` directly
181
+ // to `transcripts/`, but does NOT update `.index.json` (the index is owned by
182
+ // the spawn lifecycle, not by the in-session hook). Without this scan, the
183
+ // dashboard's left rail never shows in-session conversations even though the
184
+ // NDJSONs are on disk. Synthesizes a spawn-shaped entry per in-session file
185
+ // using filesystem timestamps; the viewer's `in-session-` prefix detection
186
+ // then labels it as `💬 conversation`.
187
+ function listInSessionTranscripts(projectDir) {
188
+ const dir = transcriptsDir(projectDir);
189
+ let files;
190
+ try { files = fs.readdirSync(dir); } catch { return []; }
191
+ const out = [];
192
+ for (const f of files) {
193
+ if (!f.startsWith("in-session-") || !f.endsWith(".ndjson")) continue;
194
+ const spawnId = f.slice(0, -".ndjson".length); // "in-session-{sessionId}"
195
+ if (!isValidSpawnId(spawnId)) continue;
196
+ let stat;
197
+ try { stat = fs.statSync(path.join(dir, f)); } catch { continue; }
198
+ out.push({
199
+ spawnId,
200
+ command: "in-session conversation",
201
+ startedAt: stat.birthtime ? stat.birthtime.toISOString() : stat.mtime.toISOString(),
202
+ lastUpdatedAt: stat.mtime.toISOString(),
203
+ status: "active", // best-effort; the viewer doesn't currently use this field
204
+ kind: "in-session",
205
+ });
206
+ }
207
+ return out;
208
+ }
209
+
179
210
  function handleTranscriptsList(req, res, projectDir, transcriptHtmlPath) {
180
211
  const idx = readTranscriptsIndex(projectDir);
181
- const sorted = idx.spawns
212
+
213
+ // Merge index entries with in-session NDJSONs from the filesystem.
214
+ // De-dupe by spawnId — index entries take precedence (richer metadata).
215
+ const known = new Set(idx.spawns.map((s) => s.spawnId));
216
+ const inSession = listInSessionTranscripts(projectDir).filter((s) => !known.has(s.spawnId));
217
+ const merged = idx.spawns.concat(inSession);
218
+
219
+ const sorted = merged
182
220
  .slice()
183
221
  .sort((a, b) => (Date.parse(b.startedAt) || 0) - (Date.parse(a.startedAt) || 0));
184
222
 
@@ -723,6 +761,7 @@ module.exports = {
723
761
  writeTranscriptsIndex,
724
762
  readIndexEntry,
725
763
  isValidSpawnId,
764
+ listInSessionTranscripts,
726
765
  handleTranscriptsList,
727
766
  handleTranscriptStream,
728
767
  handleTranscriptPage,