@tekyzinc/gsd-t 3.18.13 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,25 +103,19 @@ If rolled-back domains exist, report them to the user (or if Level 3: log to `.g
103
103
  node scripts/gsd-t-watch-state.js advance --agent-id "$GSD_T_AGENT_ID" --parent-id "${GSD_T_PARENT_AGENT_ID:-null}" --command gsd-t-integrate --step 3 --step-label "Wire Integration Points" 2>/dev/null || true
104
104
  ```
105
105
 
106
- ### Optional — Parallel Dispatch (M44)
106
+ ### Parallel Dispatch (MANDATORY — single instrument)
107
107
 
108
- When integration work spans **more than one domain simultaneously** (i.e., the integration-points.md list contains independent integration tasks across multiple domains), the ready batch may be dispatched via `gsd-t parallel` instead of the sequential task-level dispatch below. When integration touches only one domain (single-domain wiring), the conditional is a no-op and the existing sequential path runs unchanged.
109
-
110
- - **Conditional check** — only triggers when integrating > 1 domain AND the integration tasks pass D4 depgraph + D5 file-disjointness + D6 economics gates. Otherwise the block is a no-op.
111
- - **Mode auto-detection** — mode is auto-detected from `GSD_T_UNATTENDED=1` by `bin/gsd-t-parallel.cjs`. Do not hardcode `--mode` in this command file.
112
- - **Fallback** — any gate veto (unmet deps, overlapping write targets, unprovable disjointness) removes the affected tasks from the parallel batch; they fall back to the sequential task-dispatcher silently. No user prompt.
113
- - **Observability** — D2 owns the spawn observability. The parallel path writes the same `.gsd-t/events/YYYY-MM-DD.jsonl` records (`gate_veto`, `parallelism_reduced`, `task_split`) and `.gsd-t/token-log.md` rows as the sequential path via `captureSpawn`. D3 adds no new spawn machinery.
114
- - **Zero-compaction invariant (unattended)** — for `[unattended]` runs, D2 enforces zero-compaction by splitting integration tasks when D6 estimates > 60% per-worker CW.
115
- - **In-session invariant** — NEVER interrupts the user with a pause/resume prompt. If headroom is tight, D2 reduces the worker count (floor N=1) and emits `parallelism_reduced`. If all gates fail, falls back to sequential silently. No opt-out flag exists (consistent with M43 D4: `--in-session` / `--headless` were never shipped).
116
-
117
- Example (mode auto-detected from env):
108
+ Delegate to `gsd-t parallel --command` do NOT re-implement probe-and-branch logic here (M44 D9 Step 3: "create 1 instrument that accomplishes this instead of implementing it in all the commands").
118
109
 
119
110
  ```bash
120
- gsd-t parallel --milestone {milestone} --dry-run # preview plan, no spawn
121
- gsd-t parallel --milestone {milestone} # live dispatch
111
+ node bin/gsd-t.js parallel --milestone {milestone} --command gsd-t-integrate && exit 0 || true
112
+ # Exit 0 → multi-domain integration; N detached children handle disjoint subsets.
113
+ # Exit 2+ → single-domain wiring. Fall through to sequential dispatch below.
122
114
  ```
123
115
 
124
- Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0.
116
+ `runDispatch` owns the D4/D5/D6 gates + disjoint task-id partitioning + `autoSpawnHeadless()` fan-out. Mode auto-detects from `GSD_T_UNATTENDED=1`. No user prompt. Parallel-when-safe + headless-when-possible are both the default.
117
+
118
+ Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0; `.gsd-t/contracts/headless-default-contract.md` v2.0.0.
125
119
 
126
120
  **Stack Rules Detection (before spawning subagent):**
127
121
  Run via Bash to detect project stack and collect matching rules:
@@ -184,24 +184,20 @@ Proceed.
184
184
  node scripts/gsd-t-watch-state.js advance --agent-id "$GSD_T_AGENT_ID" --parent-id "${GSD_T_PARENT_AGENT_ID:-null}" --command gsd-t-quick --step 3 --step-label "Execute" 2>/dev/null || true
185
185
  ```
186
186
 
187
- ### Optional — Parallel Dispatch (M44, lightweight · conditional-only)
187
+ ### Parallel Dispatch (MANDATORY single instrument)
188
188
 
189
- **This block is a no-op for the typical quick invocation.** `gsd-t-quick` is designed for single-focus work; forcing parallel on every quick invocation would add gate overhead without benefit.
189
+ Delegate to `gsd-t parallel --command` do NOT re-implement probe-and-branch logic here (M44 D9 Step 3: "create 1 instrument that accomplishes this instead of implementing it in all the commands").
190
190
 
191
- **Trigger conditions (BOTH must hold)**:
192
- 1. `.gsd-t/domains/` contains more than one pending task, AND
193
- 2. All three gates (D4 depgraph + D5 file-disjointness + D6 economics) pass for the candidate batch.
194
-
195
- **If either condition fails** — and that includes the common case of a single-task quick invocation — skip this block entirely. The sequential single-subagent path in Step 0.1 remains unchanged.
196
-
197
- **If BOTH conditions hold** — dispatch the ready batch via `gsd-t parallel` (mode auto-detected from `GSD_T_UNATTENDED=1`; do not hardcode `--mode`):
191
+ ```bash
192
+ node bin/gsd-t.js parallel --command gsd-t-quick && exit 0 || true
193
+ # Exit 0 → fan-out happened; detached children handle the work.
194
+ # Exit 2+ → sequential (N<2 or quick is single-task — the common case).
195
+ # Fall through to the single-subagent path in Step 0.1.
196
+ ```
198
197
 
199
- - Fallback is silent any gate veto drops the affected tasks back to the sequential quick path.
200
- - D2 owns the spawn observability; the parallel path writes the same `.gsd-t/events/YYYY-MM-DD.jsonl` records and `.gsd-t/token-log.md` rows via `captureSpawn`. D3 adds no new spawn machinery.
201
- - `[unattended]` — D2 enforces the zero-compaction contract by splitting tasks when D6 estimates > 60% per-worker CW.
202
- - `[in-session]` — NEVER interrupts the user with a pause/resume prompt. If headroom is tight, D2 reduces the worker count (floor N=1). No opt-out flag exists (consistent with M43 D4: `--in-session` / `--headless` were never shipped).
198
+ `runDispatch` inside `bin/gsd-t-parallel.cjs` owns the probe + D4/D5/D6 gate math + disjoint task-id partitioning + `autoSpawnHeadless()` fan-out. Mode auto-detects from `GSD_T_UNATTENDED=1`. No user prompt. Parallel-when-safe + headless-when-possible are both the default (per headless-default-contract v2.0.0).
203
199
 
204
- Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0.
200
+ Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0; `.gsd-t/contracts/headless-default-contract.md` v2.0.0.
205
201
 
206
202
  ### Deviation Rules
207
203
 
@@ -12,6 +12,38 @@ node scripts/gsd-t-watch-state.js advance --agent-id "$GSD_T_AGENT_ID" --parent-
12
12
 
13
13
  **Worker bypass**: If the environment variable `GSD_T_UNATTENDED_WORKER=1` is set, this resume is being invoked by the unattended supervisor as a worker iteration. **SKIP this entire Step 0** — do NOT check for supervisor.pid, do NOT auto-reattach, do NOT schedule a watch tick. Fall through directly to Step 0.1. The worker's job is to do actual work, not watch itself.
14
14
 
15
+ ### Worker Sub-Dispatch (M46-D2)
16
+
17
+ When the worker bypass above applies (`GSD_T_UNATTENDED_WORKER=1` is set), the worker performs an **additional deterministic check** BEFORE falling through to Step 0.1. This lets a worker with multiple file-disjoint tasks fan out its own work as concurrent headless sub-workers, rather than running its task set serially.
18
+
19
+ Decision rule (all conditions must hold):
20
+ - `GSD_T_WORKER_TASK_IDS` is present in the environment (the supervisor declared the task set for this worker).
21
+ - The worker has **more than one task** to run (`tasks.length > 1`).
22
+ - The supervisor has exported the worker's task list to a JSON file on disk (the tasks file path is provided via env or the worker's continue-here block).
23
+ - The tasks are pairwise **file-disjoint** per `.gsd-t/contracts/file-disjointness-rules.md` (the dispatcher re-verifies this internally; the worker does not need to pre-check).
24
+
25
+ If all conditions hold, the worker invokes:
26
+
27
+ ```bash
28
+ node bin/gsd-t-worker-dispatch.cjs \
29
+ --parent-session "$GSD_T_PARENT_AGENT_ID" \
30
+ --tasks <path-to-tasks-json>
31
+ ```
32
+
33
+ Interpret the exit code as follows:
34
+ - **Exit 0** → every sub-worker succeeded. Aggregate the stdout JSON result (which contains `{parallel, taskResults, wallClockMs, reason}`) and report completion of the worker iteration with all taskIds marked done. Do NOT fall through to the serial pre-M46 path.
35
+ - **Exit 1** → at least one sub-worker failed. Report mixed status for the worker iteration, listing the failed `taskId`s from the aggregated `taskResults` (any entry with a non-zero, non-null `exitCode`). The sibling sub-workers' successful results still count — do NOT re-run them. Do NOT fall through to the serial path.
36
+ - **Exit 2** → precondition failure (missing `--parent-session`, unreadable or malformed tasks JSON). Log the stderr reason and **fall through to the current (pre-M46) worker behavior** — the serial path that existed before D2.
37
+
38
+ Skip sub-dispatch and fall through to the current behavior when:
39
+ - `tasks.length <= 1` (single-task or empty workloads have nothing to parallelize — the dispatcher itself returns early with `reason: 'single-task'` / `'no-tasks'`, but the worker can short-circuit without spawning the adapter at all).
40
+ - Tasks are not file-disjoint per the disjointness contract (the dispatcher will return `parallel: false, reason: 'file-overlap'`; the worker treats this identically to the serial fallback).
41
+ - The tasks file was not written by the supervisor (no sub-dispatch surface available this iteration).
42
+
43
+ **Rationale** — Per `.gsd-t/contracts/headless-default-contract.md` §Worker Sub-Dispatch (v2.1.0), this hand-off is the third parallelism layer (iter-parallel → supervisor fan-out → worker sub-dispatch). It is a deterministic, file-disjointness-gated consumer of the M44-verified `runDispatch` instrument — not a modification of it. The in-session dispatch path is byte-identical; this sub-section only adds a new invocation site on the unattended worker leg.
44
+
45
+ ---
46
+
15
47
  Check whether an unattended supervisor is actively running for this project:
16
48
 
17
49
  1. Check if `.gsd-t/.unattended/supervisor.pid` exists.
@@ -19,6 +19,16 @@ Wait for the subagent to complete. Relay its output to the user. **Do not read f
19
19
  **If you are the spawned subagent** (your prompt says "running gsd-t-status"):
20
20
  Continue below.
21
21
 
22
+ ## Step 0.0: Date + Version Banner (MANDATORY)
23
+
24
+ Before anything else, print the current date and GSD-T version so a multi-day-old session is immediately dated when the user reads the output:
25
+
26
+ ```bash
27
+ node -e "const{dateStamp}=require('./scripts/gsd-t-update-check.js');const fs=require('fs'),os=require('os'),path=require('path');const v=(()=>{try{return fs.readFileSync(path.join(os.homedir(),'.claude/.gsd-t-version'),'utf8').trim()}catch{return 'unknown'}})();process.stdout.write(dateStamp()+'GSD-T v'+v+' — CURRENT\n')" 2>/dev/null || true
28
+ ```
29
+
30
+ Format: `Tue: Mar 26, 2026, GSD-T v3.19.00 — CURRENT`. Currency claim is best-effort — the canonical authority is the `~/.claude/.gsd-t-update-check` cache consulted by the SessionStart hook; status mode trusts the installed version label.
31
+
22
32
  ## Step 0: Headless Read-Back Banner (MANDATORY)
23
33
 
24
34
  Before reading any files, surface any completed headless sessions the user hasn't seen yet. Run this once at the start of every status invocation:
@@ -207,7 +207,64 @@ out('DOMAIN_SUMMARY', domainSummary.join(' | '));
207
207
 
208
208
  If `PID_FILE_EXISTS=false`:
209
209
 
210
- The supervisor has finalized cleanly and removed its own PID file. Read the final `state.json` status and iter, then print the terminal report:
210
+ The supervisor has finalized cleanly and removed its own PID file.
211
+
212
+ ### Step 3a: Reconciliation probe
213
+
214
+ Before emitting a report, check whether the supervisor's final `status` agrees with the on-disk milestone evidence. A `status=failed` with a **fresh matching milestone archive** is the M45-class false-failed-marker pathology: the worker completed the milestone (including the archive rename under `.gsd-t/milestones/`) but a polarity-unsound exit-code matcher mapped the worker's clean exit to `1`, which the supervisor then finalized as `status=failed`.
215
+
216
+ ```bash
217
+ node -e "
218
+ const fs = require('fs');
219
+ const path = require('path');
220
+ const state = JSON.parse(fs.readFileSync('.gsd-t/.unattended/state.json', 'utf8'));
221
+ const milestone = (state.milestone || '').toLowerCase();
222
+ let reconciled = false;
223
+ if (state.status === 'failed' && milestone) {
224
+ // Look for a fresh archive directory matching the milestone slug.
225
+ // Pattern: .gsd-t/milestones/{slug}-YYYY-MM-DD where slug contains the
226
+ // milestone identifier (e.g. 'M45' → 'm45-conversation-stream-observability-2026-04-23').
227
+ try {
228
+ const archives = fs.readdirSync('.gsd-t/milestones')
229
+ .filter(n => n.toLowerCase().includes(milestone));
230
+ if (archives.length > 0) {
231
+ // Check the most recent archive's mtime is within the supervisor's run window.
232
+ const recent = archives
233
+ .map(n => ({ n, mtime: fs.statSync(path.join('.gsd-t/milestones', n)).mtimeMs }))
234
+ .sort((a, b) => b.mtime - a.mtime)[0];
235
+ const runStart = Date.parse(state.startedAt || 0);
236
+ if (recent && recent.mtime >= runStart) {
237
+ reconciled = true;
238
+ console.log('RECONCILED=true');
239
+ console.log('RECONCILED_ARCHIVE=' + recent.n);
240
+ }
241
+ }
242
+ } catch (_) {}
243
+ }
244
+ if (!reconciled) console.log('RECONCILED=false');
245
+ "
246
+ ```
247
+
248
+ ### Step 3b: Render reconciled or raw report
249
+
250
+ If `RECONCILED=true` AND `STATUS=failed`, the archive is the authority — render the reconciled success report:
251
+
252
+ ```
253
+ ✅ Unattended supervisor finalized — milestone completed (auto-reconciled).
254
+
255
+ Session: {SESSION}
256
+ Milestone: {MILESTONE} — archived as {RECONCILED_ARCHIVE}
257
+ Supervisor flag: status=failed (not trusted — archive present)
258
+ Iterations: {ITER} / {MAX_ITER}
259
+ Total elapsed: {Hh Mm — formatted from ELAPSED_MS}
260
+ Log: {LOG_PATH}
261
+ State: .gsd-t/.unattended/state.json
262
+
263
+ Note: the worker completed the milestone, but a polarity-unsound exit-code
264
+ matcher mapped clean exit → failed. Treat the archive as the source of truth.
265
+ ```
266
+
267
+ Otherwise (no reconciliation — `STATUS` is genuinely terminal `done`, `failed` without archive, or `stopped`), render the raw final report:
211
268
 
212
269
  ```
213
270
  ✅ Unattended supervisor finalized cleanly.
@@ -55,49 +55,52 @@ If `.gsd-t/graph/index.json` exists, the dashboard can render entity-relationshi
55
55
 
56
56
  If `$ARGUMENTS` contains "stop", skip to **Step 5**. Otherwise continue to Step 3.
57
57
 
58
- ## Step 3: Check if Server is Already Running
58
+ ## Step 3: Resolve Port + Check if Server is Already Running
59
+
60
+ The dashboard server binds to a **per-project hashed port** in `[7433, 7532]` (see `projectScopedDefaultPort` in `gsd-t-dashboard-server.js`). Resolve it once and reuse:
59
61
 
60
- Run via Bash:
61
62
  ```bash
63
+ PORT=$(node -e "console.log(require('$HOME/.claude/scripts/gsd-t-dashboard-server.js').resolvePort({ projectDir: process.cwd() }))")
62
64
  if [ -f .gsd-t/dashboard.pid ]; then
63
65
  PID=$(cat .gsd-t/dashboard.pid)
64
- curl -sf http://localhost:7433/ping 2>/dev/null | grep -q '"ok"' && echo "SERVER_RUNNING=true" || echo "SERVER_RUNNING=false"
66
+ curl -sf http://localhost:$PORT/ping 2>/dev/null | grep -q '"ok"' && echo "SERVER_RUNNING=true" || echo "SERVER_RUNNING=false"
65
67
  else
66
68
  echo "SERVER_RUNNING=false"
67
69
  fi
68
70
  ```
69
71
 
70
- If output is `SERVER_RUNNING=true`, skip Step 3a and go directly to Step 4.
72
+ If output is `SERVER_RUNNING=true`, skip Step 3a and go directly to Step 4. Carry `$PORT` forward to subsequent steps.
71
73
 
72
74
  ### Step 3a: Start Server if Not Running
73
75
 
74
- Run via Bash:
76
+ Run via Bash (reuses `$PORT` from Step 3):
75
77
  ```bash
76
78
  node ~/.claude/scripts/gsd-t-dashboard-server.js --detach || true
77
79
  for i in 1 2 3 4 5; do
78
- curl -sf http://localhost:7433/ping 2>/dev/null | grep -q '"ok"' && break
80
+ curl -sf http://localhost:$PORT/ping 2>/dev/null | grep -q '"ok"' && break
79
81
  sleep 1
80
82
  done
81
83
  ```
82
84
 
83
85
  ## Step 4: Open Browser
84
86
 
85
- Run via Bash:
87
+ Run via Bash (reuses `$PORT` from Step 3):
86
88
  ```bash
87
- node -e "const {execFileSync}=require('child_process'); const url='http://localhost:7433'; try { if(process.platform==='win32'){execFileSync('cmd',['/c','start','',url],{stdio:'ignore'})}else{execFileSync(process.platform==='darwin'?'open':'xdg-open',[url],{stdio:'ignore'})} } catch(e) { console.error('Could not open browser:', e.message); }" || true
89
+ URL="http://localhost:$PORT"
90
+ node -e "const {execFileSync}=require('child_process'); const url=process.argv[1]; try { if(process.platform==='win32'){execFileSync('cmd',['/c','start','',url],{stdio:'ignore'})}else{execFileSync(process.platform==='darwin'?'open':'xdg-open',[url],{stdio:'ignore'})} } catch(e) { console.error('Could not open browser:', e.message); }" "$URL" || true
88
91
  ```
89
92
 
90
- Report to the user: "Dashboard is running at http://localhost:7433 — browser opened."
93
+ Report to the user: "Dashboard is running at $URL — browser opened."
91
94
 
92
95
  ## Step 5: Stop Handler
93
96
 
94
- Run only when `$ARGUMENTS` contains "stop".
97
+ Run only when `$ARGUMENTS` contains "stop". Resolves the same hashed port used at start:
95
98
 
96
- Run via Bash:
97
99
  ```bash
100
+ PORT=$(node -e "console.log(require('$HOME/.claude/scripts/gsd-t-dashboard-server.js').resolvePort({ projectDir: process.cwd() }))")
98
101
  if [ -f .gsd-t/dashboard.pid ]; then
99
102
  PID=$(cat .gsd-t/dashboard.pid)
100
- curl -sf http://localhost:7433/stop 2>/dev/null || kill $PID 2>/dev/null || true
103
+ curl -sf http://localhost:$PORT/stop 2>/dev/null || kill $PID 2>/dev/null || true
101
104
  rm -f .gsd-t/dashboard.pid
102
105
  echo "Dashboard server stopped"
103
106
  else
@@ -211,18 +211,9 @@ Spawn agent → `commands/gsd-t-impact.md`
211
211
  Spawn agent → `commands/gsd-t-execute.md`
212
212
  - This is the heaviest phase. The execute agent uses **task-level dispatch** (fresh-dispatch-contract.md): one Task subagent per task within each domain, each receiving only scope.md + relevant contracts + single task + graph context + up to 5 prior summaries. The execute agent handles domain task-dispatching and QA internally.
213
213
 
214
- ##### Optional — Parallel Dispatch (M44)
214
+ ##### Parallel Dispatch (M44 D9, single instrument)
215
215
 
216
- The spawned execute agent will itself decide whether to dispatch parallel workers via `gsd-t parallel` (see `commands/gsd-t-execute.md` Step 3 → "Optional — Parallel Dispatch (M44)"). The wave orchestrator does not need to configure this — it is automatic:
217
-
218
- - If `.gsd-t/domains/` contains more than one pending task passing D4/D5/D6 gates, the execute agent dispatches via `gsd-t parallel` instead of sequentially.
219
- - Mode is auto-detected from `GSD_T_UNATTENDED=1` — the wave orchestrator inherits this env from its own spawn. Do not hardcode `--mode`.
220
- - Fallback is silent: any gate veto, unprovable disjointness, or single-task scope drops back to the sequential execute path without a user prompt.
221
- - D2 owns the spawn observability; the parallel path writes the same `.gsd-t/events/YYYY-MM-DD.jsonl` records and `.gsd-t/token-log.md` rows as the sequential path via `captureSpawn`. D3 adds no new spawn machinery.
222
- - `[unattended]` — D2 enforces the zero-compaction contract by splitting tasks when D6 estimates per-worker CW > 60%.
223
- - `[in-session]` — NEVER interrupts the user with pause/resume. If headroom is tight, D2 reduces the worker count (floor N=1) and emits `parallelism_reduced`.
224
-
225
- No wave-orchestrator-level flag exists to opt out; the parallel-vs-sequential decision is owned by the execute agent and the D2 gating math. Contract: `.gsd-t/contracts/wave-join-contract.md` v1.1.0.
216
+ The spawned execute agent delegates the parallel-vs-sequential decision to the single instrument `gsd-t parallel --command gsd-t-execute` (see `commands/gsd-t-execute.md` → "Parallel Dispatch"). The wave orchestrator does not need to configure this — it is automatic, deterministic, and inherited via `GSD_T_UNATTENDED` env. Contract: `.gsd-t/contracts/wave-join-contract.md` v1.2.0 §Single Instrument.
226
217
 
227
218
  - **Adaptive replanning**: After each domain completes, the execute agent runs a replan check (per `adaptive-replan-contract.md`). If a completed domain's task summaries reveal new constraints (e.g., deprecated API, wrong column name, incompatible library), the execute agent checks remaining domains' `tasks.md` files for invalidated assumptions and revises them on disk before dispatching the next domain. Maximum 2 replan cycles per execute run — if exceeded, execution pauses for user input. All replan decisions are logged to the Decision Log in `progress.md`. The wave phase summary includes any replan actions taken.
228
219
  - **Team/parallel mode**: If the plan defines parallel domains (same wave), the execute agent dispatches each domain teammate with `isolation: "worktree"` (per worktree-isolation-contract.md). Each domain works in an isolated git worktree. After all domains complete, the execute agent runs the Sequential Merge Protocol: merge domain A → test → merge domain B → test. Per-domain rollback if tests fail. Worktrees are cleaned up after all merges complete.
@@ -978,6 +978,72 @@ means in-flight.
978
978
  `test/m44-d8-dashboard-spawn-plans-endpoint.test.js`,
979
979
  `test/m44-d8-transcript-renderer-panel.test.js` — 36 tests total.
980
980
 
981
+ ## Parallelism Panel (M44 D9, v3.19.0+)
982
+
983
+ Pure-observer readout answering two questions:
984
+
985
+ > *Is the orchestrator actually fanning out, or serializing despite parallelism being available?*
986
+
987
+ > *When this wave finishes, did it hit the parallelism factor D6 estimated?*
988
+
989
+ Zero added LLM token cost — every number is derived from files the other
990
+ M44 domains already write: D8 spawn-plan files, D4/D5/D6 event rows, and
991
+ D7 `cw_id` columns in `.gsd-t/token-log.md`.
992
+
993
+ **Module**: `bin/parallelism-report.cjs` —
994
+ `computeParallelismMetrics({projectDir, wave?, now?})` returns the shape
995
+ defined in `.gsd-t/contracts/parallelism-report-contract.md` v1.0.0.
996
+ `buildFullReport(...)` returns a post-mortem markdown string with Summary,
997
+ Per-spawn timeline, Per-gate decisions, Per-worker Gantt, Token cost, and
998
+ Notes sections.
999
+
1000
+ **Data flow**:
1001
+
1002
+ ```
1003
+ .gsd-t/spawns/*.json ─┐ (D8 writer)
1004
+ .gsd-t/events/*.jsonl ─┼─▶ bin/parallelism-report.cjs
1005
+ .gsd-t/domains/*/tasks.md ┤ │
1006
+ .gsd-t/token-log.md ─┘ (pure I/O — never writes, never spawns)
1007
+
1008
+
1009
+ scripts/gsd-t-dashboard-server.js
1010
+ (5s in-memory cache per wave param)
1011
+ ├── GET /api/parallelism → JSON metrics
1012
+ ├── GET /api/parallelism/report → markdown
1013
+ └── POST /api/unattended-stop → writes sentinel
1014
+
1015
+
1016
+ scripts/gsd-t-transcript.html
1017
+ `<aside class="spawn-panel"> .parallelism-panel`
1018
+ polls /api/parallelism every 5s
1019
+ ```
1020
+
1021
+ **Color thresholds** (worst-of across five per-signal colors, from contract §color_state):
1022
+
1023
+ | Signal | Green | Yellow | Red |
1024
+ |--------|-------|--------|-----|
1025
+ | activeWorkers / readyTasks | ≥80% | 50–80% | <50% AND >10 min since last spawn |
1026
+ | Gate veto rate (D4) | <10% | 10–30% | >30% |
1027
+ | parallelism_factor vs. D6 estimate | ≥80% | 50–80% | <50% |
1028
+ | Spawn age (any active worker) | <30 min | 30–45 min | >45 min |
1029
+ | Time since last `spawn_started` (when ready>0) | <5 min | 5–10 min | >10 min |
1030
+
1031
+ Special: `color_state === "dimmed"` when no spawn-plan files exist (idle
1032
+ project) — never red.
1033
+
1034
+ **Silent-fail invariant**: malformed spawn-plan JSON, corrupt JSONL event
1035
+ lines, missing `.gsd-t/domains/`, missing `.gsd-t/token-log.md` — every
1036
+ case logs a note to `metrics.notes` / Full Report `## Notes` and continues
1037
+ with partial data. Observer must never throw when watching a live system.
1038
+
1039
+ **Contract**: `.gsd-t/contracts/parallelism-report-contract.md` v1.0.0.
1040
+
1041
+ **Tests**: `test/m44-d9-parallelism.test.js` — 16 tests covering metric
1042
+ shape, parallelism_factor math (live 1-worker, 4-worker, mixed-duration,
1043
+ post-wave), color-state thresholds, silent-fail on malformed inputs, Full
1044
+ Report markdown sections, and `/api/parallelism*` endpoint shape + 5s
1045
+ cache behaviour.
1046
+
981
1047
  ## Planned Architecture Changes (M23-M24)
982
1048
 
983
1049
  **M23: Headless Mode**
@@ -993,3 +1059,19 @@ means in-flight.
993
1059
  2. **Four-file synchronization**: Any command change requires updating README, GSD-T-README, CLAUDE-global template, and gsd-t-help. Manual process — no automated validation.
994
1060
  3. **Pre-Commit Gate unenforced**: Mental checklist in CLAUDE.md, not a git hook or CI check.
995
1061
  4. **Progress.md Decision Log growth**: Unbounded append-only log. May need periodic archival strategy for long-lived projects.
1062
+
1063
+ ## M46 Worker Sub-Dispatch
1064
+
1065
+ `bin/gsd-t-worker-dispatch.cjs` lets an unattended supervisor worker iteration fan out its own file-disjoint tasks as concurrent sub-workers. The module's `dispatchWorkerTasks({projectDir, parentSessionId, tasks, maxParallel})` entry point gates on three conditions: `process.env.GSD_T_UNATTENDED_WORKER === '1'` (caller runs inside a supervisor-launched worker child), `tasks.length > 1`, and pairwise file-disjointness across the task set's `files` arrays. When all three hold, it emits a spawn-plan frame with `kind: 'unattended-worker-sub'` and delegates execution to `bin/gsd-t-parallel.cjs::runDispatch` — the same M44-verified instrument the in-session planner uses. When any condition fails it returns `{parallel: false, reason}` and the worker falls through to its existing serial path.
1066
+
1067
+ Architecturally the module is a **new consumer** of `runDispatch`, not a modifier. The in-session dispatch call site is byte-identical post-D2; the worker adapter is a sibling caller. This completes the three-layer unattended parallelism model: supervisor iterations run in parallel (M46 D1 iter-parallel), each iteration's worker may sub-dispatch its disjoint tasks (M46 D2), and within the orchestrator the `/gsd` router still fans out via `runDispatch` for in-session action turns (M44). All three layers share one dispatch instrument and one disjointness predicate.
1068
+
1069
+ Contract: `.gsd-t/contracts/headless-default-contract.md` v2.1.0 §Worker Sub-Dispatch.
1070
+
1071
+ ## M46 Iteration-Parallel Supervisor
1072
+
1073
+ `bin/gsd-t-unattended.cjs` ships four helpers that scaffold iteration-level parallelism in the supervisor main loop: `_runOneIter(state, opts)` is the extract-method of the single-iter body (one `claude -p` dispatch, heartbeat bookkeeping, state mutation) and returns an `IterResult` envelope; `_computeIterBatchSize(state, opts)` is the mode-safety gate that returns `1` when `state.status === "verify-needed"`, when `state.milestoneBoundary === true`, when `state.status === "complete-milestone"`, or when the caller did not pass `opts.maxIterParallel` as a number, and otherwise returns `min(opts.maxIterParallel, remainingIters, 8)` with a hard ceiling of 8; `_runIterParallel(state, opts, iterFn, batchSize)` is the concurrent driver — it builds `batchSize` promises, awaits them with `Promise.allSettled` so a rejected slice does not cancel siblings, and maps rejections into `{status: "error", tasksDone: [], verifyNeeded: false, artifacts: [], error}` envelopes; `_reconcile(state, results)` folds the `IterResult[]` back into canonical state with append-only union on `completedTasks`, last-writer-wins on `status`, OR across `verifyNeeded`, append on `artifacts`, and an overwrite `lastBatch` metadata block.
1074
+
1075
+ The production main loop currently runs exactly one iter per pass (`batchSize === 1`) always, unless a caller explicitly threads `opts.maxIterParallel` as a number through `_computeIterBatchSize` — which today's supervisor CLI does not. The four helpers are exported via `module.exports.__test__` so the T7 unit suite and any future caller can exercise batched iteration deterministically, but iter-parallelism at this layer is **scaffolded, not engaged in production**. The gate is intentional: `_runOneIter` mutates shared `state` fields (`state.iter`, heartbeat bookkeeping, the `writeState` side effect) that are not safe to execute concurrently against the same state object. Backlog #24 tracks the follow-up to make `_runOneIter` state-clone-safe and lift the production gate so the supervisor CLI can set a non-1 default.
1076
+
1077
+ Contract: `.gsd-t/contracts/iter-parallel-contract.md` v1.0.0.
@@ -664,3 +664,23 @@ Milestone 44 D8 delivers a right-side two-layer task panel in the dashboard and
664
664
 
665
665
  Supporting contract:
666
666
  - `.gsd-t/contracts/spawn-plan-contract.md` v1.0.0 — schema + writer/reader/updater protocol + silent-fail rules.
667
+
668
+ ### M46 D2 Worker Sub-Dispatch
669
+
670
+ A supervisor worker iteration assigned more than one ready task MUST fan those tasks out as concurrent sub-workers via the M44-verified `runDispatch` instrument rather than running them serially, provided the task set is pairwise file-disjoint and the iteration is running inside an unattended worker child (`GSD_T_UNATTENDED_WORKER=1`). This closes the parallelism gap where unattended workers historically executed their assigned tasks one-at-a-time even when the disjointness precondition held, defeating the purpose of the M44 dispatch infrastructure for all unattended runs. The sub-dispatch path is a new consumer of `bin/gsd-t-parallel.cjs::runDispatch` — no changes to the in-session planner's call site, no new disjointness predicate, and no new dispatch logic. Distinguished in telemetry via the new spawn-plan `kind: 'unattended-worker-sub'` value.
671
+
672
+ Acceptance:
673
+ - `bin/gsd-t-worker-dispatch.cjs` exists and exports `dispatchWorkerTasks` plus the `_areFileDisjoint` helper and the `SPAWN_PLAN_KIND` constant (`'unattended-worker-sub'`).
674
+ - Unit + integration tests in `test/m46-d2-worker-subdispatch.test.js` pass (trigger-condition matrix, disjointness predicate, runDispatch delegation, spawn-plan frame emission, serial fallback on overlap/single-task).
675
+ - Proof measurement recorded in `.gsd-t/metrics/m46-worker-proof.json` shows a parallel-vs-serial speedup of at least **2.5×** on a representative file-disjoint worker iteration.
676
+ - `.gsd-t/contracts/headless-default-contract.md` v2.1.0 §Worker Sub-Dispatch present as the locked source of truth.
677
+
678
+ ### M46 D1 Iteration Parallelism
679
+
680
+ The unattended supervisor main loop MUST expose iter-level parallelism machinery via four extracted helpers — `_runOneIter`, `_computeIterBatchSize`, `_runIterParallel`, `_reconcile` — so unit tests and future callers can exercise batched iteration deterministically, with `_runIterParallel` using `Promise.allSettled` so a single rejected slice does not cancel siblings and `_reconcile` merging `IterResult[]` into state with append-only `completedTasks`, last-writer-wins `status`, OR across `verifyNeeded`, append on `artifacts`, and overwrite `lastBatch` metadata. The production main loop default remains serial (`batchSize = 1` always, via `_computeIterBatchSize` returning `1` whenever `opts.maxIterParallel` is not a number) pending the state-clone-safety follow-up tracked in backlog #24 — that work must land before the supervisor CLI sets a non-1 default.
681
+
682
+ Acceptance:
683
+ - `_runOneIter`, `_computeIterBatchSize`, `_runIterParallel`, and `_reconcile` are all exported via `module.exports.__test__` on `bin/gsd-t-unattended.cjs`.
684
+ - Tests in `test/m46-d1-iter-parallel.test.js` pass (serial fallback, parallel batch, mode-safety gate, error isolation, state reconciliation).
685
+ - Proof speedup ≥ **3.0×** recorded in `.gsd-t/metrics/m46-iter-proof.json` — a synthetic `batchSize = 4` measurement of the `_runIterParallel` driver, not the production main loop (which remains serial until backlog #24 lands).
686
+ - `.gsd-t/contracts/iter-parallel-contract.md` v1.0.0 present as the locked source of truth.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.18.13",
3
+ "version": "3.19.00",
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",
@@ -131,7 +131,20 @@ function writeRow(payload) {
131
131
  /**
132
132
  * Find the most-recently-modified .ndjson in <cwd>/.gsd-t/transcripts/.
133
133
  * Returns the absolute path, or null if none exists or the directory is absent.
134
+ *
135
+ * Target-selection rules (M45 D2):
136
+ * 1. Default: prefer the most-recently-modified SPAWN NDJSON (any file whose
137
+ * basename does NOT start with `in-session-`).
138
+ * 2. Fallback: if no spawn NDJSON has been modified within the last
139
+ * IN_SESSION_FALLBACK_MS (30s) AND a fresh `in-session-*.ndjson` exists,
140
+ * target that file instead. Log the decision to stderr so regressions
141
+ * are obvious during debugging.
142
+ * 3. Last resort: if only in-session NDJSONs exist, pick the newest one
143
+ * (covers the edge case where a user triggers /compact during a pure
144
+ * conversation session with no spawns in flight).
134
145
  */
146
+ const IN_SESSION_FALLBACK_MS = 30 * 1000; // 30 seconds
147
+
135
148
  function findActiveTranscript(cwd) {
136
149
  const transcriptsDir = path.join(cwd, ".gsd-t", "transcripts");
137
150
  let entries;
@@ -143,21 +156,51 @@ function findActiveTranscript(cwd) {
143
156
  const ndjsons = entries.filter((e) => e.endsWith(".ndjson"));
144
157
  if (!ndjsons.length) return null;
145
158
 
146
- let newest = null;
147
- let newestMtime = -1;
159
+ let newestSpawn = null;
160
+ let newestSpawnMtime = -1;
161
+ let newestInSession = null;
162
+ let newestInSessionMtime = -1;
148
163
  for (const name of ndjsons) {
149
164
  const full = path.join(transcriptsDir, name);
165
+ let mtimeMs;
150
166
  try {
151
- const stat = fs.statSync(full);
152
- if (stat.mtimeMs > newestMtime) {
153
- newestMtime = stat.mtimeMs;
154
- newest = full;
167
+ mtimeMs = fs.statSync(full).mtimeMs;
168
+ } catch {
169
+ continue;
170
+ }
171
+ const isInSession = name.indexOf("in-session-") === 0;
172
+ if (isInSession) {
173
+ if (mtimeMs > newestInSessionMtime) {
174
+ newestInSessionMtime = mtimeMs;
175
+ newestInSession = full;
176
+ }
177
+ } else {
178
+ if (mtimeMs > newestSpawnMtime) {
179
+ newestSpawnMtime = mtimeMs;
180
+ newestSpawn = full;
155
181
  }
182
+ }
183
+ }
184
+
185
+ const now = Date.now();
186
+ const spawnFresh = newestSpawn != null && (now - newestSpawnMtime) < IN_SESSION_FALLBACK_MS;
187
+
188
+ if (spawnFresh) return newestSpawn;
189
+
190
+ if (newestInSession != null) {
191
+ try {
192
+ process.stderr.write(
193
+ "compact-detector: targeting " + path.basename(newestInSession) + " (fallback)\n"
194
+ );
156
195
  } catch {
157
- // skip unreadable entries
196
+ // silent stderr write must never break the hook
158
197
  }
198
+ return newestInSession;
159
199
  }
160
- return newest;
200
+
201
+ // No fresh spawn, no in-session — fall back to the newest spawn (even if stale)
202
+ // so v1.0.0 behavior is preserved for legacy projects with no in-session file.
203
+ return newestSpawn;
161
204
  }
162
205
 
163
206
  /**