gsd-pi 2.26.1-next.1 → 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.
Files changed (153) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +2 -0
  4. package/dist/headless.js +99 -7
  5. package/dist/help-text.js +3 -0
  6. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  7. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  8. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  9. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  10. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  11. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  12. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  13. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  14. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  15. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  16. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  17. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  18. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  19. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  20. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  21. package/dist/resources/extensions/gsd/auto.ts +849 -1584
  22. package/dist/resources/extensions/gsd/commands.ts +3 -3
  23. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  24. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  25. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  26. package/dist/resources/extensions/gsd/export.ts +49 -1
  27. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  28. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  29. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  30. package/dist/resources/extensions/gsd/index.ts +54 -1
  31. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  32. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  33. package/dist/resources/extensions/gsd/preferences.ts +20 -1
  34. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  35. package/dist/resources/extensions/gsd/reports.ts +510 -0
  36. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  37. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  38. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  39. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  43. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  44. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  45. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  46. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  47. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  48. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  49. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  50. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  52. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  53. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  54. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  55. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  56. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  57. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  58. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  59. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  60. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  61. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  62. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  63. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  64. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  65. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  66. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  67. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  68. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  69. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  70. package/dist/resources/extensions/subagent/index.ts +46 -1
  71. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  72. package/package.json +1 -1
  73. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  74. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  75. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  76. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  78. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  81. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  82. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  83. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  84. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  85. package/packages/pi-tui/dist/components/editor.js +1 -1
  86. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  87. package/packages/pi-tui/src/components/editor.ts +3 -1
  88. package/src/resources/extensions/bg-shell/index.ts +19 -2
  89. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  90. package/src/resources/extensions/bg-shell/types.ts +21 -1
  91. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  92. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  93. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  94. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  95. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  96. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  97. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  98. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  99. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  100. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  101. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  102. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  103. package/src/resources/extensions/gsd/auto.ts +849 -1584
  104. package/src/resources/extensions/gsd/commands.ts +3 -3
  105. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  106. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  107. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  108. package/src/resources/extensions/gsd/export.ts +49 -1
  109. package/src/resources/extensions/gsd/git-service.ts +6 -0
  110. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  111. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  112. package/src/resources/extensions/gsd/index.ts +54 -1
  113. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  114. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  115. package/src/resources/extensions/gsd/preferences.ts +20 -1
  116. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  117. package/src/resources/extensions/gsd/reports.ts +510 -0
  118. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  119. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  120. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  121. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  122. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  123. package/src/resources/extensions/gsd/state.ts +30 -0
  124. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  125. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  126. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  127. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  128. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  129. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  130. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  131. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  132. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  133. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  134. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  135. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  136. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  137. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  138. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  139. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  140. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  141. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  142. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  143. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  144. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  145. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  146. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  147. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  148. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  149. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  150. package/src/resources/extensions/shared/format-utils.ts +85 -0
  151. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  152. package/src/resources/extensions/subagent/index.ts +46 -1
  153. 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. **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.
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 status
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 || !configuredAvailable) {
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' && (!configuredExists || !configuredAvailable)) {
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
@@ -21,6 +21,8 @@ export interface HeadlessOptions {
21
21
  auto?: boolean;
22
22
  verbose?: boolean;
23
23
  maxRestarts?: number;
24
+ supervised?: boolean;
25
+ responseTimeout?: number;
24
26
  }
25
27
  export declare function parseHeadlessArgs(argv: string[]): HeadlessOptions;
26
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
  // ---------------------------------------------------------------------------
@@ -65,6 +66,17 @@ export function parseHeadlessArgs(argv) {
65
66
  process.exit(1);
66
67
  }
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
+ }
68
80
  }
69
81
  else if (!positionalStarted) {
70
82
  positionalStarted = true;
@@ -77,12 +89,6 @@ export function parseHeadlessArgs(argv) {
77
89
  return options;
78
90
  }
79
91
  // ---------------------------------------------------------------------------
80
- // JSONL Helper
81
- // ---------------------------------------------------------------------------
82
- function serializeJsonLine(obj) {
83
- return JSON.stringify(obj) + '\n';
84
- }
85
- // ---------------------------------------------------------------------------
86
92
  // Extension UI Auto-Responder
87
93
  // ---------------------------------------------------------------------------
88
94
  function handleExtensionUIRequest(event, writeToStdin) {
@@ -184,6 +190,7 @@ function isMilestoneReadyNotification(event) {
184
190
  // ---------------------------------------------------------------------------
185
191
  // Quick Command Detection
186
192
  // ---------------------------------------------------------------------------
193
+ const FIRE_AND_FORGET_METHODS = new Set(['notify', 'setStatus', 'setWidget', 'setTitle', 'set_editor_text']);
187
194
  const QUICK_COMMANDS = new Set([
188
195
  'status', 'queue', 'history', 'hooks', 'export', 'stop', 'pause',
189
196
  'capture', 'skip', 'undo', 'knowledge', 'config', 'prefs',
@@ -194,6 +201,42 @@ function isQuickCommand(command) {
194
201
  return QUICK_COMMANDS.has(command);
195
202
  }
196
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
+ // ---------------------------------------------------------------------------
197
240
  // Main Orchestrator
198
241
  // ---------------------------------------------------------------------------
199
242
  // ---------------------------------------------------------------------------
@@ -254,6 +297,11 @@ async function runHeadlessOnce(options, restartCount) {
254
297
  let interrupted = false;
255
298
  const startTime = Date.now();
256
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
+ }
257
305
  // For new-milestone, load context and bootstrap .gsd/ before spawning RPC child
258
306
  if (isNewMilestone) {
259
307
  if (!options.context && !options.contextText) {
@@ -329,6 +377,17 @@ async function runHeadlessOnce(options, restartCount) {
329
377
  }
330
378
  // Stdin writer for sending extension_ui_response to child
331
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
+ }
332
391
  // Completion promise
333
392
  let resolveCompletion;
334
393
  const completionPromise = new Promise((resolve) => {
@@ -347,6 +406,8 @@ async function runHeadlessOnce(options, restartCount) {
347
406
  }, effectiveIdleTimeout);
348
407
  }
349
408
  }
409
+ // Precompute supervised response timeout
410
+ const responseTimeout = options.responseTimeout ?? 30_000;
350
411
  // Overall timeout
351
412
  const timeoutTimer = setTimeout(() => {
352
413
  process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`);
@@ -381,7 +442,22 @@ async function runHeadlessOnce(options, restartCount) {
381
442
  if (isTerminalNotification(eventObj)) {
382
443
  completed = true;
383
444
  }
384
- handleExtensionUIRequest(eventObj, stdinWriter);
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
+ }
385
461
  // If we detected a terminal notification, resolve after responding
386
462
  if (completed) {
387
463
  exitCode = blocked ? 2 : 0;
@@ -432,6 +508,18 @@ async function runHeadlessOnce(options, restartCount) {
432
508
  stdinWriter = (data) => {
433
509
  internalProcess.stdin.write(data);
434
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
+ }
435
523
  // Detect child process crash
436
524
  internalProcess.on('exit', (code) => {
437
525
  if (!completed) {
@@ -484,6 +572,10 @@ async function runHeadlessOnce(options, restartCount) {
484
572
  clearTimeout(timeoutTimer);
485
573
  if (idleTimer)
486
574
  clearTimeout(idleTimer);
575
+ pendingResponseTimers.forEach((timer) => clearTimeout(timer));
576
+ pendingResponseTimers.clear();
577
+ stopSupervisedReader?.();
578
+ process.stdin.removeListener('close', onStdinClose);
487
579
  process.removeListener('SIGINT', signalHandler);
488
580
  process.removeListener('SIGTERM', signalHandler);
489
581
  await client.stop();
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'),
@@ -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,
@@ -53,6 +53,10 @@ export interface BgProcess {
53
53
  label: string;
54
54
  command: string;
55
55
  cwd: string;
56
+ /** Session file that created this process (used for per-session cleanup) */
57
+ ownerSessionFile: string | null;
58
+ /** Whether this process should survive a new-session boundary */
59
+ persistAcrossSessions: boolean;
56
60
  startedAt: number;
57
61
  proc: import("node:child_process").ChildProcess;
58
62
  /** Unified chronologically-interleaved output buffer */
@@ -103,7 +107,17 @@ export interface BgProcess {
103
107
  /** Restart count */
104
108
  restartCount: number;
105
109
  /** Original start config for restart */
106
- startConfig: { command: string; cwd: string; label: string; processType: ProcessType; readyPattern: string | null; readyPort: number | null; group: string | null };
110
+ startConfig: {
111
+ command: string;
112
+ cwd: string;
113
+ label: string;
114
+ processType: ProcessType;
115
+ ownerSessionFile: string | null;
116
+ persistAcrossSessions: boolean;
117
+ readyPattern: string | null;
118
+ readyPort: number | null;
119
+ group: string | null;
120
+ };
107
121
  }
108
122
 
109
123
  export interface BgProcessInfo {
@@ -111,6 +125,8 @@ export interface BgProcessInfo {
111
125
  label: string;
112
126
  command: string;
113
127
  cwd: string;
128
+ ownerSessionFile: string | null;
129
+ persistAcrossSessions: boolean;
114
130
  startedAt: number;
115
131
  alive: boolean;
116
132
  exitCode: number | null;
@@ -133,6 +149,8 @@ export interface BgProcessInfo {
133
149
  export interface StartOptions {
134
150
  command: string;
135
151
  cwd: string;
152
+ ownerSessionFile?: string | null;
153
+ persistAcrossSessions?: boolean;
136
154
  label?: string;
137
155
  type?: ProcessType;
138
156
  readyPattern?: string;
@@ -154,6 +172,8 @@ export interface ProcessManifest {
154
172
  label: string;
155
173
  command: string;
156
174
  cwd: string;
175
+ ownerSessionFile: string | null;
176
+ persistAcrossSessions: boolean;
157
177
  startedAt: number;
158
178
  processType: ProcessType;
159
179
  group: string | null;