Skip to content

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.

  • agentbundle >= 0.29.1 installed (python -m pip install 'agentbundle>=0.29.1') — earlier versions lack the FSM git guard and will push the startup branch if git_push is pre-approved
  • agentbundle install --pack core run in the target repo
  • Python 3.11+ on the machine running the harness
  • An ACP-capable control harness — Claude Code via the claude-agent-acp bridge or native Agent SDK
  • workspace.toml with at least one initiative in the target repo (see orient at session start)

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"
]
}
}

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.

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_INSTRUCTION

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:

FieldTypeMeaning
current_statestring | nullWork-loop FSM phase; null when idle
gate_pendingboolTrue when human input is required before work continues
gatestring | nullGate name — e.g. SPEC-HUMAN-GATE, REVIEW-HUMAN-GATE
gate_questionstring | nullThe specific question the work-loop is asking
readyarrayBuild items (type: work) dispatchable now
shapingarrayNon-FSM items (research, design, shape, strategy); entries with unmet_needs are blocked; entries with available: false need their required_pack installed
activearrayItems currently in progress
blockedarrayItems with unmet dependencies
(per item) availablebool | absentfalse when the item’s dispatch_skill is not installed; absent when available
(per item) required_packstring | nullPack to install when available: false; e.g. "desk-research" (use agentbundle install --pack <value>)

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.

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:

  1. The agent calls elicit() → workspace-mcp sends an MCP elicitation/create JSON-RPC request to the harness (server→client)
  2. Your harness receives the request, routes the question to the human channel
  3. Your harness returns the human’s answer as the JSON-RPC response to the elicitation/create request
  4. The elicit() call unblocks and the work-loop continues
SymptomCauseFix
Session hangs indefinitelyMissing permissions.allow entriesAdd 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 checkoutRun 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 blockedExpected 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 dispatchWORKSPACE_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 modeUse 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 pathsUnstage 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 sessiongit_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.tomlWORKSPACE_MCP_DISPATCHED_ITEM references a slug that doesn’t exist in the queueVerify the slug against workspace_status() ready[] before dispatching

Full workspace-mcp architecture, notification contract, security constraints, deferred adapter roadmap, and Class B (Kiro CLI) setup: docs/architecture/workspace-mcp/design.md.