gsd-pi 2.22.0 → 2.24.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 (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
package/README.md CHANGED
@@ -221,6 +221,26 @@ gsd
221
221
 
222
222
  Both terminals read and write the same `.gsd/` files on disk. Your decisions in terminal 2 are picked up automatically at the next phase boundary — no need to stop auto mode.
223
223
 
224
+ ### Headless mode — CI and scripts
225
+
226
+ `gsd headless` runs any `/gsd` command without a TUI. Designed for CI pipelines, cron jobs, and scripted automation.
227
+
228
+ ```bash
229
+ # Run auto mode in CI
230
+ gsd headless --timeout 600000
231
+
232
+ # One unit at a time (cron-friendly)
233
+ gsd headless next
234
+
235
+ # Machine-readable status
236
+ gsd headless --json status
237
+
238
+ # Force a specific pipeline phase
239
+ gsd headless dispatch plan
240
+ ```
241
+
242
+ 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.
243
+
224
244
  ### First launch
225
245
 
226
246
  On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. If you have an existing Pi installation, your provider credentials (LLM and tool keys) are imported automatically. Run `gsd config` anytime to re-run the wizard.
@@ -254,7 +274,10 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
254
274
  | `Ctrl+Alt+V` | Toggle voice transcription |
255
275
  | `Ctrl+Alt+B` | Show background shell processes |
256
276
  | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
277
+ | `gsd update` | Update GSD to the latest version |
278
+ | `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
257
279
  | `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
280
+ | `gsd sessions` | Interactive session picker — browse and resume any saved session |
258
281
 
259
282
  ---
260
283
 
@@ -396,7 +419,7 @@ GSD ships with 14 extensions, all loaded automatically:
396
419
  | Extension | What it provides |
397
420
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
398
421
  | **GSD** | Core workflow engine, auto mode, commands, dashboard |
399
- | **Browser Tools** | Playwright-based browser with form intelligence, intent-ranked element finding, and semantic actions |
422
+ | **Browser Tools** | Playwright-based browser with form intelligence, intent-ranked element finding, semantic actions, PDF export, session state persistence, network mocking, device emulation, structured extraction, visual diffing, region zoom, test code generation, and prompt injection detection |
400
423
  | **Search the Web** | Brave Search, Tavily, or Jina page extraction |
401
424
  | **Google Search** | Gemini-powered web search with AI-synthesized answers |
402
425
  | **Context7** | Up-to-date library/framework documentation |
@@ -482,6 +505,7 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
482
505
  gsd (CLI binary)
483
506
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
484
507
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
508
+ ├─ headless.ts Headless orchestrator (spawns RPC child, auto-responds, detects completion)
485
509
  ├─ onboarding.ts First-run setup wizard (LLM provider + tool keys)
486
510
  ├─ wizard.ts Env hydration from stored auth.json credentials
487
511
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { loadStoredEnvKeys } from './wizard.js';
8
8
  import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js';
9
9
  import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
10
10
  import chalk from 'chalk';
11
- import { checkForUpdates } from './update-check.js';
11
+ import { checkForUpdates, checkAndPromptForUpdates } from './update-check.js';
12
12
  import { printHelp, printSubcommandHelp } from './help-text.js';
13
13
  function exitIfManagedResourcesAreNewer(currentAgentDir) {
14
14
  const currentVersion = process.env.GSD_VERSION || '0.0.0';
@@ -91,6 +91,59 @@ if (cliFlags.messages[0] === 'update') {
91
91
  await runUpdate();
92
92
  process.exit(0);
93
93
  }
94
+ // `gsd sessions` — list past sessions and pick one to resume
95
+ if (cliFlags.messages[0] === 'sessions') {
96
+ const cwd = process.cwd();
97
+ const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
98
+ const projectSessionsDir = join(sessionsDir, safePath);
99
+ process.stderr.write(chalk.dim(`Loading sessions for ${cwd}...\n`));
100
+ const sessions = await SessionManager.list(cwd, projectSessionsDir);
101
+ if (sessions.length === 0) {
102
+ process.stderr.write(chalk.yellow('No sessions found for this directory.\n'));
103
+ process.exit(0);
104
+ }
105
+ process.stderr.write(chalk.bold(`\n Sessions (${sessions.length}):\n\n`));
106
+ const maxShow = 20;
107
+ const toShow = sessions.slice(0, maxShow);
108
+ for (let i = 0; i < toShow.length; i++) {
109
+ const s = toShow[i];
110
+ const date = s.modified.toLocaleString();
111
+ const msgs = s.messageCount;
112
+ const name = s.name ? ` ${chalk.cyan(s.name)}` : '';
113
+ const preview = s.firstMessage
114
+ ? s.firstMessage.replace(/\n/g, ' ').substring(0, 80)
115
+ : chalk.dim('(empty)');
116
+ const num = String(i + 1).padStart(3);
117
+ process.stderr.write(` ${chalk.bold(num)}. ${chalk.green(date)} ${chalk.dim(`(${msgs} msgs)`)}${name}\n`);
118
+ process.stderr.write(` ${chalk.dim(preview)}\n\n`);
119
+ }
120
+ if (sessions.length > maxShow) {
121
+ process.stderr.write(chalk.dim(` ... and ${sessions.length - maxShow} more\n\n`));
122
+ }
123
+ // Interactive selection
124
+ const readline = await import('node:readline');
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
126
+ const answer = await new Promise((resolve) => {
127
+ rl.question(chalk.bold(' Enter session number to resume (or q to quit): '), resolve);
128
+ });
129
+ rl.close();
130
+ const choice = parseInt(answer, 10);
131
+ if (isNaN(choice) || choice < 1 || choice > toShow.length) {
132
+ process.stderr.write(chalk.dim('Cancelled.\n'));
133
+ process.exit(0);
134
+ }
135
+ const selected = toShow[choice - 1];
136
+ process.stderr.write(chalk.green(`\nResuming session from ${selected.modified.toLocaleString()}...\n\n`));
137
+ // Mark for the interactive session below to open this specific session
138
+ cliFlags.continue = true;
139
+ cliFlags._selectedSessionPath = selected.path;
140
+ }
141
+ // `gsd headless` — run auto-mode without TUI
142
+ if (cliFlags.messages[0] === 'headless') {
143
+ const { runHeadless, parseHeadlessArgs } = await import('./headless.js');
144
+ await runHeadless(parseHeadlessArgs(process.argv));
145
+ process.exit(0);
146
+ }
94
147
  // Pi's tool bootstrap can mis-detect already-installed fd/rg on some systems
95
148
  // because spawnSync(..., ["--version"]) returns EPERM despite a zero exit code.
96
149
  // Provision local managed binaries first so Pi sees them without probing PATH.
@@ -98,7 +151,10 @@ ensureManagedTools(join(agentDir, 'bin'));
98
151
  const authStorage = AuthStorage.create(authFilePath);
99
152
  loadStoredEnvKeys(authStorage);
100
153
  migratePiCredentials(authStorage);
101
- const modelRegistry = new ModelRegistry(authStorage);
154
+ // Resolve models.json path with fallback to ~/.pi/agent/models.json
155
+ const { resolveModelsJsonPath } = await import('./models-resolver.js');
156
+ const modelsJsonPath = resolveModelsJsonPath();
157
+ const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
102
158
  const settingsManager = SettingsManager.create(agentDir);
103
159
  // Run onboarding wizard on first launch (no LLM provider configured)
104
160
  if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultProvider())) {
@@ -113,9 +169,18 @@ if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultP
113
169
  process.stdin.setRawMode(false);
114
170
  process.stdin.pause();
115
171
  }
116
- // Non-blocking update check — runs at most once per 24h, fire-and-forget
172
+ // Update check — interactive prompt when stdin is a TTY, passive banner otherwise
117
173
  if (!isPrintMode) {
118
- checkForUpdates().catch(() => { });
174
+ if (process.stdin.isTTY) {
175
+ const updated = await checkAndPromptForUpdates().catch(() => false);
176
+ if (updated) {
177
+ // User chose to update — exit so they relaunch with the new version
178
+ process.exit(0);
179
+ }
180
+ }
181
+ else {
182
+ checkForUpdates().catch(() => { });
183
+ }
119
184
  }
120
185
  // Warn if terminal is too narrow for readable output
121
186
  if (!isPrintMode && process.stdout.columns && process.stdout.columns < 40) {
@@ -298,9 +363,11 @@ if (existsSync(sessionsDir)) {
298
363
  // Non-fatal — don't block startup if migration fails
299
364
  }
300
365
  }
301
- const sessionManager = cliFlags.continue
302
- ? SessionManager.continueRecent(cwd, projectSessionsDir)
303
- : SessionManager.create(cwd, projectSessionsDir);
366
+ const sessionManager = cliFlags._selectedSessionPath
367
+ ? SessionManager.open(cliFlags._selectedSessionPath, projectSessionsDir)
368
+ : cliFlags.continue
369
+ ? SessionManager.continueRecent(cwd, projectSessionsDir)
370
+ : SessionManager.create(cwd, projectSessionsDir);
304
371
  exitIfManagedResourcesAreNewer(agentDir);
305
372
  initResources(agentDir);
306
373
  const resourceLoader = buildResourceLoader(agentDir);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Headless Orchestrator — `gsd headless`
3
+ *
4
+ * Runs any /gsd subcommand without a TUI by spawning a child process in
5
+ * RPC mode, auto-responding to extension UI requests, and streaming
6
+ * progress to stderr.
7
+ *
8
+ * Exit codes:
9
+ * 0 — complete (command finished successfully)
10
+ * 1 — error or timeout
11
+ * 2 — blocked (command reported a blocker)
12
+ */
13
+ export interface HeadlessOptions {
14
+ timeout: number;
15
+ json: boolean;
16
+ model?: string;
17
+ command: string;
18
+ commandArgs: string[];
19
+ context?: string;
20
+ contextText?: string;
21
+ auto?: boolean;
22
+ verbose?: boolean;
23
+ }
24
+ export declare function parseHeadlessArgs(argv: string[]): HeadlessOptions;
25
+ export declare function runHeadless(options: HeadlessOptions): Promise<void>;
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Headless Orchestrator — `gsd headless`
3
+ *
4
+ * Runs any /gsd subcommand without a TUI by spawning a child process in
5
+ * RPC mode, auto-responding to extension UI requests, and streaming
6
+ * progress to stderr.
7
+ *
8
+ * Exit codes:
9
+ * 0 — complete (command finished successfully)
10
+ * 1 — error or timeout
11
+ * 2 — blocked (command reported a blocker)
12
+ */
13
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
14
+ import { join, resolve } from 'node:path';
15
+ // RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
16
+ // This relative path resolves correctly from both src/ (via tsx) and dist/ (compiled).
17
+ import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client.js';
18
+ // ---------------------------------------------------------------------------
19
+ // CLI Argument Parser
20
+ // ---------------------------------------------------------------------------
21
+ export function parseHeadlessArgs(argv) {
22
+ const options = {
23
+ timeout: 300_000,
24
+ json: false,
25
+ command: 'auto',
26
+ commandArgs: [],
27
+ };
28
+ const args = argv.slice(2);
29
+ let positionalStarted = false;
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i];
32
+ if (arg === 'headless')
33
+ continue;
34
+ if (!positionalStarted && arg.startsWith('--')) {
35
+ if (arg === '--timeout' && i + 1 < args.length) {
36
+ options.timeout = parseInt(args[++i], 10);
37
+ if (Number.isNaN(options.timeout) || options.timeout <= 0) {
38
+ process.stderr.write('[headless] Error: --timeout must be a positive integer (milliseconds)\n');
39
+ process.exit(1);
40
+ }
41
+ }
42
+ else if (arg === '--json') {
43
+ options.json = true;
44
+ }
45
+ else if (arg === '--model' && i + 1 < args.length) {
46
+ // --model can also be passed from the main CLI; headless-specific takes precedence
47
+ options.model = args[++i];
48
+ }
49
+ else if (arg === '--context' && i + 1 < args.length) {
50
+ options.context = args[++i];
51
+ }
52
+ else if (arg === '--context-text' && i + 1 < args.length) {
53
+ options.contextText = args[++i];
54
+ }
55
+ else if (arg === '--auto') {
56
+ options.auto = true;
57
+ }
58
+ else if (arg === '--verbose') {
59
+ options.verbose = true;
60
+ }
61
+ }
62
+ else if (!positionalStarted) {
63
+ positionalStarted = true;
64
+ options.command = arg;
65
+ }
66
+ else {
67
+ options.commandArgs.push(arg);
68
+ }
69
+ }
70
+ return options;
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // JSONL Helper
74
+ // ---------------------------------------------------------------------------
75
+ function serializeJsonLine(obj) {
76
+ return JSON.stringify(obj) + '\n';
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Extension UI Auto-Responder
80
+ // ---------------------------------------------------------------------------
81
+ function handleExtensionUIRequest(event, writeToStdin) {
82
+ const { id, method } = event;
83
+ let response;
84
+ switch (method) {
85
+ case 'select':
86
+ response = { type: 'extension_ui_response', id, value: event.options?.[0] ?? '' };
87
+ break;
88
+ case 'confirm':
89
+ response = { type: 'extension_ui_response', id, confirmed: true };
90
+ break;
91
+ case 'input':
92
+ response = { type: 'extension_ui_response', id, value: '' };
93
+ break;
94
+ case 'editor':
95
+ response = { type: 'extension_ui_response', id, value: event.prefill ?? '' };
96
+ break;
97
+ case 'notify':
98
+ case 'setStatus':
99
+ case 'setWidget':
100
+ case 'setTitle':
101
+ case 'set_editor_text':
102
+ response = { type: 'extension_ui_response', id, value: '' };
103
+ break;
104
+ default:
105
+ process.stderr.write(`[headless] Warning: unknown extension_ui_request method "${method}", cancelling\n`);
106
+ response = { type: 'extension_ui_response', id, cancelled: true };
107
+ break;
108
+ }
109
+ writeToStdin(serializeJsonLine(response));
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Progress Formatter
113
+ // ---------------------------------------------------------------------------
114
+ function formatProgress(event, verbose) {
115
+ const type = String(event.type ?? '');
116
+ switch (type) {
117
+ case 'tool_execution_start':
118
+ if (verbose)
119
+ return ` [tool] ${event.toolName ?? 'unknown'}`;
120
+ return null;
121
+ case 'agent_start':
122
+ return '[agent] Session started';
123
+ case 'agent_end':
124
+ return '[agent] Session ended';
125
+ case 'extension_ui_request':
126
+ if (event.method === 'notify') {
127
+ return `[gsd] ${event.message ?? ''}`;
128
+ }
129
+ if (event.method === 'setStatus') {
130
+ return `[status] ${event.message ?? ''}`;
131
+ }
132
+ return null;
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+ // ---------------------------------------------------------------------------
138
+ // Completion Detection
139
+ // ---------------------------------------------------------------------------
140
+ const TERMINAL_KEYWORDS = ['complete', 'stopped', 'blocked'];
141
+ const IDLE_TIMEOUT_MS = 15_000;
142
+ function isTerminalNotification(event) {
143
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
144
+ return false;
145
+ const message = String(event.message ?? '').toLowerCase();
146
+ return TERMINAL_KEYWORDS.some((kw) => message.includes(kw));
147
+ }
148
+ function isBlockedNotification(event) {
149
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
150
+ return false;
151
+ return String(event.message ?? '').toLowerCase().includes('blocked');
152
+ }
153
+ function isMilestoneReadyNotification(event) {
154
+ if (event.type !== 'extension_ui_request' || event.method !== 'notify')
155
+ return false;
156
+ return /milestone\s+m\d+.*ready/i.test(String(event.message ?? ''));
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Quick Command Detection
160
+ // ---------------------------------------------------------------------------
161
+ const QUICK_COMMANDS = new Set([
162
+ 'status', 'queue', 'history', 'hooks', 'export', 'stop', 'pause',
163
+ 'capture', 'skip', 'undo', 'knowledge', 'config', 'prefs',
164
+ 'cleanup', 'migrate', 'doctor', 'remote', 'help', 'steer',
165
+ 'triage', 'visualize',
166
+ ]);
167
+ function isQuickCommand(command) {
168
+ return QUICK_COMMANDS.has(command);
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // Main Orchestrator
172
+ // ---------------------------------------------------------------------------
173
+ // ---------------------------------------------------------------------------
174
+ // Context Loading (new-milestone)
175
+ // ---------------------------------------------------------------------------
176
+ async function readStdin() {
177
+ const chunks = [];
178
+ for await (const chunk of process.stdin) {
179
+ chunks.push(chunk);
180
+ }
181
+ return Buffer.concat(chunks).toString('utf-8');
182
+ }
183
+ async function loadContext(options) {
184
+ if (options.contextText)
185
+ return options.contextText;
186
+ if (options.context === '-') {
187
+ return readStdin();
188
+ }
189
+ if (options.context) {
190
+ return readFileSync(resolve(options.context), 'utf-8');
191
+ }
192
+ throw new Error('No context provided. Use --context <file> or --context-text <text>');
193
+ }
194
+ /**
195
+ * Bootstrap .gsd/ directory structure for headless new-milestone.
196
+ * Mirrors the bootstrap logic from guided-flow.ts showSmartEntry().
197
+ */
198
+ function bootstrapGsdProject(basePath) {
199
+ const gsdDir = join(basePath, '.gsd');
200
+ mkdirSync(join(gsdDir, 'milestones'), { recursive: true });
201
+ mkdirSync(join(gsdDir, 'runtime'), { recursive: true });
202
+ }
203
+ export async function runHeadless(options) {
204
+ const startTime = Date.now();
205
+ const isNewMilestone = options.command === 'new-milestone';
206
+ // For new-milestone, load context and bootstrap .gsd/ before spawning RPC child
207
+ if (isNewMilestone) {
208
+ if (!options.context && !options.contextText) {
209
+ process.stderr.write('[headless] Error: new-milestone requires --context <file> or --context-text <text>\n');
210
+ process.exit(1);
211
+ }
212
+ let contextContent;
213
+ try {
214
+ contextContent = await loadContext(options);
215
+ }
216
+ catch (err) {
217
+ process.stderr.write(`[headless] Error loading context: ${err instanceof Error ? err.message : String(err)}\n`);
218
+ process.exit(1);
219
+ }
220
+ // Bootstrap .gsd/ if needed
221
+ const gsdDir = join(process.cwd(), '.gsd');
222
+ if (!existsSync(gsdDir)) {
223
+ if (!options.json) {
224
+ process.stderr.write('[headless] Bootstrapping .gsd/ project structure...\n');
225
+ }
226
+ bootstrapGsdProject(process.cwd());
227
+ }
228
+ // Write context to temp file for the RPC child to read
229
+ const runtimeDir = join(gsdDir, 'runtime');
230
+ mkdirSync(runtimeDir, { recursive: true });
231
+ writeFileSync(join(runtimeDir, 'headless-context.md'), contextContent, 'utf-8');
232
+ }
233
+ // Validate .gsd/ directory (skip for new-milestone since we just bootstrapped it)
234
+ const gsdDir = join(process.cwd(), '.gsd');
235
+ if (!isNewMilestone && !existsSync(gsdDir)) {
236
+ process.stderr.write('[headless] Error: No .gsd/ directory found in current directory.\n');
237
+ process.stderr.write("[headless] Run 'gsd' interactively first to initialize a project.\n");
238
+ process.exit(1);
239
+ }
240
+ // Resolve CLI path for the child process
241
+ const cliPath = process.env.GSD_BIN_PATH || process.argv[1];
242
+ if (!cliPath) {
243
+ process.stderr.write('[headless] Error: Cannot determine CLI path. Set GSD_BIN_PATH or run via gsd.\n');
244
+ process.exit(1);
245
+ }
246
+ // Create RPC client
247
+ const clientOptions = {
248
+ cliPath,
249
+ cwd: process.cwd(),
250
+ };
251
+ if (options.model) {
252
+ clientOptions.model = options.model;
253
+ }
254
+ const client = new RpcClient(clientOptions);
255
+ // Event tracking
256
+ let totalEvents = 0;
257
+ let toolCallCount = 0;
258
+ let blocked = false;
259
+ let completed = false;
260
+ let exitCode = 0;
261
+ let milestoneReady = false; // tracks "Milestone X ready." for auto-chaining
262
+ const recentEvents = [];
263
+ function trackEvent(event) {
264
+ totalEvents++;
265
+ const type = String(event.type ?? 'unknown');
266
+ if (type === 'tool_execution_start') {
267
+ toolCallCount++;
268
+ }
269
+ // Keep last 20 events for diagnostics
270
+ const detail = type === 'tool_execution_start'
271
+ ? String(event.toolName ?? '')
272
+ : type === 'extension_ui_request'
273
+ ? `${event.method}: ${event.title ?? event.message ?? ''}`
274
+ : undefined;
275
+ recentEvents.push({ type, timestamp: Date.now(), detail });
276
+ if (recentEvents.length > 20)
277
+ recentEvents.shift();
278
+ }
279
+ // Stdin writer for sending extension_ui_response to child
280
+ let stdinWriter = null;
281
+ // Completion promise
282
+ let resolveCompletion;
283
+ const completionPromise = new Promise((resolve) => {
284
+ resolveCompletion = resolve;
285
+ });
286
+ // Idle timeout — fallback completion detection
287
+ let idleTimer = null;
288
+ function resetIdleTimer() {
289
+ if (idleTimer)
290
+ clearTimeout(idleTimer);
291
+ if (toolCallCount > 0) {
292
+ idleTimer = setTimeout(() => {
293
+ completed = true;
294
+ resolveCompletion();
295
+ }, IDLE_TIMEOUT_MS);
296
+ }
297
+ }
298
+ // Overall timeout
299
+ const timeoutTimer = setTimeout(() => {
300
+ process.stderr.write(`[headless] Timeout after ${options.timeout / 1000}s\n`);
301
+ exitCode = 1;
302
+ resolveCompletion();
303
+ }, options.timeout);
304
+ // Event handler
305
+ client.onEvent((event) => {
306
+ const eventObj = event;
307
+ trackEvent(eventObj);
308
+ resetIdleTimer();
309
+ // --json mode: forward all events as JSONL to stdout
310
+ if (options.json) {
311
+ process.stdout.write(JSON.stringify(eventObj) + '\n');
312
+ }
313
+ else {
314
+ // Progress output to stderr
315
+ const line = formatProgress(eventObj, !!options.verbose);
316
+ if (line)
317
+ process.stderr.write(line + '\n');
318
+ }
319
+ // Handle extension_ui_request
320
+ if (eventObj.type === 'extension_ui_request' && stdinWriter) {
321
+ // Check for terminal notification before auto-responding
322
+ if (isBlockedNotification(eventObj)) {
323
+ blocked = true;
324
+ }
325
+ // Detect "Milestone X ready." for auto-mode chaining
326
+ if (isMilestoneReadyNotification(eventObj)) {
327
+ milestoneReady = true;
328
+ }
329
+ if (isTerminalNotification(eventObj)) {
330
+ completed = true;
331
+ }
332
+ handleExtensionUIRequest(eventObj, stdinWriter);
333
+ // If we detected a terminal notification, resolve after responding
334
+ if (completed) {
335
+ exitCode = blocked ? 2 : 0;
336
+ resolveCompletion();
337
+ return;
338
+ }
339
+ }
340
+ // Quick commands: resolve on first agent_end
341
+ if (eventObj.type === 'agent_end' && isQuickCommand(options.command) && !completed) {
342
+ completed = true;
343
+ resolveCompletion();
344
+ return;
345
+ }
346
+ // Long-running commands: agent_end after tool execution — possible completion
347
+ // The idle timer + terminal notification handle this case.
348
+ });
349
+ // Signal handling
350
+ const signalHandler = () => {
351
+ process.stderr.write('\n[headless] Interrupted, stopping child process...\n');
352
+ exitCode = 1;
353
+ client.stop().finally(() => {
354
+ clearTimeout(timeoutTimer);
355
+ if (idleTimer)
356
+ clearTimeout(idleTimer);
357
+ process.exit(exitCode);
358
+ });
359
+ };
360
+ process.on('SIGINT', signalHandler);
361
+ process.on('SIGTERM', signalHandler);
362
+ // Start the RPC session
363
+ try {
364
+ await client.start();
365
+ }
366
+ catch (err) {
367
+ process.stderr.write(`[headless] Error: Failed to start RPC session: ${err instanceof Error ? err.message : String(err)}\n`);
368
+ clearTimeout(timeoutTimer);
369
+ process.exit(1);
370
+ }
371
+ // Access stdin writer from the internal process
372
+ const internalProcess = client.process;
373
+ if (!internalProcess?.stdin) {
374
+ process.stderr.write('[headless] Error: Cannot access child process stdin\n');
375
+ await client.stop();
376
+ clearTimeout(timeoutTimer);
377
+ process.exit(1);
378
+ }
379
+ stdinWriter = (data) => {
380
+ internalProcess.stdin.write(data);
381
+ };
382
+ // Detect child process crash
383
+ internalProcess.on('exit', (code) => {
384
+ if (!completed) {
385
+ const msg = `[headless] Child process exited unexpectedly with code ${code ?? 'null'}\n`;
386
+ process.stderr.write(msg);
387
+ exitCode = 1;
388
+ resolveCompletion();
389
+ }
390
+ });
391
+ if (!options.json) {
392
+ process.stderr.write(`[headless] Running /gsd ${options.command}${options.commandArgs.length > 0 ? ' ' + options.commandArgs.join(' ') : ''}...\n`);
393
+ }
394
+ // Send the command
395
+ const command = `/gsd ${options.command}${options.commandArgs.length > 0 ? ' ' + options.commandArgs.join(' ') : ''}`;
396
+ try {
397
+ await client.prompt(command);
398
+ }
399
+ catch (err) {
400
+ process.stderr.write(`[headless] Error: Failed to send prompt: ${err instanceof Error ? err.message : String(err)}\n`);
401
+ exitCode = 1;
402
+ }
403
+ // Wait for completion
404
+ if (exitCode === 0 || exitCode === 2) {
405
+ await completionPromise;
406
+ }
407
+ // Auto-mode chaining: if --auto and milestone creation succeeded, send /gsd auto
408
+ if (isNewMilestone && options.auto && milestoneReady && !blocked && exitCode === 0) {
409
+ if (!options.json) {
410
+ process.stderr.write('[headless] Milestone ready — chaining into auto-mode...\n');
411
+ }
412
+ // Reset completion state for the auto-mode phase
413
+ completed = false;
414
+ milestoneReady = false;
415
+ blocked = false;
416
+ const autoCompletionPromise = new Promise((resolve) => {
417
+ resolveCompletion = resolve;
418
+ });
419
+ try {
420
+ await client.prompt('/gsd auto');
421
+ }
422
+ catch (err) {
423
+ process.stderr.write(`[headless] Error: Failed to start auto-mode: ${err instanceof Error ? err.message : String(err)}\n`);
424
+ exitCode = 1;
425
+ }
426
+ if (exitCode === 0 || exitCode === 2) {
427
+ await autoCompletionPromise;
428
+ }
429
+ }
430
+ // Cleanup
431
+ clearTimeout(timeoutTimer);
432
+ if (idleTimer)
433
+ clearTimeout(idleTimer);
434
+ process.removeListener('SIGINT', signalHandler);
435
+ process.removeListener('SIGTERM', signalHandler);
436
+ await client.stop();
437
+ // Summary
438
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
439
+ const status = blocked ? 'blocked' : exitCode === 1 ? (totalEvents === 0 ? 'error' : 'timeout') : 'complete';
440
+ process.stderr.write(`[headless] Status: ${status}\n`);
441
+ process.stderr.write(`[headless] Duration: ${duration}s\n`);
442
+ process.stderr.write(`[headless] Events: ${totalEvents} total, ${toolCallCount} tool calls\n`);
443
+ // On failure, print last 5 events for diagnostics
444
+ if (exitCode !== 0) {
445
+ const lastFive = recentEvents.slice(-5);
446
+ if (lastFive.length > 0) {
447
+ process.stderr.write('[headless] Last events:\n');
448
+ for (const e of lastFive) {
449
+ process.stderr.write(` ${e.type}${e.detail ? `: ${e.detail}` : ''}\n`);
450
+ }
451
+ }
452
+ }
453
+ process.exit(exitCode);
454
+ }