diff --git a/web/content/docs/file/api-reference.mdx b/web/content/docs/file/api-reference.mdx index 9457aee..9c0eb37 100644 --- a/web/content/docs/file/api-reference.mdx +++ b/web/content/docs/file/api-reference.mdx @@ -65,12 +65,13 @@ curl -sS \ ### Write a file -`PUT /v1/workspaces/{workspaceId}/fs/file` creates or updates one file. +`PUT /v1/workspaces/{workspaceId}/fs/file` creates or updates one file. It requires **`If-Match`** as well as the usual `X-Correlation-Id`: without it the request fails `412 precondition_failed`. `If-Match: *` is create-or-overwrite; pass a revision for a conditional write. ```bash curl -sS -X PUT \ -H "Authorization: Bearer ${RELAYFILE_TOKEN}" \ -H "X-Correlation-Id: ${RELAYFILE_CORRELATION_ID}" \ + -H "If-Match: *" \ -H "Content-Type: application/json" \ "${RELAYFILE_BASE_URL}/v1/workspaces/${RELAYFILE_WORKSPACE}/fs/file" \ -d '{ "path": "/docs/guide.md", "content": "# agent guide", "contentType": "text/markdown" }' | jq . diff --git a/web/content/docs/file/cli.mdx b/web/content/docs/file/cli.mdx index 331b64f..2a87957 100644 --- a/web/content/docs/file/cli.mdx +++ b/web/content/docs/file/cli.mdx @@ -56,20 +56,18 @@ The default path delegates to `agent-relay login`. `--api-key` keeps the self-ho ## `relayfile seed` -Bulk-upload a local directory into a workspace, respecting `.gitignore` and `--exclude` patterns. +Bulk-upload a local directory into a workspace, respecting `.gitignore`. ```bash relayfile seed my-workspace ./src ``` -| Flag | Default | Description | -|---|---|---| -| `--exclude` | (none) | Glob patterns to exclude (repeatable) | -| `--dry-run` | `false` | List files that would upload, without uploading | -| `--batch-size` | `50` | Files per bulk API request | - It walks the directory and posts batches to the bulk write endpoint, printing progress. + + Two limits as of `0.10.41`: `seed` takes no flags — `--dry-run`, `--exclude`, and `--batch-size` are rejected as undefined — and it resolves credentials only from `~/.relayfile/credentials.json`, so it does not work from a relay Cloud session (`relayfile login --api-key`, or `--token`, is required). `relayfile ops list` has the same credential limitation and degrades to local-only results with a warning. + + ## `relayfile tree` List a remote workspace path without mounting. @@ -85,6 +83,55 @@ relayfile tree my-workspace / --depth 2 Prints a compact human-readable tree by default; `--json` is for scripts. `relayfile read ` (alias `relayfile cat`) reads a single file the same way. +## `relayfile listen` + +Stream the workspace event feed, optionally running a command per event. + +```bash +relayfile listen \ + --path "/linear/issues/by-state/triage/**" \ + --event file.created \ + --run "claude --print 'Triage this: {{path}}'" +``` + +| Flag | Default | Description | +|---|---|---| +| `--provider` | all | Only events from one provider | +| `--path` | all | Path glob to filter on | +| `--event` | all | Event type (`file.created`, `file.updated`, `file.deleted`) | +| `--run` | (none) | Command to execute per event, with placeholder substitution (below) | +| `--format` | `text` | `text` or `json` (one event object per line) | +| `--background` | `false` | Detach and keep listening | + +`--run` substitutes five placeholders into the command: + +| Placeholder | Expands to | Example | +|---|---|---| +| `{{path}}` | the changed path | `/runs/pr-59/findings/security.json` | +| `{{type}}` | the event type | `file.created` | +| `{{provider}}` | the owning provider | `github` | +| `{{revision}}` | the file's revision | `rev_2936535` | +| `{{event}}` | **the whole event**, as space-separated `key:value` pairs | `type:file.created path:/runs/… revision:rev_… eventId:evt_… origin:agent_write …` | + + + Quote `{{event}}`. It expands to the entire event — a dozen or more space-separated tokens — so an unquoted `--run "my-agent --event {{event}}"` splatters them across `argv`. Write `--run "my-agent --event '{{event}}'"`. The single-value placeholders are passed as one argument each. + + +Events carry `eventId`, `type`, `path`, `revision`, `provider`, `origin`, `correlationId`, and — for small files — the content inlined. See [Events and webhooks](/docs/file/events). + +To keep a subscriber running across reboots, `relayfile supervisor install` accepts every `listen` flag and embeds them verbatim into a launchd (macOS) or systemd (Linux) unit that restarts on failure: + +```bash +relayfile supervisor install \ + --path "/linear/issues/by-state/triage/**" --event file.created \ + --run "claude --print 'New triage issue at {{path}}. Assign it.'" +relayfile supervisor status +``` + + + `listen` is missing from `relayfile --help` and `relayfile help listen` prints the generic help, but `relayfile listen --help` shows its usage. Expect to supervise it: on a busy workspace the stream can end mid-message, and reconnecting immediately earns a `429` on the WebSocket handshake — run it with `--background` or under `relayfile supervisor install`, and back off between reconnects. + + ## `relayfile mount` Mount a workspace to a local directory, syncing changes in real time. This replaces the standalone daemon for end users. @@ -138,10 +185,19 @@ See [Run locally](/docs/file/run-locally) for the daemon in context and [Local d | `relayfile export` | Download a snapshot (`--format tar\|json\|patch`) | | `relayfile status` | Per-provider sync state, lag, conflicts, denials | | `relayfile integration connect / list / disconnect` | Manage provider integrations after setup | +| `relayfile integration available / search` | Browse or search the live provider catalog (`--refresh`, `--json`) | +| `relayfile integration set-metadata` | Replace flat provider metadata (Jira/Confluence `cloudId`) | +| `relayfile integration bind / unbind` | Route a provider path glob to a channel webhook | | `relayfile pull` | Force a reconcile of a path or the whole workspace | -| `relayfile permissions` | Show writable paths and expected schema for a path | +| `relayfile writeback status / list / retry` | Local pending, failed, and dead-lettered writebacks | | `relayfile ops list / replay` | Inspect and replay dead-lettered writeback ops | +| `relayfile restart` / `relayfile supervisor` | Restart a mount, or install it as a launchd/systemd service | | `relayfile stop` / `relayfile logs` | Control and read a background mount daemon | +| `relayfile observer` | Open the hosted file observer for a workspace | + + + There is no `relayfile permissions` command. To find out what a path expects, read the provider's `.adapter.md` and `.schema.json` under `/discovery//…`; to see what was denied, read `.relay/state.json` → `deniedPaths` in the mirror. + ## Global flags diff --git a/web/content/docs/file/events.mdx b/web/content/docs/file/events.mdx index 0ae2bd4..22eab11 100644 --- a/web/content/docs/file/events.mdx +++ b/web/content/docs/file/events.mdx @@ -23,16 +23,24 @@ Every change — webhook, sync, or another agent's write — surfaces as the sam ```json { - "eventId": "evt_01HQ8K7M2YV3R0XW9F4ZB6T2QA", - "type": "file.updated", - "path": "/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json", - "revision": "rev_42", - "provider": "linear", - "origin": "provider_sync", - "timestamp": "2026-05-13T14:32:01Z" + "eventId": "evt_2507297", + "type": "file.created", + "path": "/runs/pr-59/findings/security.json", + "revision": "rev_2935117", + "provider": "runs", + "origin": "agent_write", + "contentType": "application/json", + "contentHash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "content": "{}", + "inlineContent": true, + "encoding": "utf-8", + "correlationId": "lt-1-1788520629", + "timestamp": "2026-09-04T11:17:09.694Z" } ``` +Small files arrive with their content inlined (`inlineContent: true`), so a handler often needs no follow-up read. `correlationId` carries through from the write that caused the event, which is how you tie an event back to the request that produced it. + - **`type`** is one of `file.created`, `file.updated`, `file.deleted`. - **`path`** is the canonical file that changed. The path itself carries context — you know which provider and which record without parsing a payload. - **`revision`** monotonically increases per file. Use it to order events and to fetch the prior state for a diff. @@ -49,13 +57,15 @@ relayfile listen \ --run "claude --print 'Triage this: {{path}}'" ``` +`relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]` streams the workspace's event feed. `--run` substitutes `{{path}}`, `{{type}}`, `{{provider}}`, and `{{revision}}` as single values, plus `{{event}}` for the whole event as space-separated `key:value` pairs — quote that one, or its dozen-odd tokens splatter across the command's arguments. `--format json` prints one event object per line, for piping into anything that isn't a shell command. To fan events out to a channel instead of a local process, bind the glob to a webhook with `relayfile integration bind --channel … --webhook … --webhook-token …`. + Or from the SDK with `onWrite`, which subscribes over the same WebSocket stream and dispatches by pattern: ```typescript import { onWrite } from '@relayfile/sdk'; onWrite('/linear/issues/**', async (event) => { - if (event.source === 'agent') return; // ignore our own writes + if (event.origin === 'agent_write') return; // ignore our own writes await agent.handle(event); }, { client, workspaceId, operations: ['create', 'update'] }); ``` @@ -67,6 +77,7 @@ See [Agents](/docs/file/agents) for the framework helpers built on this. - **At-least-once.** Events can repeat. Deduplicate on `eventId`; treat handlers as idempotent. - **Ordering.** Per file, `revision` is the source of truth — wall-clock `timestamp` can be close together under bursty traffic. - **Catch-up.** A subscriber that connects with a cursor receives the events it missed while disconnected, so a restart doesn't drop changes. If the WebSocket can't open, the SDK degrades to HTTP polling rather than going silent. +- **Reconnect.** Long-lived subscribers do get dropped — a busy workspace can end a stream mid-message — and a reconnect storm is answered with `429` on the WebSocket handshake. Supervise the subscriber (`relayfile listen --background`, or `relayfile supervisor install`) and back off between reconnects rather than looping immediately. diff --git a/web/content/docs/file/mount-layout.mdx b/web/content/docs/file/mount-layout.mdx index 6264e71..acab8fd 100644 --- a/web/content/docs/file/mount-layout.mdx +++ b/web/content/docs/file/mount-layout.mdx @@ -1,18 +1,18 @@ --- title: 'Mount layout' -description: 'Every Relayfile mount is self-describing: LAYOUT.md, per-integration layout files, _index.json, canonical naming, and four alias views.' +description: 'Every Relayfile mount is self-describing: LAYOUT.md, per-integration layout files, _index.json, canonical naming, and per-adapter alias views.' --- -Every mount is self-describing. The agent never needs to learn paths from external documentation — `cat mount/LAYOUT.md` lists everything, and per-integration `.layout.md` files document the tree shape for each provider. This is deliberate: an agent oriented by reading the tree itself doesn't carry path knowledge in its prompt or schema. +Every mount is self-describing. The agent never needs to learn paths from external documentation — `cat mount/LAYOUT.md` lists everything, and per-integration `LAYOUT.md` files document the tree shape for each provider. This is deliberate: an agent oriented by reading the tree itself doesn't carry path knowledge in its prompt or schema. ## The tree ``` mount/ -├── LAYOUT.md # virtual, read-only — top-level guide +├── LAYOUT.md # top-level guide: every provider root ├── _index.json # root listing ├── linear/ -│ ├── .layout.md # linear-specific tree shape +│ ├── LAYOUT.md # linear-specific tree shape │ ├── issues/ │ │ ├── _index.json │ │ ├── AGE-12__fix-login-bug.json # canonical: __ @@ -20,12 +20,15 @@ mount/ │ │ ├── by-id/AGE-12.json │ │ └── by-state/in-progress/AGE-12__fix-login-bug.json │ └── users/by-name/dana.json -└── github/ - └── repos/ - ├── _index.json - ├── acme/api/ - │ └── pulls/42__bump-deps/meta.json - └── by-name/acme__api.json +├── github/ +│ ├── LAYOUT.md +│ └── repos/ +│ ├── _index.json +│ └── acme/api/ +│ ├── pulls/_index.json +│ └── pulls/42__bump-deps/meta.json # __ +└── github/repos/acme__api/ # flat alias namespace + └── pulls/by-id/42.json ``` ## Self-describing files @@ -33,12 +36,12 @@ mount/ Three kinds of metadata files make the tree navigable without prior knowledge: - **`LAYOUT.md`** at the root is a virtual, read-only guide to the top-level structure. It's the first thing an agent should `cat` after mounting. -- **`/.layout.md`** documents the tree shape for a single provider — what directories exist under `linear/`, what each holds. +- **`/LAYOUT.md`** documents the tree shape for a single provider — what directories exist under `linear/`, what each holds, and which paths are artifacts rather than records. Uppercase, no leading dot. - **`_index.json`** in every directory lists the rows that directory contains, so an agent can read one file to understand a directory rather than walking every entry. ## Canonical naming -Entity files follow a `__` convention so identifiers are recoverable from any filename. The ID is always the **last** `__`-separated segment: +Entity files put an identifier and a human-readable name in one filename, separated by `__`, so identifiers are recoverable from a listing. **Which side the id sits on is per-adapter**: Linear writes `__` (id last), GitHub writes `__` (id first). Read the provider's `LAYOUT.md` rather than assuming, and prefer `_index.json` rows for lookup: ```text inbox/threads/Re_Welcome__01HXYZ.json @@ -48,19 +51,21 @@ inbox/threads/01HXYZ.json The first path is the canonical human-readable form. The second is a legacy fallback that remains readable during the naming transition — consumers should accept both the new-style filename and the bare `` basename while producers are updated. - Because the ID is recoverable from the filename, an agent can read `AGE-12__fix-login-bug.json`, derive the identifier `AGE-12`, and write back to the canonical path without a separate lookup. + Take the exact filename from a listing or `_index.json` rather than assembling one. Some adapters require the full `__` directory on reads — for GitHub, `pulls/59/meta.json` returns `404` while `pulls/59__/meta.json` resolves — and the same adapter may still accept a bare id on the write path. -## The four alias views +## Alias views -Each resource type ships four alias views out of the box, so an agent can navigate the same records by whichever key it has in hand: +Alias views let an agent navigate the same records by whichever key it has in hand — `by-id/`, `by-title/`, `by-state/`, `by-edited/`, `by-assignee/`, `by-creator/`, `by-priority/`, `by-name/`, `by-uuid/`. They are views over the same canonical entities, not copies you have to keep in sync. -- **`by-title/`** — slug lookups (`AGE-12-fix-login-bug.json`). -- **`by-id/`** — identifier lookups (`AGE-12.json`). -- **`by-name/`** — human-readable name lookups (`dana.json`, `acme__api.json`). -- **`by-state/`** — grouped by issue or PR state (`by-state/in-progress/...`). +**Which views exist, and where they live, is decided by the adapter and the resource** — there is no fixed set. Two things to check in the provider's `LAYOUT.md` before building a path: -These are views over the same canonical entities, not copies you have to keep in sync. An agent that knows only a state can `ls by-state/in-progress/`; one that knows only a name can read `by-name/dana.json`. +- **Location.** Linear puts them under the canonical subtree (`/linear/issues/by-id/AR-100.json`). GitHub puts them in a *flat sibling namespace* keyed `__` (`/github/repos/acme__api/pulls/by-id/42.json`) — there is no `by-name` under `/github/repos/`. +- **Set.** On a live workspace, GitHub pulls expose `by-creator`, `by-edited`, `by-id`, `by-state`, and `by-title`; GitHub issues expose four of those; Linear issues add `by-assignee`, `by-priority`, and `by-uuid`. + + + A flat alias directory also has an `_index.json`, and it means something different: it is a manifest of which alias subdirectories exist (`{ "rows": [ { "title": "by-id", "file": "by-id/" } ] }`), not a record index. Only the canonical tree's `_index.json` lists records, as a bare JSON array. + ## Lazy repo materialization diff --git a/web/content/docs/file/quickstart.mdx b/web/content/docs/file/quickstart.mdx index f5606e1..d28ec0f 100644 --- a/web/content/docs/file/quickstart.mdx +++ b/web/content/docs/file/quickstart.mdx @@ -50,6 +50,10 @@ grep -rl '"state":"Todo"' ./relayfile-mount/linear/issues/ See [Reads and writes](/docs/file/reads-and-writes) for the full PATCH / CREATE / DELETE model. + + Guide: build a PR review bot — add providers, mount the same workspace in every sandbox, and post the review by writing a file. + + ## Next steps diff --git a/web/content/docs/file/reads-and-writes.mdx b/web/content/docs/file/reads-and-writes.mdx index 147d8ac..af87e7d 100644 --- a/web/content/docs/file/reads-and-writes.mdx +++ b/web/content/docs/file/reads-and-writes.mdx @@ -56,13 +56,16 @@ Writes are durable: they're recorded in Relayfile's writeback queue and processe ## Discovering schemas in-tree -You don't need an out-of-band schema registry. Per-resource schemas are discoverable in the tree itself at `/.schema.json`: +You don't need an out-of-band schema registry. Every writable resource advertises a JSON Schema and a create example in the workspace's own `/discovery` tree, whose paths carry literal placeholder segments: ```bash -cat mount/linear/issues/.schema.json +cat mount/discovery/github/.adapter.md # operations + ID patterns +cat mount/discovery/linear/issues/.schema.json # full record schema +cat mount/discovery/linear/issues/.create.example.json # minimal create payload +cat "mount/discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.schema.json" ``` -An agent that wants to create or patch a record reads the adjacent schema file to learn the expected shape, then writes a conforming JSON file. This keeps the contract co-located with the data — the same self-describing principle as [`LAYOUT.md` and `.layout.md`](/docs/file/mount-layout). +`.adapter.md` is the per-provider contract: which resources are writable, the ID pattern that decides whether a filename is a canonical record or a create draft, and which operations each resource supports. `.schema.json` is JSON Schema draft 2020-12 for the full synced record; fields marked `readOnly` are server-managed and are rejected on write. Read them before composing a payload — the same self-describing principle as [`LAYOUT.md`](/docs/file/mount-layout). The exact write semantics and field mapping per provider are defined by the [adapters](/docs/file/adapters-and-providers), which own webhook-to-path mapping and writeback behavior. diff --git a/web/content/docs/file/review-bot-brief.mdx b/web/content/docs/file/review-bot-brief.mdx new file mode 100644 index 0000000..4d5460f --- /dev/null +++ b/web/content/docs/file/review-bot-brief.mdx @@ -0,0 +1,193 @@ +--- +title: 'Review bot agent brief' +description: 'A complete, copy-paste operating document for a Relayfile-backed PR review agent — drop it in as AGENTS.md, CLAUDE.md, or a skill and hand it to the agent.' +--- + + + **This whole page is the brief.** Everything below is addressed to the agent, not to you. Take it with **Copy page as markdown** (top right) or `curl -o AGENTS.md https://agentrelay.com/docs/file/markdown/review-bot-brief.md`. + + Before handing it over, replace every `` placeholder — `/` and the specialist name are the two you set once, and ``, ``, ``, ``, ``, ``, and `` are filled per run or per finding. Verified against `relayfile` 0.10.53; it pairs with [Build a PR review bot](/docs/file/review-bot). + + +You are a specialist agent in a multi-agent pull request review. Your inputs, your peers' outputs, and the review you publish are all files in a Relayfile workspace. Read and write files; do not call GitHub, Linear, Notion, or Slack APIs directly, and do not ask for provider tokens — you don't have any and don't need any. + +## Your environment + +| Variable | What it is | +|---|---| +| `RELAYFILE_LOCAL_DIR` | The mount root, when you have a mirror. Every path below is relative to it. | +| `RELAYFILE_WORKSPACE` | The `rw_…` workspace id. Use it verbatim; never substitute another id. | +| `RELAYFILE_TOKEN` | Your scoped workspace token, already narrowed to the paths you may touch. | +| `RELAYFILE_BASE_URL` | The API base, for when you have no mirror. | + +With a mirror, `ls`, `cat`, `grep`, `find`, and ordinary writes all work. Without one, `relayfile tree ` and `relayfile read ` are the same operations against the server. Provider records are already current — the file is materialized before you're told about it, so never fetch a record from a provider API to "get the current version". + +## Orient before you act + +Do this first, every run. Do not carry path knowledge between runs and do not guess paths: + +```bash +cat "$RELAYFILE_LOCAL_DIR/LAYOUT.md" # top-level guide: every provider root +cat "$RELAYFILE_LOCAL_DIR/github/LAYOUT.md" # the GitHub adapter's own contract +``` + +The provider layout file is `/LAYOUT.md` — uppercase, no leading dot. + +Then read the index rather than walking directories. In the canonical tree, `_index.json` is a **bare JSON array**; pull rows carry `number`, `state`, `labels`, and `headRef`, which is usually enough to choose a PR without opening a record: + +```bash +jq '.[] | select(.state=="open") | {number, title, headRef}' \ + "$RELAYFILE_LOCAL_DIR/github/repos///pulls/_index.json" +``` + +Four naming rules that will otherwise cost you a run: + +1. GitHub record directories are **`__`, number first**. Linear uses `__`, id last. There is no single rule — read the provider's `LAYOUT.md`. +2. **Reads need the full directory name.** `pulls//meta.json` returns 404; only `pulls//meta.json` resolves. Take `` from the index or a listing; never assemble it. +3. **Records are envelopes.** A record is `{ provider, objectType, objectId, deleted, payload }`. The provider's object is under `payload` — `jq .payload.state`, not `jq .state`. +4. Alias views live in a flat sibling namespace for GitHub — `/github/repos/__/pulls/by-id/.json`, not under the canonical repo path — and under the canonical subtree for Linear (`/linear/issues/by-id/.json`). Which views exist varies by adapter and resource, so list the alias directory rather than assuming one. + +## What you may read and write + +- **Read** the provider trees you've been granted — typically `/github/**`, plus `/linear/**` and `/notion/**` when the run needs the ticket and the team's standards. +- **Write** only under `/runs/**`, and — if you are the publishing agent — the one review path named below. +- **Never write anything under `.relay/`.** It is daemon state. +- **Never write `merge.json` or `close.json`** under a pull request. Those are live write resources: they merge or close the PR. + +A write outside your grant is rejected, not silently dropped, and the path is recorded in `.relay/state.json` under `deniedPaths`. If that happens, stop and report it — do not route around it. + +## Read the pull request + +```bash +MOUNT="$RELAYFILE_LOCAL_DIR" +R="$MOUNT/github/repos//" + +jq '.[] | {number, state, title}' "$R/pulls/_index.json" # pick the PR +cat "$R/pulls//meta.json" | jq '.payload | {number, title, state}' +``` + +`diff.patch` and `files/**` may exist under the PR directory as artifacts — useful to read, but they are not canonical records. Then pull the context a good review needs, when those providers are mounted: + +```bash +cat "$MOUNT/linear/issues/by-id/.json" | jq '.payload.title' +grep -rl 'review checklist' "$MOUNT/notion/pages/" +``` + +`grep` searches every synced record, not a paginated API result. Prefer it over guessing which file holds something. + +## The run protocol + +One directory per review run. You are one writer among several, so stay inside your own file: + +```text +/runs// +├── recon.md # written once by the orchestrator; read-only to you +├── findings/ +│ ├── security.json # one file per specialist — write only your own +│ ├── quality.json +│ └── tests.json +└── review.json # the merged review, written by the publishing agent +``` + +`/runs/**` has no adapter behind it, so writes there persist and raise events without queueing any provider call. + +- **Read `recon.md` first.** It maps the PR — files touched, what they export, who imports them, the ticket. It was produced once so every specialist doesn't re-derive it. If it's missing, wait rather than duplicating the work. +- **Write exactly one findings file**, named for your specialty. One file each makes lost writes impossible. +- **Write it once, when you're done.** The orchestrator publishes each findings file the moment it appears and does not wait for the slowest specialist, so a partial file read as final is a real failure mode. + +Use this shape: + +```json +{ + "specialist": "security", + "commit": "", + "model": "", + "findings": [ + { + "path": "src/auth/session.ts", + "line": 42, + "severity": "high", + "title": "Session token compared with ==", + "body": "Timing-unsafe comparison. Use a constant-time compare.", + "confidence": "high" + } + ] +} +``` + +Record the commit you actually reviewed. A review attached to the wrong commit is worse than no review. + +## Publish the review + +Only the publishing agent does this. Discover the contract first — discovery documents live in their own tree with literal placeholder segments, not beside the records: + +```bash +cat "$MOUNT/discovery/github/.adapter.md" +D="$MOUNT/discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}" +cat "$D/reviews/.schema.json" +cat "$D/reviews/.create.example.json" +``` + +`.adapter.md` names every writable resource, its ID pattern, and what a draft becomes. For a review the payload requires **`event`, `body`, and `comments`** — omitting `comments` fails validation. `event` is one of `APPROVE`, `REQUEST_CHANGES`, `COMMENT`. Fields marked `readOnly` in the schema are rejected. + +Write the payload to a **non-canonical filename** in the resource directory — any name that doesn't match the resource's ID pattern (`^\d+$` for reviews) is treated as a create draft: + +```bash +cat > "$R/pulls//reviews/draft-.json" <<'JSON' +{ "event": "COMMENT", "body": "…", "comments": [] } +JSON +``` + +**The write path uses the bare pull number**, even though reads need ``. This asymmetry is real; don't derive one path from the other. + +Scratch names `partial.json`, `.tmp.json`, `*.tmp.json`, and `*.partial.json` are ignored and never become drafts. + +## Verify the write landed + +A queued write is not a delivered write. The adapter rewrites your draft into a receipt naming the real record — read the file back until `created` appears: + +```bash +cat "$R/pulls//reviews/draft-.json" +# { "created": 5112118049, "id": "5112118049", "url": "https://github.com/…#pullrequestreview-5112118049" } +``` + +That receipt is the proof, not the fact that the write succeeded locally. For writes made through a mirror you can also check the queue: + +```bash +relayfile writeback status "$RELAYFILE_WORKSPACE" --json +``` + +It reports the local mirror's `pending`, `failed`, and `dead-lettered` counts. If an op dead-lettered, read `.relay/dead-letter/.json` for `lastStatus` and `lastBody`, fix the cause — usually a payload that doesn't match the schema — then `relayfile writeback retry --opId `. Do not retry unchanged, repeatedly. + +## When something looks wrong + +| Symptom | What it means | Do this | +|---|---|---| +| `404 not_found` on a record | You assembled a path instead of reading it | Take the exact name from `_index.json` or a listing | +| `jq` returns null on a field | You read the envelope, not the record | Go through `.payload` | +| `429 workspace_busy` | The workspace is busy | Retry with backoff; it is not an error in your input | +| A path from this brief doesn't exist | Adapter layout differs by version | Re-read `LAYOUT.md` and `.adapter.md`; use what the workspace says | +| A write is rejected | Outside your ACL grant | Stop, report the path, check `.relay/state.json` → `deniedPaths` | +| Write succeeded, no receipt appears | Writeback queued, failed, or dead-lettered | `relayfile writeback status`, then the dead-letter record | +| The tree is empty or missing a provider | Mount raced the first sync, or the provider isn't connected | Report it; don't review partial data | + +## Rules + +1. Orient from `LAYOUT.md` and `_index.json` before touching anything. +2. Never invent a path, a filename, or a payload shape — discover all three. +3. Read record fields through `.payload`. +4. Write only your own findings file; never edit another agent's. +5. Never write under `.relay/`, `merge.json`, or `close.json`. +6. Read `recon.md` instead of re-deriving the PR map. +7. Record the commit you reviewed in your output. +8. Confirm the receipt before reporting success. +9. Report blockers instead of working around them. + + + + The human-facing walkthrough this brief belongs to. + + + The PATCH / CREATE / DELETE model the brief's write rules come from. + + diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx new file mode 100644 index 0000000..1514113 --- /dev/null +++ b/web/content/docs/file/review-bot.mdx @@ -0,0 +1,446 @@ +--- +title: 'Build a PR review bot' +description: 'End-to-end: connect GitHub with one command, add the rest of your context providers, give every sandbox the same workspace, and post the review back by writing a file.' +--- + +A PR review bot is the shape Relayfile fits best: several agents, several providers, one shared state. This guide runs the whole flow end to end — from an empty machine to a bot whose orchestrator and specialists all read the same PR, coordinate through files, and post the finished review back to GitHub without any of them holding a provider token. + + + Every command, path, and payload below was run against a live workspace on `relayfile` **0.10.53**. The review in step 7 was posted by writing the file this guide tells you to write — [`agentrelay.com#59` review 5112118049](https://github.com/AgentWorkforce/agentrelay.com/pull/59#pullrequestreview-5112118049). Where a path or flag is version-dependent, it says so. + + +## What you're building + +```text + Relayfile workspace (github + linear + notion + slack) + │ + ┌─────────────────────────┼─────────────────────────┐ + │ │ │ + orchestrator sandbox security sandbox quality sandbox + mount: /workspace mount: /workspace mount: /workspace + read /github/** read /github/** read /github/** + read /notion/** read /runs/** read /runs/** + write /runs/** write /runs/** write /runs/** +``` + +Every sandbox works against the *same* workspace. The orchestrator writes its recon notes to `/runs/…`; the specialists read them as ordinary files a second later. Specialists write findings to `/runs/…`; the orchestrator streams them out as each one lands. Nothing in that loop is a queue, a webhook, or a bespoke protocol — it's [real-time sync](/docs/file/realtime-sync) over a shared tree. + +The three things this guide buys you: + +- **One integration surface.** Adding Linear or Slack context later is `relayfile integration connect`, not another OAuth app, webhook endpoint, and client library. +- **Context across sandboxes.** Specialists in separate sandboxes see each other's work through the filesystem. +- **Writeback without provider tokens.** The bot posts its review by writing JSON to a path. Retries, rate limits, and dead-lettering are the writeback workers' problem. + +## Where it runs + +The integration stack is hosted either way — what changes is where the bot's processes live and how they get credentials. + +| | Workstation | Cloud sandboxes | Serverless / CI step | +|---|---|---|---| +| Provisioning | `relayfile setup` once, interactively | `relayfile setup` once by a human, **or** headless with `--cloud-token` | same one-time provisioning | +| Per-run access | long-lived `relayfile mount --background` | `ensureMountedWorkspace` per sandbox ([step 5](#5-give-every-sandbox-the-same-workspace)) | no mount — the HTTP API directly | +| Credentials in the process | your local relay session | a workspace-scoped JWT from `handle.env()` | `RELAYFILE_TOKEN` in the environment | +| Good for | building and debugging the bot | the real fleet — one sandbox per specialist | a single review pass with no daemon | + +**Provisioning happens once; access happens per run.** A cloud bot doesn't re-do OAuth on every PR — it joins a workspace that's already connected. + + + A workspace can have exactly **one registered local mirror per machine**. Pointing `relayfile mount` at a second directory fails with *"workspace … is already mirrored at …; refusing to silently re-home it"*. That's a guard, not a bug: `--rehome` **moves** the existing mirror rather than adding one. On a machine that already mounts the workspace for something else, use the API path instead of re-homing someone else's mirror. + + +## 1. Connect GitHub + +One command logs you into Cloud, creates the workspace, runs the provider OAuth, waits for the first sync, and mounts the result: + +```bash +relayfile setup \ + --provider github \ + --workspace review-bot \ + --local-dir ./relayfile-mount \ + --no-open +``` + +`--no-open` prints the login and connect URLs instead of launching a browser — always pass it when an agent (or CI) is driving the command, since a headless browser launch burns the OAuth state. + + + Both URLs are short-lived. A Cloud device code expires in minutes and the Nango connect URL has its own TTL, so complete them while the command is still waiting. If it exits first, re-run the same line — a re-run reuses the workspace and only opens a new connect flow when the provider isn't connected yet. + + +### Provisioning from a machine with no browser + +If the bot is provisioned by CI or a deploy job rather than a person at a laptop, skip the browser login with a Cloud token and skip the mount loop — this step only needs to leave a connected workspace behind: + +```bash +RELAYFILE_CLOUD_TOKEN="$CLOUD_TOKEN" relayfile setup \ + --provider github \ + --workspace review-bot \ + --skip-mount \ + --no-open +``` + +On a headless host you can also authorize from a browser on another machine — `agent-relay cloud login --device` prints a URL and a short code, then blocks until you approve it. + +The same provisioning from code, when your control plane already holds Cloud tokens: + +```ts +import { RelayfileSetup } from "@relayfile/sdk" + +const setup = RelayfileSetup.fromCloudTokens( + { accessToken, refreshToken, accessTokenExpiresAt }, + { cloudApiUrl: "https://agentrelay.com/cloud" }, +) + +const workspace = await setup.joinWorkspace("rw_…") +const client = workspace.client() // bound, auto-refreshing — used throughout this guide + +const { connectLink } = await workspace.connectIntegration("github") +if (connectLink) { + await notifyOperator(connectLink) // one-time human step + await workspace.waitForConnection("github") +} +``` + + + Use the `rw_…` workspace id for every data-plane call. It is not interchangeable with the request-side app UUID, and substituting one for the other fails in ways that look like a permissions problem. + + +## 2. Verify before you write any bot code + +```bash +relayfile status review-bot +``` + +The output is per-provider health plus a mirror footer: + +```text +workspace rw_… (review-bot) mode: poll lag: 0s +auth: agent-relay session ok + github healthy queue lag 0s event active; last event 3m12s ago + +local mirror: /Users/you/relayfile-mount +daemon: not running + +pending writebacks: 0 conflicts: 0 denied: 0 +``` + +- **`lag`** and per-provider `healthy` / `lagging` tell you whether reads will be current. +- **`daemon: not running`** means the mirror is stale until you start it: `relayfile mount review-bot ./relayfile-mount --background`. +- The footer counts (`pending writebacks`, `conflicts`, `denied`) are **local mirror state**. Dead-letter counts live in a different command — see step 7. + +Then let the workspace describe its own shape rather than hard-coding paths from this page: + +```bash +relayfile read review-bot /LAYOUT.md # top-level guide, lists every provider root +relayfile read review-bot /github/LAYOUT.md # the GitHub adapter's own contract +``` + + + The provider layout file is `/LAYOUT.md` — uppercase, no leading dot. `/github/LAYOUT.md` is long and worth reading in full: it documents index row shapes, alias views, and which paths are artifacts rather than records. + + +`relayfile tree` and `relayfile read` work against the server with no mount, which is what CI and a laptop checking on a cloud workspace both want: + +```bash +relayfile tree review-bot /github/repos --depth 2 +``` + +Two things to expect from `tree` on a real workspace: it **paginates** (it prints a `next cursor:` line on large directories), and a busy workspace returns `http 429 workspace_busy`. Retry with backoff, and prefer reading a directory's `_index.json` over walking it. + +## 3. Add the rest of the bot's context + +A review is better when the reviewer can see the ticket that motivated the PR and the standards it's supposed to follow. Don't guess provider ids — ask the live catalog: + +```bash +relayfile integration available --refresh +relayfile integration search notion --refresh --json +``` + +Then connect what you need. Each provider lands as another subtree under the same workspace: + +```bash +relayfile integration connect linear --workspace review-bot --no-open +relayfile integration connect notion --workspace review-bot --no-open +relayfile integration connect slack --workspace review-bot --no-open +relayfile integration list --workspace review-bot +``` + +`integration list` prints one row per connection with `provider / status / lag / last_event_at`, so it doubles as a health check. + +Nango is the default backend; request Composio explicitly for toolkits it brokers (`--backend composio`). If a Composio toolkit can't create managed auth automatically, the command says so — a human adds a custom auth config in Composio, then re-runs the identical command. + + + Jira and Confluence need a follow-up: a single Atlassian grant can cover several sites, so the CLI prompts for one after OAuth and stores its `cloudId`. If the picker was skipped, set it explicitly with `relayfile integration set-metadata jira cloudId=… baseUrl=https://….atlassian.net --workspace review-bot --yes`. The command replaces the whole metadata namespace, so pass every key you want to keep. + + +## 4. Read the PR + +Start from the index, not from a guessed filename: + +```bash +relayfile read review-bot /github/repos/AgentWorkforce/agentrelay.com/pulls/_index.json | jq '.[0:3]' +``` + +Canonical `_index.json` files are a **bare JSON array**. Pull rows carry the fields you'd otherwise open every record to get: + +```json +{ "id": "59", "title": "docs(relayfile): Guides section…", "updated": "2026-09-04T10:06:10Z", + "number": 59, "state": "open", "labels": [], "headRef": "docs/relayfile-review-bot-guide" } +``` + +That's enough to pick a PR by state, label, or branch without reading a single record. Then open the record: + +```bash +R=/github/repos/AgentWorkforce/agentrelay.com +relayfile read review-bot "$R/pulls/59__docs-relayfile-guides-section-pr-review-bot-walkthrough-local-cloud-and-a-copy/meta.json" +``` + +Three details that will bite an agent that guesses: + +1. **GitHub record directories are `__` — number first.** The generic "the id is the last `__` segment" rule in the root `LAYOUT.md` holds for Linear (`__`), not for GitHub. +2. **The full directory name is required for reads.** `pulls/59/meta.json` returns `404 not_found`; only `pulls/59__/meta.json` resolves. Take the name from `_index.json` or a directory listing — never assemble it. +3. **Records are envelopes.** A record is `{ provider, objectType, objectId, deleted, payload }` and the provider's own object is under `payload`. `jq .state` returns null; `jq .payload.state` is what you want. + +Alias views live in a flat sibling namespace, `__`, not under the canonical repo path: + +```bash +relayfile tree review-bot /github/repos/AgentWorkforce__agentrelay.com/pulls --depth 1 +# → by-creator/ by-edited/ by-id/ by-state/ by-title/ +``` + +Linear works the same way with its own keys — `/linear/issues/by-id/AR-100.json` resolves a ticket by its human identifier: + +```bash +relayfile read review-bot /linear/issues/by-id/AR-100.json | jq '.payload.title' +``` + +## 5. Give every sandbox the same workspace + +This is the step that makes it a fleet instead of a script. From inside an already-authorized sandbox, `ensureMountedWorkspace` mints a scoped mount token, supervises the `relayfile-mount` process, and resolves once the mirror is reachable: + +```ts +import { RelayfileSetup } from "@relayfile/sdk" + +const setup = new RelayfileSetup({ accessToken }) + +const handle = await setup.ensureMountedWorkspace({ + workspaceId: "rw_…", + provider: "github", + verifyProvider: true, + providerReadyTimeoutMs: 30_000, + localDir: "/workspace", + scopes: [ + "relayfile:fs:read:/github/**", + "relayfile:fs:read:/runs/**", + "relayfile:fs:write:/runs/**", + ], +}) + +// hand the mount to the specialist process — no provider tokens cross this line +await sandbox.process.executeCommand("claude --print 'Review /workspace'", { + env: handle.env(), +}) +``` + +Three things to keep straight when you fan this out: + +- **`verifyProvider` gates on readiness.** Without it a specialist can mount before the first sync finishes and review an empty tree. `ProviderNotConnectedError` and `ProviderNotReadyError` are typed so you can tell "never connected" from "still syncing". +- **Scope each specialist down.** The minted token's scopes are a subset of the caller's grant. Use the path-scoped form — a bare `fs:read` can fall back to a broad grant. See [ACLs](/docs/file/acls). +- **`handle.env()` is the whole handoff.** It carries `RELAYFILE_BASE_URL`, `RELAYFILE_TOKEN`, `RELAYFILE_WORKSPACE`, and `RELAYFILE_LOCAL_DIR`. The sandbox never sees your Cloud access token or the provider credentials. + +Each sandbox is a separate machine, so the one-mirror-per-machine rule doesn't constrain the fleet — it only bites when two things on the *same* box want the same workspace. + +### Where the cloud process gets its credentials + +`connect()` from `@relayfile/agents` resolves credentials in two steps: environment overrides first — `CLOUD_API_URL`, `CLOUD_API_ACCESS_TOKEN`, `CLOUD_WORKSPACE_ID` — then `~/.agentworkforce/relay/cloud-auth.json`, written by `agent-relay cloud login`. The env path is for a cloud runner; the file path is for a workstation. + +```ts +import { connect, tools } from "@relayfile/agents" + +const rf = await connect({ + agentName: "security-reviewer", + scopes: ["relayfile:fs:read:/github/**", "relayfile:fs:write:/runs/**"], +}) +``` + +### Or skip the mount entirely + +A cloud agent with no writable disk — a Lambda, a Worker, a short-lived job — doesn't need a mirror. The same workspace is reachable over HTTP with the same paths, using the `client` from step 1 (`workspace.client()`, or `rf.client` from `connect()`): + +```ts +const tree = await client.listTree(workspaceId, { path: "/github/repos/acme/api/pulls", depth: 2 }) +const pr = await client.readFile(workspaceId, "/github/repos/acme/api/pulls/42__bump-deps/meta.json") +``` + +You lose `grep` over the tree and gain a cold start measured in milliseconds. A useful split: mount in the specialist sandboxes that explore the PR, use the client in the small functions that read one record or post one result. + +## 6. Coordinate through the filesystem + +Keep the bot's own state on a path no adapter owns — `/runs/**` here. The write still persists and still raises an event, but no provider call is queued. You can see the difference in the write response: a path under a provider returns a real `opId` with `"state":"pending"`, while a path with no adapter behind it returns an empty `opId` and `"state":"succeeded"` immediately. + +```json +// PUT /runs/pr-59/findings/security.json +{"opId":"","status":"queued","targetRevision":"rev_…","writeback":{"provider":"runs","state":"succeeded"}} +``` + +The orchestrator finishes recon and publishes it as a file: + +```bash +cat > $MOUNT/runs/pr-59/recon.md <<'EOF' +# PR 59 — recon +Touched: src/auth/session.ts, src/auth/index.ts +Exported from: src/auth/index.ts (re-exported by src/server.ts) +Ticket: AR-100 +EOF +``` + +Every specialist reads that one file instead of re-deriving the map of the PR. Then each writes its own findings under its own path, so two specialists never contend for one file: + +```bash +echo '{"specialist":"security","findings":[]}' \ + > $MOUNT/runs/pr-59/findings/security.json +``` + +The orchestrator doesn't wait for the slowest specialist. It subscribes to the findings directory and publishes each result the moment it lands: + +```ts +import { readFile } from "node:fs/promises" +import { join } from "node:path" +import { onWrite } from "@relayfile/sdk" + +onWrite("/runs/pr-59/findings/**", async (event) => { + // the file is materialized before the event fires — just read it + const finding = JSON.parse(await readFile(join(mountDir, event.path), "utf8")) + await publishFinding(finding) // stream it out now, don't wait for the rest +}, { client, workspaceId, operations: ["create", "update"] }) +``` + +Delivery is at-least-once and `revision` is the per-file ordering key, so deduplicate on `eventId` and keep the handler idempotent. `event.path` is a workspace path; joining it with the mount root only works when the mount isn't scoped to a subtree with `--remote-path`. + +## 7. Post the review back to GitHub + +Writeback is a file write, but discover the contract first. Discovery documents live in their own `/discovery` tree with literal placeholder segments — **not** as siblings of the records: + +```bash +D="/discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}" +relayfile read review-bot /discovery/github/.adapter.md +relayfile read review-bot "$D/reviews/.schema.json" +relayfile read review-bot "$D/reviews/.create.example.json" +``` + +`.adapter.md` is the authority on which resources are writable, the ID pattern for each, and what a create draft becomes. For pull request reviews it gives the resource as `/github/repos/{owner}/{repo}/pulls/{pullNumber}/reviews/.json` with ID pattern `^\d+$`, and the review schema requires **`event`, `body`, and `comments`** — omitting `comments` fails validation. + +Then the three write rules: + +- **CREATE** — write a valid payload to a *non-canonical* filename in the resource directory. Any name that doesn't match the resource's ID pattern is a draft. +- **PATCH** — write only the mutable fields to the canonical `.json`. Fields marked `readOnly` in the schema are rejected. +- **DELETE** — remove the canonical file, where `.adapter.md` says delete is supported. + +```bash +cat > "$MOUNT/github/repos/AgentWorkforce/agentrelay.com/pulls/59/reviews/draft-security.json" <<'JSON' +{ "event": "COMMENT", "body": "2 findings from the security pass…", "comments": [] } +JSON +``` + + + **Write paths use the bare pull number, reads use `__`.** `pulls/59/reviews/draft-security.json` is the correct write target even though `pulls/59/meta.json` doesn't exist for reading. Don't derive one from the other. + + The same directory accepts `merge.json` and `close.json` as write resources — writing either one merges or closes the pull request. Keep a review bot's write grant scoped so a confused agent can't reach them. + + +The adapter rewrites your draft file into a receipt naming the real record: + +```json +{ "created": 5112118049, "externalId": "5112118049", "id": "5112118049", + "path": "/github/repos/AgentWorkforce/agentrelay.com/pulls/59/reviews/draft-security.json", + "url": "https://github.com/AgentWorkforce/agentrelay.com/pull/59#pullrequestreview-5112118049" } +``` + +Read the file back until `created` appears — that receipt, not the write's HTTP 200, is the proof the review exists. Editor scratch names (`partial.json`, `.tmp.json`, `*.tmp.json`, `*.partial.json`) are ignored and never become drafts. + +### Doing it over HTTP + +From a function with no mount, the same write is one request — and both headers are **required**, not decoration: + +```bash +curl -sS -X PUT \ + -H "Authorization: Bearer ${RELAYFILE_TOKEN}" \ + -H "Content-Type: application/json" \ + -H "X-Correlation-Id: review-bot-$(date +%s)" \ + -H "If-Match: *" \ + "${RELAYFILE_BASE_URL}/v1/workspaces/${RELAYFILE_WORKSPACE}/fs/file" \ + -d '{"path":"/github/repos/…/pulls/59/reviews/draft.json","content":"{…}","contentType":"application/json"}' +``` + +Omit `X-Correlation-Id` and you get `400 bad_request`; omit `If-Match` and you get `412 precondition_failed`. `If-Match: *` is create-or-overwrite; pass a revision to make the write conditional. + +### Verifying + +```bash +relayfile writeback status review-bot --json +``` + +This reports the **local mirror's** queue — `pending`, `failed`, `dead-lettered` — so it's the check for writes made through a mount. A write made over HTTP is a server-side op instead; verify that one by reading the draft back for its receipt. + +If an op dead-letters, its record under `$MOUNT/.relay/dead-letter/.json` carries `lastStatus` and a truncated `lastBody`; fix the cause, then `relayfile writeback retry --opId review-bot`. Denied paths are recorded in `$MOUNT/.relay/state.json` under `deniedPaths` (some versions also write `permissions-denied.log`). Never write anything under `.relay/` yourself. + +## 8. Trigger on a new PR without running a webhook server + +The bot doesn't need a GitHub App endpoint of its own. A provider webhook is already normalized into a file event on a canonical path, and the file is materialized *before* the event fires — so the handler starts with state on disk, not a payload to parse. + +From the CLI, `relayfile listen` streams the feed and runs a command per event: + +```bash +relayfile listen review-bot \ + --path "/github/repos/acme/api/pulls/**" \ + --event file.created \ + --run "review-bot start {{path}}" +``` + +Its full surface is `relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]`, and `--run` substitutes `{{path}}`, `{{type}}`, `{{provider}}`, `{{revision}}`, and `{{event}}` (the whole event — quote it). A delivered event carries `eventId`, `type`, `path`, `revision`, `provider`, `origin`, `correlationId`, and — for small files — the content inlined, so the handler usually needs no follow-up read: + +```json +{"type":"file.created","path":"/runs/pr-59/findings/security.json","revision":"rev_2935117", + "eventId":"evt_2507297","provider":"runs","origin":"agent_write","inlineContent":true,"content":"{}"} +``` + +`origin` is what keeps a bot from reacting to itself: `provider_sync` is a webhook from the provider, `agent_write` is another agent's write. + +Alternatives: subscribe in-process with the SDK (`connectWebSocket({ onEvent })`, or the glob-filtered `onWrite` from step 6), or fan events out to a channel with `relayfile integration bind --channel … --webhook … --webhook-token …`. + + + Supervise the subscriber. On a busy workspace the event stream drops within seconds — either mid-message or on a frame EOF — and reconnecting too fast earns a `429` on the WebSocket handshake. Run it with `--background` or under `relayfile supervisor install`, and back off between reconnects. A subscriber that reconnects with a cursor gets the events it missed. + + +Either way the reviewer wakes on provider state changing rather than on a request arriving — see [Events and webhooks](/docs/file/events). In the cloud, keep the subscriber in one long-lived orchestrator that spawns a sandbox per PR. + +## Tear down + +```bash +relayfile stop review-bot +relayfile integration disconnect github --workspace review-bot --yes +relayfile workspace delete review-bot --yes +rm -rf ./relayfile-mount +``` + + + `relayfile workspace join --name ` **renames an existing local entry** for that workspace id rather than adding a second one. If you're scripting workspace setup, check `relayfile workspace list` before and after. + + + + Copy-paste agent brief — the same contract written for the agent itself, ready to drop in as `AGENTS.md`. + + + + + Every option on `mountWorkspace` and `ensureMountedWorkspace`. + + + Scope each specialist to the paths it should read and write. + + + Why the orchestrator sees a specialist's write on the next read. + + + The copy-paste operating document to hand the agent. + + diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index e72a35f..79ad751 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -37,7 +37,7 @@ export const fileSection: ProductDocSection = { label: 'Relayfile', tagline: 'The event layer for AI agents.', repo: 'AgentWorkforce/relayfile', - version: '0.10.31', + version: '0.10.53', nav: [ { title: 'Start', @@ -47,6 +47,13 @@ export const fileSection: ProductDocSection = { { title: 'Why files', slug: 'why-files' }, ], }, + { + title: 'Guides', + items: [ + { title: 'Build a PR review bot', slug: 'review-bot' }, + { title: 'Review bot agent brief', slug: 'review-bot-brief' }, + ], + }, { title: 'Concepts', items: [ diff --git a/web/lib/test/product-docs.test.ts b/web/lib/test/product-docs.test.ts index ee7a4cf..5502b09 100644 --- a/web/lib/test/product-docs.test.ts +++ b/web/lib/test/product-docs.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { factorySection, getProductSearchIndex } from '../product-docs'; +import { factorySection, fileSection, getProductSearchIndex } from '../product-docs'; describe('Factory product docs', () => { it('publishes issue routing in navigation and scoped search', () => { @@ -22,3 +22,37 @@ describe('Factory product docs', () => { expect(searchEntry?.body).toContain('safety.requireLabel'); }); }); + +describe('Relayfile product docs', () => { + it('publishes both review-bot guides in a Guides group', () => { + const guides = fileSection.nav.find((group) => group.title === 'Guides'); + + expect(guides?.items).toEqual([ + { title: 'Build a PR review bot', slug: 'review-bot' }, + { title: 'Review bot agent brief', slug: 'review-bot-brief' }, + ]); + }); + + it('indexes the review-bot guide, including the cloud path', () => { + const searchEntry = getProductSearchIndex(fileSection).find( + (entry) => entry.slug === 'review-bot' + ); + + expect(searchEntry).toMatchObject({ title: 'Build a PR review bot' }); + expect(searchEntry?.headings).toContain('5. Give every sandbox the same workspace'); + expect(searchEntry?.headings).toContain('Where it runs'); + expect(searchEntry?.headings).toContain('Provisioning from a machine with no browser'); + expect(searchEntry?.headings).toContain('Where the cloud process gets its credentials'); + expect(searchEntry?.body).toContain('A PR review bot'); + }); + + it('indexes the copy-paste agent brief', () => { + const searchEntry = getProductSearchIndex(fileSection).find( + (entry) => entry.slug === 'review-bot-brief' + ); + + expect(searchEntry).toMatchObject({ title: 'Review bot agent brief' }); + expect(searchEntry?.headings).toContain('The run protocol'); + expect(searchEntry?.headings).toContain('Publish the review'); + }); +});