gsd-pi 2.26.0 → 2.27.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 +43 -6
- package/dist/cli.js +4 -2
- package/dist/headless.d.ts +3 -0
- package/dist/headless.js +136 -8
- package/dist/help-text.js +3 -0
- package/dist/loader.js +33 -4
- package/dist/resources/extensions/bg-shell/index.ts +19 -2
- package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/dist/resources/extensions/bg-shell/types.ts +21 -1
- package/dist/resources/extensions/gsd/auto/session.ts +224 -0
- package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
- package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/dist/resources/extensions/gsd/auto.ts +977 -1551
- package/dist/resources/extensions/gsd/commands.ts +3 -3
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/dist/resources/extensions/gsd/export-html.ts +1001 -0
- package/dist/resources/extensions/gsd/export.ts +49 -1
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gitignore.ts +4 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
- package/dist/resources/extensions/gsd/index.ts +54 -1
- package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/dist/resources/extensions/gsd/preferences.ts +62 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/reports.ts +510 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/dist/resources/extensions/gsd/types.ts +38 -0
- package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/dist/resources/extensions/shared/format-utils.ts +85 -0
- package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/dist/resources/extensions/subagent/index.ts +46 -1
- package/dist/resources/extensions/subagent/isolation.ts +9 -6
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +1 -1
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -1
- package/scripts/link-workspace-packages.cjs +22 -6
- package/src/resources/extensions/bg-shell/index.ts +19 -2
- package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
- package/src/resources/extensions/bg-shell/types.ts +21 -1
- package/src/resources/extensions/gsd/auto/session.ts +224 -0
- package/src/resources/extensions/gsd/auto-budget.ts +32 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
- package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
- package/src/resources/extensions/gsd/auto-observability.ts +74 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
- package/src/resources/extensions/gsd/auto.ts +977 -1551
- package/src/resources/extensions/gsd/commands.ts +3 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
- package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
- package/src/resources/extensions/gsd/export-html.ts +1001 -0
- package/src/resources/extensions/gsd/export.ts +49 -1
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gitignore.ts +4 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -5
- package/src/resources/extensions/gsd/index.ts +54 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
- package/src/resources/extensions/gsd/observability-validator.ts +21 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
- package/src/resources/extensions/gsd/preferences.ts +62 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/reports.ts +510 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
- package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
- package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
- package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
- package/src/resources/extensions/gsd/types.ts +38 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
- package/src/resources/extensions/gsd/verification-gate.ts +567 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
- package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
- package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
- package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
- package/src/resources/extensions/shared/format-utils.ts +85 -0
- package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
- package/src/resources/extensions/subagent/index.ts +46 -1
- package/src/resources/extensions/subagent/isolation.ts +9 -6
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
|
|
|
39
39
|
- **[Architecture](./docs/architecture.md)** — system design and dispatch pipeline
|
|
40
40
|
- **[Troubleshooting](./docs/troubleshooting.md)** — common issues, doctor, forensics, recovery
|
|
41
41
|
- **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
|
|
42
|
+
- **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
|
|
42
43
|
- **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
|
|
43
44
|
|
|
44
45
|
---
|
|
@@ -67,6 +68,9 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
|
|
|
67
68
|
| Context injection | "Read this file" | Pre-inlined into dispatch prompt |
|
|
68
69
|
| Roadmap reassessment | Manual | Automatic after each slice completes |
|
|
69
70
|
| Skill discovery | None | Auto-detect and install relevant skills during research |
|
|
71
|
+
| Verification | Manual | Automated verification commands with auto-fix retries |
|
|
72
|
+
| Reporting | None | Self-contained HTML reports with metrics and dep graphs |
|
|
73
|
+
| Parallel execution | None | Multi-worker parallel milestone orchestration |
|
|
70
74
|
|
|
71
75
|
### Migrating from v1
|
|
72
76
|
|
|
@@ -117,7 +121,7 @@ Research → Plan → Execute (per task) → Complete → Reassess Roadmap → N
|
|
|
117
121
|
Validate Milestone → Complete Milestone
|
|
118
122
|
```
|
|
119
123
|
|
|
120
|
-
**Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded. **Complete** writes the summary, UAT script, marks the roadmap, and commits. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
|
|
124
|
+
**Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
|
|
121
125
|
|
|
122
126
|
### `/gsd auto` — The Main Event
|
|
123
127
|
|
|
@@ -137,7 +141,7 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
|
|
|
137
141
|
|
|
138
142
|
3. **Git worktree isolation** — Each milestone runs in its own git worktree with a `milestone/<MID>` branch. All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit.
|
|
139
143
|
|
|
140
|
-
4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context.
|
|
144
|
+
4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context. Parallel orchestrator state is persisted to disk with PID liveness detection, so multi-worker sessions survive crashes too.
|
|
141
145
|
|
|
142
146
|
5. **Stuck detection** — If the same unit dispatches twice (the LLM didn't produce the expected artifact), it retries once with a deep diagnostic. If it fails again, auto mode stops with the exact file it expected.
|
|
143
147
|
|
|
@@ -147,7 +151,11 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
|
|
|
147
151
|
|
|
148
152
|
8. **Adaptive replanning** — After each slice completes, the roadmap is reassessed. If the work revealed new information that changes the plan, slices are reordered, added, or removed before continuing.
|
|
149
153
|
|
|
150
|
-
9. **
|
|
154
|
+
9. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
|
|
155
|
+
|
|
156
|
+
10. **Milestone validation** — After all slices complete, a `validate-milestone` gate compares roadmap success criteria against actual results before sealing the milestone.
|
|
157
|
+
|
|
158
|
+
11. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
|
|
151
159
|
|
|
152
160
|
### `/gsd` and `/gsd next` — Step Mode
|
|
153
161
|
|
|
@@ -233,17 +241,22 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
|
|
|
233
241
|
# Run auto mode in CI
|
|
234
242
|
gsd headless --timeout 600000
|
|
235
243
|
|
|
244
|
+
# Create and execute a milestone end-to-end
|
|
245
|
+
gsd headless new-milestone --context spec.md --auto
|
|
246
|
+
|
|
236
247
|
# One unit at a time (cron-friendly)
|
|
237
248
|
gsd headless next
|
|
238
249
|
|
|
239
|
-
# Machine-readable
|
|
250
|
+
# Machine-readable JSONL event stream
|
|
240
251
|
gsd headless --json status
|
|
241
252
|
|
|
242
253
|
# Force a specific pipeline phase
|
|
243
254
|
gsd headless dispatch plan
|
|
244
255
|
```
|
|
245
256
|
|
|
246
|
-
Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
|
|
257
|
+
Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Auto-restarts on crash with exponential backoff. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
|
|
258
|
+
|
|
259
|
+
**Multi-session orchestration** — headless mode supports file-based IPC in `.gsd/parallel/` for coordinating multiple GSD workers across milestones. Build orchestrators that spawn, monitor, and budget-cap a fleet of GSD workers.
|
|
247
260
|
|
|
248
261
|
### First launch
|
|
249
262
|
|
|
@@ -269,6 +282,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
|
|
|
269
282
|
| `/gsd forensics` | Post-mortem investigation of auto-mode failures |
|
|
270
283
|
| `/gsd cleanup` | Archive phase directories from completed milestones |
|
|
271
284
|
| `/gsd doctor` | Runtime health checks with auto-fix for common issues |
|
|
285
|
+
| `/gsd export --html` | Generate HTML report for current or completed milestone |
|
|
272
286
|
| `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
|
|
273
287
|
| `/voice` | Toggle real-time speech-to-text (macOS, Linux) |
|
|
274
288
|
| `/exit` | Graceful shutdown — saves session state before exiting |
|
|
@@ -277,6 +291,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
|
|
|
277
291
|
| `Ctrl+Alt+G` | Toggle dashboard overlay |
|
|
278
292
|
| `Ctrl+Alt+V` | Toggle voice transcription |
|
|
279
293
|
| `Ctrl+Alt+B` | Show background shell processes |
|
|
294
|
+
| `Alt+V` | Paste clipboard image (macOS) |
|
|
280
295
|
| `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
|
|
281
296
|
| `gsd update` | Update GSD to the latest version |
|
|
282
297
|
| `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
|
|
@@ -321,7 +336,7 @@ gsd/M001/S01 (deleted after merge):
|
|
|
321
336
|
feat(S01/T01): core types and interfaces
|
|
322
337
|
```
|
|
323
338
|
|
|
324
|
-
One squash commit per milestone on main (or whichever branch you started from). The worktree is torn down after merge. Git bisect works. Individual milestones are revertable.
|
|
339
|
+
One squash commit per milestone on main (or whichever branch you started from). The worktree is torn down after merge. Git bisect works. Individual milestones are revertable. Commit messages are generated from task summaries — no more generic "complete task" messages.
|
|
325
340
|
|
|
326
341
|
### Verification
|
|
327
342
|
|
|
@@ -343,6 +358,15 @@ The verification ladder: static checks → command execution → behavioral test
|
|
|
343
358
|
- Cost projections based on completed work
|
|
344
359
|
- Completed and in-progress units
|
|
345
360
|
|
|
361
|
+
### HTML Reports
|
|
362
|
+
|
|
363
|
+
After a milestone completes, GSD auto-generates a self-contained HTML report in `.gsd/reports/`. Each report includes project summary, progress tree, slice dependency graph (SVG DAG), cost/token metrics with bar charts, execution timeline, changelog, and knowledge base sections. No external dependencies — all CSS and JS are inlined, printable to PDF from any browser.
|
|
364
|
+
|
|
365
|
+
An auto-generated `index.html` shows all reports with progression metrics across milestones.
|
|
366
|
+
|
|
367
|
+
- **Automatic** — generated after milestone completion (configurable via `auto_report` preference)
|
|
368
|
+
- **Manual** — run `/gsd export --html` anytime
|
|
369
|
+
|
|
346
370
|
---
|
|
347
371
|
|
|
348
372
|
## Configuration
|
|
@@ -370,6 +394,10 @@ auto_supervisor:
|
|
|
370
394
|
hard_timeout_minutes: 30
|
|
371
395
|
budget_ceiling: 50.00
|
|
372
396
|
unique_milestone_ids: true
|
|
397
|
+
verification_commands:
|
|
398
|
+
- npm run lint
|
|
399
|
+
- npm run test
|
|
400
|
+
auto_report: true
|
|
373
401
|
---
|
|
374
402
|
```
|
|
375
403
|
|
|
@@ -387,6 +415,11 @@ unique_milestone_ids: true
|
|
|
387
415
|
| `skill_staleness_days` | Skills unused for N days get deprioritized (default: 60, 0 = disabled) |
|
|
388
416
|
| `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
|
|
389
417
|
| `git.isolation` | `worktree` (default) or `none` — disable worktree isolation for projects that don't need it |
|
|
418
|
+
| `verification_commands`| Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
|
|
419
|
+
| `verification_auto_fix`| Auto-retry on verification failures (default: true) |
|
|
420
|
+
| `verification_max_retries` | Max retries for verification failures (default: 2) |
|
|
421
|
+
| `require_slice_discussion` | Pause auto-mode before each slice for human discussion review |
|
|
422
|
+
| `auto_report` | Auto-generate HTML reports after milestone completion (default: true) |
|
|
390
423
|
|
|
391
424
|
### Agent Instructions
|
|
392
425
|
|
|
@@ -471,6 +504,10 @@ The best practice for working in teams is to ensure unique milestone names acros
|
|
|
471
504
|
.gsd/runtime/
|
|
472
505
|
# Git worktree working copies
|
|
473
506
|
.gsd/worktrees/
|
|
507
|
+
# Parallel orchestration IPC and worker status
|
|
508
|
+
.gsd/parallel/
|
|
509
|
+
# Generated HTML reports (regenerable via /gsd export --html)
|
|
510
|
+
.gsd/reports/
|
|
474
511
|
# Session-specific interrupted-work markers
|
|
475
512
|
.gsd/milestones/**/continue.md
|
|
476
513
|
.gsd/milestones/**/*-CONTINUE.md
|
package/dist/cli.js
CHANGED
|
@@ -239,7 +239,9 @@ const configuredExists = configuredProvider && configuredModel &&
|
|
|
239
239
|
allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
|
|
240
240
|
const configuredAvailable = configuredProvider && configuredModel &&
|
|
241
241
|
availableModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
|
|
242
|
-
if (!configuredModel || !configuredExists
|
|
242
|
+
if (!configuredModel || !configuredExists) {
|
|
243
|
+
// Model not configured at all, or removed from registry — pick a fallback.
|
|
244
|
+
// Only fires when the model is genuinely unknown (not just temporarily unavailable).
|
|
243
245
|
const piDefault = getPiDefaultModelAndProvider();
|
|
244
246
|
const preferred = (piDefault
|
|
245
247
|
? availableModels.find((m) => m.provider === piDefault.provider && m.id === piDefault.model)
|
|
@@ -254,7 +256,7 @@ if (!configuredModel || !configuredExists || !configuredAvailable) {
|
|
|
254
256
|
settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id);
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
|
-
if (settingsManager.getDefaultThinkingLevel() !== 'off' &&
|
|
259
|
+
if (settingsManager.getDefaultThinkingLevel() !== 'off' && !configuredExists) {
|
|
258
260
|
settingsManager.setDefaultThinkingLevel('off');
|
|
259
261
|
}
|
|
260
262
|
// GSD always uses quiet startup — the gsd extension renders its own branded header
|
package/dist/headless.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface HeadlessOptions {
|
|
|
20
20
|
contextText?: string;
|
|
21
21
|
auto?: boolean;
|
|
22
22
|
verbose?: boolean;
|
|
23
|
+
maxRestarts?: number;
|
|
24
|
+
supervised?: boolean;
|
|
25
|
+
responseTimeout?: number;
|
|
23
26
|
}
|
|
24
27
|
export declare function parseHeadlessArgs(argv: string[]): HeadlessOptions;
|
|
25
28
|
export declare function runHeadless(options: HeadlessOptions): Promise<void>;
|
package/dist/headless.js
CHANGED
|
@@ -15,6 +15,7 @@ import { join, resolve } from 'node:path';
|
|
|
15
15
|
// RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
|
|
16
16
|
// This relative path resolves correctly from both src/ (via tsx) and dist/ (compiled).
|
|
17
17
|
import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client.js';
|
|
18
|
+
import { attachJsonlLineReader, serializeJsonLine } from '../packages/pi-coding-agent/dist/modes/rpc/jsonl.js';
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// CLI Argument Parser
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -58,6 +59,24 @@ export function parseHeadlessArgs(argv) {
|
|
|
58
59
|
else if (arg === '--verbose') {
|
|
59
60
|
options.verbose = true;
|
|
60
61
|
}
|
|
62
|
+
else if (arg === '--max-restarts' && i + 1 < args.length) {
|
|
63
|
+
options.maxRestarts = parseInt(args[++i], 10);
|
|
64
|
+
if (Number.isNaN(options.maxRestarts) || options.maxRestarts < 0) {
|
|
65
|
+
process.stderr.write('[headless] Error: --max-restarts must be a non-negative integer\n');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (arg === '--supervised') {
|
|
70
|
+
options.supervised = true;
|
|
71
|
+
options.json = true; // supervised implies json
|
|
72
|
+
}
|
|
73
|
+
else if (arg === '--response-timeout' && i + 1 < args.length) {
|
|
74
|
+
options.responseTimeout = parseInt(args[++i], 10);
|
|
75
|
+
if (Number.isNaN(options.responseTimeout) || options.responseTimeout <= 0) {
|
|
76
|
+
process.stderr.write('[headless] Error: --response-timeout must be a positive integer (milliseconds)\n');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
61
80
|
}
|
|
62
81
|
else if (!positionalStarted) {
|
|
63
82
|
positionalStarted = true;
|
|
@@ -70,12 +89,6 @@ export function parseHeadlessArgs(argv) {
|
|
|
70
89
|
return options;
|
|
71
90
|
}
|
|
72
91
|
// ---------------------------------------------------------------------------
|
|
73
|
-
// JSONL Helper
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
function serializeJsonLine(obj) {
|
|
76
|
-
return JSON.stringify(obj) + '\n';
|
|
77
|
-
}
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
92
|
// Extension UI Auto-Responder
|
|
80
93
|
// ---------------------------------------------------------------------------
|
|
81
94
|
function handleExtensionUIRequest(event, writeToStdin) {
|
|
@@ -177,6 +190,7 @@ function isMilestoneReadyNotification(event) {
|
|
|
177
190
|
// ---------------------------------------------------------------------------
|
|
178
191
|
// Quick Command Detection
|
|
179
192
|
// ---------------------------------------------------------------------------
|
|
193
|
+
const FIRE_AND_FORGET_METHODS = new Set(['notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text']);
|
|
180
194
|
const QUICK_COMMANDS = new Set([
|
|
181
195
|
'status', 'queue', 'history', 'hooks', 'export', 'stop', 'pause',
|
|
182
196
|
'capture', 'skip', 'undo', 'knowledge', 'config', 'prefs',
|
|
@@ -187,6 +201,42 @@ function isQuickCommand(command) {
|
|
|
187
201
|
return QUICK_COMMANDS.has(command);
|
|
188
202
|
}
|
|
189
203
|
// ---------------------------------------------------------------------------
|
|
204
|
+
// Supervised Stdin Reader
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
function startSupervisedStdinReader(stdinWriter, client, onResponse) {
|
|
207
|
+
return attachJsonlLineReader(process.stdin, (line) => {
|
|
208
|
+
let msg;
|
|
209
|
+
try {
|
|
210
|
+
msg = JSON.parse(line);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
process.stderr.write(`[headless] Warning: invalid JSON from orchestrator stdin, skipping\n`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const type = String(msg.type ?? '');
|
|
217
|
+
switch (type) {
|
|
218
|
+
case 'extension_ui_response':
|
|
219
|
+
stdinWriter(line + '\n');
|
|
220
|
+
if (typeof msg.id === 'string') {
|
|
221
|
+
onResponse(msg.id);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case 'prompt':
|
|
225
|
+
client.prompt(String(msg.message ?? ''));
|
|
226
|
+
break;
|
|
227
|
+
case 'steer':
|
|
228
|
+
client.steer(String(msg.message ?? ''));
|
|
229
|
+
break;
|
|
230
|
+
case 'follow_up':
|
|
231
|
+
client.followUp(String(msg.message ?? ''));
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
process.stderr.write(`[headless] Warning: unknown message type "${type}" from orchestrator stdin\n`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
190
240
|
// Main Orchestrator
|
|
191
241
|
// ---------------------------------------------------------------------------
|
|
192
242
|
// ---------------------------------------------------------------------------
|
|
@@ -220,8 +270,38 @@ function bootstrapGsdProject(basePath) {
|
|
|
220
270
|
mkdirSync(join(gsdDir, 'runtime'), { recursive: true });
|
|
221
271
|
}
|
|
222
272
|
export async function runHeadless(options) {
|
|
273
|
+
const maxRestarts = options.maxRestarts ?? 3;
|
|
274
|
+
let restartCount = 0;
|
|
275
|
+
while (true) {
|
|
276
|
+
const result = await runHeadlessOnce(options, restartCount);
|
|
277
|
+
// Success or blocked — exit normally
|
|
278
|
+
if (result.exitCode === 0 || result.exitCode === 2) {
|
|
279
|
+
process.exit(result.exitCode);
|
|
280
|
+
}
|
|
281
|
+
// Crash/error — check if we should restart
|
|
282
|
+
if (restartCount >= maxRestarts) {
|
|
283
|
+
process.stderr.write(`[headless] Max restarts (${maxRestarts}) reached. Exiting.\n`);
|
|
284
|
+
process.exit(result.exitCode);
|
|
285
|
+
}
|
|
286
|
+
// Don't restart if SIGINT/SIGTERM was received
|
|
287
|
+
if (result.interrupted) {
|
|
288
|
+
process.exit(result.exitCode);
|
|
289
|
+
}
|
|
290
|
+
restartCount++;
|
|
291
|
+
const backoffMs = Math.min(5000 * restartCount, 30_000);
|
|
292
|
+
process.stderr.write(`[headless] Restarting in ${(backoffMs / 1000).toFixed(0)}s (attempt ${restartCount}/${maxRestarts})...\n`);
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function runHeadlessOnce(options, restartCount) {
|
|
297
|
+
let interrupted = false;
|
|
223
298
|
const startTime = Date.now();
|
|
224
299
|
const isNewMilestone = options.command === 'new-milestone';
|
|
300
|
+
// Supervised mode cannot share stdin with --context -
|
|
301
|
+
if (options.supervised && options.context === '-') {
|
|
302
|
+
process.stderr.write('[headless] Error: --supervised cannot be used with --context - (both require stdin)\n');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
225
305
|
// For new-milestone, load context and bootstrap .gsd/ before spawning RPC child
|
|
226
306
|
if (isNewMilestone) {
|
|
227
307
|
if (!options.context && !options.contextText) {
|
|
@@ -297,6 +377,17 @@ export async function runHeadless(options) {
|
|
|
297
377
|
}
|
|
298
378
|
// Stdin writer for sending extension_ui_response to child
|
|
299
379
|
let stdinWriter = null;
|
|
380
|
+
// Supervised mode state
|
|
381
|
+
const pendingResponseTimers = new Map();
|
|
382
|
+
let supervisedFallback = false;
|
|
383
|
+
let stopSupervisedReader = null;
|
|
384
|
+
const onStdinClose = () => {
|
|
385
|
+
supervisedFallback = true;
|
|
386
|
+
process.stderr.write('[headless] Warning: orchestrator stdin closed, falling back to auto-response\n');
|
|
387
|
+
};
|
|
388
|
+
if (options.supervised) {
|
|
389
|
+
process.stdin.on('close', onStdinClose);
|
|
390
|
+
}
|
|
300
391
|
// Completion promise
|
|
301
392
|
let resolveCompletion;
|
|
302
393
|
const completionPromise = new Promise((resolve) => {
|
|
@@ -315,6 +406,8 @@ export async function runHeadless(options) {
|
|
|
315
406
|
}, effectiveIdleTimeout);
|
|
316
407
|
}
|
|
317
408
|
}
|
|
409
|
+
// Precompute supervised response timeout
|
|
410
|
+
const responseTimeout = options.responseTimeout ?? 30_000;
|
|
318
411
|
// Overall timeout
|
|
319
412
|
const timeoutTimer = setTimeout(() => {
|
|
320
413
|
process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`);
|
|
@@ -349,7 +442,22 @@ export async function runHeadless(options) {
|
|
|
349
442
|
if (isTerminalNotification(eventObj)) {
|
|
350
443
|
completed = true;
|
|
351
444
|
}
|
|
352
|
-
|
|
445
|
+
const method = String(eventObj.method ?? '');
|
|
446
|
+
const shouldSupervise = options.supervised && !supervisedFallback
|
|
447
|
+
&& !FIRE_AND_FORGET_METHODS.has(method);
|
|
448
|
+
if (shouldSupervise) {
|
|
449
|
+
// Interactive request in supervised mode — let orchestrator respond
|
|
450
|
+
const eventId = String(eventObj.id ?? '');
|
|
451
|
+
const timer = setTimeout(() => {
|
|
452
|
+
pendingResponseTimers.delete(eventId);
|
|
453
|
+
handleExtensionUIRequest(eventObj, stdinWriter);
|
|
454
|
+
process.stdout.write(JSON.stringify({ type: 'supervised_timeout', id: eventId, method }) + '\n');
|
|
455
|
+
}, responseTimeout);
|
|
456
|
+
pendingResponseTimers.set(eventId, timer);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
handleExtensionUIRequest(eventObj, stdinWriter);
|
|
460
|
+
}
|
|
353
461
|
// If we detected a terminal notification, resolve after responding
|
|
354
462
|
if (completed) {
|
|
355
463
|
exitCode = blocked ? 2 : 0;
|
|
@@ -369,6 +477,7 @@ export async function runHeadless(options) {
|
|
|
369
477
|
// Signal handling
|
|
370
478
|
const signalHandler = () => {
|
|
371
479
|
process.stderr.write('\n[headless] Interrupted, stopping child process...\n');
|
|
480
|
+
interrupted = true;
|
|
372
481
|
exitCode = 1;
|
|
373
482
|
client.stop().finally(() => {
|
|
374
483
|
clearTimeout(timeoutTimer);
|
|
@@ -399,6 +508,18 @@ export async function runHeadless(options) {
|
|
|
399
508
|
stdinWriter = (data) => {
|
|
400
509
|
internalProcess.stdin.write(data);
|
|
401
510
|
};
|
|
511
|
+
// Start supervised stdin reader for orchestrator commands
|
|
512
|
+
if (options.supervised) {
|
|
513
|
+
stopSupervisedReader = startSupervisedStdinReader(stdinWriter, client, (id) => {
|
|
514
|
+
const timer = pendingResponseTimers.get(id);
|
|
515
|
+
if (timer) {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
pendingResponseTimers.delete(id);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// Ensure stdin is in flowing mode for JSONL reading
|
|
521
|
+
process.stdin.resume();
|
|
522
|
+
}
|
|
402
523
|
// Detect child process crash
|
|
403
524
|
internalProcess.on('exit', (code) => {
|
|
404
525
|
if (!completed) {
|
|
@@ -451,6 +572,10 @@ export async function runHeadless(options) {
|
|
|
451
572
|
clearTimeout(timeoutTimer);
|
|
452
573
|
if (idleTimer)
|
|
453
574
|
clearTimeout(idleTimer);
|
|
575
|
+
pendingResponseTimers.forEach((timer) => clearTimeout(timer));
|
|
576
|
+
pendingResponseTimers.clear();
|
|
577
|
+
stopSupervisedReader?.();
|
|
578
|
+
process.stdin.removeListener('close', onStdinClose);
|
|
454
579
|
process.removeListener('SIGINT', signalHandler);
|
|
455
580
|
process.removeListener('SIGTERM', signalHandler);
|
|
456
581
|
await client.stop();
|
|
@@ -460,6 +585,9 @@ export async function runHeadless(options) {
|
|
|
460
585
|
process.stderr.write(`[headless] Status: ${status}\n`);
|
|
461
586
|
process.stderr.write(`[headless] Duration: ${duration}s\n`);
|
|
462
587
|
process.stderr.write(`[headless] Events: ${totalEvents} total, ${toolCallCount} tool calls\n`);
|
|
588
|
+
if (restartCount > 0) {
|
|
589
|
+
process.stderr.write(`[headless] Restarts: ${restartCount}\n`);
|
|
590
|
+
}
|
|
463
591
|
// On failure, print last 5 events for diagnostics
|
|
464
592
|
if (exitCode !== 0) {
|
|
465
593
|
const lastFive = recentEvents.slice(-5);
|
|
@@ -470,5 +598,5 @@ export async function runHeadless(options) {
|
|
|
470
598
|
}
|
|
471
599
|
}
|
|
472
600
|
}
|
|
473
|
-
|
|
601
|
+
return { exitCode, interrupted };
|
|
474
602
|
}
|
package/dist/help-text.js
CHANGED
|
@@ -38,6 +38,8 @@ const SUBCOMMAND_HELP = {
|
|
|
38
38
|
' --timeout N Overall timeout in ms (default: 300000)',
|
|
39
39
|
' --json JSONL event stream to stdout',
|
|
40
40
|
' --model ID Override model',
|
|
41
|
+
' --supervised Forward interactive UI requests to orchestrator via stdout/stdin',
|
|
42
|
+
' --response-timeout N Timeout (ms) for orchestrator response (default: 30000)',
|
|
41
43
|
'',
|
|
42
44
|
'Commands:',
|
|
43
45
|
' auto Run all queued units continuously (default)',
|
|
@@ -59,6 +61,7 @@ const SUBCOMMAND_HELP = {
|
|
|
59
61
|
' gsd headless new-milestone --context spec.md Create milestone from file',
|
|
60
62
|
' cat spec.md | gsd headless new-milestone --context - From stdin',
|
|
61
63
|
' gsd headless new-milestone --context spec.md --auto Create + auto-execute',
|
|
64
|
+
' gsd headless --supervised auto Supervised orchestrator mode',
|
|
62
65
|
'',
|
|
63
66
|
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
|
|
64
67
|
].join('\n'),
|
package/dist/loader.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { dirname, resolve, join, delimiter } from 'path';
|
|
6
|
-
import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync, cpSync } from 'fs';
|
|
7
7
|
// Fast-path: handle --version/-v and --help/-h before importing any heavy
|
|
8
8
|
// dependencies. This avoids loading the entire pi-coding-agent barrel import
|
|
9
9
|
// (~1s) just to print a version string.
|
|
@@ -137,8 +137,12 @@ if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy
|
|
|
137
137
|
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici');
|
|
138
138
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
139
139
|
}
|
|
140
|
-
// Ensure workspace packages are linked
|
|
140
|
+
// Ensure workspace packages are linked (or copied on Windows) before importing
|
|
141
|
+
// cli.js (which imports @gsd/*).
|
|
141
142
|
// npm postinstall handles this normally, but npx --ignore-scripts skips postinstall.
|
|
143
|
+
// On Windows without Developer Mode or admin rights, symlinkSync will throw even for
|
|
144
|
+
// 'junction' type — so we fall back to cpSync (a full directory copy) which works
|
|
145
|
+
// everywhere without elevated permissions.
|
|
142
146
|
const gsdScopeDir = join(gsdNodeModules, '@gsd');
|
|
143
147
|
const packagesDir = join(gsdRoot, 'packages');
|
|
144
148
|
const wsPackages = ['native', 'pi-agent-core', 'pi-ai', 'pi-coding-agent', 'pi-tui'];
|
|
@@ -148,14 +152,39 @@ try {
|
|
|
148
152
|
for (const pkg of wsPackages) {
|
|
149
153
|
const target = join(gsdScopeDir, pkg);
|
|
150
154
|
const source = join(packagesDir, pkg);
|
|
151
|
-
if (existsSync(source)
|
|
155
|
+
if (!existsSync(source) || existsSync(target))
|
|
156
|
+
continue;
|
|
157
|
+
try {
|
|
158
|
+
symlinkSync(source, target, 'junction');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Symlink failed (common on Windows without Developer Mode / admin).
|
|
162
|
+
// Fall back to a directory copy — slower on first run but universally works.
|
|
152
163
|
try {
|
|
153
|
-
|
|
164
|
+
cpSync(source, target, { recursive: true });
|
|
154
165
|
}
|
|
155
166
|
catch { /* non-fatal */ }
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
catch { /* non-fatal */ }
|
|
171
|
+
// Validate critical workspace packages are resolvable. If still missing after the
|
|
172
|
+
// symlink+copy attempts, emit a clear diagnostic instead of a cryptic
|
|
173
|
+
// ERR_MODULE_NOT_FOUND from deep inside cli.js.
|
|
174
|
+
const criticalPackages = ['pi-coding-agent'];
|
|
175
|
+
const missingPackages = criticalPackages.filter(pkg => !existsSync(join(gsdScopeDir, pkg)));
|
|
176
|
+
if (missingPackages.length > 0) {
|
|
177
|
+
const missing = missingPackages.map(p => `@gsd/${p}`).join(', ');
|
|
178
|
+
process.stderr.write(`\nError: GSD installation is broken — missing packages: ${missing}\n\n` +
|
|
179
|
+
`This is usually caused by one of:\n` +
|
|
180
|
+
` • An outdated version installed from npm (run: npm install -g gsd-pi@latest)\n` +
|
|
181
|
+
` • The packages/ directory was excluded from the installed tarball\n` +
|
|
182
|
+
` • A filesystem error prevented linking or copying the workspace packages\n\n` +
|
|
183
|
+
`Fix it by reinstalling:\n\n` +
|
|
184
|
+
` npm install -g gsd-pi@latest\n\n` +
|
|
185
|
+
`If the issue persists, please open an issue at:\n` +
|
|
186
|
+
` https://github.com/gsd-build/gsd-2/issues\n`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
160
189
|
// Dynamic import defers ESM evaluation — config.js will see PI_PACKAGE_DIR above
|
|
161
190
|
await import('./cli.js');
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
getGroupStatus,
|
|
53
53
|
pruneDeadProcesses,
|
|
54
54
|
cleanupAll,
|
|
55
|
+
cleanupSessionProcesses,
|
|
55
56
|
persistManifest,
|
|
56
57
|
loadManifest,
|
|
57
58
|
pushAlert,
|
|
@@ -71,7 +72,7 @@ import { toPosixPath } from "../shared/path-display.js";
|
|
|
71
72
|
// ── Re-exports for consumers ───────────────────────────────────────────────
|
|
72
73
|
|
|
73
74
|
export type { ProcessStatus, ProcessType, BgProcess, BgProcessInfo, OutputDigest, OutputLine, ProcessEvent } from "./types.js";
|
|
74
|
-
export { processes, startProcess, killProcess, restartProcess, cleanupAll } from "./process-manager.js";
|
|
75
|
+
export { processes, startProcess, killProcess, restartProcess, cleanupAll, cleanupSessionProcesses } from "./process-manager.js";
|
|
75
76
|
export { generateDigest, getHighlights, getOutput, formatDigestText } from "./output-formatter.js";
|
|
76
77
|
export { waitForReady, probePort } from "./readiness-detector.js";
|
|
77
78
|
export { sendAndWait, runOnSession, queryShellEnv } from "./interaction.js";
|
|
@@ -136,7 +137,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
// Session switch resets the agent's context.
|
|
139
|
-
pi.on("session_switch", async () => {
|
|
140
|
+
pi.on("session_switch", async (event, ctx) => {
|
|
141
|
+
latestCtx = ctx;
|
|
142
|
+
if (event.reason === "new" && event.previousSessionFile) {
|
|
143
|
+
await cleanupSessionProcesses(event.previousSessionFile);
|
|
144
|
+
syncLatestCtxCwd();
|
|
145
|
+
if (latestCtx) persistManifest(latestCtx.cwd);
|
|
146
|
+
}
|
|
140
147
|
buildProcessStateAlert("Session was switched.");
|
|
141
148
|
});
|
|
142
149
|
|
|
@@ -232,6 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
232
239
|
"Use 'run' to execute a command on a persistent shell session and block until it completes — returns structured output + exit code. Shell state (env vars, cwd, virtualenvs) persists across runs.",
|
|
233
240
|
"Use 'send_and_wait' for interactive CLIs: send input and wait for expected output pattern.",
|
|
234
241
|
"Use 'env' to check the current working directory and active environment variables of a shell session — useful after cd, source, or export commands.",
|
|
242
|
+
"Background processes are session-scoped by default: a new session reaps them unless you set persist_across_sessions:true.",
|
|
235
243
|
"Use 'restart' to kill and relaunch with the same config — preserves restart count.",
|
|
236
244
|
"Background processes are auto-classified (server/build/test/watcher) based on the command.",
|
|
237
245
|
"Process crashes and errors are automatically surfaced as alerts at the start of your next turn — you don't need to poll.",
|
|
@@ -300,6 +308,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
300
308
|
group: Type.Optional(
|
|
301
309
|
Type.String({ description: "Group name for related processes (for start, group_status)" }),
|
|
302
310
|
),
|
|
311
|
+
persist_across_sessions: Type.Optional(
|
|
312
|
+
Type.Boolean({
|
|
313
|
+
description: "Keep this process running after a new session starts. Default: false.",
|
|
314
|
+
default: false,
|
|
315
|
+
}),
|
|
316
|
+
),
|
|
303
317
|
}),
|
|
304
318
|
|
|
305
319
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -318,6 +332,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
318
332
|
const bg = startProcess({
|
|
319
333
|
command: params.command,
|
|
320
334
|
cwd: ctx.cwd,
|
|
335
|
+
ownerSessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
336
|
+
persistAcrossSessions: params.persist_across_sessions ?? false,
|
|
321
337
|
label: params.label,
|
|
322
338
|
type: params.type as ProcessType | undefined,
|
|
323
339
|
readyPattern: params.ready_pattern,
|
|
@@ -341,6 +357,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
357
|
text += ` cwd: ${toPosixPath(bg.cwd)}`;
|
|
342
358
|
|
|
343
359
|
if (bg.group) text += `\n group: ${bg.group}`;
|
|
360
|
+
if (bg.persistAcrossSessions) text += `\n persist_across_sessions: true`;
|
|
344
361
|
if (bg.readyPort) text += `\n ready_port: ${bg.readyPort}`;
|
|
345
362
|
if (bg.readyPattern) text += `\n ready_pattern: ${bg.readyPattern}`;
|
|
346
363
|
if (bg.ports.length > 0) text += `\n detected ports: ${bg.ports.join(", ")}`;
|
|
@@ -67,6 +67,8 @@ export function getInfo(p: BgProcess): BgProcessInfo {
|
|
|
67
67
|
label: p.label,
|
|
68
68
|
command: p.command,
|
|
69
69
|
cwd: p.cwd,
|
|
70
|
+
ownerSessionFile: p.ownerSessionFile,
|
|
71
|
+
persistAcrossSessions: p.persistAcrossSessions,
|
|
70
72
|
startedAt: p.startedAt,
|
|
71
73
|
alive: p.alive,
|
|
72
74
|
exitCode: p.exitCode,
|
|
@@ -138,6 +140,8 @@ export function startProcess(opts: StartOptions): BgProcess {
|
|
|
138
140
|
label: opts.label || command.slice(0, 60),
|
|
139
141
|
command,
|
|
140
142
|
cwd: opts.cwd,
|
|
143
|
+
ownerSessionFile: opts.ownerSessionFile ?? null,
|
|
144
|
+
persistAcrossSessions: opts.persistAcrossSessions ?? false,
|
|
141
145
|
startedAt: Date.now(),
|
|
142
146
|
proc,
|
|
143
147
|
output: [],
|
|
@@ -170,6 +174,8 @@ export function startProcess(opts: StartOptions): BgProcess {
|
|
|
170
174
|
cwd: opts.cwd,
|
|
171
175
|
label: opts.label || command.slice(0, 60),
|
|
172
176
|
processType,
|
|
177
|
+
ownerSessionFile: opts.ownerSessionFile ?? null,
|
|
178
|
+
persistAcrossSessions: opts.persistAcrossSessions ?? false,
|
|
173
179
|
readyPattern: opts.readyPattern || null,
|
|
174
180
|
readyPort: opts.readyPort || null,
|
|
175
181
|
group: opts.group || null,
|
|
@@ -312,6 +318,8 @@ export async function restartProcess(id: string): Promise<BgProcess | null> {
|
|
|
312
318
|
cwd: config.cwd,
|
|
313
319
|
label: config.label,
|
|
314
320
|
type: config.processType,
|
|
321
|
+
ownerSessionFile: config.ownerSessionFile,
|
|
322
|
+
persistAcrossSessions: config.persistAcrossSessions,
|
|
315
323
|
readyPattern: config.readyPattern || undefined,
|
|
316
324
|
readyPort: config.readyPort || undefined,
|
|
317
325
|
group: config.group || undefined,
|
|
@@ -367,6 +375,41 @@ export function cleanupAll(): void {
|
|
|
367
375
|
processes.clear();
|
|
368
376
|
}
|
|
369
377
|
|
|
378
|
+
async function waitForProcessExit(bg: BgProcess, timeoutMs: number): Promise<boolean> {
|
|
379
|
+
if (!bg.alive) return true;
|
|
380
|
+
await new Promise<void>((resolve) => {
|
|
381
|
+
const done = () => resolve();
|
|
382
|
+
const timer = setTimeout(done, timeoutMs);
|
|
383
|
+
bg.proc.once("exit", () => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
resolve();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return !bg.alive;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function cleanupSessionProcesses(
|
|
392
|
+
sessionFile: string,
|
|
393
|
+
options?: { graceMs?: number },
|
|
394
|
+
): Promise<string[]> {
|
|
395
|
+
const graceMs = Math.max(0, options?.graceMs ?? 300);
|
|
396
|
+
const matches = Array.from(processes.values()).filter(
|
|
397
|
+
(bg) => bg.alive && !bg.persistAcrossSessions && bg.ownerSessionFile === sessionFile,
|
|
398
|
+
);
|
|
399
|
+
if (matches.length === 0) return [];
|
|
400
|
+
|
|
401
|
+
for (const bg of matches) {
|
|
402
|
+
killProcess(bg.id, "SIGTERM");
|
|
403
|
+
}
|
|
404
|
+
if (graceMs > 0) {
|
|
405
|
+
await Promise.all(matches.map((bg) => waitForProcessExit(bg, graceMs)));
|
|
406
|
+
}
|
|
407
|
+
for (const bg of matches) {
|
|
408
|
+
if (bg.alive) killProcess(bg.id, "SIGKILL");
|
|
409
|
+
}
|
|
410
|
+
return matches.map((bg) => bg.id);
|
|
411
|
+
}
|
|
412
|
+
|
|
370
413
|
// ── Persistence ────────────────────────────────────────────────────────────
|
|
371
414
|
|
|
372
415
|
export function getManifestPath(cwd: string): string {
|
|
@@ -384,6 +427,8 @@ export function persistManifest(cwd: string): void {
|
|
|
384
427
|
label: p.label,
|
|
385
428
|
command: p.command,
|
|
386
429
|
cwd: p.cwd,
|
|
430
|
+
ownerSessionFile: p.ownerSessionFile,
|
|
431
|
+
persistAcrossSessions: p.persistAcrossSessions,
|
|
387
432
|
startedAt: p.startedAt,
|
|
388
433
|
processType: p.processType,
|
|
389
434
|
group: p.group,
|