Skip to content

/gsd headless

gsd headless runs any /gsd command without the interactive TUI. It spawns a child process in RPC mode, automatically responds to interactive prompts, detects when the command completes, and exits with a meaningful exit code.

This is GSD’s entry point for automation — CI pipelines that run auto-mode on push, cron jobs that execute scheduled maintenance, and scripts that need machine-readable output from GSD commands. Progress is written to stderr; use --json (or --output-format stream-json) to stream structured JSONL events to stdout, or --output-format json to get a single structured result object at the end.

Terminal window
gsd headless [flags] [subcommand] [args...]

Any /gsd subcommand works as a positional argument. If no subcommand is given, it defaults to auto.

Terminal window
# Run auto mode (default)
gsd headless
# Run a single unit
gsd headless next
# Check project status
gsd headless status
# Run diagnostics
gsd headless doctor
# Force a specific dispatch phase
gsd headless dispatch plan
# Instant JSON snapshot — no LLM, ~50ms
gsd headless query
# Create a new milestone from a context file and start auto mode
gsd headless new-milestone --context brief.md --auto
# Create a milestone from inline text
gsd headless new-milestone --context-text "Build a REST API with auth"
# Pipe context from stdin
echo "Build a CLI tool" | gsd headless new-milestone --context -
# Resume a previous session
gsd headless --resume abc123 auto
FlagSyntaxDefaultDescription
--timeout--timeout N300000 (5 min)Overall timeout in milliseconds. The process exits with code 1 if this limit is reached. new-milestone auto-extends to 600000 (10 min) when using the default. auto mode disables the timeout entirely when using the default (sets it to 0).
--max-restarts--max-restarts N3Auto-restart on crash with exponential backoff. Set 0 to disable.
--json--jsonOffStream all events as JSONL to stdout. Equivalent to --output-format stream-json.
--output-format--output-format text|json|stream-jsontextOutput format. text writes progress to stderr only. stream-json streams events as JSONL to stdout (same as --json). json suppresses streaming and emits a single structured result object to stdout when complete.
--events--events type1,type2Filter JSONL output to specific event types. Implies --json.
--verbose--verboseOffShow detailed execution output in stderr progress lines, including streaming assistant text and thinking.
--model--model IDSession defaultOverride the model for the headless session.
--context--context <file>Context file for new-milestone. Use - to read from stdin.
--context-text--context-text <text>Inline context text for new-milestone.
--auto--autoOffChain into auto-mode after milestone creation.
--answers--answers <file>JSON file with pre-supplied answers for interactive prompts. See Answer Injection below.
--supervised--supervisedOffEnable supervised mode — forward interactive prompts to the orchestrator via stdin/stdout. Implies --json. Cannot be combined with --context -.
--response-timeout--response-timeout N30000In supervised mode, milliseconds to wait for an orchestrator response before falling back to auto-response.
--resume--resume <session>Resume a previous session by ID or prefix. Exact ID match is preferred; prefix is resolved if unambiguous.
--bare--bareOffPass --bare through to the child RPC process.

Returns a single JSON object with the full project snapshot — no LLM session, no RPC child, instant response (~50ms). This is the recommended way for orchestrators and scripts to inspect GSD state.

Terminal window
gsd headless query | jq '.state.phase'
# "executing"
gsd headless query | jq '.next'
# {"action":"dispatch","unitType":"execute-task","unitId":"M001/S01/T03"}
gsd headless query | jq '.cost.total'
# 4.25

Output schema:

{
"state": {
"phase": "executing",
"activeMilestone": { "id": "M001", "title": "..." },
"activeSlice": { "id": "S01", "title": "..." },
"activeTask": { "id": "T01", "title": "..." },
"nextAction": "Execute T01: Create auth module in slice S01.",
"registry": [{ "id": "M001", "title": "...", "status": "active" }],
"progress": {
"milestones": { "done": 0, "total": 2 },
"slices": { "done": 1, "total": 3 },
"tasks": { "done": 2, "total": 5 }
},
"blockers": [],
"requirements": { "active": 4, "validated": 2, "deferred": 0, "outOfScope": 0, "blocked": 0, "total": 6 }
},
"next": {
"action": "dispatch",
"unitType": "execute-task",
"unitId": "M001/S01/T01",
"reason": "Next incomplete task in active slice"
},
"cost": {
"workers": [{ "milestoneId": "M001", "pid": 12345, "state": "running", "cost": 1.50, "lastHeartbeat": 1716000000000 }],
"total": 1.50
}
}

Fields under state.progress (slices, tasks) are omitted when not applicable (e.g., before slice planning starts). state.requirements is omitted when no REQUIREMENTS.md exists.

Create a new milestone from a context file without the TUI. Combine with --auto to immediately start execution after creation.

Terminal window
# From a file
gsd headless new-milestone --context brief.md
# Inline text
gsd headless new-milestone --context-text "Build a REST API with auth"
# From stdin
echo "Build a CLI tool" | gsd headless new-milestone --context -
# Create and immediately start auto-mode
gsd headless new-milestone --context brief.md --auto

The timeout is automatically extended to 10 minutes for new-milestone commands when using the default 5-minute timeout, because milestone creation involves codebase investigation and writing multiple planning artifacts. If you pass a custom --timeout, it is used as-is.

The headless runner spawns GSD with --mode rpc, creating a structured communication channel between the parent (headless controller) and child (GSD session) processes. Events flow from the child to the parent as typed messages — prompts, progress updates, completion signals, and errors.

When the child process sends an interactive prompt, the headless controller applies built-in defaults to keep execution flowing without human intervention:

Prompt typeDefault behaviour
selectPicks the first available option
confirmAuto-confirms
inputSubmits an empty string
editorReturns the prefill text, or empty if none

Quick commands (status, queue, history, hooks, export, stop, pause, capture, skip, undo, knowledge, config, prefs, cleanup, migrate, doctor, remote, help, steer, triage, visualize) resolve on the first agent_end event rather than waiting for a terminal notification. Use --answers to override these defaults with specific answers for predictable decision trees.

The controller monitors for completion signals — the child process reporting that the dispatched command finished, errored, or hit a blocker. Auto-mode and step-mode completion is detected by terminal notification messages ("Auto-mode stopped...", "Step-mode stopped..."). Multi-turn commands (auto, next, discuss, plan) use this notification path. An idle timeout fires as a fallback: 15 seconds for most commands, 2 minutes for new-milestone.

Timeout handling varies by command:

  • Default (--timeout 300000): new-milestone extends this to 10 minutes automatically. auto mode disables the timeout entirely (sets it to 0) since auto-mode sessions are long-running and have their own internal per-unit supervision.
  • Custom --timeout: Always used as-is. Pass --timeout 0 to explicitly disable the timeout for any command.

When --max-restarts is greater than 0 (default: 3), an error exit triggers an exponential backoff restart — 5 seconds after the first crash, up to a maximum of 30 seconds. SIGINT/SIGTERM interrupts skip the restart and exit immediately with code 11.

Three output formats are available via --output-format (or the --json shorthand):

FormatFlagStdoutStderr
text(default)Progress lines
stream-json--json or --output-format stream-jsonJSONL event streamProgress lines suppressed
json--output-format jsonSingle JSON result object on exitProgress lines suppressed

The json batch mode emits a HeadlessJsonResult object when the session ends:

{
"status": "success",
"exitCode": 0,
"sessionId": "abc123",
"duration": 42300,
"cost": {
"total": 1.23,
"input_tokens": 45000,
"output_tokens": 8000,
"cache_read_tokens": 12000,
"cache_write_tokens": 3000
},
"toolCalls": 47,
"events": 187
}

Status values: success, error, timeout, blocked, cancelled.

On every exit, headless writes a summary to stderr:

[headless] Status: complete
[headless] Duration: 42.3s
[headless] Events: 187 total, 43 tool calls

Additional lines appear when applicable:

[headless] Event filter: tool_execution_start, agent_end
[headless] Restarts: 2
[headless] Answers: 5 answered, 1 defaulted, 2 secrets

On failure, the last 5 events are also printed to aid diagnostics.

--answers <file> loads a JSON file with pre-supplied answers for interactive selection prompts. This lets you drive headless sessions through predictable decision trees without supervised mode.

Answer file format:

{
"questions": {
"question-id": "selected option label",
"multi-select-id": ["option A", "option B"]
},
"secrets": {
"ENV_VAR_NAME": "secret-value"
},
"defaults": {
"strategy": "first_option"
}
}
  • questions — Map of question ID to the label of the answer to select. Multi-select questions accept an array.
  • secrets — Environment variables injected into the RPC child process. The child’s checkExistingEnvKeys() detects them in process.env and marks them as already configured — no interactive prompt is shown. Secrets are never logged or included in event streams.
  • defaults.strategy — What to do when a question isn’t in the file: "first_option" (default) selects the first available option; "cancel" cancels the prompt.

The injector correlates tool_execution_start events for ask_user_questions with subsequent extension_ui_request events. It handles out-of-order events via a deferred processing queue with a 500ms timeout.

--supervised turns headless into an interactive orchestrator bridge. Rather than auto-responding to every prompt, the headless process forwards extension_ui_request events to the outer process via stdout JSONL, and waits for responses on stdin. If no response arrives within --response-timeout milliseconds, it falls back to auto-response.

Both --answers and --supervised can be active simultaneously: the answer injector tries first, supervised mode handles unmatched prompts next, and the auto-responder catches any remaining ones.

Headless supports concurrent parallel workers for multi-milestone execution. Workers coordinate through file-based IPC in .gsd/parallel/ — no sockets or ports.

Spawning workers:

Terminal window
GSD_MILESTONE_LOCK=M001 GSD_PARALLEL_WORKER=1 \
gsd headless --json auto 2>logs/M001.log &

Each worker isolates its state via GSD_MILESTONE_LOCK=<MID> — state derivation only sees its assigned milestone. Workers write heartbeat files atomically to .gsd/parallel/<milestoneId>.status.json:

{
"milestoneId": "M001",
"pid": 12345,
"state": "running",
"currentUnit": { "type": "task", "id": "T03", "startedAt": 1710000000000 },
"completedUnits": 7,
"cost": 1.23,
"lastHeartbeat": 1710000015000,
"startedAt": 1710000000000,
"worktreePath": ".gsd/worktrees/M001"
}

Worker states: running, paused, stopped, error

Monitoring workers:

Terminal window
for f in .gsd/parallel/*.status.json; do
jq -r '[.milestoneId, .state, (.currentUnit.id // "idle"), "\(.cost)$"] | join("\t")' "$f"
done

Sending signals: write a signal file — the worker consumes and deletes it on the next dispatch cycle:

Terminal window
echo '{"signal":"pause","sentAt":'$(date +%s000)',"from":"coordinator"}' \
> .gsd/parallel/M001.signal.json

Signal commands: pause, resume, stop, rebase

Liveness: a session is stale when its PID is dead (kill -0 $pid fails) or its lastHeartbeat is older than 30 seconds. Use gsd headless query for an instant aggregate cost snapshot across all workers.

See /gsd parallel for the built-in parallel orchestration commands.

CodeMeaningWhen
0SuccessCommand completed successfully
1Error or timeoutCommand failed, crashed (and restarts exhausted), or --timeout was exceeded
10BlockedExecution hit a blocker requiring human input
11CancelledSIGINT or SIGTERM received
FilePurpose
.gsd/runtime/headless-context.mdTemp file written for new-milestone — passes the context spec to the RPC child. Deleted by the child after reading.
.gsd/Bootstrapped if missing during new-milestone
.gsd/PROJECT.mdWritten by new-milestone — full project vision
.gsd/REQUIREMENTS.mdWritten by new-milestone — capability contract
.gsd/DECISIONS.mdSeeded by new-milestone — initial decisions log
.gsd/milestones/<MID>/<MID>-CONTEXT.mdWritten by new-milestone — milestone scope and assumptions
.gsd/milestones/<MID>/<MID>-ROADMAP.mdWritten by new-milestone — slice breakdown with checkboxes
.gsd/milestones/<MID>/slices/Created by new-milestone for subsequent slice planning
.gsd/parallel/<MID>.status.jsonWritten by parallel workers — heartbeat and state
FilePurpose
.gsd/Checked for existence before spawning RPC child (except new-milestone)
--answers <file>Answer injection JSON file, if provided
--context <file>Context spec for new-milestone, or stdin if -
FilePurpose
.gsd/parallel/<MID>.signal.jsonWritten by external coordinators to send signals to workers

Run auto-mode in CI:

Terminal window
# In a GitHub Actions workflow
gsd headless auto
echo "Exit code: $?"

Run auto-mode with an explicit timeout (auto-mode disables the default timeout — set one explicitly if needed):

Terminal window
gsd headless --timeout 1800000 auto

Auto-restart on crash (up to 5 times):

Terminal window
gsd headless --max-restarts 5 auto

Get a single structured result after a quick command:

Terminal window
gsd headless --output-format json status
# {"status":"success","exitCode":0,"duration":3200,"cost":{...},...}

Stream only specific event types:

Terminal window
gsd headless --events tool_execution_start,agent_end auto

Available event types: agent_start, agent_end, tool_execution_start, tool_execution_end, tool_execution_update, extension_ui_request, message_start, message_end, message_update, turn_start, turn_end, cost_update, execution_complete

Run a specific dispatch phase:

Terminal window
gsd headless dispatch execute

Use a different model:

Terminal window
gsd headless --model claude-sonnet-4-20250514 auto

Pre-supply answers for a non-interactive setup wizard:

Terminal window
gsd headless --answers answers.json new-milestone --context brief.md --auto

Resume a previous session:

Terminal window
gsd headless --resume abc123 auto

Poll-and-react loop (no LLM cost between checks):

Terminal window
PHASE=$(gsd headless query | jq -r '.state.phase')
case "$PHASE" in
complete) echo "Done" ;;
blocked) echo "Needs intervention" ;;
*) gsd headless next ;;
esac