# Proof Agent Docs

## If Proof Looks Wrong

Use one tool call:

  POST /api/bridge/report_bug

If you already have a document slug, you can also call:

  POST /d/<slug>/bridge/report_bug

Send what you know. The server decides whether the report is complete, enriches it with server traces and document events, creates a GitHub issue when it has enough context, and returns a `fixerBrief` you can hand directly to another agent.

### Transient failure guidance

If a non-destructive read or action fails because of a timeout, slow load, or stale state, retry once. If the retry still fails, report it with raw evidence instead of a summary from memory.

### Detailed issue reporting guidance

If the failure looks transient:

- Retry once if the action was non-destructive and the failure looked like intermittent downtime, a timeout, a slow load, or a stale read.
- If the retry succeeds and the doc fully recovers, treat it as recovery noise unless you saw data loss, a stuck paused state, or the same failure twice on the same slug.
- If the retry still fails, report it with raw evidence instead of summarizing it from memory.
- Include whether the failure came from the homepage, library, shared doc HTML, `state`, `snapshot`, `collab-refresh`, `edit/v2`, comment routes, or another write route.

Report these issues right now:

- A doc stays paused after refresh/reopen and blocks normal reading or writing.
- `state`, shared doc HTML, or `snapshot` repeatedly times out or returns 5xx for the same slug.
- `edit/v2`, comment writes, or mark mutations fail after a fresh read and one safe retry.
- The UI shows blank, stale, duplicated, or rolled-back content after the server said a request succeeded.
- Comments, suggestions, or marks disappear, reappear, or disagree across refreshes or devices.
- Two surfaces disagree about document status, for example the page says paused but writes still apply, or `state` looks fresh while shared HTML is obviously stale.

Usually report these only when they repeat:

- A single slow homepage or library load that succeeds on retry.
- A one-off timeout with no document slug and no persistent symptoms.
- Expected `409` conflict responses caused by stale `baseToken` values, stale snapshots, or ambiguous anchors.
- Brief reconnect jitter where content, comments, and writes recover normally.
The most useful evidence is:
- Exact request URL, method, status, and `x-request-id`
- The document slug, if there is one
- Whether retry changed the outcome
- The raw response body or console/network error text

If the response says `status: "needs_more_info"`, ask the suggested questions and call the same endpoint again.

If you want reference code while debugging or writing up the report, you can optionally inspect the open-source repo:

  https://github.com/EveryInc/proof-sdk

## Which Editing Method Should I Use?

Proof has three editing approaches. Pick one per change.

| Goal | Method | Endpoint |
|------|--------|----------|
| **Add/replace/insert a few lines** (recommended) | Edit V2 (block-level) | `GET /snapshot` → `POST /edit/v2` |
| **Replace entire document** | Rewrite | `POST /ops` with `rewrite.apply` |
| **Add a comment** | Ops | `POST /ops` with `comment.add` |

**Start with Edit V2** for most tasks. It uses precise block refs, handles concurrent edits cleanly, and returns clean markdown without internal HTML annotations.

`suggestion.add` now matches against annotated documents correctly and preserves stable anchors, but `edit/v2` is still the better default for programmatic content changes.

`rewrite.apply` is still disruptive. Avoid it if anyone might have the document open: hosted environments block rewrites while live authenticated collaborators are connected, and `force` is ignored there.

## I Just Received A Proof Link

No browser automation is required. Use HTTP directly (for example, `curl` or your tool's `web_fetch`).

If you received a shared link like:

  https://www.proofeditor.ai/d/<slug>?token=<token>

You can discover the API and read the document in one step using **content negotiation** on that same URL.

Fetch JSON (recommended):

  curl -H "Accept: application/json" "https://www.proofeditor.ai/d/<slug>?token=<token>"

Fetch raw markdown:

  curl -H "Accept: text/markdown" "https://www.proofeditor.ai/d/<slug>?token=<token>"

The JSON response includes:
- `markdown` (document content)
- `_links` (state, ops, docs)
- `agent.auth` hints (how to use the token)

### Quick copy/paste flow (token already in the shared URL)

```bash
SHARE_URL='https://www.proofeditor.ai/d/<slug>?token=<token>'
TOKEN='<token>'
SLUG='<slug>'
AGENT_ID='ai:your-agent'

curl -H "Accept: application/json" "$SHARE_URL"
curl -H "Accept: text/markdown" "$SHARE_URL"
curl -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: $AGENT_ID" "https://www.proofeditor.ai/api/agent/$SLUG/state"
```

## Read Comments And Thread State

Use:

  GET /api/agent/<slug>/state

The state response is the canonical read surface for document text plus mark metadata.

Comment bodies, replies, and resolved state live in `marks` on the state response. For comment marks, `thread` is the chronological message list and includes the root comment as `thread[0]`; legacy `text` remains the root comment body. `state.marks` is a union of mark kinds, so comments-only workflows should request:

  GET /api/agent/<slug>/state?kinds=comment

The response still uses `marks`; the query parameter filters that object server-side. The same URL is advertised in `_links.stateComments` and `agent.commentReadApi`.

For comment threads, read filtered `state.marks` and inspect comment marks like:

  {
    "kind": "comment",
    "text": "Comment body",
    "threadId": "comment-123",
    "thread": [
      {"by":"human:reviewer","text":"Comment body","at":"...","root":true},
      {"by":"human:editor","text":"Reply","at":"..."},
      {"by":"ai:reviewer","text":"Fixed in this reply.","at":"...","resolvedHere":true}
    ],
    "resolved": false
  }

Common mark kinds:

| Kind | Key fields | Intended callers |
|------|------------|------------------|
| `comment` | `text`, `threadId`, `thread`, `replies`, `resolved`, `quote` | Agents replying to or resolving human review threads. |
| `authored` | `by`, `createdAt`, `range`, `quote`, `startRel`, `endRel` | Authorship/provenance UI; agents should not process these as review comments. |
| `insert` | `content`, `quote`, `status`, `target` | Track-changes suggestion workflows. |
| `delete` | `quote`, `status`, `target` | Track-changes suggestion workflows. |
| `replace` | `content`, `quote`, `status`, `target` | Track-changes suggestion workflows. |

Use comma-separated filters when a workflow intentionally needs more than comments, for example `GET /api/agent/<slug>/state?kinds=comment,suggestion,provenance`. Semantic aliases expand server-side: `comment` -> `comment`, `suggestion` -> `insert`, `delete`, `replace`, and `provenance` -> `authored`.

Use `/snapshot` only for block refs and `edit/v2`. It is block-focused and does not include comment thread bodies.

Use the comment mark's `markId` as the reply target when calling `comment.reply`. `threadId` is part of the read surface, but the write route expects `markId`.
Set `resolve: true` on `comment.reply` to reply and resolve the thread in one mutation.
Replies created with `resolve: true` include `resolvedHere: true` on that reply's thread message, so clients can tell which message carried the resolution. Standalone `comment.resolve` and `comment.unresolve` only change the top-level `resolved` flag; they do not append synthetic thread messages.
`comment.resolve` keeps the comment in the marks map and flips `resolved: true`; it does not delete the comment. The UI/native bridge has a separate delete action, but `comment.delete` is not part of the public `/ops` contract today.

Use `/events/pending` to notice activity that may require a refresh. Do not treat it as the canonical source of comment thread text.

## Auth: Token From URL

If a URL contains `?token=`, treat it as an access token:

- Preferred: `Authorization: Bearer <token>`
- Also accepted: `x-share-token: <token>`

## Edit Via Ops (Comments, Suggestions, Rewrite)

Use:

  POST /api/agent/<slug>/ops

`by` controls authorship. Presence identity is separate.
- Bearer/share tokens authenticate document access.
- Preferred: send `X-Agent-Id: <your-agent-id>`.
- Also accepted on `/presence`: `agentId` or `agent.id` in the JSON body.
- `by` records authorship.
All public mutation routes require `baseToken` from `GET /api/agent/<slug>/state` or `GET /api/agent/<slug>/snapshot`. Successful `/ops` responses include the next `mutationBase.token`; reuse it as the next `baseToken` when chaining writes.

Supported op types:
- `comment.add`
- `comment.reply`
- `comment.resolve`
- `comment.unresolve`
- `suggestion.add` with `kind: "insert" | "delete" | "replace"`
- `suggestion.accept`
- `suggestion.reject`
- `rewrite.apply`

Use `suggestion.accept` or `suggestion.reject` to finish suggestions.
For comments, the public API supports resolve/unresolve, not delete.

Add a comment:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"comment.add","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","quote":"text to anchor","text":"comment body"}'

Reply to an existing thread:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"comment.reply","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","markId":"comment-123","text":"Reply text"}'

Reply and resolve in one mutation:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"comment.reply","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","markId":"comment-123","text":"Fixed.","resolve":true}'

Batch replies/resolves across multiple existing threads:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","operations":[{"type":"comment.reply","markId":"comment-123","text":"Fixed first.","resolve":true},{"type":"comment.reply","markId":"comment-456","text":"Fixed second.","resolve":true}]}'

Batch `/ops` currently supports existing-thread comment mutations: `comment.reply`, `comment.resolve`, and `comment.unresolve`. The whole batch validates against one `baseToken` and persists as one authoritative marks mutation.

Suggest a replace:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"suggestion.add","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","kind":"replace","quote":"old text","content":"new text"}'

Create and immediately apply a suggestion:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"suggestion.add","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","kind":"replace","quote":"old text","content":"new text","status":"accepted"}'

Accept a suggestion:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"suggestion.accept","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","markId":"mark-123"}'

Reject a suggestion:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"suggestion.reject","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","markId":"mark-123"}'

Rewrite the whole document:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/ops?token=<token>" \
    -H "Content-Type: application/json" \
    -H "X-Agent-Id: your-agent" \
    -d '{"type":"rewrite.apply","by":"ai:your-agent","baseToken":"mt1:<token-from-state-or-snapshot>","content":"# New markdown..."}'

## Update Title Metadata

Use:

  PUT /api/documents/<slug>/title

Example:

  curl -X PUT "https://www.proofeditor.ai/api/documents/<slug>/title" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer <token>" \
    -d '{"title":"Updated document title"}'

Discovery:
- `GET /api/agent/<slug>/state` includes `_links.title` and `agent.titleApi`.
- Authenticate with `Authorization: Bearer <token>` or `x-share-token: <token>`.

## Edit V2 (Block IDs + Authoritative Base Locking)

Use v2 for top-level block edits with precise block refs and authoritative optimistic locking. Read `/snapshot`, send that response's `mutationBase.token` as `baseToken`, and use `blocks[].ref` for the block you want to target. Treat block refs as opaque request tokens.

Payload shape reminder:
- `/api/agent/<slug>/ops` uses a top-level `type` field for single operations, or a top-level `operations` array for comment batches.
- `/api/agent/<slug>/edit/v2` uses a top-level `operations` array, and each entry uses `op` like `replace_block` or `insert_after`.
- Do not mix these request shapes.

### Get a snapshot

  GET /api/agent/<slug>/snapshot

Example:

  curl -H "Authorization: Bearer <token>" "https://www.proofeditor.ai/api/agent/<slug>/snapshot"

The response includes an ordered `blocks` array. Each block has:

- `ref`: the opaque identifier to send back to `/edit/v2`
- `ordinalRef`: a human-readable position hint such as `b3`
- `markdown`: clean markdown for that block

When authoritative mutations are available, the snapshot also includes `mutationBase.token`; use that token as your write precondition. `blocks[].ref` is the mutation target identifier. Do not rely on the ref format or assume refs are stable across snapshots.

### Apply edits

  POST /api/agent/<slug>/edit/v2

Example:

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/edit/v2" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer <token>" \
    -H "Idempotency-Key: <uuid>" \
    -d '{
      "by": "ai:your-agent",
      "baseToken": "mt1:<token-from-snapshot>",
      "operations": [
        { "op": "replace_block", "ref": "<block-ref-from-snapshot>", "block": { "markdown": "Updated paragraph." } },
        { "op": "insert_after", "ref": "<block-ref-from-snapshot>", "blocks": [{ "markdown": "## New Section" }] }
      ]
    }'

On success, the response includes the new `revision`, a `snapshot` payload, and a `collab` status. Full successful `/edit/v2` responses include the next `mutationBase.token` and fresh `snapshot.blocks[].ref` values; reuse both when chaining another block-ref edit.
If your `baseToken` is stale, you'll receive `STALE_BASE` plus the latest snapshot for retry. If a block ref cannot be resolved in the current document, re-read `/snapshot` and retry with the fresh `mutationBase.token` and `blocks[].ref`.
Within one `/edit/v2` request, all `ref`, `fromRef`, and `toRef` values are resolved against the same base snapshot before any operation is applied.

Validate a proposed batch without writing by adding `?dryRun=1` or `?validate=1` to `/edit/v2`. Send the same body; dry-run responses include `valid`, `appliedCount`, and per-op `results[]`; no document mutation, events, or broadcasts occur.
Use `?return=minimal` on `/edit/v2` to receive `ok`, `revision`, `appliedCount`, next `mutationBase`, and optional `warnings`. Minimal responses are token-only and omit fresh block refs; re-read `/snapshot` before another `/edit/v2` mutation that needs `blocks[].ref`.
`post_commit_verification_pending` means the write committed, but the server could not complete post-commit verification before responding; read `/state` or `/snapshot` to confirm convergence before retrying the same edit. Mutation base tokens are content-addressed, so a token can repeat when the document returns to an exact previous state.

Supported `/edit/v2` operation kinds:
- `replace_block`
- `insert_before`
- `insert_after`
- `delete_block`
- `replace_range`
  `replace_range` uses `fromRef` and `toRef`.
- `find_replace_in_block`
  `occurrence` may be `first` or `all` and defaults to `first`.
- `find_replace_in_doc`
  Literal document-wide find/replace. `occurrence` may be `first` or `all` and defaults to `all`; optional `fromRef`, `toRef`, and `block_filter.kind` constrain the sweep. Responses include `operationResults` with per-block match counts. Treat `operationResults.blockMatches[].ref` as reporting/display data; use fresh `/snapshot` `blocks[].ref` values for follow-up mutations.

v2 convergence fields:
- `collab.status` reports convergence status (`confirmed|pending`) and is fragment-authoritative.
- `collab.fragmentStatus` and `collab.markdownStatus` expose render-vs-projection split directly.
- `202` is only expected when fragment convergence is pending.

Precondition contract for v2:
- `baseToken` is required.
- Read it from `/snapshot` when you are sending block refs.
- Use `blocks[].ref` from that snapshot as the mutation target; `ordinalRef` is a display hint.

Idempotency guidance:
- Send `Idempotency-Key` for mutation requests.
- `/edit/v2` examples include this header because block-level retries are common in automation.

Mutation contract discovery:
- Read `contract.mutationStage` from `GET /api/agent/<slug>/state` to detect Stage A/B/C rollout.
- `contract.idempotencyRequired` and `contract.preconditionMode` summarize current requirements.

Common mutation contract error codes:
- `IDEMPOTENCY_KEY_REQUIRED`: mutation request omitted idempotency key in required stage.
- `IDEMPOTENCY_KEY_REUSED`: same key reused with a different payload hash.
- `BASE_TOKEN_REQUIRED`: route requires `baseToken`.
- `INVALID_BASE_TOKEN`: `baseToken` is malformed and must be an `mt1:` token.
- `STALE_BASE`: document changed since the provided `baseToken`.
- `PRECISE_REF_REQUIRED`: live `/edit/v2` needs a snapshot block ref; re-read `/snapshot` and use `blocks[].ref`.
- `INVALID_REF`: `/edit/v2` received a block ref that cannot be resolved in the current block map; re-read `/snapshot` and retry with `blocks[].ref`.
- `STALE_BLOCK_REF`: `/edit/v2` received a snapshot block ref from a different document or `baseToken`; retry with `mutationBase.token` and `blocks[].ref` from a fresh snapshot.
- `LIVE_CLIENTS_PRESENT`: rewrite blocked because active authenticated collab clients are connected.
  Use `retryWithState` to refresh state. Hosted environments use `/edit/v2` or wait until `connectedClients === 0`.
  This response is retryable and includes `reason` + `nextSteps`.
- `YJS_UPDATE_TOO_LARGE`: mutation would exceed the authoritative Yjs update size limit.
  This response is terminal for the submitted payload; split or reduce the change before retrying.
- `REWRITE_BARRIER_FAILED`: rewrite safety barrier failed before mutation; no rewrite was applied.
  This response is retryable and includes `reason` + `nextSteps`; retry with bounded exponential backoff and jitter.

## Presence And Event Polling

Poll for changes:

  GET /api/agent/<slug>/events/pending?after=<cursor>&limit=100

Ack processed events (editor/owner):

  POST /api/agent/<slug>/events/ack
  Body: {"upToId": <cursor>, "by": "ai:your-agent"}

Presence identity:
- `POST /api/agent/<slug>/presence` requires explicit agent identity.
- Bearer/share tokens authenticate document access.
- Preferred: `X-Agent-Id: <your-agent-id>`.
- Also accepted: `agentId` or `agent.id` in the JSON body.
- `by` is not used for presence identity.

## Local Bridge

For the local Proof app bridge (localhost:9847) and deeper workflows, see:

  https://www.proofeditor.ai/agent-setup

## Projection Guardrails And QA

Operational metrics:
- `authoritative_read_degraded_total{source,reason}`
- `collab_persisted_yjs_update_bytes_bucket{source,outcome,reason,le}`

Staging soak (live browser viewers + repeated `/edit/v2`):

  SHARE_BASE_URL=https://proof-web-staging.up.railway.app \
  SOAK_DURATION_MS=300000 \
  npx tsx scripts/staging-collab-projection-soak.ts

## Create A New Shared Doc

If you need to create a share from scratch, use:

  POST /share/markdown

This is the canonical public create route.

## Recommended Workflow: Adding Content To An Existing Doc

This is the most reliable way to add a line, row, or section to an existing document:

### Step 1: Get the snapshot

  curl -H "Authorization: Bearer <token>" "https://www.proofeditor.ai/api/agent/<slug>/snapshot"

This returns clean markdown per block, opaque `ref` identifiers, `ordinalRef` position hints, and `mutationBase.token`. Use that token as your write precondition and `ref` as the block target.
### Step 2: Find the right block

Look through the `blocks` array for the block you want to edit or insert near. Each block has:
- `ref`: opaque identifier to send to `/edit/v2`
- `ordinalRef`: position hint (e.g., `b3`)
- `markdown`: the clean markdown content of that block
- `type`: block type (e.g., `paragraph`, `heading`, `table`)

### Step 3: Apply your edit

  curl -X POST "https://www.proofeditor.ai/api/agent/<slug>/edit/v2" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer <token>" \
    -H "Idempotency-Key: <uuid>" \
    -d '{
      "by": "ai:your-agent",
      "baseToken": "mt1:<token-from-snapshot>",
      "operations": [
        { "op": "insert_after", "ref": "<block-ref-from-snapshot>", "blocks": [{ "markdown": "New content here." }] }
      ]
    }'

### Step 4: Handle conflicts

If you get `STALE_BASE`, `INVALID_REF`, or `STALE_BLOCK_REF`, the response includes the latest state or snapshot. Re-read `/snapshot` and retry with the fresh `mutationBase.token` and `blocks[].ref`.

## Troubleshooting

### `web_fetch` / `WebFetch` cannot access `proofeditor.ai`

If your tool reports a blocked or disallowed domain, ask the user to allow `proofeditor.ai` in the assistant's **Capabilities** settings (Web access allowed domains), then retry the same request.

### `LIVE_CLIENTS_PRESENT` on `rewrite.apply`

`rewrite.apply` is blocked when authenticated collaborators are connected. Outside hosted environments you can pass `"force": true`, but on hosted environments `force` is ignored. If you still prefer the safer path:
1. Use `/edit/v2` instead (it works with live clients).
2. Wait for clients to disconnect (poll `/state` and check `connectedClients`).

### Suggestion anchors not matching

`suggestion.add` now resolves quotes against clean text even when the stored markdown contains internal `<span data-proof="authored">` annotations. If you still get `ANCHOR_NOT_FOUND`, re-read state and verify the quote text genuinely exists.

### Document content looks corrupted after suggestion reject cycles

Repeated suggest/reject cycles on annotated documents now preserve stable suggestion anchors so the document text should remain unchanged. If you still see unexpected content drift, re-read `Accept: text/markdown` and report the exact request/response pair.

### `COLLAB_SYNC_FAILED` errors

Edits via the API can fail when a browser has the document open with an active Yjs collab session. `/edit/v2` handles this gracefully, but `rewrite.apply` does not. If you hit this, retry after a short delay or use `/edit/v2` instead.
