@tekyzinc/gsd-t 3.12.15 → 3.13.10

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,38 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.13.10] - 2026-04-17
6
+
7
+ ### Added — M39: Fast Unattended + Universal Watch-Progress Tree
8
+
9
+ Closes the 3–5× speed gap between unattended and in-session execution, adds a universal task-list progress view under every `--watch` surface, and keeps supervisor→worker handoffs inside the 5-minute Anthropic prompt-cache TTL.
10
+
11
+ **D2 — progress-watch (12 tasks)**:
12
+ - `.gsd-t/contracts/watch-progress-contract.md` v1.0.0 — state-file schema, tree-reconstruction algorithm, stale-state expiry (24h), renderer contract, integration invariants.
13
+ - `scripts/gsd-t-watch-state.js` — zero-dep writer CLI with shim-safe agent-id resolution (CLI arg → `GSD_T_AGENT_ID` env → auto-minted `shell-{pid}-{ts}` fallback). Atomic tmp-write+rename; `start`/`advance`/`done`/`skip`/`fail` subcommands.
14
+ - `bin/watch-progress.js` — tree builder (parent_agent_id lineage, orphan handling) + renderer (✅/🔄/⬜/➡️/❌ markers; expanded-current-subtree + collapsed-siblings layout).
15
+ - 189 step-shims across 17 workflow command files — every numbered step now writes its progress state under `.gsd-t/.watch-state/{agent_id}.json`.
16
+ - Integration into `bin/gsd-t-unattended.cjs`, `bin/unattended-watch-format.cjs`, `bin/headless-auto-spawn.cjs` — tree appends below the existing banner (banner preserved intact).
17
+
18
+ **D3 — parallel-exec (4 tasks)**:
19
+ - Team Mode prompt block inserted into `_spawnWorker` at the worker instruction boundary. Unattended worker now spawns up to 15 concurrent `Task` subagents per wave (intra-wave parallel), waits for all, then advances (inter-wave sequential). Falls back to sequential when the wave contains only one domain.
20
+ - `.gsd-t/contracts/unattended-supervisor-contract.md` §15 v1.3.0 — Team Mode contract: cap of 15, dependency-graph preservation, wave-boundary semantics.
21
+
22
+ **D4 — cache-warm-pacing (3 tasks)**:
23
+ - `DEFAULT_WORKER_TIMEOUT_MS = 270000` (270 s) in `bin/gsd-t-unattended.cjs` + `.js`. Preserves the Anthropic 5-min prompt-cache TTL with a ~30 s supervisor→worker handoff budget, eliminating the cold-cache penalty that was adding minutes per iter.
24
+ - `--worker-timeout=<ms>` CLI flag parsed and merged into the live config (was documented in §6 but silently ignored pre-M39).
25
+ - `.gsd-t/contracts/unattended-supervisor-contract.md` §16 v1.3.0 — cache-warm pacing contract: inline rationale comment requirement, inter-iteration sleep invariant (< 5 s), timeout override semantics.
26
+
27
+ **Red Team**: Initial FAIL (2 CRITICAL + 2 HIGH) → fixes → GRUDGING PASS.
28
+ - BUG-1 (CRITICAL): `GSD_T_AGENT_ID` had no producer — 189 shims would silently fail. Fixed by injecting `supervisor-iter-{N}` in `_spawnWorker`, `headless-{id}` in `autoSpawnHeadless`, and adding an auto-mint fallback chain to the writer CLI.
29
+ - BUG-2 (CRITICAL): `--worker-timeout` flag documented in §6 but no `case "worker-timeout":` in `parseArgs`. Fixed with parse case + config merge + test assertion.
30
+ - BUG-3 (HIGH): `.js` and `.cjs` variants of unattended + safety files had divergent defaults (3600000 vs 270000). Fixed by aligning all four files to 270000.
31
+ - BUG-4 (HIGH): Team Mode prompt referenced "Step 4" but the current execute flow uses "Step 3". Fixed in both the prompt string and the contract §15.
32
+
33
+ **Tests**: 1227/1227 pass (+3 new: shim-safe agent-id auto-mint, env-var fallback, `--worker-timeout` flag parse).
34
+
35
+ **Impact**: bee-poc's next supervisor relaunch on v3.13.10 should complete iters 3–5× faster than the v3.12.13 baseline, with visible task-list progression under every `--watch` surface.
36
+
5
37
  ## [3.12.15] - 2026-04-17
6
38
 
7
39
  ### Fixed — Decision Log Trim — stop live progress.md bloat
@@ -55,7 +55,7 @@ const DEFAULTS = Object.freeze({
55
55
  maxIterations: 200,
56
56
  hours: 24,
57
57
  gutterNoProgressIters: 5,
58
- workerTimeoutMs: 3600000,
58
+ workerTimeoutMs: 270000,
59
59
  });
60
60
 
61
61
  // ── Glob → regex helper ─────────────────────────────────────────────────────
@@ -55,7 +55,7 @@ const DEFAULTS = Object.freeze({
55
55
  maxIterations: 200,
56
56
  hours: 24,
57
57
  gutterNoProgressIters: 5,
58
- workerTimeoutMs: 3600000,
58
+ workerTimeoutMs: 270000,
59
59
  });
60
60
 
61
61
  // ── Glob → regex helper ─────────────────────────────────────────────────────
@@ -73,7 +73,13 @@ const RUN_LOG = "run.log";
73
73
 
74
74
  const DEFAULT_HOURS = 24;
75
75
  const DEFAULT_MAX_ITERATIONS = 200;
76
- const DEFAULT_WORKER_TIMEOUT_MS = 3600000; // 1 hour per contract §13
76
+ // Anthropic prompt-cache TTL is 5 minutes (300,000 ms). The supervisor→worker
77
+ // handoff budget is ~30 s (process exit + state persist + next spawn). A 270 s
78
+ // worker timeout leaves room to complete the iter AND still relaunch against
79
+ // a warm cache. If a single iter legitimately exceeds 270 s, the supervisor
80
+ // kills the worker and logs a cache-miss warning; the next iter pays a
81
+ // cold-cache cost but execution continues.
82
+ const DEFAULT_WORKER_TIMEOUT_MS = 270 * 1000; // 270s — see cache-warm-pacing note above; contract §13/§16
77
83
 
78
84
  const TERMINAL_STATUSES = new Set(["done", "failed", "stopped", "crashed"]);
79
85
  const VALID_STATUSES = new Set([
@@ -205,6 +211,17 @@ function parseArgs(argv) {
205
211
  case "test-mode":
206
212
  out.testMode = true;
207
213
  break;
214
+ case "worker-timeout": {
215
+ // Per unattended-supervisor-contract §6 + §16: user-supplied override
216
+ // of DEFAULT_WORKER_TIMEOUT_MS (270 s). Accepts ms. Must not be raised
217
+ // above 270000 without a separate user-approved contract revision —
218
+ // the supervisor clamps but does not refuse silently.
219
+ const n = parseInt(val, 10);
220
+ if (Number.isFinite(n) && n > 0) {
221
+ out.workerTimeoutMs = n;
222
+ }
223
+ break;
224
+ }
208
225
  default:
209
226
  // Unknown flag — ignore for forward compatibility
210
227
  break;
@@ -558,6 +575,13 @@ function doUnattended(argv, deps) {
558
575
  ) {
559
576
  opts.maxIterations = config.maxIterations;
560
577
  }
578
+ if (
579
+ opts.workerTimeoutMs === undefined &&
580
+ typeof config.workerTimeoutMs === "number" &&
581
+ config.workerTimeoutMs > 0
582
+ ) {
583
+ opts.workerTimeoutMs = config.workerTimeoutMs;
584
+ }
561
585
  // CLI values now win — mirror them back into config so the pre-worker
562
586
  // safety caps (checkIterationCap / checkWallClockCap) use the effective
563
587
  // supervisor-scoped limits rather than the on-disk file defaults.
@@ -705,6 +729,21 @@ function doUnattended(argv, deps) {
705
729
  console.log(
706
730
  `[gsd-t-unattended] running — sessionId=${state.sessionId} pid=${process.pid} milestone=${state.milestone} platform=${state.platform}`,
707
731
  );
732
+ // M39 D2 — append watch-progress tree below banner (best-effort).
733
+ // Banner preserved verbatim above; renderer output appended below per
734
+ // watch-progress-contract.md §7. Never throws into the supervisor loop.
735
+ try {
736
+ const wp = require("./watch-progress.js");
737
+ const stateDir = path.join(projectDir, ".gsd-t", ".watch-state");
738
+ const tree = wp.buildTree(stateDir);
739
+ const rendered = wp.renderTree(tree, { currentAgent: state.sessionId });
740
+ if (rendered) {
741
+ // eslint-disable-next-line no-console
742
+ console.log(rendered);
743
+ }
744
+ } catch (_) {
745
+ /* watch-progress is best-effort; never crash the watch */
746
+ }
708
747
  }
709
748
 
710
749
  // ── SUPERVISOR-INIT HOOK (contract §12) ──────────────────────────────────
@@ -1138,11 +1177,48 @@ function _spawnWorker(state, opts) {
1138
1177
  else if (process.env.GSD_T_TRACE_ID) workerEnv.GSD_T_TRACE_ID = process.env.GSD_T_TRACE_ID;
1139
1178
  if (state && state.model) workerEnv.GSD_T_MODEL = state.model;
1140
1179
  else if (process.env.GSD_T_MODEL) workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
1180
+ // D2 watch-progress: mint a per-worker agent id and forward the supervisor's
1181
+ // id as parent, so shims inside the worker write state files that the tree
1182
+ // builder can attach under the supervisor root.
1183
+ workerEnv.GSD_T_AGENT_ID =
1184
+ "supervisor-iter-" + (state && state.iter ? state.iter : Date.now());
1185
+ if (process.env.GSD_T_AGENT_ID) {
1186
+ workerEnv.GSD_T_PARENT_AGENT_ID = process.env.GSD_T_AGENT_ID;
1187
+ }
1141
1188
  const res = platformSpawnWorker(opts.cwd, opts.timeout, {
1142
1189
  bin,
1143
1190
  args: [
1144
1191
  "-p",
1145
- "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.",
1192
+ [
1193
+ "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.",
1194
+ "",
1195
+ "# Team Mode (Intra-Wave Parallelism)",
1196
+ "",
1197
+ "Before executing tasks for this iteration, read `.gsd-t/partition.md` to",
1198
+ "identify the current wave and which domains belong to it.",
1199
+ "",
1200
+ "If the current wave has MULTIPLE independent domains/tasks (check",
1201
+ "`.gsd-t/domains/*/tasks.md` — 2 or more domains with incomplete tasks in the",
1202
+ "current wave):",
1203
+ "",
1204
+ " SPAWN PARALLEL SUBAGENTS — up to 15 concurrent Task subagents, one per",
1205
+ " domain, using `general-purpose` subagent_type. Use the same subagent",
1206
+ " prompt pattern as `/gsd-t-execute` Team Mode (see `commands/gsd-t-execute.md`",
1207
+ " Step 3 Team Mode section). Each subagent:",
1208
+ " - Receives the domain name, its scope.md, its tasks.md (only incomplete",
1209
+ " tasks from the current wave), and the relevant contracts",
1210
+ " - Works ONLY within its domain boundary",
1211
+ " - Returns when all its current-wave tasks are committed",
1212
+ " WAIT for ALL spawned subagents to report back before advancing.",
1213
+ "",
1214
+ "If the current wave has only 1 domain with incomplete tasks, execute",
1215
+ "sequentially in this worker (no subagent spawn needed).",
1216
+ "",
1217
+ "Inter-wave boundaries always remain sequential — never parallelize across",
1218
+ "waves, because wave-N+1 may depend on wave-N contract/state updates.",
1219
+ "",
1220
+ "Your job: run /gsd-t-resume but skip the unattended supervisor auto-reattach check in Step 0.",
1221
+ ].join("\n"),
1146
1222
  "--dangerously-skip-permissions",
1147
1223
  ],
1148
1224
  env: workerEnv,
@@ -74,7 +74,11 @@ const RUN_LOG = "run.log";
74
74
 
75
75
  const DEFAULT_HOURS = 24;
76
76
  const DEFAULT_MAX_ITERATIONS = 200;
77
- const DEFAULT_WORKER_TIMEOUT_MS = 3600000; // 1 hour per contract §13
77
+ // Anthropic prompt-cache TTL is 5 minutes (300,000 ms). The supervisor→worker
78
+ // handoff budget is ~30 s (process exit + state persist + next spawn). A 270 s
79
+ // worker timeout leaves room to complete the iter AND still relaunch against
80
+ // a warm cache. See contract §16 (cache-warm pacing).
81
+ const DEFAULT_WORKER_TIMEOUT_MS = 270 * 1000;
78
82
 
79
83
  const TERMINAL_STATUSES = new Set(["done", "failed", "stopped", "crashed"]);
80
84
  const VALID_STATUSES = new Set([
@@ -84,6 +84,21 @@ function autoSpawnHeadless(opts) {
84
84
  // watch=true + validation → warn on stderr; proceed headless
85
85
  // watch=false → headless (default behavior)
86
86
  if (watch && spawnType === "primary") {
87
+ // M39 D2 — append watch-progress tree below banner (best-effort).
88
+ // Banner here is the in-context-fallback signal printed by the caller;
89
+ // we don't own it, so we only render the tree to stdout. Never throws.
90
+ try {
91
+ const wp = require("./watch-progress.js");
92
+ const stateDir = path.join(projectDir, ".gsd-t", ".watch-state");
93
+ const tree = wp.buildTree(stateDir);
94
+ const rendered = wp.renderTree(tree, { currentAgent: null });
95
+ if (rendered) {
96
+ // eslint-disable-next-line no-console
97
+ console.log(rendered);
98
+ }
99
+ } catch (_) {
100
+ /* watch-progress is best-effort; never crash the watch */
101
+ }
87
102
  return {
88
103
  id: null,
89
104
  pid: null,
@@ -156,6 +171,10 @@ function autoSpawnHeadless(opts) {
156
171
  if (process.env.GSD_T_MODEL) {
157
172
  workerEnv.GSD_T_MODEL = process.env.GSD_T_MODEL;
158
173
  }
174
+ workerEnv.GSD_T_AGENT_ID = "headless-" + id;
175
+ if (process.env.GSD_T_AGENT_ID) {
176
+ workerEnv.GSD_T_PARENT_AGENT_ID = process.env.GSD_T_AGENT_ID;
177
+ }
159
178
 
160
179
  const child = spawn("node", childArgs, {
161
180
  cwd: projectDir,
@@ -99,6 +99,21 @@ function _formatIteration(iter, evs) {
99
99
  * @param {number} [args.now] — override for Date.now() (tests)
100
100
  * @returns {string}
101
101
  */
102
+ function _renderWatchProgressTree(args) {
103
+ // M39 D2 — best-effort watch-progress tree appended below the formatter
104
+ // output. Banner preserved verbatim; returns "" on any failure so callers
105
+ // never see a tree when state is missing/unavailable.
106
+ try {
107
+ const wp = require("./watch-progress.js");
108
+ const stateDir = (args && args.stateDir) || ".gsd-t/.watch-state";
109
+ const current = args && args.currentAgent ? args.currentAgent : null;
110
+ const tree = wp.buildTree(stateDir);
111
+ return wp.renderTree(tree, { currentAgent: current }) || "";
112
+ } catch (_) {
113
+ return "";
114
+ }
115
+ }
116
+
102
117
  function formatWatchTick(args) {
103
118
  const events = Array.isArray(args && args.events) ? args.events : [];
104
119
  const state = (args && args.state) || {};
@@ -108,21 +123,30 @@ function formatWatchTick(args) {
108
123
  const iter = Number.isFinite(state.iter) ? state.iter : 0;
109
124
  const header = `[unattended supervisor — iter ${iter}${elapsed ? `, ${elapsed}` : ""}]`;
110
125
 
126
+ let body;
111
127
  if (events.length === 0) {
112
- return `${header} (no new activity since last tick)`;
128
+ body = `${header} (no new activity since last tick)`;
129
+ } else {
130
+ const groups = _groupByIter(events);
131
+ const blocks = [];
132
+ for (const [groupIter, evs] of groups) {
133
+ const groupHeader = groups.length > 1 ? `[iter ${groupIter}]` : header;
134
+ const bodyLines = _formatIteration(groupIter, evs);
135
+ blocks.push([groupHeader, ...bodyLines].join("\n"));
136
+ }
137
+ body = groups.length > 1 ? `${header}\n${blocks.join("\n")}` : blocks[0];
113
138
  }
114
139
 
115
- const groups = _groupByIter(events);
116
- const blocks = [];
117
- for (const [groupIter, evs] of groups) {
118
- const groupHeader = groups.length > 1 ? `[iter ${groupIter}]` : header;
119
- const body = _formatIteration(groupIter, evs);
120
- blocks.push([groupHeader, ...body].join("\n"));
121
- }
122
- if (groups.length > 1) {
123
- return `${header}\n${blocks.join("\n")}`;
124
- }
125
- return blocks[0];
140
+ // M39 D2 — watch-progress tree below the existing formatter output.
141
+ // Banner preserved verbatim; tree appended only when caller opts in via
142
+ // `args.withWatchProgress = true`. Default-off preserves backward
143
+ // compatibility with existing snapshot-style callers (tests).
144
+ if (!(args && args.withWatchProgress === true)) return body;
145
+ const tree = _renderWatchProgressTree({
146
+ stateDir: args && args.stateDir,
147
+ currentAgent: args && (args.currentAgent || state.sessionId),
148
+ });
149
+ return tree ? `${body}\n${tree}` : body;
126
150
  }
127
151
 
128
152
  /**
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/watch-progress.js — GSD-T Watch-Progress Tree Builder + Renderer
4
+ *
5
+ * Reads `.gsd-t/.watch-state/*.json` state files written by
6
+ * `scripts/gsd-t-watch-state.js` and reconstructs the agent workflow tree
7
+ * via `parent_agent_id` lineage. Renders a task-list with ✅/🔄/⬜ markers
8
+ * below every `--watch` surface (banner preserved intact).
9
+ *
10
+ * Contract: `.gsd-t/contracts/watch-progress-contract.md` v1.0.0
11
+ * Owner: D2 (M39 `d2-progress-watch`)
12
+ * Zero external deps.
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+
20
+ const STALE_MS = 24 * 60 * 60 * 1000; // 24h
21
+
22
+ const MARKERS = {
23
+ done: "✅",
24
+ in_progress: "🔄",
25
+ pending: "⬜",
26
+ skipped: "➡️",
27
+ failed: "❌",
28
+ };
29
+
30
+ function _readAll(stateDir) {
31
+ if (!stateDir || !fs.existsSync(stateDir)) return [];
32
+ let entries;
33
+ try {
34
+ entries = fs.readdirSync(stateDir);
35
+ } catch (_) {
36
+ return [];
37
+ }
38
+ const records = [];
39
+ for (const f of entries) {
40
+ if (!f.endsWith(".json")) continue;
41
+ try {
42
+ const raw = fs.readFileSync(path.join(stateDir, f), "utf8");
43
+ const rec = JSON.parse(raw);
44
+ if (rec && typeof rec === "object" && typeof rec.agent_id === "string") {
45
+ records.push(rec);
46
+ }
47
+ } catch (_) { /* skip malformed */ }
48
+ }
49
+ return records;
50
+ }
51
+
52
+ function _isStale(record, now) {
53
+ if (!record.completed_at) return false;
54
+ const t = Date.parse(record.completed_at);
55
+ if (!Number.isFinite(t)) return false;
56
+ return (now - t) > STALE_MS;
57
+ }
58
+
59
+ function buildTree(stateDir, options) {
60
+ const now = options && Number.isFinite(options.now) ? options.now : Date.now();
61
+ const records = _readAll(stateDir).filter((r) => !_isStale(r, now));
62
+ const index = new Map();
63
+ for (const r of records) index.set(r.agent_id, { record: r, children: [] });
64
+ const roots = [];
65
+ const orphans = [];
66
+ for (const r of records) {
67
+ const node = index.get(r.agent_id);
68
+ const parentId = r.parent_agent_id;
69
+ if (parentId === null || parentId === undefined) {
70
+ roots.push(node);
71
+ } else if (index.has(parentId)) {
72
+ index.get(parentId).children.push(node);
73
+ } else {
74
+ orphans.push(node);
75
+ }
76
+ }
77
+ return { roots, orphans };
78
+ }
79
+
80
+ function _marker(status) {
81
+ return MARKERS[status] || MARKERS.pending;
82
+ }
83
+
84
+ function _countDone(node) {
85
+ let count = 0;
86
+ const s = node.record.status;
87
+ if (s === "done" || s === "skipped") count = 1;
88
+ for (const c of node.children) count += _countDone(c);
89
+ return count;
90
+ }
91
+
92
+ function _flattenIds(node, acc) {
93
+ acc.push(node.record.agent_id);
94
+ for (const c of node.children) _flattenIds(c, acc);
95
+ return acc;
96
+ }
97
+
98
+ function _containsAgent(node, agentId) {
99
+ if (!agentId) return false;
100
+ return _flattenIds(node, []).includes(agentId);
101
+ }
102
+
103
+ function _pickCurrent(tree) {
104
+ let pick = null;
105
+ let latest = -Infinity;
106
+ const walk = (node) => {
107
+ const r = node.record;
108
+ if (r.status === "in_progress" && r.started_at) {
109
+ const t = Date.parse(r.started_at);
110
+ if (Number.isFinite(t) && t > latest) { latest = t; pick = r.agent_id; }
111
+ }
112
+ for (const c of node.children) walk(c);
113
+ };
114
+ for (const root of tree.roots) walk(root);
115
+ return pick;
116
+ }
117
+
118
+ function _renderExpanded(node, depth, lines) {
119
+ const indent = " ".repeat(depth);
120
+ const r = node.record;
121
+ lines.push(`${indent}${_marker(r.status)} ${r.step_label || r.command || r.agent_id}`);
122
+ for (const c of node.children) _renderExpanded(c, depth + 1, lines);
123
+ }
124
+
125
+ function _renderCollapsed(node, depth, lines) {
126
+ const indent = " ".repeat(depth);
127
+ const r = node.record;
128
+ const doneCount = _countDone(node);
129
+ const label = r.step_label || r.command || r.agent_id;
130
+ lines.push(`${indent}${_marker(r.status)} ${label} (${doneCount} tasks done)`);
131
+ }
132
+
133
+ function renderTree(tree, options) {
134
+ if (!tree || (tree.roots.length === 0 && tree.orphans.length === 0)) return "";
135
+ const opts = options || {};
136
+ const currentAgent = opts.currentAgent || _pickCurrent(tree);
137
+ const lines = [];
138
+ for (const root of tree.roots) {
139
+ if (_containsAgent(root, currentAgent)) _renderExpanded(root, 0, lines);
140
+ else _renderCollapsed(root, 0, lines);
141
+ }
142
+ if (tree.orphans.length > 0) {
143
+ lines.push("⬜ (orphan subtree — parent state missing)");
144
+ for (const o of tree.orphans) _renderExpanded(o, 1, lines);
145
+ }
146
+ return lines.join("\n");
147
+ }
148
+
149
+ module.exports = { buildTree, renderTree, MARKERS, STALE_MS, _pickCurrent };
150
+
151
+ if (require.main === module) {
152
+ const stateDir = process.argv[2] || path.join(".gsd-t", ".watch-state");
153
+ const out = renderTree(buildTree(stateDir));
154
+ if (out) process.stdout.write(out + "\n");
155
+ }