@tekyzinc/gsd-t 3.22.11 → 3.23.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,102 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.23.11] - 2026-05-07
6
+
7
+ ### Fixed — `/api/parallelism` 500 — install `parallelism-report.cjs` to `~/.claude/bin/`
8
+
9
+ - **Root cause**: `scripts/gsd-t-dashboard-server.js::_loadParallelismReporter` resolves `require(path.join(__dirname, "..", "bin", "parallelism-report.cjs"))`. With `__dirname = ~/.claude/scripts/`, it looks for `~/.claude/bin/parallelism-report.cjs` — but no installer code path ever populated `~/.claude/bin/`. Every dashboard 500'd on `/api/parallelism` with `Cannot find module …/parallelism-report.cjs`; the right-rail PARALLELISM panel silently dimmed for every project. The break suggests `~/.claude/bin/` propagation has been silently broken since the M44 D9 panel shipped.
10
+ - **Fix** (`bin/gsd-t.js`, ~30 LOC additive): new `GLOBAL_BIN_DIR = ~/.claude/bin` constant; new `GLOBAL_BIN_TOOLS = ["parallelism-report.cjs"]` array; new `installGlobalBinTools()` mirroring `installUtilityScripts` shape (symlink-safe `copyFile`, eol-normalised idempotent compare, `+x` chmod). Wired into `doInstall()` between Utility Scripts and Context Meter so it runs on both `install` and `update`. `gsd-t doctor` gains `checkDoctorGlobalBin()` flagging missing tools with a clear "re-run install" hint.
11
+ - **Hot-patch**: `mkdir -p ~/.claude/bin && cp bin/parallelism-report.cjs ~/.claude/bin/` applied immediately so the live dashboard recovers without waiting for npm publish. Verified `curl http://localhost:7488/api/parallelism` returns 200 with the schema-versioned envelope (was 500 module-unavailable).
12
+ - **Doctrinal shift**: per the user's "test the real setup" directive, the regression spec lives under a new `e2e/live-journeys/` tree that probes the **running** dashboard instead of an in-process `startServer(0, ...)` fixture. Two specs added:
13
+ - `e2e/live-journeys/parallelism-endpoint.spec.ts` (4 tests) — schema envelope, no-500 sentinel, right-rail DOM populates from `/transcripts`, file-system regression sentinel for `~/.claude/bin/parallelism-report.cjs` existence.
14
+ - `e2e/live-journeys/dashboard-endpoint-coverage.spec.ts` (12 tests) — covers every dashboard route (`/`, `/transcripts`, `/ping`, `/metrics`, `/api/main-session`, `/api/spawn-plans`, `/api/parallelism`, `/api/parallelism/report`, `/events`, `/api/spawn-plans/stream`, 404 catch-all) plus a regression sentinel for the "parallelism-report module unavailable" string.
15
+ - Both specs `test.skip()` cleanly when no dashboard is reachable (`GSD_T_LIVE_DASHBOARD_URL` env override; default `http://localhost:7488`), keeping non-local CI green.
16
+ - **Adversarial Red Team** (focused, in-session): reviewed `update-all` self-heal gap (mitigated by `gsd-t doctor`), symlink safety (covered via `copyFile`), cross-platform path resolution (covered via `os.homedir()`), project-bin sweep collision (`parallelism-report.cjs` not in `DEPRECATED_BIN_STRAYS` so existing project copies are untouched). VERDICT: GRUDGING PASS — 0 blocking issues.
17
+ - **Verification**: unit suite **2233/2233 pass** (zero regressions). Playwright `e2e/journeys/` + `e2e/viewer/` **43/43 pass + 1 placeholder skip**. New live spec **4/4 pass** against the running :7488 dashboard.
18
+ - **Architecture doc**: `docs/architecture.md` Parallelism Observability section now records the install location and the distinction between `GLOBAL_BIN_TOOLS` (`~/.claude/bin/`) and `PROJECT_BIN_TOOLS` (per-project `bin/`).
19
+
20
+ ### Fixed — M53b conversation-capture project-routing: parallel sessions cross-talk
21
+
22
+ - **Root cause**: `scripts/hooks/gsd-t-conversation-capture.js::_resolveProjectDir(payload)` fell through to `_walkUpForProject(process.cwd())` when `GSD_T_PROJECT_DIR` was unset and `payload.cwd` was absent. `process.cwd()` for a Claude Code Stop/UserPromptSubmit hook is the directory the user launched `claude` from. When two parallel Claude Code sessions ran in different projects (`/Users/david/projects/GSD-T` and `/Users/david/projects/Move-Zoom-Recordings-to-GDrive`), the SAME node-runtime hook process resolved to whichever project the hook inherited via cwd — frames from one session landed in the other project's `.gsd-t/transcripts/` dir. Confirmed concrete misroute: GSD-T orchestrator's NDJSON written into `Move-Zoom-Recordings-to-GDrive/.gsd-t/transcripts/in-session-800d4b3b-….ndjson`.
23
+ - **Fix**: `_resolveProjectDir` now decodes `payload.transcript_path`'s `~/.claude/projects/{slug}/{sid}.jsonl` slug back to the real project root. New helpers:
24
+ - `_slugFromTranscriptPath(p)` — extracts the slug (first path segment after `~/.claude/projects/`); rejects paths outside that root.
25
+ - `_slugToProjectDir(slug)` — DFS-walks the filesystem, greedily consuming `-`-separated tokens as directory names; first leaf where `.gsd-t/` exists wins. Disambiguates literal-hyphen project names like `Move-Zoom-Recordings-to-GDrive` by consulting the disk. Rejects slugs containing `..`, `/`, `\\`, `\0`, or not starting with `-`.
26
+ - `_resolveProjectDir` priority is now: env → transcript_path slug → `payload.cwd` → cwd walk-up. Walk-up emits a one-line stderr warning ("project-dir resolved via cwd walk-up — unreliable for parallel sessions") so misroutes stay diagnosable.
27
+ - **Tests**: `test/m53b-conversation-routing.test.js` (new, 16 tests) covers happy-path slug-decode, parallel-session no-cross-talk, literal-hyphen disambiguation, non-GSD-T target fallthrough, path-traversal slug rejection, env-priority preservation, plus 8 unit-level helper tests. `test/m53b-conversation-routing-redteam.test.js` (new, 7 tests) — three adversarial `_resolveProjectDir` variants (walk-up only / naive slug-decode without `.gsd-t/` check / literal-slug-as-path) each violate at least one of four named invariants (I1 own-project / I2 .gsd-t/-existence / I3 slug-decoded / I4 not-neutral-cwd), with two positive controls proving the real fix passes all four on both clean-name and literal-hyphen projects.
28
+ - **Journey spec**: `e2e/journeys/conversation-routing.spec.ts` (new, 3 tests) — fires the real hook process twice with two different `transcript_path` values pointing at slugs encoding two different projects under a fake `$HOME`; asserts each NDJSON lands in the matching project, neither cross-routes, neutral-cwd has no `.gsd-t/transcripts/` tree (proves walk-up did not fire), and slug-as-literal-path attack is rejected. Manifest entry added to `.gsd-t/journey-manifest.json`.
29
+ - **Verification**: full unit suite **2226/2226 pass** (was 2210; +16 routing + 7 redteam = +23, zero regressions; the 2 pre-existing flakes from gsd-t-debug-env-induced `event-stream.test.js` / `watch-progress-writer.test.js` remain unchanged — pass cleanly when those env vars are unset). Playwright `e2e/journeys/` **16/16 pass** (was 13; +3 conversation-routing).
30
+ - **Note**: existing misrouted NDJSONs in `Move-Zoom-Recordings-to-GDrive/.gsd-t/transcripts/` remain (acceptable historical records). Going-forward NDJSONs will route correctly. Hot-patch applied to `~/.claude/scripts/hooks/gsd-t-conversation-capture.js`; full propagation on next `npm publish` + `/gsd-t-version-update-all`.
31
+ - **Contract**: `conversation-capture-contract.md` v1.1.0 → v1.2.0 (project-dir resolution algorithm documented with priority order + slug-decode protocol + defenses-against-pitfalls table; schema unchanged).
32
+
33
+ ### Fixed — M45 D2 conversation-capture regression: bodyless `assistant_turn` frames
34
+
35
+ - **Root cause**: `scripts/hooks/gsd-t-conversation-capture.js::_extractAssistantContent` tried payload shapes (`assistant_message`, `message.content`, `content`) that Claude Code's Stop hook never sends. Stop hook payload is `{session_id, transcript_path, hook_event_name, stop_hook_active}` — message body lives in the transcript JSONL at `transcript_path`. Function fell through to `null`; every `assistant_turn` frame written since v3.18.14 (M45 D2 ship 2026-04-23) was bodyless. Two weeks of broken capture; viewer correctly rendered empty bubbles.
36
+ - **Fix**: hook now reads the assistant body from `transcript_path`. New helpers:
37
+ - `_safeTranscriptPath(p)` — locks the path to `${HOME}/.claude/projects/`. Path-traversal attempts (`/etc/passwd`, relative paths) fail open (`return null`).
38
+ - `_readFileTail(filePath, 64*1024)` — opens fd, reads last 64 KB, drops leading mid-line partial. Multi-MB transcripts never get fully loaded.
39
+ - `_readAssistantFromTranscript(transcriptPath)` — scans tail bottom-up, parses each line as JSON (skips corrupt), picks the latest `type === 'assistant' && isSidechain !== true` row, concatenates all `text`-type content blocks (ignores `tool_use` / `tool_result` / `thinking`), skips tool_use-only rows.
40
+ - `_extractAssistantContent(payload)` — transcript-first; original 3 fallback shapes preserved for legacy/test payloads.
41
+ - **Tests**: `test/m45-d2-conversation-capture.test.js` +11 cases (transcript happy-path, multi-block concatenation, latest-row selection, sidechain skipping, tool_use-only skipping, /etc/passwd rejection, relative-path rejection, missing transcript_path → fallback, unreadable file → stub, >1 MB tail-only read, corrupt-JSON line skipping). `test/m53-conversation-content-redteam.test.js` (new, 4 tests) — three broken extractor variants (regress-to-old-code, picks-user-message, first-text-block-only) each violate one of three named invariants (I1 non-empty / I2 marker-match / I3 tail-marker-present), with a positive control proving the harness isn't trivially broken.
42
+ - **Journey spec**: `e2e/journeys/conversation-content.spec.ts` — writes a 7-frame in-session NDJSON fixture with 3 assistant_turn frames (one multi-paragraph with HEAD + TAIL markers); navigates to `/transcripts`; asserts `#main-stream .frame.assistant-turn` count = 3, every `.body` non-empty, each carries its expected marker, multi-paragraph TAIL marker present, USER-PROMPT marker absent from any assistant bubble, no `.frame.raw` JSON-dump fallback.
43
+ - **Verification**: full unit suite **2210/2210 pass** (was 2195; +11 M45 D2 + 4 M53 redteam = +15, zero regressions). Playwright `e2e/journeys/` + `e2e/viewer/` **36/36 pass** (was 35; +1 conversation-content).
44
+ - **Note**: existing 6 bodyless NDJSONs (`Move-Zoom-Recordings-to-GDrive/.gsd-t/transcripts/`) remain — historical records, acceptable. Going-forward NDJSONs will be populated. The installed hook at `~/.claude/scripts/hooks/gsd-t-conversation-capture.js` syncs on next `npm publish` + `/gsd-t-version-update-all`.
45
+ - **Contract**: `conversation-capture-contract.md` v1.0.0 → v1.1.0 (assistant-body extraction protocol documented; schema unchanged — same `assistant_turn` frame, just populated where v1.0.0 was bodyless).
46
+
47
+ ## [3.23.10] - 2026-05-06
48
+
49
+ ### Added — Rigorous User-Journey Coverage + Anti-Drift Test Quality (M52)
50
+
51
+ Closes the M48→M49→M50→M51 drift pattern where each test round caught the bug shape the previous round named, never the unnamed shape. M52's architectural fix is two-fold: (a) MECHANICAL coverage via a regex-based listener detector + pre-commit gate that blocks viewer-source commits with uncovered listeners; (b) DOCTRINAL change to what "rigorous" means — journey specs walk every interactive surface end-to-end with assertions on user-visible state, real-data NDJSON fixtures, adversarial Red Team scoped to JOURNEYS not lines.
52
+
53
+ **D1 — Journey-coverage tooling:**
54
+ - `bin/journey-coverage.cjs` (308 lines) — regex listener detector with single-pass string-mask precomputation (handles JS strings + HTML/JS comments). Recognises 6 listener kinds per contract §3. Zero parser deps. Sub-100ms on the full viewer file set.
55
+ - `bin/journey-coverage-cli.cjs` (107 lines) → `gsd-t check-coverage` — supports `--staged-only`, `--manifest PATH`, `--quiet`. Exit 0 (clean) / 4 (gap or stale) / 2 (manifest missing).
56
+ - `scripts/hooks/pre-commit-journey-coverage` (mode 0755, `set -e`) — viewer-source pattern set per contract §4. Marker block `# >>> GSD-T journey-coverage gate >>>` mirrors M50 idiom. Fail-open on detector internal exception.
57
+ - `bin/gsd-t.js` wiring (+46 lines under 50-line budget): `installJourneyCoverageHook` (idempotent), `gsd-t check-coverage` CLI dispatch, `gsd-t doctor --install-journey-hook` flag. Hook auto-installed by `init` after Playwright install for UI projects.
58
+ - `.gsd-t/contracts/journey-coverage-contract.md` v1.0.0 (PROPOSED → STABLE on D1 task-5).
59
+
60
+ **D2 — Journey specs + fixtures:**
61
+ - `e2e/journeys/` — 12 inaugural journey specs (`main-session-stream`, `click-completed-conversation`, `click-spawn-entry`, `splitter-drag`, `splitter-keyboard`, `right-rail-toggle`, `completed-collapse-toggle`, `auto-follow-toggle`, `kill-button`, `sessionstorage-persistence`, `keyboard-shortcuts`, `hashchange`). Every assertion verifies state changed / data flowed / content loaded / widget responded — zero `toBeVisible`/`toBeAttached` shallow assertions.
62
+ - `.gsd-t/journey-manifest.json` (new) — 12 entries 1:1 with the spec files; `covers[]` arrays span all 17 distinct viewer listeners (multiple listeners per spec where appropriate).
63
+ - `e2e/fixtures/journeys/` — 3 real-data NDJSONs sliced from captured `in-session-*.ndjson` (~50 / ~150 / ~80 frames). PII scrub: any user content > 200 chars truncated with `[…truncated]` marker.
64
+ - `e2e/fixtures/journeys/replay-helpers.ts` — `startReplayServer({fixture, asSessionId, inSession})` mounts the fixture into a temp project, starts the dashboard server with `port: 0` ephemeral, returns `{baseUrl, dispose}`. Zero new runtime deps.
65
+ - `templates/prompts/red-team-subagent.md` — additive new category "Test Pass-Through — Journey Edition" (existing categories untouched). Adversarial mandate: write ≥5 broken viewer patches, run the journey specs, every patch must be caught.
66
+
67
+ **Adversarial Red Team result:** 5 broken viewer patches written, all 5 caught by journey specs (P1: splitter:mousedown drag handler stripped → splitter-drag FAILS as expected; P2: `_ssSet(SS_KEY_SPLITTER, ...)` redirected to wrong key → splitter-drag + splitter-keyboard FAIL; P3: right-rail toggle handler stubbed to early-return → right-rail-toggle FAILS; P4: M52 narrowed-guard reverted to broken M48 wide-guard → click-completed-conversation FAILS, catching the M52 root-cause regression itself; P5: auto-follow change handler localStorage write removed → auto-follow-toggle FAILS). VERDICT: GRUDGING PASS. See `.gsd-t/red-team-report.md` § "M52 JOURNEY-EDITION RED TEAM".
68
+
69
+ **Hook end-to-end exercise:** synthetic `fakeBtn:click` listener appended to `scripts/gsd-t-transcript.html:1617` + staged → `bash .git/hooks/pre-commit` exit 1 with structured GAP report (`GAP: scripts/gsd-t-transcript.html:1617 fakeBtn:click (addEventListener) no spec covers this`); manifest extended with covering entry → re-run hook exit 0. Block + unblock paths both logged.
70
+
71
+ **Suite:** unit 2195/2195 pass (was 2167; +28 from M52 D1 detector + CLI + hook + helpers). E2E 35/35 + 1 skip preserved (was 23/35; +12 journey specs). `gsd-t check-coverage` reports `OK: 20 listeners, 12 specs` (exit 0, zero gaps, zero stale entries).
72
+
73
+ **Migration:** none. Hook is auto-installed on `gsd-t init` and re-installable via `gsd-t doctor --install-journey-hook`. Existing projects pick it up on next `gsd-t update-all`.
74
+
75
+ ### Fixed — historical in-session conversations are clickable again (M52 quick patch)
76
+
77
+ The M48 Bug 4 dual-pane mirror prevention was overcorrected: `scripts/gsd-t-transcript.html` early-returned for ANY rail entry whose spawn-id began with `in-session-`. But every COMPLETED rail entry is `in-session-*` (that's how the M45 D2 hook captures conversations), so clicking ANY completed conversation did nothing — there was no pane that would load it.
78
+
79
+ **Mental model fix:** Top pane is for the LIVE main session (owned by `/api/main-session`). Bottom pane is for ANYTHING else the user clicks — regular spawns OR historical in-session conversations. Only the LIVE main session's spawn id should be blocked from the bottom pane (that's the actual M48 Bug 4 case).
80
+
81
+ **Source narrowing in `scripts/gsd-t-transcript.html`:**
82
+ - `connectMain(sessionId)` exposes `window.__mainSessionId` so click + hashchange handlers can discriminate.
83
+ - renderRailEntry click handler: `if (isInSession && node.spawnId === ('in-session-' + window.__mainSessionId)) return;` — was `if (isInSession) return;`.
84
+ - hashchange handler: `if (id && id === ('in-session-' + window.__mainSessionId)) { return; }` — was `if (id && id.indexOf('in-session-') === 0) { return; }`.
85
+ - Legacy renderTree click handler: same narrowed pattern.
86
+ - Removed unconditional in-session-* scrub from initial-bottom-id seeding (historical sessionStorage selections survive reload now).
87
+ - Added `fetchMainSession` callback that clears the bottom-pane seed when it collides with the live main session id (preserves M48 Bug 4 mirror prevention).
88
+
89
+ **New journey spec `e2e/viewer/click-completed.spec.ts` (4 tests):**
90
+ - Rail renders 1 main session + 3 completed in-session entries.
91
+ - Clicking each completed entry loads it into the BOTTOM pane (and TOP pane stays on the live main session).
92
+ - Clicking the live MAIN entry does NOT load it into the bottom pane.
93
+ - sessionStorage persists across reload — bottom pane resumes the previously-clicked entry.
94
+
95
+ **Adversarial Red Team (3 broken patches, all caught):** (a) revert both checks to `if (isInSession) return;`; (b) `connect(id)` short-circuits on in-session prefix; (c) click handler routes in-session entries to TOP pane via `connectMain(...)` (clobbers main session). See `.gsd-t/red-team-report.md` § M52 RED TEAM FINDINGS.
96
+
97
+ **Unit-test ripple:** 5 source-pinned assertions in `test/m48-viewer-rendering-fixes.test.js` flipped from "asserts pre-M52 unconditional bail" to "asserts M52 narrowed live-main check". 1 new test for the fetchMainSession seed-collision callback.
98
+
99
+ **Suite:** 2167/2167 unit pass (added 1); E2E 23/23 + 1 placeholder skip (added 4 journey tests).
100
+
5
101
  ## [3.22.11] - 2026-05-06
6
102
 
7
103
  ### Fixed — viewer Playwright specs are now actually rigorous + adversarially proven (M51)
package/README.md CHANGED
@@ -16,6 +16,7 @@ A methodology for reliable, parallelizable development using Claude Code with op
16
16
  **External Task Orchestrator + Streaming Watcher UI (M40, v3.14.10)** — JS orchestrator drives `claude -p` one task per spawn: short-lived, fresh context, architecturally compaction-free. Benchmarks 0.72× wall-clock vs in-session on 20-task/3-wave workloads. Paired with a zero-Claude-cost local streaming UI at `127.0.0.1:7842` that renders all workers' stream-json output as a continuous claude.ai-style feed — task/wave banners, duration + usage chips, token corner bar, localStorage filters, replay via `WS /feed?from=N`. Recovery: `--resume` reconciles interrupted runs using commit + progress.md evidence; ambiguous tasks (commit without progress entry) are flagged for operator triage, never silently claimed done. CLI: `gsd-t orchestrate`, `gsd-t benchmark-orchestrator`, `gsd-t stream-feed`. Contracts: `stream-json-sink-contract.md` v1.1.0, `wave-join-contract.md`, `completion-signal-contract.md`, `metrics-schema-contract.md`.
17
17
  **Always-Headless Spawn (M43 D4, v3.16.x+) — Channel Separation** — every GSD-T command spawns detached, unconditionally. No `--watch`, no `--in-session`, no `--headless` opt-in, no context-meter threshold that reroutes. The dialog channel is reserved for human↔Claude conversation; every workflow turn is a detached headless child. Interactive session shows a launch banner + live-transcript URL + event-stream path, then exits — results surface via the read-back banner on the user's next message. Detached workers emit JSONL events to `.gsd-t/events/YYYY-MM-DD.jsonl` at every phase boundary — shared by dashboard and (historically) the watch command. The only in-session surface is the `/gsd` router (for dialog-only exploratory turns). See `.gsd-t/contracts/headless-default-contract.md` v2.0.0 and `unattended-event-stream-contract.md` v1.0.0.
18
18
  **Live Transcript as Primary Surface (M43 D6, v3.16.13 — extended in M47, v3.21.10)** — every detached spawn prints a one-line banner (`▶ Live transcript: http://127.0.0.1:{port}/transcript/{id}`) pointing at a browser viewer that SSE-streams the child's stdout and renders a collapsible "Tool Cost" sidebar panel showing per-tool attributed tokens and cost (sourced from `/transcript/:id/tool-cost`, which proxies to the M43 D2 tool-attribution library). The dashboard server auto-starts (`scripts/gsd-t-dashboard-autostart.cjs`) idempotently on each spawn — a port probe backs off when a server is already running, otherwise a fork-detach writes `.gsd-t/.dashboard.pid`. Port is project-scoped via `projectScopedDefaultPort(projectDir)` so multi-project workflows don't clobber each other.
19
+ **Rigorous User-Journey Coverage + Anti-Drift Test Quality (M52, v3.23.10)** — closes the M48→M51 drift pattern where each test round only caught previously-named bug shapes. Two-part fix: (a) `bin/journey-coverage.cjs` regex listener detector + `gsd-t check-coverage` CLI + `scripts/hooks/pre-commit-journey-coverage` commit gate (auto-installed on `gsd-t init`) — blocks viewer-source commits when uncovered listeners exist; (b) 12 inaugural journey specs in `e2e/journeys/` covering all 20 detected viewer listeners with functional assertions (zero `toBeVisible`-only tests). Red Team GRUDGING PASS: 5/5 broken viewer patches caught; hook block-then-unblock exercised. Suite: 2195/2195 unit + 35/35 E2E pass.
19
20
  **Universal Playwright Bootstrap + Deterministic UI Enforcement (M50, v3.22.10)** — converts the prose-only "Playwright Readiness Guard" into three executable enforcement layers so agents cannot skip UI tests. Layer 1: `bin/playwright-bootstrap.cjs` + `bin/ui-detection.cjs` wired into `init`/`update-all`/`doctor`/new `gsd-t setup-playwright [path]` subcommand — idempotent installer detects package manager, installs `@playwright/test` + chromium, writes config from a contract-locked template, scaffolds `e2e/`. Layer 2: `bin/headless-auto-spawn.cjs::autoSpawnHeadless()` auto-installs before any of 9 whitelisted testing/UI commands when `hasUI && !hasPlaywright`; install failure → `mode: 'blocked-needs-human'` exit-4. Layer 3: `scripts/hooks/pre-commit-playwright-gate` (opt-in via `gsd-t doctor --install-hooks`) reads `.gsd-t/.last-playwright-pass` and blocks viewer-source commits when staged files are newer than the last playwright test pass. Also ships the M47/M48/M49 E2E viewer specs (`e2e/viewer/title`, `timestamps`, `chat-bubbles`, `dual-pane`, `lazy-dashboard`). 62 new unit tests; E2E 8/9 pass.
20
21
  **Focused Visualizer Redesign (M47, v3.21.10)** — `/transcripts` opens directly to a dual-pane focused view: the **top pane** auto-streams the orchestrator's main in-session conversation (zero clicks — fetched via new `GET /api/main-session`), and the **bottom pane** streams whichever spawn the user clicks. A keyboard- and mouse-resizable splitter sits between them, with position persisted in `sessionStorage` (along with selection, completed-section toggle, and right-rail collapsed state). The left rail splits into three sections — `★ Main Session`, `Live Spawns`, and `Completed` (last 100 spawns, newest first, status-badged, collapsible). When a spawn transitions running → completed it moves rail sections live without a full reload, and stays selected if focused. The right rail (Spawn Plan / Parallelism / Tool Cost) is preserved under a new collapsible toggle. Bookmarks to `/transcript/:spawnId` continue to land that spawn pre-selected in the bottom pane. Contract: `dashboard-server-contract.md` v1.3.0 (additive — `status: 'active' \| 'completed'` field on in-session entries, derived from a 30s mtime window; `/api/main-session` endpoint with path-traversal guard + `Cache-Control: no-store`).
21
22
  - **Surgical model selection** — `bin/model-selector.js` assigns haiku/sonnet/opus per phase via a declarative rules table; `/advisor` escalation path with convention-based fallback.
package/bin/gsd-t.js CHANGED
@@ -45,6 +45,7 @@ const { mapHeadlessExitCode } = require(path.join(__dirname, "headless-exit-code
45
45
  const CLAUDE_DIR = path.join(os.homedir(), ".claude");
46
46
  const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
47
47
  const SCRIPTS_DIR = path.join(CLAUDE_DIR, "scripts");
48
+ const GLOBAL_BIN_DIR = path.join(CLAUDE_DIR, "bin");
48
49
  const CLAUDE_TEMPLATES_DIR = path.join(CLAUDE_DIR, "templates");
49
50
  const GLOBAL_CLAUDE_MD = path.join(CLAUDE_DIR, "CLAUDE.md");
50
51
  const SETTINGS_JSON = path.join(CLAUDE_DIR, "settings.json");
@@ -657,6 +658,35 @@ function installCaptureLintHook(projectDir) {
657
658
  return true;
658
659
  }
659
660
 
661
+ // M52 D1 — journey-coverage gate hook installer. Block-delimited so uninstall
662
+ // is a clean string excise. Stock script lives at scripts/hooks/pre-commit-journey-coverage.
663
+ const JOURNEY_COVERAGE_HOOK_BEGIN = "# >>> GSD-T journey-coverage gate >>>";
664
+ const JOURNEY_COVERAGE_HOOK_END = "# <<< GSD-T journey-coverage gate <<<";
665
+
666
+ function installJourneyCoverageHook(projectDir) {
667
+ const gitDir = path.join(projectDir, ".git");
668
+ if (!fs.existsSync(gitDir)) { warn("No .git directory — skipping journey-coverage hook install"); return false; }
669
+ const hooksDir = path.join(gitDir, "hooks");
670
+ try { fs.mkdirSync(hooksDir, { recursive: true }); } catch (_) {}
671
+ const hookPath = path.join(hooksDir, "pre-commit");
672
+ const stockSrc = path.join(PKG_ROOT, "scripts", "hooks", "pre-commit-journey-coverage");
673
+ let stock;
674
+ try { stock = fs.readFileSync(stockSrc, "utf8"); } catch (_) { warn("Could not read pre-commit-journey-coverage script"); return false; }
675
+ const block = JOURNEY_COVERAGE_HOOK_BEGIN + "\n" + stock.replace(/^#!.*\n/, "") + "\n" + JOURNEY_COVERAGE_HOOK_END + "\n";
676
+ if (!fs.existsSync(hookPath)) {
677
+ fs.writeFileSync(hookPath, "#!/usr/bin/env bash\nset -e\n\n" + block);
678
+ try { fs.chmodSync(hookPath, 0o755); } catch (_) {}
679
+ success(`Journey-coverage gate installed at ${path.relative(projectDir, hookPath)}`);
680
+ return true;
681
+ }
682
+ const existing = fs.readFileSync(hookPath, "utf8");
683
+ if (existing.includes(JOURNEY_COVERAGE_HOOK_BEGIN)) { info("Journey-coverage block already present — no change"); return true; }
684
+ fs.writeFileSync(hookPath, existing.trimEnd() + "\n\n" + block);
685
+ try { fs.chmodSync(hookPath, 0o755); } catch (_) {}
686
+ success(`Journey-coverage block appended to ${path.relative(projectDir, hookPath)}`);
687
+ return true;
688
+ }
689
+
660
690
  // Idempotent — if an existing hook references CONTEXT_METER_HOOK_MARKER the
661
691
  // command string is refreshed/migrated in-place to the canonical form.
662
692
  // Stale entries matching CONTEXT_METER_STALE_PATTERNS are migrated on the spot.
@@ -1140,6 +1170,33 @@ function installUtilityScripts() {
1140
1170
  }
1141
1171
  }
1142
1172
 
1173
+ // ─── Global Bin Tools (~/.claude/bin/) ───────────────────────────────────────
1174
+ // Modules resolved by globally-installed scripts via
1175
+ // `path.join(__dirname, "..", "bin", <tool>)` (e.g. gsd-t-dashboard-server.js
1176
+ // → parallelism-report.cjs). Distinct from PROJECT_BIN_TOOLS, which copy into
1177
+ // each registered project's bin/.
1178
+ const GLOBAL_BIN_TOOLS = ["parallelism-report.cjs"];
1179
+
1180
+ function installGlobalBinTools() {
1181
+ ensureDir(GLOBAL_BIN_DIR);
1182
+ for (const tool of GLOBAL_BIN_TOOLS) {
1183
+ const src = path.join(PKG_ROOT, "bin", tool);
1184
+ const dest = path.join(GLOBAL_BIN_DIR, tool);
1185
+ if (!fs.existsSync(src)) {
1186
+ warn(`Global bin tool source missing: ${tool} — skipping`);
1187
+ continue;
1188
+ }
1189
+ const srcContent = fs.readFileSync(src, "utf8");
1190
+ const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
1191
+ if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
1192
+ copyFile(src, dest, `bin/${tool}`);
1193
+ try { fs.chmodSync(dest, 0o755); } catch {}
1194
+ } else {
1195
+ info(`bin/${tool} unchanged`);
1196
+ }
1197
+ }
1198
+ }
1199
+
1143
1200
  // ─── CGC (CodeGraphContext) ──────────────────────────────────────────────────
1144
1201
 
1145
1202
  function installCgc() {
@@ -1479,6 +1536,9 @@ async function doInstall(opts = {}) {
1479
1536
  heading("Utility Scripts");
1480
1537
  installUtilityScripts();
1481
1538
 
1539
+ heading("Global Bin Tools (~/.claude/bin/)");
1540
+ installGlobalBinTools();
1541
+
1482
1542
  heading("Context Meter (PostToolUse)");
1483
1543
  const cmHook = configureContextMeterHooks(SETTINGS_JSON);
1484
1544
  if (cmHook.installed) {
@@ -1720,6 +1780,9 @@ async function doInit(projectName) {
1720
1780
  }
1721
1781
  }
1722
1782
 
1783
+ // M52 D1: auto-install the journey-coverage gate. Idempotent — no-op if marker present.
1784
+ if (hasUI(projectDir)) installJourneyCoverageHook(projectDir);
1785
+
1723
1786
  showInitTree(projectDir);
1724
1787
  }
1725
1788
 
@@ -2693,6 +2756,20 @@ function checkDoctorInstallation() {
2693
2756
  issues += checkDoctorClaudeMd();
2694
2757
  issues += checkDoctorSettings();
2695
2758
  issues += checkDoctorEncoding(installed);
2759
+ issues += checkDoctorGlobalBin();
2760
+ return issues;
2761
+ }
2762
+
2763
+ function checkDoctorGlobalBin() {
2764
+ let issues = 0;
2765
+ const missing = GLOBAL_BIN_TOOLS.filter((tool) => !fs.existsSync(path.join(GLOBAL_BIN_DIR, tool)));
2766
+ if (missing.length === 0) {
2767
+ success(`All ${GLOBAL_BIN_TOOLS.length} global bin tool${GLOBAL_BIN_TOOLS.length === 1 ? "" : "s"} installed (~/.claude/bin/)`);
2768
+ } else {
2769
+ warn(`Missing global bin tool${missing.length === 1 ? "" : "s"} in ~/.claude/bin/: ${missing.join(", ")}`);
2770
+ info("Fix: re-run `npx @tekyzinc/gsd-t install` (or `update`)");
2771
+ issues++;
2772
+ }
2696
2773
  return issues;
2697
2774
  }
2698
2775
 
@@ -3055,6 +3132,12 @@ async function doDoctor(opts) {
3055
3132
  heading("Installing pre-commit hooks");
3056
3133
  installPlaywrightGateHook(process.cwd());
3057
3134
  }
3135
+ // M52 D1: opt-in install of the journey-coverage pre-commit hook.
3136
+ if (opts && opts.installJourneyHook) {
3137
+ log("");
3138
+ heading("Installing journey-coverage pre-commit hook");
3139
+ installJourneyCoverageHook(process.cwd());
3140
+ }
3058
3141
  log("");
3059
3142
  if (issues === 0) {
3060
3143
  log(`${GREEN}${BOLD} All checks passed!${RESET}`);
@@ -4325,15 +4408,22 @@ if (require.main === module) {
4325
4408
  doUninstall();
4326
4409
  break;
4327
4410
  case "doctor": {
4328
- const doctorOpts = { prune: false, installPlaywright: false, installHooks: false };
4411
+ const doctorOpts = { prune: false, installPlaywright: false, installHooks: false, installJourneyHook: false };
4329
4412
  for (let i = 1; i < args.length; i++) {
4330
4413
  if (args[i] === "--prune") doctorOpts.prune = true;
4331
4414
  if (args[i] === "--install-playwright") doctorOpts.installPlaywright = true;
4332
4415
  if (args[i] === "--install-hooks") doctorOpts.installHooks = true;
4416
+ if (args[i] === "--install-journey-hook") doctorOpts.installJourneyHook = true;
4333
4417
  }
4334
4418
  doDoctor(doctorOpts).catch((e) => { error(e.message || String(e)); process.exit(1); });
4335
4419
  break;
4336
4420
  }
4421
+ case "check-coverage": {
4422
+ const cli = path.join(__dirname, "journey-coverage-cli.cjs");
4423
+ const { spawnSync } = require("child_process");
4424
+ const res = spawnSync(process.execPath, [cli, ...args.slice(1)], { stdio: "inherit" });
4425
+ process.exit(res.status == null ? 1 : res.status);
4426
+ }
4337
4427
  case "setup-playwright": {
4338
4428
  // Single-project explicit installer. Thin wrapper around installPlaywright().
4339
4429
  const targetDir = args[1] && !args[1].startsWith("-") ? path.resolve(args[1]) : process.cwd();
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execSync } = require('child_process');
7
+
8
+ const jc = require('./journey-coverage.cjs');
9
+
10
+ const VIEWER_FILES = [
11
+ 'scripts/gsd-t-transcript.html',
12
+ 'scripts/gsd-t-dashboard-server.js',
13
+ ];
14
+
15
+ function discoverViewerFiles(projectDir) {
16
+ const out = [];
17
+ for (const rel of VIEWER_FILES) {
18
+ if (fs.existsSync(path.join(projectDir, rel))) out.push(rel);
19
+ }
20
+ const binDir = path.join(projectDir, 'bin');
21
+ if (fs.existsSync(binDir)) {
22
+ for (const f of fs.readdirSync(binDir)) {
23
+ if (/^gsd-t-dashboard.*\.cjs$/.test(f)) out.push(path.join('bin', f));
24
+ }
25
+ }
26
+ const journeyDir = path.join(projectDir, 'e2e', 'journeys');
27
+ if (fs.existsSync(journeyDir)) {
28
+ for (const f of fs.readdirSync(journeyDir)) {
29
+ if (/\.spec\.ts$/.test(f)) out.push(path.join('e2e', 'journeys', f));
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function discoverStagedViewerFiles(projectDir) {
36
+ let raw;
37
+ try {
38
+ raw = execSync('git diff --cached --name-only', { cwd: projectDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
39
+ } catch {
40
+ return [];
41
+ }
42
+ const all = raw.split('\n').filter(Boolean);
43
+ return all.filter((rel) => jc.isViewerSource(rel));
44
+ }
45
+
46
+ function parseArgs(argv) {
47
+ const out = { stagedOnly: false, manifest: null, quiet: false, projectDir: process.cwd() };
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const a = argv[i];
50
+ if (a === '--staged-only') out.stagedOnly = true;
51
+ else if (a === '--manifest') { out.manifest = argv[++i]; }
52
+ else if (a === '--quiet') out.quiet = true;
53
+ else if (a === '--project-dir') { out.projectDir = argv[++i]; }
54
+ else if (a === '-h' || a === '--help') {
55
+ process.stdout.write([
56
+ 'Usage: gsd-t check-coverage [--staged-only] [--manifest PATH] [--quiet]',
57
+ '',
58
+ 'Exit codes:',
59
+ ' 0 All detected listeners covered. Manifest fresh.',
60
+ ' 4 Coverage gap or stale manifest entry.',
61
+ ' 2 Manifest missing or unreadable (fail-closed).',
62
+ '',
63
+ ].join('\n'));
64
+ process.exit(0);
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ function main(argv) {
71
+ const opts = parseArgs(argv);
72
+ const projectDir = opts.projectDir;
73
+
74
+ // --staged-only is a gate: only RUN when at least one viewer-source file is
75
+ // staged. The actual scan always covers the full viewer source — otherwise
76
+ // a commit that touches only spec files would falsely flag every manifest
77
+ // entry as stale (because the staged-only file list contains no listeners).
78
+ if (opts.stagedOnly) {
79
+ const staged = discoverStagedViewerFiles(projectDir);
80
+ if (staged.length === 0) {
81
+ if (!opts.quiet) process.stdout.write('OK: 0 listeners, 0 specs (no staged viewer files)\n');
82
+ process.exit(0);
83
+ }
84
+ }
85
+ const files = discoverViewerFiles(projectDir);
86
+
87
+ const listeners = jc.detectListeners(files, { projectDir });
88
+
89
+ let manifest;
90
+ try {
91
+ if (opts.manifest) {
92
+ const p = path.isAbsolute(opts.manifest) ? opts.manifest : path.join(projectDir, opts.manifest);
93
+ const raw = fs.readFileSync(p, 'utf8');
94
+ manifest = JSON.parse(raw);
95
+ if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.specs)) {
96
+ const err = new Error('manifest-shape-invalid');
97
+ err.code = 'MANIFEST_INVALID';
98
+ throw err;
99
+ }
100
+ } else {
101
+ manifest = jc.loadManifest(projectDir);
102
+ }
103
+ } catch (e) {
104
+ if (e && (e.code === 'MANIFEST_MISSING' || e.code === 'MANIFEST_UNREADABLE' || e.code === 'MANIFEST_INVALID')) {
105
+ if (listeners.length === 0) {
106
+ if (!opts.quiet) process.stdout.write('OK: 0 listeners, 0 specs (no manifest, no listeners — vacuous pass)\n');
107
+ process.exit(0);
108
+ }
109
+ process.stderr.write('check-coverage: manifest missing or unreadable: ' + (e.path || '.gsd-t/journey-manifest.json') + '\n');
110
+ process.stderr.write('hint: run a D2-authored manifest or pass --manifest PATH\n');
111
+ process.exit(2);
112
+ }
113
+ throw e;
114
+ }
115
+
116
+ const gaps = jc.findGaps(listeners, manifest);
117
+ const specCount = (manifest.specs || []).length;
118
+ if (gaps.length === 0) {
119
+ if (!opts.quiet) process.stdout.write('OK: ' + listeners.length + ' listeners, ' + specCount + ' specs\n');
120
+ process.exit(0);
121
+ }
122
+ process.stderr.write(jc.formatReport(gaps) + '\n');
123
+ process.exit(4);
124
+ }
125
+
126
+ if (require.main === module) {
127
+ main(process.argv.slice(2));
128
+ }
129
+
130
+ module.exports = { main, discoverViewerFiles, discoverStagedViewerFiles, parseArgs };