Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

StatusBodyCause
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:

StatusBodyCause
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.

SubcommandPublishes / 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 the bash .git-paw/scripts/broker.sh form), precisely so one permission rule scoped to that exact path covers every boot call — never a broad curl * rule, and never per-endpoint curl prefixes 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.

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 Enter after a settle delay)
  • Spec-driven, non-supervisor mode (--specs / --from-all-specs without --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 start with 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.sh helper at one stable path, so a single permission rule in your agent CLI can allow it — rather than a broad curl grant. 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_create hook 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 permitted on 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 — ps is 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.question describing 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.status and agent.artifact are normally automatic. The filesystem watcher publishes agent.status (with modified_files) whenever a worktree’s git status --porcelain output changes, and the post-commit git hook publishes agent.artifact with status: "committed" and the committed file list every time an agent commits. The manual curl examples 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:

  1. git fetch origin <base> to bring the new SHA local.
  2. git log HEAD..origin/<base> --oneline to see exactly what landed.
  3. 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"
  • wait is whole seconds and is capped at 60. A larger value is clamped; a non-numeric value is rejected with HTTP 400.
  • With wait and nothing newer than since, 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.status whose status or phase differs from that agent’s previously recorded values, or any agent.artifact, agent.verified, agent.feedback, or agent.advanced-main.
  • A branch tip moves. The broker’s filesystem watcher sees an agent worktree’s HEAD commit change.

What does not count, however much of it arrives:

  • a repeated identical agent.status — whatever its modified_files set;
  • 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 own supervisor.verify-now nudge;
  • an agent.status carrying one of the stuck-detection helper’s synthetic alert phases — stuck-on-prompt, stuck-stream-timeout, context-bloat, no-progress or blocked-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-progress says 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.intent with overlapping files. Both receive agent.feedback listing the other agent and the overlapping paths. Toggle with [supervisor.conflict] warn_on_intent_overlap.
  • In-flight conflicts — two agents’ agent.status modified_files sets overlap on the same file. Both receive agent.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 via agent.question (an overlap whose declared regions are disjoint is downgraded to an informational agent.feedback instead).
  • Ownership violations — an agent’s modified_files includes a file outside its own active agent.intent and inside another agent’s active intent. The violator receives agent.feedback; if [supervisor.conflict] escalate_on_violation = true the supervisor inbox also receives a follow-up agent.question. The shared spec tasks.md every 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:

  1. 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.
  2. A manual agent.artifact { status: "done" } (rare). Used only for code-less tasks or to announce named exports peers 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.verified the 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 in git 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 /publish envelope’s agent_id, every /messages/<id> URL, and payload fields that name another agent, such as from in agent.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:

  1. List before pop — git stash list first; inspect every entry’s branch label and timestamp.
  2. Inspect before pop — git stash show -p stash@{N} to read the patch contents of the specific entry before popping.
  3. 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 prefix task/). These are parallelisable.
  • One consolidated worktree per non-[P] task group (branch prefix phase/). 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:
    1. Works through the listed tasks in tasks.md order.
    2. Flips - [ ] to - [x] for each completed task in the worktree’s tasks.md. The writeback can be a separate commit or bundled with the task’s code change.
    3. Publishes agent.intent for the union of files across the next 1–2 tasks (with a generous TTL) rather than one publish per task.
    4. Publishes agent.artifact with status: "done" only when every listed task shows - [x] in tasks.md. Partial completion is not “done”.

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:

  1. Reads the spec or task description in full to understand the scope.
  2. Publishes agent.intent listing the specific files it plans to modify, a one-line summary, and a TTL in seconds (default 900 = 15 minutes). This advertises ownership to the broker conflict detector so forward conflicts are caught before any edit lands.
  3. Polls its inbox once for warnings or overlapping peer intents — not a busy loop, a single poll.
  4. 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.intent with the reduced scope), or escalate (publish agent.question describing 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-publishes agent.intent with the expanded files list 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.intent arrives in the inbox naming a file in the same module the agent is editing, the agent sends agent.question describing 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.intent polling 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 files
  • range — { "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_token matches Validate Token() and fn validate_token. Genuinely distinct symbols (foo_bar vs foobar) stay distinct.
  • Cross-kind comparisons are conservative. A function/class/block region compared against a range always intersects (the detector can’t resolve a symbol to line numbers without parsing source). Named regions of different kinds — say function foo vs block 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 like token 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.intent when 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.artifact publish, so reducing context is never lossy.
  • Proactive context-bloat flagging. When an agent’s pane shows a /clear to save <N>k tokens hint with N at or above [supervisor] context_bloat_threshold_k (default 250), the supervisor flags it with a synthetic agent.status carrying phase: "context-bloat"; the agent commits (or publishes an agent.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.