diff --git a/bench/render.py b/bench/render.py
index 29ff3830..331c8202 100644
--- a/bench/render.py
+++ b/bench/render.py
@@ -176,8 +176,8 @@ def clean_count(case: dict, tool: str) -> int:
"",
f'graft vs git, p50 ms (lower is faster)',
'teal graft, grey git. '
- "1k: vogt-prod, seed 2294, 5 iterations. "
- "10k: graft-dev, seed 2294, 5 iterations. "
+ "1k host A: 24 CPUs, 62.6 GiB, seed 2294, 5 iterations. "
+ "10k host B: 4 vCPUs, 7.8 GiB, seed 2294, 5 iterations. "
"Fresh CLI process per step.",
f'',
]
diff --git a/changes/WI-2542.md b/changes/WI-2542.md
new file mode 100644
index 00000000..b96607fd
--- /dev/null
+++ b/changes/WI-2542.md
@@ -0,0 +1 @@
+Added a user glossary and git-to-graft concept table to the guide.
diff --git a/changes/WI-2544.md b/changes/WI-2544.md
new file mode 100644
index 00000000..e766d434
--- /dev/null
+++ b/changes/WI-2544.md
@@ -0,0 +1 @@
+Added a complete MCP registration and tool reference, enforced by a test that every registered tool is documented.
diff --git a/changes/WI-2545.md b/changes/WI-2545.md
new file mode 100644
index 00000000..01bd70cc
--- /dev/null
+++ b/changes/WI-2545.md
@@ -0,0 +1 @@
+Document server configuration, authorization, administration, health, and schema upgrades.
diff --git a/changes/WI-2546.md b/changes/WI-2546.md
new file mode 100644
index 00000000..4f1651a0
--- /dev/null
+++ b/changes/WI-2546.md
@@ -0,0 +1 @@
+- Add a Git migration guide covering lossless import, GitHub follow, batch pull-request delivery, full export, and the advanced authority switch.
diff --git a/changes/WI-2547.md b/changes/WI-2547.md
new file mode 100644
index 00000000..42037fc9
--- /dev/null
+++ b/changes/WI-2547.md
@@ -0,0 +1,2 @@
+Added a concise guide FAQ covering interoperability, offline use, lines,
+conflicts, CI, git export, performance, licensing, and supported platforms.
diff --git a/changes/WI-2548.md b/changes/WI-2548.md
new file mode 100644
index 00000000..c8f1cb97
--- /dev/null
+++ b/changes/WI-2548.md
@@ -0,0 +1 @@
+- Benchmark documentation describes the measured hosts by CPU and memory instead of estate-specific names, and the agent guide now documents `graft serve --dev` as available.
diff --git a/crates/graft-mcp/tests/guide.rs b/crates/graft-mcp/tests/guide.rs
new file mode 100644
index 00000000..b6c3db36
--- /dev/null
+++ b/crates/graft-mcp/tests/guide.rs
@@ -0,0 +1,23 @@
+//! Documentation coverage for the public MCP tool registry.
+
+use std::fs;
+
+#[test]
+fn mcp_guide_lists_every_registered_tool() {
+ let guide = fs::read_to_string(
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/guide/mcp.md"),
+ )
+ .expect("read docs/guide/mcp.md");
+
+ let missing: Vec<&str> = graft_mcp::TOOLS
+ .iter()
+ .map(|tool| tool.name)
+ .filter(|name| !guide.contains(&format!("### `{name}`")))
+ .collect();
+
+ assert!(
+ missing.is_empty(),
+ "docs/guide/mcp.md is missing registered MCP tools: {}",
+ missing.join(", ")
+ );
+}
diff --git a/docs/agents.md b/docs/agents.md
index 1593c235..57b1096c 100644
--- a/docs/agents.md
+++ b/docs/agents.md
@@ -13,7 +13,7 @@ export GRAFT_AUTHOR_NAME="Name" GRAFT_AUTHOR_EMAIL="name@example"
The token is a bearer credential. Never print it, never pass it as an argument, never copy it into the checkout or a prompt. `graft login` copies it into a private token file and stores only that file's path in the user config. For command overrides, endpoint and token resolve independently in this order: `--endpoint` / `--token-file`, `GRAFT_ENDPOINT` / `GRAFT_TOKEN`, the working copy's named server, then the user's `default_server`.
-`graft serve --dev` (a local server with a dev token) is also coming in v0.1.0.
+`graft serve --dev` starts a local server with a dev token.
## The loop
diff --git a/docs/guide/bench.svg b/docs/guide/bench.svg
index 20c0cbe6..12eedf3b 100644
--- a/docs/guide/bench.svg
+++ b/docs/guide/bench.svg
@@ -14,7 +14,7 @@
}
graft vs git, p50 ms (lower is faster)
-teal graft, grey git. 1k: 24-CPU host, seed 2294, 5 iterations. 10k: graft-dev, seed 2294, 5 iterations. Fresh CLI process per step.
+teal graft, grey git. 1k host A: 24 CPUs, 62.6 GiB, seed 2294, 5 iterations. 10k host B: 4 vCPUs, 7.8 GiB, seed 2294, 5 iterations. Fresh CLI process per step.
1,000 commits
diff --git a/docs/guide/benchmarks.md b/docs/guide/benchmarks.md
index 87602b63..eb55d5cf 100644
--- a/docs/guide/benchmarks.md
+++ b/docs/guide/benchmarks.md
@@ -11,8 +11,8 @@ CLI process for every step (graft and git alike):
| Scale | File | Host | graft commit |
| --- | --- | --- | --- |
-| 1,000 commits | `bench/results/2026-10-23-scales-10-1000/results.json` | 24 CPUs, 62.6 GiB | `35c308759e8f` |
-| 10,000 commits | `bench/results/2026-10-23-scale-10000/results.json` | graft-dev, 4 vCPU, 7.8 GiB | `2c32be2d39a3` |
+| 1,000 commits | `bench/results/2026-10-23-scales-10-1000/results.json` | host A: 24 CPUs, 62.6 GiB | `35c308759e8f` |
+| 10,000 commits | `bench/results/2026-10-23-scale-10000/results.json` | host B: 4 vCPUs, 7.8 GiB | `2c32be2d39a3` |
Bars are p50 milliseconds. Lower is faster. Teal is graft, grey is git.
Operations: full clone, sparse clone, `log -50`, `log --path`, blame,
diff --git a/docs/guide/concepts.md b/docs/guide/concepts.md
new file mode 100644
index 00000000..f5e80ab0
--- /dev/null
+++ b/docs/guide/concepts.md
@@ -0,0 +1,119 @@
+# Graft concepts for git users
+
+Graft is a server-centric source control system. A working copy contains the
+files you edit, while the server owns history, workspaces, drafts, lines, and
+the operations that move work between them.
+
+## Glossary
+
+### Repository
+
+A repository is a named namespace on a server. It contains the repository's
+objects, changesets, refs, workspaces, and journal.
+
+### Object
+
+An object is an immutable, content-addressed value. A blob holds file bytes;
+a tree describes one directory.
+
+### Changeset
+
+A changeset is the unit of work. It names its parents, records file changes
+against its first parent, and identifies the resulting root tree.
+
+### Draft
+
+A draft is a changeset that no line has reached yet. `graft commit` creates a
+draft from inline content or the working copy, and a workspace holds it while
+it is active.
+
+### Landed
+
+A changeset is landed when it is on a line's first-parent history. Landed
+changesets remain part of that line's history.
+
+### Line
+
+A line is a protected ref, such as `main` or `release/1.4`, that moves forward
+through landed changesets. `graft line create` forks a new line at a changeset
+and never overwrites an existing line.
+
+### Ref
+
+A ref is a named pointer to a changeset. Lines are movable refs; tags are
+immutable refs.
+
+### Tag
+
+A tag is an immutable name for a changeset. `graft tag create` creates it once
+and never moves it.
+
+### Workspace
+
+A workspace is a named, owned, leased server record with a base and a head. It
+is the server-side twin of a working copy; `graft workspace` can show, list,
+open, renew, or close workspaces.
+
+### Stack
+
+A stack is the drafts between a workspace's base and head, oldest first.
+`graft stack` lists, squashes, reorders, splits, or drops those drafts.
+
+### Journal
+
+The journal is the repository's append-only, sequence-numbered record of
+writes. Watchers and mirrors follow it without scanning repository history.
+
+### Base
+
+The base is the landed changeset on a line that a workspace starts from.
+`graft sync` moves the workspace base to the line tip and rebases its stack.
+
+### Head
+
+The head is the newest draft in a workspace, or the base when the stack is
+empty. Working-copy status is measured against the workspace head.
+
+### Land
+
+To land is to put the workspace stack on a line. `graft land` rebases the
+stack on the server and moves the line only if its expected version still
+matches.
+
+### Sync
+
+To sync is to move the workspace base to the line tip. The server rebases the
+stack, and the client updates the working-copy files.
+
+### Line merge
+
+A line merge brings one line into another and records a merge point between
+them. `graft line merge` uses a three-way merge and writes unresolved
+conflicts into the working copy for resolution.
+
+### Conflict
+
+A conflict is a path that cannot be merged automatically. Text conflicts use
+markers in the file; other conflicts use a `.graft-conflict` sidecar until the
+workspace records a resolution.
+
+## Git to graft
+
+Graft keeps familiar command names where their meaning is close, but its
+server-owned workspaces and drafts replace git's local-history workflow.
+
+| Git term | Graft term | What differs |
+| --- | --- | --- |
+| Repository | Repository | A remote repository is a named namespace on one server, not a collection of peer clones and remotes. |
+| Commit | Changeset; `graft commit` creates a draft | The changeset is created on the server and is visible immediately; there is no later push of a private local commit. |
+| Branch | Line for shared history; workspace stack for work in progress | Lines are few protected refs. Ongoing work stays as leased drafts rather than short-lived branches. |
+| Tag | Tag | A graft tag is an immutable ref to a changeset and never moves. |
+| Working tree | Working copy | The files are the client-side view of a server workspace, often limited to selected path prefixes. |
+| Index / staging area | None | `graft commit` records the working copy's changes, or only the paths named by the caller. |
+| Push | `graft land` | Land rebases the workspace stack on the server and advances the line with a compare-and-swap. |
+| Pull / fetch | `graft sync`; object fetch | Sync moves the workspace base to the line tip and updates files. Object fetch retrieves named content rather than negotiating histories. |
+| Merge | Land, or line merge | Ordinary work lands by server-side rebase. A line merge integrates one line into another and records a merge point. |
+| Rebase | Server-side rebase during sync or land | The server rebases the workspace stack; the client resolves only paths that cannot merge automatically. |
+| Cherry-pick | `graft pick` | Pick creates a draft that re-applies a landed changeset; conflicts are written into the working-copy files. |
+| Revert | `graft revert` | Revert creates a draft that inverts a changeset, ready to land like any other draft. |
+| Pull request | Review of a workspace stack before land | Graft itself has no pull-request object or branch-per-change requirement; review can use changesets, diffs, and workspace metadata directly. |
diff --git a/docs/guide/faq.md b/docs/guide/faq.md
new file mode 100644
index 00000000..5999cd5a
--- /dev/null
+++ b/docs/guide/faq.md
@@ -0,0 +1,67 @@
+# Frequently asked questions
+
+## Why is graft not compatible with git on the wire?
+
+Graft is server-centric: the server owns workspaces, drafts, rebases, history,
+and landing. It therefore does not speak the git wire protocol or store git
+objects; familiar command names are used only where their meanings remain
+close.
+
+## Can I use graft offline?
+
+A working copy of a remote repository has a bounded cache, so reads of cached
+objects continue to work while disconnected. Remote writes wait for the server;
+for fully local, single-user work, `graft init` runs the engine in the client
+process and needs no network connection.
+
+## Are lines the same as branches?
+
+Lines are a small set of shared, protected histories rather than a feature
+branch for every change. Work in progress lives in a leased workspace as a
+stack of drafts, and landing rebases that stack on the server before moving the
+line with compare-and-swap.
+
+## How do changes move between lines?
+
+`graft line merge` brings one line into another with a three-way merge and
+records a merge point between them. The source line is read, not moved, and an
+unresolved merge is returned to the working copy for resolution.
+
+## How do conflicts reach me?
+
+Graft reports conflicting paths and leaves the shared line unchanged. After
+`graft sync` or `graft land --sync`, text conflicts appear as markers in the
+files and other conflicts use `.graft-conflict` sidecars, so they can be
+resolved and committed from the working copy.
+
+## Where does CI run?
+
+In phase one, graft runs no build, test, check, or checker anywhere, including
+the land path. For git-primary repositories, CI stays on the forge and runs on
+the batch pull request that carries landed changes back to git.
+
+## Can I go back to git?
+
+Yes. `graft mirror export` writes the full history back to git, and import keeps
+raw tag objects so migration does not discard them; deterministic full export
+is the exit path, not an afterthought.
+
+## Is graft faster than git?
+
+It depends on the operation and scale. Recorded results show graft ahead on
+full and sparse clone, path log, blame, and some concurrent lands, but behind
+on recent log, status, and sync; see the [benchmark results](benchmarks.md) for
+the measurements and limitations.
+
+## What does AGPL-3.0-only mean for an internal server?
+
+The licence is AGPL-3.0-only; if users interact over a network with your
+modified version, those users must be offered its corresponding source. See
+the [FSF licence FAQ](https://www.gnu.org/licenses/gpl-faq.html#AGPLv3InteractingRemotely)
+for the rule and consult your own counsel for legal advice.
+
+## Which platforms are supported?
+
+The documented release archives target Linux on x86_64 and aarch64, plus macOS
+on Apple Silicon. There is no Intel macOS archive, and no Windows archive is
+currently documented.
diff --git a/docs/guide/git-migration.md b/docs/guide/git-migration.md
new file mode 100644
index 00000000..673ae0cb
--- /dev/null
+++ b/docs/guide/git-migration.md
@@ -0,0 +1,147 @@
+# Migrate a Git repository gradually
+
+Graft can sit beside an existing GitHub repository without changing where the
+repository is authoritative. In the recommended setup for this release, Git
+remains the authority: maintainers continue to protect and merge the default
+branch in GitHub, while `graft-mirror` follows that branch into a graft line.
+Work may land on a separate graft integration line and leave graft as a batch
+pull request.
+
+The examples below use these placeholders:
+
+- `/srv/git/project.git` is a local bare clone of the GitHub repository.
+- `project` is the graft repository name.
+- `main` is both the GitHub default branch and its followed graft line.
+- `integration` is the graft line that accepts native lands.
+- `/etc/graft/mirror.toml` is the mirror configuration file.
+- `/run/secrets/graft-mirror-token` is a file containing the graft token.
+
+## Import once
+
+Start with a bare Git repository that has the branches and tags you want to
+preserve. Import its default branch into the corresponding graft line:
+
+```sh
+graft-mirror import /srv/git/project.git \
+ --repo project \
+ --server https://graft.example.com \
+ --db /var/lib/graft-mirror/project.sqlite \
+ --token-file /run/secrets/graft-mirror-token \
+ --line refs/heads/main=main
+```
+
+Import preserves the reachable commit graph, tags, commit headers, modes, and
+the mapping between Git commit SHAs and graft changesets. It performs a
+fidelity check for every commit: exporting an imported commit must reproduce
+the original SHA. A commit with a path that graft cannot represent, such as a
+non-UTF-8 or non-portable path, stops the import and reports the offenders. If
+an operator deliberately accepts a known exception, repeat `--allow-lossy
+` for those commits; otherwise do not bypass the fidelity check.
+
+## Follow the GitHub default branch
+
+Create a mirror configuration. Keep credentials in token files, not in this
+file or on the command line:
+
+```toml
+server = "https://graft.example.com"
+repo = "project"
+upstream = "https://github.com/example/project.git"
+mirror_dir = "/var/lib/graft-mirror/project.git"
+db = "/var/lib/graft-mirror/project.sqlite"
+token_file = "/run/secrets/graft-mirror-token"
+poll_interval_secs = 15
+
+[github]
+owner = "example"
+repo = "project"
+token_file = "/run/secrets/github-token"
+
+[export]
+default_author = "Graft Export "
+
+[[lines]]
+branch = "main"
+line = "main"
+
+[[lines]]
+branch = "graft/integration"
+line = "integration"
+```
+
+Run the follower as a long-lived service. It fetches GitHub and advances the
+configured graft lines when their Git branches move:
+
+```sh
+graft-mirror follow --config /etc/graft/mirror.toml
+```
+
+The followed `main` line remains Git-primary. Do not land directly on it;
+GitHub remains the place where its protected default branch moves.
+
+## Deliver lands as batch pull requests
+
+Land work on the graft `integration` line. To publish its pending lands, sync
+them onto the followed GitHub base and create or update one batch pull request:
+
+```sh
+graft-mirror pr-batch integration \
+ --base main \
+ --config /etc/graft/mirror.toml
+```
+
+Run the command again when more lands are ready. Each land is exported as one
+commit in the batch. `graft-mirror` creates or updates the pull request but
+never merges it. Review and merge that pull request with your normal process;
+afterward, `follow` imports the resulting GitHub history and settles the batch.
+
+Graft runs no build, test, check, checker, or CI job when work lands. Required
+checks run in the user's forge on the batch pull request, and the user's merge
+process decides when the pull request may enter the protected branch.
+
+## Export the complete repository
+
+A full export is the exit path. It writes every line, tag, and landed
+changeset that graft can reach into a bare Git repository:
+
+```sh
+graft-mirror export \
+ --to /srv/git/project-export.git \
+ --repo project \
+ --db /var/lib/graft-mirror/project.sqlite \
+ --server https://graft.example.com \
+ --token-file /run/secrets/graft-mirror-token
+```
+
+Keep the mirror database with the graft backup while migration is in progress:
+it contains the imported Git headers and SHA mapping used for exact round trips.
+The exported bare repository can be checked with ordinary Git tools and pushed
+to a forge if you decide to leave graft.
+
+## Advanced: switch authority
+
+Switching a line from Git-primary to graft-primary is an advanced operation and
+is not recommended in this release. It changes which system may accept writes,
+so schedule it as an operator-controlled migration rather than a routine part
+of onboarding.
+
+Inspect the current state, freeze writes, verify recent history, flip the
+authority, and unfreeze only after the new authority is ready:
+
+```sh
+graft-mirror authority status --config /etc/graft/mirror.toml --line main
+graft-mirror authority freeze --config /etc/graft/mirror.toml --line main
+graft-mirror authority verify --config /etc/graft/mirror.toml --line main --last 100
+graft-mirror authority flip --config /etc/graft/mirror.toml --line main
+graft-mirror authority unfreeze --config /etc/graft/mirror.toml --line main
+```
+
+After a successful switch, the continuous exporter is the graft-primary bridge
+back to GitHub:
+
+```sh
+graft-mirror export --config /etc/graft/mirror.toml
+```
+
+Do not skip or reorder the authority steps. If verification finds divergence,
+leave the line frozen and resolve it explicitly before flipping authority.
diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md
new file mode 100644
index 00000000..c2648c76
--- /dev/null
+++ b/docs/guide/mcp.md
@@ -0,0 +1,255 @@
+# MCP guide
+
+Graft's MCP server exposes bounded source-control reads and writes to agents.
+Every result is capped at 64 KiB. List and patch tools may return less than the
+requested limit to stay under that cap.
+
+## Register the server
+
+From a Graft working copy, print the registration for your agent client:
+
+```sh
+graft mcp --print-config claude
+graft mcp --print-config codex
+graft mcp --print-config cursor
+```
+
+The generated registration starts `graft mcp` over stdio. It never prints a
+token. The token is read from Graft's configured server, a configured token
+file, or `GRAFT_TOKEN`; it is never a tool argument.
+
+An agent without a checkout can connect by streamable HTTP to the Graft
+server's `/mcp` endpoint. Configure that URL and its bearer token in the MCP
+client rather than passing a token to any tool. HTTP provides repository and
+workspace operations, while working-copy-only behavior is available only from
+`graft mcp` over stdio inside a working copy.
+
+## Results, paging, and errors
+
+- Results are structured JSON. Tool arguments reject unknown fields.
+- When a result has `next`, call the same tool again with that value as
+ `cursor`. Continue until `next` is `null`; a short page is not the last page.
+- `graft_read` uses `next_offset`: pass it back as `offset` until it is `null`.
+- Patches are capped. Each text patch is at most 4 KiB and reports
+ `truncated: true` when clipped; binary changes have a null patch.
+- Errors are structured and include `kind` and `message`. API errors also
+ report `retryable`. Common kinds include `invalid_argument`, `not_found`,
+ `unauthenticated`, `forbidden`, `stale_base`, `conflict`, `contended`,
+ `unavailable`, and `needs_working_copy`. A `conflict` result identifies the
+ target and paths to resolve; `stale_base` means the line moved and the
+ workspace must be synchronized before retrying.
+
+## Read tools
+
+### `graft_log`
+
+Reads first-parent history, optionally restricted to paths.
+
+- Arguments: `repo` and `from` are required; `until`, `paths`, `limit` (1-100,
+ default 50), and `cursor` are optional.
+- Result: `changesets` contains bounded changeset summaries and `next` is the
+ resume token. Keep paging with `cursor` while `next` is set.
+- Errors: invalid refs or paths use `invalid_argument` or `not_found`.
+
+### `graft_show`
+
+Reads one changeset and its first-parent patch.
+
+- Arguments: required `repo` and `changeset`; optional `limit` (1-100, default
+ 20) and `cursor`.
+- Result: `changeset`, `files`, and `next`. File entries include `path`, `op`,
+ `kind`/`id` for puts, `patch`, and `truncated`. Keep paging with `cursor`.
+- Errors: an unknown changeset is `not_found`.
+
+### `graft_diff`
+
+Diffs two snapshots, optionally for selected paths.
+
+- Arguments: required `repo`, `from`, and `to`; optional `paths`, `patch`
+ (default true), `limit` (1-100, default 20), and `cursor`.
+- Result: `files` and `next`; file entries use the same capped patch shape as
+ `graft_show`. Keep paging with `cursor`.
+- Errors: invalid snapshots or paths are typed errors.
+
+### `graft_blame`
+
+Reads line origins for a bounded window of one file.
+
+- Arguments: required `repo`, `at`, and `path`; optional zero-based
+ `start_line` and exclusive `end_line` (at most 1,000 lines by default).
+- Result: file identity and a `lines` array with line ranges and originating
+ changesets. Request another range to continue.
+- Errors: missing or non-text files return a typed error.
+
+### `graft_read`
+
+Reads a bounded byte range from one file.
+
+- Arguments: required `repo`, `at`, and `path`; optional `offset` (default 0)
+ and `limit` (1-65,536 bytes, default 16,384).
+- Result: `path`, `kind`, `encoding`, `data`, `offset`, and `next_offset`.
+ Continue by passing `next_offset` as `offset` until it is null.
+- Errors: missing paths, invalid offsets, and directories are typed errors.
+
+### `graft_ls`
+
+Lists a directory at a snapshot.
+
+- Arguments: required `repo` and `at`; optional `path` (root by default),
+ `recursive` (default false), `limit` (1-500, default 100), and `cursor`.
+- Result: `entries` with path, kind, and object id, plus `next`. Keep paging
+ with `cursor`.
+- Errors: a missing directory or invalid path is typed.
+
+### `graft_status`
+
+Reads the current working copy's repository, workspace, and changed paths.
+
+- Arguments: required `repo`.
+- Result: working-copy repository/workspace identity and a capped changed-file
+ summary.
+- Errors: `needs_working_copy` outside stdio `graft mcp`, or when the requested
+ repository is not the current working copy.
+
+### `graft_upstream`
+
+Shows how far a workspace is behind its line and predicts whether its work
+merges cleanly onto the tip.
+
+- Arguments: required `repo`; optional `workspace` (defaults to the current
+ working copy), `limit` (1-100, default 50), and `cursor`.
+- Result: `line`, `workspace`, `base`, `head`, `tip`, `up_to_date`, `behind`,
+ `changed`, `overlaps`, `stack_conflict`, `clean`, and `next`. Keep paging the
+ `changed` list with `cursor`.
+- Errors: `workspace` is required outside a working copy; missing workspaces
+ and refs are typed errors.
+
+### `graft_draft_overlap`
+
+Finds other live workspaces whose open drafts touch files or directory
+prefixes you plan to change. It never returns file contents.
+
+- Arguments: required `repo` and `paths` (1-64); optional `line` (default
+ `main`), `include_own` (default false), `limit` (1-200, default 50), and
+ `cursor`.
+- Result: `line`, grouped `overlaps` with workspace, owner, session, work item,
+ draft age, lease and paths, plus `next`. Keep paging with `cursor`.
+- Errors: requires the repository drafts scope; otherwise returns `forbidden`.
+
+## Workspace and draft tools
+
+### `graft_workspace`
+
+Opens, reads, renews, or closes a server-side workspace.
+
+- Arguments: required `repo` and `action` (`open`, `get`, `renew`, `close`).
+ `name` is required except when opening; `line`, `at`, and `lease_ms` apply to
+ open/renew, and `keep_drafts` applies to close.
+- Result: the workspace view (`name`, `line`, `base`, `head`, owner,
+ attribution, and lease), or close confirmation.
+- Errors: invalid action-specific arguments, missing workspaces, or contention
+ are typed errors.
+
+### `graft_commit`
+
+Creates a draft on a workspace from inline file changes or current
+working-copy changes.
+
+- Arguments: required `repo`, `workspace`, and `message`; `files` contains up
+ to 256 `{path, op, content, encoding, kind}` entries and at most 1 MiB of
+ content. `from_working_copy` selects current local changes. Optional `parent`
+ and `authored_at_ms` make retries deterministic.
+- Result: draft changeset id, `parent`, and `authored_at_ms`.
+- Errors: invalid content/path data, `needs_working_copy`, `stale_base`, or
+ workspace errors are typed.
+
+### `graft_sync`
+
+Rebases a workspace stack onto its line tip or a named target.
+
+- Arguments: required `repo` and `workspace`; optional `onto`.
+- Result: updated workspace/base/head and any rebased draft ids.
+- Errors: `conflict` reports the target and conflicting paths; contention and
+ missing refs are typed.
+
+### `graft_land`
+
+Lands a workspace stack by compare-and-swap onto one line.
+
+- Arguments: required `repo` and `workspace`; optional `head`, `line`,
+ `squash`, merge mode (`line` default or `file`), `check` (default true), and
+ `merged_from` for a resolved line merge.
+- Result: landed changeset id, line, previous tip, and pre-land summary.
+- Errors: `stale_base` means synchronize and retry; `conflict` supplies paths;
+ `contended` is retryable. The pre-land check never runs a build or test.
+
+### `graft_conflicts`
+
+Previews a workspace stack's conflicts against a target without moving refs.
+
+- Arguments: required `repo` and `workspace`; optional `onto`, `limit` (1-64,
+ default 16), and `cursor`.
+- Result: target, expected workspace head, `conflicts` with bounded markers,
+ and `next`. Keep paging with `cursor`.
+- Errors: invalid targets and workspace state are typed.
+
+### `graft_resolve`
+
+Rebases a conflicted stack using caller-provided resolutions.
+
+- Arguments: required `repo`, `workspace`, `onto`, and `resolutions` using the
+ same file-change shape as `graft_commit`; optional `expected_head` protects
+ the update with compare-and-swap.
+- Result: updated workspace and rebased draft ids.
+- Errors: incomplete or stale resolutions return `conflict` or `stale_base`.
+
+### `graft_revert`
+
+Creates a draft that inverses a changeset.
+
+- Arguments: required `repo` and `changeset`; optional `onto` and `workspace`.
+- Result: new draft changeset id and workspace information.
+- Errors: overlapping inverse changes return `conflict`; unknown changesets
+ and invalid workspace state are typed.
+
+### `graft_pick`
+
+Creates a draft that reapplies a landed changeset.
+
+- Arguments: required `repo` and `changeset`; optional `onto`, `workspace`,
+ and merge mode (`line` default or `file`).
+- Result: new draft changeset id and workspace information.
+- Errors: overlapping changes return `conflict`; unknown changesets and
+ invalid workspace state are typed.
+
+## Ref tools
+
+### `graft_line_create`
+
+Creates a new line at a changeset without overwriting an existing line.
+
+- Arguments: required `repo`, `name`, and `from`.
+- Result: line name and target changeset.
+- Errors: an existing line is `already_exists`; invalid names or refs are
+ typed.
+
+### `graft_merge`
+
+Merges one line into another while recording a merge point.
+
+- Arguments: required `repo` and source line `src`; optional target `into`
+ (default `main`), source `at`, `message`, and merge mode (`line` default or
+ `file`).
+- Result: landed changeset, source and target line information, and merge
+ point.
+- Errors: conflicts return `conflict`; stale or contended target updates are
+ typed and never move two refs at once.
+
+### `graft_tag`
+
+Creates an immutable tag at a changeset.
+
+- Arguments: required `repo`, `name`, and `at`; `tags/` is added when absent.
+- Result: tag name and target changeset.
+- Errors: an existing tag is `already_exists`; invalid names or refs are
+ typed. Tags never move.
diff --git a/docs/guide/server.md b/docs/guide/server.md
new file mode 100644
index 00000000..bfce01b7
--- /dev/null
+++ b/docs/guide/server.md
@@ -0,0 +1,233 @@
+# Server guide
+
+This page covers a Graft server's local-development start, production
+configuration, authentication, repository and line administration, operational
+endpoints, and database upgrades.
+
+## Quick start
+
+For a loopback-only development server, run:
+
+```sh
+graft serve --dev
+```
+
+The command creates a `dev` repository, chooses a free loopback port, stores the
+generated administrator token in the user configuration, and prints a `graft
+clone` command. Its data defaults to `./.graft-serve` and its signing-key state
+to `./.graft-serve-state`. Use `--repo`, `--listen`, `--data`, `--state`, or
+`--server` to override those defaults. A non-loopback `--listen` also requires
+`--insecure-bind`.
+
+For a configured server, `graft serve` runs the production server role. It reads
+`--config `, then `GRAFT_SERVER_CONFIG`, then
+`/etc/graft/server.toml`.
+
+## Configuration reference
+
+The configuration is TOML. Unknown keys and invalid combinations are rejected
+at startup. Optional values shown as `none` are absent when omitted; required
+values have no default and must be present when their table is configured.
+
+A minimal production configuration is:
+
+```toml
+listen = "127.0.0.1:7878"
+metrics = "127.0.0.1:9464"
+objects_dir = "/var/lib/graft/objects"
+meta_db = "/var/lib/graft/meta.db"
+key_file = "/var/lib/graft/signing.key"
+json_logs = true
+
+[[lines]]
+name = "main"
+policy = "direct"
+```
+
+### Server keys
+
+| Key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `listen` | string | `"127.0.0.1:7878"` | Listener for gRPC, the JSON facade, MCP, metrics, and health endpoints. |
+| `metrics` | string or none | none | Optional second listener serving only metrics and health endpoints. |
+| `objects_dir` | path | `"/var/lib/graft/objects"` | Filesystem object-store root. |
+| `meta_db` | path | `"/var/lib/graft/meta.db"` | SQLite metadata database. |
+| `key_file` | path or none | none | Ed25519 signing-key seed. When omitted, `signing.key` beside `meta_db`; created on first use with mode `0600`. |
+| `min_free_fraction` | number | `0.05` | Writes are refused when either storage volume's free fraction is lower. |
+| `min_free_bytes` | integer | `0` | Writes are refused when either storage volume has fewer free bytes. |
+| `json_logs` | boolean | `true` | Emit JSON logs instead of plain text. |
+| `otlp_endpoint` | string or none | none | OTLP/HTTP trace endpoint. |
+| `max_token_ttl_s` | integer | `43200` | Maximum lifetime, in seconds, for tokens minted through the API. |
+| `shutdown_grace_s` | integer | `10` | Graceful-shutdown wait for in-flight calls, in seconds. |
+| `max_watch_wait_ms` | integer | `60000` | Maximum idle wait requested by one watch call, in milliseconds. |
+| `max_watch_lifetime_s` | integer | `3600` | Maximum watch-stream lifetime, also bounded by token expiry. |
+| `max_watches_per_principal` | integer | `16` | Concurrent watch streams allowed per principal. |
+| `max_watches` | integer | `1024` | Concurrent watch streams allowed server-wide. |
+| `max_put_call_bytes` | integer | `67108864` | Maximum frame bytes buffered by one `PutObjects` call; cannot exceed 80 MiB. |
+| `put_buffer_bytes` | integer | `536870912` | Shared frame-byte budget across in-flight `PutObjects` calls. |
+| `put_wait_ms` | integer | `10000` | Maximum wait for shared put-buffer capacity; at most one hour. |
+| `put_idle_ms` | integer | `30000` | Maximum gap between messages in one `PutObjects` call; at most one hour. |
+| `ci` | table | defaults below | CI identity-exchange configuration. |
+| `lines` | array of tables | `[]` | Per-line land-policy overrides. With no override, lines use `direct`. |
+| `default_line` | table or none | none | Policy for lines not named by `lines`; it has no `name` key. |
+| `gc` | table | defaults below | Scheduled garbage-collection configuration. |
+
+### `[gc]`
+
+| Key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `gc.enabled` | boolean | `false` | Run the sweep scheduler. Deletion must also be enabled per repository. |
+| `gc.interval_s` | integer | `3600` | Seconds between scheduled passes. |
+| `gc.repos` | array of strings | `[]` | Repositories swept by the scheduler. |
+| `gc.grace_s` | integer | `900` | Grace between marking an object doomed and deleting it; minimum 60 seconds. |
+| `gc.retention_s` | integer | `604800` | Retention for abandoned and superseded drafts, in seconds. |
+| `gc.verify` | boolean | `true` | Verify the plan against the ancestry walk before each pass. |
+
+### `[ci]`, `[[ci.issuers]]`, and `[[ci.bindings]]`
+
+With no issuer configured, CI token exchange is disabled.
+
+| Key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `ci.max_token_ttl_s` | integer | `900` | Maximum exchanged-token lifetime; also capped by the source token. |
+| `ci.issuers` | array of tables | `[]` | Trusted CI issuers. |
+| `ci.bindings` | array of tables | `[]` | Mappings from CI repositories to Graft repositories. |
+| `ci.issuers[].provider` | `github` or `woodpecker` | required | CI provider. |
+| `ci.issuers[].issuer` | string | required | Exact expected issuer claim. |
+| `ci.issuers[].audience` | string | required | Deployment-specific expected audience. |
+| `ci.issuers[].jwks_file` | path or none | none | JWKS file read at startup; mutually exclusive with `jwks_url`. |
+| `ci.issuers[].jwks_url` | string or none | none | HTTPS JWKS URL, or HTTP on loopback; mutually exclusive with `jwks_file`. |
+| `ci.issuers[].jwks_refresh_s` | integer | `3600` | Refresh interval for URL-backed keys. |
+| `ci.issuers[].leeway_s` | integer | `60` | Clock-skew allowance for `nbf` and `iat`; maximum 300 seconds. |
+| `ci.issuers[].max_lifetime_s` | integer | `3600` | Maximum accepted `exp - iat`. |
+| `ci.bindings[].provider` | `github` or `woodpecker` | required | Provider whose identities the binding accepts. |
+| `ci.bindings[].ci_repo` | string | required | CI repository in `owner/name` form. |
+| `ci.bindings[].repo_id` | string or none | none | Numeric CI repository ID that must match when set. |
+| `ci.bindings[].graft_repo` | string | required | Graft repository available to the job. |
+| `ci.bindings[].refs` | array of strings | `[]` | Allowed refs; an empty list allows any ref. |
+| `ci.bindings[].workflows` | array of strings | `[]` | Allowed workflows; GitHub bindings require a workflow restriction. |
+| `ci.bindings[].scopes` | array of strings | required | One or more `read` or `attest` scopes on `graft_repo`. |
+
+### `[[lines]]`, `[default_line]`, and path rules
+
+`[[lines]]` entries require `name`. `[default_line]` accepts the same keys
+except `name`.
+
+| Key | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `lines[].name` | string | required | Line receiving this policy. Not valid in `[default_line]`. |
+| `lines[].policy` | `direct`, `attested`, or `queued` | `direct` | Land policy. Only `direct` is enabled in this release; the others are parked. |
+| `lines[].checks` | array of strings | `[]` | Required check names for non-direct policies. A direct line takes no checks. |
+| `lines[].attest_merged` | boolean | `false` | Require attestations over merged changes; valid only with `attested`. |
+| `lines[].authority` | `graft-primary` or `git-primary` | `graft-primary` | Which side of a mirrored line accepts writes. |
+| `lines[].path_rules` | array of tables | `[]` | Prefix-specific merge and parked check/review rules. |
+| `lines[].post_land` | table or none | none | Parked post-land policy. |
+| `lines[].path_rules[].prefix` | string | required | Repository path prefix matched by the rule. |
+| `lines[].path_rules[].checks` | array of strings | `[]` | Parked path-specific checks. |
+| `lines[].path_rules[].human_review` | boolean | `false` | Parked path-specific human-review requirement. |
+| `lines[].path_rules[].merge` | `line`, `none`, or omitted | omitted | `line` performs three-way line merge; `none` makes every concurrent change conflict. Omission uses the engine's normal merge behavior. |
+| `lines[].post_land.checks` | array of strings | `[]` | Parked checks run by a post-land policy. |
+| `lines[].post_land.auto_revert` | boolean | `false` | Parked automatic revert after a failed post-land check. |
+
+Policies other than `direct`, all check fields, human-review requirements, and
+post-land behavior are not enabled in this release. A direct land never runs a
+build, test, check, or checker. Path merge rules are available independently:
+use `merge = "line"` for bounded text merge or `merge = "none"` for a conflict
+whenever both sides changed a matching path.
+
+## Tokens and scopes
+
+Tokens are signed bearer tokens. Clients read them from a token file or from
+`GRAFT_TOKEN`; command-line token values are not accepted. Each scope is one of:
+
+| Scope | Grants |
+| --- | --- |
+| `repo::read` | Read the repository. |
+| `repo::write` | Write any repository path and read the repository. |
+| `repo::write:` | Write only the prefix and its descendants, and read the repository. |
+| `repo::land:` | Land on one line and read the repository. |
+| `repo::attest` | Post attestations only; it does not grant read. |
+| `repo::drafts` | Read other sessions' open drafts by path only. |
+| `repo::admin` | Perform all repository operations, including attestations and draft-overlap reads. |
+| `objects:read` | Read objects for repositories the caller can read. |
+
+Mint an operator token offline with the server administration command, piping
+stdout directly to a mode-`0600` file or secret store:
+
+```sh
+graft serve --config /etc/graft/server.toml admin token mint \
+ --subject operator --kind human \
+ --scope repo:example:admin --ttl 3600 > token.tmp
+chmod 600 token.tmp
+```
+
+The default administration-command TTL is 30 days. API minting defaults to the
+server's `max_token_ttl_s`, refuses a requested lifetime above that limit, and
+never creates a token that outlives its caller. A non-service caller can mint
+only its own identity and no broader scopes; a service principal may delegate a
+different subject within its own scopes.
+
+Revoke a live token by passing it on standard input, never as an argument:
+
+```sh
+graft serve --config /etc/graft/server.toml admin token revoke < token.tmp
+```
+
+Revocation stores only the token ID and expiry. It takes effect for writes;
+read-only requests continue to accept an otherwise valid revoked token until it
+expires.
+
+## Repositories and lines
+
+Create a repository with the offline administration command:
+
+```sh
+graft serve --config /etc/graft/server.toml admin repo create example
+```
+
+The command is idempotent. With a server running, create a line from an
+existing changeset or ref:
+
+```sh
+graft --repo example line create release --from main
+```
+
+Line creation is compare-and-swap from absence: it never overwrites an existing
+line. Land policy is server-wide by line name, not per repository. In this
+release, use the default `direct` policy; configured `attested` and `queued`
+policies are parked and not enabled.
+
+## Metrics and health
+
+Every main listener serves:
+
+- `GET /healthz`: liveness; returns `ok` while the HTTP service is running.
+- `GET /readyz`: readiness; returns `ready` only after startup is ready to serve.
+- `GET /metrics`: Prometheus text exposition, including a fresh free-space
+ admission sample.
+
+When `metrics` is configured, the second listener serves only those three
+endpoints. Keep it on a trusted network; these endpoints do not require bearer
+authentication.
+
+## Upgrade and schema versions
+
+The current SQLite schema version is **7**, stored in SQLite's `user_version`.
+Opening the metadata database applies each missing migration in order, one
+migration per transaction, before the server begins serving. Existing data is
+preserved across supported forward upgrades. Released migrations are append-only
+and are never edited; a crash leaves the database at either the old version or
+the next version, never between them. A database whose schema version is newer
+than the running binary is refused rather than downgraded.
+
+Back up the metadata database and object store before replacing a server binary.
+Keep the signing key with the deployment: replacing it invalidates existing
+tokens.
+
+## Running with compose
+
+Compose deployment instructions will be added below.
+
+## Backup and restore
+
+Backup and restore instructions will be added below.