All skills
Skillintermediate

HITL Review Mode

Human-in-the-loop iteration loop for a markdown document shared via Proof. Invoked either by an upstream skill (`ce-brainstorm`, `ce-ideate`, `ce-plan`) handing off a draft it produced, or directly by the user asking to iterate on an existing markdown file they already have on disk ("share this to proof and iterate", "HITL this doc with me"). Mechanics are identical in both cases: upload the local

Claude Code Knowledge Pack7/10/2026

Overview

HITL Review Mode

Human-in-the-loop iteration loop for a markdown document shared via Proof. Invoked either by an upstream skill (ce-brainstorm, ce-ideate, ce-plan) handing off a draft it produced, or directly by the user asking to iterate on an existing markdown file they already have on disk ("share this to proof and iterate", "HITL this doc with me"). Mechanics are identical in both cases: upload the local doc, 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.

This mode assumes a local markdown file exists. There is no "from scratch" entry — if the user wants a fresh doc, create one with the normal proof create workflow first, then invoke HITL.

Load this file when HITL review mode is requested — whether by an upstream caller or directly by the user.


Invocation Contract

Inputs:

  • Source file path (required): absolute or repo-relative path to the local markdown file. When an upstream caller invokes this mode, it passes the path explicitly. When the user invokes directly ("share that doc to proof and let's iterate"), derive the path from conversation context — the file the user just referenced, created, or edited. If ambiguous, ask the user which file.
  • Doc title (required): display title for the Proof doc. Upstream callers pass this explicitly; on direct-user invocation, default to the file's H1 heading, falling back to the filename (minus extension) if no H1 exists.
  • Recommended next step (optional, caller-specific): short string the caller wants echoed in the final terminal output (e.g., "Recommended next: /ce-plan"). Not used on direct-user invocation — the terminal report simply summarizes the iteration and asks what's next.

Agent identity is fixed, not a parameter: every API call uses agent ID ai:compound-engineering and display name Compound Engineering. Callers do not override this.

Return shape (used by upstream callers to resume their handoff; also shown to the user in the terminal when invoked directly):

  • status: proceeded | done_for_now | aborted
  • localPath: the source file path (same as input)
  • localSynced: true if Phase 5 wrote the reviewed doc back to localPath; false if the user declined the sync and local is stale. Only present on proceeded.
  • docUrl: the tokenUrl for the Proof doc
  • openThreadCount: number of unresolved threads still in the doc
  • revision: final doc revision after end-sync (only on proceeded)

Phase 1: Upload and Wait

  1. Read the local markdown file into memory. Remember this content as uploadedMarkdown — Phase 5 compares against it to detect whether anything changed during the session.

  2. POST https://www.proofeditor.ai/share/markdown with {title, markdown} → capture slug, accessToken, tokenUrl

  3. POST /api/agent/{slug}/presence with X-Agent-Id: ai:compound-engineering, x-share-token: <token>, body {"name":"Compound Engineering","status":"reading","summary":"Uploaded doc for review"}

  4. Display prominently in the terminal:

    Doc ready for review: <tokenUrl>
    
  5. Ask the user with the platform's blocking question tool: AskUserQuestion in Claude Code (call ToolSearch with select:AskUserQuestion first if its schema isn't loaded), request_user_input in Codex, ask_user in Gemini, ask_user in Pi (requires the pi-ask-user extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question.

    Question: "Highlight text in Proof to leave a comment. The agent will read each one, reply in-thread or apply the fix, then sync changes back to your local file. What's next?"

    Options:

    • I'm done with feedback — read it and apply
    • I have no feedback — proceed

    If the user is still reviewing, they leave the prompt open — the blocking question waits naturally. A third "still working" option would be a no-op wrapper for that.

    On I have no feedback — proceed: skip to Phase 5 (end-sync); return to caller with status: proceeded.

    On I'm done with feedback: continue to Phase 2.


Phase 2: Ingest Pass

A single pass over the current doc state. Deterministic, idempotent, derivable from marks — no session cache, no sidecar state.

At the start of the pass, update presence to status: "acting" with a short summary like "Reading your feedback" so anyone watching the Proof tab sees the agent is live on their comments. Update to status: "waiting" before the Phase 3 terminal report so the tab signals "ball is in your court" while the terminal asks for the next signal. Same POST /presence call as Phase 1 — just different status/summary.

2.1 Read fresh state

GET /api/agent/{slug}/state
Headers: x-share-token: <token>

Capture:

  • markdown (current body — includes any user direct edits and accepted suggestions)
  • revision
  • marks (object keyed by markId)
  • mutationBase.token — the baseToken required for this round's mutations

2.2 Identify marks that need attention

Filter marks to items where all of the following hold:

  • by starts with human: (authored by a human, not the agent)
  • resolved is false
  • Either thread has no entry authored by any ai:* identity, OR the latest entry in thread is authored by human:* with an at timestamp newer than the latest ai:* entry (user responded to a prior agent reply)

Skip everything else. Agent-authored marks, resolved threads, and threads already replied to with no new human response are done.

2.3 Read each mark and decide how to respond

The point of HITL is to give the user a natural way to steer the doc without dragging every decision into the terminal. Most feedback can be auto-applied. Only escalate when the agent genuinely can't make a confident call alone.

Real feedback blends types — "this is wrong, rename to Y" is both objection and directive; "why X? I'd prefer Z" is both question and suggestion. Don't force a clean classification. Read the comment text, the anchored quote, and any prior thread replies, and decide:

Can the agent apply a fix directly with confidence? Imperatives ("rename X to Y", "remove this", "add a section about Z") usually qualify. Apply the edit, reply with a one-line summary of what changed, resolve.

Is this a question with a clear answer? Answer in-thread. Resolve if the answer stands on its own. If answering surfaces a new decision the user should weigh in on, leave open and surface it in the terminal report.

Is this a disagreement? ("this is wrong", "contradicts §2", "this won't work"). Evaluate the claim against current content. If the agent agrees, fix and reply "Agreed — updated to X". If the agent disagrees, reply with the reasoning and leave open. Don't silently apply an objection without evaluating it — the whole point is that the user flagged it because they think the plan is wrong.

Is the intent genuinely unclear? First try: attempt the most reasonable interpretation, apply it, and reply "I read this as X — let me know if I should revert." That's cheaper than a round-trip when stakes are low. Ask for clarification only when the interpretations lead to meaningfully different outcomes. When asking, use the platform's blocking question tool for a quick multiple-choice when the options are discrete, or leave it as an open thread comment when free-form response is more natural. Either way the thread stays open so the next pass picks up the user's reply.

Invariant: every attention-needing mark ends the pass with an agent reply in its thread. Unreplied = "still to do" — the next pass re-classifies it. This is what makes the loop idempotent without a sidecar: mark state is the state. Even when the agent disagrees or can't decide, reply (with reasoning or a question) rather than silently skip.

Parallelize independent thread ops. comment.reply and comment.resolve across different marks don't conflict — they touch different thread state and a stale baseToken on one doesn't poison another (retry-on-STALE_BASE is cheap, per-mark, and local). When there are more than ~3 attention-needing marks that classify as plain replies or resolves, dispatch them in parallel — either via multiple tool calls in one turn or via sub-agents (Agent/Task in Claude Code, spawn_agent in Codex, subagent in Pi). Keep block-mutating edits (suggestion.add, /edit/v2) sequential or batch them through one /edit/v2 call — concurrent block edits can stale one another's baseToken and force retries, and they interact in ways that are easier to reason about as an ordered sequence.

2.4 Apply edits

The user is collaborating in the doc, not waiting on approval. Every mutation works with live clients — only whole-doc rewrite.apply is gated. Pick the tool that matches intent:

Default: suggestion.add with status: "accepted" for content changes anchored on a quote (reword, rename, clarify, correct, add a sentence inline). One call creates a tracked suggestion mark and commits the change. The user sees committed text (no pending approval needed), and the mark persists as audit trail with per-edit attribution and a one-click reject-to-revert. This is the right primitive for HITL auto-applied edits — it gives the user a reversible trail without asking them to re-review anything.

{"type":"suggestion.add","kind":"replace","quote":"<anchor>","content":"<new>","by":"ai:compound-engineering","status":"accepted","baseToken":"<token>"}

Use kind: "insert" | "delete" | "replace" as appropriate; all three support status: "accepted".

Use /edit/v2 silently only when the trail is actively wrong or technically blocked:

  • Atomicity is required — multiple coordinated edits must commit together or not at all (e.g., insert new section + update a reference in another block + delete the obsolete paragraph). /edit/v2 takes an operations array that commits atomically; separate suggestion.add calls can partially succeed.
  • Pre-user self-correction — the agent is fixing its own output before the user has looked at the doc (e.g., spotted a mistake mid-ingest-pass). A tracked mark would imply "there was an old version," which is misleading from the user's perspective.
  • Pure structural insertion with no quote anchor — adding an entirely new block/section where no existing text serves as an anchor. suggestion.add requires a quote; /edit/v2 has insert_before / insert_after keyed on block ref.
  • Structural list-item or block removalsuggestion.add with kind: "delete" only deletes the text inside a list item; the bullet marker (*, -, or numeric 1.) stays behind as an orphan line. Use /edit/v2 delete_block to remove an entire block, or find_replace_in_block to splice out the item plus its surrounding whitespace cleanly.
# Get snapshot for block refs + baseToken
curl -s "https://www.proofeditor.ai/api/agent/{slug}/snapshot" -H "x-share-token: <token>"
# Apply
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:compound-engineering" -H "Idempotency-Key: <uuid>" \\
  -d '{"by":"ai:compound-engineering","baseToken":"<token>","operations":[...]}'

Per-op body shape (singular block for replace_block; plural blocks:[{markdown},...] for anything that can add content; the server returns 422 on the wrong shape):

{"op":"replace_block","ref":"b8","block":{"markdown":"new content"}}
{"op":"insert_after","ref":"b3","blocks":[{"markdown":"new block"}]}
{"op":"insert_before","ref":"b3","blocks":[{"markdown":"new block"}]}
{"op":"delete_block","ref":"b6"}
{"op":"find_replace_in_block","ref":"b4","find":"old","replace":"new","occurrence":"first"}
{"op":"replace_range","fromRef":"b2","toRef":"b5","blocks":[{"markdown":"..."}]}

Block ref values drift across revisions — re-fetch /snapshot for fresh refs before each /edit/v2 call if any writes have landed since the last snapshot.

Bulk mechanical sweep — prefer one /edit/v2 call over N suggestion.add calls. When a uniform change hits more than ~5 blocks (emdash sweep, terminology rename across a doc, heading-style normalization), batch it as a single /edit/v2 with many operations. One round-trip, one atomic commit, one audit entry — versus N separate ops-endpoint calls, N baseToken reads (without the caching below), and N tracked marks for what is one logical change. Use suggestion.add + accepted when edits are distinct and anchored (each deserves its own reject-to-revert trail); use /edit/v2 batch when they're variations of the same mechanical rule.

Use pending suggestion.add (no status) when the change is judgment-sensitive enough that the agent wants explicit user approval before commit — rare in HITL, since the point of auto-applied edits is to reduce round-trips. Most judgment-sensitive cases are better handled by leaving the thread open with a clarifying question.

rewrite.apply is not needed during a live review. It's blocked by LIVE_CLIENTS_PRESENT anyway.

Mutation requirements (every write, including replies and resolves):

  • Top-level field is type on /ops; operations[].op on /edit/v2. Do not mix.
  • Include baseToken from /state.mutationBase.token (or /snapshot.mutationBase.token for /edit/v2).
  • Set by: "ai:compound-engineering" and header X-Agent-Id: ai:compound-engineering.
  • Include an Idempotency-Key header (fresh UUID per logical write). Reuse the same key on a proven retry of the same payload; use a new key for a new logical write.
  • Reply: {"type":"comment.reply","markId":"<id>","by":"ai:compound-engineering","text":"..."}. Resolve: {"type":"comment.resolve","markId":"<id>","by":"ai:compound-engineering"}. Reopen if needed: {"type":"comment.unresolve", ...}.

Retry after any error is verify-first, not retry-first. The Proof API can commit canonically and still return a non-2xx or a 202 with collab.status: "pending"; network timeouts can hit after the server has already written. Retrying without verifying is the most common cause of duplicate marks (same comment twice, same suggestion twice) that then need a manual cleanup pass.

  • On STALE_BASE / BASE_TOKEN_REQUIRED / MISSING_BASE / INVALID_BASE_TOKEN: pre-commit, token-related. Re-read /state, send the same payload with a fresh baseToken, retry once. The mutate() helper below auto-retries these.
  • On ANCHOR_NOT_FOUND / ANCHOR_AMBIGUOUS: pre-commit, but the quote no longer matches uniquely. Re-read is not enough; the caller must tighten or regenerate the anchor before retrying. The helper surfaces the error instead of auto-retrying.
  • On `INVALID_OPER