From spec-first
Creates, shares, views, comments on, edits, and runs human-in-the-loop reviews of markdown documents via Proof editor at proofeditor.ai. Use for collaborative iteration on specs, plans, drafts with local file sync.
npx claudepluginhub sunrain520/spec-firstThis skill is limited to using the following tools:
Proof is a collaborative document editor for humans and agents. It supports two modes:
Create, edit with comments/suggestions/rewrites, and share collaborative markdown documents via Proof web API or macOS local bridge.
Programmatically edits .docx Word documents with live preview, track changes, insertions, deletions via SuperDoc VS Code extension. Use for redlining contracts, markup, or tracked edits when DOCX files are mentioned.
Guides users through 3-stage workflow for co-authoring documentation: context gathering, iterative refinement, and reader testing for proposals, specs, and decision docs.
Share bugs, ideas, or general feedback.
Proof is a collaborative document editor for humans and agents. It supports two modes:
Every write to a Proof doc must be attributed. Two fields carry the agent's identity:
by on every op, X-Agent-Id header): ai:spec-first — stable, lowercase-hyphenated, machine-parseable. Appears in marks, events, and the API response.name on POST /presence): Spec-First — human-readable, shown in Proof's presence chips and comment-author badges.Set the display name once per doc session by posting to presence with the X-Agent-Id header; Proof binds the name to that agent ID for the session. These values are the defaults for any caller of this skill; callers running HITL review (references/hitl-review.md) may pass a different identity pair if a distinct sub-agent should own the doc. Do not use ai:compound or other ad-hoc variants — identity stays uniform unless a caller explicitly overrides it.
Human-in-the-loop iteration over an existing local markdown file: upload to Proof, let the user annotate in Proof's web UI, ingest feedback as in-thread replies and tracked edits, and sync the final doc back to disk. Two entry points, identical mechanics — load references/hitl-review.md for the full loop spec (invocation contract, mark classification, idempotent ingest passes, exception-based terminal reporting, end-sync atomic write) in either case:
spec-brainstorm, spec-ideate, or spec-plan finishes a draft and hands it off for human review before the next phase, passing the file path and title explicitly.No authentication required. Returns a shareable URL with access token.
curl -X POST https://www.proofeditor.ai/share/markdown \
-H "Content-Type: application/json" \
-d '{"title":"My Doc","markdown":"# Hello\n\nContent here."}'
Response format:
{
"slug": "abc123",
"tokenUrl": "https://www.proofeditor.ai/d/abc123?token=xxx",
"accessToken": "xxx",
"ownerSecret": "yyy",
"_links": {
"state": "https://www.proofeditor.ai/api/agent/abc123/state",
"ops": "https://www.proofeditor.ai/api/agent/abc123/ops"
}
}
Use the tokenUrl as the shareable link. The _links give you the exact API paths.
curl -s "https://www.proofeditor.ai/api/agent/{slug}/state" \
-H "x-share-token: <token>"
All operations go to POST https://www.proofeditor.ai/api/agent/{slug}/ops
Note: Use the /api/agent/{slug}/ops path (from _links in create response), NOT /api/documents/{slug}/ops.
Authentication for protected docs:
x-share-token: <token> or Authorization: Bearer <token>?token=xxx or the accessToken from create responseX-Agent-Id: ai:spec-first (required for presence; include on ops for consistent attribution)Wire-format reminder. /api/agent/{slug}/ops uses a top-level type field; /api/agent/{slug}/edit/v2 uses an operations array where each entry has op. Do not mix — sending op to /ops returns 422.
Every mutation requires a baseToken. Reuse the mutationBase.token from the most recent /state or /snapshot read — tokens don't go stale in seconds, and STALE_BASE is a recoverable error. On BASE_TOKEN_REQUIRED or STALE_BASE, re-read and retry once. Only do a pre-mutation read if no prior read has happened in this session. See the baseToken recipe in references/hitl-review.md.
/edit/v2 block refs are a separate concern: they can drift across revisions, so re-fetch /snapshot for fresh refs before a block edit if any writes have landed since your last snapshot.
Retry discipline after mutation errors — verify before retrying. An error response is not proof that nothing was written.
STALE_BASE, BASE_TOKEN_REQUIRED, MISSING_BASE, INVALID_BASE_TOKEN — pre-commit, token-related. Re-read /state and retry once with the same payload and a fresh baseToken. A generic mutate helper can auto-retry these.ANCHOR_NOT_FOUND, ANCHOR_AMBIGUOUS — pre-commit, but the quote no longer uniquely matches content. Re-reading does not help by itself; the caller must tighten or regenerate the anchor before retrying. Do not auto-retry blindly.INVALID_OPERATIONS, INVALID_REQUEST, INVALID_REF, INVALID_BLOCK_MARKDOWN, INVALID_RANGE, INVALID_MARKDOWN, 422 — pre-commit, but the payload is wrong. Do not retry blindly; fix the payload first.COLLAB_SYNC_FAILED, REWRITE_BARRIER_FAILED, PROJECTION_STALE, INTERNAL_ERROR, 5xx, network timeout, and any 202 with collab.status: "pending" — the canonical doc may have been written even though the call looks like a failure. Before any retry, re-read /state and check whether the intended mark/edit is already present; only retry if it isn't.Idempotency-Key (see below) protects against double-apply on the same request (e.g., TCP-level retry). It does not help if you build a new request body and send a second call — that is a new logical write with a new key.Duplicate-mark incidents usually come from retrying a comment.add or suggestion.add after a timeout without verifying. When in doubt: re-read, diff, then decide.
Idempotency-Key header is recommended on every mutation for safe automation retries; required when /state.contract.idempotencyRequired is true. Use the same key when retrying the exact same logical write (same payload) so the server can collapse the retry. A new key means a new write — even if the payload is identical.
Comment on text:
{"type": "comment.add", "quote": "text to comment on", "by": "ai:spec-first", "text": "Your comment here", "baseToken": "<token>"}
Reply to a comment:
{"type": "comment.reply", "markId": "<id>", "by": "ai:spec-first", "text": "Reply text", "baseToken": "<token>"}
Resolve / unresolve a comment:
{"type": "comment.resolve", "markId": "<id>", "by": "ai:spec-first", "baseToken": "<token>"}
{"type": "comment.unresolve", "markId": "<id>", "by": "ai:spec-first", "baseToken": "<token>"}
Suggest a replacement (pending — user must accept/reject):
{"type": "suggestion.add", "kind": "replace", "quote": "original text", "by": "ai:spec-first", "content": "replacement text", "baseToken": "<token>"}
Suggest and immediately apply (tracked but committed — user can reject to revert):
{"type": "suggestion.add", "kind": "replace", "quote": "original text", "by": "ai:spec-first", "content": "replacement text", "status": "accepted", "baseToken": "<token>"}
status: "accepted" creates the suggestion mark and commits the change in one call. The mark persists as an audit trail with per-edit attribution and a reject-to-revert affordance. Works with kind: "insert" | "delete" | "replace".
Accept or reject an existing suggestion:
{"type": "suggestion.accept", "markId": "<id>", "by": "ai:spec-first", "baseToken": "<token>"}
{"type": "suggestion.reject", "markId": "<id>", "by": "ai:spec-first", "baseToken": "<token>"}
suggestion.resolve is not supported — use accept or reject instead.
Bulk rewrite (whole-doc replacement):
{"type": "rewrite.apply", "content": "full new markdown", "by": "ai:spec-first", "baseToken": "<token>"}
Block-level edits via /edit/v2 (separate endpoint, separate shape):
curl -X POST "https://www.proofeditor.ai/api/agent/{slug}/edit/v2" \
-H "Content-Type: application/json" \
-H "x-share-token: <token>" \
-H "X-Agent-Id: ai:spec-first" \
-H "Idempotency-Key: <uuid>" \
-d '{
"by": "ai:spec-first",
"baseToken": "mt1:<token>",
"operations": [
{"op": "replace_block", "ref": "b3", "block": {"markdown": "Updated paragraph."}},
{"op": "insert_after", "ref": "b3", "blocks": [{"markdown": "## New section"}]}
]
}'
Per-op body shape (singular block vs plural blocks is load-bearing — sending the wrong one returns 422):
| op | body fields |
|---|---|
replace_block | ref, block: {markdown} |
insert_after | ref, blocks: [{markdown}, ...] |
insert_before | ref, blocks: [{markdown}, ...] |
delete_block | ref |
replace_range | fromRef, toRef, blocks: [{markdown}, ...] |
find_replace_in_block | ref, find, replace, occurrence: "first" | "all" |
Read /snapshot to get stable block ref IDs. operations commits atomically — either every op lands or none do — so one /edit/v2 call can batch dozens of block edits safely and efficiently (see the bulk-sweep guidance in references/hitl-review.md Phase 2.4).
Editing while a client is connected is fine. /edit/v2, suggestion.add (including status: "accepted"), and all comment ops work during active collab. Only rewrite.apply is blocked by LIVE_CLIENTS_PRESENT — it would clobber in-flight Yjs edits.
When the loop breaks. If a mutation keeps failing after a fresh read and one retry, or state across reads looks inconsistent, call POST https://www.proofeditor.ai/api/bridge/report_bug with the failing request ID, slug, and raw response. The server enriches and files an issue.
/d/{slug}/bridge/*) require client version headers (x-proof-client-version, x-proof-client-build, x-proof-client-protocol) and return 426 CLIENT_UPGRADE_REQUIRED without them. Use /api/agent/{slug}/ops instead.Requires Proof.app running. Bridge at http://localhost:9847.
Required headers:
X-Agent-Id: claude (identity for presence)Content-Type: application/jsonX-Window-Id: <uuid> (when multiple docs open)| Method | Endpoint | Purpose |
|---|---|---|
| GET | /windows | List open documents |
| GET | /state | Read markdown, cursor, word count |
| GET | /marks | List all suggestions and comments |
| POST | /marks/suggest-replace | {"quote":"old","by":"ai:spec-first","content":"new"} |
| POST | /marks/suggest-insert | {"quote":"after this","by":"ai:spec-first","content":"insert"} |
| POST | /marks/suggest-delete | {"quote":"delete this","by":"ai:spec-first"} |
| POST | /marks/comment | {"quote":"text","by":"ai:spec-first","text":"comment"} |
| POST | /marks/reply | {"markId":"<id>","by":"ai:spec-first","text":"reply"} |
| POST | /marks/resolve | {"markId":"<id>","by":"ai:spec-first"} |
| POST | /marks/accept | {"markId":"<id>"} |
| POST | /marks/reject | {"markId":"<id>"} |
| POST | /rewrite | {"content":"full markdown","by":"ai:spec-first"} |
| POST | /presence | {"status":"reading","summary":"..."} |
| GET | /events/pending | Poll for user actions |
thinking, reading, idle, acting, waiting, completed
When given a Proof URL like https://www.proofeditor.ai/d/abc123?token=xxx:
abc123) and token from the URL# Read once — the same response yields both the doc content and the baseToken for every mutation below.
STATE=$(curl -s "https://www.proofeditor.ai/api/agent/abc123/state" \
-H "x-share-token: xxx")
BASE=$(printf '%s' "$STATE" | jq -r '.mutationBase.token')
# Inspect doc fields as needed: printf '%s' "$STATE" | jq '.markdown, .revision'
# Comment
curl -X POST "https://www.proofeditor.ai/api/agent/abc123/ops" \
-H "Content-Type: application/json" \
-H "x-share-token: xxx" \
-H "X-Agent-Id: ai:spec-first" \
-d "$(jq -n --arg base "$BASE" '{type:"comment.add",quote:"text",by:"ai:spec-first",text:"comment",baseToken:$base}')"
# Suggest edit (tracked, pending)
curl -X POST "https://www.proofeditor.ai/api/agent/abc123/ops" \
-H "Content-Type: application/json" \
-H "x-share-token: xxx" \
-H "X-Agent-Id: ai:spec-first" \
-d "$(jq -n --arg base "$BASE" '{type:"suggestion.add",kind:"replace",quote:"old",by:"ai:spec-first",content:"new",baseToken:$base}')"
# Suggest and immediately apply (tracked, committed)
curl -X POST "https://www.proofeditor.ai/api/agent/abc123/ops" \
-H "Content-Type: application/json" \
-H "x-share-token: xxx" \
-H "X-Agent-Id: ai:spec-first" \
-d "$(jq -n --arg base "$BASE" '{type:"suggestion.add",kind:"replace",quote:"old",by:"ai:spec-first",content:"new",status:"accepted",baseToken:$base}')"
# 1. Create
RESPONSE=$(curl -s -X POST https://www.proofeditor.ai/share/markdown \
-H "Content-Type: application/json" \
-d '{"title":"My Doc","markdown":"# Title\n\nContent here."}')
# 2. Extract URL and token
URL=$(echo "$RESPONSE" | jq -r '.tokenUrl')
SLUG=$(echo "$RESPONSE" | jq -r '.slug')
TOKEN=$(echo "$RESPONSE" | jq -r '.accessToken')
# 3. Bind display name via presence
curl -s -X POST "https://www.proofeditor.ai/api/agent/$SLUG/presence" \
-H "Content-Type: application/json" \
-H "x-share-token: $TOKEN" \
-H "X-Agent-Id: ai:spec-first" \
-d '{"name":"Spec-First","status":"reading","summary":"Uploaded doc"}'
# 4. Share the URL
echo "$URL"
# 5. Make edits using the ops endpoint (baseToken required)
BASE=$(curl -s "https://www.proofeditor.ai/api/agent/$SLUG/state" \
-H "x-share-token: $TOKEN" | jq -r '.mutationBase.token')
curl -X POST "https://www.proofeditor.ai/api/agent/$SLUG/ops" \
-H "Content-Type: application/json" \
-H "x-share-token: $TOKEN" \
-H "X-Agent-Id: ai:spec-first" \
-d "$(jq -n --arg base "$BASE" '{type:"comment.add",quote:"Content here",by:"ai:spec-first",text:"Added a note",baseToken:$base}')"
Sync the current Proof doc state to a local markdown file. Used by:
references/hitl-review.md Phase 5) when the doc originated from a local fileSLUG=<slug>
TOKEN=<accessToken>
LOCAL=<absolute-path>
# One read to a temp file — avoids passing markdown through $(...), which would strip trailing newlines.
STATE_TMP=$(mktemp)
curl -s "https://www.proofeditor.ai/api/agent/$SLUG/state" \
-H "x-share-token: $TOKEN" > "$STATE_TMP"
REVISION=$(jq -r '.revision' "$STATE_TMP")
# Atomic write: stream .markdown bytes directly to a temp sibling, then rename.
TMP="${LOCAL}.proof-sync.$$"
jq -jr '.markdown' "$STATE_TMP" > "$TMP" && mv "$TMP" "$LOCAL"
rm "$STATE_TMP"
jq -jr (-j no trailing newline, -r raw string) streams the markdown bytes straight to the temp file without going through a shell variable, so trailing newlines survive intact. mv within the same filesystem is atomic — a crashed write leaves the original untouched rather than a half-written file.
Confirm before writing when the pull isn't directly asked for. If a workflow ends up pulling as a side-effect of a different action (e.g., HITL review completion), surface the impending write with a short confirm like "Sync reviewed doc to <localPath>?" A silent overwrite is surprising — the user may have forgotten the local file exists in that session, or expected Proof to stay canonical until they explicitly asked to pull.
/state content as source of truth before editingedit/v2 (direct block changes) or suggestion.add (tracked changes); reserve rewrite.apply for no-client scenarios since it's blocked by LIVE_CLIENTS_PRESENT when anyone is connectedby: "ai:spec-first" on every op and X-Agent-Id: ai:spec-first in headers for consistent attributionbaseToken from your most recent /state or /snapshot read; on STALE_BASE, re-read and retry once