Seam

For agents

An agent is a member of its own. It signs in with its own key, works in its own squares, and every commit it makes says which agent made it and for whom.

Everything here works the same from any AI tool: the CLI, the MCP tools and HTTP all reach the same server operations and answer in the same words.

Sign an agent in

  1. On heyquilt.com, make an invite for the agent (Agents) and register the agent with it, as the invite says. The agent gets its own keys: an access key (it starts with qa_), a refresh key and a resume key. Its id is on its row under Agents.
  2. A person who admins the quilt adds the agent as a member:
    $ seam quilts add agent:5c1e… --level write --quilt my-app
  3. Give seam the agent's access key with SEAM_TOKEN:
    $ export SEAM_TOKEN=qa_…

With SEAM_TOKEN set, every seam command acts as the agent instead of whoever signed in with seam login. SEAM_API_URL points it at another server; the default is https://api.heyquilt.com.

Each commit records the agent as its author, with its name, member id and AI tool, and the person who owns the agent as on_behalf_of. seam log shows it as Author: builder <agent:5c1e…> via claude.

The CLI on a cloud machine

On a server, a container or an agent's sandbox, install with npm install -g @heyquilt/seam, or use npx @heyquilt/seam without installing, with SEAM_TOKEN set. One square per task:

$ seam square open "Rate limit sign-in" --quilt my-app --dir work -- src
$ cd work
# edit files with your own tools
$ seam status
$ seam commit -m "Rate limit the sign-in route"
$ seam merge --dry-run
$ seam merge
Merged as patch main:784 (b7c8d9e0f1a2).
  • Add --json to any command for the server's full record instead of text.
  • Check seam events (or seam square show) between steps. When it says a patch would conflict, run seam rebase and settle it in your square before you merge.
  • seam commit works only in a block folder. To commit without a folder, use seam_commit with file contents, or the HTTP API.

MCP tools

seam mcp serves the same commands as MCP tools over stdio, for any AI tool that speaks MCP: the same tools, descriptions and answers. Add it to your tool as a server named seam that runs seam mcp (with SEAM_TOKEN in its environment for an agent).

{ "mcpServers": { "seam": { "command": "seam", "args": ["mcp"] } } }
  • In a block folder (one with .seam/block.json), the quilt and square come from the folder, and seam_commit seals the folder's changed files. Edit with your own tools.
  • Anywhere else, name the quilt and square in each call, and pass seam_commit the new contents: files: [{ path, content }] (base64: true for bytes) and delete: [paths].
ToolWhat it does
seam_quiltsLists the quilts you can reach, or makes a new one (new: its slug).
seam_open_squareOpens a square from the tip of main (or line) and returns its sq: id.
seam_squaresThe open squares, with holder, head, how far behind main and conflicted paths. all: true for every square.
seam_logHistory, newest first: rev, paths, limit (default 20).
seam_showOne commit or patch: who, when, why, which square, and its diff.
seam_diffThe difference between two revisions (from, to).
seam_blameEach line of a file with the commit and patch that last changed it.
seam_catA file at any revision; a conflicted file comes back with its markers (format diff, snapshot or git).
seam_lsThe files at a revision, under paths.
seam_commitSeals your square's changes as a commit: the block folder's changed files, or files: [{ path, content }] and delete.
seam_mergeMerges your square into a line (into: "main"), or another square or revision into yours (from). dry_run says what would happen.
seam_rebaseMoves your square onto the tip of its line (or onto). Conflicts become markers, never a stop.
seam_revertOpens a new square whose one commit undoes a patch or commit.
seam_cherry_pickApplies one commit's change (from) to your square as a new commit.
seam_resolveSettles conflicted files by choosing a side: ours, theirs, base, suggestion or delete.
seam_square_statusOne square as the server sees it: head, behind_by, conflicts, what merged under it, what would conflict.
seam_square_handoffHands your square to another member (to) with a note on what is done and what is left.
seam_abandonAbandons a square whose work is not wanted. Its commits stay readable.
seam_reopenReopens an abandoned square; you become its holder.
seam_foldFolds a square into another square you hold (into) as one commit, and closes it.
seam_splitSplits your square by paths: each group moves to a new square.
seam_eventsYour events since the cursor after: patch.merged, conflict.added, square.handoff.
seam_events_webhookHas the server POST your events to a URL, signed; url: "off" removes it.

Revisions are the same as everywhere: main, main~3, patch:12, sq:<id>, a commit id or git:<sha>.

An agent that only speaks HTTP (no command line, no MCP) uses the HTTP API with its access key.

Habits that keep work safe

  • Read a file from your own square (rev: sq:<id>), not from main. main moves while you work, and a commit built from a newer main than your square started from can undo work that merged in between.
  • When a write answers that the square moved on since you looked, look again with seam_square_status and retry.
  • A refused merge is not a failure: the conflict is in your square. Settle it and merge again.

Events and webhooks

Three kinds of event reach a member:

EventSent whenTo
patch.mergedA patch merged that changed a file your open square on that line also changed. Says whether your merge would now conflict.The square's holder
conflict.addedA commit wrote a new conflict into your square.The square's holder
square.handoffA square was handed from one member to another, with the note.Both of them

Read them

$ seam events --quilt my-app
#41 patch.merged: patch 784 by person:7f3e… "Fix sign-in" changed src/auth.js in your square sq:77aa01bc; would conflict in src/auth.js
(next: after=41)
$ seam events --quilt my-app --after 41 --kinds patch.merged,conflict.added

Pass the cursor from the last answer as --after to get only new ones. Over HTTP: GET /v1/quilts/<q>/events?after=41.

Be woken by a webhook

$ seam events --quilt my-app --webhook https://agent.example.com/quilt
Events (patch.merged, conflict.added, square.handoff) now POST to https://agent.example.com/quilt
Signing secret (shown once): …
$ seam events --quilt my-app --webhook off
Webhook removed.
  • One webhook per member per quilt. The URL must use https.
  • --kinds picks the events (all three by default). --secret sets your own signing secret, 16 to 200 characters; otherwise one is made and shown once.
  • Each POST is JSON with the headers x-quilt-event, x-quilt-delivery, x-quilt-timestamp and x-quilt-signature.
  • A failed POST is tried again after 1, 5 and 25 seconds. Answer with a 2xx quickly.
A webhook body
{
  "event": "patch.merged",
  "id": "019a1f2c-…:41",
  "to": "agent:5c1e…",
  "ts": 1761400000000,
  "kind": "patch.merged",
  "event_seq": 41,
  "at": 1761400000000,
  "cause_seq": 5120,
  "quilt": "019a1f2c-…",
  "square": "019a2b7e-…-0d3e77aa01bc",
  "square_short": "sq:77aa01bc",
  "line": "main",
  "patch_no": 784,
  "by": { "member_id": "person:7f3e…", "kind": "person", "name": "Ada" },
  "paths": ["src/auth.js"],
  "payload": {
    "patch_no": 784,
    "commit": "5f4e3d2c1b0a…",
    "message": "Fix sign-in",
    "author": "person:7f3e…",
    "stat": [{ "path": "src/auth.js", "op": "put" }],
    "would_conflict": true,
    "would_conflict_paths": ["src/auth.js"]
  }
}

Check the signature before you trust a POST. It is sha256= and the HMAC-SHA256 of <timestamp>.<body> with your secret:

Node.js
import crypto from 'node:crypto'

function fromQuilt (secret, req, body) {
  const ts = req.headers['x-quilt-timestamp']
  const want = 'sha256=' + crypto.createHmac('sha256', secret).update(`${ts}.${body}`).digest('hex')
  const got = String(req.headers['x-quilt-signature'] || '')
  return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))
}