@tekyzinc/gsd-t 3.10.16 → 3.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.11.10] - 2026-04-16
6
+
7
+ ### Added — Universal Context Auto-Pause (M37)
8
+
9
+ **Background**: The Context Meter (M34) correctly measures context window usage and emits an `additionalContext` signal at the configured threshold (default 75%). However, Claude consistently ignores the single-line suggestion format, continuing to work until hitting the runtime's ~95% `/compact` wall — which destroys context silently and loses work.
10
+
11
+ ### Changed
12
+ - **`scripts/context-meter/threshold.js`** — `buildAdditionalContext()` now returns a 6-line MANDATORY STOP instruction instead of a single-line suggestion. The message starts with `🛑 MANDATORY STOP` and includes step-by-step instructions (pause → clear → resume) with explicit reference to the Destructive Action Guard enforcement weight.
13
+ - **`.gsd-t/contracts/context-meter-contract.md`** — bumped to v1.2.0. New §"Universal Auto-Pause Rule" documents the mandatory behavioral requirement. Rule #8 added: `additionalContext` is a MANDATORY STOP signal with Destructive Action Guard enforcement weight.
14
+ - **`templates/CLAUDE-global.md`** — new `## Universal Auto-Pause Rule (MANDATORY)` section added between Context Meter and API Documentation Guard sections. Same enforcement weight as the Destructive Action Guard.
15
+ - **5 loop command files** (`gsd-t-execute`, `gsd-t-wave`, `gsd-t-integrate`, `gsd-t-quick`, `gsd-t-debug`) — Step 0.2 added: Universal Auto-Pause Rule enforcement. If `🛑 MANDATORY STOP` appears in `additionalContext` at any point, immediately halt, pause, and instruct clear+resume.
16
+ - **Tests**: All 1228 tests pass (1224 unit + 4 e2e). `threshold.test.js` and `gsd-t-context-meter.e2e.test.js` updated for new multi-line format.
17
+
5
18
  ## [3.10.16] - 2026-04-15
6
19
 
7
20
  ### Fixed — unattended supervisor launch friction (3 bugs + UX improvements)
package/README.md CHANGED
@@ -338,7 +338,7 @@ gsd-t unattended --hours=24
338
338
  **How it works:**
339
339
 
340
340
  - `gsd-t unattended` spawns `bin/gsd-t-unattended.js` as a fully detached OS process. The supervisor runs `claude -p` workers in a relay — one worker per iteration — each in a fresh context window. State is written atomically to `.gsd-t/.unattended/state.json` between iterations.
341
- - `/user:gsd-t-unattended` does the same from inside Claude Code, then calls `ScheduleWakeup(270, '/user:gsd-t-unattended-watch')` to start an in-session watch loop that ticks every 270 seconds and prints progress.
341
+ - `/user:gsd-t-unattended` does the same from inside Claude Code, then calls `ScheduleWakeup(270, '/gsd-t-unattended-watch')` to start an in-session watch loop that ticks every 270 seconds and prints progress.
342
342
  - If you run `/clear` + `/user:gsd-t-resume` during a live run, the resume command auto-detects the running supervisor and re-attaches the watch loop — no re-launch needed.
343
343
  - The supervisor halts automatically when: the milestone reaches COMPLETED status, the `--hours` wall-clock cap expires, `--max-iterations` is reached, safety rails detect a stall or unrecoverable error, or the stop sentinel is touched.
344
344
 
@@ -178,8 +178,8 @@ function spawnWorker(projectDir, timeoutMs, opts = {}) {
178
178
  * state file, and relays `claude -p` workers until the milestone terminates.
179
179
  *
180
180
  * Spawn recipe:
181
- * - `node {binPath} {...args}` — binPath should be the absolute path to
182
- * `bin/gsd-t-unattended.cjs` (the supervisor entry point), NOT gsd-t.js.
181
+ * - `node {binPath} unattended {...args}` — the `unattended` subcommand is
182
+ * prepended automatically so callers pass only user-facing args.
183
183
  * - `detached: true` — the child becomes a process-group leader on POSIX
184
184
  * (darwin/linux) so it survives the parent closing its terminal. On win32
185
185
  * the equivalent flag produces a separate process tree.
@@ -194,13 +194,13 @@ function spawnWorker(projectDir, timeoutMs, opts = {}) {
194
194
  * `docs/unattended-windows-caveats.md` (Task 3).
195
195
  *
196
196
  * @param {object} params
197
- * @param {string} params.binPath Absolute path to `bin/gsd-t-unattended.cjs`.
198
- * @param {string[]} params.args CLI args passed directly to the supervisor.
197
+ * @param {string} params.binPath Absolute path to `bin/gsd-t.js`.
198
+ * @param {string[]} params.args Extra args appended after `unattended`.
199
199
  * @param {string} params.cwd Project directory (supervisor's cwd).
200
200
  * @returns {{ pid: number }} The detached child's PID.
201
201
  */
202
202
  function spawnSupervisor({ binPath, args, cwd }) {
203
- const spawnArgs = [binPath, ...(args || [])];
203
+ const spawnArgs = [binPath, "unattended", ...(args || [])];
204
204
  const opts = {
205
205
  cwd,
206
206
  detached: true,
@@ -151,27 +151,6 @@ function loadConfig(projectDir) {
151
151
  return merged;
152
152
  }
153
153
 
154
- // ── saveConfig ─────────────────────────────────────────────────────────────
155
- //
156
- // Persists the given config object back to `.gsd-t/.unattended/config.json`.
157
- // Creates the directory if missing. Used by auto-whitelist to remember newly
158
- // whitelisted dirty-tree entries so subsequent launches don't re-warn.
159
-
160
- function saveConfig(projectDir, config) {
161
- const configDir = path.join(projectDir, ".gsd-t", ".unattended");
162
- const configPath = path.join(configDir, "config.json");
163
- if (!fs.existsSync(configDir)) {
164
- fs.mkdirSync(configDir, { recursive: true });
165
- }
166
- const serializable = {};
167
- for (const key of Object.keys(DEFAULTS)) {
168
- if (config[key] !== undefined) {
169
- serializable[key] = config[key];
170
- }
171
- }
172
- fs.writeFileSync(configPath, JSON.stringify(serializable, null, 2) + "\n");
173
- }
174
-
175
154
  // ── checkGitBranch ──────────────────────────────────────────────────────────
176
155
  //
177
156
  // Runs `git branch --show-current` in projectDir. An empty result indicates
@@ -766,7 +745,6 @@ function detectBlockerSentinel(runLogTail) {
766
745
  module.exports = {
767
746
  DEFAULTS,
768
747
  loadConfig,
769
- saveConfig,
770
748
  checkGitBranch,
771
749
  checkWorktreeCleanliness,
772
750
  checkIterationCap,
@@ -35,7 +35,6 @@ const { mapHeadlessExitCode } = require("./gsd-t.js");
35
35
  const {
36
36
  DEFAULTS: SAFETY_DEFAULTS,
37
37
  loadConfig,
38
- saveConfig,
39
38
  checkGitBranch,
40
39
  checkWorktreeCleanliness,
41
40
  checkIterationCap,
@@ -494,7 +493,6 @@ function doUnattended(argv, deps) {
494
493
  releaseSleep: deps._releaseSleep || releaseSleep,
495
494
  notify: deps._notify || notify,
496
495
  loadConfig: deps._loadConfig || loadConfig,
497
- saveConfig: deps._saveConfig || saveConfig,
498
496
  };
499
497
 
500
498
  // ── Load config (optional .gsd-t/.unattended/config.json) ────────────────
@@ -551,23 +549,7 @@ function doUnattended(argv, deps) {
551
549
  };
552
550
  }
553
551
  const treeRes = fn.checkWorktreeCleanliness(projectDir, config);
554
- if (!treeRes.ok && treeRes.dirtyFiles && treeRes.dirtyFiles.length > 0) {
555
- // Auto-whitelist: add the dirty files to the config and persist, then
556
- // proceed instead of refusing. This keeps unattended launch frictionless.
557
- for (const file of treeRes.dirtyFiles) {
558
- if (!config.dirtyTreeWhitelist.includes(file)) {
559
- config.dirtyTreeWhitelist.push(file);
560
- }
561
- }
562
- try {
563
- fn.saveConfig(projectDir, config);
564
- } catch (_) { /* best effort — config dir may not exist yet */ }
565
- // eslint-disable-next-line no-console
566
- console.error(
567
- `[gsd-t-unattended] auto-whitelisted ${treeRes.dirtyFiles.length} dirty file(s): ${treeRes.dirtyFiles.slice(0, 5).join(", ")}${treeRes.dirtyFiles.length > 5 ? ", …" : ""}`,
568
- );
569
- } else if (!treeRes.ok) {
570
- // Non-file failure (e.g. git error) — still refuse.
552
+ if (!treeRes.ok) {
571
553
  // eslint-disable-next-line no-console
572
554
  console.error(
573
555
  `[gsd-t-unattended] preflight-refusal: ${treeRes.reason || "dirty worktree"}`,
@@ -1005,9 +987,15 @@ function runMainLoop(state, dir, opts, deps, ctx) {
1005
987
  */
1006
988
  function _spawnWorker(state, opts) {
1007
989
  const bin = (state && state.claudeBin) || resolveClaudePath();
990
+ const workerEnv = { ...process.env, GSD_T_UNATTENDED_WORKER: "1" };
1008
991
  const res = platformSpawnWorker(opts.cwd, opts.timeout, {
1009
992
  bin,
1010
- args: ["-p", "/gsd-t-resume"],
993
+ args: [
994
+ "-p",
995
+ "You are an unattended worker iteration. CRITICAL: Do NOT check supervisor.pid, do NOT auto-reattach to a watch loop, do NOT schedule any ScheduleWakeup. You ARE the worker spawned by the supervisor. Skip Step 0 (auto-reattach) entirely and go directly to Step 0.1. Run /gsd-t-resume but skip the unattended supervisor auto-reattach check in Step 0.",
996
+ "--dangerously-skip-permissions",
997
+ ],
998
+ env: workerEnv,
1011
999
  });
1012
1000
  return {
1013
1001
  status: typeof res.status === "number" ? res.status : null,
package/bin/gsd-t.js CHANGED
@@ -1232,9 +1232,6 @@ async function doInstall(opts = {}) {
1232
1232
  saveInstalledVersion();
1233
1233
 
1234
1234
  showInstallSummary(gsdtCommands.length, utilityCommands.length);
1235
-
1236
- // Interactive prompt (skipped silently in non-TTY shells)
1237
- await promptForApiKeyIfMissing(resolveApiKeyEnvVar(process.cwd()));
1238
1235
  }
1239
1236
 
1240
1237
  function showInstallSummary(gsdtCount, utilCount) {
@@ -1444,9 +1441,6 @@ async function doInit(projectName) {
1444
1441
  if (registerProject(projectDir)) success("Registered in ~/.claude/.gsd-t-projects");
1445
1442
 
1446
1443
  showInitTree(projectDir);
1447
-
1448
- // Interactive prompt (skipped silently in non-TTY shells)
1449
- await promptForApiKeyIfMissing(resolveApiKeyEnvVar(projectDir));
1450
1444
  }
1451
1445
 
1452
1446
  function showInitTree(projectDir) {
@@ -1523,12 +1517,7 @@ function showStatusContextMeter() {
1523
1517
  const rel = state.timestamp ? formatRelativeTime(state.timestamp) : "never measured";
1524
1518
  log(` ${RED}${BOLD}✗ CONTEXT METER DEAD${RESET} ${RED}— error: ${code}, last check: ${rel}${RESET}`);
1525
1519
  log(` ${RED}The context-window guardrail is NOT working. Long sessions will hit /compact.${RESET}`);
1526
- if (code === "missing_key") {
1527
- log(` ${YELLOW}Fix: export ANTHROPIC_API_KEY in your shell profile${RESET}`);
1528
- log(` ${YELLOW} (measurement only — inference stays on Claude Code subscription)${RESET}`);
1529
- } else {
1530
- log(` ${YELLOW}Fix: run 'gsd-t doctor' for diagnostics${RESET}`);
1531
- }
1520
+ log(` ${YELLOW}Fix: run 'gsd-t doctor' for diagnostics${RESET}`);
1532
1521
  return;
1533
1522
  }
1534
1523
 
@@ -2308,8 +2297,8 @@ function checkDoctorCgc() {
2308
2297
  return issues;
2309
2298
  }
2310
2299
 
2311
- // Verify context meter wiring: API key env var, hook registration,
2312
- // hook script presence, config validity, and a live count_tokens dry-run.
2300
+ // Verify context meter wiring: hook registration, hook script presence,
2301
+ // config validity, and a local estimation dry-run.
2313
2302
  // Returns number of issues (RED results). Mirrors checkDoctorCgc shape.
2314
2303
  async function checkDoctorContextMeter(projectDir) {
2315
2304
  let issues = 0;
@@ -2317,8 +2306,8 @@ async function checkDoctorContextMeter(projectDir) {
2317
2306
 
2318
2307
  const cwd = projectDir || process.cwd();
2319
2308
 
2320
- // Load config (used by checks 1, 4, and 5). Missing file → defaults; invalid
2321
- // JSON or schema-mismatch → throws (handled in Check 4).
2309
+ // Load config (used by checks 3 and 4). Missing file → defaults; invalid
2310
+ // JSON or schema-mismatch → throws (handled in Check 3).
2322
2311
  let cfg = null;
2323
2312
  let cfgLoadErr = null;
2324
2313
  try {
@@ -2327,19 +2316,8 @@ async function checkDoctorContextMeter(projectDir) {
2327
2316
  } catch (e) {
2328
2317
  cfgLoadErr = e;
2329
2318
  }
2330
- const apiKeyEnvVar = (cfg && cfg.apiKeyEnvVar) || "ANTHROPIC_API_KEY";
2331
-
2332
- // Check 1: API key env var present
2333
- const apiKeyValue = process.env[apiKeyEnvVar];
2334
- const apiKeyPresent = typeof apiKeyValue === "string" && apiKeyValue.length > 0;
2335
- if (apiKeyPresent) {
2336
- success(`API key present ($${apiKeyEnvVar})`);
2337
- } else {
2338
- error(`Missing API key: set $${apiKeyEnvVar} — https://console.anthropic.com/settings/keys`);
2339
- issues++;
2340
- }
2341
2319
 
2342
- // Check 2: Hook registered in ~/.claude/settings.json
2320
+ // Check 1: Hook registered in ~/.claude/settings.json
2343
2321
  let hookRegistered = false;
2344
2322
  try {
2345
2323
  if (fs.existsSync(SETTINGS_JSON)) {
@@ -2367,7 +2345,7 @@ async function checkDoctorContextMeter(projectDir) {
2367
2345
  issues++;
2368
2346
  }
2369
2347
 
2370
- // Check 3: Hook script file exists in project
2348
+ // Check 2: Hook script file exists in project
2371
2349
  const scriptPath = path.join(cwd, "scripts", CONTEXT_METER_SCRIPT);
2372
2350
  if (fs.existsSync(scriptPath)) {
2373
2351
  success("Hook script present");
@@ -2376,7 +2354,7 @@ async function checkDoctorContextMeter(projectDir) {
2376
2354
  issues++;
2377
2355
  }
2378
2356
 
2379
- // Check 4: Config file parses via loader
2357
+ // Check 3: Config file parses via loader
2380
2358
  const configPath = path.join(cwd, CONTEXT_METER_CONFIG_DEST);
2381
2359
  if (cfgLoadErr) {
2382
2360
  error(`Config file invalid: ${cfgLoadErr.message} — fix ${CONTEXT_METER_CONFIG_DEST}`);
@@ -2387,34 +2365,27 @@ async function checkDoctorContextMeter(projectDir) {
2387
2365
  warn("Using default config — run gsd-t install to copy template");
2388
2366
  }
2389
2367
 
2390
- // Check 5: Dry-run count_tokens API call (skip if no API key)
2391
- if (!apiKeyPresent) {
2392
- log(` ${DIM}Skipped count_tokens dry-run (no API key)${RESET}`);
2368
+ // Check 4: Dry-run local token estimation
2369
+ const estimatorPath = path.join(cwd, "scripts", "context-meter", "estimate-tokens.js");
2370
+ if (!fs.existsSync(estimatorPath)) {
2371
+ error("Token estimator missing at scripts/context-meter/estimate-tokens.js — run gsd-t update");
2372
+ issues++;
2393
2373
  } else {
2394
- const clientPath = path.join(cwd, "scripts", "context-meter", "count-tokens-client.js");
2395
- if (!fs.existsSync(clientPath)) {
2396
- error("count_tokens client missing at scripts/context-meter/count-tokens-client.js — run gsd-t update");
2397
- issues++;
2398
- } else {
2399
- try {
2400
- const { countTokens } = require(clientPath);
2401
- const result = await countTokens({
2402
- apiKey: apiKeyValue,
2403
- model: "claude-opus-4-6",
2404
- system: "",
2405
- messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
2406
- timeoutMs: 5000,
2407
- });
2408
- if (result && typeof result.inputTokens === "number") {
2409
- success(`count_tokens dry-run OK (${result.inputTokens} tokens)`);
2410
- } else {
2411
- error("count_tokens API call failed — check API key and network");
2412
- issues++;
2413
- }
2414
- } catch (e) {
2415
- error(`count_tokens dry-run threw: ${e.message}`);
2374
+ try {
2375
+ const { estimateTokens } = require(estimatorPath);
2376
+ const result = estimateTokens({
2377
+ system: "",
2378
+ messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
2379
+ });
2380
+ if (result && typeof result.inputTokens === "number") {
2381
+ success(`Token estimator dry-run OK (${result.inputTokens} tokens)`);
2382
+ } else {
2383
+ error("Token estimator returned null");
2416
2384
  issues++;
2417
2385
  }
2386
+ } catch (e) {
2387
+ error(`Token estimator dry-run threw: ${e.message}`);
2388
+ issues++;
2418
2389
  }
2419
2390
  }
2420
2391
 
@@ -65,6 +65,16 @@ If `can_start === false`, the headless continuation has been spawned and the int
65
65
 
66
66
  **Contract**: `.gsd-t/contracts/runway-estimator-contract.md` v1.0.0; stop threshold (85%) mirrors `.gsd-t/contracts/token-budget-contract.md` v3.0.0.
67
67
 
68
+ ## Step 0.2: Universal Auto-Pause Rule (MANDATORY — context-meter-contract v1.2.0, M37)
69
+
70
+ **If at ANY point during this command you see a `🛑 MANDATORY STOP` message in `additionalContext` (delivered by the Context Meter PostToolUse hook), you MUST:**
71
+ 1. Immediately stop all work — do NOT continue debugging, do NOT spawn the next subagent
72
+ 2. Run `/user:gsd-t-pause` to save exact position
73
+ 3. Tell the user to run `/clear` then `/user:gsd-t-resume`
74
+ 4. STOP — do not continue
75
+
76
+ This has the same enforcement weight as the Destructive Action Guard. The signal means the context window is at or above the configured threshold (default 75%) and continuing risks hitting the runtime's ~95% `/compact` wall.
77
+
68
78
  ## Step 0.1: Launch via Subagent
69
79
 
70
80
  To give this debug session a fresh context window and prevent compaction, always execute via a Task subagent.
@@ -83,6 +83,16 @@ Use the returned `threshold` as the gate signal for the rest of this run. The ga
83
83
 
84
84
  Why: every `/user:gsd-t-execute` invocation is a fresh orchestrator session and needs a current reading of context utilization before spawning any subagents. The authoritative source is the Context Meter state file; the fallback keeps the gate functional on projects that haven't installed the hook yet.
85
85
 
86
+ ## Step 0.2: Universal Auto-Pause Rule (MANDATORY — context-meter-contract v1.2.0, M37)
87
+
88
+ **If at ANY point during this command you see a `🛑 MANDATORY STOP` message in `additionalContext` (delivered by the Context Meter PostToolUse hook), you MUST:**
89
+ 1. Immediately stop all work — do NOT finish the current task, do NOT spawn the next subagent
90
+ 2. Run `/user:gsd-t-pause` to save exact position
91
+ 3. Tell the user to run `/clear` then `/user:gsd-t-resume`
92
+ 4. STOP — do not continue
93
+
94
+ This has the same enforcement weight as the Destructive Action Guard. The signal means the context window is at or above the configured threshold (default 75%) and continuing risks hitting the runtime's ~95% `/compact` wall.
95
+
86
96
  ## Step 1: Load State
87
97
 
88
98
  Read:
@@ -65,6 +65,16 @@ If `can_start === false`, the headless continuation has been spawned and the int
65
65
 
66
66
  **Contract**: `.gsd-t/contracts/runway-estimator-contract.md` v1.0.0; stop threshold (85%) mirrors `.gsd-t/contracts/token-budget-contract.md` v3.0.0.
67
67
 
68
+ ## Step 0.2: Universal Auto-Pause Rule (MANDATORY — context-meter-contract v1.2.0, M37)
69
+
70
+ **If at ANY point during this command you see a `🛑 MANDATORY STOP` message in `additionalContext` (delivered by the Context Meter PostToolUse hook), you MUST:**
71
+ 1. Immediately stop all work — do NOT continue integration, do NOT spawn the next subagent
72
+ 2. Run `/user:gsd-t-pause` to save exact position
73
+ 3. Tell the user to run `/clear` then `/user:gsd-t-resume`
74
+ 4. STOP — do not continue
75
+
76
+ This has the same enforcement weight as the Destructive Action Guard. The signal means the context window is at or above the configured threshold (default 75%) and continuing risks hitting the runtime's ~95% `/compact` wall.
77
+
68
78
  ## Step 1: Load Full State
69
79
 
70
80
  Read everything:
@@ -65,6 +65,16 @@ If `can_start === false`, the headless continuation has been spawned and the int
65
65
 
66
66
  **Contract**: `.gsd-t/contracts/runway-estimator-contract.md` v1.0.0; stop threshold (85%) mirrors `.gsd-t/contracts/token-budget-contract.md` v3.0.0.
67
67
 
68
+ ## Step 0.2: Universal Auto-Pause Rule (MANDATORY — context-meter-contract v1.2.0, M37)
69
+
70
+ **If at ANY point during this command you see a `🛑 MANDATORY STOP` message in `additionalContext` (delivered by the Context Meter PostToolUse hook), you MUST:**
71
+ 1. Immediately stop all work — do NOT continue the task, do NOT spawn the next subagent
72
+ 2. Run `/user:gsd-t-pause` to save exact position
73
+ 3. Tell the user to run `/clear` then `/user:gsd-t-resume`
74
+ 4. STOP — do not continue
75
+
76
+ This has the same enforcement weight as the Destructive Action Guard. The signal means the context window is at or above the configured threshold (default 75%) and continuing risks hitting the runtime's ~95% `/compact` wall.
77
+
68
78
  ## Step 0.1: Launch via Subagent
69
79
 
70
80
  To give this task a fresh context window and prevent compaction during consecutive quick runs, always execute via a Task subagent.
@@ -6,6 +6,8 @@ You are resuming work after an interruption. This handles both same-session paus
6
6
 
7
7
  **This step runs FIRST, before reading any docs, contracts, or continue-here files.**
8
8
 
9
+ **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.
10
+
9
11
  Check whether an unattended supervisor is actively running for this project:
10
12
 
11
13
  1. Check if `.gsd-t/.unattended/supervisor.pid` exists.
@@ -22,7 +24,7 @@ Check whether an unattended supervisor is actively running for this project:
22
24
  - **Terminal status** (`done`, `failed`, `stopped`, `crashed`) → the supervisor has finished and is waiting for cleanup. Fall through to Step 0.1 so normal resume flow runs (it will see progress.md state and continue from where the supervisor left off).
23
25
  - **Non-terminal status** (`initializing`, `running`, or any unrecognized value) → **AUTO-REATTACH**:
24
26
  - Print the current watch status using the data in `state.json` (elapsed time, current iteration, milestone/wave/task, last worker exit code).
25
- - Call `ScheduleWakeup(270, '/user:gsd-t-unattended-watch', reason='resumed watch')`.
27
+ - Call `ScheduleWakeup(270, '/gsd-t-unattended-watch', reason='resumed watch')`.
26
28
  - **STOP reading resume.md entirely. Do NOT proceed to Step 0.1 or any later step. Do NOT read docs, contracts, or continue-here files. Do NOT display a headless read-back banner.** The watcher will display the live status block and re-schedule itself. Return now.
27
29
 
28
30
  Contract reference: `unattended-supervisor-contract.md` §9 (Resume Auto-Reattach Handshake)
@@ -66,14 +66,16 @@ This is race-free, terminal-close-safe, and language-agnostic (per contract §10
66
66
  Output the confirmation block:
67
67
 
68
68
  ```
69
- 🛑 Stop sentinel written. Supervisor will halt between next worker iterations (within ~5 minutes). State file will finalize with status=stopped.
69
+ 🛑 Stop sentinel written. Supervisor will halt after the current worker finishes (up to ~5 min).
70
70
 
71
71
  Session: {SESSION from Step 3}
72
72
  Iter: {ITER from Step 3}
73
73
  Status: {STATUS from Step 3}
74
74
 
75
- The current worker will run to completion (up to ~1h). Stop is honored at the next pre-worker checkpoint.
76
- No kill signal is sent this is a clean cooperative halt.
75
+ The current worker runs to completion. Stop is honored at the next pre-worker checkpoint.
76
+ The watch loop (if running) will detect status=stopped on its next tick and stop itself.
77
+ If a watch tick fires before the supervisor processes the sentinel, it may reschedule once
78
+ more — this is expected. It will catch the terminal status on the following tick.
77
79
  ```
78
80
 
79
81
  ## Step 6: Return Immediately
@@ -20,7 +20,7 @@ See `unattended-supervisor-contract.md` §8 (Watch Tick Decision Tree), §4 (Sta
20
20
  - failed → failure summary, STOP
21
21
  - stopped → user-stop confirm, STOP
22
22
  - initializing | running → render watch block, go to 4
23
- 4. ScheduleWakeup(270, '/user:gsd-t-unattended-watch', reason='unattended tick {iter}')
23
+ 4. ScheduleWakeup(270, '/gsd-t-unattended-watch', reason='unattended tick {iter}')
24
24
  ```
25
25
 
26
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.
@@ -126,6 +126,72 @@ const last50 = tailLines.slice(-50);
126
126
  const last1 = tailLines.length > 0 ? tailLines[tailLines.length - 1] : '';
127
127
  out('LOG_TAIL_LAST1', last1);
128
128
  out('LOG_TAIL_LAST50', last50.join('\n'));
129
+
130
+ // --- workflow progress from progress.md and domain task files ---
131
+ let phase = 'unknown';
132
+ let waveInfo = '';
133
+ let waveCurrent = 0;
134
+ let waveTotal = 0;
135
+ let tasksDone = 0;
136
+ let tasksTotal = 0;
137
+ let domainSummary = [];
138
+ try {
139
+ const prog = fs.readFileSync('.gsd-t/progress.md', 'utf8');
140
+ // Extract phase from Status line
141
+ const statusM = prog.match(/^## Status:.*?(\b(?:DEFINED|PARTITIONED|PLANNED|EXECUTING|EXECUTED|INTEGRATING|VERIFYING|VERIFIED|COMPLETE|IN.PROGRESS)\b)/mi);
142
+ if (statusM) phase = statusM[1].trim();
143
+ // Extract "Wave N of M" or latest "Wave N" from Decision Log
144
+ const waveOfM = prog.match(/Wave\s+(\d+)\s+(?:of|\/)\s+(\d+)/gi);
145
+ if (waveOfM && waveOfM.length > 0) {
146
+ const last = waveOfM[waveOfM.length - 1];
147
+ const parts = last.match(/(\d+)\s+(?:of|\/)\s+(\d+)/);
148
+ if (parts) { waveCurrent = parseInt(parts[1], 10); waveTotal = parseInt(parts[2], 10); }
149
+ } else {
150
+ const waveM = prog.match(/Wave\s+(\d+)/gi);
151
+ if (waveM && waveM.length > 0) {
152
+ const last = waveM[waveM.length - 1].match(/(\d+)/);
153
+ if (last) waveCurrent = parseInt(last[1], 10);
154
+ }
155
+ }
156
+ } catch (_) {}
157
+
158
+ // Read domain task files for completion counts and wave totals
159
+ try {
160
+ const domainsDir = '.gsd-t/domains';
161
+ if (fs.existsSync(domainsDir)) {
162
+ const domains = fs.readdirSync(domainsDir).filter(d => {
163
+ try { return fs.statSync(path.join(domainsDir, d)).isDirectory(); } catch (_) { return false; }
164
+ });
165
+ let maxWave = 0;
166
+ for (const d of domains) {
167
+ const tasksFile = path.join(domainsDir, d, 'tasks.md');
168
+ if (!fs.existsSync(tasksFile)) continue;
169
+ const txt = fs.readFileSync(tasksFile, 'utf8');
170
+ const lines = txt.split('\n');
171
+ let done = 0, total = 0;
172
+ for (const line of lines) {
173
+ // Count wave headers to determine total waves
174
+ const wH = line.match(/^#+\s*Wave\s+(\d+)/i);
175
+ if (wH) { const w = parseInt(wH[1], 10); if (w > maxWave) maxWave = w; }
176
+ if (/^[-*]\s*\[x\]/i.test(line)) { done++; total++; }
177
+ else if (/^[-*]\s*\[\s?\]/.test(line)) { total++; }
178
+ }
179
+ if (total > 0) {
180
+ tasksDone += done;
181
+ tasksTotal += total;
182
+ domainSummary.push(d + ':' + done + '/' + total);
183
+ }
184
+ }
185
+ if (maxWave > 0 && waveTotal === 0) waveTotal = maxWave;
186
+ }
187
+ } catch (_) {}
188
+ if (waveCurrent > 0 && waveTotal > 0) waveInfo = 'Wave ' + waveCurrent + ' of ' + waveTotal;
189
+ else if (waveCurrent > 0) waveInfo = 'Wave ' + waveCurrent;
190
+ out('PHASE', phase);
191
+ out('WAVE_INFO', waveInfo);
192
+ out('TASKS_DONE', tasksDone);
193
+ out('TASKS_TOTAL', tasksTotal);
194
+ out('DOMAIN_SUMMARY', domainSummary.join(' | '));
129
195
  "
130
196
  ```
131
197
 
@@ -239,23 +305,31 @@ The supervisor is still alive but has transitioned to a terminal status on its l
239
305
 
240
306
  If `PID_FILE_EXISTS=true` AND `ALIVE=true` AND `STATUS` is `initializing` or `running`:
241
307
 
242
- Render the compact watch block below. Format rules:
308
+ Render the enriched watch block below. The key insight: users need **workflow progress** (phase, wave, tasks done/remaining), not just supervisor mechanics (iter, PID, exit code).
309
+
310
+ Format rules:
243
311
  - One extra space after each emoji (per CLAUDE.md markdown table rules — preserves alignment in terminal views).
244
312
  - Elapsed formatted as `{H}h{M}m` from `ELAPSED_MS`.
245
- - Last-tick age formatted as `{S}s` or `{M}m{S}s`. If age > 540s (2× tick cadence), append ` ⚠️ stale` as a soft warning (not a crash — per contract §3 write semantics).
246
- - `LAST_EXIT` duration from `LAST_ELAPSED_MS` rendered as seconds.
247
- - Wave/task lines are omitted when absent from state.
248
- - Last non-empty `run.log` line is truncated to 80 chars.
313
+ - Last-tick age formatted as `{S}s` or `{M}m{S}s`. If age > 540s (2× tick cadence), append ` ⚠️ stale` as a soft warning.
314
+ - Tasks progress bar: `[████████░░░░] 8/12` filled blocks proportional to done/total.
315
+ - Domain breakdown only shown if ≤6 domains (otherwise too noisy).
249
316
 
250
317
  ```
251
- ⚙ Unattended — {MILESTONE}{ Wave {WAVE}}{ · Task {TASK}}
252
- Iter {ITER} / {MAX_ITER} · elapsed {Hh Mm} · last tick {tickAge}
253
- 📊 Last exit: {LAST_EXIT} ({durationSec}s) · PID {PID} · session {SESSION}
254
- 📝 {truncated last log line or "(no output yet)"}
318
+ ⚙ Unattended — {MILESTONE} · {PHASE}{ · {WAVE_INFO}}
319
+ 📋 Tasks: {TASKS_DONE}/{TASKS_TOTAL} [{progress bar}] · elapsed {Hh Mm}
320
+ 🔧 {DOMAIN_SUMMARY or "No domains yet (pre-partition)"}
321
+ Iter {ITER}/{MAX_ITER} · last tick {tickAge} · last exit {LAST_EXIT} ({durationSec}s)
255
322
  ⏰ Next tick in 270s · Stop: /user:gsd-t-unattended-stop
256
323
  ```
257
324
 
258
- (5 lines the contract §8 format allows up to 10. Keep it tight.)
325
+ Progress bar rendering (8 chars wide):
326
+ - If `TASKS_TOTAL > 0`: filled = round(TASKS_DONE / TASKS_TOTAL * 8), empty = 8 - filled. Use `█` for filled, `░` for empty.
327
+ - If `TASKS_TOTAL == 0`: show `[░░░░░░░░]` (pre-partition, no tasks yet).
328
+
329
+ Domain summary formatting:
330
+ - Each domain: `{name}: {done}/{total}` separated by ` | `
331
+ - If domains > 6, show top 3 incomplete + "... +{N} more"
332
+ - If no domains exist, show phase-appropriate message: "Partitioning..." / "Planning..." / "No domains yet"
259
333
 
260
334
  ## Step 7: Reschedule via ScheduleWakeup (Non-Terminal Only)
261
335
 
@@ -264,7 +338,7 @@ Render the compact watch block below. Format rules:
264
338
  Call the harness `ScheduleWakeup` tool with these exact parameters:
265
339
 
266
340
  - `delaySeconds`: `270` (fixed — inside the 5-minute prompt-cache TTL)
267
- - `prompt`: `/user:gsd-t-unattended-watch`
341
+ - `prompt`: `/gsd-t-unattended-watch`
268
342
  - `reason`: `unattended tick {ITER}` — substituting the integer `ITER` from Step 2
269
343
 
270
344
  Tool invocation pattern (make this real tool call, not a bash command):
@@ -272,7 +346,7 @@ Tool invocation pattern (make this real tool call, not a bash command):
272
346
  ```
273
347
  ScheduleWakeup(
274
348
  delaySeconds: 270,
275
- prompt: "/user:gsd-t-unattended-watch",
349
+ prompt: "/gsd-t-unattended-watch",
276
350
  reason: "unattended tick {ITER}"
277
351
  )
278
352
  ```
@@ -424,7 +424,7 @@ Print the launch confirmation block:
424
424
  Call the harness `ScheduleWakeup` tool with these exact parameters:
425
425
 
426
426
  - `delaySeconds`: `270` (fixed — inside the 5-minute prompt-cache TTL)
427
- - `prompt`: `/user:gsd-t-unattended-watch`
427
+ - `prompt`: `/gsd-t-unattended-watch`
428
428
  - `reason`: `first unattended tick`
429
429
 
430
430
  Tool invocation pattern (make this a real tool call, not a bash command):
@@ -432,7 +432,7 @@ Tool invocation pattern (make this a real tool call, not a bash command):
432
432
  ```
433
433
  ScheduleWakeup(
434
434
  delaySeconds: 270,
435
- prompt: "/user:gsd-t-unattended-watch",
435
+ prompt: "/gsd-t-unattended-watch",
436
436
  reason: "first unattended tick"
437
437
  )
438
438
  ```
@@ -74,6 +74,16 @@ node -e "const tb = require('./bin/token-budget.cjs'); const s = tb.getSessionSt
74
74
 
75
75
  This calls `getSessionStatus()` (v2.0.0) which reads `.gsd-t/.context-meter-state.json` produced by the Context Meter PostToolUse hook. The returned `threshold` drives the gate logic in the Phase Agent Spawn Pattern below — it enforces the three-band stop boundary (85%) so the wave orchestrator itself never runs out of context mid-wave. When the state file is absent or stale, the call falls back to a historical heuristic from `.gsd-t/token-log.md`. Band boundaries and `modelWindowSize` are configured in `.gsd-t/context-meter-config.json` and `bin/token-budget.cjs` (THRESHOLDS constant).
76
76
 
77
+ ## Step 0.2: Universal Auto-Pause Rule (MANDATORY — context-meter-contract v1.2.0, M37)
78
+
79
+ **If at ANY point during this command you see a `🛑 MANDATORY STOP` message in `additionalContext` (delivered by the Context Meter PostToolUse hook), you MUST:**
80
+ 1. Immediately stop all work — do NOT advance to the next phase, do NOT spawn the next subagent
81
+ 2. Run `/user:gsd-t-pause` to save exact position
82
+ 3. Tell the user to run `/clear` then `/user:gsd-t-resume`
83
+ 4. STOP — do not continue
84
+
85
+ This has the same enforcement weight as the Destructive Action Guard. The signal means the context window is at or above the configured threshold (default 75%) and continuing risks hitting the runtime's ~95% `/compact` wall.
86
+
77
87
  ## Step 1: Load State (Lightweight)
78
88
 
79
89
  Read ONLY:
@@ -345,7 +345,7 @@ Closes the M35 parent/child race in `bin/headless-auto-spawn.js`. When the runwa
345
345
 
346
346
  ### Resume Auto-Reattach
347
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.
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, '/gsd-t-unattended-watch', ...)`. The user transparently re-enters the watch loop without any manual step.
349
349
 
350
350
  ---
351
351
 
@@ -287,9 +287,9 @@
287
287
 
288
288
  **M36 Functional Requirements:**
289
289
  - **REQ-079**: `bin/gsd-t-unattended.js` implements the supervisor relay loop: spawn worker → await exit → post-worker safety check → next iter. State written atomically to `.gsd-t/.unattended/state.json`. Contract: `unattended-supervisor-contract.md` v1.0.0.
290
- - **REQ-080**: `commands/gsd-t-unattended.md` pre-flights branch + dirty tree, spawns supervisor detached, polls for PID readiness, displays initial watch block, and calls `ScheduleWakeup(270, '/user:gsd-t-unattended-watch')`.
290
+ - **REQ-080**: `commands/gsd-t-unattended.md` pre-flights branch + dirty tree, spawns supervisor detached, polls for PID readiness, displays initial watch block, and calls `ScheduleWakeup(270, '/gsd-t-unattended-watch')`.
291
291
  - **REQ-081**: `commands/gsd-t-unattended-watch.md` implements the watch tick decision tree (§8 of contract): reads PID → liveness probe → reads state.json → reschedule or terminal report.
292
- - **REQ-082**: `commands/gsd-t-resume.md` Step 0 checks `supervisor.pid` before any other resume logic. If live + non-terminal: skip normal resume, print watch block, call `ScheduleWakeup(270, '/user:gsd-t-unattended-watch')`.
292
+ - **REQ-082**: `commands/gsd-t-resume.md` Step 0 checks `supervisor.pid` before any other resume logic. If live + non-terminal: skip normal resume, print watch block, call `ScheduleWakeup(270, '/gsd-t-unattended-watch')`.
293
293
  - **REQ-083**: Supervisor relay architecture ensures each worker gets a fresh context window. No compaction state carries over between workers — only `.gsd-t/` milestone state files.
294
294
  - **REQ-084**: `bin/gsd-t-unattended-safety.js` exports: `checkGitBranch`, `checkWorktreeCleanliness`, `validateState`, `checkIterationCap`, `checkWallClockCap`, `detectBlockerSentinel`, `detectGutter`. Called at all 4 supervisor hook points.
295
295
  - **REQ-085**: `bin/gsd-t-unattended-platform.js` exports: `spawnSupervisor`, `preventSleep`, `releaseSleep`, `notify`, `resolveClaudePath`. Windows: `preventSleep` is a documented no-op. Windows caveats documented in `docs/unattended-windows-caveats.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.10.16",
3
+ "version": "3.11.11",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 61 slash commands with unattended supervisor relay, headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, 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",