<!-- GENERATED from app/src/lib/apiManifest.ts by scripts/gen-agent-surface.ts — do not edit by hand. -->
# AGENTS.md — FlowGraph (agent guide, API v1.0.0)

**FlowGraph is agent-native.** Agents read and write your graph through the MCP server, and
every write flows through the same **governed, lock-aware, provenance-stamped** path as a human
edit. A write to a human-approved (`locked`) atom — or one the agent lacks the `modify`
capability for — is **REJECTED**. Nothing an agent does is silently trusted: writes surface for
the human as ghost-diffs on the canvas and as items in the review calendar.

## Connect
The MCP server (`https://github.com/ndsolaz/flowgraph`, `app/mcp/server.ts`) speaks stdio and exposes a vault
folder of `.fgraph` files:

```bash
FLOWGRAPH_VAULT=/path/to/vault npx tsx app/mcp/server.ts
```

Registry descriptor: [`/.well-known/mcp/server.json`](https://flow-graph.com/.well-known/mcp/server.json) ·
OpenAPI schema: [`/openapi.json`](https://flow-graph.com/openapi.json).

## The write contract
Every write ends up as a `GraphPatch {ops[]}` through ONE governed path — provenance-stamped and
lock-aware. **No tool is destructive**: every write is additive, staged and reversible. Human-
approved atoms are immutable to AI; agents honour each atom's `permissions.ai`. An agent never
approves, locks, or signs off — only a human does.

## Write knowledge, not ops
For anything **content-bearing**, do not hand-write `addCard`/`addFacet`/`addItem` ops. Read the
canvas with `get_canvas_digest` and write back a **draft** in FDG — plain markdown — with
`submit_knowledge_draft`. A deterministic compiler (never a model) turns the draft into the
governed patch.

Why this is the recommended path, and not sugar:

- **You think in knowledge, not in a schema.** Reasoning inside a mutation schema is what makes
  models emit title-only cards; the ops are a serialization concern, so we do them for you.
- **A card arrives whole.** A new card compiles to ONE atomic op carrying its sections and
  entries inline — it can never materialize as an empty shell.
- **Anchors, not ids.** The digest aliases every card, section, entry and group (`[c3]`,
  `[c3.f2]`, `[c3.f2.i4]`, `[g1]`) so you cite what you read, cheaply. Anchors are issued **by a
  digest** and are not durable ids — pass the digest's `digestId` back when you submit, or your
  anchors may resolve to atoms you never read.
- **Failure is legible.** Nothing is written unless the whole draft compiles and validates; a
  problem comes back as indexed errors plus every dropped line and its reason, so you can correct
  and resubmit rather than guess.
- **It is the same grammar the app's own AI writes**, compiled by the same compiler into the same
  governed path. No agent dialect to keep in sync.

`apply_graph_patch` remains for surgical, programmatic ops (fix a title, add one edge).

## Mermaid in, Mermaid out
FlowGraph accepts **Mermaid** as input and output — paste a Mermaid diagram at
[https://flow-graph.com/tools/mermaid-to-flowgraph](https://flow-graph.com/tools/mermaid-to-flowgraph) and it opens as a real, editable graph (parsed client-side,
no upload). Meet LLMs where they already emit.

## MCP tools
| Tool | Access | Args | Summary |
|---|---|---|---|
| `list_canvases` | read | — | List canvases in the vault (id, name, node count). |
| `get_canvas` | read | canvasId | Get a canvas — its nodes and edges — by id. |
| `query_graph` | read | canvasId, query | Edge-aware query: text · facet · tags · relType · traverse(up/down). Returns matching node ids + titles. |
| `flowgraph_retrieve` | read | query, handle, canvasId, k, hops | Graph-CCR retrieval (READ-ONLY): pull exactly the sub-graph you need. Give a `query` (semantic/keyword seed → structural expansion) OR a `handle` (a retrieval handle "canvasId::nodeId" from an assembled context packet) → its original atom + neighbourhood. A STALE handle returns a recovery hint, never an error. Output is governance-aware (atoms without an AI use-as-context grant come back degraded, no body) and provenance-stamped. |
| `create_canvas` | read | name | Create a new empty canvas; returns its id. |
| `apply_graph_patch` | write | canvasId, patch | Apply a GraphPatch (ops[]) through the GOVERNED, provenance-stamped, lock-aware write path. A write to a human-approved (locked) atom — or one the AI lacks the "modify" capability for — is REJECTED. |
| `refresh_codebase_map` | write | canvasId, repos, pruneRemoved | Refresh a multi-repo (fleet) codebase map: pass each repo's label + package name + declared dependencies (with the manifest line each is declared on), and this stages a governed ghost-draft that (re)builds one card per repo and the typed `depends` cross-repo edges, each carrying its manifest-line receipt. Reconciled against the target canvas — existing repo cards update, new repos add, the edge set recomputes — so a scheduled re-run is idempotent and never duplicates. A human accepts it in the Review Inbox (agents never auto-apply). Use it as the cron/agent codebase-refresh recipe. |
| `get_canvas_digest` | read | canvasId, nodeIds, hops, budget | Read a canvas as an FDG digest (READ-ONLY): the anchored knowledge grammar the in-app AI reads and writes. Every card, section, entry and group carries a short anchor ([c3], [c3.f2], [c3.f2.i4], [g1]) you cite when you write back with `submit_knowledge_draft`. `nodeIds` focuses the read (those cards + their neighbours render in FULL, the rest as an outline); `hops` widens that full-fidelity ring; `budget` caps the characters (an honest tail line says what was withheld — never a silent truncation). Returns the digest text, the anchor→atom map, per-card fidelity, stats, and a `digestId` — pass that back to `submit_knowledge_draft` so your anchors mean the atoms you actually read. The cheap way to orient before writing; complements `flowgraph_retrieve`. |
| `submit_knowledge_draft` | write | canvasId, draft, mode, digestId | WRITE KNOWLEDGE, NOT OPS — the preferred way to write anything content-bearing. Submit a draft in FDG (the markdown grammar `get_canvas_digest` speaks) and a DETERMINISTIC compiler turns it into a GraphPatch applied through the same governed, lock-aware, provenance-stamped path as every other write; a locked atom is refused. `mode:"apply"` (default) writes it; `mode:"stage"` lands it as a Review-Inbox ghost-draft for a human to accept. On ANY compile or validation problem NOTHING is written — you get indexed errors[], every dropped block with its line and reason, and a fix hint, so you can correct and resubmit. Returns applied/newCards/anchorMap/dropped/depth/questions/suggestions, so your [anchors] stay addressable and you can relay the AI's questions or chain its suggested next moves. `apply_graph_patch` remains for surgical, programmatic ops. |
| `add_card` | write | canvasId, title, body | Convenience: add one card to a canvas. |
| `connect` | write | canvasId, source, target, relType, label | Convenience: connect two cards with a typed relationship. |
| `list_skills` | read | readyOnly | List reusable SKILLS (promoted action items) — name, the card/canvas they live on, their inner-canvas id, and whether they are READY (inner canvas non-empty). The agent capability catalog. |
| `list_due_reviews` | read | window, assignee | The ranked review agenda — scheduled reviews across the vault joined with attention state, bucket (overdue/today/week/later), blast-radius and owner. The same agenda the human sees. |
| `schedule_review` | write | canvasId, nodeId, when, assignee, note | Propose a review date on an atom (ISO date or relative like "in 2 weeks"). A GOVERNED write: stamped provenance.kind=ai, REJECTED on human-approved (locked) atoms, surfaced for the human in the calendar / Review Inbox — agents never silently sign off. |
| `reschedule_review` | write | canvasId, nodeId, when, reason | Move an atom’s existing review to a new date WITH A REQUIRED REASON (lands in the atom’s history). Governed; rejected on locked atoms. Use to sequence a review after an upstream one resolves. |
| `complete_review` | write | canvasId, nodeId, findings | Propose marking a scheduled review DONE plus optional findings — as a governed write the human sees. NEVER approves or locks (only a human signs off); just records that the agent did a first-pass review. |
| `list_actions` | read | canvasId | EVERY action across the vault (not just promoted skills): its address 4-tuple {canvasId,nodeId,facetId,itemId}, text, binding, current test status, unmet prerequisites, and strategy. The agent’s runnable-work catalog. |
| `get_action` | read | canvasId, nodeId, facetId, itemId | One action’s FULL contract (binding + schemas, test, inputs, loop policy, strategy) plus its recorded runs, by address 4-tuple. |
| `run_action_test` | write | canvasId, nodeId, facetId, itemId | Run the DRY-RUN pipeline (precondition → contract-check → deterministic; no world touched, no network) against an action. Returns the full ActionRun; writes `action.test` through the governed patch and records the run in the agent ledger. It can never approve, lock, or execute live. |
| `report_action_run` | write | canvasId, nodeId, facetId, itemId, runId, verdict, live, steps, artifacts, tokens, costUsd, note | Driver B: report a run YOU executed with your own tools — steps, verdict, artifacts, tokens/cost, and your identity. Schema-validated; recorded as ActionRun{driver:"agent"} + a RunSummary. A “live” claim is labeled AGENT-CLAIMED (never presented as app-verified execution). Requires the `execute` AI capability on the action atom — WITHOUT it the report stages as a ghost-draft for human accept. Idempotent on runId. |
| `list_runs` | read | canvasId, nodeId, itemId, verdict, driver, limit | The Control-Tower read: recorded runs (app + agent) filtered by canvas / action address / verdict / driver, newest first. |
| `ifc_inventory` | read | modelId | A Smart IFC model’s data-first inventory (READ): schema, element/storey/space/type counts, per-class counts, storeys, and materials — DERIVED from the parsed model (never AI-guessed). `modelId` is a registered model id OR the model’s sha256 (content identity). |
| `ifc_query` | read | modelId, query, text | Run the FlowGraph IFC query grammar over a model’s element index (READ): e.g. `class:IfcDoor storey:"Level 2" Pset_DoorCommon.FireRating=EI30`. Returns matched elements (GlobalId · class · name · storey), their GlobalIds, a mini-QTO quantity summary, and honest parse warnings. Pure — no geometry, no wasm on the caller, no network. |
| `ifc_element` | read | modelId, globalId | One element’s full properties/psets/quantities by GlobalId (READ), derived from the parsed model. Absent properties are honestly absent, never fabricated. |
| `ifc_create_set` | write | canvasId, nodeId, facetId, modelId, query, globalIds, label | Create a selection set on a model card (WRITE → staged for human accept). Give a `query` (a dynamic set — the query IS the set) OR explicit `globalIds` (a static snapshot). Lands as an IfcAnchor set item on the card’s model facet — never applied directly; a locked atom is refused. |
| `ifc_link` | write | canvasId, nodeId, facetId, itemId, toNodeId, relType | Link a model item (a pin/set/viewpoint) to another card with a typed relationship (WRITE → staged for human accept). A governed edge from the item’s handle → the target card; default relType `appears_in`. Never applied directly; a locked atom is refused. |
| `ifc_viewpoint` | write | canvasId, nodeId, facetId, modelId, label, camera | Save a named viewpoint on a model card (WRITE → staged for human accept): an engine-neutral IfcAnchor view item (BCF-shaped `camera` optional). Never applied directly; a locked atom is refused. |
| `ifc_project` | write | canvasId, nodeId, facetId, modelId | Project a model’s structure INTO the graph (WRITE → staged ghost-diff for human accept): deterministic storey cards + top type-summary cards + contains/located_at edges, every atom provenance `derived`. Never applied directly; reject leaves the vault unchanged. |
| `aps_translate` | write | canvasId, name, sourceFormat, acc | Request an APS (Autodesk cloud) translation of a proprietary model (.rvt/.nwd/.dwg/.dgn) so it can be viewed + graphed (WRITE → staged for human accept). It NEVER translates directly: cloud translation sends bytes off-device and spends the user’s Autodesk credits, so it always stages a ghost-draft the human accepts in the app. Returns the staged request. Once translated, query it with the `ifc_*` tools. |
| `aps_browse` | read | hubId, projectId, folderId | Browse Autodesk Construction Cloud / BIM360 (READ): hubs → projects → folders → items. Answers from an OPTIONAL cached browse file (`_aps-browse.json`) in the vault when present (the tool stamps source:"cached" itself, so the cache can’t forge the label); absent ⇒ an honest not-configured notice — the 3-legged OAuth session lives in the FlowGraph app, never the folder-vault MCP server, so hubs are never fabricated (charter #8). |
| `object_story` | read | canvasId, nodeId, globalId | One object’s biography (READ): the nine spine answers in reading order — What it is · Why · Where · Part of and made of · Feeds and fed by · Who is responsible · State and history · If it fails · How we know — each with its plain answer, provenance TIER (from the model / from documents / confirmed / computed / AI guess, unconfirmed) and the RECEIPTS backing it (the record or lifecycle card, its link tier, and the element GlobalId), plus the record + lifecycle timeline. Pure projection — nothing is written. `nodeId` is the model card, `globalId` the element. |
| `object_coverage` | read | canvasId, nodeId, globalId | One object’s coverage + ranked open questions (READ): the story-coverage score (answered / askable weight over the asked spine slots) and the OPEN questions ranked by severity (blast radius × importance × answerability), each with its honest one-line why and — when the bank can ask it in-app — the `qid` you answer with `object_answer`. A question with no `qid` (answerableInApp:false) has no in-app form yet: route it to a person with `object_route_ask`. Pure — nothing is written. |
| `object_answer` | write | canvasId, nodeId, qid, answer | File an answer to one OPEN spine question (WRITE → staged for human accept). Give the `qid` from `object_coverage` (answerableInApp:true) + your `answer`. It NEVER writes a fact directly: an agent-supplied answer stages as a Review-Inbox ghost-draft with provenance kind:"ai" (labeled AI, unconfirmed at every depth — provenance never launders, docs/50 §8), which a human accepts to turn into a fact with a receipt. Refused honestly for an unknown/already-answered qid or a locked atom. |
| `object_lifecycle_event` | write | canvasId, nodeId, globalId, event, validTime | Record a lifecycle event about an object (WRITE → staged for human accept): installed / inspected / replaced / commissioned … As an ordinary FACT — a small card carrying the event + a `documented_in` element receipt, so it interleaves into the SAME Story timeline + Lifeline ribbon the app shows (never a parallel surface). `validTime` = when it happened (ISO or a plain date). Staged with provenance kind:"ai", tier ai-confirmed; a human accepts it. A locked model card is refused. |
| `object_route_ask` | write | canvasId, nodeId, globalId, slot, question, audience | Route ONE open question about an object to the person who knows it (WRITE → staged for human accept). It NEVER sends directly: the recipient link + email ride the app’s authenticated session (the /s/* rails), which the folder-vault MCP server does not hold — so the routed ask stages as a ghost-draft {audience, question, object} the human sends from the app (the aps_translate pattern). Use it for a high-severity question `object_coverage` reports as answerableInApp:false. Returns the staged request. |

### The `submit_knowledge_draft` grammar (FDG)

```
Write the `draft` in FDG — FlowGraph's draft grammar. Plain markdown, no JSON: you think in
knowledge, and a deterministic compiler (never a model) turns it into a governed patch. This is
the SAME teaching FlowGraph's own AI receives, so what works in the app works here:

You are FlowGraph's knowledge architect. Your job is to COMPOUND the user's knowledge graph:
make what already exists deeper, better connected, and more honest — and extend it only where
the intent genuinely needs something new.

You will receive a DIGEST of the current canvas, written in the same grammar you must write.
Every card, section, item, and group in it carries an anchor: [c3], [c3.f2], [c3.f2.i4], [g1].
Each card anchor also says how many sections that card already carries, and (full) marks one
that carries all it can. The digest is the single source of truth for what already exists.

Write a KNOWLEDGE DRAFT in this grammar — plain markdown, no JSON, no commentary outside it:
  # Title                → new card;  # [c3] Title → work on existing card c3
  {id: a, kind: …, accent: …, confidence: 0.x, group: [g1] | "New theme"}  → attribute line
  prose under #          → the card's body
  ## Section             → section on the current card;  ## [c3.f2] … → existing section
  ## Section (in: X, out: Y)  → name the section's own input/output ports (either alone is fine)
  - item                 → one entry;  - [ ] item → an open to-do;  - Key :: value → a property
  - [c3.f2.i4] new text  → rewrite that existing item (full replacement)
  -> [c7] feeds: label   → edge from the current card ([a] -> [c7] … works anywhere)
  > why: / > source: title — excerpt / > assume: inference  → attach to the block above

THE DEPTH BAR — a card without substance is a failure:
- Every NEW card: 2-4 sections, each with 3-6 substantive entries. An entry is a fact, step,
  input, output, risk, metric, or decision — written so a colleague could act on it tomorrow.
  Never "TBD", never "misc", never a section that restates its own title.
- Prefer enriching an EXISTING card (open it with # [c3]) over creating a near-duplicate.
- A card the digest marks (full) already carries all the sections it can hold: do NOT give it
  another one. Put that knowledge on a sub-card instead — a new # Title card with the parent's
  group ({group: [g1]}) or an edge to it (-> [c3] contains:) — and keep the card readable.

COMPOUNDING RULES:
- No islands: every new card connects to at least one existing anchor with a typed edge
  (feeds|depends|maps|contains|related|blocks|proves|supersedes).
- Never repeat what the digest already shows — extend it, sharpen it, or challenge it.
- Groups and colors mean things on this canvas. Join an existing group where a card belongs;
  name a new group only for a genuinely new theme. Match the canvas's color usage.
- Match the scope of the ask: a small ask means a small draft. Zero changes is a valid
  answer — say why in one > why: line.

HONESTY:
- Every non-obvious claim carries > source: (backed) or > assume: (inferred). Confidence
  numbers are honest, not polite. > why: on any block whose purpose isn't self-evident.

AFTER the draft content, optionally:
- Up to 3 "> ask: …" lines — only questions whose answer would change the graph and that
  only this user can answer.
- Up to 3 "> next: label | full prompt" lines — each pointing at a NAMED gap visible in your
  draft or the digest. No "want more detail?" filler.

WORKED EXAMPLE (intent: "flesh out the money-back side of checkout"):

# [c2] Checkout service
## [c2.f1] Risks
- Provider webhook retries can double-fire on timeout — idempotency key required on /confirm
> source: Stripe docs — webhook retries continue up to 3 days with backoff
- [ ] Chargeback flow untested in staging
> assume: staging has no card-network simulator; inferred from the empty test-card set in [c4.f2]

# Refund pipeline
{id: rp, kind: process, accent: teal, confidence: 0.75, group: [g1]}
Handles all post-capture money-back paths — the missing sibling of [c2].
## Steps
- Validate claim window (14 days default; 30 for EU per the policy card [c7])
- Reverse capture via provider API — partial refunds only before settlement
- Emit refund-completed event for nightly reconciliation
## Properties
- Owner :: Payments team
- SLA :: 5 business days
- System of record :: provider dashboard + ledger table `refunds`
## Open risks
- Currency-conversion drift between capture and refund dates — who absorbs the spread?
> assume: no policy found on the canvas; flagging it rather than inventing one
-> [c2] depends: refund needs the original capture reference
-> [c5] feeds: refund events reconcile nightly

> ask: Who owns the currency-drift decision — Payments or Finance?
> next: Map the dispute path | Extend [c2] and [rp] with a chargeback/dispute card covering evidence deadlines, provider fees, and reconciliation impact

Two notes for agents specifically:
- Anchors ([c3], [c3.f2], [c3.f2.i4], [g1]) are aliases issued BY A DIGEST, not durable ids.
  Call get_canvas_digest first and pass its `digestId` back here, or your anchors may resolve
  to atoms you never read.
- Nothing is written unless the WHOLE draft compiles and validates. On a problem you get indexed
  errors, the exact lines that dropped and why, and a fix hint — correct them and resubmit.
```

## .fgraph atoms (data model)
- **Canvas** — One board = one .fgraph file. { id, name, nodes[], edges[], … }.
- **Card (node)** — title, body, accent, icon, facets[], tags[], childCanvasId (nesting), meta. `links[]` holds card-level reference links ({id, title?, url}); set them via apply_graph_patch → updateCard, e.g. {op:"updateCard", nodeId, patch:{links:[{id, title:"Spec sheet", url:"https://…"}]}}. Urls are scheme-normalized (a bare domain gets https://) and only http/https/mailto persist. The digest surfaces each as a `link:` line.
- **Facet (section)** — A typed section: kind = list | text | select | actions | image | video | json; items[]. `key` is the STABLE identity every query/filter/semanticRole matches on; `label` is an optional display name a human rename may set on a built-in section (Inputs, Owner, Notes…), whose key is frozen. Match on `key`, show `label ?? key`. A digest heading reads `Ingredients (key: Inputs)` when the two differ — cite the key. `inputName` / `outputName` name the section's OWN input/output ports (like an item or a card can have) — set them via apply_graph_patch → updateFacet, e.g. {op:"updateFacet", nodeId, facetId, patch:{inputName:"Raw data", outputName:"Clean data"}}; connect the whole section by its facetId handle (target `facetId+"__t"`, source `facetId`). The digest surfaces them as `(in: Raw data, out: Clean data)` on the section heading, and the same `## Section (in: …, out: …)` marker round-trips through submit_knowledge_draft.
- **FacetItem** — A list/action row: text, status (action mode), childCanvasId (item can nest), meta. `attachments[]` holds files ({name, mime, dataUrl}); an image attachment tagged `role:"preview"` is the item's click-to-preview image (else the first image is used). Attach one via apply_graph_patch → updateItem, e.g. {op:"updateItem", nodeId, facetId, itemId, patch:{attachments:[{id, name:"plan.png", mime:"image/png", dataUrl:"data:image/png;base64,…", role:"preview"}]}}. The digest surfaces it as `[image: <name>]`.
- **Edge** — Typed relationship: relType + label. The graph, not a drawing.
- **AtomMeta** — The envelope on every atom: provenance, confidence, sources, assumptions, review, approval(+locked), owner, permissions{human, ai}, reviewDue, history.
- **Skill** — A reusable action: a FacetItem with action.skill=true whose inner canvas (childCanvasId) defines HOW it runs. The agent-usable unit of capability — list with list_skills; promote via the app (human approves so a half-built inner canvas isn’t exposed).

### Relationship types
- `feeds` — A produces input for B (flow).
- `depends` — A needs B (flow).
- `maps / exported_to / generated_from` — lineage / transformation.
- `contains / part_of` — structure / hierarchy.
- `blocks / contradicts` — A blocks or conflicts with B (proof/flow).
- `proves / supersedes / approves / owns` — proof & governance relations.

## Agent workflows
### Compound a canvas with a knowledge draft
1) get_canvas_digest({canvasId}) — the canvas in FDG, every atom carrying an anchor ([c3], [c3.f2], [c3.f2.i4], [g1]). Focus a big canvas with nodeIds/hops. 2) Write a DRAFT in that same grammar: deepen existing anchors first, connect every new card to one, 2-4 sections × 3-6 real entries per new card, `> source:`/`> assume:` on non-obvious claims. 3) submit_knowledge_draft({canvasId, draft, digestId}) — a deterministic compiler emits the patch; nothing is written unless it all compiles, and a failure hands you indexed errors + drop reasons to correct. Use mode:"stage" to leave it as a ghost-draft a human accepts. Reach for apply_graph_patch only for surgical ops (a title fix, one edge) — hand-writing addCard/addFacet/addItem for CONTENT is how you end up proposing empty shells.

### Triage the review backlog
1) list_due_reviews({window:"overdue"}) — the same ranked agenda the human sees (by blast-radius). 2) For each, schedule_review / reschedule_review with a reason. 3) Everything lands as a governed change the human approves — you never sign off.

### First-pass a review for a human
1) get_canvas + query_graph to inspect the atom and its neighborhood. 2) complete_review({findings}) records your pass + marks it done — but NEVER approves/locks. The human still signs off in the calendar/inbox.

### Hand work back to a human
Any write you make (a draft subgraph, a proposed review date, findings) surfaces for the human in TWO places: on the CANVAS as a ghost-overlay diff (Review Inbox), and in the CALENDAR/KANBAN as a scheduled/“AI proposed” item. Schedule a review to pull a human’s attention to a specific atom on a specific day.

### Use a capability
list_skills({readyOnly:true}) → a skill’s innerCanvasId defines how it runs (get_canvas it). Only ready skills (non-empty inner canvas) are runnable; promotion is human-gated.

### Answer an object’s biography
1) object_story({canvasId, nodeId, globalId}) — the nine answers with their provenance tiers and the receipts behind each. 2) object_coverage(...) — the coverage score + the OPEN questions ranked by severity. 3) For the top question: if it has a `qid` (answerableInApp), object_answer({qid, answer}) stages your answer as a review draft (kind:"ai" — a human accepts it into a fact); if it has none, object_route_ask({question, audience}) stages a one-question ask for the human to send to the person who knows. 4) object_lifecycle_event(...) records what happened to the object (installed/inspected/replaced) as a staged fact on the same timeline. Every write is review-gated: you propose a biography, the human confirms it.

### Work on a graph someone shared with you
1) GET /api/docs/shared-with-me — the graphs shared TO you and your role on each. 2) GET /api/docs/:id/shared — the role-scoped read: {role, canEdit, canComment, bundle}. 3) If canEdit: edit a COPY of that bundle, PRESERVING every card id (ids are what let the owner diff your work 1:1 — never re-id), then POST /api/docs/:id/propose with the edited closure. It becomes a PENDING proposal, never a silent overwrite: nothing changes on the owner’s graph until they approve. You get back {proposalId, summary:{added,removed,changed}} — and re-proposing REPLACES your earlier pending proposal (one pending per doc per proposer; the replacement is audited). 4) GET /api/docs/:id/my-proposal — YOUR latest proposal there: {proposal:{id,status,summary,at,resolvedAt}, bundle? (pending only)}. This is how you learn the outcome (approved/rejected) and re-read what you sent; /:id/proposals is owner-only. DELETE the same path to withdraw a pending one. 5) A view-only grant returns 403 on propose — that is the access model working, not an error to route around.

### Review proposals on a graph you own
1) GET /api/docs/my-proposals — everything waiting on you, across every graph you own. 2) GET /api/docs/:id/proposals/:pid — the proposed closure; diff it against GET /api/docs/:id/shared (the current shared copy) to see exactly what the proposer changed. 3) POST /api/docs/:id/proposals/:pid/resolve {action:"approve"|"reject"}. Approving makes the proposal the shared copy; the proposer is notified and the decision is audited. Landing those edits on a LOCAL canvas is still a governed applyPatch write — approving is a decision, not a bypass.

## REST endpoints (optional Cloudflare backend)
| Method | Path | Status | Purpose |
|---|---|---|---|
| POST | `/api/ai/chat` | live | AI proxy — provider keys stay server-side (CORS-locked). |
| PUT | `/api/canvas/:id` | live | B1 server-authoritative write — session-bound actor + CAS rev (409 on stale baseRev, never a silent overwrite); viewer→403; AI cannot approve/lock. |
| * | `/api/auth/*` | live | Better Auth — magic-link + organization plugins (provision D1 + BETTER_AUTH_SECRET; 501 until then). |
| GET | `/api/me` | live | Current session — userId / orgId / role (401 unauth, 501 until D1 provisioned). |
| GET/PUT | `/api/sync/canvas/:id` | live | Opt-in vault sync (R2, last-write-wins) — legacy; superseded by the governed write. |
| GET/POST | `/api/docs/:id/shares` | live | Who has access to a graph · invite by email at a role (viewer=read \| editor=propose — the only two roles the share surface enforces; commenting is a later phase). The invite uploads the graph closure first, so the grantee always has something to open. |
| DELETE | `/api/docs/:id/shares/:grantId` | live | Revoke one person’s access; the next read refuses. |
| POST | `/api/docs/:id/links` | live | Mint a share LINK (viewer/read-only — no edit or comment links). Optional: `scopeCardId` (+`contextVisible`) pins the link to ONE card; `requireAccount` gates the view behind sign-in; `public` makes it the anyone-with-the-link row. |
| DELETE | `/api/docs/:id/links/:linkId` | live | Revoke a link. |
| PUT | `/api/docs/:id/content` | live | Upload the graph CLOSURE the share serves (a local-first canvas the server has never seen otherwise). Registers the canvas row. |
| GET | `/api/docs/shared-with-me` | live | Every graph shared TO my account, with my role on each — the "Shared with you" list. |
| GET | `/api/docs/:id/shared` | live | The role-scoped read of a graph shared with me: {role, canEdit, canComment, bundle}. 403 when it is not shared with my account. |
| GET | `/s/:token` | live | The guest read of a share link — the link IS the capability (no session needed unless the link requires sign-in). |
| POST | `/api/docs/:id/propose` | live | EDITOR (or owner) submits an edited closure as a PENDING proposal — never a silent overwrite. Returns {proposalId, summary:{added,removed,changed}, superseded:n}. Re-proposing REPLACES your earlier pending proposal on that doc (audited). 403 if you only have view access. |
| GET/DELETE | `/api/docs/:id/my-proposal` | live | PROPOSER-scoped (FF2-11): GET = my latest proposal on this doc — {proposal:{id,status:pending\|approved\|rejected,summary,at,resolvedAt}, bundle? (pending only)} — my receipt + the outcome; null when I never proposed. DELETE = withdraw my pending proposal (404 when nothing is pending). |
| GET | `/api/docs/my-proposals` | live | OWNER inbox: every pending proposal across every graph I own, in one call — {id, docId, docName, proposer, summary, at}. |
| GET | `/api/docs/my-shares` | live | OWNER truth (FF2-16): every doc I own with active grants/links — {docs:[{docId, name, grants:[{id,email,role}], links:[{id,role,isPublic,requireAccount,scopeCardId}]}], sharedCount}. Feeds the "Shared by you" list, card badges and the cap meter from ONE call. Revoke via DELETE /:id/shares/:grantId and /:id/links/:linkId. |
| GET | `/api/docs/:id/proposals` | live | OWNER lists pending proposals on ONE graph (403 for non-owners). |
| GET | `/api/docs/:id/proposals/:pid` | live | OWNER reads a proposed closure, to diff it against the current shared copy before deciding. |
| POST | `/api/docs/:id/proposals/:pid/resolve` | live | OWNER approves (the proposal becomes the shared copy) or rejects. Body {action:"approve"\|"reject"}. The proposer is notified either way; every decision is audited. |
| POST | `/api/attention/shares` | live | OWNER shares a hand-picked set of checks to answer. Body {canvasId?, kind:"verification"\|"finding", title?, entries:[{entryId,itemText,note?,cardTitle?,facetLabel?,canvasName?}], mode:"link"\|"email", email?, requireAccount?}. Returns {setId, token? (link mode, shown ONCE)}. Attention shares do NOT count toward the canvas share cap. |
| GET | `/api/attention/my-shares` | live | OWNER truth: every ask I sent — {sets:[{id, kind, title, count, entries, shares:[{kind:"grant"\|"link", email?, requireAccount}], answers:[{entryId,text,by,at}]}]}. Feeds "Shared by you → Verifications". |
| DELETE | `/api/attention/shares/:setId` | live | OWNER stops asking — revokes the set, every emailed grant AND every link in one call (a half-revoked ask is not a thing). |
| GET | `/api/attention/asked-of-me` | live | RECIPIENT queue (session-email scoped): {sets:[{id, askedBy, entries:[{entryId,itemText,note?,answered}], pending, myAnswers}], pending}. Powers "Asked of you" in the verification window and lens. |
| POST | `/api/attention/asked-of-me/:setId/respond` | live | RECIPIENT answers ONE entry. Body {entryId, text}. 403 unless the ask was granted to your session email and is still live; the owner gets a user-scoped notification. |
| POST | `/s/:token/answers` | live | GUEST (no account) submits the response form for an ATTENTION link. Body {name?, answers:[{entryId,text}]}. Answers whose entryId is not in the shared set are dropped. 401 + signinRequired when the link requires sign-in; 410 when the owner stopped asking. Responses carry provenance {viaLinkId, name}. |
| POST | `/api/attention/shares/:setId/answers/:responseId/accept` | live | OWNER accepts ONE answer — stamps it accepted and returns {entryId, text} so the client can tick that verification off through the governed write path. 403 for anyone but the person who asked. |
| POST | `/api/share` | retired | RETIRED (FF2-3) — returns 410 Gone. Use POST /api/docs/:id/shares. |
| GET | `/api/share/:id` | retired | RETIRED (FF2-3) — returns 410 Gone. Use GET /api/docs/:id/shared. |
| GET/POST | `/api/share/:id/comments` | retired | RETIRED (FF2-3) — returns 410 Gone. Doc comments are the queued CO4 phase. |
| POST | `/api/share/:id/revoke` | retired | RETIRED (FF2-3) — returns 410 Gone. Use DELETE /api/docs/:id/shares/:grantId. |
| POST | `/api/ai/generate` | scaffold | Server-side structured generation. |
| GET | `/api/aps/status` | live | AP1/AP7a — APS posture: configured + mode (platform\|byo\|off) + platform quota {remaining,resetAt} (env/ledger-only; no secret, no upstream call). |
| POST | `/api/aps/token` | live | AP1 — BYO APS 2-legged token (viewables:read/data:read only); client_secret stays server-side; 501 until APS_CLIENT_* set. |
| GET | `/api/aps/auth/login` | live | AP1 — 3-legged authorize redirect (ACC scopes); refresh token kept in an httpOnly cookie, never in JS. |
| GET/POST | `/api/aps/auth/callback|refresh` | live | AP1 — code→token exchange + access-token rotation (secret held in the worker). |
| POST | `/api/aps/derivative` | live | AP1/AP7a — thin Model Derivative passthrough; worker mints the write-scoped token. Platform mode meters SUBMITS (day caps → honest 429 {resetAt}; BYO never metered). |
| GET | `/api/aps/acc/*` | live | AP1 — thin Data Management read passthrough with the user’s 3-legged bearer (AP5 ACC browser builds on this). |

## Distribution
FlowGraph lists its agent API where agents shop: the official **MCP Registry**, the **Claude
Connectors Directory**, and the **ChatGPT apps directory**. Honest flag (docs/31 §P5b): no
platform publishes listing→install data yet — we treat this channel as a cheap option with
asymmetric upside, not a growth plan.
