Agent Coordination
When multiple AI agents work in parallel, they benefit from knowing what the others are doing. The coordination broker is a lightweight HTTP server that lets agents share status updates, publish artifacts, and flag blockers – all without touching git.
Enabling the Broker
Add a [broker] section to your .git-paw/config.toml:
[broker]
enabled = true
When you run git paw start, pane 0 becomes a dashboard instead of an agent pane and the agents start at pane 1. Supervisor mode keeps the same layout: the orchestrator runs outside the session (see Supervisor § Session layout). The broker runs in its own process in a detached broker window; the dashboard is a client of it and displays a live status table.
How Agents Discover the Broker
git-paw sets the GIT_PAW_BROKER_URL environment variable in every agent pane. Agents use this URL to send and receive messages. A typical value is http://127.0.0.1:9119.
With the broker enabled, each agent’s boot block calls the bundled broker helper (or, for an MCP-speaking CLI, the MCP publish tools), and the agent’s AGENTS.md view carries the bundled coordination skill with the broker URL pre-expanded, so agents know how to use the broker without any manual setup.
Requests the broker refuses
The broker is unauthenticated, so before routing any request it checks two headers to keep browsers and DNS-rebinding pages out. A refused request is not routed and changes no broker state:
| Status | Body | Cause |
|---|---|---|
403 | {"error":"forbidden host"} | The broker is bound to a loopback address and the request’s Host is missing, or its host part is not localhost, an IPv4 literal in 127.0.0.0/8, or [::1]. The port is not checked, so tunnels and port-forwards are fine. |
403 | {"error":"forbidden origin"} | The request carries an Origin header whose host is not loopback, or the serialized value null, or the header was sent more than once or with unreadable bytes. This check applies on every bind. A request with no Origin — which is what every non-browser client sends — is unaffected. |
A Host must also be followed by nothing or a well-formed :<port>, so
localhost:evil.com is refused; a duplicated or unreadable Host is refused;
and an absolute-form request target (GET http://host/path) must name a
loopback authority too.
Because the check runs before routing, it also answers for unknown paths and
wrong methods: a request with a foreign Host gets 403, not 404/405.
Every client git-paw ships — the broker.sh and sweep.sh helpers, the
post-commit hook, the CLI, the MCP server and the dashboard — addresses the
broker by its configured http://<bind>:<port> URL and sends no Origin, so
none of them is affected. See
Security Posture
for the threat model, the non-loopback-bind exception, and the reverse-proxy
migration.
A verdict message is refused on its own terms, after those checks:
| Status | Body | Cause |
|---|---|---|
403 | {"error":"authority required"} | The message is an agent.verified, or an agent.feedback whose from is supervisor, and the request’s X-Paw-Authority header is missing or does not match the session’s authority credential. Nothing is delivered or logged. |
Feedback from any other sender — the post-commit branch-guard hook, the
in-process conflict-detector, a person — is not a verdict and needs no
header. Root-side helpers (sweep.sh verified, feedback-gate,
conflict-ack) attach the credential for you; see
Security Posture.
Broker helper
git paw init installs a bundled agent-side helper at
.git-paw/scripts/broker.sh — the agent-facing analogue of the supervisor’s
sweep.sh. .git-paw/scripts/ is gitignored, so a fresh agent-worktree
checkout does not contain it. git paw start and git paw add therefore
auto-provision the helper into each agent worktree at setup: they write
broker.sh (whenever the broker is enabled) and docs-fetch.sh (whenever
docs_base_url is configured) — with the shared _paw_common.sh preamble
both scripts source — into the worktree’s .git-paw/scripts/,
executable, from the same bundled assets init installs. The scripts are
(re)written on every attach, so a worktree’s helper always matches the running
binary’s version and agents never copy it by hand from assets/. The agent
invokes the helper by its stable relative path.
The helper wraps every agent→broker curl an agent is allowed to make. It
discovers the broker URL from .git-paw/config.toml [broker] (defaulting to
http://127.0.0.1:9119) and shapes the JSON internally, so callers pass only
simple arguments. The agent id comes from --agent <id> (the boot block
passes the pre-expanded branch id) or, absent one, from slugifying the current
worktree branch. Run .git-paw/scripts/broker.sh --help for the full surface.
In the repository’s main working tree the helper needs an explicit
--agent <id>. Agents publish from linked worktrees; the main working tree is
the orchestrator’s, and deriving an id from its branch would mint a phantom
agent named after the default branch (main → agent-main) rather than
identifying anybody. Run there without --agent, the helper exits non-zero with
a diagnostic naming the remedy and publishes nothing. Pass the identity you mean
— broker.sh --agent supervisor question "which order?" — and it publishes as
before. Nothing changes for an agent in its own worktree, where derivation works
exactly as it always has.
| Subcommand | Publishes / reads |
|---|---|
status <message> | agent.status (status:"working", the message, modified_files:[]) |
artifact [--exports a,b] [--files a,b] | agent.artifact (status:"done") — the code-less DONE fallback |
blocked <needs> <from> | agent.blocked with dependency info |
question <text> | agent.question |
intent <summary> <files> [valid_for_seconds] | agent.intent (forward-coordination file claim); the TTL defaults to 900 seconds when omitted |
poll [since] | reads GET <broker>/messages/<agent-id>?since=<n> — this agent’s inbox |
valid_for_seconds is a required field of agent.intent, so the helper always
sends one: give a TTL explicitly when you want a different window, and the
default of 900 seconds (15 minutes) applies otherwise.
A publish the broker rejects is loud. Every publish subcommand checks the
broker’s HTTP response. When the broker does not accept the publish — any
non-2xx status, or no response at all because the broker is not running — the
helper prints the broker’s error to stderr and exits non-zero; it never
reports the publish as done. Treat a non-zero exit as “this event did not
reach the broker” and fix the cause (a malformed agent id, a stopped broker)
rather than moving on. The same holds for the supervisor-side sweep.sh
publish verbs.
The helpers are refreshed on every attach. git paw start and
git paw add (re)write <worktree>/.git-paw/scripts/ from the bundled assets
of the binary that launched the session, on every launch path, so a worktree’s
helper always matches that binary’s version. Re-attaching a worktree that
already has the scripts rewrites them rather than failing — which also means
local edits to a worktree’s helper copy are overwritten. Edit
assets/scripts/ (when developing git-paw itself) rather than a deployed copy.
Why a script and not a git paw publish subcommand? The helper is an
agent-internal mechanism. A user-facing subcommand would surface in --help
and produce confusing errors when run by a human (no broker, no session). A
script under .git-paw/scripts/ is unambiguously agent-internal, mirrors the
supervisor’s sweep.sh, and gives you a single stable path that one
permission rule in your agent CLI can name — see
Boot calls and permission prompts.
Boot-Prompt Injection
To ensure reliable agent self-reporting, git-paw automatically injects a standardized boot instruction block into every agent’s initial prompt. The block calls .git-paw/scripts/broker.sh (with the pre-expanded --agent <id>) for four essential operations — no raw curl and no broker URL appear in the boot block:
1. REGISTER - Immediate Status Publication
Agents automatically publish their working status with a “booting” message as their very first action:
.git-paw/scripts/broker.sh --agent feat-auth status booting
2. DONE - Task Completion Reporting
The primary completion path is git commit. The git-paw post-commit hook auto-publishes agent.artifact { status: "committed" } with modified_files derived from git diff HEAD~1 --name-only, so agents working on code changes do not publish anything manually — they commit and the hook reports on their behalf.
The boot block retains a manual agent.artifact { status: "done" } fallback for code-less tasks (docs-only updates handled outside the worktree, planning notes, exploration tasks where the artifact is information reported to the broker). The block warns agents NOT to publish manual done while their worktree has uncommitted changes — they should commit instead.
.git-paw/scripts/broker.sh --agent feat-auth artifact --exports "" --files ""
3. BLOCKED - Dependency Waiting Notification
Agents can properly declare when they’re waiting on dependencies:
.git-paw/scripts/broker.sh --agent feat-api blocked "auth token format" feat-auth
4. QUESTION - Uncertainty Escalation (Critical)
Agents are instructed to publish questions and wait for answers rather than guessing:
.git-paw/scripts/broker.sh --agent feat-auth question "Should the JWT use RS256 or HS256 signing?"
IMPORTANT: The boot block explicitly instructs agents: “DO NOT CONTINUE UNTIL YOU RECEIVE AN ANSWER!”
MCP tool form
The four events above are the broker.sh helper form. For a CLI known to
speak MCP (Model Context Protocol) — the built-in set is claude, codex,
cursor-agent, and agy — the boot block instead expresses each event as an
invocation of the matching MCP publish tool:
publish_status, publish_artifact, publish_blocked, publish_question.
The events, their meanings, and their ordering are identical between the two
forms; only the invocation mechanism differs, and each MCP publish tool
resolves the publishing agent id from the session’s own worktree branch
automatically — the boot block never needs to pass one.
An unrecognized or unknown-capability CLI always gets the broker.sh form.
This is the safe default: a boot block naming tools a CLI cannot call would
strand the agent at the very first step, so git-paw only switches to the MCP
form when it is confident the target CLI can act on it.
A [clis.<name>] mcp = true|false
entry overrides the built-in set, and is consulted first. The lookup is keyed
on the CLI’s identity — the [clis.<name>] key, or the built-in /
pass-through name — never on the command that key launches, so a custom
variant such as [clis.claude-oss] command = "claude" keeps the helper form
until you set mcp = true on it.
The MCP publish surface has no inbox or poll tool, so even the MCP form of
the boot block reads the agent’s inbox with
.git-paw/scripts/broker.sh --agent <id> poll.
Boot calls and permission prompts
Whether an agent’s first boot action stalls on a permission prompt is decided by the agent CLI’s own permission system — nothing in git-paw decides it, and git-paw grants nothing on your behalf. Two things are in play:
- the agent CLI’s resolved permission mode, set via
agent_approval(see Configuration → Supervisor). Under a mode that still prompts for shell commands, the boot publish will prompt; - your own allow rules in that CLI. The boot block always invokes the
helper at the single stable path
.git-paw/scripts/broker.sh(or thebash .git-paw/scripts/broker.shform), precisely so one permission rule scoped to that exact path covers every boot call — never a broadcurl *rule, and never per-endpointcurlprefixes that drift with URL normalisation or flag order.
git-paw writes to no CLI settings file — not the repo-root
.claude/settings.json, not a per-worktree copy, and not a configured
[clis.<name>].settings_path. It also never answers the prompt for you: if
the boot publish prompts, an --unattended run escalates it for a human
decision like any other prompt, and the rest of the wave keeps moving. The
same is true of routine dev commands (git status, grep, find, your
stack’s build/test verbs) — they run without asking only when the CLI’s own
mode or rules permit it.
Pre-approving the helper calls, with your consent
If you would rather not answer those prompts at all, you can pre-approve git-paw’s helper calls in the agent CLI’s own allow rules:
git paw setup-cli <CLI> --allowlist --dry-run # see exactly what it writes
git paw setup-cli <CLI> --allowlist # apply it
This is optional and consent-only: you run it, it prints its plan before
writing anything, and git paw setup-cli <CLI> --undo takes back exactly what
it added. No launch path, no drive loop and no MCP tool ever runs it. A call
that runs without a prompt still does so only through the agent CLI’s own
permission system and the rules in its settings — git-paw makes no safety
decision about any command. See Per-CLI Setup for the grant
table, the protection rules that come with it, and the residual risks.
Boot Block Injection Modes
- Supervisor Mode: Boot block is prepended to each agent’s task prompt, then submitted (injected, then a separate
Enterafter a settle delay) - Spec-driven, non-supervisor mode (
--specs/--from-all-specswithout--supervisor): the boot block (when the broker is enabled) is prepended to the task prompt the same way, and submitted the same way — the task comes from the spec, not a human, so the combined prompt cannot be left for someone to complete and submit by hand - Bare, human-driven mode (
git paw startwith no spec selection): Boot block is pre-filled into each agent pane’s input line and left unsubmitted (user pastes their task after the boot instructions, then submits both together)
Both submitting modes end the injected prompt with the worktree rule. When the agent’s spec format declares a workflow entry point, a second line follows as its own message once the pane goes idle — see The workflow entry point.
The worktree rule
Every prompt git-paw injects into a coding agent pane at launch ends with a
bundled worktree rule naming that agent’s absolute worktree root. It is
appended by every injecting path — the bare pre-fill (which is still left
unsubmitted, with no Enter), spec-driven and supervised start, git paw add, and a prompt held for an agent added while the session was paused — so
no path can forget it. The rule is placed last, for recency: it is the
constraint the agent should still be holding when it starts editing.
The rendered text, with the pane’s own recorded worktree path substituted:
Your worktree root is
<worktree root>. Every file you create or edit SHALL lie under that directory. Never write outside it — not by an absolute path, and not by a relative path that climbs out of it (such as../).
The ../ clause is the point. Under child
placement the repository’s main checkout is an
ancestor of every worktree, so a relative path that climbs far enough
reaches it — and an agent told only to “use relative paths” can edit the main
checkout without ever typing an absolute path. Naming the root makes the
boundary something the agent can check a path against.
Like the rest of the boot prompt, the rule is instruction, not enforcement. What actually stops an out-of-worktree write is the agent CLI’s own outside-directory permission prompt and, if you have configured one, the FS-scoped sandbox — see Memory Isolation below. The template is project-agnostic: it names no tool, CLI or project, only the path.
Paste Handling
The boot block includes instructions for proper paste handling, particularly the requirement to send an additional Enter key after paste operations to ensure full content processing.
Benefits
- Reliable Monitoring: Agents self-report immediately on boot
- Consistent Behavior: All agents follow the same coordination pattern
- One rule covers every boot call: the boot block invokes the bundled
broker.shhelper at one stable path, so a single permission rule in your agent CLI can allow it — rather than a broadcurlgrant. Whether it prompts at all is that CLI’s decision; if it does, the prompt is escalated to you, never auto-answered - Supervisor Visibility: Questions and blockers surface to the dashboard promptly
- Audit Trail: All boot operations are logged in the broker log
Worktree Environment
A fresh worktree surfaces a few conditions that look broken but are actually expected policy. The bundled coordination skill briefs agents to recognise them and adapt rather than spend budget diagnosing a non-bug:
- Gitignored install artifacts. Per-stack install artifacts (dependency
directories, build/restore caches) are absent from a fresh checkout by
design and may already be provisioned by an operator-configured worktree
on_createhook before the agent’s CLI starts. Agents are told not to reach for the repo-root copy or symlink it in, and to run their stack’s install step themselves only when the artifacts are genuinely missing. - FS-confinement is policy, not a fault. When a worktree is FS-confined
by an operator-configured sandbox, an
Operation not permittedon a path outside the worktree is the sandbox working as intended. Agents are told to adapt rather than probe it (xattr,id,ls -lO@, write-probes) and to keep working inside their worktree. - Setuid binaries under a sandbox. Some setuid-root system binaries —
psis the canonical example — cannot exec inside a sandbox. This is expected and not fixable, and does not indicate anything else is wrong.
This orientation is stack-agnostic: it never names a specific package-manager invocation, referring instead to “the stack’s install step.”
Memory Isolation
Coding agents are briefed (via the bundled coordination skill) that their worktree is their entire writable territory:
- Persistent artifacts live in the worktree. Memory files, notes, scratch state, and any configuration an agent generates for itself SHALL be created inside the agent’s own worktree — never elsewhere on the machine.
- Operator configuration directories are off-limits for writes.
Home-level CLI configuration directories (and the per-project memory
subtrees beneath them) and the host repository’s
.claude/and.git-paw/control directories belong to the operator. Writing there mutates state shared across sessions and agents. - Question instead of write. When a task appears to require writing
outside the worktree, the agent publishes
agent.questiondescribing the path and waits for an answer rather than writing.
The agent is told the same boundary twice, on purpose: once here, by the bundled coordination skill it reads from its sidecar, and once at the end of its very first prompt, by the worktree rule — which names the absolute path rather than leaving the agent to infer it.
This boundary is carried by doctrine, not by a mechanical check in git-paw: a write targeting operator territory raises the CLI’s own permission prompt, which is escalated to a human and never answered automatically (see Agent permissions). For mechanical enforcement, add deny rules in the agent CLI’s own settings or use the sandbox. The supervisor treats an observed out-of-worktree write attempt as a boundary violation with scoped feedback and repeat-escalation (see Out-of-worktree write violations). The guidance closes the remaining gap: persistence paths that never raise a prompt are governed by the briefing itself.
Message Types
Most broker messages use the same JSON envelope:
{
"type": "agent.<variant>",
"agent_id": "<slug>",
"payload": { ... }
}
<slug> is the agent’s slugified branch name (lowercase alphanumeric + -
only; slashes and underscores from a branch name like feat/auth or
feat/my_auth become hyphens — feat-auth, feat-my-auth). Eight variants
use this envelope: agent.status,
agent.artifact, agent.blocked, agent.intent, agent.question,
agent.feedback, agent.answer, and agent.verified. Three more do not:
agent.advanced-main carries its fields flat alongside type (see
Advanced Main); agent.learning, published by the
learnings aggregator,
carries its agent_id inside payload; and supervisor.verify-now is
emitted by the broker itself — carrying only the committing branch_id — to
the supervisor inbox whenever an agent.artifact { status: "committed" }
arrives and [supervisor] verify_on_commit_nudge is on (the default).
src/broker/messages.rs is the source of truth for the payload schemas.
agent.statusandagent.artifactare normally automatic. The filesystem watcher publishesagent.status(withmodified_files) whenever a worktree’sgit status --porcelainoutput changes, and the post-commit git hook publishesagent.artifactwithstatus: "committed"and the committed file list every time an agent commits. The manualcurlexamples below are escape hatches for cases where the automatic publishers do not apply (e.g. code-less tasks or heartbeat injection during read-only investigation).
Status
An agent reports what it is currently doing along with any files it has already modified in this work step.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.status","agent_id":"feat-auth","payload":{"status":"working","modified_files":["src/auth.rs"],"message":"implementing login endpoint"}}'
Artifact
An agent shares the result of a commit (or the analogous output of a code-less
task) so peers can see exports and modified files. The modified_files array
is what the conflict detector watches for in-flight overlap.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.artifact","agent_id":"feat-auth","payload":{"status":"committed","exports":["AuthClient"],"modified_files":["src/auth.rs","src/auth/client.rs"]}}'
Blocked
An agent declares that it is waiting on something specific from another agent
(or external resource). from names the agent that can unblock it.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.blocked","agent_id":"feat-api","payload":{"needs":"auth token format","from":"feat-auth"}}'
Intent
An agent declares which files it plans to modify before any edit lands. The
broker conflict detector reads agent.intent to flag forward conflicts when
two agents target overlapping paths. valid_for_seconds is the TTL after which
consumers MAY treat the intent as stale.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-auth","payload":{"files":["src/auth.rs","src/auth/client.rs"],"summary":"wire AuthClient","valid_for_seconds":900}}'
Each files entry may also be an object declaring the regions within a file
the agent intends to touch — see Declaring regions
for the region-level granularity added in v0.6.0.
Question
An agent escalates an uncertainty to the supervisor inbox. The asking agent blocks at its prompt until a typed reply arrives.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.question","agent_id":"feat-auth","payload":{"question":"Should the JWT use RS256 or HS256 signing?"}}'
Feedback
A supervisor (or the broker’s auto-emitted [conflict-detector] voice) sends
a list of error messages to a target agent. The target agent_id field on the
envelope is the receiver; from inside the payload is the sender.
Feedback from supervisor is a verdict: the broker refuses it without the
session’s authority credential (403 {"error":"authority required"}), so the
supervisor publishes it through the helper, which attaches the credential:
.git-paw/scripts/sweep.sh feedback-gate feat-auth testing "test for HS256 path is failing"
The wire body it sends:
{"type":"agent.feedback","agent_id":"feat-auth","payload":{"from":"supervisor","errors":["[testing] test for HS256 path is failing"]}}
Answer
A supervisor replies to an asking agent’s agent.question with a non-error
answer. Like feedback, the envelope agent_id is the receiver (the agent
being answered) and from inside the payload is the sender; the message
routes to the target agent’s inbox only. answer carries the reply text and
the optional re field is a short reference to the question being answered
(omitted when absent). An answer is authoritative guidance to act on — it is
not an error report, so the receiving agent has nothing to fix and nothing to
re-publish. Reserve agent.feedback for corrective errors.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.answer","agent_id":"feat-auth","payload":{"from":"supervisor","answer":"Use RS256; the key rotation tooling assumes it","re":"RS256 or HS256?"}}'
Verified
A supervisor confirms that an agent’s work has passed every verification gate.
The agent_id is the agent whose work was verified; verified_by names the
verifier. Every agent.verified is a verdict, refused without the session’s
authority credential, so it is published through the root-side helper:
.git-paw/scripts/sweep.sh verified feat-auth "all five gates pass"
The wire body it sends:
{"type":"agent.verified","agent_id":"feat-auth","payload":{"verified_by":"supervisor","message":"all five gates pass"}}
Advanced Main
The supervisor publishes this after every successful merge to the default
branch so dependent agents learn the base moved. The payload fields are flat
(alongside type, not nested under payload): from, merged_branch,
new_main_sha, base (the resolved default-branch name), merged_at, and an
optional summary.
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.advanced-main","from":"supervisor","merged_branch":"feat/auth","new_main_sha":"a1b2c3d4e5f6","base":"main","merged_at":"2026-06-04T13:30:00Z","summary":"landed AuthClient"}'
The broker rejects a publish missing any required field (merged_branch,
new_main_sha, base, merged_at) with a 400-class error naming the field.
Each event carries a deterministic id derived from
merged_branch + new_main_sha + base + UTC-hour-bucket, so re-publishing the
same merge within the hour dedups to one logical event.
When You Depend on a Peer’s Work
When a peer’s agent.artifact lists work an agent depends on, the bundled
coordination skill tells the agent to treat it as a signal, not an
instruction to graft the peer’s commit: publish agent.blocked naming the
peer and what it needs, then continue on unblocked work (or wait if none
remains). Agents must not cherry-pick a peer’s commit into their own
branch — the supervisor later merges that commit from the peer’s own branch,
so the grafted copy leaves the dependent branch divergent and unmergeable.
Peer work arrives through the base branch instead, via the
agent.advanced-main discipline below.
When Main Advances
An agent.advanced-main event arrives on your normal
/messages/<agent_id> poll — no special subscription is needed. When the
base it names is one your branch depends on, react deliberately, never
automatically:
git fetch origin <base>to bring the new SHA local.git log HEAD..origin/<base> --onelineto see exactly what landed.- Decide between rebase, merge, or wait based on what changed and the state of your working set.
Agents do not auto-rebase on receipt: rebasing rewrites history and can
conflict with in-flight work, so it always requires judgment. If you do rebase,
commit or deliberately stash your work first so a rebase conflict can never wipe
uncommitted edits. (The bundled coordination skill teaches this discipline to
coding agents directly.)
Separately, when the operator sets [supervisor] branch_refresh = true, the
supervisor’s unattended drive loop may itself rebase an idle, clean worker
branch onto the default branch after a merge. The worker is then told its HEAD
moved by an agent.feedback tagged [branch-refresh] — never by
agent.advanced-main. See
Supervisor → Branch refresh.
Polling for Messages
Agents poll for messages from other agents using cursor-based pagination. The since parameter is a sequence number – the broker returns only messages with a sequence greater than the given value.
# First poll -- get all messages
curl -s "$GIT_PAW_BROKER_URL/messages/feat-auth?since=0"
The response includes a last_seq field. Pass this value as since on the next poll to get only new messages:
# Subsequent poll -- only new messages since last check
curl -s "$GIT_PAW_BROKER_URL/messages/feat-auth?since=42"
This cursor-based approach is lossless – no messages are missed between polls, regardless of timing.
Agents can use the bundled helper instead of a raw curl for the same read — .git-paw/scripts/broker.sh poll [since] issues GET /messages/<agent-id>?since=<n> and prints the returned messages (see Broker helper).
Waiting for Events
/messages/<agent_id> is one agent’s inbox. GET /log is the whole session’s
message log, and it is the read surface for anything that watches the session
as a whole — a supervisor, an external orchestrator, a monitoring script.
curl -s "$GIT_PAW_BROKER_URL/log?since=0"
The response is {"entries": [...], "last_seq": N}. since is the same
cursor the inbox poll uses: the broker returns only entries whose sequence is
greater than the value you pass, and last_seq is the cursor to send next.
An empty response reports last_seq equal to the since you sent, so a
reader’s cursor never rewinds and never has to re-read the log from the start.
(Before v0.18 an empty response reported 0, which rewound every reader that
trusted it — this is the one behaviour change in the response.)
The broker epoch — reset your cursor when it changes
Sequence numbers are per-broker and restart at zero every time a broker
starts, which a git paw pause + git paw start cycle does. Because a
cursor never rewinds, a reader that kept its old cursor across a restart
would sit above everything the new broker appends: it would see nothing,
forever, and time out every long poll while the session worked normally
around it.
Every GET /log response therefore carries an epoch identifying the
running broker instance (its start time in Unix milliseconds), and so do
sweep.sh events and wait_for_events:
{ "entries": [ ... ], "last_seq": 57, "epoch": 1761500000123, "wave": { ... } }
Record the epoch alongside your cursor, and whenever it differs from the
one you recorded, send since=0 on your next request — the entries you
have already seen belong to a broker that no longer exists, so re-reading
from the start is correct and happens exactly once per restart. The first
epoch you see needs no reset; there is no earlier cursor to invalidate.
git-paw’s own in-tree readers (the drive loop’s observer, the MCP tool)
already do this.
The long poll
Add wait=<seconds> and the broker holds the request open until there is
something to return:
# Block for up to 30s, or return the moment an entry newer than 42 is appended
curl -s "$GIT_PAW_BROKER_URL/log?since=42&wait=30"
waitis whole seconds and is capped at 60. A larger value is clamped; a non-numeric value is rejected with HTTP400.- With
waitand nothing newer thansince, the request is held until an entry is appended or the wait elapses — whichever comes first. A wait that elapses with nothing new returns{"entries": [], "last_seq": <since>}. - Without
wait, the request returns immediately, exactly as before.
This is what makes an external reader event-driven rather than capture-diff polling. A reader that samples pane output on a timer silently misses anything that starts and finishes between two samples; a reader that blocks on the cursor sees every appended entry exactly once, whatever the timing.
The bundled helper verb
The supervisor-side helper wraps both reads in one call:
.git-paw/scripts/sweep.sh events <since> [wait]
wait defaults to 30 seconds. The verb calls GET /log?since=&wait= and
GET /status, and prints one JSON object to stdout:
{ "entries": [ ... ], "last_seq": 57, "epoch": 1761500000123, "wave": { ... } }
It is strictly a read: it sends no keystroke and publishes nothing. With no
reachable broker it prints an object carrying "no_session": true and exits
0, so a caller never has to distinguish a crash from a session that is not
running. It is granted read-only by
git paw setup-cli <CLI> --allowlist.
An MCP-speaking client reaches the same surface through the
wait_for_events tool.
Checking Overall Status
The /status endpoint returns a summary of all agents and their latest state:
curl -s "$GIT_PAW_BROKER_URL/status"
Wave staleness
Per-agent heartbeats answer “is this agent alive?” They do not answer “is this
wave getting anywhere?” A session can sit on an unanswered permission prompt
for days with every pane alive and every agent reporting working. So
/status also carries a wave object describing the session as a whole:
"wave": {
"last_progress_unix_secs": 1761500000,
"idle_seconds": 11520,
"stall_threshold_seconds": 1800,
"stalled": true
}
What counts as progress is deliberately narrow — exactly two things:
- A wave phase advances. An
agent.statuswhosestatusorphasediffers from that agent’s previously recorded values, or anyagent.artifact,agent.verified,agent.feedback, oragent.advanced-main. - A branch tip moves. The broker’s filesystem watcher sees an agent
worktree’s
HEADcommit change.
What does not count, however much of it arrives:
- a repeated identical
agent.status— whatever itsmodified_filesset; - uncommitted file churn;
agent.question, including the unattended drive loop’s escalation of a live permission prompt and its re-escalation of the same prompt;agent.learning,agent.blocked,agent.intent,agent.answer, and the broker’s ownsupervisor.verify-nownudge;- an
agent.statuscarrying one of the stuck-detection helper’s synthetic alert phases —stuck-on-prompt,stuck-stream-timeout,context-bloat,no-progressorblocked-on-supervisor. Each is a report about a wave that is not moving, so counting it would let the detection of a stall reset the stall clock;no-progresssays as much in its own name. A status moving an agent back out of one of those phases is an ordinary phase advance and does count.
That exclusion list is the point: those are precisely the messages that keep arriving while a wave sits on an unanswered prompt, so counting them would make the signal report healthy exactly when it matters most.
A broker that has accepted no message at all reports its own start time as the
last progress. stalled is true when the threshold is non-zero and
idle_seconds exceeds it.
The threshold is the [broker] wave_stall_minutes key — default 30
minutes, 0 to disable the flag while still reporting idle_seconds. See the
configuration reference. Thirty minutes
sits just above the unattended drive loop’s 25-minute heartbeat, so a wave
that is merely between heartbeats is not reported as stalled.
The same object is surfaced by
git paw status (human and --json),
the dashboard status line, the MCP
tools get_session_status and wait_for_events, and
sweep.sh events — so a waiter whose long poll times out with no entries
still learns that the wave has stalled.
Multi-Repo Considerations
Each git-paw session runs its own broker. If you have multiple repos running sessions simultaneously, each needs a unique port:
# In repo-a/.git-paw/config.toml
[broker]
enabled = true
port = 9119
# In repo-b/.git-paw/config.toml
[broker]
enabled = true
port = 9120
git-paw enforces this: a broker reports the repository it serves on GET /status, and a session that finds another repository’s broker on its port is refused with an error naming that repository and telling you to set a distinct [broker] port. See Multi-repo port assignment.
The default port is 9119. The broker binds to 127.0.0.1 (localhost only) by default — the [broker] bind setting — and should never be exposed to the network.
Automatic Conflict Detection (v0.5.0)
When supervisor mode is active, the broker runs an in-process conflict
detector that auto-emits agent.feedback (tagged [conflict-detector] in the
errors array) and, for unresolved in-flight conflicts, escalates to the
supervisor inbox via agent.question. Three failure shapes are detected:
- Forward conflicts — two agents publish
agent.intentwith overlappingfiles. Both receiveagent.feedbacklisting the other agent and the overlapping paths. Toggle with[supervisor.conflict] warn_on_intent_overlap. - In-flight conflicts — two agents’
agent.statusmodified_filessets overlap on the same file. Both receiveagent.feedback. The broker waits[supervisor.conflict] window_seconds(default 120) for one side to stop touching the file; if both keep modifying it, the detector escalates a true collision to the supervisor inbox viaagent.question(an overlap whose declared regions are disjoint is downgraded to an informationalagent.feedbackinstead). - Ownership violations — an agent’s
modified_filesincludes a file outside its own activeagent.intentand inside another agent’s active intent. The violator receivesagent.feedback; if[supervisor.conflict] escalate_on_violation = truethe supervisor inbox also receives a follow-upagent.question. The shared spectasks.mdevery agent ticks its own line in is exempt.
See the Conflict Detection chapter for the full
walkthrough — failure shapes, the [conflict-detector] tag, supervisor inbox
routing, and the configuration knobs.
Audit Trail
The broker appends every message to a <session_name>.broker.log file in git-paw’s session-state directory (~/Library/Application Support/git-paw/sessions/ on macOS, $XDG_DATA_HOME/git-paw/sessions/ — default ~/.local/share/git-paw/sessions/ — on Linux), outside the repository. Each line is plain text — [<seq>] <unix-seconds> [<agent_id>] <message summary> — and the file is flushed every 5 seconds, giving an audit trail of agent communication. For the full JSON of every message, query the broker’s GET /log endpoint (see Waiting for Events).
The log file is automatically cleaned up by git paw purge.
Working Heartbeat
The broker’s filesystem watcher publishes agent.status whenever a worktree’s
git status changes, which keeps the dashboard’s last_seen timestamp fresh during
active editing. The watcher cannot observe read-only tool uses (file reads,
greps, searches), permission-prompt waits, or LLM-only deliberation between tool
calls — so a long read-heavy investigation looks stuck on the dashboard even
though the agent is making progress.
To bridge that gap, the embedded coordination skill instructs agents to publish
a lightweight agent.status heartbeat every 5 tool uses while actively working,
through the bundled broker helper, which publishes
{"status":"working","message":"<text>","modified_files":[]}:
.git-paw/scripts/broker.sh --agent feat-auth status "reviewing auth tests"
The heartbeat reuses the existing agent.status shape — no new wire format is
introduced. The broker merges heartbeats with watcher-driven updates without
conflict.
Activity phrases
A status payload may also carry an optional activity field — a short phrase
naming what the agent is doing right now:
curl -s -X POST http://127.0.0.1:9119/publish \
-H "Content-Type: application/json" \
-d '{"type":"agent.status","agent_id":"feat-auth","payload":{"status":"working","activity":"running the test suite","modified_files":[]}}'
The field is optional and backward compatible: a payload that omits it deserializes and re-serializes exactly as before, so existing agents and stored messages are unaffected. When present, the dashboard’s Broker log panel renders the phrase alongside the status. Because the panel also collapses consecutive identical heartbeats, a phrase that changes as the work moves along keeps each phase on its own row.
Prefer Native Read Tools Over Shell Pipelines
The bundled coordination skill teaches agents to prefer their CLI’s own native file-read and search facilities over a shell text-extraction pipeline (a command that pipes output through line-slicing or field-extracting utilities) when inspecting source. Two costs motivate this:
- The approval stall — a shell pipeline typically requires a permission approval before it runs, so the agent waits on a prompt every time.
- The dead grant — a durable approval is keyed on the exact command string, so the next inspection (a different line range, a different search term) never matches it, and the allowlist accumulates entries that exist only to be ignored.
The same mechanic governs scratch files: when an agent captures a command’s output across repeated iterations, the skill tells it to reuse one scratch path rather than writing a fresh numbered file each time, since a durable approval keyed on a path does not carry over to a new filename.
The guidance is deliberately CLI-agnostic — it names no specific CLI, tool, or vendor — so it holds for any CLI git-paw launches.
Commit Cadence
The bundled coordination skill teaches a per-group commit cadence. When a
change has an OpenSpec-style tasks.md with numbered groups (## 1.,
## 2., …), the agent commits after every - [ ] item in a group is
- [x] — one group, one commit (by default) — before starting the next
group.
The skill bounds uncommitted work to roughly ten files at a time. If a
single group exceeds that mid-implementation, the agent splits into multiple
commits using a (part N of M) suffix:
close per-scenario coverage gaps (part 1 of 2)
close per-scenario coverage gaps (part 2 of 2)
Each commit must be a releasable unit — it builds and passes its own gates
on its own, not a checkpoint of half-finished work. When the agent needs to fix
the commit it just made (a typo, a missed file) and that commit has not yet
been verified, the skill tells it to fold the fix in with git commit --amend
rather than land a separate fix typo micro-commit. It must not --amend an
already-verified commit or an earlier group’s commit — --amend applies only to
the most-recent, not-yet-verified commit. This keeps the history as clean,
verifiable units and avoids the manual release-time squashes earlier cycles
needed (148→10 in v0.6.0, 4→1 in v0.7.0).
The bundled skill does not mandate, default to, or recommend a
commit-message format. Message format is a per-project convention, so the
skill defers entirely to the host project’s injected AGENTS.md (subject
style, any prefix or scope convention, any “no AI-assistant trailer” rule), and
any commit example it shows — like the (part N of M) split above — uses a
format-neutral subject. git-paw’s own repo uses Conventional Commits, but only
via git-paw’s own AGENTS.md, never via the skill the binary exports to other
projects.
Per-group cadence protects against agent crashes, conflict mediation, and
/clear resets losing unbounded work, and it maps cleanly to the post-commit
hook’s agent.artifact { status: "committed" } event sequence the supervisor
consumes during verification.
Terminal Action — Commit Then Publish, Never Archive
The bundled coordination skill defines the coding agent’s terminal action as:
- A commit. The post-commit git hook auto-publishes
agent.artifact { status: "committed" }with the committed file list. For code changes this is the canonical “done” signal. - A manual
agent.artifact { status: "done" }(rare). Used only for code-less tasks or to announce namedexportspeers depend on.
The skill is explicit that the coding agent SHALL NOT invoke
/opsx:verify <change-id> or /opsx:archive <change-id> — both are
off-limits for the coding agent and are the supervisor’s job:
- Verification runs the supervisor’s five-gate framework (testing → regression
→ spec audit → doc audit → security audit) against the committed branch.
Self-verification by the coding agent bypasses gates and produces a
premature
agent.verifiedthe supervisor never reviewed. - Archiving happens on the release branch during the supervisor’s cherry-pick
- merge flow, not on the agent’s feature branch. Archiving from a feature branch leaves the change directory deleted on an unmerged branch and produces confused history.
The skill frames this positively as a stand-by-after-commit protocol: once
the final commit lands, the agent publishes the terminal signal and then waits
— it does not reach for verify/archive. While standing by it waits for one of
three supervisor messages: agent.verified (work passed — pick up the next
task), agent.feedback (fix the listed errors and re-publish agent.artifact),
or a further agent.intent (new scope to pick up). This is the actionable
counterpart to the role-gating forbidden-commands rule — what to do instead of
self-verifying. On the supervisor side, that post-commit agent.artifact is the
cue for the supervisor (not the agent) to run /opsx:verify and /opsx:archive.
This is a paw-specific rule for the bundled coordination skill. Single-agent
workflows that self-verify can override the rule via the standard skill
resolution chain (a user override at
<config_dir>/git-paw/agent-skills/coordination.md wins over the bundled
default).
Identifier Forms — Branch vs agent_id
Two related forms of an agent identifier appear throughout the broker protocol:
- Branch name — the original git ref (e.g.
feat/no-supervisor-flag). Used ingit checkout,git worktree,git push, and any other git command. agent_id— the dashed slug form (e.g.feat-no-supervisor-flag). Used in every/publishenvelope’sagent_id, every/messages/<id>URL, and payload fields that name another agent, such asfrominagent.blocked.
agent_id is the slugified form of the branch name. The conversion (named
slugify_branch in the source) lowercases the input, replaces every character
outside [a-z0-9] with - — underscores included — collapses runs of -,
trims leading and trailing -, falls back to the literal agent-unnamed if the
result is empty, and prefixes a single-word result with agent-
(hotfix → agent-hotfix). Every output therefore matches the agent_id
pattern /publish enforces, and no branch can derive the reserved id
supervisor.
A branch name may keep _; its agent_id never does. Branch names git-paw
derives from spec text keep underscores — a Spec Kit task naming ROOM_SETUP.md
produces the branch task/t001-create-room_setup-md — because _ is legal in a
git ref and the branch is durable state that recovery and --resume use to look
up work. Only the broker’s id grammar forbids _, so the mapping happens at the id
boundary: that branch publishes as task-t001-create-room-setup-md.
Match the form to the context: dashed agent_id in any JSON going to or coming
from the broker; slashed branch name in any shell command involving git.
Stash Hygiene in Worktrees
When multiple worktrees run side-by-side, every worktree shares the same
underlying git stash list. A git stash pop invoked without inspection can pop
an entry created by a different worktree, conflict with your in-progress
changes, and wipe work. The embedded coordination skill teaches agents three
rules:
- List before pop —
git stash listfirst; inspect every entry’s branch label and timestamp. - Inspect before pop —
git stash show -p stash@{N}to read the patch contents of the specific entry before popping. - Pop only your own — only pop entries you authored on the current
worktree. If authorship is uncertain, leave the stash alone and escalate via
agent.question.
Blind git stash pop is a data-loss pattern in a multi-worktree session and is
not recommended.
Supervisor Acknowledgement of agent.question
When an agent publishes agent.question, it blocks at its prompt waiting for a
typed reply. A blocked agent does not poll its inbox for agent.answer
responses, so a supervisor that only publishes agent.answer to the broker
will see its answer recorded on the dashboard while the asking agent stays
blocked indefinitely.
The supervisor skill therefore instructs supervisors (both human and LLM) to
both publish agent.answer (the non-error reply shape — agent.feedback
stays reserved for corrective errors) and send the answer text to the
asking agent’s tmux pane via tmux send-keys. The MCP publish tools include
no inbox read, so the dual write applies to agents booted with the
MCP tool form too.
Spec Kit Consolidated Worktrees
When git-paw drives a Spec Kit project (.specify/specs/<feature>/), each feature’s current phase decomposes into multiple worktrees:
- One worktree per
[P]-marked task (branch prefixtask/). These are parallelisable. - One consolidated worktree per non-
[P]task group (branch prefixphase/). Non-[P]tasks share files or context, so a single agent works through them sequentially.
The embedded coordination skill picks up on the branch prefix:
task/<task-id>-<slug>branches: the agent runs the standard “before/while editing” coordination pattern for a single task.phase/<feature>-<phase-slug>branches: the agent:- Works through the listed tasks in
tasks.mdorder. - Flips
- [ ]to- [x]for each completed task in the worktree’stasks.md. The writeback can be a separate commit or bundled with the task’s code change. - Publishes
agent.intentfor the union of files across the next 1–2 tasks (with a generous TTL) rather than one publish per task. - Publishes
agent.artifactwithstatus: "done"only when every listed task shows- [x]intasks.md. Partial completion is not “done”.
- Works through the listed tasks in
When tasks.md is the merge-conflict surface between worktrees, git’s line-level merge handles per-task checkbox flips automatically. A checkbox writeback to the shared tasks.md is never reported as an ownership violation — the conflict detector exempts that file — while forward and in-flight overlap detection still apply to it.
Workflow phases
The bundled coordination skill structures an agent’s editing work into two phases that mirror the skill’s “Before you start editing” and “While you’re editing” sections.
Before you start editing
Before touching any file, the agent:
- Reads the spec or task description in full to understand the scope.
- Publishes
agent.intentlisting the specific files it plans to modify, a one-line summary, and a TTL in seconds (default900= 15 minutes). This advertises ownership to the broker conflict detector so forward conflicts are caught before any edit lands. - Polls its inbox once for warnings or overlapping peer intents — not a busy loop, a single poll.
- Decides on overlap: if a peer’s intent already covers the same
files, the agent picks among wait (peer’s TTL is short, work is
small), split (narrow the file list to avoid overlap, re-publish
agent.intentwith the reduced scope), or escalate (publishagent.questiondescribing the overlap so the supervisor or human can decide). If no overlap is reported, the agent proceeds to edit immediately — there is no explicit go-ahead to wait for.
While you’re editing
Once editing is underway, the agent keeps the intent honest and asks rather than racing:
- Re-publish intent on scope growth. If the in-progress work touches
files that were not in the original
agent.intent, the agent re-publishesagent.intentwith the expandedfileslist before touching the new files. The re-published intent replaces the previous claim for downstream consumers. - Question on peer overlap. If a peer’s
agent.intentarrives in the inbox naming a file in the same module the agent is editing, the agent sendsagent.questiondescribing the overlap and pauses edits on the contested file. Silently racing the peer to a commit is forbidden.
The agent MUST NOT:
- Perform pairwise check-ins on every change — the broker is not a chat channel and peers are not waiting for status pings.
- Wait for an explicit go-ahead from peers when no conflict signal exists — silence from the broker means “no overlap detected”, not “permission pending”.
- Block on broker silence — if
agent.intentpolling returns no overlap, the agent proceeds.
Declaring regions (v0.6.0)
By default an intent claims whole files, so the detector warns any two agents
who name the same path. When several agents collaborate on different parts of
one shared file, that whole-file warning is noise — and noisy warnings get
dismissed, taking real overlaps with them. From v0.6.0, each files entry MAY
be an object that declares the regions the agent intends to touch, and the
detector then warns only when the declared regions actually intersect.
A files array may mix bare-path strings (file-level intent, the v0.5.0
shape) and region objects freely. Four region kinds are recognised:
function—{ "kind": "function", "name": "<symbol>" }class—{ "kind": "class", "name": "<symbol>" }block—{ "kind": "block", "anchor": "<heading or landmark>" }for prose or config filesrange—{ "kind": "range", "start_line": N, "end_line": M }when no symbolic name fits
Worked example. Two agents both intend src/auth.rs, but feat-auth is
hardening validate_token while feat-session reworks refresh_session:
# feat-auth
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-auth","payload":{"files":[{"path":"src/auth.rs","regions":[{"kind":"function","name":"validate_token"}]}],"summary":"harden token checks","valid_for_seconds":900}}'
# feat-session
curl -s -X POST "$GIT_PAW_BROKER_URL/publish" \
-H "Content-Type: application/json" \
-d '{"type":"agent.intent","agent_id":"feat-session","payload":{"files":[{"path":"src/auth.rs","regions":[{"kind":"function","name":"refresh_session"}]}],"summary":"rework session refresh","valid_for_seconds":900}}'
Because the declared functions differ, no forward-conflict warning fires —
the two agents proceed in parallel on the same file. Had both named
validate_token, the warning would fire and name the intersecting function.
Four rules govern detection:
- Both sides must declare regions for region-level matching. If either side omits regions (a bare path string), the detector conservatively falls back to a file-level warning — the v0.5.0 safety net.
- Name matching is normalized. Region names are compared after
case-folding, trimming, collapsing separator runs (space, underscore,
hyphen) to a single form, stripping a trailing
(), and stripping a leading declaration keyword (fn,def,function,class) — so spelling variants of the same symbol intersect:validate_tokenmatchesValidate Token()andfn validate_token. Genuinely distinct symbols (foo_barvsfoobar) stay distinct. - Cross-kind comparisons are conservative. A
function/class/blockregion compared against arangealways intersects (the detector can’t resolve a symbol to line numbers without parsing source). Named regions of different kinds — sayfunction foovsblock foo— intersect when their normalized names match, because they almost always describe the same construct through different lenses. Both shapes carry a hint noting the comparison was conservative. - Don’t manufacture narrow regions to dodge a warning. A region you don’t really own hides a collision that resurfaces later as a merge conflict.
Because matching is string-based, three declaration rules keep intents detector-comparable:
- Use the canonical source spelling — declare
validate_token, not a paraphrase liketoken validation logic; the detector cannot equate two paraphrases of the same symbol. - Declare all the regions the work touches, including shared constant blocks, import sections, and asset files — not only the headline function.
- Re-publish
agent.intentwhen scope grows mid-task, before touching a region that was never declared.
This guidance is taught to agents by the bundled coordination skill
(assets/agent-skills/coordination.md, the “Declaring regions” section),
which is the authoritative source.
Context budget
After the “While you’re editing” discipline, the coordination skill teaches agents to manage their own context window so they don’t hit an opaque “context length exceeded” failure mid-task and lose uncommitted work. The guidance has four parts:
- Residual-budget heuristic. After the boot block, skill prose, and governance docs load, an agent aims to keep at least ~60% of the model’s context window free for task work. This is a heuristic target judged by the agent, not a config field.
- Three named moments to compact / clear / summarise, in priority order: after each spec scenario completes (compact), when the working set grows past ~40% of the window (compact), and when switching between sub-tasks that don’t share state (clear).
- Commit before you compact. Every compact / clear / summarise operation
is preceded by a commit or an
agent.artifactpublish, so reducing context is never lossy. - Proactive context-bloat flagging. When an agent’s pane shows a
/clear to save <N>k tokenshint withNat or above[supervisor] context_bloat_threshold_k(default 250), the supervisor flags it with a syntheticagent.statuscarryingphase: "context-bloat"; the agent commits (or publishes anagent.artifact) first, then clears or compacts.
The bundled coordination skill (assets/agent-skills/coordination.md, the
“Context budget” section) is the authoritative source for the exact
heuristics, the named moments, and the per-CLI compact/clear mechanism table
(claude, claude-oss, and a generic fallback). This chapter only
summarises it.