@tekyzinc/gsd-t 2.76.10 → 3.10.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.
@@ -0,0 +1,420 @@
1
+ # GSD-T: Unattended — Launch the Background Supervisor
2
+
3
+ **Model**: sonnet (pre-flight reasoning + spawn coordination)
4
+
5
+ You are launching the GSD-T unattended supervisor — a detached background process that drives the active milestone to completion by spawning `claude -p` worker iterations in a relay, without human intervention. The supervisor runs outside the interactive Claude session and survives `/clear`, terminal close, and session expiry.
6
+
7
+ This command performs pre-flight checks, spawns the supervisor via the cross-platform helper, verifies liveness, displays the initial status block, and schedules the first watch tick. It then returns — the watch loop takes over.
8
+
9
+ See `unattended-supervisor-contract.md` §7 (Launch Handshake), §4 (Status Enum), §3 (state.json schema).
10
+
11
+ ## Step 1: Pre-Flight Checks
12
+
13
+ ### 1a: Verify GSD-T Project
14
+
15
+ Run via Bash:
16
+
17
+ ```bash
18
+ if [ ! -f ".gsd-t/progress.md" ]; then
19
+ echo "ERROR: .gsd-t/progress.md not found — not a GSD-T project. Run /user:gsd-t-init first."
20
+ exit 1
21
+ fi
22
+ ```
23
+
24
+ If the file is missing, stop immediately with the error above. Do NOT proceed.
25
+
26
+ ### 1b: Check for an Already-Running Supervisor
27
+
28
+ Run via Bash:
29
+
30
+ ```bash
31
+ node -e "
32
+ const fs = require('fs');
33
+ const PID_FILE = '.gsd-t/.unattended/supervisor.pid';
34
+
35
+ if (!fs.existsSync(PID_FILE)) {
36
+ console.log('PID_RUNNING=false');
37
+ process.exit(0);
38
+ }
39
+
40
+ let pid = null;
41
+ try {
42
+ pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
43
+ } catch (_) {}
44
+
45
+ if (!pid || !Number.isFinite(pid)) {
46
+ console.log('PID_RUNNING=false');
47
+ process.exit(0);
48
+ }
49
+
50
+ let alive = false;
51
+ try { process.kill(pid, 0); alive = true; }
52
+ catch (e) { alive = e.code === 'EPERM'; }
53
+
54
+ console.log('PID_RUNNING=' + alive);
55
+ console.log('PID=' + pid);
56
+ "
57
+ ```
58
+
59
+ If `PID_RUNNING=true`, **STOP** and print:
60
+
61
+ ```
62
+ 🔴 Unattended supervisor is already running (PID {PID}).
63
+
64
+ Use /user:gsd-t-unattended-stop to request a clean halt, then wait for the
65
+ watch loop to confirm it has stopped before relaunching.
66
+
67
+ To just watch the current run: /user:gsd-t-unattended-watch
68
+ ```
69
+
70
+ Do NOT spawn a second supervisor. End the turn.
71
+
72
+ ### 1c: Parse Arguments and Resolve Milestone
73
+
74
+ Parse `$ARGUMENTS` for:
75
+ - `--hours=N` — wall-clock cap in hours (default: `24`)
76
+ - `--milestone=LABEL` — milestone to drive (default: read from `.gsd-t/progress.md`)
77
+ - `--max-iterations=N` — iteration cap (default: `200`)
78
+ - `--dry-run` — preflight only; print what would be spawned, do NOT spawn
79
+
80
+ **Persistent overrides**: create `.gsd-t/.unattended/config.json` to change
81
+ defaults per-project (caps, protected branches, dirty-tree whitelist). CLI
82
+ flags always win over the config file. See `docs/unattended-config.md` for
83
+ the full schema and common recipes (e.g. `{"protectedBranches": []}` for
84
+ solo projects that commit directly to main).
85
+
86
+ Run via Bash to read the current milestone from progress.md:
87
+
88
+ ```bash
89
+ node -e "
90
+ const fs = require('fs');
91
+ let milestone = '';
92
+ try {
93
+ const text = fs.readFileSync('.gsd-t/progress.md', 'utf8');
94
+ // Look for '## Current Milestone: M36' or 'Milestone: M36' patterns
95
+ const m = text.match(/(?:##\s*)?(?:Current\s+)?Milestone[:\s]+(\S+)/i);
96
+ if (m) milestone = m[1].replace(/[^A-Za-z0-9_.-]/g, '');
97
+ } catch (_) {}
98
+ console.log('MILESTONE=' + (milestone || 'unknown'));
99
+ "
100
+ ```
101
+
102
+ Also read the project name from `package.json` (best-effort):
103
+
104
+ ```bash
105
+ node -e "
106
+ try {
107
+ const p = JSON.parse(require('fs').readFileSync('package.json', 'utf8'));
108
+ console.log('PROJECT_NAME=' + (p.name || ''));
109
+ } catch (_) {
110
+ console.log('PROJECT_NAME=');
111
+ }
112
+ "
113
+ ```
114
+
115
+ ### 1d: Check for Stale Stop Sentinel
116
+
117
+ If `.gsd-t/.unattended/stop` exists from a previous run, remove it before spawning (per contract §10 — the launch command cleans the stale sentinel):
118
+
119
+ ```bash
120
+ if [ -f ".gsd-t/.unattended/stop" ]; then
121
+ rm ".gsd-t/.unattended/stop"
122
+ echo "STALE_SENTINEL=removed"
123
+ else
124
+ echo "STALE_SENTINEL=none"
125
+ fi
126
+ ```
127
+
128
+ If the sentinel was removed, print: `ℹ️ Removed stale stop sentinel from previous run.`
129
+
130
+ ### 1e: Verify Required Software is Installed
131
+
132
+ The unattended supervisor depends on platform-specific helpers. Check them up front and fail fast with clear install instructions rather than crashing mid-run.
133
+
134
+ Run via Bash:
135
+
136
+ ```bash
137
+ node -e "
138
+ const { execSync } = require('child_process');
139
+ const os = require('os');
140
+ const platform = os.platform();
141
+
142
+ function has(cmd) {
143
+ try {
144
+ const which = platform === 'win32' ? 'where' : 'command -v';
145
+ execSync(which + ' ' + cmd, { stdio: 'ignore' });
146
+ return true;
147
+ } catch (_) { return false; }
148
+ }
149
+
150
+ const missing = [];
151
+ const warnings = [];
152
+
153
+ // REQUIRED on all platforms
154
+ if (!has('node')) missing.push({ name: 'node', install: 'https://nodejs.org (>= 16)' });
155
+ if (!has('claude')) {
156
+ missing.push({
157
+ name: 'claude',
158
+ install: platform === 'win32'
159
+ ? 'npm install -g @anthropic-ai/claude-code (then ensure claude.cmd is on PATH)'
160
+ : 'npm install -g @anthropic-ai/claude-code'
161
+ });
162
+ }
163
+ if (!has('git')) missing.push({ name: 'git', install: 'https://git-scm.com/downloads' });
164
+
165
+ // PLATFORM-SPECIFIC (warnings only — supervisor runs without them, just less resilient)
166
+ if (platform === 'darwin') {
167
+ if (!has('caffeinate')) warnings.push('caffeinate (macOS built-in) — missing means sleep may interrupt long runs');
168
+ } else if (platform === 'linux') {
169
+ if (!has('systemd-inhibit') && !has('caffeine')) {
170
+ warnings.push('systemd-inhibit or caffeine — missing means sleep/screen-lock may interrupt long runs');
171
+ }
172
+ if (!has('notify-send')) {
173
+ warnings.push('notify-send (libnotify-bin) — missing means no desktop notifications on milestone events');
174
+ }
175
+ } else if (platform === 'win32') {
176
+ warnings.push('Windows: sleep prevention uses PowerShell SetThreadExecutionState — no external helper required');
177
+ warnings.push('Windows: desktop notifications use BurntToast PowerShell module — install with: Install-Module BurntToast');
178
+ }
179
+
180
+ if (missing.length > 0) {
181
+ console.log('PREFLIGHT=fail');
182
+ console.log('MISSING=' + JSON.stringify(missing));
183
+ } else {
184
+ console.log('PREFLIGHT=ok');
185
+ }
186
+ if (warnings.length > 0) {
187
+ console.log('WARNINGS=' + JSON.stringify(warnings));
188
+ }
189
+ "
190
+ ```
191
+
192
+ If `PREFLIGHT=fail`, **STOP** and print:
193
+
194
+ ```
195
+ ❌ Required software missing — cannot launch unattended supervisor.
196
+
197
+ Missing:
198
+ {for each item in MISSING:}
199
+ • {name}
200
+ Install: {install}
201
+
202
+ Install the missing software and retry: /user:gsd-t-unattended
203
+ ```
204
+
205
+ End the turn. Do NOT spawn.
206
+
207
+ If `WARNINGS` were emitted, print them as a non-blocking advisory before proceeding:
208
+
209
+ ```
210
+ ⚠️ Platform advisories (non-blocking):
211
+ {for each warning:}
212
+ • {warning}
213
+
214
+ Supervisor will still launch, but consider installing these for best reliability.
215
+ ```
216
+
217
+ ## Step 2: Spawn the Detached Supervisor
218
+
219
+ ⚙ [sonnet] gsd-t-unattended → spawning detached supervisor
220
+
221
+ **Before spawn — read starting context tokens (observability bracket):**
222
+
223
+ ```bash
224
+ T_START=$(date +%s) && DT_START=$(date +"%Y-%m-%d %H:%M")
225
+ T0_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
226
+ T0_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
227
+ ```
228
+
229
+ If `--dry-run` was specified, print:
230
+
231
+ ```
232
+ 🔎 Dry-run mode — would spawn:
233
+ node bin/gsd-t.js unattended --hours={hours} --milestone={milestone} --max-iterations={maxIterations}
234
+ Project: {cwd}
235
+
236
+ No supervisor launched. Remove --dry-run to proceed.
237
+ ```
238
+
239
+ Then end the turn.
240
+
241
+ Otherwise, run the actual spawn:
242
+
243
+ ```bash
244
+ node -e "
245
+ const path = require('path');
246
+ const { spawnSupervisor } = require('./bin/gsd-t-unattended-platform.js');
247
+
248
+ // Parse CLI args forwarded from the launch command
249
+ const hours = parseInt(process.env.GSD_T_HOURS || '24', 10) || 24;
250
+ const milestone = process.env.GSD_T_MILESTONE || '';
251
+ const maxIterations = parseInt(process.env.GSD_T_MAX_ITERATIONS || '200', 10) || 200;
252
+
253
+ const binPath = path.resolve(__dirname, 'bin', 'gsd-t.js');
254
+ const cwd = process.cwd();
255
+
256
+ const extraArgs = [];
257
+ if (hours !== 24) extraArgs.push('--hours=' + hours);
258
+ if (milestone) extraArgs.push('--milestone=' + milestone);
259
+ if (maxIterations !== 200) extraArgs.push('--max-iterations=' + maxIterations);
260
+
261
+ const result = spawnSupervisor({ binPath, args: extraArgs, cwd });
262
+ console.log('SPAWNED_PID=' + result.pid);
263
+ " \
264
+ GSD_T_HOURS={hours} \
265
+ GSD_T_MILESTONE={milestone} \
266
+ GSD_T_MAX_ITERATIONS={maxIterations}
267
+ ```
268
+
269
+ Capture `SPAWNED_PID` from the output.
270
+
271
+ **After spawn — record observability bracket:**
272
+
273
+ ```bash
274
+ T_END=$(date +%s) && DT_END=$(date +"%Y-%m-%d %H:%M") && DURATION=$((T_END-T_START))
275
+ T1_TOKENS=$(node -e "try{const s=require('fs').readFileSync('.gsd-t/.context-meter-state.json','utf8');process.stdout.write(String(JSON.parse(s).inputTokens||0))}catch(_){process.stdout.write('0')}")
276
+ T1_PCT=$(node -e "try{const tb=require('./bin/token-budget.js');process.stdout.write(String(tb.getSessionStatus('.').pct||0))}catch(_){process.stdout.write('0')}")
277
+ COUNTER=$(node bin/task-counter.cjs status 2>/dev/null | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{process.stdout.write(String(JSON.parse(s).count||''))}catch(_){process.stdout.write('')}})")
278
+ ```
279
+
280
+ Append to `.gsd-t/token-log.md` (create with header if missing):
281
+
282
+ ```bash
283
+ node -e "
284
+ const fs = require('fs');
285
+ const LOG = '.gsd-t/token-log.md';
286
+ const header = '| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Domain | Task | Tasks-Since-Reset |\n|---|---|---|---|---|---|---|---|---|---|\n';
287
+ const row = '| ${DT_START} | ${DT_END} | gsd-t-unattended | Step 2 | sonnet | ${DURATION}s | supervisor spawned PID ${SPAWNED_PID} | | | ${COUNTER} |\n';
288
+ if (!fs.existsSync(LOG)) fs.writeFileSync(LOG, header);
289
+ fs.appendFileSync(LOG, row);
290
+ "
291
+ ```
292
+
293
+ ## Step 3: Verify Supervisor Liveness
294
+
295
+ Wait up to 2 seconds for the supervisor to write its PID file and transition out of `initializing`:
296
+
297
+ ```bash
298
+ node -e "
299
+ const fs = require('fs');
300
+ const path = require('path');
301
+
302
+ const PID_FILE = '.gsd-t/.unattended/supervisor.pid';
303
+ const STATE_FILE = '.gsd-t/.unattended/state.json';
304
+ const LOG_FILE = '.gsd-t/.unattended/run.log';
305
+ const SPAWNED_PID = parseInt(process.env.SPAWNED_PID || '0', 10);
306
+ const WAIT_MS = 2000;
307
+ const POLL_MS = 100;
308
+
309
+ function sleep(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
310
+
311
+ let alive = false;
312
+ let pidFromFile = null;
313
+ const deadline = Date.now() + WAIT_MS;
314
+
315
+ while (Date.now() < deadline) {
316
+ // Check process liveness via process.kill(pid, 0)
317
+ if (SPAWNED_PID > 0) {
318
+ try { process.kill(SPAWNED_PID, 0); alive = true; } catch (e) {
319
+ alive = e.code === 'EPERM';
320
+ }
321
+ }
322
+ // Also try reading the PID file the supervisor writes
323
+ if (fs.existsSync(PID_FILE)) {
324
+ try { pidFromFile = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10); } catch (_) {}
325
+ }
326
+ if (alive || pidFromFile) break;
327
+ sleep(POLL_MS);
328
+ }
329
+
330
+ if (!alive && !pidFromFile) {
331
+ console.log('LIVENESS=dead');
332
+ // Emit last few lines of run.log if it exists
333
+ if (fs.existsSync(LOG_FILE)) {
334
+ try {
335
+ const txt = fs.readFileSync(LOG_FILE, 'utf8');
336
+ const tail = txt.split('\n').filter(l => l.trim()).slice(-20).join('\n');
337
+ console.log('LOG_TAIL=' + JSON.stringify(tail));
338
+ } catch (_) { console.log('LOG_TAIL='); }
339
+ } else {
340
+ console.log('LOG_TAIL=');
341
+ }
342
+ } else {
343
+ console.log('LIVENESS=ok');
344
+ console.log('PID_FROM_FILE=' + (pidFromFile || SPAWNED_PID));
345
+ }
346
+ " SPAWNED_PID=${SPAWNED_PID}
347
+ ```
348
+
349
+ If `LIVENESS=dead`, the supervisor crashed at startup. Print diagnostics and **STOP**:
350
+
351
+ ```
352
+ 💥 Supervisor crashed at startup (PID {SPAWNED_PID} no longer alive after 2s).
353
+
354
+ Startup diagnostics (run.log tail):
355
+ ──────────────────────────────────────────────────────────────
356
+ {LOG_TAIL}
357
+ ──────────────────────────────────────────────────────────────
358
+
359
+ Common causes:
360
+ • Missing Node.js or claude binary on PATH
361
+ • .gsd-t/progress.md has no active milestone
362
+ • Protected branch or dirty worktree rejected by safety rails
363
+ • Permissions error on .gsd-t/.unattended/ directory
364
+
365
+ Fix the issue above and retry: /user:gsd-t-unattended
366
+ ```
367
+
368
+ End the turn without scheduling a watch tick.
369
+
370
+ ## Step 4: Display Initial Status Block
371
+
372
+ Print the launch confirmation block:
373
+
374
+ ```
375
+ 🟢 Unattended supervisor launched.
376
+
377
+ PID: {PID_FROM_FILE}
378
+ Project: {PROJECT_NAME}
379
+ Milestone: {milestone}
380
+ Max hours: {hours}
381
+ Max iterations: {maxIterations}
382
+ Log: .gsd-t/.unattended/run.log
383
+ State: .gsd-t/.unattended/state.json
384
+ Watch: ScheduleWakeup every 270s
385
+
386
+ The supervisor is running detached — it survives /clear and terminal close.
387
+ Stop gracefully: /user:gsd-t-unattended-stop
388
+ Watch manually: /user:gsd-t-unattended-watch
389
+ ```
390
+
391
+ ## Step 5: Schedule the First Watch Tick
392
+
393
+ Call the harness `ScheduleWakeup` tool with these exact parameters:
394
+
395
+ - `delaySeconds`: `270` (fixed — inside the 5-minute prompt-cache TTL)
396
+ - `prompt`: `/user:gsd-t-unattended-watch`
397
+ - `reason`: `first unattended tick`
398
+
399
+ Tool invocation pattern (make this a real tool call, not a bash command):
400
+
401
+ ```
402
+ ScheduleWakeup(
403
+ delaySeconds: 270,
404
+ prompt: "/user:gsd-t-unattended-watch",
405
+ reason: "first unattended tick"
406
+ )
407
+ ```
408
+
409
+ After the tool call, end the turn. The in-session watch loop takes over from here. Do NOT output a "Next Up" block — this is a self-rescheduling loop, not a phase workflow.
410
+
411
+ ## Notes
412
+
413
+ - **Singleton**: only one supervisor per project at a time. PID collision → refuse with "already running" message.
414
+ - **Stale stop sentinel**: if `.gsd-t/.unattended/stop` exists from a prior run, Step 1d removes it before spawning.
415
+ - **Platform helper**: uses `spawnSupervisor` from `bin/gsd-t-unattended-platform.js` — never hand-rolls `child_process.spawn` directly. This handles macOS/Linux/Windows differences.
416
+ - **Dry-run**: `--dry-run` prints the would-be invocation without spawning. Useful for validating flags before a long overnight run.
417
+ - **No doc ripple, no pre-commit gate**: this command spawns a background process; it does not modify any source files or contracts.
418
+ - **watch command is stateless**: after this command returns, every `/user:gsd-t-unattended-watch` tick re-reads state from disk. There is no in-memory state to preserve.
419
+
420
+ $ARGUMENTS
@@ -201,7 +201,7 @@ The JSON on stdout contains `{consumed, estimated_remaining, pct, threshold}`
201
201
  Exit-code handling (three-band model per `token-budget-contract.md` v3.0.0):
202
202
  - `0` (normal, <70%) → proceed to the next phase at full quality.
203
203
  - `13` (warn, 70–85%) → log the warning to `.gsd-t/token-log.md` and proceed to the next phase at full quality. **Never downgrade models or skip phases** — the runway estimator (m35-runway-estimator) is responsible for halting cleanly before reaching `stop`.
204
- - `10` (stop, ≥85%) → STOP the wave loop. Save checkpoint to `.gsd-t/progress.md` — record which phases are complete, which remain. Output exactly: `⏸️ Wave orchestrator context gate reached ({pct}% of model window). Progress saved. Run /clear then /user:gsd-t-wave to continue from the next phase.` Do NOT spawn the next phase agent.
204
+ - `10` (stop, ≥85%) → STOP the wave loop. Save checkpoint to `.gsd-t/progress.md` — record which phases are complete, which remain. Call `autoSpawnHeadless({command: 'gsd-t-wave', args, projectDir})` — this spawns a fresh headless session that auto-resumes via `/gsd-t-resume` without any manual `/clear`. Output: `⏸️ Wave orchestrator context gate reached ({pct}% of model window) handing off to a fresh headless session (ID: {id}). Progress saved.` Return cleanly. Do NOT spawn the next phase agent, do NOT exit with a special code — the handoff is the success path.
205
205
 
206
206
  As of v2.0.0 (M34), the wave orchestrator reads the SAME `bin/token-budget.js` real-source measurement as the execute orchestrator — both trace back to `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. Each phase spawn (PARTITION, DISCUSS, PLAN, IMPACT, EXECUTE, TEST-SYNC, INTEGRATE, VERIFY+COMPLETE, DOC-RIPPLE) causes post-call updates to the state file, so each subsequent gate check reflects the real context consumption trajectory. When the state file is absent or stale, the call falls back to the historical heuristic.
207
207
 
@@ -114,6 +114,9 @@ GSD-T reads all state files and tells you exactly where you left off.
114
114
 
115
115
  | Command | Purpose | Auto |
116
116
  |---------|---------|------|
117
+ | `/user:gsd-t-unattended` | Launch detached supervisor — runs active milestone to completion with zero human intervention | Manual |
118
+ | `/user:gsd-t-unattended-watch` | Watch tick — fires every 270s via ScheduleWakeup, reports supervisor status | Auto |
119
+ | `/user:gsd-t-unattended-stop` | Touch stop sentinel — supervisor halts after current worker finishes | Manual |
117
120
  | `/user:gsd-t-wave` | Full cycle, auto-advances all phases | Manual |
118
121
  | `/user:gsd-t-status` | Cross-domain progress view with token breakdown, global ELO and cross-project rankings | Manual |
119
122
  | `/user:gsd-t-resume` | Restore context, continue | Manual |
@@ -286,6 +289,20 @@ Drop a `.md` file into `templates/stacks/` to add a new stack. Files prefixed wi
286
289
 
287
290
  ---
288
291
 
292
+ ## Unattended Execution (M36)
293
+
294
+ Run the active milestone to completion over hours or days with zero human intervention. The unattended supervisor is an OS-level process that spawns `claude -p` workers in a relay — each worker runs in a fresh context window, completing one round of wave tasks before handing off to the next. The supervisor survives `/clear`, terminal close, and sleep/wake cycles (macOS/Linux; see `docs/unattended-windows-caveats.md` for Windows).
295
+
296
+ **Relay model**: Each iteration spawns a fresh `claude -p` session with a compact prompt derived from `.gsd-t/progress.md` state. The supervisor waits for the worker to exit, records the exit code in `state.json`, runs safety checks (gutter detection, blocker sentinel scan), and spawns the next worker. Workers never overlap. Context rot is impossible — each worker starts clean.
297
+
298
+ **Watch loop**: `/user:gsd-t-unattended` starts an in-session ScheduleWakeup loop that ticks every 270 seconds. Each tick reads `state.json` and `supervisor.pid` to render a live progress block. When the supervisor reaches a terminal state (`done`, `failed`, `stopped`), the watch loop stops rescheduling and prints a final summary. A `/clear` + `/user:gsd-t-resume` transparently re-attaches: the resume command checks for a live `supervisor.pid` and re-starts the watch loop automatically.
299
+
300
+ **Safety rails**: Branch protection (refuses to run on `main`/`master`/`release/*` by default), dirty-tree check (whitelists GSD-T runtime files), per-iteration gutter detection (repeated error patterns, file thrash, no-progress stall), wall-clock and iteration caps, and a blocker sentinel that halts on unrecoverable worker errors.
301
+
302
+ **Contract**: `.gsd-t/contracts/unattended-supervisor-contract.md` v1.0.0 is the authoritative reference for the state file schema, exit codes, CLI surface, and platform matrix.
303
+
304
+ ---
305
+
289
306
  ## Headless Mode
290
307
 
291
308
  Run GSD-T non-interactively in CI/CD pipelines or automated workflows.
@@ -68,14 +68,14 @@ The framework has no runtime — it is consumed entirely by Claude Code's slash
68
68
 
69
69
  ### Headless Mode (M23 — complete)
70
70
  - **doHeadless(args)**: Dispatch function for the `headless` CLI subcommand.
71
- - **doHeadlessExec(command, cmdArgs, flags)**: Wraps `claude -p "/user:gsd-t-{command}"` via `execFileSync`. Verifies claude CLI availability, enforces timeout, writes log file if `--log` requested. Returns structured JSON if `--json` flag set.
71
+ - **doHeadlessExec(command, cmdArgs, flags)**: Wraps `claude -p "/gsd-t-{command}"` via `execFileSync`. Verifies claude CLI availability, enforces timeout, writes log file if `--log` requested. Returns structured JSON if `--json` flag set. (M36 Phase 0: prompt form is `/gsd-t-X`, NOT `/user:gsd-t-X` — non-interactive mode rejects the `/user:` namespace prefix.)
72
72
  - **parseHeadlessFlags(args)**: Extracts `--json`, `--timeout=N`, `--log` from raw args. Returns `{ flags, positional }`.
73
- - **buildHeadlessCmd(command, cmdArgs)**: Builds the `/user:gsd-t-{command}` prompt string.
74
- - **mapHeadlessExitCode(processExitCode, output)**: Maps process exit code + output text patterns to GSD-T exit codes (0–4).
73
+ - **buildHeadlessCmd(command, cmdArgs)**: Builds the bare `/gsd-t-{command}` prompt string. Interactive-mode `/user:` prefix deliberately omitted — see `.gsd-t/M36-spike-findings.md` Spike A.
74
+ - **mapHeadlessExitCode(processExitCode, output)**: Maps process exit code + output text patterns to GSD-T exit codes (0–5).
75
75
  - **headlessLogPath(projectDir, timestamp)**: Generates `.gsd-t/headless-{timestamp}.log` path.
76
76
  - **doHeadlessQuery(type)**: Dispatches to one of 7 query functions. All pure Node.js file reads, no LLM calls, <100ms.
77
77
  - **Query functions** (7): `queryStatus`, `queryDomains`, `queryContracts`, `queryDebt`, `queryContext`, `queryBacklog`, `queryGraph` — each reads corresponding `.gsd-t/` file and returns typed JSON result.
78
- - **Exit codes**: 0=success, 1=verify-fail, 2=context-budget-exceeded, 3=error, 4=blocked-needs-human
78
+ - **Exit codes**: 0=success, 1=verify-fail, 2=context-budget-exceeded, 3=error, 4=blocked-needs-human, 5=command-dispatch-failed (M36 Phase 0 — `claude -p` returned `Unknown command:` for the slash command; caller should treat as a bug not a transient failure)
79
79
  - **CI/CD examples**: `docs/ci-examples/github-actions.yml` (GitHub Actions), `docs/ci-examples/gitlab-ci.yml` (GitLab CI)
80
80
 
81
81
  ### Compaction-Proof Debug Loop (M29 — complete)
@@ -272,6 +272,83 @@ QA runs inline or as Task subagent depending on phase (M10 refactor). Removed fr
272
272
  - **Resource limits**: Heartbeat stdin capped at 1MB, HTTP responses capped at 1MB (M5), 5s/8s timeouts, 7-day file cleanup
273
273
  - **Wave security**: `bypassPermissions` mode documented with attack surface analysis and mitigations (M5)
274
274
 
275
+ ## Unattended Supervisor (M36)
276
+
277
+ The unattended supervisor is a cross-session relay engine that runs an active GSD-T milestone to completion over hours or days without human intervention. It spans the boundary between the interactive Claude session and the OS process layer.
278
+
279
+ ### Component Diagram
280
+
281
+ ```
282
+ Interactive Claude session
283
+ └── /user:gsd-t-unattended (launch command)
284
+ ├── Pre-flight safety checks (branch, dirty tree)
285
+ └── spawn(detached) → Supervisor process (bin/gsd-t-unattended.js)
286
+ ├── writes .gsd-t/.unattended/supervisor.pid
287
+ ├── writes .gsd-t/.unattended/state.json (atomic rewrite each iter)
288
+ ├── appends .gsd-t/.unattended/run.log (worker stdout+stderr)
289
+ ├── checks .gsd-t/.unattended/stop (sentinel — presence = halt)
290
+ └── relay loop:
291
+ spawnSync('claude -p "/gsd-t-resume"')
292
+ → worker exits → post-worker safety check → next iter
293
+
294
+ In-session watch loop (every 270s via ScheduleWakeup)
295
+ └── /user:gsd-t-unattended-watch
296
+ ├── reads supervisor.pid (kill -0 liveness)
297
+ ├── reads state.json (status, iter, lastTick)
298
+ └── reschedules or reports final status
299
+ ```
300
+
301
+ ### State Directory Layout
302
+
303
+ ```
304
+ .gsd-t/.unattended/
305
+ ├── supervisor.pid — Integer PID. Exists ONLY while supervisor is alive.
306
+ ├── state.json — Live state snapshot. Atomically rewritten between iterations.
307
+ ├── run.log — Append-only worker stdout+stderr. Never truncated during a run.
308
+ ├── stop — Sentinel file. Absence = run. Presence = user-requested stop.
309
+ └── config.json — Optional per-project config overrides (maxIterations, hours, etc.)
310
+ ```
311
+
312
+ Sibling: `.gsd-t/.handoff/` — owned by M35-gap-fixes for single-shot handoff locks (see below).
313
+
314
+ ### Contract
315
+
316
+ `.gsd-t/contracts/unattended-supervisor-contract.md` v1.0.0 — authoritative source for: state schema, status enum, exit-code table, launch handshake, watch tick decision tree, resume auto-reattach handshake, stop mechanism, safety-rails hook points, and CLI surface.
317
+
318
+ ### Platform Abstraction Layer (`bin/gsd-t-unattended-platform.js`)
319
+
320
+ Exports four cross-platform functions:
321
+
322
+ | Export | macOS | Linux | Windows |
323
+ |--------|-------|-------|---------|
324
+ | `spawnSupervisor(args)` | `spawn(node, ...)` detached | same | same (`windowsHide:true`) |
325
+ | `preventSleep()` | `caffeinate -i` subprocess | `systemd-inhibit` or no-op | no-op (not supported — see docs/unattended-windows-caveats.md) |
326
+ | `releaseSleep(handle)` | kill caffeinate PID | release inhibit or no-op | no-op |
327
+ | `notify(title, msg, level)` | `osascript` | `notify-send` | no-op |
328
+ | `resolveClaudePath()` | PATH lookup | PATH lookup | `claude.cmd` via PATH |
329
+
330
+ ### Safety Rails (`bin/gsd-t-unattended-safety.js`)
331
+
332
+ Called at four supervisor hook points (pre-launch, supervisor-init, pre-worker, post-worker):
333
+
334
+ - **Gutter detection**: stall pattern — repeated identical errors or no file changes for N iterations
335
+ - **Blocker sentinels**: scan worker stdout for unrecoverable-error markers (`BLOCKED_NEEDS_HUMAN`, `DISPATCH_FAILED`)
336
+ - **Iteration cap**: `maxIterations` guard (default 200)
337
+ - **Wall-clock cap**: `hours` guard (default 24h)
338
+ - **Branch/dirty-tree pre-flight**: refuses to start on protected branches or uncleaned worktrees
339
+
340
+ Each check returns `{ ok, reason?, code? }`. A `false` result halts with `status = 'failed'` and the corresponding exit code (6=gutter, 7=protected-branch, 8=dirty-tree).
341
+
342
+ ### Handoff-Lock Primitive (`bin/handoff-lock.js`)
343
+
344
+ Closes the M35 parent/child race in `bin/headless-auto-spawn.js`. When the runway estimator fires `autoSpawnHeadless()`, the parent session writes a lock file in `.gsd-t/.handoff/` before spawning the child and removes it only after the child has confirmed PID + state-ready. Prevents the child from beginning execution before the parent has cleanly exited — eliminating the race where both sessions wrote to the same `.gsd-t/` files simultaneously.
345
+
346
+ ### Resume Auto-Reattach
347
+
348
+ `/user:gsd-t-resume` Step 0 checks for a live supervisor before any other resume logic. If `supervisor.pid` exists and `kill -0` succeeds and `state.json.status` is non-terminal, the resume command skips normal resume flow entirely, prints the current watch block, and calls `ScheduleWakeup(270, '/user:gsd-t-unattended-watch', ...)`. The user transparently re-enters the watch loop without any manual step.
349
+
350
+ ---
351
+
275
352
  ## Design Decisions
276
353
 
277
354
  | Date | Decision | Rationale | Alternatives Considered |
@@ -312,6 +312,110 @@ Read-only surface onto the token telemetry stream. Backward-compatible with pre-
312
312
 
313
313
  When a sonnet-default phase hits a complexity signal that warrants opus (e.g., cross-module refactor detected mid-execution), the command may emit an `/advisor` hook line in its output — a structured suggestion for the orchestrator to escalate the **next** spawn of that phase to opus. This is a plan-time signal, not a runtime swap: the current spawn completes at its assigned tier, and the escalation applies to subsequent work. See `.gsd-t/contracts/model-selection-contract.md` for the hook schema.
314
314
 
315
+ ## Unattended Supervisor Setup (M36)
316
+
317
+ The unattended supervisor runs an active GSD-T milestone to completion in a detached OS process — no terminal needed, no human intervention required.
318
+
319
+ ### Quick Start
320
+
321
+ ```bash
322
+ # From within an interactive Claude session:
323
+ /user:gsd-t-unattended
324
+
325
+ # From the terminal (detached — returns immediately):
326
+ gsd-t unattended --hours=24 --milestone=M36
327
+
328
+ # Watch current run status (in-session, 270s tick):
329
+ /user:gsd-t-unattended-watch
330
+
331
+ # Request a graceful stop:
332
+ /user:gsd-t-unattended-stop
333
+ ```
334
+
335
+ ### CLI Flags
336
+
337
+ ```
338
+ gsd-t unattended [OPTIONS]
339
+ --hours=24 Wall-clock cap in hours (default: 24)
340
+ --max-iterations=200 Worker iteration cap (default: 200)
341
+ --project=. Project directory (default: cwd)
342
+ --branch=AUTO Branch to run on; AUTO = current non-protected branch
343
+ --on-done=print Terminal action: print | merge-commit (merge-commit is v2)
344
+ --dry-run Preflight only; no spawn
345
+ --verbose Extra log detail
346
+ --test-mode Uses stub worker; for CI and smoke tests
347
+ ```
348
+
349
+ ### Config File (optional)
350
+
351
+ `.gsd-t/unattended-config.json` — per-project overrides. Absence = hardcoded defaults.
352
+
353
+ ```json
354
+ {
355
+ "hours": 24,
356
+ "maxIterations": 200,
357
+ "gutterNoProgressIters": 5,
358
+ "workerTimeoutMs": 3600000,
359
+ "protectedBranches": ["main", "master", "develop", "trunk"],
360
+ "dirtyTreeWhitelist": [".gsd-t/.unattended/*", ".gsd-t/events/*.jsonl"]
361
+ }
362
+ ```
363
+
364
+ ### State Files
365
+
366
+ | File | Purpose |
367
+ |------|---------|
368
+ | `.gsd-t/.unattended/supervisor.pid` | Integer PID. Exists only while supervisor is alive. |
369
+ | `.gsd-t/.unattended/state.json` | Live state snapshot — status, iter, milestone, lastTick, etc. Full schema in `unattended-supervisor-contract.md`. |
370
+ | `.gsd-t/.unattended/run.log` | Append-only worker stdout+stderr. Never truncated during a run. |
371
+ | `.gsd-t/.unattended/stop` | Sentinel — touching this file requests a graceful stop. |
372
+ | `.gsd-t/.unattended/config.json` | Optional per-project config overrides (same keys as CLI flags). |
373
+
374
+ ### Required Platform Helpers
375
+
376
+ | Platform | Sleep Prevention | Notifications |
377
+ |----------|-----------------|---------------|
378
+ | macOS | `caffeinate` (built-in) | `osascript` (built-in) |
379
+ | Linux | `systemd-inhibit` or no-op | `notify-send` (install via `apt`/`dnf`) |
380
+ | Windows | NOT supported — see `docs/unattended-windows-caveats.md` | no-op |
381
+
382
+ macOS and Linux work out of the box on standard installs. Windows can run the supervisor but the machine may sleep mid-run.
383
+
384
+ ### Contract
385
+
386
+ `.gsd-t/contracts/unattended-supervisor-contract.md` v1.0.0 — authoritative state schema, exit-code table, status enum, launch/resume handshakes.
387
+
388
+ ### Troubleshooting
389
+
390
+ **Supervisor won't start**
391
+ - Check `.gsd-t/.unattended/run.log` for error output
392
+ - Run `gsd-t unattended --dry-run` to run pre-flight checks without spawning
393
+ - Verify no protected branch: `git branch --show-current`
394
+
395
+ **"Already running" error**
396
+ ```bash
397
+ # Verify supervisor is actually alive:
398
+ kill -0 $(cat .gsd-t/.unattended/supervisor.pid) && echo "alive" || echo "stale PID"
399
+
400
+ # If stale, remove PID file:
401
+ rm .gsd-t/.unattended/supervisor.pid
402
+
403
+ # Or request a graceful stop:
404
+ /user:gsd-t-unattended-stop
405
+ # (or) touch .gsd-t/.unattended/stop
406
+ ```
407
+
408
+ **Watch loop stopped firing**
409
+ - Re-invoke `/user:gsd-t-resume` from a fresh session
410
+ - Step 0 auto-reattach reads `supervisor.pid` — if the supervisor is still alive, it re-enters the watch loop automatically (no manual steps needed)
411
+
412
+ **Supervisor crashed mid-run**
413
+ - The watch loop detects crash via `kill -0` failure
414
+ - Check `.gsd-t/.unattended/run.log` and final `state.json` for diagnostics
415
+ - Resume normally with `/user:gsd-t-resume` — the milestone continues from its last checkpoint
416
+
417
+ ---
418
+
315
419
  ## Security Notes
316
420
 
317
421
  - Zero npm dependencies — no supply chain risk