@tekyzinc/gsd-t 3.11.11 → 3.12.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +4 -10
  3. package/bin/event-stream.cjs +205 -0
  4. package/bin/gsd-t-unattended.cjs +87 -1
  5. package/bin/gsd-t-unattended.js +87 -1
  6. package/bin/gsd-t.js +15 -117
  7. package/bin/headless-auto-spawn.cjs +44 -4
  8. package/bin/headless-auto-spawn.js +44 -4
  9. package/bin/scan-data-collector.js +39 -11
  10. package/bin/token-budget.cjs +43 -126
  11. package/bin/unattended-watch-format.cjs +154 -0
  12. package/commands/gsd-t-backlog-list.md +0 -38
  13. package/commands/gsd-t-complete-milestone.md +2 -30
  14. package/commands/gsd-t-debug.md +18 -122
  15. package/commands/gsd-t-doc-ripple.md +0 -21
  16. package/commands/gsd-t-execute.md +58 -117
  17. package/commands/gsd-t-help.md +3 -59
  18. package/commands/gsd-t-integrate.md +18 -78
  19. package/commands/gsd-t-quick.md +22 -80
  20. package/commands/gsd-t-resume.md +2 -2
  21. package/commands/gsd-t-scan.md +15 -1
  22. package/commands/gsd-t-status.md +2 -32
  23. package/commands/gsd-t-unattended-watch.md +37 -1
  24. package/commands/gsd-t-verify.md +14 -2
  25. package/commands/gsd-t-wave.md +22 -91
  26. package/commands/gsd.md +43 -4
  27. package/docs/GSD-T-README.md +2 -6
  28. package/docs/architecture.md +10 -8
  29. package/docs/infrastructure.md +8 -14
  30. package/docs/methodology.md +10 -4
  31. package/docs/prd-harness-evolution.md +1 -1
  32. package/docs/requirements.md +28 -12
  33. package/package.json +2 -2
  34. package/scripts/context-meter/threshold.js +25 -46
  35. package/scripts/context-meter/threshold.test.js +52 -80
  36. package/scripts/gsd-t-agent-dashboard-server.js +4 -4
  37. package/scripts/gsd-t-agent-dashboard.html +699 -380
  38. package/scripts/gsd-t-context-meter.e2e.test.js +4 -3
  39. package/scripts/gsd-t-context-meter.js +1 -1
  40. package/scripts/gsd-t-context-meter.test.js +58 -50
  41. package/templates/CLAUDE-global.md +7 -25
  42. package/templates/CLAUDE-project.md +22 -23
  43. package/bin/qa-calibrator.js +0 -194
  44. package/bin/runway-estimator.cjs +0 -242
  45. package/bin/runway-estimator.js +0 -242
  46. package/bin/token-optimizer.cjs +0 -471
  47. package/bin/token-optimizer.js +0 -471
  48. package/bin/token-telemetry.cjs +0 -246
  49. package/bin/token-telemetry.js +0 -246
  50. package/commands/gsd-t-audit.md +0 -196
  51. package/commands/gsd-t-brainstorm.md +0 -201
  52. package/commands/gsd-t-discuss.md +0 -178
  53. package/commands/gsd-t-optimization-apply.md +0 -91
  54. package/commands/gsd-t-optimization-reject.md +0 -94
  55. package/commands/gsd-t-prompt.md +0 -137
  56. package/commands/gsd-t-reflect.md +0 -130
  57. package/scripts/context-meter/count-tokens-client.js +0 -221
  58. package/scripts/context-meter/count-tokens-client.test.js +0 -308
  59. package/scripts/context-meter/test-injector.js +0 -55
package/CHANGELOG.md CHANGED
@@ -2,6 +2,80 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.12.10] - 2026-04-17
6
+
7
+ ### M38: Headless-by-Default + Meter Reduction
8
+
9
+ **Background**: M37 was right that the context meter needed to do more — but escalating to a MANDATORY STOP banner in the interactive session was the wrong fix. M38 removes the cause instead of bandaging the symptom: headless spawning is now the default for all primary workflow subagents, so the parent context grows much slower and the single-band meter threshold is sufficient. Seven commands removed. Five contracts folded. Net result: same "work never stops" UX achieved by structure instead of instrumentation.
10
+
11
+ ### Added
12
+ - **`bin/event-stream.cjs`** — new module for structured JSONL event emission to `.gsd-t/events/YYYY-MM-DD.jsonl`. Emits `task_start`, `task_complete`, `subagent_verdict`, `file_changed`, `test_result`, `error`, `retry` event types. Used by unattended supervisor and watch tick.
13
+ - **`bin/headless-auto-spawn.cjs`** `watch` + `spawnType` parameters — propagation rules: `spawnType:'validation'` always headless regardless of `--watch`; `spawnType:'primary'` + `watch:true` returns `{mode:'in-context'}` for live streaming.
14
+ - **`.gsd-t/contracts/headless-default-contract.md`** v1.0.0 — defines the headless spawn primitive, Conversion Map (7 primary commands converted), `--watch` flag spec, validation-spawn enforcement, and migration path. Folds headless-auto-spawn-contract v1.0.0.
15
+ - **`.gsd-t/contracts/unattended-event-stream-contract.md`** v1.0.0 — JSONL event schema, watch tick activity log format, supervisor emission requirements.
16
+ - **`test/headless-default.test.js`** — 11 tests covering the 4-cell propagation matrix (primary/validation × watch/no-watch) + regression coverage.
17
+ - **`test/event-stream.test.js`**, **`test/unattended-watch.test.js`**, **`test/router-intent.test.js`** — new test files for M38 components.
18
+ - **`commands/gsd.md`** intent classifier — handles conversational requests directly (workflow → existing command; conversational → respond; ambiguous → default to conversation).
19
+
20
+ ### Changed
21
+ - **7 command files** (`gsd-t-execute`, `gsd-t-wave`, `gsd-t-integrate`, `gsd-t-quick`, `gsd-t-debug`, `gsd-t-scan`, `gsd-t-verify`) — converted to `autoSpawnHeadless({spawnType:'primary', watch:$WATCH_FLAG})` pattern. Validation spawns (QA, Red Team, Design Verification) always headless.
22
+ - **`bin/gsd-t-unattended.{cjs,js}`** — rejects `--watch` flag with clear error. Emits structured JSONL events at every phase boundary.
23
+ - **`.gsd-t/contracts/context-meter-contract.md`** v1.3.0 — drops three-band model, dead-meter detection, stale-band logic, Universal Auto-Pause elevation. Single-band model: one threshold (default 85%), one action (silent headless handoff). Replaces v1.2.0.
24
+ - **`.gsd-t/contracts/unattended-supervisor-contract.md`** v1.1.0 — adds §9 Event Stream Emission requirement: supervisor MUST emit structured events; watch tick MUST read events and format activity log.
25
+ - **`templates/CLAUDE-global.md`** — Universal Auto-Pause Rule section removed; Context Meter section updated to single-band description.
26
+ - **5 loop commands** (`gsd-t-execute`, `gsd-t-wave`, `gsd-t-integrate`, `gsd-t-quick`, `gsd-t-debug`) — Step 0.2 Universal Auto-Pause enforcement stripped.
27
+ - **`scripts/gsd-t-context-meter.test.js`** — rewritten for single-band model.
28
+ - **`test/filesystem.test.js`** — command count updated from 61 to 54.
29
+ - **`docs/requirements.md`** — REQ-073..078 updated to `SUPERSEDED by REQ-08X (M38)` with replacement pointers. REQ-088..093 added (M38 requirements).
30
+ - **`docs/methodology.md`** §3–§5 — historical framing added; deleted machinery marked as superseded by M38.
31
+ - **`docs/prd-harness-evolution.md`** — Status updated to `HISTORICAL — M31 shipped; M32/M33 SUPERSEDED by M38`.
32
+ - **`docs/architecture.md`**, **`docs/workflows.md`**, **`docs/infrastructure.md`**, **`GSD-T-README.md`** — updated to reflect headless-by-default spawn path, event stream, simplified meter.
33
+
34
+ ### Removed
35
+ - **7 commands deleted**: `gsd-t-optimization-apply`, `gsd-t-optimization-reject`, `gsd-t-reflect`, `gsd-t-audit` (self-improvement loop), `gsd-t-prompt`, `gsd-t-brainstorm`, `gsd-t-discuss` (conversational — router intent classifier handles these)
36
+ - **`bin/runway-estimator.cjs`** + **`bin/token-telemetry.cjs`** — deleted; replaced by headless-by-default approach
37
+ - **`bin/qa-calibrator.js`** + **`bin/token-optimizer.js`** — deleted with self-improvement loop
38
+ - **5 contracts folded/deleted**: `runway-estimator-contract.md`, `token-telemetry-contract.md`, `headless-auto-spawn-contract.md`, `qa-calibration-contract.md`, `harness-audit-contract.md`
39
+ - **`test/runway-estimator.test.js`**, **`test/token-telemetry.test.js`**, **`test/qa-calibrator.test.js`**, **`test/token-optimizer.test.js`** — deleted with removed modules
40
+
41
+ ### Migration Notes
42
+ - **Spawn pattern**: replace `autoSpawnHeadless()` (no args) with `autoSpawnHeadless({spawnType:'primary', watch:$WATCH_FLAG})` in any downstream command files that call the spawn primitive directly.
43
+ - **Context meter**: if you depend on the three-band model (`normal`/`warn`/`stop`) or dead-meter detection in `token-budget.cjs`, those fields are removed. `getSessionStatus()` returns `{pct, threshold}` only.
44
+ - **Deleted contracts**: any downstream references to `runway-estimator-contract.md`, `token-telemetry-contract.md`, or `headless-auto-spawn-contract.md` should point to `headless-default-contract.md` v1.0.0 instead.
45
+ - **Deleted commands**: `gsd-t-prompt`, `gsd-t-brainstorm`, `gsd-t-discuss` — use plain text messages to Claude instead; the router classifier handles conversational requests. `gsd-t-optimization-apply/reject`, `gsd-t-reflect`, `gsd-t-audit` — removed; the self-improvement backlog is no longer maintained.
46
+
47
+ ### Testing
48
+ - 1176/1177 tests pass. 1 pre-existing failure (`scan.test.js:287`) carried forward — scan-data-collector regex drift vs current prose format, unrelated to M38 scope.
49
+
50
+ ## [3.11.12] - 2026-04-16
51
+
52
+ ### Added — M38 Partition + Plan + Domain H1 Progress
53
+
54
+ **Background**: M38 (Headless-by-Default + Meter Reduction) partitioned into 5 domains across 2 waves. Domain H1 (headless-spawn-default) executed through T6 by unattended supervisor Iter 2 but did not commit. This checkin captures all M38 setup work + H1 in-flight progress + Scan #11 regeneration.
55
+
56
+ ### Added
57
+ - **5 M38 domain directories** under `.gsd-t/domains/` (m38-headless-spawn-default, m38-meter-reduction, m38-unattended-event-stream, m38-router-conversational, m38-cleanup-and-docs) with scope.md, constraints.md, tasks.md each — 35 atomic tasks total
58
+ - **2 new contracts**: `.gsd-t/contracts/headless-default-contract.md` (v1.0.0 DRAFT, folds 3 M35 contracts), `.gsd-t/contracts/unattended-event-stream-contract.md` (v1.0.0 DRAFT)
59
+ - **`test/headless-default.test.js`** — 11 tests covering the 4-cell propagation matrix (primary/validation × watch/no-watch) + existing regression coverage
60
+ - **`bin/gsd-t.js`** `unattended` passthrough subcommand — dispatches to `bin/gsd-t-unattended.cjs` so defense-in-depth `--watch` rejection reaches the supervisor rejection logic
61
+ - **M38 Scan #11** artifacts under `.gsd-t/scan/` (architecture, business-rules, contract-drift, quality, security, test-baseline + scan-report.html)
62
+
63
+ ### Changed
64
+ - **`bin/headless-auto-spawn.{cjs,js}`** — added `watch` + `spawnType` parameters; propagation rules implemented (validation spawns always headless, primary+watch returns `{mode: 'in-context'}`)
65
+ - **7 command files** converted to `autoSpawnHeadless({...spawnType: 'primary', watch: $WATCH_FLAG})` pattern: `gsd-t-execute`, `gsd-t-wave`, `gsd-t-integrate`, `gsd-t-quick`, `gsd-t-debug`, `gsd-t-scan`, `gsd-t-verify`
66
+ - **`bin/gsd-t-unattended.{cjs,js}`** — rejects `--watch` flag with clear error (validation-spawn enforcement in unattended context)
67
+ - **`bin/gsd-t.js`** `installContextMeter()` — removed `test-injector.js` skip (file deleted; no longer needed)
68
+ - **`.gsd-t/contracts/integration-points.md`** — M38 dependency graph + 5 checkpoints (M38-CP1 → M38-CP5) + file ownership map
69
+ - **`.gsd-t/progress.md`** — M38 partition + plan entries added to Decision Log
70
+ - **`docs/architecture.md` + `docs/infrastructure.md`** — Scan #11 staleness callouts added (TD-103 doc-ripple candidate noted)
71
+
72
+ ### Removed
73
+ - **`scripts/context-meter/count-tokens-client.{js,test.js}`** — retired with v3.11.11 local-estimator switch (count_tokens API no longer called)
74
+ - **`scripts/context-meter/test-injector.js`** — test-only infrastructure, no longer referenced
75
+
76
+ ### Testing
77
+ - 1234/1242 tests pass. 8 pre-existing failures carried forward: 7 stranded context-meter tests (TD-102, owned by M38-MR) + 1 scan.test.js live-state test (unrelated). No regressions from H1 work.
78
+
5
79
  ## [3.11.10] - 2026-04-16
6
80
 
7
81
  ### Added — Universal Context Auto-Pause (M37)
package/README.md CHANGED
@@ -14,14 +14,10 @@ A methodology for reliable, parallelizable development using Claude Code with op
14
14
  **Self-learning rule engine** — declarative rules in rules.jsonl detect failure patterns from task metrics. Candidate patches progress through a 5-stage lifecycle (candidate, applied, measured, promoted, graduated) with >55% improvement gates before becoming permanent methodology artifacts.
15
15
  **Cross-project learning** — proven rules propagate to `~/.claude/metrics/` and sync across all registered projects via `update-all`. Rules validated in 3+ projects become universal; 5+ projects qualify for npm distribution. Cross-project signal comparison and global ELO rankings available via `gsd-t-metrics --cross-project` and `gsd-t-status`.
16
16
  **Stack Rules Engine** — auto-detects project tech stack (React, TypeScript, Node API, Python, Go, Rust) from manifest files and injects mandatory best-practice rules into subagent prompts at execute-time. Universal security rules always apply; stack-specific rules layer on top. Includes **design-to-code** rules for pixel-perfect frontend implementation from Figma, screenshots, or design images — with Figma MCP integration, design token extraction, stack capability evaluation, and mandatory visual verification: every screen is rendered in a real browser, screenshotted at mobile/tablet/desktop, and compared pixel-by-pixel against the Figma design. Auto-bootstraps during partition when design references are detected. Extensible: drop a `.md` file in `templates/stacks/` to add a new stack.
17
- **Self-Calibrating QA** — `qa-calibrator.js` tracks QA miss-rates across milestones, detects weak-spot categories (error paths, boundaries, state transitions), and automatically injects targeted guidance into QA subagent prompts. Projects on the same stack share miss-rate data for faster calibration.
18
- **Runway-Protected Execution (M35, v2.76.10)** — three-band token budget (`normal` < 70%, `warn` 70–85%, `stop` ≥ 85%) with **zero silent quality degradation**. No model downgrades, no phase skips under pressure. Instead:
17
+ **Headless-by-Default Spawn (M38, v3.12.10)** — long-running workflow commands (execute, wave, integrate, debug repair loops) spawn detached by default via the unattended supervisor. The interactive session prints a launch banner, logs the event-stream path, and exits. Pass `--watch` to keep a live status block in the session (270s `ScheduleWakeup` ticks, cache-window-safe). The supervisor emits JSONL events to `.gsd-t/events/YYYY-MM-DD.jsonl` at every phase boundary — shared by watch command and dashboard. See `.gsd-t/contracts/headless-default-contract.md` v1.0.0 and `unattended-event-stream-contract.md` v1.0.0.
19
18
  - **Surgical model selection** — `bin/model-selector.js` assigns haiku/sonnet/opus per phase via a declarative rules table; `/advisor` escalation path with convention-based fallback.
20
- - **Pre-flight runway estimator** — `bin/runway-estimator.js` reads per-spawn token telemetry and projects whether a long-running command would cross 85%. If yes, the run is refused *before burning any tokens*.
21
- - **Headless auto-spawn on refusal** `bin/headless-auto-spawn.js` detaches a child process to continue the work in a fresh context. The interactive session never sees a `/clear` prompt.
22
- - **Per-spawn token telemetry** — `.gsd-t/token-metrics.jsonl` records one 18-field row per Task subagent spawn. Feeds the runway estimator and the retrospective optimization backlog.
23
- - **Optimization backlog** — `bin/token-optimizer.js` runs at `complete-milestone` and appends model-tier recalibration recommendations (`demote`, `escalate`, `runway-tune`, `investigate`) to `.gsd-t/optimization-backlog.md`. **Never auto-applies.** User promotes via `/user:gsd-t-optimization-apply {ID}` or dismisses via `/user:gsd-t-optimization-reject {ID} [--reason "..."]` with a 5-milestone cooldown.
24
- **Context Meter (M34)** — real-time context window measurement via the Anthropic `count_tokens` API. A PostToolUse hook streams the current transcript to `count_tokens`, writes the exact input-token count and threshold band to `.gsd-t/.context-meter-state.json`, and `token-budget.getSessionStatus()` reads that state file as the authoritative context-burn signal. Replaces the v2.74.12 task-counter proxy. Requires an `ANTHROPIC_API_KEY` — `gsd-t doctor` hard-gates on it. See the Context Meter Setup section below.
19
+ - **Per-spawn token telemetry** — `.gsd-t/token-metrics.jsonl` records one 18-field row per Task subagent spawn.
20
+ **Context Meter (M34/M38)** — PostToolUse hook writes `.gsd-t/.context-meter-state.json` via local token estimation. Single-band model (`context-meter-contract.md` v1.3.0): one threshold (default 85%), one action hand off to a detached headless spawn. The meter informs spawn-time routing, not in-flight pauses.
25
21
  **Quality North Star** — projects define a `## Quality North Star` section in CLAUDE.md (1–3 sentences, e.g., "This is a published npm library. Every public API must be intuitive and backward-compatible."). `gsd-t-init` auto-detects preset (library/web-app/cli) from package.json signals; `gsd-t-setup` configures it for existing projects. Subagents read it as a quality lens; absent = silent skip (backward compatible).
26
22
  **Design Brief Artifact** — during partition, UI/frontend projects (React, Vue, Svelte, Flutter, Tailwind) automatically get `.gsd-t/contracts/design-brief.md` with color palette, typography, spacing system, component patterns, and tone/voice. Non-UI projects skip silently. User-customized briefs are preserved. Referenced in plan phase for visual consistency.
27
23
  **Design Verification Agent** — after QA passes on design-to-code projects, a dedicated verification agent opens a browser with both the built frontend AND the original design (Figma page, design image, or MCP screenshot) side-by-side for direct visual comparison. Produces a structured element-by-element comparison table (30+ rows) with specific design values vs. implementation values and MATCH/DEVIATION verdicts. An artifact gate enforces that the comparison table exists — missing it blocks completion. Separation of concerns: coding agents code, verification agents verify. Wired into execute (Step 5.25) and quick (Step 5.25). Only fires when `.gsd-t/contracts/design-contract.md` exists — non-design projects are unaffected.
@@ -37,7 +33,7 @@ A methodology for reliable, parallelizable development using Claude Code with op
37
33
  npx @tekyzinc/gsd-t install
38
34
  ```
39
35
 
40
- This installs 51 GSD-T commands + 5 utility commands (56 total) to `~/.claude/commands/` and the global CLAUDE.md to `~/.claude/CLAUDE.md`. Works on Windows, Mac, and Linux.
36
+ This installs 49 GSD-T commands + 5 utility commands (54 total) to `~/.claude/commands/` and the global CLAUDE.md to `~/.claude/CLAUDE.md`. Works on Windows, Mac, and Linux.
41
37
 
42
38
  ### Start Using It
43
39
 
@@ -192,7 +188,6 @@ This will replace changed command files, back up your CLAUDE.md if customized, a
192
188
  | `/user:gsd-t-status` | Cross-domain progress view with token breakdown by domain/task/phase | Manual |
193
189
  | `/user:gsd-t-resume` | Restore context, continue | Manual |
194
190
  | `/user:gsd-t-quick` | Fast task with GSD-T guarantees | Manual |
195
- | `/user:gsd-t-reflect` | Generate retrospective from event stream, propose memory updates | Manual |
196
191
  | `/user:gsd-t-visualize` | Launch browser dashboard — SSE server + React Flow agent visualization | Manual |
197
192
  | `/user:gsd-t-debug` | Systematic debugging with state | Manual |
198
193
  | `/user:gsd-t-metrics` | View task telemetry, process ELO, signal distribution, domain health, and cross-project comparison (`--cross-project`) | Manual |
@@ -202,7 +197,6 @@ This will replace changed command files, back up your CLAUDE.md if customized, a
202
197
  | `/user:gsd-t-version-update` | Update GSD-T to latest version | Manual |
203
198
  | `/user:gsd-t-version-update-all` | Update GSD-T + all registered projects | Manual |
204
199
  | `/user:gsd-t-triage-and-merge` | Auto-review, merge, and publish GitHub branches | Manual |
205
- | `/user:gsd-t-audit` | Harness self-audit — analyze cost/benefit of enforcement components | Manual |
206
200
  | `/user:gsd-t-design-audit` | Compare built screen against Figma design — structured deviation report | Manual |
207
201
  | `/user:gsd-t-design-build` | Build from design contracts with two-terminal review (Term 1 builder) | Manual |
208
202
  | `/user:gsd-t-design-review` | Independent review agent for design build (Term 2 reviewer) | Auto |
@@ -0,0 +1,205 @@
1
+ /**
2
+ * bin/event-stream.cjs
3
+ *
4
+ * Structured event stream for the unattended supervisor watch-tick (M38 ES).
5
+ * Workers append events to `.gsd-t/events/YYYY-MM-DD.jsonl`; watch tick reads
6
+ * new events since the persisted cursor.
7
+ *
8
+ * Contract: `.gsd-t/contracts/unattended-event-stream-contract.md` v1.0.0.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const EVENTS_DIR_REL = path.join(".gsd-t", "events");
17
+ const CURSOR_PATH_REL = path.join(".gsd-t", ".unattended", "event-cursor");
18
+
19
+ function eventsDir(projectDir) {
20
+ return path.join(projectDir, EVENTS_DIR_REL);
21
+ }
22
+
23
+ function cursorPath(projectDir) {
24
+ return path.join(projectDir, CURSOR_PATH_REL);
25
+ }
26
+
27
+ function todayFileDate(now) {
28
+ const d = now || new Date();
29
+ const y = d.getUTCFullYear();
30
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
31
+ const day = String(d.getUTCDate()).padStart(2, "0");
32
+ return `${y}-${m}-${day}`;
33
+ }
34
+
35
+ function eventFileFor(projectDir, fileDate) {
36
+ return path.join(eventsDir(projectDir), `${fileDate}.jsonl`);
37
+ }
38
+
39
+ /**
40
+ * Append one event to today's jsonl file. Atomic via append-only write with
41
+ * a newline-terminated record. Never throws — failures are logged to stderr.
42
+ *
43
+ * @param {string} projectDir
44
+ * @param {object} eventObj — `type`, `iter`, `source`, plus type-specific fields
45
+ * @returns {boolean} true on success, false on failure
46
+ */
47
+ function appendEvent(projectDir, eventObj) {
48
+ try {
49
+ if (!eventObj || typeof eventObj !== "object") return false;
50
+ const ev = Object.assign({}, eventObj);
51
+ if (typeof ev.ts !== "string" || !ev.ts) {
52
+ ev.ts = new Date().toISOString();
53
+ }
54
+ const dir = eventsDir(projectDir);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ const fileDate = todayFileDate(new Date(ev.ts));
57
+ const file = eventFileFor(projectDir, fileDate);
58
+ const line = JSON.stringify(ev) + "\n";
59
+ fs.appendFileSync(file, line, "utf8");
60
+ return true;
61
+ } catch (err) {
62
+ try {
63
+ process.stderr.write(`[event-stream] appendEvent failed: ${err.message}\n`);
64
+ } catch (_) { /* ignore */ }
65
+ return false;
66
+ }
67
+ }
68
+
69
+ function readCursor(projectDir) {
70
+ try {
71
+ const raw = fs.readFileSync(cursorPath(projectDir), "utf8");
72
+ const parsed = JSON.parse(raw);
73
+ if (parsed && typeof parsed.fileDate === "string" && Number.isFinite(parsed.offset)) {
74
+ return parsed;
75
+ }
76
+ } catch (_) { /* missing or malformed → treated as uninitialized */ }
77
+ return null;
78
+ }
79
+
80
+ function writeCursor(projectDir, cursor) {
81
+ try {
82
+ const p = cursorPath(projectDir);
83
+ fs.mkdirSync(path.dirname(p), { recursive: true });
84
+ const tmp = `${p}.tmp`;
85
+ fs.writeFileSync(tmp, JSON.stringify(cursor), "utf8");
86
+ fs.renameSync(tmp, p);
87
+ return true;
88
+ } catch (err) {
89
+ try {
90
+ process.stderr.write(`[event-stream] writeCursor failed: ${err.message}\n`);
91
+ } catch (_) { /* ignore */ }
92
+ return false;
93
+ }
94
+ }
95
+
96
+ function fileSize(p) {
97
+ try {
98
+ return fs.statSync(p).size;
99
+ } catch (_) {
100
+ return 0;
101
+ }
102
+ }
103
+
104
+ function parseJsonlSlice(buf) {
105
+ const text = buf.toString("utf8");
106
+ const lines = text.split("\n");
107
+ const hasPartialTail = text.length > 0 && !text.endsWith("\n");
108
+ const consumable = hasPartialTail ? lines.slice(0, -1) : lines;
109
+ const events = [];
110
+ for (const line of consumable) {
111
+ if (!line) continue;
112
+ try {
113
+ events.push(JSON.parse(line));
114
+ } catch (_) {
115
+ try {
116
+ process.stderr.write(`[event-stream] skipping malformed line\n`);
117
+ } catch (__) { /* ignore */ }
118
+ }
119
+ }
120
+ const consumedBytes = hasPartialTail
121
+ ? Buffer.byteLength(consumable.join("\n") + (consumable.length ? "\n" : ""), "utf8")
122
+ : buf.length;
123
+ return { events, consumedBytes };
124
+ }
125
+
126
+ function readSlice(filePath, startOffset) {
127
+ const size = fileSize(filePath);
128
+ if (size <= startOffset) return { events: [], consumedBytes: 0, fileSize: size };
129
+ const fd = fs.openSync(filePath, "r");
130
+ try {
131
+ const length = size - startOffset;
132
+ const buf = Buffer.alloc(length);
133
+ fs.readSync(fd, buf, 0, length, startOffset);
134
+ const { events, consumedBytes } = parseJsonlSlice(buf);
135
+ return { events, consumedBytes, fileSize: size };
136
+ } finally {
137
+ try { fs.closeSync(fd); } catch (_) { /* ignore */ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Read all events from cursor to EOF. Handles day-boundary per contract §3:
143
+ * if cursor points to a past file date, read remaining events from that file,
144
+ * then read today's file from byte 0; advance cursor to today's EOF.
145
+ *
146
+ * If cursor is missing, initialize to today's file at current EOF (no backlog
147
+ * surfaced on first read — matches contract §3 initial-cursor behavior).
148
+ *
149
+ * @param {string} projectDir
150
+ * @returns {{events: object[], newCursor: {fileDate: string, offset: number}}}
151
+ */
152
+ function readSinceCursor(projectDir) {
153
+ const today = todayFileDate(new Date());
154
+ let cursor = readCursor(projectDir);
155
+ if (!cursor) {
156
+ const todayFile = eventFileFor(projectDir, today);
157
+ const size = fileSize(todayFile);
158
+ const newCursor = { fileDate: today, offset: size };
159
+ writeCursor(projectDir, newCursor);
160
+ return { events: [], newCursor };
161
+ }
162
+
163
+ const result = [];
164
+ let { fileDate, offset } = cursor;
165
+
166
+ if (fileDate !== today) {
167
+ const prev = eventFileFor(projectDir, fileDate);
168
+ if (fs.existsSync(prev)) {
169
+ const slice = readSlice(prev, offset);
170
+ result.push(...slice.events);
171
+ }
172
+ fileDate = today;
173
+ offset = 0;
174
+ }
175
+
176
+ const todayFile = eventFileFor(projectDir, today);
177
+ if (fs.existsSync(todayFile)) {
178
+ const slice = readSlice(todayFile, offset);
179
+ result.push(...slice.events);
180
+ offset = offset + slice.consumedBytes;
181
+ } else {
182
+ return { events: result, newCursor: { fileDate, offset } };
183
+ }
184
+
185
+ return { events: result, newCursor: { fileDate, offset } };
186
+ }
187
+
188
+ /**
189
+ * Persist the cursor returned by readSinceCursor.
190
+ * @param {string} projectDir
191
+ * @param {{fileDate: string, offset: number}} newCursor
192
+ */
193
+ function advanceCursor(projectDir, newCursor) {
194
+ if (!newCursor || typeof newCursor.fileDate !== "string" || !Number.isFinite(newCursor.offset)) {
195
+ return false;
196
+ }
197
+ return writeCursor(projectDir, newCursor);
198
+ }
199
+
200
+ module.exports = {
201
+ appendEvent,
202
+ readSinceCursor,
203
+ advanceCursor,
204
+ _internal: { todayFileDate, eventsDir, cursorPath, readCursor, writeCursor },
205
+ };
@@ -55,6 +55,13 @@ const {
55
55
  notify,
56
56
  } = require("./gsd-t-unattended-platform.cjs");
57
57
 
58
+ // Event stream (M38 ES) — additive, non-blocking. `_emit` swallows its own
59
+ // errors per unattended-event-stream-contract.md §6.
60
+ const { appendEvent: _esAppendEvent } = require("./event-stream.cjs");
61
+ function _emit(projectDir, ev) {
62
+ try { _esAppendEvent(projectDir, ev); } catch (_) { /* never halt the loop */ }
63
+ }
64
+
58
65
  // ── Constants ───────────────────────────────────────────────────────────────
59
66
 
60
67
  const CONTRACT_VERSION = "1.0.0";
@@ -476,7 +483,32 @@ function finalizeState(state, dir, terminalStatus) {
476
483
  */
477
484
  function doUnattended(argv, deps) {
478
485
  deps = deps || {};
479
- const opts = parseArgs(argv || []);
486
+ const rawArgv = argv || [];
487
+
488
+ // --watch rejection (headless-default-contract §2) — unattended is detached
489
+ // by definition; passing --watch is a category error. Refuse fast so the
490
+ // user sees a clear message before any state.json / PID work happens.
491
+ if (
492
+ Array.isArray(rawArgv) &&
493
+ rawArgv.some(
494
+ (a) => typeof a === "string" && (a === "--watch" || a.startsWith("--watch=")),
495
+ )
496
+ ) {
497
+ // eslint-disable-next-line no-console
498
+ console.error(
499
+ "[gsd-t-unattended] --watch is incompatible with unattended.\n" +
500
+ "Unattended supervisor is detached by definition.\n" +
501
+ "Run /user:gsd-t-unattended-watch from your interactive session to see live activity.",
502
+ );
503
+ return {
504
+ ok: false,
505
+ dryRun: false,
506
+ exitCode: 2,
507
+ reason: "--watch is incompatible with unattended",
508
+ };
509
+ }
510
+
511
+ const opts = parseArgs(rawArgv);
480
512
  const projectDir = path.resolve(opts.project || ".");
481
513
 
482
514
  // ── Resolve injection points (real impls by default) ─────────────────────
@@ -866,6 +898,16 @@ function runMainLoop(state, dir, opts, deps, ctx) {
866
898
  state.lastWorkerStartedAt = workerStart.toISOString();
867
899
  writeState(state, dir);
868
900
 
901
+ _emit(projectDir, {
902
+ ts: workerStart.toISOString(),
903
+ iter: state.iter,
904
+ type: "task_start",
905
+ source: "supervisor",
906
+ milestone: state.milestone || "",
907
+ wave: state.wave || "",
908
+ task: state.nextTask || "",
909
+ });
910
+
869
911
  let res;
870
912
  try {
871
913
  res = spawnWorker(state, {
@@ -903,6 +945,29 @@ function runMainLoop(state, dir, opts, deps, ctx) {
903
945
  state.lastElapsedMs = elapsedMs;
904
946
  writeState(state, dir);
905
947
 
948
+ // Event-stream: task_complete on success, error on non-zero.
949
+ const durationS = Math.round(elapsedMs / 1000);
950
+ if (exitCode === 0) {
951
+ _emit(projectDir, {
952
+ ts: workerEnd.toISOString(),
953
+ iter: state.iter,
954
+ type: "task_complete",
955
+ source: "supervisor",
956
+ task: state.nextTask || "",
957
+ verdict: "pass",
958
+ duration_s: durationS,
959
+ });
960
+ } else {
961
+ _emit(projectDir, {
962
+ ts: workerEnd.toISOString(),
963
+ iter: state.iter,
964
+ type: "error",
965
+ source: "supervisor",
966
+ error: `worker exit ${exitCode}`,
967
+ recoverable: exitCode !== 4 && exitCode !== 5,
968
+ });
969
+ }
970
+
906
971
  // ── POST-WORKER HOOK (contract §12) ────────────────────────────────────
907
972
  // Read the tail of run.log for pattern detection. ~200 lines is enough
908
973
  // to span the last several iteration blocks for the gutter detector.
@@ -941,6 +1006,13 @@ function runMainLoop(state, dir, opts, deps, ctx) {
941
1006
  break;
942
1007
  }
943
1008
  // Not yet done — continue relay.
1009
+ _emit(projectDir, {
1010
+ iter: state.iter,
1011
+ type: "retry",
1012
+ source: "supervisor",
1013
+ attempt: state.iter,
1014
+ reason: "milestone_incomplete",
1015
+ });
944
1016
  continue;
945
1017
  }
946
1018
  if (exitCode === 4) {
@@ -957,9 +1029,23 @@ function runMainLoop(state, dir, opts, deps, ctx) {
957
1029
  }
958
1030
  if (exitCode === 124) {
959
1031
  // Timeout — continue unless the iter cap is hit on the next check.
1032
+ _emit(projectDir, {
1033
+ iter: state.iter,
1034
+ type: "retry",
1035
+ source: "supervisor",
1036
+ attempt: state.iter,
1037
+ reason: "timeout",
1038
+ });
960
1039
  continue;
961
1040
  }
962
1041
  // Non-terminal (1/2/3) — continue the relay.
1042
+ _emit(projectDir, {
1043
+ iter: state.iter,
1044
+ type: "retry",
1045
+ source: "supervisor",
1046
+ attempt: state.iter,
1047
+ reason: `exit_${exitCode}`,
1048
+ });
963
1049
  }
964
1050
 
965
1051
  // If we exited because the user dropped a stop sentinel and no terminal
@@ -56,6 +56,13 @@ const {
56
56
  notify,
57
57
  } = require("./gsd-t-unattended-platform.js");
58
58
 
59
+ // Event stream (M38 ES) — additive, non-blocking. `_emit` swallows its own
60
+ // errors per unattended-event-stream-contract.md §6.
61
+ const { appendEvent: _esAppendEvent } = require("./event-stream.cjs");
62
+ function _emit(projectDir, ev) {
63
+ try { _esAppendEvent(projectDir, ev); } catch (_) { /* never halt the loop */ }
64
+ }
65
+
59
66
  // ── Constants ───────────────────────────────────────────────────────────────
60
67
 
61
68
  const CONTRACT_VERSION = "1.0.0";
@@ -477,7 +484,32 @@ function finalizeState(state, dir, terminalStatus) {
477
484
  */
478
485
  function doUnattended(argv, deps) {
479
486
  deps = deps || {};
480
- const opts = parseArgs(argv || []);
487
+ const rawArgv = argv || [];
488
+
489
+ // --watch rejection (headless-default-contract §2) — unattended is detached
490
+ // by definition; passing --watch is a category error. Refuse fast so the
491
+ // user sees a clear message before any state.json / PID work happens.
492
+ if (
493
+ Array.isArray(rawArgv) &&
494
+ rawArgv.some(
495
+ (a) => typeof a === "string" && (a === "--watch" || a.startsWith("--watch=")),
496
+ )
497
+ ) {
498
+ // eslint-disable-next-line no-console
499
+ console.error(
500
+ "[gsd-t-unattended] --watch is incompatible with unattended.\n" +
501
+ "Unattended supervisor is detached by definition.\n" +
502
+ "Run /user:gsd-t-unattended-watch from your interactive session to see live activity.",
503
+ );
504
+ return {
505
+ ok: false,
506
+ dryRun: false,
507
+ exitCode: 2,
508
+ reason: "--watch is incompatible with unattended",
509
+ };
510
+ }
511
+
512
+ const opts = parseArgs(rawArgv);
481
513
  const projectDir = path.resolve(opts.project || ".");
482
514
 
483
515
  // ── Resolve injection points (real impls by default) ─────────────────────
@@ -884,6 +916,16 @@ function runMainLoop(state, dir, opts, deps, ctx) {
884
916
  state.lastWorkerStartedAt = workerStart.toISOString();
885
917
  writeState(state, dir);
886
918
 
919
+ _emit(projectDir, {
920
+ ts: workerStart.toISOString(),
921
+ iter: state.iter,
922
+ type: "task_start",
923
+ source: "supervisor",
924
+ milestone: state.milestone || "",
925
+ wave: state.wave || "",
926
+ task: state.nextTask || "",
927
+ });
928
+
887
929
  let res;
888
930
  try {
889
931
  res = spawnWorker(state, {
@@ -921,6 +963,29 @@ function runMainLoop(state, dir, opts, deps, ctx) {
921
963
  state.lastElapsedMs = elapsedMs;
922
964
  writeState(state, dir);
923
965
 
966
+ // Event-stream: task_complete on success, error on non-zero.
967
+ const durationS = Math.round(elapsedMs / 1000);
968
+ if (exitCode === 0) {
969
+ _emit(projectDir, {
970
+ ts: workerEnd.toISOString(),
971
+ iter: state.iter,
972
+ type: "task_complete",
973
+ source: "supervisor",
974
+ task: state.nextTask || "",
975
+ verdict: "pass",
976
+ duration_s: durationS,
977
+ });
978
+ } else {
979
+ _emit(projectDir, {
980
+ ts: workerEnd.toISOString(),
981
+ iter: state.iter,
982
+ type: "error",
983
+ source: "supervisor",
984
+ error: `worker exit ${exitCode}`,
985
+ recoverable: exitCode !== 4 && exitCode !== 5,
986
+ });
987
+ }
988
+
924
989
  // ── POST-WORKER HOOK (contract §12) ────────────────────────────────────
925
990
  // Read the tail of run.log for pattern detection. ~200 lines is enough
926
991
  // to span the last several iteration blocks for the gutter detector.
@@ -959,6 +1024,13 @@ function runMainLoop(state, dir, opts, deps, ctx) {
959
1024
  break;
960
1025
  }
961
1026
  // Not yet done — continue relay.
1027
+ _emit(projectDir, {
1028
+ iter: state.iter,
1029
+ type: "retry",
1030
+ source: "supervisor",
1031
+ attempt: state.iter,
1032
+ reason: "milestone_incomplete",
1033
+ });
962
1034
  continue;
963
1035
  }
964
1036
  if (exitCode === 4) {
@@ -975,9 +1047,23 @@ function runMainLoop(state, dir, opts, deps, ctx) {
975
1047
  }
976
1048
  if (exitCode === 124) {
977
1049
  // Timeout — continue unless the iter cap is hit on the next check.
1050
+ _emit(projectDir, {
1051
+ iter: state.iter,
1052
+ type: "retry",
1053
+ source: "supervisor",
1054
+ attempt: state.iter,
1055
+ reason: "timeout",
1056
+ });
978
1057
  continue;
979
1058
  }
980
1059
  // Non-terminal (1/2/3) — continue the relay.
1060
+ _emit(projectDir, {
1061
+ iter: state.iter,
1062
+ type: "retry",
1063
+ source: "supervisor",
1064
+ attempt: state.iter,
1065
+ reason: `exit_${exitCode}`,
1066
+ });
981
1067
  }
982
1068
 
983
1069
  // If we exited because the user dropped a stop sentinel and no terminal