docs/design/factory-worker-api.md
Design for the API boundary between Gas Town and AI agent runtimes.
Ref: gt-5zs8
Gas Town has no stable interface with AI agents. Every integration is a hack:
❯ prompt prefix (with NBSP
normalization) and ⏵⏵ status bar parsing~/.claude/projects/<slug>/<session>.jsonlpane_current_command + pgrep--dangerously-* flags, one per agent vendor28+ touch points cataloged. All depend on implementation details of Claude Code, tmux, macOS Keychain, or filesystem conventions that can change without notice.
run_id threads through every event from
spawn to death.The runtime reports lifecycle transitions. GT does not infer them.
POST /lifecycle
{
"event": "started" | "ready" | "busy" | "idle" | "stopping" | "stopped",
"run_id": "uuid",
"session_id": "gt-crew-max",
"timestamp": "2026-03-01T15:00:00Z",
"metadata": {} // event-specific (e.g., exit_code for "stopped")
}
Replaces: prompt prefix matching, status bar parsing, pane_current_command,
IsAgentAlive(), GetSessionActivity(), heartbeat files, WaitForIdle() polling,
WaitForRuntimeReady() polling.
Key events:
| Event | Replaces | When |
|---|---|---|
started | session creation detection | Agent process begins |
ready | WaitForRuntimeReady() polling | Agent ready for first prompt |
idle | prompt prefix + status bar detection | Turn complete, awaiting input |
busy | "esc to interrupt" detection | Processing a prompt |
stopping | done-intent file detection | Agent initiating shutdown |
stopped | process tree check | Agent process exited |
GT sends structured messages. No terminal injection.
POST /prompt
{
"run_id": "uuid",
"content": "Review PR #2068...",
"priority": "normal" | "urgent" | "system",
"source": "nudge" | "mail" | "sling" | "prime",
"metadata": {
"from": "gastown/crew/tom",
"bead_id": "gt-abc12"
}
}
Response:
{
"accepted": true,
"queued": false, // true if agent was busy and queued it
"position": 0 // queue position if queued
}
Replaces: NudgeSession() (8-step tmux send-keys protocol), 512-byte chunking,
ESC+readline dance, debounce timers, nudge queue JSON files, UserPromptSubmit
hook drain, large-prompt temp file workaround.
Priority semantics:
system: injected at turn boundary (replaces `` blocks)urgent: interrupts current work (replaces immediate nudge)normal: delivered when idle (replaces wait-idle + queue fallback)Structured context delivery at session start and compaction.
POST /context
{
"run_id": "uuid",
"sections": [
{"type": "role", "content": "You are a polecat worker..."},
{"type": "work", "content": "AUTONOMOUS WORK MODE: gt-abc12..."},
{"type": "mail", "content": "2 unread messages..."},
{"type": "checkpoint", "content": "Previous session state..."},
{"type": "directive", "content": "Execute your hooked work."}
],
"mode": "full" | "compact" | "resume"
}
Replaces: gt prime pipeline (10-section output), SessionStart hook,
PreCompact hook, beacon injection, startup nudge fallback for non-hook agents,
role template rendering to stdout.
The runtime asks GT before executing tools. GT decides.
POST /authorize
{
"run_id": "uuid",
"tool": "Bash",
"input": {"command": "git push --force"},
"context": {
"role": "polecat",
"rig": "gastown",
"bead_id": "gt-abc12"
}
}
Response:
{
"allowed": false,
"reason": "force push blocked by dangerous-command guard"
}
Replaces: PreToolUse hook with exit code 2, PR-workflow guard, dangerous-command
guard, patrol-formula guard, per-agent --dangerously-* flags.
Permission model:
The runtime pushes structured events. No filesystem scraping.
POST /telemetry
{
"run_id": "uuid",
"events": [
{
"type": "turn_complete",
"timestamp": "2026-03-01T15:01:00Z",
"usage": {
"input_tokens": 12000,
"output_tokens": 3500,
"cache_read_tokens": 8000,
"cache_creation_tokens": 0,
"model": "claude-opus-4-6",
"cost_usd": 0.2325
},
"tools_called": [
{"name": "Bash", "success": true, "duration_ms": 1200},
{"name": "Read", "success": true, "duration_ms": 50}
]
}
]
}
Replaces: JSONL transcript scraping, extractCostFromWorkDir(), hardcoded pricing
table, agentlog package (Claude Code JSONL tailing), RecordPaneRead, stop hook
gt costs record, 6 separate log files with no correlation.
What flows:
run_id for correlationGT assigns identity; the runtime authenticates.
POST /identity
{
"run_id": "uuid",
"role": "polecat",
"rig": "gastown",
"agent_name": "alpha",
"session_id": "gt-gastown-alpha",
"credentials": {
"type": "api_key" | "oauth" | "token",
"value": "sk-ant-...",
"expires_at": "2026-03-02T00:00:00Z"
},
"env": {
"GT_ROLE": "gastown/polecats/alpha",
"BD_ACTOR": "gastown/polecats/alpha",
"GT_ROOT": "/Users/stevey/gt"
}
}
Replaces: AgentEnv() (30+ env vars via tmux SetEnvironment + PrependEnv),
macOS keychain token swapping, CLAUDE_CONFIG_DIR isolation, account switching
symlinks, GT_QUOTA_ACCOUNT env var, credential passthrough allowlist.
Credential rotation:
Bidirectional health checks.
GET /health
Response:
{
"status": "healthy" | "degraded" | "unhealthy",
"run_id": "uuid",
"uptime_seconds": 3600,
"current_state": "idle" | "busy" | "stopping",
"last_activity": "2026-03-01T15:00:00Z",
"context_usage": 0.73, // fraction of context window used
"error": null
}
Replaces: CheckSessionHealth() (3-level tmux check), IsAgentAlive() (process
tree walking), GetSessionActivity() (tmux activity timestamp), heartbeat files,
TouchSessionHeartbeat(), zombie detection heuristics, spawn storm detection.
Context window pressure is a new signal — the runtime knows how full its context is. GT can use this to trigger compaction/handoff before the agent degrades.
The API is local-only. Two options:
Unix domain socket (preferred): $GT_ROOT/.runtime/worker.sock
Embedded HTTP: localhost with random port, written to a well-known file.
$GT_ROOT/.runtime/worker-<session>.portThe factory worker API doesn't require replacing Claude Code. It can be implemented as a sidecar that:
This lets us validate the API design before building a full GT-native runtime.