- Docs
- Guides
- The Build Loop (core)
- How-to
- Run a headless session with workspace-mcp
Run a headless session with workspace-mcp
Drive unattended sessions through structured queue discovery, state observation, scoped Git operations, and explicit gate handling.
A control harness drives Claude Code sessions programmatically — no human watching each turn. workspace-mcp is the per-session MCP server the core pack ships for exactly this use: structured queue discovery, FSM-state observability, and scoped git operations.
What your harness gets: workspace_status() returns the queue of ready and blocked items. Gates arrive as elicitation/create requests the harness routes to a human (Step 5); gate_pending and gate_question are readable from a separate reconciliation session but are not a push signal.
Stage 1 scope: Claude Code via ACP. Codex is planned; Kiro CLI, Copilot CLI, and Gemini CLI are deferred.
Prerequisites
Section titled “Prerequisites”agentbundle >= 0.29.1installed (python -m pip install 'agentbundle>=0.29.1') — earlier versions lack the FSM git guard and will push the startup branch ifgit_pushis pre-approvedagentbundle install --pack corerun in the target repo- Python 3.11+ on the machine running the harness
- An ACP-capable control harness — Claude Code via the
claude-agent-acpbridge or native Agent SDK workspace.tomlwith at least one initiative in the target repo (see orient at session start)
Step 1 — Enable headless permissions
Section titled “Step 1 — Enable headless permissions”Claude Code requires explicit permission for each MCP tool before a headless session can use it. Without this, sessions hang waiting for interactive approval.
Add the six workspace-mcp tool strings to permissions.allow in .claude/settings.json in the target repo:
{ "permissions": { "allow": [ "mcp__workspace-mcp__workspace_status", "mcp__workspace-mcp__elicit", "mcp__workspace-mcp__git_status", "mcp__workspace-mcp__git_branch", "mcp__workspace-mcp__git_commit", "mcp__workspace-mcp__git_push" ] }}Step 2 — Discover the work queue
Section titled “Step 2 — Discover the work queue”Open a short-lived discovery session with no environment variables. Send a prompt asking the agent to call workspace_status(), then read the ready[] and shaping[] arrays to choose what to dispatch.
{ "method": "session/new", "params": { "cwd": "/absolute/path/to/repo", "mcpServers": [ { "name": "workspace-mcp", "command": "python3", "args": [".claude/skills/workspace-status/scripts/workspace_mcp_server.py"] } ] }}The workspace_status() response:
{ "ready": [ { "ini_slug": "my-initiative", "type": "work", "slug": "fix-login-bug", "dispatch_skill": "work-loop", "has_gates": true } ], "shaping": [], "blocked": [], "active": [], "gate_pending": false, "current_state": null}Stage 1: pick an item from ready[] only. shaping[] items (research, design, shape, strategy) are not supported until Stage 3 — dispatching one opens a bound session with no usable skill flow. Use shaping[] as informational: it shows what is waiting, not what can be dispatched today.
Skip items where unmet_needs is non-empty (blocked) or available is false (required pack not installed — run agentbundle install --pack <required_pack> first). Close the discovery session.
Step 3 — Dispatch the item
Section titled “Step 3 — Dispatch the item”The env var you set depends on the item’s type. One env var selects the session mode.
Work items (type: "work", dispatch_skill: "work-loop")
Section titled “Work items (type: "work", dispatch_skill: "work-loop")”Set WORKSPACE_MCP_SPEC_PATH to the spec directory path (relative to cwd). work-loop manages its own git lifecycle — git_branch, git_commit, and git_push are intentionally unavailable. The harness role is to monitor workspace_status() and respond to gates.
{ "method": "session/new", "params": { "cwd": "/absolute/path/to/repo", "mcpServers": [ { "name": "workspace-mcp", "command": "python3", "args": [".claude/skills/workspace-status/scripts/workspace_mcp_server.py"], "env": [ { "name": "WORKSPACE_MCP_SPEC_PATH", "value": "docs/specs/fix-login-bug" } ] } ], "_meta": { "systemPrompt": { "type": "preset", "preset": "claude_code", "append": "<DEFAULT_SESSION_INSTRUCTION>" } } }}Then send the first message to start the agent (the session is idle until a prompt arrives):
{ "method": "session/prompt", "params": { "sessionId": "<id from session/new response>", "prompt": [{ "type": "text", "text": "Run the work-loop for the dispatched item at docs/specs/fix-login-bug." }] }}Non-FSM items (type: "research" | "design" | "shape" | "strategy")
Section titled “Non-FSM items (type: "research" | "design" | "shape" | "strategy")”Set WORKSPACE_MCP_DISPATCHED_ITEM as {ini_slug}/{type}:{slug}. This unlocks git_branch, git_commit, and git_push scoped to the item’s configured output paths.
{ "method": "session/new", "params": { "cwd": "/absolute/path/to/repo", "mcpServers": [ { "name": "workspace-mcp", "command": "python3", "args": [".claude/skills/workspace-status/scripts/workspace_mcp_server.py"], "env": [ { "name": "WORKSPACE_MCP_DISPATCHED_ITEM", "value": "my-initiative/research:competitive-analysis" } ] } ] }}Retrieve the session instruction at runtime:
from agentbundle.workspace_mcp import DEFAULT_SESSION_INSTRUCTIONStep 4 — Monitor progress
Section titled “Step 4 — Monitor progress”Gates surface as incoming elicitation/create requests from workspace-mcp to the harness (Step 5) — not through a harness-callable poll. When the work-loop reaches a gate, the agent calls elicit(), which blocks the current turn and sends an elicitation/create JSON-RPC request containing the gate question. The turn does not complete until the harness responds, so there is no “response from the agent” to poll after.
If your harness needs to read FSM state independently (e.g., to reconcile after a session restart), open a short-lived session with WORKSPACE_MCP_SPEC_PATH set to the active spec path and prompt the agent to call workspace_status(). Do not omit WORKSPACE_MCP_SPEC_PATH: without it, _EventBridge starts with spec_dir=None and cannot bind to the active run — workspace_status() returns current_state=null and gate_pending=false regardless of the actual work-loop state. The response includes:
Key fields in the workspace_status() response:
| Field | Type | Meaning |
|---|---|---|
current_state | string | null | Work-loop FSM phase; null when idle |
gate_pending | bool | True when human input is required before work continues |
gate | string | null | Gate name — e.g. SPEC-HUMAN-GATE, REVIEW-HUMAN-GATE |
gate_question | string | null | The specific question the work-loop is asking |
ready | array | Build items (type: work) dispatchable now |
shaping | array | Non-FSM items (research, design, shape, strategy); entries with unmet_needs are blocked; entries with available: false need their required_pack installed |
active | array | Items currently in progress |
blocked | array | Items with unmet dependencies |
(per item) available | bool | absent | false when the item’s dispatch_skill is not installed; absent when available |
(per item) required_pack | string | null | Pack to install when available: false; e.g. "desk-research" (use agentbundle install --pack <value>) |
Step 5 — Respond to gates
Section titled “Step 5 — Respond to gates”When gate_pending is true, the work-loop is paused waiting for a human decision. The DEFAULT_SESSION_INSTRUCTION directs the agent to call elicit(gate_question) at gate states. This means gate responses require ACP elicitation, not session/prompt.
Required: declare elicitation capability
Section titled “Required: declare elicitation capability”Declare elicitation support under clientCapabilities in the ACP init handshake. claude-agent-acp reads clientCapabilities.elicitation.form (or .url) — capabilities.elicitation is not the correct key and leaves MCP forwarding disabled. At minimum, include:
{ "method": "initialize", "params": { "clientCapabilities": { "elicitation": { "form": true } } }}The agent then sends elicitation/create and blocks until the harness resolves it:
- The agent calls
elicit()→ workspace-mcp sends an MCPelicitation/createJSON-RPC request to the harness (server→client) - Your harness receives the request, routes the question to the human channel
- Your harness returns the human’s answer as the JSON-RPC response to the
elicitation/createrequest - The
elicit()call unblocks and the work-loop continues
When it doesn’t work
Section titled “When it doesn’t work”| Symptom | Cause | Fix |
|---|---|---|
| Session hangs indefinitely | Missing permissions.allow entries | Add all six mcp__workspace-mcp__* strings to .claude/settings.json (Step 1) |
workspace_status() returns {"error": "workspace_status_engine.py not found…"} | agentbundle install --pack core has not been run in the checkout | Run agentbundle install --pack core in the target repo |
git_branch, git_commit, or git_push returns "not available in work-loop (FSM) mode" | WORKSPACE_MCP_SPEC_PATH is set — work-loop manages its own git lifecycle; mutating git tools are blocked | Expected for work items; monitor gates, don’t call git mutating tools |
git_commit returns "git_commit is not available in discovery mode" after a work-type dispatch | WORKSPACE_MCP_DISPATCHED_ITEM set for a work-type item — work items have no output_pattern; workspace-mcp clears the dispatch and falls back to discovery mode | Use WORKSPACE_MCP_SPEC_PATH for work items (not DISPATCHED_ITEM); DISPATCHED_ITEM is for non-FSM types only |
git_commit returns "refusing commit: N pre-staged file(s) outside output_pattern" | The repo has pre-staged files outside the item’s output paths | Unstage those files before calling git_commit, or use git reset HEAD |
git_branch returns "session branch already set" | git_branch was called a second time in the same dispatched session | git_branch may only be called once per non-FSM session; a resumed session may already have a locked branch |
Item slug not found in workspace.toml | WORKSPACE_MCP_DISPATCHED_ITEM references a slug that doesn’t exist in the queue | Verify the slug against workspace_status() ready[] before dispatching |
Reference
Section titled “Reference”Full workspace-mcp architecture, notification contract, security constraints, deferred adapter roadmap, and Class B (Kiro CLI) setup: docs/architecture/workspace-mcp/design.md.