diff --git a/README.md b/README.md index af55473d6..759eba06a 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,26 @@ gsd 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. +### Headless mode — CI and scripts + +`gsd headless` runs any `/gsd` command without a TUI. Designed for CI pipelines, cron jobs, and scripted automation. + +```bash +# Run auto mode in CI +gsd headless --timeout 600000 + +# One unit at a time (cron-friendly) +gsd headless next + +# Machine-readable status +gsd headless --json status + +# Force a specific pipeline phase +gsd headless dispatch plan +``` + +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. + ### First launch 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,6 +274,8 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro | `Ctrl+Alt+V` | Toggle voice transcription | | `Ctrl+Alt+B` | Show background shell processes | | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) | +| `gsd update` | Update GSD to the latest version | +| `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) | | `gsd --continue` (`-c`) | Resume the most recent session for the current directory | --- @@ -482,6 +504,7 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK. gsd (CLI binary) └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode + ├─ headless.ts Headless orchestrator (spawns RPC child, auto-responds, detects completion) ├─ onboarding.ts First-run setup wizard (LLM provider + tool keys) ├─ wizard.ts Env hydration from stored auth.json credentials ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json diff --git a/docs/commands.md b/docs/commands.md index c38c65f5f..2cdda8e0c 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -69,5 +69,41 @@ |------|-------------| | `gsd` | Start a new interactive session | | `gsd --continue` (`-c`) | Resume the most recent session for the current directory | +| `gsd --model ` | Override the default model for this session | +| `gsd --print "msg"` (`-p`) | Single-shot prompt mode (no TUI) | +| `gsd --mode ` | Output mode for non-interactive use | +| `gsd --list-models [search]` | List available models and exit | | `gsd --debug` | Enable structured JSONL diagnostic logging for troubleshooting dispatch and state issues | | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) | +| `gsd update` | Update GSD to the latest version | + +## Headless Mode + +`gsd headless` runs `/gsd` commands without a TUI — designed for CI, cron jobs, and scripted automation. It spawns a child process in RPC mode, auto-responds to interactive prompts, detects completion, and exits with meaningful exit codes. + +```bash +# Run auto mode (default) +gsd headless + +# Run a single unit +gsd headless next + +# Machine-readable output +gsd headless --json status + +# With timeout for CI +gsd headless --timeout 600000 auto + +# Force a specific phase +gsd headless dispatch plan +``` + +| Flag | Description | +|------|-------------| +| `--timeout N` | Overall timeout in milliseconds (default: 300000 / 5 min) | +| `--json` | Stream all events as JSONL to stdout | +| `--model ID` | Override the model for the headless session | + +**Exit codes:** `0` = complete, `1` = error or timeout, `2` = blocked. + +Any `/gsd` subcommand works as a positional argument — `gsd headless status`, `gsd headless doctor`, `gsd headless dispatch execute`, etc. diff --git a/src/headless.ts b/src/headless.ts index bb8d6b646..dacdb40d7 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -26,7 +26,6 @@ import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client export interface HeadlessOptions { timeout: number json: boolean - verbose: boolean model?: string command: string commandArgs: string[] @@ -58,7 +57,6 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions { const options: HeadlessOptions = { timeout: 300_000, json: false, - verbose: false, command: 'auto', commandArgs: [], } @@ -79,9 +77,8 @@ export function parseHeadlessArgs(argv: string[]): HeadlessOptions { } } else if (arg === '--json') { options.json = true - } else if (arg === '--verbose') { - options.verbose = true } else if (arg === '--model' && i + 1 < args.length) { + // --model can also be passed from the main CLI; headless-specific takes precedence options.model = args[++i] } } else if (!positionalStarted) { @@ -147,23 +144,13 @@ function handleExtensionUIRequest( // Progress Formatter // --------------------------------------------------------------------------- -function formatProgress( - event: Record, - verbose: boolean, -): string | null { +function formatProgress(event: Record): string | null { const type = String(event.type ?? '') switch (type) { case 'tool_execution_start': return `[tool] ${event.toolName ?? 'unknown'}` - case 'tool_execution_end': - if (verbose) { - const result = String(event.result ?? '').slice(0, 200) - return `[tool:result] ${event.toolName ?? 'unknown'}: ${result}` - } - return null - case 'agent_start': return '[agent] Session started' @@ -176,14 +163,6 @@ function formatProgress( } return null - case 'message_update': - if (verbose) { - const msgEvent = event.assistantMessageEvent as Record | undefined - const text = String(msgEvent?.text ?? '').slice(0, 200) - if (text) return `[assistant] ${text}` - } - return null - default: return null } @@ -258,7 +237,6 @@ export async function runHeadless(options: HeadlessOptions): Promise { // Event tracking let totalEvents = 0 let toolCallCount = 0 - let sawToolExecution = false let blocked = false let completed = false let exitCode = 0 @@ -270,7 +248,6 @@ export async function runHeadless(options: HeadlessOptions): Promise { if (type === 'tool_execution_start') { toolCallCount++ - sawToolExecution = true } // Keep last 20 events for diagnostics @@ -290,10 +267,8 @@ export async function runHeadless(options: HeadlessOptions): Promise { // Completion promise let resolveCompletion: () => void - let rejectCompletion: (err: Error) => void - const completionPromise = new Promise((resolve, reject) => { + const completionPromise = new Promise((resolve) => { resolveCompletion = resolve - rejectCompletion = reject }) // Idle timeout — fallback completion detection @@ -301,7 +276,7 @@ export async function runHeadless(options: HeadlessOptions): Promise { function resetIdleTimer(): void { if (idleTimer) clearTimeout(idleTimer) - if (sawToolExecution) { + if (toolCallCount > 0) { idleTimer = setTimeout(() => { completed = true resolveCompletion() @@ -327,7 +302,7 @@ export async function runHeadless(options: HeadlessOptions): Promise { process.stdout.write(JSON.stringify(eventObj) + '\n') } else { // Progress output to stderr - const line = formatProgress(eventObj, options.verbose) + const line = formatProgress(eventObj) if (line) process.stderr.write(line + '\n') } diff --git a/src/help-text.ts b/src/help-text.ts index a38471f76..dc63b1198 100644 --- a/src/help-text.ts +++ b/src/help-text.ts @@ -37,26 +37,16 @@ const SUBCOMMAND_HELP: Record = { '', 'Run /gsd commands without the TUI. Default command: auto', '', - 'Flags (before command):', + 'Flags:', ' --timeout N Overall timeout in ms (default: 300000)', ' --json JSONL event stream to stdout', - ' --verbose Detailed progress output', ' --model ID Override model', '', - 'Commands:', - ' auto /gsd auto (default)', - ' next /gsd next — one unit', - ' status /gsd status', - ' queue /gsd queue', - ' discuss /gsd discuss', - ' doctor [mode] /gsd doctor [fix|heal|audit]', - ' steer "desc" /gsd steer', - ' dispatch Direct unit-type dispatch', - ' ... Any /gsd subcommand', - '', - 'Dispatch phases:', - ' research, plan, execute, complete, reassess, uat, replan', - ' Also: research-milestone, plan-slice, execute-task, etc.', + 'Examples:', + ' gsd headless Run /gsd auto', + ' gsd headless next Run one unit', + ' gsd headless --json status Machine-readable status', + ' gsd headless --timeout 60000 With 1-minute timeout', '', 'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked', ].join('\n'), diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index 80799d96a..61356cf69 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -125,6 +125,18 @@ import { reconcileMergeState, } from "./auto-recovery.js"; import { resolveDispatch, resetRewriteCircuitBreaker } from "./auto-dispatch.js"; +import { + buildResearchSlicePrompt, + buildResearchMilestonePrompt, + buildPlanSlicePrompt, + buildPlanMilestonePrompt, + buildExecuteTaskPrompt, + buildCompleteSlicePrompt, + buildCompleteMilestonePrompt, + buildReassessRoadmapPrompt, + buildRunUatPrompt, + buildReplanSlicePrompt, +} from "./auto-prompts.js"; import { type AutoDashboardData, updateProgressWidget as _updateProgressWidget,