@tekyzinc/gsd-t 3.29.11 โ 4.0.12
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 +146 -0
- package/bin/gsd-t-parallel.cjs +64 -13
- package/bin/gsd-t.js +48 -376
- package/bin/journey-coverage.cjs +6 -3
- package/bin/parallel-cli.cjs +13 -1
- package/commands/gsd-t-debug.md +47 -533
- package/commands/gsd-t-design-decompose.md +24 -497
- package/commands/gsd-t-doc-ripple.md +23 -139
- package/commands/gsd-t-execute.md +41 -958
- package/commands/gsd-t-help.md +0 -2
- package/commands/gsd-t-impact.md +26 -295
- package/commands/gsd-t-integrate.md +44 -369
- package/commands/gsd-t-milestone.md +25 -124
- package/commands/gsd-t-partition.md +28 -539
- package/commands/gsd-t-plan.md +26 -472
- package/commands/gsd-t-prd.md +25 -311
- package/commands/gsd-t-quick.md +1 -1
- package/commands/gsd-t-resume.md +0 -33
- package/commands/gsd-t-verify.md +52 -569
- package/commands/gsd-t-wave.md +41 -430
- package/package.json +1 -1
- package/scripts/gsd-t-calibration-hook.js +3 -1
- package/scripts/hooks/pre-commit-journey-coverage +0 -2
- package/scripts/hooks/pre-commit-playwright-gate +0 -2
- package/templates/CLAUDE-global.md +43 -173
- package/templates/CLAUDE-project.md +118 -104
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +5 -2
- package/templates/workflows/_lib.js +176 -0
- package/templates/workflows/gsd-t-debug.workflow.js +86 -0
- package/templates/workflows/gsd-t-execute.workflow.js +206 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
- package/templates/workflows/gsd-t-phase.workflow.js +94 -0
- package/templates/workflows/gsd-t-quick.workflow.js +72 -0
- package/templates/workflows/gsd-t-verify.workflow.js +348 -0
- package/templates/workflows/gsd-t-wave.workflow.js +43 -0
- package/bin/check-headless-sessions.js +0 -140
- package/bin/context-budget-audit.cjs +0 -447
- package/bin/context-meter-config.cjs +0 -101
- package/bin/context-meter-config.test.cjs +0 -101
- package/bin/event-stream.cjs +0 -205
- package/bin/gsd-t-benchmark-orchestrator.js +0 -437
- package/bin/gsd-t-capture-lint.cjs +0 -440
- package/bin/gsd-t-economics.cjs +0 -315
- package/bin/gsd-t-in-session-usage.cjs +0 -213
- package/bin/gsd-t-orchestrator-config.cjs +0 -161
- package/bin/gsd-t-orchestrator-queue.cjs +0 -180
- package/bin/gsd-t-orchestrator-recover.cjs +0 -231
- package/bin/gsd-t-orchestrator-worker.cjs +0 -219
- package/bin/gsd-t-orchestrator.js +0 -535
- package/bin/gsd-t-parallel-probe.cjs +0 -132
- package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
- package/bin/gsd-t-ratelimit-probe.cjs +0 -648
- package/bin/gsd-t-report-tokens.cjs +0 -549
- package/bin/gsd-t-stream-feed-client.cjs +0 -151
- package/bin/gsd-t-token-backfill.cjs +0 -366
- package/bin/gsd-t-token-capture.cjs +0 -321
- package/bin/gsd-t-token-dashboard.cjs +0 -353
- package/bin/gsd-t-token-regenerate-log.cjs +0 -129
- package/bin/gsd-t-tool-attribution.cjs +0 -377
- package/bin/gsd-t-tool-cost.cjs +0 -195
- package/bin/gsd-t-transcript-tee.cjs +0 -246
- package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
- package/bin/gsd-t-unattended-platform.cjs +0 -551
- package/bin/gsd-t-unattended-platform.js +0 -381
- package/bin/gsd-t-unattended-safety.cjs +0 -773
- package/bin/gsd-t-unattended-safety.js +0 -788
- package/bin/gsd-t-unattended.cjs +0 -2109
- package/bin/gsd-t-unattended.js +0 -1367
- package/bin/gsd-t-worker-dispatch.cjs +0 -211
- package/bin/handoff-lock.cjs +0 -249
- package/bin/handoff-lock.js +0 -249
- package/bin/headless-auto-spawn.cjs +0 -711
- package/bin/headless-auto-spawn.js +0 -373
- package/bin/headless-exit-codes.cjs +0 -67
- package/bin/live-activity-report.cjs +0 -615
- package/bin/log-tail.cjs +0 -81
- package/bin/m44-proof-measure.cjs +0 -285
- package/bin/m46-iter-proof.cjs +0 -149
- package/bin/m46-worker-proof.cjs +0 -201
- package/bin/m55-substrate-proof.cjs +0 -134
- package/bin/metrics-rollup.js +0 -200
- package/bin/model-windows.cjs +0 -99
- package/bin/model-windows.test.cjs +0 -75
- package/bin/parallelism-report.cjs +0 -535
- package/bin/runway-estimator.cjs +0 -242
- package/bin/spawn-plan-derive.cjs +0 -163
- package/bin/spawn-plan-status-updater.cjs +0 -292
- package/bin/spawn-plan-writer.cjs +0 -204
- package/bin/supervisor-pid-fingerprint.cjs +0 -126
- package/bin/token-budget.cjs +0 -265
- package/bin/unattended-watch-format.cjs +0 -178
- package/bin/watch-progress.js +0 -155
- package/commands/gsd-t-unattended-stop.md +0 -85
- package/commands/gsd-t-unattended-watch.md +0 -465
- package/commands/gsd-t-unattended.md +0 -476
- package/commands/gsd-t-visualize.md +0 -116
- package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
- package/scripts/gsd-t-context-meter.js +0 -341
- package/scripts/gsd-t-context-meter.test.js +0 -471
- package/scripts/gsd-t-dashboard-server.js +0 -1203
- package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
- package/scripts/gsd-t-transcript.html +0 -1821
- package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
- package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
- package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
- package/templates/hooks/post-commit-spawn-plan.sh +0 -85
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
# GSD-T: Unattended Watch โ Stateless Watch Tick
|
|
2
|
-
|
|
3
|
-
**Model**: haiku (polling a state file is mechanical โ no reasoning needed)
|
|
4
|
-
|
|
5
|
-
You are running one tick of the unattended supervisor watch loop. This command is **stateless**: every firing re-reads `.gsd-t/.unattended/supervisor.pid` + `state.json` + `run.log` from disk and makes a fresh decision. There is no in-memory state between ticks, and a `/clear` followed by `/gsd-t-resume` during a live unattended run is a no-op.
|
|
6
|
-
|
|
7
|
-
The watch loop is the user's visibility surface into the running supervisor. It fires every 270 seconds (inside the 5-minute prompt-cache TTL) and either:
|
|
8
|
-
1. Renders a compact status block + reschedules itself, OR
|
|
9
|
-
2. Prints a terminal report and STOPS rescheduling.
|
|
10
|
-
|
|
11
|
-
See `unattended-supervisor-contract.md` ยง8 (Watch Tick Decision Tree), ยง4 (Status Enum), ยง3 (state.json schema).
|
|
12
|
-
|
|
13
|
-
## Decision Tree (Contract ยง8)
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
1. supervisor.pid absent โ finalized cleanly โ final report, STOP
|
|
17
|
-
2. kill -0 {pid} fails โ crashed โ crash diagnostics + log tail, STOP
|
|
18
|
-
3. state.json.status:
|
|
19
|
-
- done โ success report, STOP
|
|
20
|
-
- failed โ failure summary, STOP
|
|
21
|
-
- stopped โ user-stop confirm, STOP
|
|
22
|
-
- initializing | running โ render watch block, go to 4
|
|
23
|
-
4. ScheduleWakeup(270, '/gsd-t-unattended-watch', reason='unattended tick {iter}')
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Critical**: Every branch except `initializing`/`running` is TERMINAL โ do NOT reschedule. When you reach a terminal state, print the report and end the turn. Do not add a "Next Up" block โ this is a self-rescheduling loop, not a phase workflow.
|
|
27
|
-
|
|
28
|
-
## Step 1: Check State Directory
|
|
29
|
-
|
|
30
|
-
Run via Bash:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
if [ ! -d ".gsd-t/.unattended" ]; then
|
|
34
|
-
echo "No supervisor state directory at .gsd-t/.unattended/ โ nothing to watch."
|
|
35
|
-
exit 0
|
|
36
|
-
fi
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
If the directory does not exist, there is no supervisor to watch. Exit 0, do NOT reschedule. End the turn.
|
|
40
|
-
|
|
41
|
-
## Step 2: Read State, PID, and Run.log Tail
|
|
42
|
-
|
|
43
|
-
Run a single `node -e` block that gathers everything needed for the decision tree. This reads the last ~2KB of `run.log` via `fs.open` + `fs.read` with a negative offset from end (not `readFileSync`), so the command stays cheap even when `run.log` is hundreds of MB.
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
node -e "
|
|
47
|
-
const fs = require('fs');
|
|
48
|
-
const path = require('path');
|
|
49
|
-
|
|
50
|
-
const UDIR = '.gsd-t/.unattended';
|
|
51
|
-
const PID_FILE = path.join(UDIR, 'supervisor.pid');
|
|
52
|
-
const STATE_FILE = path.join(UDIR, 'state.json');
|
|
53
|
-
const LOG_FILE = path.join(UDIR, 'run.log');
|
|
54
|
-
|
|
55
|
-
function out(k, v) { console.log(k + '=' + JSON.stringify(v ?? null)); }
|
|
56
|
-
|
|
57
|
-
// --- PID file (JSON or legacy bare-integer; canonical reader handles both) ---
|
|
58
|
-
let pid = null;
|
|
59
|
-
let pidFileExists = fs.existsSync(PID_FILE);
|
|
60
|
-
if (pidFileExists) {
|
|
61
|
-
try {
|
|
62
|
-
const { readPidFile } = require('./bin/supervisor-pid-fingerprint.cjs');
|
|
63
|
-
const entry = readPidFile(process.cwd());
|
|
64
|
-
pid = entry && Number.isInteger(entry.pid) ? entry.pid : null;
|
|
65
|
-
} catch (_) { pid = null; }
|
|
66
|
-
}
|
|
67
|
-
out('PID_FILE_EXISTS', pidFileExists);
|
|
68
|
-
out('PID', pid);
|
|
69
|
-
|
|
70
|
-
// --- kill -0 liveness ---
|
|
71
|
-
let alive = null;
|
|
72
|
-
if (pid !== null) {
|
|
73
|
-
try { process.kill(pid, 0); alive = true; }
|
|
74
|
-
catch (_) { alive = false; }
|
|
75
|
-
}
|
|
76
|
-
out('ALIVE', alive);
|
|
77
|
-
|
|
78
|
-
// --- state.json ---
|
|
79
|
-
let state = null;
|
|
80
|
-
try { state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); }
|
|
81
|
-
catch (_) {
|
|
82
|
-
// tolerate transient rename race โ retry once
|
|
83
|
-
try { state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); }
|
|
84
|
-
catch (_) { state = null; }
|
|
85
|
-
}
|
|
86
|
-
if (state) {
|
|
87
|
-
out('STATUS', state.status);
|
|
88
|
-
out('SESSION', state.sessionId);
|
|
89
|
-
out('MILESTONE', state.milestone);
|
|
90
|
-
out('WAVE', state.wave);
|
|
91
|
-
out('TASK', state.task);
|
|
92
|
-
out('ITER', state.iter);
|
|
93
|
-
out('MAX_ITER', state.maxIterations);
|
|
94
|
-
out('STARTED_AT', state.startedAt);
|
|
95
|
-
out('LAST_TICK', state.lastTick);
|
|
96
|
-
out('LAST_EXIT', state.lastExit);
|
|
97
|
-
out('LAST_ELAPSED_MS', state.lastElapsedMs);
|
|
98
|
-
out('LOG_PATH', state.logPath || LOG_FILE);
|
|
99
|
-
// elapsed
|
|
100
|
-
const started = state.startedAt ? Date.parse(state.startedAt) : null;
|
|
101
|
-
const elapsedMs = started ? (Date.now() - started) : null;
|
|
102
|
-
out('ELAPSED_MS', elapsedMs);
|
|
103
|
-
// last tick age
|
|
104
|
-
const lt = state.lastTick ? Date.parse(state.lastTick) : null;
|
|
105
|
-
const tickAgeMs = lt ? (Date.now() - lt) : null;
|
|
106
|
-
out('TICK_AGE_MS', tickAgeMs);
|
|
107
|
-
// v3.13.11 Bug 3: IS_STALE computed deterministically here (not in Haiku's
|
|
108
|
-
// rendering step) โ stale iff tickAgeMs > 540000 (2ร 270s tick cadence per
|
|
109
|
-
// unattended-supervisor-contract ยง3). Below threshold โ false. At/above
|
|
110
|
-
// threshold โ true. Renderer just reads this flag; no prose interpretation.
|
|
111
|
-
const IS_STALE = tickAgeMs !== null && tickAgeMs > 540000;
|
|
112
|
-
out('IS_STALE', IS_STALE);
|
|
113
|
-
} else {
|
|
114
|
-
out('STATUS', null);
|
|
115
|
-
out('IS_STALE', false);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// --- run.log tail (last ~2KB via fd seek, not full read) ---
|
|
119
|
-
let tailLines = [];
|
|
120
|
-
try {
|
|
121
|
-
const st = fs.statSync(LOG_FILE);
|
|
122
|
-
const size = st.size;
|
|
123
|
-
const readLen = Math.min(size, 2048);
|
|
124
|
-
const offset = size - readLen;
|
|
125
|
-
const fd = fs.openSync(LOG_FILE, 'r');
|
|
126
|
-
const buf = Buffer.alloc(readLen);
|
|
127
|
-
fs.readSync(fd, buf, 0, readLen, offset);
|
|
128
|
-
fs.closeSync(fd);
|
|
129
|
-
const text = buf.toString('utf8');
|
|
130
|
-
tailLines = text.split('\n').filter(l => l.trim().length > 0);
|
|
131
|
-
} catch (_) { tailLines = []; }
|
|
132
|
-
// last 50 lines for crash path; last 1 non-empty for normal path
|
|
133
|
-
const last50 = tailLines.slice(-50);
|
|
134
|
-
const last1 = tailLines.length > 0 ? tailLines[tailLines.length - 1] : '';
|
|
135
|
-
out('LOG_TAIL_LAST1', last1);
|
|
136
|
-
out('LOG_TAIL_LAST50', last50.join('\n'));
|
|
137
|
-
|
|
138
|
-
// --- workflow progress from progress.md and domain task files ---
|
|
139
|
-
let phase = 'unknown';
|
|
140
|
-
let waveInfo = '';
|
|
141
|
-
let waveCurrent = 0;
|
|
142
|
-
let waveTotal = 0;
|
|
143
|
-
let tasksDone = 0;
|
|
144
|
-
let tasksTotal = 0;
|
|
145
|
-
let domainSummary = [];
|
|
146
|
-
try {
|
|
147
|
-
const prog = fs.readFileSync('.gsd-t/progress.md', 'utf8');
|
|
148
|
-
// Extract phase from Status line
|
|
149
|
-
const statusM = prog.match(/^## Status:.*?(\b(?:DEFINED|PARTITIONED|PLANNED|EXECUTING|EXECUTED|INTEGRATING|VERIFYING|VERIFIED|COMPLETE|IN.PROGRESS)\b)/mi);
|
|
150
|
-
if (statusM) phase = statusM[1].trim();
|
|
151
|
-
// Extract "Wave N of M" or latest "Wave N" from Decision Log
|
|
152
|
-
const waveOfM = prog.match(/Wave\s+(\d+)\s+(?:of|\/)\s+(\d+)/gi);
|
|
153
|
-
if (waveOfM && waveOfM.length > 0) {
|
|
154
|
-
const last = waveOfM[waveOfM.length - 1];
|
|
155
|
-
const parts = last.match(/(\d+)\s+(?:of|\/)\s+(\d+)/);
|
|
156
|
-
if (parts) { waveCurrent = parseInt(parts[1], 10); waveTotal = parseInt(parts[2], 10); }
|
|
157
|
-
} else {
|
|
158
|
-
const waveM = prog.match(/Wave\s+(\d+)/gi);
|
|
159
|
-
if (waveM && waveM.length > 0) {
|
|
160
|
-
const last = waveM[waveM.length - 1].match(/(\d+)/);
|
|
161
|
-
if (last) waveCurrent = parseInt(last[1], 10);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} catch (_) {}
|
|
165
|
-
|
|
166
|
-
// Read domain task files for completion counts and wave totals
|
|
167
|
-
try {
|
|
168
|
-
const domainsDir = '.gsd-t/domains';
|
|
169
|
-
if (fs.existsSync(domainsDir)) {
|
|
170
|
-
const domains = fs.readdirSync(domainsDir).filter(d => {
|
|
171
|
-
try { return fs.statSync(path.join(domainsDir, d)).isDirectory(); } catch (_) { return false; }
|
|
172
|
-
});
|
|
173
|
-
let maxWave = 0;
|
|
174
|
-
for (const d of domains) {
|
|
175
|
-
const tasksFile = path.join(domainsDir, d, 'tasks.md');
|
|
176
|
-
if (!fs.existsSync(tasksFile)) continue;
|
|
177
|
-
const txt = fs.readFileSync(tasksFile, 'utf8');
|
|
178
|
-
const lines = txt.split('\n');
|
|
179
|
-
let done = 0, total = 0;
|
|
180
|
-
for (const line of lines) {
|
|
181
|
-
// Count wave headers to determine total waves
|
|
182
|
-
const wH = line.match(/^#+\s*Wave\s+(\d+)/i);
|
|
183
|
-
if (wH) { const w = parseInt(wH[1], 10); if (w > maxWave) maxWave = w; }
|
|
184
|
-
if (/^[-*]\s*\[x\]/i.test(line)) { done++; total++; }
|
|
185
|
-
else if (/^[-*]\s*\[\s?\]/.test(line)) { total++; }
|
|
186
|
-
}
|
|
187
|
-
if (total > 0) {
|
|
188
|
-
tasksDone += done;
|
|
189
|
-
tasksTotal += total;
|
|
190
|
-
domainSummary.push(d + ':' + done + '/' + total);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (maxWave > 0 && waveTotal === 0) waveTotal = maxWave;
|
|
194
|
-
}
|
|
195
|
-
} catch (_) {}
|
|
196
|
-
if (waveCurrent > 0 && waveTotal > 0) waveInfo = 'Wave ' + waveCurrent + ' of ' + waveTotal;
|
|
197
|
-
else if (waveCurrent > 0) waveInfo = 'Wave ' + waveCurrent;
|
|
198
|
-
out('PHASE', phase);
|
|
199
|
-
out('WAVE_INFO', waveInfo);
|
|
200
|
-
out('TASKS_DONE', tasksDone);
|
|
201
|
-
out('TASKS_TOTAL', tasksTotal);
|
|
202
|
-
out('DOMAIN_SUMMARY', domainSummary.join(' | '));
|
|
203
|
-
"
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## Step 3: Branch on PID File Absence (Finalized Cleanly)
|
|
207
|
-
|
|
208
|
-
If `PID_FILE_EXISTS=false`:
|
|
209
|
-
|
|
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:
|
|
268
|
-
|
|
269
|
-
```
|
|
270
|
-
โ
Unattended supervisor finalized cleanly.
|
|
271
|
-
|
|
272
|
-
Session: {SESSION}
|
|
273
|
-
Milestone: {MILESTONE}
|
|
274
|
-
Final status: {STATUS}
|
|
275
|
-
Iterations: {ITER} / {MAX_ITER}
|
|
276
|
-
Total elapsed: {Hh Mm โ formatted from ELAPSED_MS}
|
|
277
|
-
Log: {LOG_PATH}
|
|
278
|
-
State: .gsd-t/.unattended/state.json
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
If `state.json` was also unreadable (null STATUS), emit:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
โ ๏ธ Unattended supervisor PID file absent but state.json unreadable.
|
|
285
|
-
Manual inspection required: ls -la .gsd-t/.unattended/
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
**STOP** โ do NOT reschedule. End the turn.
|
|
289
|
-
|
|
290
|
-
## Step 4: Branch on Dead PID (Crashed)
|
|
291
|
-
|
|
292
|
-
If `PID_FILE_EXISTS=true` AND `ALIVE=false`:
|
|
293
|
-
|
|
294
|
-
The supervisor process is gone but it did not remove its PID file โ this is a crash. Print crash diagnostics including the last 50 lines of `run.log`:
|
|
295
|
-
|
|
296
|
-
```
|
|
297
|
-
๐ฅ Unattended supervisor CRASHED (PID {PID} no longer alive).
|
|
298
|
-
|
|
299
|
-
Last known state:
|
|
300
|
-
Session: {SESSION}
|
|
301
|
-
Status: {STATUS}
|
|
302
|
-
Iter: {ITER} / {MAX_ITER}
|
|
303
|
-
Last tick: {LAST_TICK} (age {tickAgeMs โ seconds})
|
|
304
|
-
Last exit: {LAST_EXIT}
|
|
305
|
-
|
|
306
|
-
Last 50 lines of run.log:
|
|
307
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
308
|
-
{LOG_TAIL_LAST50}
|
|
309
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
310
|
-
|
|
311
|
-
Recovery: inspect .gsd-t/.unattended/run.log and .gsd-t/.unattended/state.json.
|
|
312
|
-
Remove the stale PID file with: rm .gsd-t/.unattended/supervisor.pid
|
|
313
|
-
Then relaunch with: /gsd-t-unattended
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
**STOP** โ do NOT reschedule. End the turn.
|
|
317
|
-
|
|
318
|
-
## Step 5: Branch on Terminal Status
|
|
319
|
-
|
|
320
|
-
If `PID_FILE_EXISTS=true` AND `ALIVE=true` AND `STATUS` is terminal (`done` | `failed` | `stopped`):
|
|
321
|
-
|
|
322
|
-
The supervisor is still alive but has transitioned to a terminal status on its last tick. The next supervisor pass will remove the PID file โ we just caught it mid-cleanup. Print the matching report and STOP.
|
|
323
|
-
|
|
324
|
-
### 5a: STATUS=done
|
|
325
|
-
|
|
326
|
-
```
|
|
327
|
-
๐ Unattended supervisor COMPLETED the milestone.
|
|
328
|
-
|
|
329
|
-
Session: {SESSION}
|
|
330
|
-
Milestone: {MILESTONE} โ DONE
|
|
331
|
-
Iterations: {ITER} / {MAX_ITER}
|
|
332
|
-
Total elapsed: {Hh Mm}
|
|
333
|
-
Log: {LOG_PATH}
|
|
334
|
-
|
|
335
|
-
The supervisor will clean up its PID file momentarily.
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### 5b: STATUS=failed
|
|
339
|
-
|
|
340
|
-
```
|
|
341
|
-
โ Unattended supervisor HALTED โ status=failed.
|
|
342
|
-
|
|
343
|
-
Session: {SESSION}
|
|
344
|
-
Milestone: {MILESTONE}
|
|
345
|
-
Iterations: {ITER} / {MAX_ITER}
|
|
346
|
-
Last exit: {LAST_EXIT}
|
|
347
|
-
Total elapsed: {Hh Mm}
|
|
348
|
-
Log: {LOG_PATH}
|
|
349
|
-
|
|
350
|
-
See run.log for the halt reason. The supervisor finalized state.json before exiting.
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
### 5c: STATUS=stopped
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
๐ Unattended supervisor STOPPED by user.
|
|
357
|
-
|
|
358
|
-
Session: {SESSION}
|
|
359
|
-
Milestone: {MILESTONE}
|
|
360
|
-
Iterations: {ITER} / {MAX_ITER}
|
|
361
|
-
Total elapsed: {Hh Mm}
|
|
362
|
-
Log: {LOG_PATH}
|
|
363
|
-
|
|
364
|
-
Stop sentinel was honored between worker iterations. Relaunch with /gsd-t-unattended.
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
**STOP** โ do NOT reschedule. End the turn.
|
|
368
|
-
|
|
369
|
-
## Step 6: Render Live Watch Block (Non-Terminal)
|
|
370
|
-
|
|
371
|
-
If `PID_FILE_EXISTS=true` AND `ALIVE=true` AND `STATUS` is `initializing` or `running`:
|
|
372
|
-
|
|
373
|
-
Render two blocks: the workflow-progress header (existing) plus the M38 **event-stream activity block** driven by `.gsd-t/events/*.jsonl` since the last cursor.
|
|
374
|
-
|
|
375
|
-
### 6a: Workflow Progress Header
|
|
376
|
-
|
|
377
|
-
Format rules:
|
|
378
|
-
- One extra space after each emoji (per CLAUDE.md markdown table rules โ preserves alignment in terminal views).
|
|
379
|
-
- Elapsed formatted as `{H}h{M}m` from `ELAPSED_MS`.
|
|
380
|
-
- Last-tick age formatted as `{S}s` or `{M}m{S}s`. If `IS_STALE=true` (computed deterministically in Step 2 as `tickAgeMs > 540000`), append ` โ ๏ธ stale`. The renderer must NOT interpret the threshold itself โ Step 2 is the source of truth; rendering just reads the IS_STALE flag verbatim.
|
|
381
|
-
- Tasks progress bar: `[โโโโโโโโโโโโ] 8/12` โ filled blocks proportional to done/total.
|
|
382
|
-
- Domain breakdown only shown if โค6 domains (otherwise too noisy).
|
|
383
|
-
|
|
384
|
-
```
|
|
385
|
-
โ Unattended โ {MILESTONE} ยท {PHASE}{ ยท {WAVE_INFO}}
|
|
386
|
-
๐ Tasks: {TASKS_DONE}/{TASKS_TOTAL} [{progress bar}] ยท elapsed {Hh Mm}
|
|
387
|
-
๐ง {DOMAIN_SUMMARY or "No domains yet (pre-partition)"}
|
|
388
|
-
โฑ Iter {ITER}/{MAX_ITER} ยท last tick {tickAge} ยท last exit {LAST_EXIT} ({durationSec}s)
|
|
389
|
-
โฐ Next tick in 270s ยท Stop: /gsd-t-unattended-stop
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
Progress bar rendering (8 chars wide):
|
|
393
|
-
- If `TASKS_TOTAL > 0`: filled = round(TASKS_DONE / TASKS_TOTAL * 8), empty = 8 - filled. Use `โ` for filled, `โ` for empty.
|
|
394
|
-
- If `TASKS_TOTAL == 0`: show `[โโโโโโโโ]` (pre-partition, no tasks yet).
|
|
395
|
-
|
|
396
|
-
Domain summary formatting:
|
|
397
|
-
- Each domain: `{name}: {done}/{total}` separated by ` | `
|
|
398
|
-
- If domains > 6, show top 3 incomplete + "... +{N} more"
|
|
399
|
-
- If no domains exist, show phase-appropriate message: "Partitioning..." / "Planning..." / "No domains yet"
|
|
400
|
-
|
|
401
|
-
### 6b: Event-Stream Activity Block (M38)
|
|
402
|
-
|
|
403
|
-
After the workflow header, render the structured activity log by reading
|
|
404
|
-
`.gsd-t/events/YYYY-MM-DD.jsonl` since the last cursor (`.gsd-t/.unattended/event-cursor`).
|
|
405
|
-
The formatter is pure and lives at `bin/unattended-watch-format.cjs` โ invoke it via `node -e`:
|
|
406
|
-
|
|
407
|
-
```bash
|
|
408
|
-
node -e "
|
|
409
|
-
const es = require('./bin/event-stream.cjs');
|
|
410
|
-
const wf = require('./bin/unattended-watch-format.cjs');
|
|
411
|
-
const fs = require('fs');
|
|
412
|
-
let state = {};
|
|
413
|
-
try { state = JSON.parse(fs.readFileSync('.gsd-t/.unattended/state.json','utf8')); } catch(_){}
|
|
414
|
-
const { events, newCursor } = es.readSinceCursor('.');
|
|
415
|
-
console.log(wf.formatWatchTick({ events, state, withWatchProgress: true }));
|
|
416
|
-
es.advanceCursor('.', newCursor);
|
|
417
|
-
"
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
Output shape (per contract ยง4):
|
|
421
|
-
|
|
422
|
-
```
|
|
423
|
-
[unattended supervisor โ iter 14, +9m elapsed]
|
|
424
|
-
โถ task: m38-h1-T3 (wave wave-1)
|
|
425
|
-
๐ 6 files modified (commands/gsd-t-execute.md, ...)
|
|
426
|
-
โ
test_result: unit 1228/1228 pass
|
|
427
|
-
โ
subagent_verdict: qa pass (0 findings)
|
|
428
|
-
โฑ duration: 8m 12s ยท verdict: pass
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Zero-event window output: `[unattended supervisor โ iter N, +Tm elapsed] (no new activity since last tick)`.
|
|
432
|
-
|
|
433
|
-
The formatter applies the emoji-spacing rule itself; do not add post-processing.
|
|
434
|
-
|
|
435
|
-
## Step 7: Reschedule via ScheduleWakeup (Non-Terminal Only)
|
|
436
|
-
|
|
437
|
-
**This step runs ONLY when Step 6 rendered a live watch block.** All terminal branches (Steps 3, 4, 5) must NOT reach this step โ they STOP.
|
|
438
|
-
|
|
439
|
-
Call the harness `ScheduleWakeup` tool with these exact parameters:
|
|
440
|
-
|
|
441
|
-
- `delaySeconds`: `270` (fixed โ inside the 5-minute prompt-cache TTL)
|
|
442
|
-
- `prompt`: `/gsd-t-unattended-watch`
|
|
443
|
-
- `reason`: `unattended tick {ITER}` โ substituting the integer `ITER` from Step 2
|
|
444
|
-
|
|
445
|
-
Tool invocation pattern (make this real tool call, not a bash command):
|
|
446
|
-
|
|
447
|
-
```
|
|
448
|
-
ScheduleWakeup(
|
|
449
|
-
delaySeconds: 270,
|
|
450
|
-
prompt: "/gsd-t-unattended-watch",
|
|
451
|
-
reason: "unattended tick {ITER}"
|
|
452
|
-
)
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
After the tool call, end the turn. Do NOT output a "Next Up" hint, do NOT continue with further steps, do NOT summarize. The watch block from Step 6 plus the ScheduleWakeup tool call IS the entire turn output.
|
|
456
|
-
|
|
457
|
-
## Notes
|
|
458
|
-
|
|
459
|
-
- **Stateless**: every tick re-reads state from disk. No memory between firings.
|
|
460
|
-
- **Terminal = STOP**: any terminal branch (3, 4, 5) ends the loop. The user relaunches via `/gsd-t-unattended` if needed.
|
|
461
|
-
- **Never spawn subagents**: this is a pure polling command โ no Task, no TeamCreate, no observability logging block.
|
|
462
|
-
- **No branch guard, no pre-commit gate, no doc ripple**: this command does not modify any files.
|
|
463
|
-
- **Stale-tick tolerance**: `IS_STALE=true` iff `tickAgeMs > 540000` (computed deterministically in Step 2). When stale, warn soft (`โ ๏ธ stale`) but still reschedule โ the supervisor may be mid-worker. The threshold math lives in Step 2's node block; rendering reads the flag verbatim.
|
|
464
|
-
|
|
465
|
-
$ARGUMENTS
|