@tekyzinc/gsd-t 3.20.11 → 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 +33 -0
- package/bin/gsd-t.js +103 -0
- package/commands/cpua.md +155 -0
- package/package.json +1 -1
- package/scripts/gsd-t-dashboard-server.js +40 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
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
|
+
|
|
21
|
+
## [3.20.12] - 2026-05-05
|
|
22
|
+
|
|
23
|
+
### Fixed — install: auto-configure M43 D1 + M45 D2 in-session hooks
|
|
24
|
+
|
|
25
|
+
`gsd-t install` did not deploy or wire up the M43 D1 token-usage hook (`gsd-t-in-session-usage-hook.js`) or the M45 D2 conversation-capture hook (`gsd-t-conversation-capture.js`), even though the global CLAUDE.md "In-Session Conversation Capture" section documented these as mandatory. Result: the dashboard's `/transcripts` left rail never showed `💬 conversation` entries for in-session orchestrator dialog (this conversation right now); discovered when the live chat feed showed "no spawns yet" while the project's `.gsd-t/transcripts/` was missing entirely.
|
|
26
|
+
|
|
27
|
+
**Changes:**
|
|
28
|
+
- `bin/gsd-t.js`: new `installInSessionHooks()` + `configureInSessionHooks()` functions copy `scripts/hooks/gsd-t-conversation-capture.js` and `scripts/hooks/gsd-t-in-session-usage-hook.js` to `~/.claude/scripts/hooks/`, then register them in `~/.claude/settings.json` on the right events:
|
|
29
|
+
- `gsd-t-conversation-capture.js` → SessionStart, UserPromptSubmit, Stop (PostToolUse stays opt-in via the `GSD_T_CAPTURE_TOOL_USES=1` env flag)
|
|
30
|
+
- `gsd-t-in-session-usage-hook.js` → Stop
|
|
31
|
+
- New install heading **In-Session Hooks (Conversation Capture + Token Usage)** runs in the install pipeline immediately after Auto-Route.
|
|
32
|
+
- `test/filesystem.test.js`: bumped command-count assertions (54 → 55, utility 5 → 6) for the `cpua.md` command added in this session.
|
|
33
|
+
|
|
34
|
+
**Migration:** existing installs pick up the wiring on next `gsd-t update-all` or `gsd-t install` run. The configure step is idempotent; re-running is safe. Suite: 2042/2042 pass.
|
|
35
|
+
|
|
36
|
+
**Why this matters:** the conversation-capture hook is the only thing that lets you scroll back through your visualizer's `/transcripts` view and see chat with Claude in this session — without it, the dashboard's left rail is permanently empty for the in-session conversation. The token-usage hook records per-turn cost so the meter and economics dashboards have real data.
|
|
37
|
+
|
|
5
38
|
## [3.20.11] - 2026-05-05
|
|
6
39
|
|
|
7
40
|
### Fixed — install: ship `gsd-t-token-capture.cjs` to every project
|
package/bin/gsd-t.js
CHANGED
|
@@ -936,6 +936,106 @@ function configureAutoRouteHook(scriptPath) {
|
|
|
936
936
|
}
|
|
937
937
|
}
|
|
938
938
|
|
|
939
|
+
// ─── In-Session Hooks (M43 D1 token usage + M45 D2 conversation capture) ────
|
|
940
|
+
|
|
941
|
+
const HOOKS_DIR = path.join(SCRIPTS_DIR, "hooks");
|
|
942
|
+
const PKG_HOOKS = path.join(PKG_SCRIPTS, "hooks");
|
|
943
|
+
|
|
944
|
+
// Each entry: { script, events, async } — `events` is the array of hook event
|
|
945
|
+
// names this script must be wired into. The `gsd-t-conversation-capture.js`
|
|
946
|
+
// hook runs on SessionStart, UserPromptSubmit, and Stop (per the global
|
|
947
|
+
// CLAUDE.md M45 D2 install block — PostToolUse stays opt-in via the
|
|
948
|
+
// GSD_T_CAPTURE_TOOL_USES env flag, so we don't auto-register it).
|
|
949
|
+
// `gsd-t-in-session-usage-hook.js` runs on Stop (per M43 D1 contract).
|
|
950
|
+
const IN_SESSION_HOOKS = [
|
|
951
|
+
{
|
|
952
|
+
script: "gsd-t-conversation-capture.js",
|
|
953
|
+
events: ["SessionStart", "UserPromptSubmit", "Stop"],
|
|
954
|
+
async: true,
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
script: "gsd-t-in-session-usage-hook.js",
|
|
958
|
+
events: ["Stop"],
|
|
959
|
+
async: true,
|
|
960
|
+
},
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
function installInSessionHooks() {
|
|
964
|
+
ensureDir(SCRIPTS_DIR);
|
|
965
|
+
ensureDir(HOOKS_DIR);
|
|
966
|
+
|
|
967
|
+
if (!fs.existsSync(PKG_HOOKS)) {
|
|
968
|
+
info("No scripts/hooks/ in package — skipping in-session hooks");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Copy each script into ~/.claude/scripts/hooks/
|
|
973
|
+
for (const hook of IN_SESSION_HOOKS) {
|
|
974
|
+
const src = path.join(PKG_HOOKS, hook.script);
|
|
975
|
+
const dest = path.join(HOOKS_DIR, hook.script);
|
|
976
|
+
if (!fs.existsSync(src)) {
|
|
977
|
+
warn(`In-session hook source missing: ${hook.script} — skipping`);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const srcContent = fs.readFileSync(src, "utf8");
|
|
981
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
982
|
+
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
983
|
+
copyFile(src, dest, `hooks/${hook.script}`);
|
|
984
|
+
try { fs.chmodSync(dest, 0o755); } catch {}
|
|
985
|
+
} else {
|
|
986
|
+
info(`In-session hook unchanged: ${hook.script}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
configureInSessionHooks();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function configureInSessionHooks() {
|
|
994
|
+
const parsed = readSettingsJson();
|
|
995
|
+
if (parsed === null && fs.existsSync(SETTINGS_JSON)) {
|
|
996
|
+
warn("settings.json has invalid JSON — cannot configure in-session hooks");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const settings = parsed || {};
|
|
1000
|
+
if (!settings.hooks) settings.hooks = {};
|
|
1001
|
+
|
|
1002
|
+
let added = 0;
|
|
1003
|
+
for (const hook of IN_SESSION_HOOKS) {
|
|
1004
|
+
const scriptPath = path.join(HOOKS_DIR, hook.script);
|
|
1005
|
+
const cmd = `node "${scriptPath.replace(/\\/g, "\\\\")}"`;
|
|
1006
|
+
|
|
1007
|
+
for (const event of hook.events) {
|
|
1008
|
+
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
1009
|
+
const already = settings.hooks[event].some((entry) =>
|
|
1010
|
+
entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(hook.script))
|
|
1011
|
+
);
|
|
1012
|
+
if (already) continue;
|
|
1013
|
+
const hookEntry = { type: "command", command: cmd };
|
|
1014
|
+
if (hook.async) hookEntry.async = true;
|
|
1015
|
+
settings.hooks[event].push({
|
|
1016
|
+
matcher: "",
|
|
1017
|
+
hooks: [hookEntry],
|
|
1018
|
+
});
|
|
1019
|
+
added++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (added === 0) {
|
|
1024
|
+
info("In-session hooks already configured");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (isSymlink(SETTINGS_JSON)) {
|
|
1028
|
+
warn("Skipping settings.json write — target is a symlink");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
|
|
1033
|
+
success(`${added} in-session hook entr${added === 1 ? "y" : "ies"} configured in settings.json`);
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
warn(`Failed to write settings.json: ${e.message}`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
939
1039
|
// ─── Figma MCP ──────────────────────────────────────────────────────────────
|
|
940
1040
|
|
|
941
1041
|
const FIGMA_MCP_URL = "https://mcp.figma.com/mcp";
|
|
@@ -1321,6 +1421,9 @@ async function doInstall(opts = {}) {
|
|
|
1321
1421
|
heading("Auto-Route (UserPromptSubmit)");
|
|
1322
1422
|
installAutoRoute();
|
|
1323
1423
|
|
|
1424
|
+
heading("In-Session Hooks (Conversation Capture + Token Usage)");
|
|
1425
|
+
installInSessionHooks();
|
|
1426
|
+
|
|
1324
1427
|
heading("Figma MCP (Design-to-Code)");
|
|
1325
1428
|
configureFigmaMcp();
|
|
1326
1429
|
|
package/commands/cpua.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# CPUA — Commit, Publish, Update All
|
|
2
|
+
|
|
3
|
+
You are running the GSD-T release flow: commit pending in-tree changes, bump the version, publish to npm, tag the release, push to origin, and propagate to all registered projects.
|
|
4
|
+
|
|
5
|
+
This is a **scoped release command** — only run in the GSD-T source repo (`/Users/david/projects/GSD-T` or whichever directory hosts `package.json` with `"name": "@tekyzinc/gsd-t"`). If invoked elsewhere, abort with a clear error.
|
|
6
|
+
|
|
7
|
+
## Step 0: Verify scope
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node -e "
|
|
11
|
+
const pkg = require('./package.json');
|
|
12
|
+
if (pkg.name !== '@tekyzinc/gsd-t') {
|
|
13
|
+
console.error('ERROR: /cpua must be run from the GSD-T source repo. Current dir is not @tekyzinc/gsd-t.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
console.log('Scope OK — releasing', pkg.name, 'v' + pkg.version);
|
|
17
|
+
"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If this exits non-zero, stop and tell the user.
|
|
21
|
+
|
|
22
|
+
## Step 1: Pre-flight checks
|
|
23
|
+
|
|
24
|
+
Before staging anything:
|
|
25
|
+
|
|
26
|
+
1. **Run the test suite.** A failing suite blocks the release. Show the result count.
|
|
27
|
+
```bash
|
|
28
|
+
npm test 2>&1 | tail -10
|
|
29
|
+
```
|
|
30
|
+
2. **Show the pending changes** so the user can scan what's about to be committed:
|
|
31
|
+
```bash
|
|
32
|
+
git status -s
|
|
33
|
+
git diff --stat
|
|
34
|
+
```
|
|
35
|
+
3. **Decide the version bump** based on the nature of changes (semver per `~/.claude/CLAUDE.md` § Versioning):
|
|
36
|
+
- **Patch** (`x.y.10` → `x.y.11`) — bug fix, doc fix, minor improvement
|
|
37
|
+
- **Minor** (`x.y.zz` → `x.(y+1).10`) — new feature, new capability, behavior addition
|
|
38
|
+
- **Major** (`x.y.zz` → `(x+1).0.10`) — breaking change, major rework
|
|
39
|
+
- Patch numbers always ≥ 10 (per the patch-2-digit convention).
|
|
40
|
+
|
|
41
|
+
If the bump is ambiguous (e.g., the changes mix bug fix + new feature), ask the user which they want; default to the higher bump if the user doesn't answer immediately.
|
|
42
|
+
|
|
43
|
+
4. **Stop if any of these are true:**
|
|
44
|
+
- Test suite has failures
|
|
45
|
+
- Working tree is clean (nothing to commit — tell user "nothing to release")
|
|
46
|
+
- Current branch is not `main` (warn user; ask before proceeding)
|
|
47
|
+
- There are unstaged changes outside the GSD-T scope (e.g., `.gsd-t/.unattended/run.log`, `.gsd-t/headless-*.log`, `.gsd-t/events/*.jsonl`) — these are runtime noise and should NOT be committed. Stage explicitly by file, never `git add -A`.
|
|
48
|
+
|
|
49
|
+
## Step 2: Stage, version-bump, and update CHANGELOG + progress.md
|
|
50
|
+
|
|
51
|
+
1. **Stage only relevant files explicitly** — `git add path1 path2 ...`. Never `git add -A` or `git add .` (would scoop up runtime logs).
|
|
52
|
+
2. **Bump `package.json`** version to the new value.
|
|
53
|
+
3. **Add a CHANGELOG entry** at the top, immediately under the header. Use this format:
|
|
54
|
+
```markdown
|
|
55
|
+
## [NEW_VERSION] - YYYY-MM-DD
|
|
56
|
+
|
|
57
|
+
### {Added|Fixed|Changed|Removed} — {one-line summary}
|
|
58
|
+
|
|
59
|
+
{2-4 sentences explaining what + why. Bullet the file-level changes.}
|
|
60
|
+
|
|
61
|
+
- `path/to/file`: {what changed}
|
|
62
|
+
- `path/to/test`: {regression test added if applicable}
|
|
63
|
+
|
|
64
|
+
{migration note if any}
|
|
65
|
+
```
|
|
66
|
+
Use the live system clock for the date — pull from `[GSD-T NOW]` or `node -e "console.log(new Date().toISOString().slice(0,10))"`. Never use `currentDate`.
|
|
67
|
+
4. **Append a Decision Log entry** to `.gsd-t/progress.md` under `## Decision Log`. Format:
|
|
68
|
+
```
|
|
69
|
+
- YYYY-MM-DD HH:MM: [tag] {summary} (vNEW_VERSION) — {what changed and why}.
|
|
70
|
+
```
|
|
71
|
+
Tag is one of: `[fix]`, `[feature]`, `[refactor]`, `[docs]`, `[chore]`, `[release]`.
|
|
72
|
+
5. **Update `.gsd-t/progress.md` header** — bump the `## Date:` to today and `## Version:` to NEW_VERSION.
|
|
73
|
+
6. **Stage** the CHANGELOG and progress.md changes too.
|
|
74
|
+
|
|
75
|
+
## Step 3: Commit
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git commit -m "$(cat <<'EOF'
|
|
79
|
+
{type}({scope}): {short summary} (v{NEW_VERSION})
|
|
80
|
+
|
|
81
|
+
{2-3 sentences explaining the change and motivation.}
|
|
82
|
+
|
|
83
|
+
- {file}: {change}
|
|
84
|
+
- {file}: {change}
|
|
85
|
+
|
|
86
|
+
Suite: N/N pass.
|
|
87
|
+
|
|
88
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
|
89
|
+
EOF
|
|
90
|
+
)"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`{type}` is `fix` | `feat` | `chore` | `docs` | `refactor` per the convention seen in `git log --oneline`. `{scope}` is a short module/area identifier (e.g., `install`, `banner`, `parallel`).
|
|
94
|
+
|
|
95
|
+
## Step 4: Tag and push
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git tag v{NEW_VERSION}
|
|
99
|
+
git push origin main --tags
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If push fails (auth, network, conflict), surface the error and stop — do NOT publish a version that's not in origin.
|
|
103
|
+
|
|
104
|
+
## Step 5: Publish to npm
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm publish --access public 2>&1 | tail -10
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Verify the output shows `+ @tekyzinc/gsd-t@{NEW_VERSION}`. If publish fails:
|
|
111
|
+
- Auth issue → tell user to `npm login` and retry
|
|
112
|
+
- Version already published → version bump was wrong; redo Step 2 with a higher version
|
|
113
|
+
- Network → retry once, then surface the error
|
|
114
|
+
|
|
115
|
+
## Step 6: Update global install + propagate to projects
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install -g @tekyzinc/gsd-t@{NEW_VERSION}
|
|
119
|
+
gsd-t update-all 2>&1 | tail -30
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `npm install -g @tekyzinc/gsd-t@{NEW_VERSION}` (NOT `npm update -g`) — npm rejects update for some legacy version strings (e.g., `3.19.00` is invalid semver because of the leading-zero patch). Pinning to the exact version sidesteps this.
|
|
123
|
+
|
|
124
|
+
## Step 7: Report
|
|
125
|
+
|
|
126
|
+
Print a concise summary:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
✅ CPUA complete — v{OLD} → v{NEW}
|
|
130
|
+
|
|
131
|
+
Commit: {short_sha} {commit message subject}
|
|
132
|
+
Tag: v{NEW}
|
|
133
|
+
Pushed: origin/main + tags
|
|
134
|
+
npm: @tekyzinc/gsd-t@{NEW} published
|
|
135
|
+
Global: {old global version} → v{NEW}
|
|
136
|
+
Projects: N updated, M already current
|
|
137
|
+
|
|
138
|
+
Test suite: N/N pass — zero regressions.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If anything failed, surface the specific step + error and tell the user what to do next.
|
|
142
|
+
|
|
143
|
+
## Behavior
|
|
144
|
+
|
|
145
|
+
- **Always interactive on the version bump decision** — unless the user pre-specified the bump in `$ARGUMENTS` (e.g., `/cpua patch` or `/cpua minor`).
|
|
146
|
+
- **Never `git add -A`** — explicitly stage by file. Runtime logs and event streams stay out.
|
|
147
|
+
- **Never amend commits** — if anything goes wrong post-commit, create a new commit. (Per global CLAUDE.md.)
|
|
148
|
+
- **Never skip Step 1 pre-flight** — a broken release worse than a delayed one.
|
|
149
|
+
- **Always use the live system clock** for CHANGELOG and progress.md dates — `[GSD-T NOW]` or `node -e "..."`.
|
|
150
|
+
|
|
151
|
+
$ARGUMENTS
|
|
152
|
+
|
|
153
|
+
## Auto-Clear
|
|
154
|
+
|
|
155
|
+
All work is committed and propagated. Execute `/clear` to free the context window for the next command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekyzinc/gsd-t",
|
|
3
|
-
"version": "3.20.
|
|
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
|
-
|
|
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,
|