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.
Files changed (171) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +3 -0
  4. package/dist/headless.js +136 -8
  5. package/dist/help-text.js +3 -0
  6. package/dist/loader.js +33 -4
  7. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  8. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  9. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  10. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  11. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  12. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  13. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  15. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  16. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  19. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  20. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  22. package/dist/resources/extensions/gsd/auto.ts +977 -1551
  23. package/dist/resources/extensions/gsd/commands.ts +3 -3
  24. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  25. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  26. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  27. package/dist/resources/extensions/gsd/export.ts +49 -1
  28. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  29. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  30. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  31. package/dist/resources/extensions/gsd/index.ts +54 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  33. package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  35. package/dist/resources/extensions/gsd/preferences.ts +62 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/reports.ts +510 -0
  39. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  41. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  42. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  43. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  44. package/dist/resources/extensions/gsd/state.ts +30 -0
  45. package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
  46. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  47. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  48. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  49. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  52. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  54. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  55. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  57. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  58. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  59. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  60. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  61. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  62. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  63. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  64. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  65. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  66. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  67. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  68. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  69. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  70. package/dist/resources/extensions/gsd/types.ts +38 -0
  71. package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
  72. package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
  73. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  74. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  75. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  76. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  77. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  78. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  79. package/dist/resources/extensions/subagent/index.ts +46 -1
  80. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  83. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  84. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  85. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  90. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  91. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  92. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  93. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  94. package/packages/pi-tui/dist/components/editor.js +1 -1
  95. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  96. package/packages/pi-tui/src/components/editor.ts +3 -1
  97. package/scripts/link-workspace-packages.cjs +22 -6
  98. package/src/resources/extensions/bg-shell/index.ts +19 -2
  99. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  100. package/src/resources/extensions/bg-shell/types.ts +21 -1
  101. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  102. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  103. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  104. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  105. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  107. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  108. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  109. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  110. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  111. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  113. package/src/resources/extensions/gsd/auto.ts +977 -1551
  114. package/src/resources/extensions/gsd/commands.ts +3 -3
  115. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  116. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  117. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  118. package/src/resources/extensions/gsd/export.ts +49 -1
  119. package/src/resources/extensions/gsd/git-service.ts +6 -0
  120. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  121. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  122. package/src/resources/extensions/gsd/index.ts +54 -1
  123. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  124. package/src/resources/extensions/gsd/observability-validator.ts +21 -0
  125. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  126. package/src/resources/extensions/gsd/preferences.ts +62 -1
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
  128. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  129. package/src/resources/extensions/gsd/reports.ts +510 -0
  130. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  131. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  132. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  133. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  134. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  135. package/src/resources/extensions/gsd/state.ts +30 -0
  136. package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
  137. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  138. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  139. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  140. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  141. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  142. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  143. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  144. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  146. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  147. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  148. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  149. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  150. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  151. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  152. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  153. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  154. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  155. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  156. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  157. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  158. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  159. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  161. package/src/resources/extensions/gsd/types.ts +38 -0
  162. package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
  163. package/src/resources/extensions/gsd/verification-gate.ts +567 -0
  164. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  165. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  166. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  167. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  168. package/src/resources/extensions/shared/format-utils.ts +85 -0
  169. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  170. package/src/resources/extensions/subagent/index.ts +46 -1
  171. 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
@@ -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
- 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
+ }
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
- process.exit(exitCode);
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 before importing cli.js (which imports @gsd/*).
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) && !existsSync(target)) {
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
- symlinkSync(source, target, 'junction');
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,