fix: simplify headless flags, add missing imports, document headless mode

- Remove --verbose flag from headless (use --json for detailed output)
- Remove redundant sawToolExecution state variable
- Remove unused rejectCompletion
- Add missing build*Prompt imports in auto.ts (fixes CI typecheck:extensions)
- Document headless mode in README.md and docs/commands.md
- Simplify help text with examples instead of exhaustive command catalog
This commit is contained in:
frizynn 2026-03-16 19:46:56 -03:00
parent 8ddea154e5
commit f56b8c69f0
5 changed files with 82 additions and 46 deletions

View file

@ -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

View file

@ -69,5 +69,41 @@
|------|-------------|
| `gsd` | Start a new interactive session |
| `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
| `gsd --model <id>` | Override the default model for this session |
| `gsd --print "msg"` (`-p`) | Single-shot prompt mode (no TUI) |
| `gsd --mode <text\|json\|rpc\|mcp>` | 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.

View file

@ -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<string, unknown>,
verbose: boolean,
): string | null {
function formatProgress(event: Record<string, unknown>): 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<string, unknown> | 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<void> {
// 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<void> {
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<void> {
// Completion promise
let resolveCompletion: () => void
let rejectCompletion: (err: Error) => void
const completionPromise = new Promise<void>((resolve, reject) => {
const completionPromise = new Promise<void>((resolve) => {
resolveCompletion = resolve
rejectCompletion = reject
})
// Idle timeout — fallback completion detection
@ -301,7 +276,7 @@ export async function runHeadless(options: HeadlessOptions): Promise<void> {
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<void> {
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')
}

View file

@ -37,26 +37,16 @@ const SUBCOMMAND_HELP: Record<string, string> = {
'',
'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 <phase> 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'),

View file

@ -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,