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.
- package/README.md +25 -1
- package/dist/cli.js +62 -4
- package/dist/headless.d.ts +21 -0
- package/dist/headless.js +346 -0
- package/dist/help-text.js +32 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/dist/resources/extensions/gsd/auto.ts +437 -11
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +20 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +20 -1
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/guided-flow.ts +10 -5
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
- package/src/resources/extensions/gsd/auto.ts +437 -11
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +20 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +16 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +20 -1
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/guided-flow.ts +10 -5
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +91 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- 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();
|