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.
- package/README.md +25 -1
- package/dist/cli.js +74 -7
- package/dist/headless.d.ts +25 -0
- package/dist/headless.js +454 -0
- package/dist/help-text.js +47 -0
- package/dist/mcp-server.d.ts +20 -3
- package/dist/mcp-server.js +21 -1
- package/dist/models-resolver.d.ts +32 -0
- package/dist/models-resolver.js +50 -0
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/dist/resources/extensions/bg-shell/types.ts +33 -1
- package/dist/resources/extensions/browser-tools/capture.ts +18 -16
- package/dist/resources/extensions/browser-tools/index.ts +20 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +560 -52
- package/dist/resources/extensions/gsd/captures.ts +49 -0
- package/dist/resources/extensions/gsd/commands.ts +194 -11
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/dist/resources/extensions/gsd/diff-context.ts +73 -80
- package/dist/resources/extensions/gsd/doctor.ts +76 -12
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/forensics.ts +95 -52
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
- package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
- package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
- package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
- package/src/resources/extensions/bg-shell/types.ts +33 -1
- package/src/resources/extensions/browser-tools/capture.ts +18 -16
- package/src/resources/extensions/browser-tools/index.ts +20 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
- package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
- package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
- package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
- package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
- package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
- package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
- package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
- package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
- package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +560 -52
- package/src/resources/extensions/gsd/captures.ts +49 -0
- package/src/resources/extensions/gsd/commands.ts +194 -11
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
- package/src/resources/extensions/gsd/diff-context.ts +73 -80
- package/src/resources/extensions/gsd/doctor.ts +76 -12
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/forensics.ts +95 -52
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +85 -5
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/mcp-server.ts +33 -12
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
- package/src/resources/extensions/gsd/session-forensics.ts +36 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
- package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
- package/src/resources/extensions/gsd/workspace-index.ts +34 -6
- package/src/resources/extensions/subagent/index.ts +5 -0
- 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
|
|
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
|
-
|
|
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
|
-
//
|
|
172
|
+
// Update check — interactive prompt when stdin is a TTY, passive banner otherwise
|
|
117
173
|
if (!isPrintMode) {
|
|
118
|
-
|
|
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.
|
|
302
|
-
? SessionManager.
|
|
303
|
-
:
|
|
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>;
|
package/dist/headless.js
ADDED
|
@@ -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
|
+
}
|