@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.
- package/CHANGELOG.md +114 -0
- package/bin/gsd-t-parallel-probe.cjs +132 -0
- package/bin/gsd-t-parallel.cjs +422 -9
- package/bin/gsd-t-task-graph.cjs +80 -19
- package/bin/gsd-t-unattended.cjs +634 -229
- package/bin/gsd-t-worker-dispatch.cjs +211 -0
- package/bin/headless-auto-spawn.cjs +44 -1
- package/bin/headless-exit-codes.cjs +36 -18
- package/bin/m44-proof-measure.cjs +285 -0
- package/bin/m46-iter-proof.cjs +149 -0
- package/bin/m46-worker-proof.cjs +201 -0
- package/bin/parallelism-report.cjs +535 -0
- package/bin/spawn-plan-writer.cjs +1 -1
- package/commands/gsd-t-debug.md +10 -14
- package/commands/gsd-t-execute.md +10 -16
- package/commands/gsd-t-help.md +1 -0
- package/commands/gsd-t-integrate.md +8 -14
- package/commands/gsd-t-quick.md +10 -14
- package/commands/gsd-t-resume.md +32 -0
- package/commands/gsd-t-status.md +10 -0
- package/commands/gsd-t-unattended-watch.md +58 -1
- package/commands/gsd-t-visualize.md +15 -12
- package/commands/gsd-t-wave.md +2 -11
- package/docs/architecture.md +82 -0
- package/docs/requirements.md +20 -0
- package/package.json +1 -1
- package/scripts/gsd-t-compact-detector.js +51 -8
- package/scripts/gsd-t-dashboard-server.js +138 -85
- package/scripts/gsd-t-transcript.html +152 -1
- package/scripts/gsd-t-update-check.js +13 -4
- package/scripts/hooks/gsd-t-conversation-capture.js +258 -0
- package/templates/CLAUDE-global.md +54 -0
|
@@ -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
|
-
###
|
|
106
|
+
### Parallel Dispatch (MANDATORY — single instrument)
|
|
107
107
|
|
|
108
|
-
|
|
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} --
|
|
121
|
-
|
|
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
|
-
|
|
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:
|
package/commands/gsd-t-quick.md
CHANGED
|
@@ -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
|
-
###
|
|
187
|
+
### Parallel Dispatch (MANDATORY — single instrument)
|
|
188
188
|
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
-
|
|
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
|
|
package/commands/gsd-t-resume.md
CHANGED
|
@@ -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.
|
package/commands/gsd-t-status.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
package/commands/gsd-t-wave.md
CHANGED
|
@@ -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
|
-
#####
|
|
214
|
+
##### Parallel Dispatch (M44 D9, single instrument)
|
|
215
215
|
|
|
216
|
-
The spawned execute agent
|
|
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.
|
package/docs/architecture.md
CHANGED
|
@@ -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.
|
package/docs/requirements.md
CHANGED
|
@@ -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.
|
|
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
|
|
147
|
-
let
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
//
|
|
196
|
+
// silent — stderr write must never break the hook
|
|
158
197
|
}
|
|
198
|
+
return newestInSession;
|
|
159
199
|
}
|
|
160
|
-
|
|
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
|
/**
|