gsd-pi 2.22.0 → 2.23.0

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 (128) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +62 -4
  3. package/dist/headless.d.ts +21 -0
  4. package/dist/headless.js +346 -0
  5. package/dist/help-text.js +32 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  11. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  12. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  13. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  14. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  15. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  16. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  17. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  18. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  19. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  20. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  21. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  22. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  23. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  24. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  25. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  26. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  27. package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
  28. package/dist/resources/extensions/gsd/auto.ts +437 -11
  29. package/dist/resources/extensions/gsd/captures.ts +49 -0
  30. package/dist/resources/extensions/gsd/commands.ts +20 -3
  31. package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  32. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  33. package/dist/resources/extensions/gsd/doctor.ts +20 -1
  34. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  35. package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
  36. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  37. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  40. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  41. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  42. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  43. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  44. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  45. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  46. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  47. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  48. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  49. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  50. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  51. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  52. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  55. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  56. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  57. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  58. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  59. package/package.json +1 -1
  60. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  61. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  63. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  65. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  67. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  71. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  73. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/index.js +1 -1
  75. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  76. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  77. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  78. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  79. package/packages/pi-coding-agent/src/index.ts +1 -0
  80. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  81. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  82. package/src/resources/extensions/bg-shell/types.ts +33 -1
  83. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  84. package/src/resources/extensions/browser-tools/index.ts +20 -0
  85. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  86. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  87. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  88. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  89. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  90. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  91. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  92. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  93. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  94. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  95. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  96. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  97. package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
  98. package/src/resources/extensions/gsd/auto.ts +437 -11
  99. package/src/resources/extensions/gsd/captures.ts +49 -0
  100. package/src/resources/extensions/gsd/commands.ts +20 -3
  101. package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
  102. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  103. package/src/resources/extensions/gsd/doctor.ts +20 -1
  104. package/src/resources/extensions/gsd/forensics.ts +95 -52
  105. package/src/resources/extensions/gsd/guided-flow.ts +10 -5
  106. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  107. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  108. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  109. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  110. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  111. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  112. package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
  113. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  114. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  115. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  116. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  117. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
  118. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  119. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  120. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  121. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  122. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  123. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  124. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  125. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  126. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  127. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  128. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Integration test for `gsd headless` CLI subcommand
3
+ *
4
+ * Validates that the headless CLI entry point works end-to-end:
5
+ * 1. Creates a temp dir with a complete .gsd/ project fixture
6
+ * 2. Initializes a git repo in the temp dir
7
+ * 3. Spawns `node dist/loader.js headless --json next` as a child process
8
+ * 4. Waits for the process to exit (with a 5-minute timeout)
9
+ * 5. Validates exit code, JSONL stdout, stderr progress, and task artifact
10
+ *
11
+ * Auth: Uses OAuth credentials from ~/.gsd/agent/auth.json (Claude Code Max).
12
+ * Falls back to ANTHROPIC_API_KEY env var if OAuth is not configured (D013).
13
+ *
14
+ * Usage:
15
+ * npx tsx src/resources/extensions/gsd/tests/integration/headless-command.ts
16
+ * Add --dry-run to validate fixture without running the agent.
17
+ */
18
+
19
+ import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { tmpdir, homedir } from "node:os";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname } from "node:path";
24
+ import { spawn, execSync } from "node:child_process";
25
+
26
+ // ── Configuration ────────────────────────────────────────────────────────────
27
+
28
+ const TIMEOUT_MS = parseInt(process.env.HEADLESS_TIMEOUT_MS ?? "300000", 10); // 5 minutes
29
+ const DRY_RUN = process.argv.includes("--dry-run");
30
+
31
+ // ── Fixture Data ─────────────────────────────────────────────────────────────
32
+ // A complete .gsd/ project state that deriveState() can parse.
33
+ // The trivial task asks the agent to create a single file — zero questions needed.
34
+
35
+ const FIXTURE_PROJECT_MD = `# Project
36
+
37
+ ## What This Is
38
+
39
+ Headless proof test project. A minimal fixture used to validate GSD auto-mode via RPC.
40
+
41
+ ## Core Value
42
+
43
+ Proves headless auto-mode works end-to-end.
44
+
45
+ ## Current State
46
+
47
+ Empty project with GSD milestone planned.
48
+
49
+ ## Architecture / Key Patterns
50
+
51
+ - Single milestone, single slice, single task
52
+
53
+ ## Capability Contract
54
+
55
+ None.
56
+
57
+ ## Milestone Sequence
58
+
59
+ - [ ] M001: Headless Proof — Create a test file to prove the agent loop works
60
+ `;
61
+
62
+ const FIXTURE_STATE_MD = `# GSD State
63
+
64
+ **Active Milestone:** M001 — Headless Proof
65
+ **Active Slice:** S01 — Create Test File
66
+ **Phase:** executing
67
+ **Requirements Status:** 0 active · 0 validated · 0 deferred · 0 out of scope
68
+
69
+ ## Milestone Registry
70
+ - 🔄 **M001:** Headless Proof
71
+
72
+ ## Recent Decisions
73
+ - None recorded
74
+
75
+ ## Blockers
76
+ - None
77
+
78
+ ## Next Action
79
+ Execute T01: Create hello.txt in slice S01.
80
+ `;
81
+
82
+ const FIXTURE_CONTEXT_MD = `# M001: Headless Proof — Context
83
+
84
+ **Gathered:** 2025-01-01
85
+ **Status:** Ready for planning
86
+
87
+ ## Project Description
88
+
89
+ A minimal test project for validating GSD auto-mode in headless/RPC mode.
90
+
91
+ ## Why This Milestone
92
+
93
+ Proves that the agent loop can complete a task without a TUI attached.
94
+
95
+ ## User-Visible Outcome
96
+
97
+ ### When this milestone is complete, the user can:
98
+
99
+ - Run GSD in headless mode and have it complete a trivial task
100
+
101
+ ### Entry point / environment
102
+
103
+ - Entry point: RPC mode via headless-proof.ts
104
+ - Environment: local dev
105
+ - Live dependencies involved: none
106
+
107
+ ## Completion Class
108
+
109
+ - Contract complete means: agent creates the requested file
110
+ - Integration complete means: not applicable
111
+ - Operational complete means: not applicable
112
+
113
+ ## Final Integrated Acceptance
114
+
115
+ To call this milestone complete, we must prove:
116
+
117
+ - Agent creates hello.txt with the correct content
118
+
119
+ ## Risks and Unknowns
120
+
121
+ - None — this is a trivial proof task
122
+
123
+ ## Existing Codebase / Prior Art
124
+
125
+ - None
126
+
127
+ ## Relevant Requirements
128
+
129
+ - None
130
+
131
+ ## Scope
132
+
133
+ ### In Scope
134
+
135
+ - Creating a single file
136
+
137
+ ### Out of Scope / Non-Goals
138
+
139
+ - Everything else
140
+
141
+ ## Technical Constraints
142
+
143
+ - None
144
+
145
+ ## Integration Points
146
+
147
+ - None
148
+
149
+ ## Open Questions
150
+
151
+ - None
152
+ `;
153
+
154
+ const FIXTURE_ROADMAP_MD = `# M001: Headless Proof
155
+
156
+ **Vision:** Prove GSD auto-mode works headlessly.
157
+
158
+ ## Success Criteria
159
+
160
+ - Agent creates hello.txt with content "Hello from headless GSD"
161
+
162
+ ## Key Risks / Unknowns
163
+
164
+ - None
165
+
166
+ ## Slices
167
+
168
+ - [ ] **S01: Create Test File** \`risk:low\` \`depends:[]\`
169
+ > After this: hello.txt exists in the project root
170
+
171
+ ## Boundary Map
172
+
173
+ ### S01
174
+
175
+ Produces:
176
+ - hello.txt file in project root
177
+
178
+ Consumes:
179
+ - nothing (first slice)
180
+ `;
181
+
182
+ const FIXTURE_PLAN_MD = `# S01: Create Test File
183
+
184
+ **Goal:** Create a single file to prove the agent loop works headlessly.
185
+ **Demo:** hello.txt exists with the correct content after the agent runs.
186
+
187
+ ## Must-Haves
188
+
189
+ - hello.txt created with content "Hello from headless GSD"
190
+
191
+ ## Verification
192
+
193
+ - File hello.txt exists in project root with content "Hello from headless GSD"
194
+
195
+ ## Tasks
196
+
197
+ - [ ] **T01: Create hello.txt** \`est:5m\`
198
+ - Why: Proves the agent can execute a tool call and produce an artifact
199
+ - Files: \`hello.txt\`
200
+ - Do: Create a file called hello.txt in the project root with the content "Hello from headless GSD"
201
+ - Verify: File exists with correct content
202
+ - Done when: hello.txt exists with content "Hello from headless GSD"
203
+
204
+ ## Files Likely Touched
205
+
206
+ - \`hello.txt\`
207
+ `;
208
+
209
+ const FIXTURE_TASK_PLAN_MD = `---
210
+ estimated_steps: 1
211
+ estimated_files: 1
212
+ ---
213
+
214
+ # T01: Create hello.txt
215
+
216
+ **Slice:** S01 — Create Test File
217
+ **Milestone:** M001
218
+
219
+ ## Description
220
+
221
+ Create a file called hello.txt in the project root with the content "Hello from headless GSD".
222
+
223
+ ## Steps
224
+
225
+ 1. Create the file hello.txt with the content "Hello from headless GSD"
226
+
227
+ ## Must-Haves
228
+
229
+ - [ ] hello.txt created with content "Hello from headless GSD"
230
+
231
+ ## Verification
232
+
233
+ - File hello.txt exists in project root with content "Hello from headless GSD"
234
+
235
+ ## Expected Output
236
+
237
+ - \`hello.txt\` — file containing "Hello from headless GSD"
238
+ `;
239
+
240
+ // ── Fixture Creation ─────────────────────────────────────────────────────────
241
+
242
+ function createFixture(): string {
243
+ const tmpDir = mkdtempSync(join(tmpdir(), "gsd-headless-cmd-"));
244
+
245
+ // Initialize git repo (GSD requires it for branch-per-slice)
246
+ execSync("git init -b main", { cwd: tmpDir, stdio: "pipe" });
247
+ execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: "pipe" });
248
+ execSync('git config user.name "Test"', { cwd: tmpDir, stdio: "pipe" });
249
+
250
+ // Create .gsd/ structure
251
+ const gsdDir = join(tmpDir, ".gsd");
252
+ const milestonesDir = join(gsdDir, "milestones");
253
+ const m001Dir = join(milestonesDir, "M001");
254
+ const slicesDir = join(m001Dir, "slices");
255
+ const s01Dir = join(slicesDir, "S01");
256
+ const tasksDir = join(s01Dir, "tasks");
257
+
258
+ mkdirSync(tasksDir, { recursive: true });
259
+
260
+ // Write fixture files
261
+ writeFileSync(join(gsdDir, "PROJECT.md"), FIXTURE_PROJECT_MD);
262
+ writeFileSync(join(gsdDir, "STATE.md"), FIXTURE_STATE_MD);
263
+ writeFileSync(join(m001Dir, "M001-CONTEXT.md"), FIXTURE_CONTEXT_MD);
264
+ writeFileSync(join(m001Dir, "M001-ROADMAP.md"), FIXTURE_ROADMAP_MD);
265
+ writeFileSync(join(s01Dir, "S01-PLAN.md"), FIXTURE_PLAN_MD);
266
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), FIXTURE_TASK_PLAN_MD);
267
+
268
+ // Add .gitignore for runtime files
269
+ writeFileSync(join(tmpDir, ".gitignore"), [
270
+ ".gsd/auto.lock",
271
+ ".gsd/completed-units.json",
272
+ ".gsd/metrics.json",
273
+ ".gsd/activity/",
274
+ ".gsd/runtime/",
275
+ ].join("\n") + "\n");
276
+
277
+ // Initial commit so GSD has a clean git state
278
+ execSync("git add -A && git commit -m 'init: headless command test fixture'", {
279
+ cwd: tmpDir,
280
+ stdio: "pipe",
281
+ });
282
+
283
+ return tmpDir;
284
+ }
285
+
286
+ function cleanup(dir: string): void {
287
+ try {
288
+ rmSync(dir, { recursive: true, force: true });
289
+ } catch {
290
+ // Best effort
291
+ console.warn(` [warn] Failed to clean up temp dir: ${dir}`);
292
+ }
293
+ }
294
+
295
+ // ── JSONL Parsing ────────────────────────────────────────────────────────────
296
+
297
+ interface JsonlEvent {
298
+ type?: string;
299
+ [key: string]: unknown;
300
+ }
301
+
302
+ function parseJsonlLines(output: string): JsonlEvent[] {
303
+ const events: JsonlEvent[] = [];
304
+ for (const line of output.split("\n")) {
305
+ const trimmed = line.trim();
306
+ if (!trimmed) continue;
307
+ try {
308
+ events.push(JSON.parse(trimmed) as JsonlEvent);
309
+ } catch {
310
+ // Not valid JSON — skip (could be non-JSONL output)
311
+ }
312
+ }
313
+ return events;
314
+ }
315
+
316
+ // ── Main ─────────────────────────────────────────────────────────────────────
317
+
318
+ async function main(): Promise<void> {
319
+ const __filename = fileURLToPath(import.meta.url);
320
+ const __dirname = dirname(__filename);
321
+ // Resolve gsd-2 repo root (6 levels up from tests/integration/)
322
+ const repoRoot = join(__dirname, "..", "..", "..", "..", "..", "..");
323
+
324
+ console.log("=== GSD Headless Command Integration Test ===\n");
325
+
326
+ // ── Step 1: Create fixture ──────────────────────────────────────────────
327
+ console.log("[1/6] Creating fixture...");
328
+ const fixtureDir = createFixture();
329
+ console.log(` Fixture created at: ${fixtureDir}`);
330
+
331
+ // Validate fixture structure
332
+ const requiredFiles = [
333
+ ".gsd/PROJECT.md",
334
+ ".gsd/STATE.md",
335
+ ".gsd/milestones/M001/M001-CONTEXT.md",
336
+ ".gsd/milestones/M001/M001-ROADMAP.md",
337
+ ".gsd/milestones/M001/slices/S01/S01-PLAN.md",
338
+ ".gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md",
339
+ ];
340
+
341
+ for (const file of requiredFiles) {
342
+ const fullPath = join(fixtureDir, file);
343
+ if (!existsSync(fullPath)) {
344
+ console.error(` FAIL: Missing fixture file: ${file}`);
345
+ cleanup(fixtureDir);
346
+ process.exit(1);
347
+ }
348
+ console.log(` OK ${file}`);
349
+ }
350
+
351
+ // ── Step 2: Validate environment ────────────────────────────────────────
352
+ console.log("\n[2/6] Validating environment...");
353
+
354
+ // Auth: prefer OAuth credentials from ~/.gsd/agent/auth.json (D013).
355
+ // Fall back to ANTHROPIC_API_KEY env var if present.
356
+ const authJsonPath = join(homedir(), ".gsd", "agent", "auth.json");
357
+ let hasOAuth = false;
358
+ if (existsSync(authJsonPath)) {
359
+ try {
360
+ const authData = JSON.parse(readFileSync(authJsonPath, "utf-8"));
361
+ hasOAuth = authData?.anthropic?.type === "oauth";
362
+ } catch {
363
+ // Non-fatal
364
+ }
365
+ }
366
+
367
+ if (hasOAuth) {
368
+ console.log(" OK OAuth credentials found in ~/.gsd/agent/auth.json (Claude Code Max)");
369
+ } else if (process.env.ANTHROPIC_API_KEY) {
370
+ console.log(" OK ANTHROPIC_API_KEY present (env var fallback)");
371
+ } else {
372
+ console.error(" FAIL: No auth available. Need either:");
373
+ console.error(" - OAuth credentials in ~/.gsd/agent/auth.json (Claude Code Max)");
374
+ console.error(" - ANTHROPIC_API_KEY environment variable");
375
+ cleanup(fixtureDir);
376
+ process.exit(1);
377
+ }
378
+
379
+ const loaderPath = join(repoRoot, "dist", "loader.js");
380
+ if (!existsSync(loaderPath)) {
381
+ console.error(` FAIL: CLI not found at ${loaderPath}. Run 'npm run build' first.`);
382
+ cleanup(fixtureDir);
383
+ process.exit(1);
384
+ }
385
+ console.log(` OK CLI found at ${loaderPath}`);
386
+
387
+ // ── Step 3: Dry-run exit ────────────────────────────────────────────────
388
+ if (DRY_RUN) {
389
+ console.log("\n[dry-run] Fixture validated. Skipping headless execution.");
390
+ console.log("[dry-run] All checks passed.\n");
391
+ cleanup(fixtureDir);
392
+ process.exit(0);
393
+ }
394
+
395
+ // ── Step 4: Spawn headless command ──────────────────────────────────────
396
+ console.log("\n[3/6] Spawning headless command...");
397
+ console.log(` Command: node ${loaderPath} headless --json next`);
398
+ console.log(` CWD: ${fixtureDir}`);
399
+ console.log(` Timeout: ${TIMEOUT_MS / 1000}s`);
400
+
401
+ const { exitCode, stdout, stderr } = await new Promise<{
402
+ exitCode: number | null;
403
+ stdout: string;
404
+ stderr: string;
405
+ }>((resolve) => {
406
+ let stdoutBuf = "";
407
+ let stderrBuf = "";
408
+ let settled = false;
409
+
410
+ const child = spawn("node", [loaderPath, "headless", "--json", "next"], {
411
+ cwd: fixtureDir,
412
+ env: { ...process.env },
413
+ stdio: ["ignore", "pipe", "pipe"],
414
+ });
415
+
416
+ child.stdout.on("data", (chunk: Buffer) => {
417
+ stdoutBuf += chunk.toString();
418
+ });
419
+
420
+ child.stderr.on("data", (chunk: Buffer) => {
421
+ const text = chunk.toString();
422
+ stderrBuf += text;
423
+ // Stream stderr for live progress visibility
424
+ process.stderr.write(` [headless] ${text}`);
425
+ });
426
+
427
+ const timer = setTimeout(() => {
428
+ if (!settled) {
429
+ settled = true;
430
+ console.error(`\n TIMEOUT: Process did not exit within ${TIMEOUT_MS / 1000}s. Killing...`);
431
+ child.kill("SIGTERM");
432
+ // Give it a moment to exit gracefully, then force kill
433
+ setTimeout(() => {
434
+ if (!child.killed) child.kill("SIGKILL");
435
+ }, 5000);
436
+ resolve({ exitCode: null, stdout: stdoutBuf, stderr: stderrBuf });
437
+ }
438
+ }, TIMEOUT_MS);
439
+
440
+ child.on("close", (code) => {
441
+ if (!settled) {
442
+ settled = true;
443
+ clearTimeout(timer);
444
+ resolve({ exitCode: code, stdout: stdoutBuf, stderr: stderrBuf });
445
+ }
446
+ });
447
+
448
+ child.on("error", (err) => {
449
+ if (!settled) {
450
+ settled = true;
451
+ clearTimeout(timer);
452
+ stderrBuf += `\nSpawn error: ${err.message}`;
453
+ resolve({ exitCode: 1, stdout: stdoutBuf, stderr: stderrBuf });
454
+ }
455
+ });
456
+ });
457
+
458
+ // ── Step 5: Validate results ────────────────────────────────────────────
459
+ console.log("\n[4/6] Validating process output...");
460
+
461
+ let allPassed = true;
462
+
463
+ // Check 1: Exit code
464
+ const exitOk = exitCode === 0;
465
+ console.log(` ${exitOk ? "PASS" : "FAIL"} Exit code: ${exitCode ?? "null (timeout)"}`);
466
+ if (!exitOk) allPassed = false;
467
+
468
+ // Check 2: stdout contains JSONL events
469
+ const events = parseJsonlLines(stdout);
470
+ const hasJsonlEvents = events.length > 0;
471
+ console.log(` ${hasJsonlEvents ? "PASS" : "FAIL"} JSONL events in stdout: ${events.length}`);
472
+ if (!hasJsonlEvents) allPassed = false;
473
+
474
+ if (hasJsonlEvents) {
475
+ // Summarize event types
476
+ const typeCounts: Record<string, number> = {};
477
+ for (const event of events) {
478
+ const type = String(event.type ?? "unknown");
479
+ typeCounts[type] = (typeCounts[type] ?? 0) + 1;
480
+ }
481
+ console.log(` Event types: ${JSON.stringify(typeCounts)}`);
482
+ }
483
+
484
+ // Check 3: stderr contains progress output
485
+ const hasStderrOutput = stderr.trim().length > 0;
486
+ console.log(` ${hasStderrOutput ? "PASS" : "FAIL"} stderr contains progress output: ${hasStderrOutput} (${stderr.length} bytes)`);
487
+ if (!hasStderrOutput) allPassed = false;
488
+
489
+ // ── Step 6: Verify artifact ─────────────────────────────────────────────
490
+ console.log("\n[5/6] Verifying task artifact...");
491
+
492
+ const helloPath = join(fixtureDir, "hello.txt");
493
+ const artifactExists = existsSync(helloPath);
494
+ console.log(` ${artifactExists ? "PASS" : "FAIL"} hello.txt exists: ${artifactExists}`);
495
+ if (!artifactExists) allPassed = false;
496
+
497
+ if (artifactExists) {
498
+ const content = readFileSync(helloPath, "utf-8").trim();
499
+ const contentMatch = content === "Hello from headless GSD";
500
+ console.log(` ${contentMatch ? "PASS" : "WARN"} hello.txt content: "${content.slice(0, 80)}"`);
501
+ }
502
+
503
+ // ── Summary ─────────────────────────────────────────────────────────────
504
+ console.log("\n[6/6] Summary");
505
+ console.log(` Exit code: ${exitCode ?? "null (timeout)"}`);
506
+ console.log(` JSONL events: ${events.length}`);
507
+ console.log(` stderr length: ${stderr.length} bytes`);
508
+ console.log(` hello.txt exists: ${artifactExists}`);
509
+
510
+ // Cleanup
511
+ cleanup(fixtureDir);
512
+
513
+ if (allPassed) {
514
+ console.log("\n=== PASSED ===\n");
515
+ process.exit(0);
516
+ } else {
517
+ // Print diagnostic info on failure
518
+ if (stdout.length > 0) {
519
+ console.log(`\n--- stdout (last 2000 chars) ---`);
520
+ console.log(stdout.slice(-2000));
521
+ }
522
+ if (stderr.length > 0) {
523
+ console.log(`\n--- stderr (last 2000 chars) ---`);
524
+ console.log(stderr.slice(-2000));
525
+ }
526
+ console.log("\n=== FAILED ===\n");
527
+ process.exit(1);
528
+ }
529
+ }
530
+
531
+ main().catch((err) => {
532
+ console.error("Unhandled error:", err);
533
+ process.exit(1);
534
+ });
@@ -1,5 +1,5 @@
1
1
  import { parseRoadmap } from "../files.ts";
2
- import { parseRoadmapSlices } from "../roadmap-slices.ts";
2
+ import { parseRoadmapSlices, expandDependencies } from "../roadmap-slices.ts";
3
3
  import { createTestContext } from './test-helpers.ts';
4
4
 
5
5
  const { assertEq, assertTrue, report } = createTestContext();
@@ -38,4 +38,46 @@ assertEq(roadmap.title, "M003: Current", "roadmap title preserved");
38
38
  assertEq(roadmap.vision, "Build the thing.", "roadmap vision preserved");
39
39
  assertTrue(roadmap.boundaryMap.length === 1, "boundary map still parsed");
40
40
 
41
+ // ─── expandDependencies unit tests ─────────────────────────────────────
42
+
43
+ console.log("\n=== expandDependencies: plain IDs pass through ===");
44
+ assertEq(expandDependencies([]), [], "empty list");
45
+ assertEq(expandDependencies(["S01"]), ["S01"], "single plain ID");
46
+ assertEq(expandDependencies(["S01", "S03"]), ["S01", "S03"], "multiple plain IDs");
47
+
48
+ console.log("\n=== expandDependencies: dash range expansion ===");
49
+ assertEq(expandDependencies(["S01-S04"]), ["S01", "S02", "S03", "S04"], "S01-S04 expands correctly");
50
+ assertEq(expandDependencies(["S01-S01"]), ["S01"], "single-element range");
51
+ assertEq(expandDependencies(["S03-S05"]), ["S03", "S04", "S05"], "mid-range expansion");
52
+
53
+ console.log("\n=== expandDependencies: dot-range expansion ===");
54
+ assertEq(expandDependencies(["S01..S03"]), ["S01", "S02", "S03"], "S01..S03 dot range");
55
+
56
+ console.log("\n=== expandDependencies: zero-padding preserved ===");
57
+ assertEq(expandDependencies(["S01-S03"]), ["S01", "S02", "S03"], "zero-padded IDs preserved");
58
+
59
+ console.log("\n=== expandDependencies: mixed list ===");
60
+ assertEq(expandDependencies(["S01-S03", "S05"]), ["S01", "S02", "S03", "S05"], "range + plain mixed");
61
+
62
+ console.log("\n=== expandDependencies: invalid range passes through unchanged ===");
63
+ assertEq(expandDependencies(["S04-S01"]), ["S04-S01"], "reversed range not expanded (start > end)");
64
+ assertEq(expandDependencies(["S01-T04"]), ["S01-T04"], "mismatched prefix not expanded");
65
+
66
+ // ─── parseRoadmapSlices: range syntax in depends ─────────────────────
67
+
68
+ console.log("\n=== parseRoadmapSlices: range syntax in depends expanded ===");
69
+ {
70
+ const rangeContent = `# M016: Test\n\n## Slices\n- [x] **S01: A** \`risk:low\` \`depends:[]\`\n- [x] **S02: B** \`risk:low\` \`depends:[]\`\n- [x] **S03: C** \`risk:low\` \`depends:[]\`\n- [x] **S04: D** \`risk:low\` \`depends:[]\`\n- [ ] **S05: E** \`risk:low\` \`depends:[S01-S04]\`\n > After this: all done\n`;
71
+ const rangeSlices = parseRoadmapSlices(rangeContent);
72
+ assertEq(rangeSlices.length, 5, "5 slices parsed");
73
+ assertEq(rangeSlices[4]?.depends, ["S01", "S02", "S03", "S04"], "S01-S04 range expanded to individual IDs");
74
+ }
75
+
76
+ console.log("\n=== parseRoadmapSlices: comma-separated depends still works ===");
77
+ {
78
+ const commaContent = `# M001: Test\n\n## Slices\n- [ ] **S05: E** \`risk:low\` \`depends:[S01,S02,S03,S04]\`\n > After this: done\n`;
79
+ const commaSlices = parseRoadmapSlices(commaContent);
80
+ assertEq(commaSlices[0]?.depends, ["S01", "S02", "S03", "S04"], "comma-separated depends unchanged");
81
+ }
82
+
41
83
  report();