From d65adeae42d44647f86bb9e9b11e34e1d839df41 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 10:34:56 +0200 Subject: [PATCH 1/6] docs(relayfile): add a Guides section with an end-to-end PR review bot walkthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Relayfile docs explained each primitive well but never walked one build end to end, so the question "how do I actually wire this into my bot?" had no single page to land on. This adds a Guides group to the Relayfile sidebar and its first guide: a PR review bot, start to finish. The guide follows one build through the whole flow — `relayfile setup` for GitHub, verifying the mount and reading LAYOUT.md/.layout.md/_index.json instead of hard-coding paths, adding Linear/Notion/Slack via the live integration catalog, mounting the same workspace in every specialist sandbox with ensureMountedWorkspace and path-scoped tokens, coordinating orchestrator and specialists through shared files (recon in, findings out, streamed as each lands rather than waiting for the slowest), posting the review back by writing a discovered-schema JSON file, and triggering on new PRs with `relayfile listen` instead of running a webhook server. Verification and the dead-letter recovery path are part of the walkthrough, not an appendix. Quickstart gets a banner link to it so the guide is reachable from the page people land on first. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/quickstart.mdx | 4 + web/content/docs/file/review-bot.mdx | 273 +++++++++++++++++++++++++++ web/lib/product-docs-nav.ts | 4 + web/lib/test/product-docs.test.ts | 18 +- 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 web/content/docs/file/review-bot.mdx 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/review-bot.mdx b/web/content/docs/file/review-bot.mdx new file mode 100644 index 0000000..ae626ba --- /dev/null +++ b/web/content/docs/file/review-bot.mdx @@ -0,0 +1,273 @@ +--- +title: 'Build a PR review bot' +description: 'End-to-end: connect GitHub with one command, add the rest of your context providers, mount the same workspace in every reviewer sandbox, 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. + +## 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 mounts 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, in sub-second time. +- **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. + +## 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. Open the two URLs it prints, complete them, and the command blocks until GitHub reports ready. + + + The Nango connect URL has a short TTL. If you finish the OAuth after the command has already exited, re-run the same `relayfile setup` line — a re-run reuses the workspace and only opens a new connect flow when the provider isn't connected yet. + + +## 2. Verify the mount before you write any bot code + +```bash +relayfile status review-bot +``` + +Read three fields and move on: + +- **`lag`** — how stale the mirror is. Under a few seconds is healthy; over 60s means investigate before blaming your agent. +- **`daemon`** — if it says `not running`, restart it with `relayfile mount review-bot ./relayfile-mount --background`. +- **`dead-lettered`** — gate on this one. `failed` is a lifetime counter and is informational; dead-lettered means writes gave up. + +Then let the mount tell you its own shape rather than hard-coding paths from this page: + +```bash +cat ./relayfile-mount/LAYOUT.md +cat ./relayfile-mount/github/.layout.md +cat ./relayfile-mount/github/repos/_index.json +``` + +`LAYOUT.md`, per-provider `.layout.md`, and `_index.json` are the contract — see [Mount layout](/docs/file/mount-layout). An agent that `cat`s them at startup needs no path knowledge in its prompt. + +## 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 engineering standards it's supposed to follow. Don't guess provider ids — ask the live catalog, which spans both backends: + +```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 mount: + +```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 --json +``` + +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 are the one provider pair that needs 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. + + +The bot's context is now four providers wide and still one interface. That's the property worth designing around: adding `/notion` cost the reviewer agent zero new tool schemas, because `ls`/`cat`/`grep` already worked. + +## 4. Read the PR + +Reads are reads. Enumerate the PRs, open one, and pull the surrounding context from other providers with the same handful of verbs: + +```bash +MOUNT=./relayfile-mount + +# what's open +cat $MOUNT/github/repos/acme/api/pulls/_index.json + +# the PR under review +cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/meta.json + +# the ticket it references, by id +cat $MOUNT/linear/issues/by-id/AGE-12.json + +# the standards the reviewer should enforce +grep -rl 'review checklist' $MOUNT/notion/pages/ +``` + +`grep` here is exhaustive over synced records, not a paginated provider search — that difference is most of why a file-shaped reviewer beats an API-shaped one on recall. The `__` naming means the reviewer can recover `42` from the directory name and write back to the canonical path later without a lookup. + + + Path shapes are adapter-owned and versioned with the adapter, so treat `.layout.md` and `_index.json` in *your* mount as authoritative over the examples above. + + +## 5. Mount the same workspace in every sandbox + +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" without a second round-trip. +- **Scope each specialist down.** The minted token's scopes are a subset of the caller's grant, so the security reviewer gets `read:/github/**` and `write:/runs/**` and nothing else. 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`. Spread it into the child process; the sandbox never sees your Cloud access token or the provider credentials. + +Use `handle.status()` for a readiness snapshot and `handle.stop()` on teardown — it drains in-flight syncs and never deletes the local directory. + +## 6. Coordinate through the filesystem + +Keep the bot's own state on a path no adapter owns — `/runs/**` here. Writeback is adapter-driven, so a path outside every provider subtree stays inside the workspace instead of queueing a provider call: a durable, shared scratch space with the same sub-second visibility as the provider trees. `relayfile writeback status` is the check — writes under `/runs/**` shouldn't add pending ops. + +The orchestrator finishes recon and publishes it as a file: + +```bash +cat > $MOUNT/runs/pr-42/recon.md <<'EOF' +# PR 42 — recon +Touched: src/auth/session.ts, src/auth/index.ts +Exported from: src/auth/index.ts (re-exported by src/server.ts) +Ticket: AGE-12 (Fix login bug) +EOF +``` + +Every specialist reads that one file instead of re-deriving the map of the PR — the recon cost is paid once and shared. 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-42/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-42/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. A specialist that crashes and restarts re-reads `recon.md` and resumes; nothing about the run lived only in a dead process's memory. + +## 7. Post the review back to GitHub + +Writeback is a file write, but discover the contract first instead of guessing a shape: + +```bash +cat $MOUNT/github/.adapter.md +cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/reviews/.schema.json +cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/reviews/.create.example.json +``` + +The schema is JSON Schema draft 2020-12 for the full synced record; fields marked `"readOnly": true` are server-managed and must not be written. Then the three rules from [Reads and writes](/docs/file/reads-and-writes) apply: + +- **CREATE** — write a valid payload to any *non-canonical* filename in the resource directory. The adapter creates the real record and rewrites your draft file as a receipt pointing at the canonical path. +- **PATCH** — write only the mutable fields to the canonical `.json`. Omitted fields are left alone. +- **DELETE** — remove the canonical file, when the resource's `.adapter.md` says delete is supported. + +```bash +cat > $MOUNT/github/repos/acme/api/pulls/42__bump-deps/reviews/draft-security.json <<'EOF' +{ "event": "COMMENT", "body": "2 findings from the security pass…" } +EOF +``` + +From the SDK, the same write carries optimistic concurrency — pass the revision you read, catch `RevisionConflictError`, re-read, retry: + +```ts +await client.writeFile({ + workspaceId, + path: "/github/repos/acme/api/pulls/42__bump-deps/reviews/draft-security.json", + baseRevision: "*", + content: JSON.stringify(review), + contentType: "application/json", +}) +``` + +Confirm it landed: + +```bash +relayfile writeback status review-bot --json | jq +``` + +Pending writebacks should drain to zero within a sync cycle, and the command exits non-zero only when there are dead-lettered ops — so gate CI on the exit code, not on the lifetime `failed` counter. If `deadLettered` is non-empty, each record under `$MOUNT/.relay/dead-letter/.json` carries `lastStatus` and a truncated `lastBody` — fix the payload, then `relayfile writeback retry --opId review-bot`. Read denials are preserved separately in `$MOUNT/.relay/permissions-denied.log`, so an ACL mistake surfaces as a logged denial rather than a silent no-op. 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: + +```bash +relayfile listen \ + --path "/github/repos/acme/api/pulls/**" \ + --event file.created \ + --run "review-bot start {{path}}" +``` + +Or in-process with `onWrite`, filtered by the same glob. Either way the reviewer wakes on provider state changing rather than on a request arriving — see [Events and webhooks](/docs/file/events). + +## 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 +``` + + + + 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. + + + Every flag used in this guide. + + diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index e72a35f..ca52847 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -47,6 +47,10 @@ export const fileSection: ProductDocSection = { { title: 'Why files', slug: 'why-files' }, ], }, + { + title: 'Guides', + items: [{ title: 'Build a PR review bot', slug: 'review-bot' }], + }, { title: 'Concepts', items: [ diff --git a/web/lib/test/product-docs.test.ts b/web/lib/test/product-docs.test.ts index ee7a4cf..c4bad0c 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,19 @@ describe('Factory product docs', () => { expect(searchEntry?.body).toContain('safety.requireLabel'); }); }); + +describe('Relayfile product docs', () => { + it('publishes the review-bot guide in a Guides group and scoped search', () => { + const guides = fileSection.nav.find((group) => group.title === 'Guides'); + + expect(guides?.items).toEqual([{ title: 'Build a PR review bot', slug: 'review-bot' }]); + + const searchEntry = getProductSearchIndex(fileSection).find( + (entry) => entry.slug === 'review-bot' + ); + + expect(searchEntry).toMatchObject({ title: 'Build a PR review bot' }); + expect(searchEntry?.headings).toContain('5. Mount the same workspace in every sandbox'); + expect(searchEntry?.body).toContain('A PR review bot'); + }); +}); From 5f15280e226c2b764a54fbf03422dbe4f2c2b5c4 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 12:05:42 +0200 Subject: [PATCH 2/6] docs(relayfile): cover the cloud path and add a copy-paste agent brief MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review bot guide only described a workstation: `relayfile setup` with a browser, a long-lived local mount, a local fleet. That leaves the more common production shape undocumented — a bot provisioned once by CI and run in cloud sandboxes per PR. - A "Where it runs" table up front splits workstation / cloud sandboxes / serverless and states the rule the rest of the guide leans on: provisioning happens once, mounting happens per run. - Headless provisioning with RELAYFILE_CLOUD_TOKEN and --skip-mount, plus the SDK equivalent via RelayfileSetup.fromCloudTokens for a control plane that already holds Cloud tokens, with the rw_ id warning that goes with it. - Inspecting a workspace with no mount at all (`relayfile tree` / `read`), the credential resolution order a cloud runner actually uses (CLOUD_API_* env before ~/.agentworkforce/relay/cloud-auth.json), and the no-mount RelayFileClient path for functions with no writable disk. - Cursor-based catch-up noted where the event listener belongs in the cloud, so a deploy mid-afternoon doesn't drop PRs. Adds a second guide, "Review bot agent brief": a complete operating document addressed to the agent rather than the reader — environment, orient-first rules, ACL boundaries, the /runs run protocol and findings shape, discovery-first writeback, writeback verification, and a troubleshooting table. The page body *is* the document, so the existing markdown mirror makes it copy-paste ready: "Copy page as markdown", or curl /docs/file/markdown/review-bot-brief.md straight into AGENTS.md. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/review-bot-brief.mdx | 173 +++++++++++++++++++++ web/content/docs/file/review-bot.mdx | 98 +++++++++++- web/lib/product-docs-nav.ts | 5 +- web/lib/test/product-docs.test.ts | 22 ++- 4 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 web/content/docs/file/review-bot-brief.mdx 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..4f89604 --- /dev/null +++ b/web/content/docs/file/review-bot-brief.mdx @@ -0,0 +1,173 @@ +--- +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 as-is.' +--- + + + **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`, then substitute the three placeholders — `/`, the specialist name, and the run id — and hand it over unchanged. It pairs with [Build a PR review bot](/docs/file/review-bot), which is the human-facing version of the same flow. + + +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 mounted 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. Every path in this document is relative to it. | +| `RELAYFILE_WORKSPACE` | The `rw_…` workspace id. Use it verbatim for any SDK call; never substitute another id. | +| `RELAYFILE_TOKEN` | Your scoped workspace token. It is already narrowed to the paths you're allowed to touch. | +| `RELAYFILE_BASE_URL` | The API base, for SDK calls when you don't want to go through the mirror. | + +The mount is a real directory. `ls`, `cat`, `grep`, `find`, and ordinary file writes all work. Provider records are JSON or Markdown files that are already up to date — the file is materialized before you're told about it, so there is never a reason to fetch a record over an 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 from memory: + +```bash +cat "$RELAYFILE_LOCAL_DIR/LAYOUT.md" # top-level guide +cat "$RELAYFILE_LOCAL_DIR/github/.layout.md" # this provider's tree shape +cat "$RELAYFILE_LOCAL_DIR/github/repos/_index.json" # what's actually here +``` + +`_index.json` exists in every directory and lists that directory's rows. Read it instead of walking entries one at a time. + +Entity filenames are `__` and the id is the **last** `__`-separated segment, so you can always recover an identifier from a filename. Alias views let you look a record up by whichever key you have: `by-id/`, `by-title/`, `by-name/`, `by-state/`. + +## 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/**` (your scratch and output space) and, if you are the publishing agent, the specific provider resource named in *Publish the review* below. +- **Never write anything under `.relay/`.** It is daemon state; files you put there are ignored or corrupt the mount's bookkeeping. + +A write outside your grant is rejected, not silently dropped, and the denial is recorded in `.relay/permissions-denied.log`. If a write fails that way, stop and report it — do not try to route around it. + +## Read the pull request + +```bash +MOUNT="$RELAYFILE_LOCAL_DIR" +REPO="$MOUNT/github/repos//" + +cat "$REPO/pulls/_index.json" # open PRs +cat "$REPO/pulls//meta.json" # the PR under review +``` + +Then pull the context a good review needs, when those providers are mounted: + +```bash +cat "$MOUNT/linear/issues/by-id/.json" # why the change exists +grep -rl 'review checklist' "$MOUNT/notion/pages/" # standards you must enforce +``` + +`grep` here 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 +``` + +- **Read `recon.md` first.** It maps the PR — files touched, what they export, who imports them, the ticket it belongs to. It was produced once so that every specialist doesn't re-derive it. If it's missing, wait for it rather than duplicating the work. +- **Write exactly one findings file**, named for your specialty. Two agents writing one path is the only way to lose work here; one file each makes that 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 for a findings file: + +```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 before writing — never invent a payload shape: + +```bash +cat "$MOUNT/github/.adapter.md" # operations + id patterns +cat "$REPO/pulls//reviews/.schema.json" # full record schema +cat "$REPO/pulls//reviews/.create.example.json" # minimal create payload +``` + +`.schema.json` is JSON Schema draft 2020-12 for the synced record. Fields marked `"readOnly": true` are server-managed — do not write them. Then: + +- **Create** a record by writing a valid payload to a *non-canonical* filename in the resource directory (`draft-security.json`). The adapter creates the real record and rewrites your draft as a receipt pointing at the canonical path. +- **Patch** an existing record by writing only the mutable fields to its canonical `.json`. Omitted fields are left alone. +- **Delete** by removing the canonical file — only where `.adapter.md` says delete is supported. + +```bash +cat > "$REPO/pulls//reviews/draft-review.json" <<'JSON' +{ "event": "COMMENT", "body": "…" } +JSON +``` + +## Verify the write landed + +A queued write is not a delivered write. Check before you report success: + +```bash +relayfile writeback status "$RELAYFILE_WORKSPACE" --json +``` + +Pending ops should drain within a sync cycle. The command exits non-zero when there are dead-lettered ops — that is the signal to act on. The lifetime `failed` counter is informational; do not treat it as a failure. + +If an op dead-lettered, its record explains why: + +```bash +cat "$MOUNT/.relay/dead-letter/"*.json +relayfile writeback retry --opId "$RELAYFILE_WORKSPACE" +``` + +Fix the cause first — usually a payload that doesn't match the schema, or a record that changed upstream — then retry. Do not retry unchanged, repeatedly. + +## When something looks wrong + +| Symptom | What it means | Do this | +|---|---|---| +| A path from this document doesn't exist | Adapter layout differs from the example | Re-read `.layout.md` and `_index.json`; use what the mount says | +| The tree is empty or missing a provider | Mount raced the first sync, or that provider isn't connected | Report it; don't proceed on partial data | +| A write is rejected | Outside your ACL grant | Stop, report the path; check `.relay/permissions-denied.log` | +| A write succeeds but nothing appears upstream | Writeback queued, failed, or dead-lettered | `relayfile writeback status`, then the dead-letter record | +| A file you read looks stale | You cached it yourself | Re-read from the mount; the mirror invalidates on write | + +## Rules + +1. Orient from `LAYOUT.md` and `_index.json` before touching anything. +2. Never invent a path or a payload shape — discover both in-tree. +3. Write only your own findings file; never edit another agent's. +4. Never write under `.relay/`. +5. Read `recon.md` instead of re-deriving the PR map. +6. Record the commit you reviewed in your output. +7. Verify writeback before reporting success. +8. 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 index ae626ba..ae76959 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -27,6 +27,19 @@ The three things this guide buys you: - **Context across sandboxes.** Specialists in separate sandboxes see each other's work through the filesystem, in sub-second time. - **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. Pick a lane before step 1; the steps below are annotated for both. + +| | 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 mount | long-lived `relayfile mount --background` | `ensureMountedWorkspace` per sandbox ([step 5](#5-mount-the-same-workspace-in-every-sandbox)) | `relayfile mount --once`, or no mount at all | +| 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 | + +The important part: **provisioning happens once, mounting happens per run.** A cloud bot doesn't re-do OAuth on every PR — it joins a workspace that's already connected and mounts it in seconds. + ## 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: @@ -45,6 +58,45 @@ relayfile setup \ The Nango connect URL has a short TTL. If you finish the OAuth after the command has already exited, re-run the same `relayfile setup` 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 +``` + +`--skip-mount` returns as soon as the workspace exists and the provider reports ready, which is exactly the boundary between provisioning and running. The provider OAuth itself still needs a human the first time — print the connect URL, have someone complete it once, and every later run just joins. + +The same thing 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 { connectLink } = await workspace.connectIntegration("github") +if (connectLink) { + await notifyOperator(connectLink) // one-time human step + await workspace.waitForConnection("github") +} +``` + +`fromCloudTokens` refreshes inside the refresh window, so a long-lived control plane doesn't have to re-auth between runs. Store the returned `rw_…` id — that's what every sandbox joins later. + + + Use the `rw_…` workspace id from `joinWorkspace` (or from `relayfile setup`) 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 the mount before you write any bot code ```bash @@ -67,6 +119,15 @@ cat ./relayfile-mount/github/repos/_index.json `LAYOUT.md`, per-provider `.layout.md`, and `_index.json` are the contract — see [Mount layout](/docs/file/mount-layout). An agent that `cat`s them at startup needs no path knowledge in its prompt. +Without a mount — from CI, or from a laptop checking on the cloud workspace — `relayfile tree` and `relayfile read` answer the same questions against the server: + +```bash +relayfile tree review-bot /github --depth 2 +relayfile read review-bot /LAYOUT.md +``` + +Both resolve a token from `--token`, then `RELAYFILE_TOKEN`, then your relay session, so a CI job needs one secret and no login step. + ## 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 engineering standards it's supposed to follow. Don't guess provider ids — ask the live catalog, which spans both backends: @@ -155,6 +216,33 @@ Three things to keep straight when you fan this out: Use `handle.status()` for a readiness snapshot and `handle.stop()` on teardown — it drains in-flight syncs and never deletes the local directory. +### Where the cloud process gets its credentials + +`connect()` from `@relayfile/agents` runs the same bootstrap and 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 the one to use in a cloud runner; the file path is the one to use on a workstation. + +```ts +import { connect, tools } from "@relayfile/agents" + +// CLOUD_API_ACCESS_TOKEN + CLOUD_WORKSPACE_ID come from the runner's secrets +const rf = await connect({ + agentName: "security-reviewer", + scopes: ["relayfile:fs:read:/github/**", "relayfile:fs:write:/runs/**"], +}) +``` + +One login per machine; the per-agent scoping is what you vary between specialists. + +### Or skip the mount entirely + +A cloud agent with no writable disk — a Lambda, a Worker, a short-lived job — doesn't need the mirror. The same workspace is reachable over HTTP with the same paths: + +```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 for a review bot: mount in the specialist sandboxes that actually explore the PR, use the client directly in the small functions that only read one record or post one result. Both see the same writes. + ## 6. Coordinate through the filesystem Keep the bot's own state on a path no adapter owns — `/runs/**` here. Writeback is adapter-driven, so a path outside every provider subtree stays inside the workspace instead of queueing a provider call: a durable, shared scratch space with the same sub-second visibility as the provider trees. `relayfile writeback status` is the check — writes under `/runs/**` shouldn't add pending ops. @@ -248,6 +336,12 @@ relayfile listen \ Or in-process with `onWrite`, filtered by the same glob. 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 this belongs in one long-lived orchestrator process — `relayfile listen` under your supervisor, or `connectWebSocket` inside the service — which then spawns a sandbox per PR. A subscriber that reconnects with a cursor gets the events it missed while it was down, so a deploy in the middle of a busy afternoon doesn't drop PRs on the floor. + + + Copy-paste agent brief — the same contract written for the agent itself, ready to drop in as `AGENTS.md`. + + ## Tear down ```bash @@ -267,7 +361,7 @@ rm -rf ./relayfile-mount Why the orchestrator sees a specialist's write on the next read. - - Every flag used in this guide. + + 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 ca52847..93c7092 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -49,7 +49,10 @@ export const fileSection: ProductDocSection = { }, { title: 'Guides', - items: [{ title: 'Build a PR review bot', slug: 'review-bot' }], + items: [ + { title: 'Build a PR review bot', slug: 'review-bot' }, + { title: 'Review bot agent brief', slug: 'review-bot-brief' }, + ], }, { title: 'Concepts', diff --git a/web/lib/test/product-docs.test.ts b/web/lib/test/product-docs.test.ts index c4bad0c..466c892 100644 --- a/web/lib/test/product-docs.test.ts +++ b/web/lib/test/product-docs.test.ts @@ -24,17 +24,35 @@ describe('Factory product docs', () => { }); describe('Relayfile product docs', () => { - it('publishes the review-bot guide in a Guides group and scoped search', () => { + 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' }]); + 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. Mount the same workspace in every sandbox'); + 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'); + }); }); From cb5ef2d7d1128248eda41db3dea8aca548e278de Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 13:08:47 +0200 Subject: [PATCH 3/6] docs(relayfile): correct the guide and its sources against a live workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ran the review-bot guide end to end against a live Cloud workspace on relayfile 0.10.41, including a real writeback: the review at agentrelay.com#59 (pullrequestreview-5112118049) was posted by writing the JSON draft the guide tells you to write. Several things the guide said — inherited from the docs it was built on — turned out to be wrong. Commands that do not exist in 0.10.41: - `relayfile listen` (invented; also in events.mdx and introduction.mdx). Replaced with `integration bind` for channel fan-out and the SDK's onWrite. - `relayfile permissions` (cli.mdx). Removed, with a pointer to .adapter.md and .relay/state.json deniedPaths instead. - `seed --dry-run / --exclude / --batch-size` (cli.mdx). All rejected as undefined flags. Also documented that `seed` and `ops list` resolve credentials only from ~/.relayfile/credentials.json, not a Cloud session. Paths that 404 against a live mount: - The provider layout file is `/LAYOUT.md`, not `.layout.md`. - Schemas and adapter contracts live under `/discovery//…` with literal {owner}/{repo}/{pullNumber} segments, not beside the records. Contract corrections: - GitHub names records `__` (number first); the "id is the last __ segment" rule is Linear's, not universal. Reads need the full directory name, while the review write path takes the bare pull number. - Records are envelopes: the provider object is under `.payload`. - A review payload requires event, body AND comments. - PUT /fs/file requires If-Match as well as X-Correlation-Id (412 without it). - `relayfile status` reports pending/conflicts/denied for the local mirror; dead-letter counts come from `writeback status`. Denials land in .relay/state.json deniedPaths. - One registered mirror per workspace per machine; a second mount is refused rather than added, and `workspace join --name` renames an existing entry. Also addresses the PR review: the guide/cli.mdx command mismatch is resolved in cli.mdx's favour of the real binary, the brief now says every angle-bracket placeholder must be filled, and the no-mount snippet names where `client` comes from. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/api-reference.mdx | 3 +- web/content/docs/file/cli.mdx | 20 +- web/content/docs/file/events.mdx | 10 +- web/content/docs/file/introduction.mdx | 11 +- web/content/docs/file/mount-layout.mdx | 12 +- web/content/docs/file/reads-and-writes.mdx | 8 +- web/content/docs/file/review-bot-brief.mdx | 138 ++++++----- web/content/docs/file/review-bot.mdx | 266 +++++++++++++-------- web/lib/test/product-docs.test.ts | 2 +- 9 files changed, 280 insertions(+), 190 deletions(-) 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..1f4f03d 100644 --- a/web/content/docs/file/cli.mdx +++ b/web/content/docs/file/cli.mdx @@ -62,14 +62,12 @@ Bulk-upload a local directory into a workspace, respecting `.gitignore` and `--e 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. @@ -138,11 +136,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 | + + 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 | Flag | Env var | Description | diff --git a/web/content/docs/file/events.mdx b/web/content/docs/file/events.mdx index 0ae2bd4..d5eeac6 100644 --- a/web/content/docs/file/events.mdx +++ b/web/content/docs/file/events.mdx @@ -40,16 +40,14 @@ Every change — webhook, sync, or another agent's write — surfaces as the sam ## Consuming events -Filter by path glob and event type so an agent only wakes for what it cares about. From the CLI: +Filter by path glob and event type so an agent only wakes for what it cares about. To fan events out to a channel, the CLI binds a path glob to a webhook: ```bash -relayfile listen \ - --path "/linear/issues/by-state/triage/**" \ - --event file.created \ - --run "claude --print 'Triage this: {{path}}'" +relayfile integration bind linear "/linear/issues/by-state/triage/**" \ + --channel triage --webhook "$WEBHOOK_ID" --webhook-token "$WEBHOOK_TOKEN" ``` -Or from the SDK with `onWrite`, which subscribes over the same WebSocket stream and dispatches by pattern: +To react in your own process, use the SDK's `onWrite`, which subscribes over the same WebSocket stream and dispatches by pattern: ```typescript import { onWrite } from '@relayfile/sdk'; diff --git a/web/content/docs/file/introduction.mdx b/web/content/docs/file/introduction.mdx index 7d50dcc..9819961 100644 --- a/web/content/docs/file/introduction.mdx +++ b/web/content/docs/file/introduction.mdx @@ -14,11 +14,12 @@ What if you could do this with just a few lines of code and your agent wakes wit Relayfile materializes your full provider state as a file tree and keeps it current with webhooks. When an event fires, `/linear/issues/ENG-123.json` is already there. `/linear/issues/by-state/triage/` is already there. No API calls needed. -```bash -relayfile listen \ - --path "/linear/issues/by-state/triage/**" \ - --event file.created \ - --run "claude --print 'Triage this: {{path}}'" +```ts +import { onWrite } from '@relayfile/sdk'; + +onWrite('/linear/issues/by-state/triage/**', async (event) => { + await agent.run(`Triage this: ${event.path}`); +}, { client, workspaceId, operations: ['create'] }); ``` diff --git a/web/content/docs/file/mount-layout.mdx b/web/content/docs/file/mount-layout.mdx index 6264e71..3b03658 100644 --- a/web/content/docs/file/mount-layout.mdx +++ b/web/content/docs/file/mount-layout.mdx @@ -3,16 +3,16 @@ title: 'Mount layout' description: 'Every Relayfile mount is self-describing: LAYOUT.md, per-integration layout files, _index.json, canonical naming, and four 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: __ @@ -33,12 +33,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,7 +48,7 @@ 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 diff --git a/web/content/docs/file/reads-and-writes.mdx b/web/content/docs/file/reads-and-writes.mdx index 147d8ac..3a08aa9 100644 --- a/web/content/docs/file/reads-and-writes.mdx +++ b/web/content/docs/file/reads-and-writes.mdx @@ -56,13 +56,15 @@ 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/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 index 4f89604..5e2f6fd 100644 --- a/web/content/docs/file/review-bot-brief.mdx +++ b/web/content/docs/file/review-bot-brief.mdx @@ -1,65 +1,79 @@ --- 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 as-is.' +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`, then substitute the three placeholders — `/`, the specialist name, and the run id — and hand it over unchanged. It pairs with [Build a PR review bot](/docs/file/review-bot), which is the human-facing version of the same flow. + **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.41; 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 mounted 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. +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. Every path in this document is relative to it. | -| `RELAYFILE_WORKSPACE` | The `rw_…` workspace id. Use it verbatim for any SDK call; never substitute another id. | -| `RELAYFILE_TOKEN` | Your scoped workspace token. It is already narrowed to the paths you're allowed to touch. | -| `RELAYFILE_BASE_URL` | The API base, for SDK calls when you don't want to go through the mirror. | +| `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. | -The mount is a real directory. `ls`, `cat`, `grep`, `find`, and ordinary file writes all work. Provider records are JSON or Markdown files that are already up to date — the file is materialized before you're told about it, so there is never a reason to fetch a record over an API to "get the current version." +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 from memory: +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 -cat "$RELAYFILE_LOCAL_DIR/LAYOUT.md" # top-level guide -cat "$RELAYFILE_LOCAL_DIR/github/.layout.md" # this provider's tree shape -cat "$RELAYFILE_LOCAL_DIR/github/repos/_index.json" # what's actually here +jq '.[] | select(.state=="open") | {number, title, headRef}' \ + "$RELAYFILE_LOCAL_DIR/github/repos///pulls/_index.json" ``` -`_index.json` exists in every directory and lists that directory's rows. Read it instead of walking entries one at a time. +Four naming rules that will otherwise cost you a run: -Entity filenames are `__` and the id is the **last** `__`-separated segment, so you can always recover an identifier from a filename. Alias views let you look a record up by whichever key you have: `by-id/`, `by-title/`, `by-name/`, `by-state/`. +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 (`by-id/`, `by-title/`, `by-state/`, `by-creator/`, `by-edited/`) live in a flat sibling namespace, `/github/repos/__/pulls/…`, not under the canonical repo path. Linear's are at `/linear/issues/by-id/.json`. ## 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/**` (your scratch and output space) and, if you are the publishing agent, the specific provider resource named in *Publish the review* below. -- **Never write anything under `.relay/`.** It is daemon state; files you put there are ignored or corrupt the mount's bookkeeping. +- **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 denial is recorded in `.relay/permissions-denied.log`. If a write fails that way, stop and report it — do not try to route around it. +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" -REPO="$MOUNT/github/repos//" +R="$MOUNT/github/repos//" -cat "$REPO/pulls/_index.json" # open PRs -cat "$REPO/pulls//meta.json" # the PR under review +jq '.[] | {number, state, title}' "$R/pulls/_index.json" # pick the PR +cat "$R/pulls//meta.json" | jq '.payload | {number, title, state}' ``` -Then pull the context a good review needs, when those providers are mounted: +`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" # why the change exists -grep -rl 'review checklist' "$MOUNT/notion/pages/" # standards you must enforce +cat "$MOUNT/linear/issues/by-id/.json" | jq '.payload.title' +grep -rl 'review checklist' "$MOUNT/notion/pages/" ``` -`grep` here searches every synced record, not a paginated API result. Prefer it over guessing which file holds something. +`grep` searches every synced record, not a paginated API result. Prefer it over guessing which file holds something. ## The run protocol @@ -75,17 +89,19 @@ One directory per review run. You are one writer among several, so stay inside y └── review.json # the merged review, written by the publishing agent ``` -- **Read `recon.md` first.** It maps the PR — files touched, what they export, who imports them, the ticket it belongs to. It was produced once so that every specialist doesn't re-derive it. If it's missing, wait for it rather than duplicating the work. -- **Write exactly one findings file**, named for your specialty. Two agents writing one path is the only way to lose work here; one file each makes that impossible. +`/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 for a findings file: +Use this shape: ```json { "specialist": "security", - "commit": "", - "model": "", + "commit": "", + "model": "", "findings": [ { "path": "src/auth/session.ts", @@ -103,65 +119,69 @@ Record the commit you actually reviewed. A review attached to the wrong commit i ## Publish the review -Only the publishing agent does this. Discover the contract before writing — never invent a payload shape: +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/github/.adapter.md" # operations + id patterns -cat "$REPO/pulls//reviews/.schema.json" # full record schema -cat "$REPO/pulls//reviews/.create.example.json" # minimal create payload +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" ``` -`.schema.json` is JSON Schema draft 2020-12 for the synced record. Fields marked `"readOnly": true` are server-managed — do not write them. Then: +`.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. -- **Create** a record by writing a valid payload to a *non-canonical* filename in the resource directory (`draft-security.json`). The adapter creates the real record and rewrites your draft as a receipt pointing at the canonical path. -- **Patch** an existing record by writing only the mutable fields to its canonical `.json`. Omitted fields are left alone. -- **Delete** by removing the canonical file — only where `.adapter.md` says delete is supported. +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 > "$REPO/pulls//reviews/draft-review.json" <<'JSON' -{ "event": "COMMENT", "body": "…" } +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. Check before you report success: +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 -relayfile writeback status "$RELAYFILE_WORKSPACE" --json +cat "$R/pulls//reviews/draft-.json" +# { "created": 5112118049, "id": "5112118049", "url": "https://github.com/…#pullrequestreview-5112118049" } ``` -Pending ops should drain within a sync cycle. The command exits non-zero when there are dead-lettered ops — that is the signal to act on. The lifetime `failed` counter is informational; do not treat it as a failure. - -If an op dead-lettered, its record explains why: +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 -cat "$MOUNT/.relay/dead-letter/"*.json -relayfile writeback retry --opId "$RELAYFILE_WORKSPACE" +relayfile writeback status "$RELAYFILE_WORKSPACE" --json ``` -Fix the cause first — usually a payload that doesn't match the schema, or a record that changed upstream — then retry. Do not retry unchanged, repeatedly. +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 | |---|---|---| -| A path from this document doesn't exist | Adapter layout differs from the example | Re-read `.layout.md` and `_index.json`; use what the mount says | -| The tree is empty or missing a provider | Mount raced the first sync, or that provider isn't connected | Report it; don't proceed on partial data | -| A write is rejected | Outside your ACL grant | Stop, report the path; check `.relay/permissions-denied.log` | -| A write succeeds but nothing appears upstream | Writeback queued, failed, or dead-lettered | `relayfile writeback status`, then the dead-letter record | -| A file you read looks stale | You cached it yourself | Re-read from the mount; the mirror invalidates on write | +| `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 or a payload shape — discover both in-tree. -3. Write only your own findings file; never edit another agent's. -4. Never write under `.relay/`. -5. Read `recon.md` instead of re-deriving the PR map. -6. Record the commit you reviewed in your output. -7. Verify writeback before reporting success. -8. Report blockers instead of working around them. +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. diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index ae76959..3e2af47 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -1,10 +1,14 @@ --- title: 'Build a PR review bot' -description: 'End-to-end: connect GitHub with one command, add the rest of your context providers, mount the same workspace in every reviewer sandbox, and post the review back by writing a file.' +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.41**. 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 @@ -19,26 +23,30 @@ A PR review bot is the shape Relayfile fits best: several agents, several provid write /runs/** write /runs/** write /runs/** ``` -Every sandbox mounts 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. +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, in sub-second time. +- **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. Pick a lane before step 1; the steps below are annotated for both. +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 mount | long-lived `relayfile mount --background` | `ensureMountedWorkspace` per sandbox ([step 5](#5-mount-the-same-workspace-in-every-sandbox)) | `relayfile mount --once`, or no mount at all | +| 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 | -The important part: **provisioning happens once, mounting happens per run.** A cloud bot doesn't re-do OAuth on every PR — it joins a workspace that's already connected and mounts it in seconds. +**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 @@ -52,10 +60,10 @@ relayfile setup \ --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. Open the two URLs it prints, complete them, and the command blocks until GitHub reports ready. +`--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. - The Nango connect URL has a short TTL. If you finish the OAuth after the command has already exited, re-run the same `relayfile setup` line — a re-run reuses the workspace and only opens a new connect flow when the provider isn't connected yet. + 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 @@ -70,9 +78,9 @@ RELAYFILE_CLOUD_TOKEN="$CLOUD_TOKEN" relayfile setup \ --no-open ``` -`--skip-mount` returns as soon as the workspace exists and the provider reports ready, which is exactly the boundary between provisioning and running. The provider OAuth itself still needs a human the first time — print the connect URL, have someone complete it once, and every later run just joins. +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 thing from code, when your control plane already holds Cloud tokens: +The same provisioning from code, when your control plane already holds Cloud tokens: ```ts import { RelayfileSetup } from "@relayfile/sdk" @@ -83,6 +91,7 @@ const setup = RelayfileSetup.fromCloudTokens( ) 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) { @@ -91,96 +100,120 @@ if (connectLink) { } ``` -`fromCloudTokens` refreshes inside the refresh window, so a long-lived control plane doesn't have to re-auth between runs. Store the returned `rw_…` id — that's what every sandbox joins later. - - Use the `rw_…` workspace id from `joinWorkspace` (or from `relayfile setup`) 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. + 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 the mount before you write any bot code +## 2. Verify before you write any bot code ```bash relayfile status review-bot ``` -Read three fields and move on: +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`** — how stale the mirror is. Under a few seconds is healthy; over 60s means investigate before blaming your agent. -- **`daemon`** — if it says `not running`, restart it with `relayfile mount review-bot ./relayfile-mount --background`. -- **`dead-lettered`** — gate on this one. `failed` is a lifetime counter and is informational; dead-lettered means writes gave up. +- **`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 mount tell you its own shape rather than hard-coding paths from this page: +Then let the workspace describe its own shape rather than hard-coding paths from this page: ```bash -cat ./relayfile-mount/LAYOUT.md -cat ./relayfile-mount/github/.layout.md -cat ./relayfile-mount/github/repos/_index.json +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 ``` -`LAYOUT.md`, per-provider `.layout.md`, and `_index.json` are the contract — see [Mount layout](/docs/file/mount-layout). An agent that `cat`s them at startup needs no path knowledge in its prompt. + + 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. + -Without a mount — from CI, or from a laptop checking on the cloud workspace — `relayfile tree` and `relayfile read` answer the same questions against the server: +`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 --depth 2 -relayfile read review-bot /LAYOUT.md +relayfile tree review-bot /github/repos --depth 2 ``` -Both resolve a token from `--token`, then `RELAYFILE_TOKEN`, then your relay session, so a CI job needs one secret and no login step. +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 engineering standards it's supposed to follow. Don't guess provider ids — ask the live catalog, which spans both backends: +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 mount: +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 --json +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 are the one provider pair that needs 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. + 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. -The bot's context is now four providers wide and still one interface. That's the property worth designing around: adding `/notion` cost the reviewer agent zero new tool schemas, because `ls`/`cat`/`grep` already worked. - ## 4. Read the PR -Reads are reads. Enumerate the PRs, open one, and pull the surrounding context from other providers with the same handful of verbs: +Start from the index, not from a guessed filename: ```bash -MOUNT=./relayfile-mount +relayfile read review-bot /github/repos/AgentWorkforce/agentrelay.com/pulls/_index.json | jq '.[0:3]' +``` -# what's open -cat $MOUNT/github/repos/acme/api/pulls/_index.json +Canonical `_index.json` files are a **bare JSON array**. Pull rows carry the fields you'd otherwise open every record to get: -# the PR under review -cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/meta.json +```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" } +``` -# the ticket it references, by id -cat $MOUNT/linear/issues/by-id/AGE-12.json +That's enough to pick a PR by state, label, or branch without reading a single record. Then open the record: -# the standards the reviewer should enforce -grep -rl 'review checklist' $MOUNT/notion/pages/ +```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" ``` -`grep` here is exhaustive over synced records, not a paginated provider search — that difference is most of why a file-shaped reviewer beats an API-shaped one on recall. The `__` naming means the reviewer can recover `42` from the directory name and write back to the canonical path later without a lookup. +Three details that will bite an agent that guesses: - - Path shapes are adapter-owned and versioned with the adapter, so treat `.layout.md` and `_index.json` in *your* mount as authoritative over the examples above. - +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: -## 5. Mount the same workspace in every sandbox +```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: @@ -210,59 +243,61 @@ await sandbox.process.executeCommand("claude --print 'Review /workspace'", { 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" without a second round-trip. -- **Scope each specialist down.** The minted token's scopes are a subset of the caller's grant, so the security reviewer gets `read:/github/**` and `write:/runs/**` and nothing else. 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`. Spread it into the child process; the sandbox never sees your Cloud access token or the provider credentials. +- **`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. -Use `handle.status()` for a readiness snapshot and `handle.stop()` on teardown — it drains in-flight syncs and never deletes the local directory. +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` runs the same bootstrap and 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 the one to use in a cloud runner; the file path is the one to use on a workstation. +`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" -// CLOUD_API_ACCESS_TOKEN + CLOUD_WORKSPACE_ID come from the runner's secrets const rf = await connect({ agentName: "security-reviewer", scopes: ["relayfile:fs:read:/github/**", "relayfile:fs:write:/runs/**"], }) ``` -One login per machine; the per-agent scoping is what you vary between specialists. - ### Or skip the mount entirely -A cloud agent with no writable disk — a Lambda, a Worker, a short-lived job — doesn't need the mirror. The same workspace is reachable over HTTP with the same paths: +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 for a review bot: mount in the specialist sandboxes that actually explore the PR, use the client directly in the small functions that only read one record or post one result. Both see the same writes. +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. Writeback is adapter-driven, so a path outside every provider subtree stays inside the workspace instead of queueing a provider call: a durable, shared scratch space with the same sub-second visibility as the provider trees. `relayfile writeback status` is the check — writes under `/runs/**` shouldn't add pending ops. +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-42/recon.md <<'EOF' -# PR 42 — recon +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: AGE-12 (Fix login bug) +Ticket: AR-100 EOF ``` -Every specialist reads that one file instead of re-deriving the map of the PR — the recon cost is paid once and shared. Then each writes its own findings under its own path, so two specialists never contend for one file: +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-42/findings/security.json +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: @@ -272,75 +307,94 @@ import { readFile } from "node:fs/promises" import { join } from "node:path" import { onWrite } from "@relayfile/sdk" -onWrite("/runs/pr-42/findings/**", async (event) => { +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. A specialist that crashes and restarts re-reads `recon.md` and resumes; nothing about the run lived only in a dead process's memory. +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 instead of guessing a shape: +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 -cat $MOUNT/github/.adapter.md -cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/reviews/.schema.json -cat $MOUNT/github/repos/acme/api/pulls/42__bump-deps/reviews/.create.example.json +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" ``` -The schema is JSON Schema draft 2020-12 for the full synced record; fields marked `"readOnly": true` are server-managed and must not be written. Then the three rules from [Reads and writes](/docs/file/reads-and-writes) apply: +`.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 any *non-canonical* filename in the resource directory. The adapter creates the real record and rewrites your draft file as a receipt pointing at the canonical path. -- **PATCH** — write only the mutable fields to the canonical `.json`. Omitted fields are left alone. -- **DELETE** — remove the canonical file, when the resource's `.adapter.md` says delete is supported. +- **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/acme/api/pulls/42__bump-deps/reviews/draft-security.json <<'EOF' -{ "event": "COMMENT", "body": "2 findings from the security pass…" } -EOF +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 ``` -From the SDK, the same write carries optimistic concurrency — pass the revision you read, catch `RevisionConflictError`, re-read, retry: + + **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. + -```ts -await client.writeFile({ - workspaceId, - path: "/github/repos/acme/api/pulls/42__bump-deps/reviews/draft-security.json", - baseRevision: "*", - content: JSON.stringify(review), - contentType: "application/json", -}) +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" } ``` -Confirm it landed: +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 -relayfile writeback status review-bot --json | jq +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"}' ``` -Pending writebacks should drain to zero within a sync cycle, and the command exits non-zero only when there are dead-lettered ops — so gate CI on the exit code, not on the lifetime `failed` counter. If `deadLettered` is non-empty, each record under `$MOUNT/.relay/dead-letter/.json` carries `lastStatus` and a truncated `lastBody` — fix the payload, then `relayfile writeback retry --opId review-bot`. Read denials are preserved separately in `$MOUNT/.relay/permissions-denied.log`, so an ACL mistake surfaces as a logged denial rather than a silent no-op. Never write anything under `.relay/` yourself. +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. -## 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: +### Verifying ```bash -relayfile listen \ - --path "/github/repos/acme/api/pulls/**" \ - --event file.created \ - --run "review-bot start {{path}}" +relayfile writeback status review-bot --json ``` -Or in-process with `onWrite`, filtered by the same glob. Either way the reviewer wakes on provider state changing rather than on a request arriving — see [Events and webhooks](/docs/file/events). +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. -In the cloud this belongs in one long-lived orchestrator process — `relayfile listen` under your supervisor, or `connectWebSocket` inside the service — which then spawns a sandbox per PR. A subscriber that reconnects with a cursor gets the events it missed while it was down, so a deploy in the middle of a busy afternoon doesn't drop PRs on the floor. +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. - - Copy-paste agent brief — the same contract written for the agent itself, ready to drop in as `AGENTS.md`. - +## 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. + +Subscribe in-process with the SDK — `connectWebSocket({ onEvent })`, or the glob-filtered `onWrite` from step 6 pointed at `/github/repos///pulls/**`. To fan events out to a channel instead of a process, the CLI binds a path glob to a webhook: + +```bash +relayfile integration bind github "/github/repos/acme/api/pulls/**" \ + --channel reviews --webhook "$WEBHOOK_ID" --webhook-token "$WEBHOOK_TOKEN" +``` + +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; a subscriber that reconnects with a cursor gets the events it missed while it was down. ## Tear down @@ -351,6 +405,14 @@ 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`. diff --git a/web/lib/test/product-docs.test.ts b/web/lib/test/product-docs.test.ts index 466c892..5502b09 100644 --- a/web/lib/test/product-docs.test.ts +++ b/web/lib/test/product-docs.test.ts @@ -39,7 +39,7 @@ describe('Relayfile product docs', () => { ); expect(searchEntry).toMatchObject({ title: 'Build a PR review bot' }); - expect(searchEntry?.headings).toContain('5. Mount the same workspace in every sandbox'); + 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'); From 839cb63a838d14c28b41a67bf782316bea6a721a Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 13:26:38 +0200 Subject: [PATCH 4/6] docs(relayfile): correct my listen claim, verify on 0.10.53, document the CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I tested against relayfile 0.10.41 while 0.10.53 was published, and worse, I concluded `relayfile listen` did not exist because it was absent from `relayfile --help`. It exists in both versions — it is simply hidden from the help summary. `relayfile listen --help` prints its usage, and the command delivered events for three of three test writes. Reverts that wrong edit in events.mdx, introduction.mdx, and the guide, and documents `listen` properly instead: - cli.mdx gains a `relayfile listen` section — the command was missing from the CLI reference entirely — with its real flags (`--provider`, `--path`, `--event`, `--run`, `--format text|json`, `--background`) and a note that it is absent from `--help` and that `relayfile help listen` prints the generic help. - events.mdx carries a real captured event rather than an invented one: small files arrive with content inlined (`inlineContent: true`) and a `correlationId` that ties an event back to the write that caused it. - Both docs now say to supervise a subscriber. On a busy workspace the stream ends mid-message (`read limited at 32769 bytes`) or on a frame EOF within seconds, and reconnecting immediately earns a 429 on the WebSocket handshake. Re-verified on 0.10.53, unchanged: no `relayfile permissions`, `seed` still rejects `--dry-run`/`--exclude`/`--batch-size`, `status` reports pending/conflicts/denied, `writeback status` reports pending/failed/ dead-lettered, and a second mount is refused without `--rehome`. Sidebar version badge 0.10.31 → 0.10.53. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/cli.mdx | 27 +++++++++++++++++ web/content/docs/file/events.mdx | 35 +++++++++++++++------- web/content/docs/file/introduction.mdx | 11 ++++--- web/content/docs/file/review-bot-brief.mdx | 2 +- web/content/docs/file/review-bot.mdx | 27 +++++++++++++---- web/lib/product-docs-nav.ts | 2 +- 6 files changed, 80 insertions(+), 24 deletions(-) diff --git a/web/content/docs/file/cli.mdx b/web/content/docs/file/cli.mdx index 1f4f03d..666a749 100644 --- a/web/content/docs/file/cli.mdx +++ b/web/content/docs/file/cli.mdx @@ -83,6 +83,32 @@ 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; `{{path}}` is substituted | +| `--format` | `text` | `text` or `json` (one event object per line) | +| `--background` | `false` | Detach and keep listening | + +Events carry `eventId`, `type`, `path`, `revision`, `provider`, `origin`, `correlationId`, and — for small files — the content inlined. See [Events and webhooks](/docs/file/events). + + + `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. @@ -144,6 +170,7 @@ See [Run locally](/docs/file/run-locally) for the daemon in context and [Local d | `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. diff --git a/web/content/docs/file/events.mdx b/web/content/docs/file/events.mdx index d5eeac6..d73c361 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. @@ -40,14 +48,18 @@ Every change — webhook, sync, or another agent's write — surfaces as the sam ## Consuming events -Filter by path glob and event type so an agent only wakes for what it cares about. To fan events out to a channel, the CLI binds a path glob to a webhook: +Filter by path glob and event type so an agent only wakes for what it cares about. From the CLI: ```bash -relayfile integration bind linear "/linear/issues/by-state/triage/**" \ - --channel triage --webhook "$WEBHOOK_ID" --webhook-token "$WEBHOOK_TOKEN" +relayfile listen \ + --path "/linear/issues/by-state/triage/**" \ + --event file.created \ + --run "claude --print 'Triage this: {{path}}'" ``` -To react in your own process, use the SDK's `onWrite`, which subscribes over the same WebSocket stream and dispatches by pattern: +`relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]` streams the workspace's event feed. `--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'; @@ -65,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/introduction.mdx b/web/content/docs/file/introduction.mdx index 9819961..7d50dcc 100644 --- a/web/content/docs/file/introduction.mdx +++ b/web/content/docs/file/introduction.mdx @@ -14,12 +14,11 @@ What if you could do this with just a few lines of code and your agent wakes wit Relayfile materializes your full provider state as a file tree and keeps it current with webhooks. When an event fires, `/linear/issues/ENG-123.json` is already there. `/linear/issues/by-state/triage/` is already there. No API calls needed. -```ts -import { onWrite } from '@relayfile/sdk'; - -onWrite('/linear/issues/by-state/triage/**', async (event) => { - await agent.run(`Triage this: ${event.path}`); -}, { client, workspaceId, operations: ['create'] }); +```bash +relayfile listen \ + --path "/linear/issues/by-state/triage/**" \ + --event file.created \ + --run "claude --print 'Triage this: {{path}}'" ``` diff --git a/web/content/docs/file/review-bot-brief.mdx b/web/content/docs/file/review-bot-brief.mdx index 5e2f6fd..7db206a 100644 --- a/web/content/docs/file/review-bot-brief.mdx +++ b/web/content/docs/file/review-bot-brief.mdx @@ -6,7 +6,7 @@ description: 'A complete, copy-paste operating document for a Relayfile-backed P **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.41; it pairs with [Build a PR review bot](/docs/file/review-bot). + 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. diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index 3e2af47..5e46c97 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -6,7 +6,7 @@ description: 'End-to-end: connect GitHub with one command, add the rest of your 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.41**. 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. + 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 @@ -387,14 +387,31 @@ If an op dead-letters, its record under `$MOUNT/.relay/dead-letter/.json` 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. -Subscribe in-process with the SDK — `connectWebSocket({ onEvent })`, or the glob-filtered `onWrite` from step 6 pointed at `/github/repos///pulls/**`. To fan events out to a channel instead of a process, the CLI binds a path glob to a webhook: +From the CLI, `relayfile listen` streams the feed and runs a command per event: ```bash -relayfile integration bind github "/github/repos/acme/api/pulls/**" \ - --channel reviews --webhook "$WEBHOOK_ID" --webhook-token "$WEBHOOK_TOKEN" +relayfile listen default \ + --path "/github/repos/acme/api/pulls/**" \ + --event file.created \ + --run "review-bot start {{path}}" ``` -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; a subscriber that reconnects with a cursor gets the events it missed while it was down. +Its full surface is `relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]`. 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 diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 93c7092..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', From b20e1aba62ac31cc38a313311fa749b5d23cd968 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 13:31:44 +0200 Subject: [PATCH 5/6] docs(relayfile): verify listen --run substitution and document all five placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed `--run` placeholder substitution live rather than taking the docs' word for it. The event stream on a busy workspace drops within seconds, so a single attempt is unreliable; a driver that reconnects with backoff and writes only once the stream reports "Listening on" caught it on the second try. The handler received: ARG1=/runs/doc-verify-20260904/findings/drive-1-1.json {{path}} ARG2=file.created {{type}} ARG3=runs {{provider}} ARG4=rev_2936535 {{revision}} ARG5..17=type:file.created path:/runs/… revision:rev_… {{event}} The surprise is `{{event}}`: it expands to the entire event as space-separated key:value pairs — 13 tokens in this capture — so unquoted it splatters across argv. That is why the CLI's own embedded example writes '{{event}}' in quotes. Documented with a warning. All five placeholders come from the binary's own flag help ("shell command per event; supports {{path}}, {{type}}, {{provider}}, {{revision}}, {{event}}"), which no page documented; four of them appeared nowhere in our docs at all. Also documents `relayfile supervisor install`, which accepts every listen flag and embeds them verbatim into a launchd/systemd unit that restarts on failure — the supported way to keep a subscriber alive given the stream instability. Verification scratch files under /runs/doc-verify-20260904/ have been deleted from the workspace; /runs is empty again. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/cli.mdx | 25 ++++++++++++++++++++++++- web/content/docs/file/events.mdx | 2 +- web/content/docs/file/review-bot.mdx | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/web/content/docs/file/cli.mdx b/web/content/docs/file/cli.mdx index 666a749..b8602ff 100644 --- a/web/content/docs/file/cli.mdx +++ b/web/content/docs/file/cli.mdx @@ -99,12 +99,35 @@ relayfile listen \ | `--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; `{{path}}` is substituted | +| `--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. diff --git a/web/content/docs/file/events.mdx b/web/content/docs/file/events.mdx index d73c361..648699d 100644 --- a/web/content/docs/file/events.mdx +++ b/web/content/docs/file/events.mdx @@ -57,7 +57,7 @@ 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. `--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 …`. +`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: diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index 5e46c97..fb3d3cf 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -396,7 +396,7 @@ relayfile listen default \ --run "review-bot start {{path}}" ``` -Its full surface is `relayfile listen [WORKSPACE] [--provider PROVIDER] [--path GLOB] [--event TYPE] [--run CMD] [--format text|json] [--background]`. 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: +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", From 592ceca0fcf50fee3e77a8e33102ed24c55308f3 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Fri, 4 Sep 2026 13:35:23 +0200 Subject: [PATCH 6/6] docs(relayfile): address the second round of PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five findings, all valid: - cli.mdx still described `seed` as respecting `--exclude` two lines above the warning saying it takes no flags. Dropped the claim. - reads-and-writes.mdx advertised a create example the code block never showed. Added the `.create.example.json` read. - The guide's step 8 ran `relayfile listen default` — the workspace this guide creates is `review-bot`, so the example would have failed. Fixed. - events.mdx's `onWrite` snippet filtered its own writes with `event.source === 'agent'`, a field the event schema doesn't have. The captured event uses `origin`, so it is now `event.origin === 'agent_write'`. - mount-layout.mdx claimed a fixed set of "four alias views" including `by-name/` under `github/repos/`, contradicting the guide and the brief. The alias conflict was real and the reviewer was right to flag it; checked against the live workspace to settle it rather than picking a side: /github/repos/__/pulls/ → by-creator, by-edited, by-id, by-state, by-title (flat namespace) /github/repos/__/issues/ → by-edited, by-id, by-state, by-title /linear/issues/ → by-id, by-assignee, by-edited, … (under the canonical subtree) /github/repos/by-name/… → 404, does not exist So the set and the location are both per-adapter and per-resource. mount- layout.mdx now says that, with the two locations contrasted, and notes that a flat alias directory's `_index.json` is a manifest of alias subdirectories rather than a record index. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NVwyjaMnm1PTRC6m7oXHJV Session-Id: 13b5d5ea-39b6-42f3-86df-6d7bf3570e4b --- web/content/docs/file/cli.mdx | 2 +- web/content/docs/file/events.mdx | 2 +- web/content/docs/file/mount-layout.mdx | 33 +++++++++++++--------- web/content/docs/file/reads-and-writes.mdx | 1 + web/content/docs/file/review-bot-brief.mdx | 2 +- web/content/docs/file/review-bot.mdx | 2 +- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/web/content/docs/file/cli.mdx b/web/content/docs/file/cli.mdx index b8602ff..2a87957 100644 --- a/web/content/docs/file/cli.mdx +++ b/web/content/docs/file/cli.mdx @@ -56,7 +56,7 @@ 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 diff --git a/web/content/docs/file/events.mdx b/web/content/docs/file/events.mdx index 648699d..22eab11 100644 --- a/web/content/docs/file/events.mdx +++ b/web/content/docs/file/events.mdx @@ -65,7 +65,7 @@ Or from the SDK with `onWrite`, which subscribes over the same WebSocket stream 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'] }); ``` diff --git a/web/content/docs/file/mount-layout.mdx b/web/content/docs/file/mount-layout.mdx index 3b03658..acab8fd 100644 --- a/web/content/docs/file/mount-layout.mdx +++ b/web/content/docs/file/mount-layout.mdx @@ -1,6 +1,6 @@ --- 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. @@ -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 @@ -51,16 +54,18 @@ The first path is the canonical human-readable form. The second is a legacy fall 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/reads-and-writes.mdx b/web/content/docs/file/reads-and-writes.mdx index 3a08aa9..af87e7d 100644 --- a/web/content/docs/file/reads-and-writes.mdx +++ b/web/content/docs/file/reads-and-writes.mdx @@ -61,6 +61,7 @@ You don't need an out-of-band schema registry. Every writable resource advertise ```bash 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" ``` diff --git a/web/content/docs/file/review-bot-brief.mdx b/web/content/docs/file/review-bot-brief.mdx index 7db206a..4d5460f 100644 --- a/web/content/docs/file/review-bot-brief.mdx +++ b/web/content/docs/file/review-bot-brief.mdx @@ -45,7 +45,7 @@ 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 (`by-id/`, `by-title/`, `by-state/`, `by-creator/`, `by-edited/`) live in a flat sibling namespace, `/github/repos/__/pulls/…`, not under the canonical repo path. Linear's are at `/linear/issues/by-id/.json`. +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 diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index fb3d3cf..1514113 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -390,7 +390,7 @@ The bot doesn't need a GitHub App endpoint of its own. A provider webhook is alr From the CLI, `relayfile listen` streams the feed and runs a command per event: ```bash -relayfile listen default \ +relayfile listen review-bot \ --path "/github/repos/acme/api/pulls/**" \ --event file.created \ --run "review-bot start {{path}}"