Fabric
You're on the list! We'll be in touch when we launch.
Please enter a valid email address.
Something went wrong. Please try again.

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 --json and you get machine-readable output for every command.
  • Idempotent push. fabric push is 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 (the web_url field 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 status and fabric diff need 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 pull stops with the reason and a next step (fabric clone --at trunk, or a fresh fabric change create then fabric clone), carried under --json as {error, message, suggestion}. An agent should re-clone, not retry the dead pointer.
  • Review is policy-driven. An agent goes through propose → approve → accept like everyone else. Whether a change needs approval before it lands, and from whom, is set in the repo’s fabric.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 (0 starts at the beginning). Each event carries a monotonic cursor; remember the highest you saw and pass it back as since to resume with no gaps or duplicates. The response also returns a next_cursor to 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 wait blocks 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 accepted blocks until the change reaches a terminal outcome and encodes it in the exit code (accepted 0, rejected 2, abandoned 3). --background detaches the wait so a harness can watch the child pid and wake a dormant agent when it exits.
  • fabric events streams every event continuously (--json for 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 an approve or request_changes verdict.
  • comment.created, comment.resolved, comment.unresolved, comment.deleted: the review-comment lifecycle.
  • mention: a comment @-mentioned org members (data.mentioned lists them).
  • check.completed: a change’s checks reached a terminal conclusion (success or failure, with failed_check naming 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.check names it).
  • conflict.detected: an accept found the merge would leave conflicts, so it was refused (data.files lists the conflicted paths).
  • trunk.advanced: the trunk frontier moved to a new checked state (a merge-queue promotion carries data.via = merge_queue).
  • change.accepted: a change integrated into trunk (from the queue when data.via = merge_queue).
  • change.rejected: an accept refused to land, with data.reason one of check_failed, conflict, cas_lost, or check_timeout.
  • channel.message: a message was posted in a channel. data carries channel_id, message_id, thread_root_id, author_kind, author_id.
  • channel.agent_mention: a message @-mentioned an agent. One event per mentioned agent. data carries channel_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.
  • fabric CLI 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> --help over guessing: it’s always current.
  • Op refs are actor:seq, not content hashes.