This page is the executable spec for the four things every implementation must agree on: identity & URLs, revisions, comments, and the agent surface. Where a real standard already exists we adopt it instead of inventing — W3C Web Annotation for anchors, OAuth device flow for CLI auth, SSE for realtime. Boring on purpose.
Conformance
A tool doesn't need the whole loop to be a citizen. Levels let a static host, a CLI, or a full server all interoperate — and give us a story where other vendors implement Level 0 long before they implement Level 3.
POST /v1/artifacts returns a stable URL + short_id. Content served faithfully.≈ Netlify Drop could do this@vN deep links, restore, machine-readable version list.≈ Gist-grade historybase_version semantics.the loop becomes possibleIdentity & URLs
short_id, foreverAn artifact's permanent identity is its short_id: 6–10 chars, base36, generated server-side, never reused. Slugs are cosmetic and mutable; the short_id is what agents store and what comments reference. URL grammar: https://{org}.{host}/{short_id}[-{slug}][@v{n}]. Anything after short_id- is ignored for routing, so renames never break links.
Every uploaded file is stored under its SHA-256. Two identical publishes share one blob; integrity checks and dedup come free. A version is a pointer (artifact, n) → blob_key, never a copy.
Two kinds, one rule: if it's static, it just works. A file artifact is a single text/html or text/markdown document. A bundle artifact is any folder/zip of static assets — an Astro or Vite build, a Next.js export, Docusaurus, mdBook, an Observable export. index.html is the entry; nested paths serve with correct MIME types; a 200.html (or spa: true on publish) enables client-side routing fallback. Markdown is always also served raw at {url}.md — humans get the rendered page, machines get the source.
Bundle versioning is whole-bundle: a version snapshots the entire tree (content-addressed per file, so unchanged assets dedupe across versions). Per-version asset URLs are immutable — which is also what makes cross-artifact imports safe.
Wire format
| Op | Endpoint | Notes |
|---|---|---|
| POST | /v1/artifacts | Multipart: a file or a zipped bundle + JSON meta (title, visibility, slug?, spa?). Returns artifact object below. |
| POST | /v1/artifacts/:short_id/versions | New version. Body includes message and optional resolves: [comment_ids] — republish and resolve in one call. |
| GET | /v1/artifacts/:short_id | Accept: application/json → metadata + version list. Accept: text/markdown → content read-back (HTML is converted; MD is raw). |
| GET | /v1/artifacts/:short_id/comments?state=open | Threads with anchors, quotes, and base_version. |
| POST | /v1/artifacts/:short_id/comments | Create comment or reply (thread_id present = reply). Agents and humans use the same endpoint. |
The artifact object every call returns or accepts — this is the whole shared vocabulary:
Comments
The hard problem in commenting on documents that get republished is where the comment sticks. The W3C Web Annotation Data Model solved this a decade ago (it's what Hypothes.is runs on). We use its selector vocabulary verbatim, so existing annotation tooling understands our comments for free.
When v2 lands: (1) try CssSelector exact match; (2) try TextQuoteSelector exact text; (3) fuzzy-match quote with context (normalized Levenshtein ≥ 0.8 within prefix/suffix window); (4) else mark orphaned — still visible in the sidebar, pinned to its base_version, one click to view v1 where it anchored. Comments are never silently dropped.
A thread is an append-only list of comments. No edits-in-place across users, no merge conflicts, no CRDT needed. Author can edit/delete own comments (tombstoned, audit-logged). Resolve/reopen are state flips on the thread, by humans or agents. In bundle artifacts every target also carries its path, so feedback works on any page of a multi-page site.
For MD artifacts the same TextQuoteSelector works against the raw source; a line_range hint is stored alongside for editor integrations (jump-to-line in the agent's working file).
Multiplayer
Multiplayer here means: comments and versions appear live, and you can see who's looking. It does not mean co-editing the artifact body — agents republish, humans annotate. That asymmetry is what lets us skip CRDTs/OT and WebSocket infrastructure entirely.
POST /presence every 30s while the tab is open, fanned out on the same stream. Ephemeral, in-memory, lost on restart — by design.Revisions
Versions are a strictly increasing integer per artifact. No forks, no merge — an artifact is a published surface, not a repo. "Restore v2" publishes a new v4 whose blob is v2's (history never rewrites). Branch-like workflows happen upstream in git where they belong.
…/8f3kx9@v1 always renders exactly v1, immutably — what reviews, audits, and comment base_version jumps rely on. The bare URL means "current". ?diff=v1..v2 renders a diff: line-diff for MD, rendered text-diff for HTML (structural HTML diff is explicitly out of scope for v0).
Self-host keeps everything by default (blobs are deduped; cost is trivial). Hosted free tier prunes to last 5 versions, except any version referenced by a comment's base_version — feedback context is never garbage-collected.
Every version records its principal: user:jess@acme.com or agent:claude-code (token-derived, not self-declared). The UI renders agent publishes distinctly — provenance is a feature, not metadata.
Agent surface
| MCP tool | Maps to |
|---|---|
publish_artifact | POST /v1/artifacts — returns URL + short_id for the agent to report to the user |
publish_version | POST …/versions with resolves — the loop-closer |
get_artifact | GET … as Markdown — read what's live, any version |
list_comments | GET …/comments?state=open — structured feedback queue |
reply_comment | POST …/comments — agents can discuss, not just resolve |
Ships as a published MCP server + a Claude Code / Codex skill that wraps the CLI. Same five verbs everywhere.
dock login prints a code, opens the browser, polls for grant — works headless and inside containers. Result is a scoped token in ~/.config/dock. CI uses org-issued service tokens (DOCK_TOKEN env var). No passwords ever touch the CLI.
Tokens carry org + optional artifact-prefix scope and a kind (user-delegated vs service). An agent token can publish and read comments; it cannot change visibility, delete artifacts, or read other artifacts unless granted. Revocation is instant (tokens are DB rows, checked per request — at this traffic, no JWT statelessness needed).
comment.created / version.published POSTed with HMAC signature — so a GitHub Action can wake an agent when feedback arrives, instead of polling.
The spec lives in the open repo as spec/APP-0.md with a JSON Schema + OpenAPI file and a conformance test suite (npx dock-conformance <base-url>). Versioned independently of the product; changes by RFC. The test suite is the standard — if it passes, you're conformant, whoever you are.