COSAW Under the Hood: frao-cosaw-mcp and the Phase-Orchestrator
Reference companion to "Everything Is a Prompt: Context Engineering with COSAW". That article argues for the workflow. This one specifies the machinery — every mode, every tool, every step of self-injection.
Two tools, one division of labor
The COSAW runtime is two programs that never overlap.
frao-cosaw-mcp is the control plane. It is a Go server compiled to a single static binary. It knows how to create workflows, register phases, launch task agents, watch them, close them, and route messages between them. It does not own long-term state on its own terms. It writes workflow state to the .cosaw/ directory inside each repo and reports up to the phase-orchestrator.
phase-orchestrator is the system of record. It is a Rust service built on axum and tokio. It owns briefs, tasks, and resource metrics in SQLite. It exposes a REST API and streams live metrics over Server-Sent Events to a dashboard. It drives cosaw-mcp through an interface called CosawBackend that has two implementations — one shells to the cosaw-mcp CLI, the other speaks to the cosaw-mcp MCP process.
The division is deliberate. The orchestrator persists and displays. The MCP executes. Neither edits application code. Neither builds images. They orchestrate and record.
The two modes of frao-cosaw-mcp
The binary runs in two roles. Both share the same Go engine and the same store, so the behavior is identical whichever transport you use.
cosaw-mcp mcp — main-session mode. This runs inside the main session’s Claude, registered through the repo’s .mcp.json. The main session’s Claude sees the cosaw_* tools. This instance holds the launch tools. It can create workflows, add phases, launch tasks, read task status, send bus messages, monitor context, and migrate the main session.
cosaw-mcp agent — task-agent mode. This runs inside each task agent’s Claude, wired through the worktree’s .mcp.json. This instance holds the report tools. It reports status and completion back to the main session over the shared bus. It cannot launch anything. It cannot see other agents.
The same engine functions back both the MCP tools and the control-plane CLI. One implementation, two transports. This is why the phase-orchestrator’s REST proxy can call the CLI while the main session uses the MCP tools and both behave identically.
Self-injection, exactly
This is the mechanism that delivers a brief into a task agent’s model context. Read it in order.
The two parties
- The main session runs
cosaw-mcp mcp. It holdscosaw_task_launchand the tmux client. - The task agent runs
cosaw-mcp agent. It is a fresh Claude in its own worktree. It owns its composer and its Enter key. - Neither can call the other’s model. There is no API from the main session into the agent’s Claude. The only path in is the agent’s own interface.
The launch sequence
cosaw_task_launchcreates the worktree, the branch, and the tmux session. The session is namedphase-<phase>-<slug>.- tmux starts
claude --dangerously-skip-permissionsinside that session. The agent boots with its own empty context and its own.mcp.jsonexposingcosaw-mcp agent. - The main session writes the prompt plus the brief to a temp file. The file is plain text. Nothing is pre-processed.
tmux load-bufferreads the file as raw bytes into the tmux buffer.tmux paste-bufferwrites those bytes into the agent’s pane. The brief lands in the agent’s composer. This is placement. The main session has put the instruction into the agent’s environment. It has not touched the model.- The main session sends
tmux send-keys Enteras a separate invocation. This is the FM13 rule. Text and Enter are two distinct events. A compoundsend-keys "text" Enteris interpreted by the TUI as Shift+Enter, which inserts a newline instead of submitting. The separate Enter is what submits. - The agent’s own TUI processes that Enter. The text in its composer becomes a submitted message. The instruction enters the model’s context through the agent’s own action. This is self-injection. The orchestrator placed the text. The agent injected it into itself.
- The main session polls.
capture-panereads the agent’s screen. It looks for processing indicators first:Thought,Reading,Forging,Skill,Unravelling, or● Bash(. A bare prompt character is not proof of work. The brief may have landed without being processed. - If no processing appears within a few seconds the main session resends Enter. If that fails it re-pastes the buffer. The escalation is explicit. No magic sleeps.
- Progress is read the same way.
capture-panegrabs the live screen. The main session watches without ever attaching. - Completion is a contract. The agent stages its work with
git add -Aand prints===PHASE_COMPLETE===. Before closing a session the main session confirms the agent is actually idle. It checks process state. It samples CPU twice to confirm ticks are flat. It looks for work appearing after the signal. A session that printed the signal and then started new work is treated as running. - Closing is graceful.
/quitfirst, thenkill-session. Force-killing is a last resort.
The distinction in one sentence
The main session injects instructions into the agent’s environment. The agent injects them into its own context. Both run the same binary in different roles. Neither ever calls the other’s model.
The message bus
The bus is a shared filesystem structure under .cosaw/<workflow>/bus/. It contains a registry of sessions, an append-only audit log, and per-recipient inboxes. Every write is flock-protected. Every read skips partial or corrupt lines rather than failing.
The design choice is deliberate: no open ports. The bus works identically on the host and inside the orchestrator container’s mounted workspace. It is how otherwise-isolated agents exchange messages — reports back to the main session, messages between sessions — without ever sharing a model context.
Context monitoring
cosaw-mcp tracks the context size of every session — task agents and the main session alike. It reads the statusline JSON Claude Code pipes to the statusline script, falling back to a tmux scrape when needed.
- It records growth deltas per session.
- It warns at soft thresholds (default
300000,350000). - At the hard threshold (default
500000) it runs a ladder: compact once, then migrate or flag. - The whole monitor is idle-gated. Running work is never disturbed.
- Thresholds are parameterizable live through
cosaw_context_thresholds_setor thecontext-thresholds setCLI.
The .cosaw/ store
Every workflow leaves a durable record at .cosaw/<workflow>/ inside the repo:
| Artifact | Content |
|---|---|
workflow.json |
workflow id, repo, purpose, status, main session, phases, events |
STATUS.md |
lifecycle log: planning → greenlit → in-flight → merged → archived |
briefs/ |
canonical phase briefs, one file per phase |
MIGRATION.md |
the continuation prompt that started the workflow |
bus/ |
the message bus: registry, audit, inboxes |
The .cosaw/ directory is committed to git. It is the durable, portable record — a workflow resumes from it on any host that has the repo.
Security model
- Identifier allowlist. Workflow, phase, slug, and session identifiers are validated against
^[A-Za-z0-9_-]+$before any filesystem use. Path traversal is rejected and tested. - Argument arrays. Commands execute as
exec.Commandargument arrays, never shell strings. - Buffer paste. Briefs are delivered via tmux buffer paste with zero shell interpretation.
- Private tmux socket. The recommended deployment runs a private tmux server per orchestrator (
COSAW_TMUX_SOCKETwith a unique label) rather than mounting a shared host socket. - Volume type.
flock()requires a filesystem that supports it — use a local orhostPathvolume, not NFS.
Caveman mode
Task agents can operate in a terse, token-efficient style that cuts roughly 75% of output tokens. The caveman skill is embedded in the binary and deployed into each task agent’s worktree at launch. task_launch appends a ## Caveman Mode block to the brief. It is on by default for task agents; pass caveman:false to disable.
The control-plane CLI
The phase-orchestrator’s REST proxy drives cosaw-mcp through a CLI instead of JSON-RPC. This is a frozen contract — the subcommand names and JSON output shapes are stable. JSON goes to stdout. Errors go to stderr as plain text with exit code 1.
cosaw-mcp workflow-create <purpose>
cosaw-mcp workflow-list
cosaw-mcp workflow-get <workflow_id>
cosaw-mcp phase-add <workflow_id> <phase> <slug> <brief>
cosaw-mcp phase-list <workflow_id>
cosaw-mcp phase-brief <workflow_id> <phase>
cosaw-mcp task-launch <repo> <phase> <slug> <brief> [branch_base] [caveman]
cosaw-mcp task-status <session>
cosaw-mcp task-close <session>
cosaw-mcp context-status <session>
cosaw-mcp context-history <session> [--limit N]
cosaw-mcp context-thresholds [set --soft=.. --compact=.. --migrate=..]
Every subcommand reuses the exact store and engine functions the MCP tools call. No orchestration logic is duplicated.
Environment variables
| Variable | Mode | Purpose |
|---|---|---|
COSAW_REPO |
mcp | repo root where .cosaw/ lives |
COSAW_WORKFLOW_ID |
mcp | bound workflow, default for tools |
COSAW_WORKFLOW_DIR |
agent | absolute path to .cosaw/<workflow>/ |
COSAW_SESSION |
both | this instance’s session name (main reserved) |
COSAW_ROLE |
both | main or agent |
COSAW_ORCHESTRATOR |
mcp | phase-orchestrator base URL for reporting |
COSAW_AGENT_CMD |
mcp | command typed into task sessions |
COSAW_MAIN_TMUX_SESSION |
mcp | tmux session the main Claude runs in |
COSAW_TMUX_SOCKET |
both | tmux socket label |
COSAW_CAVEMAN |
agent | 1 enables Caveman Mode |
COSAW_CTX_* |
mcp | context monitor thresholds, poll interval, snapshot dir |
The phase-orchestrator
A Rust service (axum 0.8, tokio, SQLite via sqlx) on port 9763. It is the system of record for the COSAW runtime: briefs, tasks, and resource metrics. It exposes a dashboard and a REST API.
Launch modes
| Mode | Behavior |
|---|---|
direct |
Spawns the command as a child process; tracks the PID via /proc |
detached |
Same as direct but disowned via setsid; survives service restarts |
tmux-attach |
Opens a new window in an existing tmux session |
tmux-dedicated |
Creates a shared phase-orchestrator tmux session; one window per task |
tmux-per-task |
One brand new session per task, with companion windows for htop/btop and live process detail |
Resource monitoring
CPU sampled every second, memory every 3 seconds, I/O every 5 seconds via /proc. The dashboard streams it over SSE, and per-task history holds the last 300 data points.
The CosawBackend trait
The orchestrator drives cosaw-mcp through an interface with two implementations — the control-plane CLI and the MCP process. The app-engine talks to one REST surface regardless of which backend is live. cosaw-mcp reports workflow state back through cosaw_workflow_report, keeping the dashboard and the .cosaw/ store in sync.
The app-engine integration
Frao App Engine is an agent-driven PaaS. When a user submits a prompt, the app-engine launches a COSAW workflow through the phase-orchestrator over REST. The flow:
- The app-engine POSTs a workflow with the app’s workspace path and the prompt.
- The phase-orchestrator maps host paths to container paths and asks cosaw-mcp to launch the main session.
- The main session decomposes the requirement and launches task agents in worktrees under
/workspace. - Task agents implement and signal
===PHASE_COMPLETE===. - The workflow reaches
merged. The app builds, deploys, and goes live. - On build or health failure the workflow relaunches with feedback, bounded to a few iterations.
Host and container paths are translated. Host orchestrator sessions live on the default tmux socket; container orchestrator sessions live on a private cosaw-orch socket. The two are never mixed.
Docker wiring
The orchestrator image bakes in the orchestrator binary, cosaw-mcp, Claude, tmux, and fish. It runs as UID 1337. The entrypoint installs cosaw-mcp from the mounted workspace binary if present, or builds it from source.
| Mount | Container path | Purpose |
|---|---|---|
~/prhyme/projects |
/workspace |
repos, worktrees, cosaw-mcp source |
/tmp/tmux-1337 |
/tmp/tmux-1337 |
host tmux socket (hybrid ops) |
/var/run/docker.sock |
/var/run/docker.sock |
spawn task containers |
~/.claude |
/mnt/host-claude |
read-only Claude config |
The whole stack is one image. It moves to any Docker host as a unit.
Full MCP tool reference
| Group | Tools |
|---|---|
| Workflow & phases | cosaw_workflow_create/get/list, set_status, log, report, write_doc; cosaw_phase_add/list/brief |
| Task lifecycle | cosaw_task_launch, wait, status, read, tail, close, interrupt |
| Bus | cosaw_bus_send, broadcast, read, who |
| Context monitor | cosaw_context_status/history/probe/thresholds/thresholds_set |
| Migration | cosaw_migrate_main, cosaw_main_session_get/set |
| Session & plugins | cosaw_session_command, cosaw_plugin_enable |
Ports and databases
| Service | Port | Store |
|---|---|---|
| Phase-orchestrator | 9763 |
SQLite phase-orchestrator.db |
| Advisor dashboard | 9753 |
SQLite advisor.db |
| cosaw-mcp | — | .cosaw/<workflow>/ + bus |
The start of this architecture is a single observation: a model reasons only over the context you give it. Everything upstream of that observation — the phases, the isolation, the tmux plumbing, the containers — exists to make that context excellent on purpose.