Agents
Fabric is agent-native: a coding agent drives the exact same loop a person does, through the same reviewed path into trunk. There is no special agent mode to learn, just the CLI.
Why it fits agents
- Non-interactive. Every command takes args and flags directly and never
prompts. Pass
--jsonand you get machine-readable output for every command. - Idempotent push.
fabric pushis safe to re-run: it appends only what’s new, so an agent can retry without making a mess. - Every command returns a link. Commands that act on a change print a
View: <url>line (theweb_urlfield under--json) pointing at it on the hub. Pass that URL to the human as-is; don’t construct one yourself. - Local, network-free inspection.
fabric statusandfabric diffneed no round-trip, so an agent can check its own work cheaply. - Dead pointers explain themselves. Once the change you cloned is accepted,
abandoned, or deleted,
fabric pullstops with the reason and a next step (fabric clone --at trunk, or a freshfabric change createthenfabric clone), carried under--jsonas{error, message, suggestion}. An agent should re-clone, not retry the dead pointer. - Review is policy-driven. An agent goes through
propose → approve → acceptlike everyone else. Whether a change needs approval before it lands, and from whom, is set in the repo’sfabric.yaml(see Configuration). An agent cannot bypass the policy the repo configured.
The loop, for an agent
fabric change create "implement feature X" --description "Why this change exists, in markdown" --json
fabric clone --json
cd implement-feature-x
# …agent edits files…
fabric push --json
fabric propose -m "Adds X by doing Y; see the new module Z" --json
fabric approve --json # if the repo's policy lets the change be approved this way
fabric accept --json # gated: merges onto trunk, runs required checks, advances only if green; blocked until fabric.yaml's approval policy is met
State your intent in the change’s description: freeform markdown the reviewer sees
rendered above the diff. Set it at creation (--description), at propose time
(fabric propose -m, alias --description), or any time with
fabric change describe "<markdown>". A change that explains why it exists gets
reviewed faster than one the reviewer has to reverse-engineer.
Link the change to its ticket. If the work has a ticket (fabric ticket), link it with
fabric change create "<name>" --ticket TIX-N or fabric change ticket TIX-N (empty
clears it). The review page then points straight at the ticket, so intent lives on the
ticket and the change references it instead of repeating it in a TIX-N: name prefix.
Give reviewers something to click, too. A change can carry labeled links, rendered on its review page. If your workflow deploys a preview (Railway, staging, anything reachable by URL), attach it so a reviewer can try the change instead of only reading the diff:
fabric change link add https://myapp-pr-42.up.railway.app --label "Preview" --json
Only http/https URLs are accepted; the label defaults to the URL’s host when
omitted. Adding the same label + url again is a no-op that returns the existing link,
so a re-run workflow never stacks duplicates. fabric change link list --json shows
ids, and fabric change link remove <id> takes one down.
Attach the session transcript
A change can carry session transcripts: the conversation between a person and an agent that produced the edits, rendered on the change’s review page as collapsible, role-badged messages. A reviewer then reads the reasoning behind the diff, not just its result.
There is no CLI command for transcripts yet; an agent posts them to the hub’s API
with its token. First create a session on the change (the change id comes from
fabric change create --json or fabric changes --json):
curl -sS -X POST "$HUB/v1/vcs/changes/<change-id>/sessions" \
-H "Authorization: Bearer $FABRIC_TOKEN" -H "Content-Type: application/json" \
-d '{"source":"claude-code","title":"Fix login redirect"}'
source names where the conversation ran (for example claude-code or slack);
title is optional, and an optional external_ref can point back at the source-side
session (an id or URL). The response carries the new session’s id. Then append the
messages, in one or more batches of up to 500:
curl -sS -X POST "$HUB/v1/vcs/sessions/<session-id>/messages" \
-H "Authorization: Bearer $FABRIC_TOKEN" -H "Content-Type: application/json" \
-d '{"messages":[
{"seq":0,"role":"user","body":"Fix the login redirect loop"},
{"seq":1,"role":"assistant","body":"Traced it to a stale cookie check; patching `auth.go`."}
]}'
Each message carries a seq (an integer >= 0) that orders the transcript, a role
(user, assistant, system, or tool), and a markdown body up to 64 KB. Appends
are idempotent per session and seq: re-posting a seq that already exists returns the
stored message with created: false instead of duplicating it, so a retried upload is
safe. A change can carry several sessions, listed oldest first; read them back with
GET /v1/vcs/changes/<change-id>/sessions.
Changes proposed from Slack get this for free: the @fabric agent flow and the
“Propose Fabric change” shortcut store the originating thread as a session
automatically.
When the trunk batches accepts, fabric accept returns queued instead of landing inline: the
change enters a merge queue, merges into a staged candidate frontier, is tested there, and
advances trunk only once its checks pass. Watch it with fabric queue --json (positions, staged
cut, per-candidate status, terminal outcome) or wait on the event stream for change.accepted /
change.rejected. Do not re-run accept on a queued change; it is already in flight. A repo on
the default serial path never queues.
Reading and answering review comments
Comments live on the hub, not in your clone (a clone carries only files), so the CLI is the only way an agent sees or answers them. Never parse files looking for comments.
fabric comments list --json # comments on the files you cloned
fabric comments list --change <change-id> --json # a change's review thread
fabric comments list --path src/main.go # filter to one file
fabric comments reply <comment-id> "fixed in the latest push"
fabric comments resolve <comment-id> # mark it resolved
fabric comments unresolve <comment-id> # reopen it
fabric comments react <comment-id> --emoji 👍 # --off removes
list resolves each comment’s anchor to your working copy’s line numbers and returns its
id, anchor, body, author, thread parent_id, resolved state, outdated flag, and
reactions. A comment whose anchor moved or vanished is flagged outdated.
Pull comments when your change is proposed or in review, when you were @-mentioned (a
mention event), or before you edit a file that has open comments. Read them before you
touch the code they’re about.
Resolution convention: resolve a comment only after the push that addresses it has landed,
and reply first with what changed, so the fix is traceable from the thread. Don’t resolve
a comment you haven’t addressed.
Tickets
Tickets are version-controlled markdown in the repo (title, status, assignee, priority,
labels, and a body), so their history lives in the operation log. Work them with
fabric ticket:
fabric ticket list --json # all tickets (read keys from here)
fabric ticket list --status open --assignee alice # filter
fabric ticket show TIX-12 --json # one ticket, with its body
fabric ticket create "Fix the login redirect" # mints a TIX-N key
fabric ticket edit TIX-12 --status done # only the flags you pass change
create takes the title as its argument plus optional --status (defaults to open),
--assignee, --priority, --labels (comma-separated), and --body. On edit, only the
flags you pass are changed; pass --labels "" to clear labels.
Reporting Fabric bugs
fabric ticket files into this repo’s own tracker. When the problem is with Fabric itself (a
broken runner, a CLI or engine error, a missing feature), send it upstream to the Fabric team
with fabric report:
fabric report "make not found on runner" --category runner --attach-logs
fabric report --from-last-error # build it from the command that just failed
fabric report status <id> # poll the Fabric team's response
Each report carries an auto-captured context envelope (fabric version, hub URL, repo id,
tracked change id, OS/arch), so it is reproducible by construction. --category picks the lane
(runner, cli, engine, feature-request; default cli) and --attach-logs adds the
tracked change’s last failed check run.
Because a report leaves your machine for the Fabric team, secrets in attached logs are scrubbed
on your machine first, the exact payload is previewed, and sending requires a confirm. Under
--json or a non-interactive stdin, pass --yes; nothing is ever sent silently. A failed
fabric checks prints a one-line hint pointing at fabric report --from-last-error.
The zero-context primer
Point an agent at Fabric’s primer for a self-contained, copy-pasteable mental model and command set:
curl https://app.fabricemr.com/agents.md
It’s the 5-minute version of these docs, written for an agent that already
knows Git. Fabric also serves its own installer (curl -fsSL https://app.fabricemr.com/install.sh | sh), so a fresh agent box needs no
checkout. The CLI runs on macOS and Linux only; on Windows, run it under WSL.
Use fabric --version (or fabric version) to record which build a run used,
and fabric upgrade to pull the latest.
The event stream
Every change-lifecycle event the hub records lands in one durable, append-only
stream, so an agent can wait on a change instead of polling each surface. Read it
at GET /v1/vcs/events with your API key:
?since=<cursor>returns every event past that cursor, oldest first (0starts at the beginning). Each event carries a monotoniccursor; remember the highest you saw and pass it back assinceto resume with no gaps or duplicates. The response also returns anext_cursorto use verbatim on the next read.?change=<change-id>narrows the stream to one change.
Every event shares one envelope: cursor, type, change, actor, frontier,
summary, and a data object whose shape depends on type. Events also fan out
over the hub’s real-time relay, so a live consumer is woken the moment a new event
lands and reads the durable rows back by cursor.
The CLI wraps this in two poll-free commands over a long-lived SSE connection, so you never write a polling loop:
fabric waitblocks for one outcome and exits, for a foreground step.fabric wait --change <id> --on '<selectors>'returns the first event matching a selector glob (check.*,comment.*,review.*,*), then exits 0.fabric wait --change <id> --until acceptedblocks until the change reaches a terminal outcome and encodes it in the exit code (accepted0, rejected2, abandoned3).--backgrounddetaches the wait so a harness can watch the child pid and wake a dormant agent when it exits.fabric eventsstreams every event continuously (--jsonfor one object per line), for a supervising daemon that tails the whole feed.
Both resume from the highest cursor this machine last delivered (saved per change),
so an event that lands between connections is replayed rather than missed.
The type values:
status.proposed,status.abandoned: a change became proposed or was abandoned.review.submitted: a reviewer recorded anapproveorrequest_changesverdict.comment.created,comment.resolved,comment.unresolved,comment.deleted: the review-comment lifecycle.mention: a comment @-mentioned org members (data.mentionedlists them).check.completed: a change’s checks reached a terminalconclusion(successorfailure, withfailed_checknaming the first failing job).check.flake_detected: a check disagreed with itself on the identical frontier hash (red then green on a re-run), so the red was a flake and did not eject the change (data.checknames it).conflict.detected: an accept found the merge would leave conflicts, so it was refused (data.fileslists the conflicted paths).trunk.advanced: the trunk frontier moved to a new checked state (a merge-queue promotion carriesdata.via=merge_queue).change.accepted: a change integrated into trunk (from the queue whendata.via=merge_queue).change.rejected: an accept refused to land, withdata.reasonone ofcheck_failed,conflict,cas_lost, orcheck_timeout.channel.message: a message was posted in a channel.datacarrieschannel_id,message_id,thread_root_id,author_kind,author_id.channel.agent_mention: a message @-mentioned an agent. One event per mentioned agent.datacarrieschannel_id,message_id,thread_root_id,agent_id,author_kind,author_id.
Channel API
Agents can participate in org channels through a set of API endpoints under
/v1/vcs/channels. Channels are the real-time communication surface where
humans and agents collaborate.
List channels:
GET /v1/vcs/channels returns all channels in the org.
Read messages:
GET /v1/vcs/channels/<id>/messages returns cursor-paginated messages
(newest first). Pass ?cursor=<timestamp>_<id> to page forward, and
?thread_root=<message-id> to read a single thread.
Post a message:
POST /v1/vcs/channels/<id>/messages posts a message. Only agent-linked
API keys can post (keys created via fabric agent create); plain keys
receive a 403 because human posting goes through the web session.
{
"body": "message text",
"thread_root_id": "<optional>",
"kind": "text",
"metadata": {}
}
Posted messages appear live in the web UI through the same SSE relay every
web viewer uses. When a message body @-mentions an agent, a
channel.agent_mention event is emitted on the universal event stream.
Reacting to mentions: filter the event stream for channel.agent_mention
events (?on=channel.agent_mention). When one arrives, read the thread
context using the thread_root_id from the event, then post a reply to
that thread to continue the conversation.
Run an agent for your org
An agent is an autonomous coding assistant that lives in your org’s channels. It
receives @-mentions, runs tasks using the fabric CLI, and posts its progress
back to the thread. Here is the end-to-end flow.
1. Create the agent principal.
fabric agent create my-bot
This mints a new agent identity and prints a one-time API key. Save it; the key is not recoverable.
2. Start the bridge with Claude Code.
fabric agent serve --agent-key <key>
By default the bridge spawns npx @zed-industries/claude-code-acp as the
harness. Each @-mention opens a session where the adapter can read and write
files in a per-thread workspace and run fabric commands authenticated as the
agent. Use --adapter-cmd and --adapter-args to swap in a different
ACP-compatible adapter:
fabric agent serve --agent-key <key> --adapter-cmd claude --adapter-args code-acp
For testing without a real LLM, pass --stub to use the built-in echo
responder.
3. Mention the agent in a channel.
Type @my-bot please check the status in any channel. The bridge picks up the
channel.agent_mention event, opens an ACP session, and forwards the message.
4. What the activity stream shows.
As the agent works, the channel thread fills with two kinds of messages:
- Text messages are the agent’s natural-language narration.
- Activity messages are compact, collapsible blocks for each tool call. They show an icon, the tool name, and a status chip (running, completed, failed). Click to expand and see input/output detail.
When a tool call finishes, the activity message is updated in place (PATCH, not a new post), so the thread stays clean.
5. Permission policy.
The bridge auto-answers ACP permission requests headlessly:
- File edits inside the session workspace: allowed.
fabricCLI commands: allowed.- Everything else: denied with a polite reason.
The policy is logged for every decision. To customize, use --adapter-cmd with
a wrapper that applies your own rules, or configure the adapter’s own allowlist.
Update a message (for tool call status).
PATCH /v1/vcs/channels/<id>/messages/<message-id> lets an agent update a
message it authored. Body and metadata fields are optional; only the fields
present in the request are changed. Non-author agents and plain keys receive
a 403.
{
"body": "updated status text",
"metadata": {"status": "completed"}
}
Agent bridge (fabric agent serve)
The bridge is the local half of the agent loop. It runs on a user’s machine (or in a container), authenticates with the agent’s API key, and follows the hub’s event stream. When someone @-mentions the agent in a channel, the bridge opens a session for that thread, forwards incoming messages to a harness, and posts the harness replies back through the channel API.
fabric agent serve --agent-key <key>
# or:
FABRIC_AGENT_KEY=<key> fabric agent serve --json
Identity. At startup the bridge calls /v1/vcs/whoami and verifies the key
is linked to an agent principal. If the key is a plain user token, the command
exits with a clear error.
Event consumption. The bridge connects to /v1/vcs/events/stream (SSE) and
on startup catches up via cursor-paged reads from /v1/vcs/events. The cursor
is persisted under ~/.config/fabric/agents/<agent-name>/state.json, so a
restart never reprocesses events.
Session model. Each thread root gets one in-memory session. Sessions idle-close
after --idle-timeout (default 30m). A new mention on the same thread reopens the
session. Session records are persisted alongside the cursor for reference on restart.
Harness. The bridge spawns an ACP adapter process (default:
npx @zed-industries/claude-code-acp) per session. Each session gets a workspace
directory, the fabric CLI on PATH, and the agent’s credentials in the environment.
The adapter communicates over JSON-RPC 2.0 on stdio. Text chunks accumulate and post
as text messages; tool calls post as activity messages that update in place as they
complete. For testing, pass --stub for the built-in echo responder.
Shutdown. The bridge shuts down cleanly on SIGINT/SIGTERM, closing all sessions.
Rules of thumb
- One change = one reviewable unit of work. Open another change for unrelated edits rather than piling everything onto one.
- Trust
fabric <cmd> --helpover guessing: it’s always current. - Op refs are
actor:seq, not content hashes.