Skip to content

terry-li-hm/obsidian-review-bridge

Repository files navigation

Obsidian Review Bridge

A two-agent CriticMarkup agreement loop for an Obsidian vault. Agents anchor comments and tracked-substitution suggestions against a selected passage, reply across iterations, and converge — with provenance preserved on the file as Roughdraft-compatible trailing attributes, not in a separate decision ledger. Under the hood it is a localhost HTTP bridge inside Obsidian; pair with Fevol/obsidian-criticmarkup to render and accept/reject marks inside Obsidian.

Status: production stance, v1.0.0. The wire-format freeze (HTTP routes, SSE event types, error codes, response field shapes, env vars) is pinned by negative-only exact-set tests; v1.0 deliverables are documented in docs/v1.0-ceiling-saturation-report.md. Two consumers ship from one codebase:

  • Obsidian plugin — read + comment + suggest + accept/reject with accepted-suggestion provenance + resolve + reply + events + active-thread actions + the two-agent agreement loop. Local bearer auth. Bound to 127.0.0.1:9848 inside Obsidian.
  • review-bridge-headless CLI — runs the bridge against a directory of bare Markdown files with no Obsidian dependency, so agents can review without installing the plugin. Bound to 127.0.0.1:9849. Ships in bin/review-bridge-headless.js. See "Headless CLI" below.

Both surfaces consume the obsidian-review-bridge-core npm package, which holds the vault adapter interface, the CriticMarkup parser, the thread state machine, the cross-restart-safe Idempotency-Key dedup store, and the HTTP route handlers.

The repo and scratch-vault paths use the review-bridge slug; the product and protocol name are Obsidian Review Bridge / Review Bridge.

Review Bridge is a local-first Obsidian collaboration surface for humans, Codex, Claude Code, and other local agents. The durable review substrate is Markdown: CriticMarkup plus Roughdraft-compatible trailing attributes such as {id="c1" by="Codex" at="2026-05-27T00:00:00.000Z" re="c0"}. The sidebar is the primary reading and decision UI; the Markdown remains portable across Obsidian, Roughdraft, Git, and plain editors.

This project is inspired by Proof and Roughdraft-style Markdown review workflows. It is not affiliated with or endorsed by Proof, Roughdraft, Obsidian, or Fevol's Commentator plugin. Proof SDK support, if added, should be an import/export adapter rather than the core persistence or decision layer.

Direction

Review Bridge should evolve around three stable boundaries:

  • Persistence: Markdown with CriticMarkup and Roughdraft-compatible attributes is the source of truth for review marks, authorship, timestamps, and reply links.
  • Obsidian runtime: the plugin owns selection capture, range anchoring, sidebar rendering, local HTTP/MCP endpoints, and human decision events.
  • Agent collaboration: Codex, Claude Code, and future agents communicate through Review Bridge verbs and visible Markdown marks. They should not write invisible sidecar-only review state.

The current product milestone is hardening the agent agreement loop into a dependable daily workflow. Codex and Claude Code can already propose, reply, converge on comment threads, preserve accepted suggestion history, and run from an Obsidian-launched listener while the human keeps final authority in Obsidian; the remaining work is to make background setup, observability, and adapter-failure recovery more turnkey.

Agreement Loop Status

  1. Core parity: CriticMarkup/Roughdraft parsing, serialization, fenced-code handling, and id/re thread grouping are covered by round-trip tests.
  2. Agent identity: Codex and Claude Code are canonicalized into stable participant labels for agreement summaries and replies.
  3. Two-agent pass: review-bridge agreement-loop and review_bridge_agreement_loop run Codex and Claude Code adapter commands sequentially over open comment threads.
  4. Convergence state: agreed, human-needed, and iteration serialize as Roughdraft-compatible attributes and summarize into /state.threads[*].agreement.
  5. Human handoff: the loop stops after agreement, escalation, or the iteration cap, and leaves every reply visible in Markdown and the Obsidian sidebar.
  6. Next hardening: make listener setup and failure recovery more turnkey, and add optional Proof SDK import/export only after the Markdown-native loop is reliable.

Build

cd ~/obsidian-review-bridge
npm install
npm run build

Install into a vault

After npm run build produces main.js, copy it and manifest.json into the vault's plugin directory with the explicit install command:

node bin/review-bridge.js install /path/to/vault --confirm-not-chromatin

The command resolves <vault-path>, refuses any path whose lowercased absolute form contains chromatin, then copies main.js and manifest.json into <vault>/.obsidian/plugins/review-bridge/, creating the directory if needed. The --confirm-not-chromatin flag is required on every invocation; it is the human's affirmative acknowledgement that the target is not a live Chromatin vault. There is no auto-install path — npm install only fetches dependencies, never copies plugin bundles. --bundle <path> and --manifest <path> override the defaults for tests and one-off installs from a pre-built bundle.

For scratch-vault development, ~/review-bridge-testvault/ has Hot Reload installed and review-bridge/.hotreload enabled. Use:

npm run dev:vault

This builds directly to ~/review-bridge-testvault/.obsidian/plugins/review-bridge/main.js, where Hot Reload can reload the plugin after changes. Both dev:vault and build:vault run scripts/guard-vault-target.js before esbuild to ensure the output path is the scratch vault only. scripts/guard-vault-target.js remains the strict floor: it hard-blocks any path containing chromatin and only allows the configured scratch vault, independent of the install command and its --confirm-not-chromatin opt-in.

Headless CLI

review-bridge-headless runs the bridge against a plain directory of Markdown files. No Obsidian dependency, no plugin install. Agents can read state, anchor CriticMarkup comments, post substitution suggestions, and accept/reject them against bare .md files on disk.

node bin/review-bridge-headless.js --vault /path/to/notes [--port 9849] [--token-file <path>]

On first run the binary mints a 256-bit bearer token (mode 0600) under <vault>/.review-bridge/token and creates a persistent Idempotency-Key dedup store at <vault>/.review-bridge/idempotency.json. Subsequent runs reuse both — the dedup store survives process restart, so a retried POST after a bridge crash will not double-write. A <vault>/.review-bridge/lock file enforces single-process-per-vault.

Three startup guards refuse to launch:

  • Chromatin segment check (lexical AND realpath-resolved) — refuses any vault whose absolute form contains a chromatin-segment. Exit code 3. Mirrors the plugin's --confirm-not-chromatin discipline.
  • Token file mode — refuses if <vault>/.review-bridge/token exists with permissions wider than 0600. Exit code 4. A chmod 644 between launches will not silently widen the bearer token's exposure.
  • Single-process lock — refuses if <vault>/.review-bridge/lock is held by another bridge process. Exit code 5; prints the holder PID.

Surface (v1.0 frozen):

Method Path Purpose
GET /health Liveness + bridge identity
GET /state List markdown files in the vault
GET /state/:path File body + parsed marks + revision
GET /threads Flat list of every thread in the vault
GET /events SSE event stream (Last-Event-ID resume)
POST /comment Add a CriticMarkup comment
POST /suggest Add a CriticMarkup substitution
POST /accept Accept a substitution (applies change)
POST /reject Reject a substitution (keeps original)
POST /reply Reply to a comment thread
POST /resolve Close a comment thread (resolved=true)
POST /reopen Re-open a resolved thread
POST /delete Delete every comment in a thread + prune sidecar

Every request requires Authorization: Bearer <token>. The Origin header is refused (confused-deputy guard). POSTs carrying Idempotency-Key are replayed verbatim (with idempotentReplay: true) within the TTL window; idempotent replays do NOT emit a second SSE event.

The /events SSE stream emits eight typed events on the wire: comment-added, suggestion-added, suggestion-accepted, suggestion-rejected, reply-added, thread-resolved, thread-reopened, thread-deleted. These names are wire-format contract surface — renames are breaking changes per the bridge-core public API carve-out (AGENTS.md). Clients may resume via Last-Event-ID: <N> and the bridge replays any events with eventId > N from a bounded 1024-entry history; reconnects with a Last-Event-ID older than the history's minimum receive the surviving suffix silently (a bridge-history-gap signal is a v0.5 follow-up — see docs/v0.4-residual.md).

Active-thread focus actions remain Obsidian-only; the plugin's /events vocabulary (marks-changed, selection-pinned, etc.) is distinct from the headless vocabulary above and is documented in the SSE section below. The route-handler unification that would collapse the two vocabularies onto one surface is deferred to v0.5.

obsidian-review-bridge-core (v1.0.0)

The package both surfaces consume. Vault-adapter-agnostic. Dual ESM+CJS with format-matched .d.ts/.d.cts declarations. Frozen public API at v1.0 — every export is pinned by a published-package contract test, the wire-format closed sets (BRIDGE_SSE_EVENT_TYPES, BRIDGE_HTTP_ERROR_CODES, BRIDGE_HTTP_ROUTES, BRIDGE_ENV_VARS) are immutable until a major bump, and breaking changes require a CHANGELOG.md entry per the carve-out in AGENTS.md. The contract surface covers:

  • VaultAdapter interface + assertVaultRelativePath + VaultPathError + SIDECAR_SUFFIX
  • FileSystemVaultAdapter (sub-export obsidian-review-bridge-core/adapters/filesystem) — atomic writes via temp+rename, sibling .review-bridge.json sidecars, cycle-safe non-symlink-following enumeration
  • FilePersistence — JSON-file backing for the idempotency store
  • IdempotencyStore, PersistentIdempotencyStore, idempotencyKeyFor, DEFAULT_IDEMPOTENCY_*
  • createBridgeServer, generateBearerToken
  • parseMarks, serializeMark, buildCommentMarkWithMetadata, buildSubstitutionMarkWithMetadata, shiftMark, resolveMark, deleteCommentThreadMarks, the threads / sidecar / decisions / agreement / agreementLoop modules
  • BRIDGE_CORE_VERSION (derived from package.json version via JSON import — there is no second write site to drift)

A published-package contract test (src/publishedPackageContract.test.ts) runs npm pack against the workspace, installs the tarball into a temp consumer, and asserts every name above resolves under both ESM import and CJS require on every gate cycle. A migration corpus (packages/bridge-core/src/wireFormatMigration.test.ts) exercises every wire-format change from v0.3 through v1.0 with old-shape clients against a current-shape server — the corpus is the load-bearing reason a v0.3 agent client continues to function on a v1.0 bridge without changes.

API

Base URL: http://localhost:9848.

Required headers:

  • Authorization: Bearer <bridgeToken>
  • x-agent-id: ai:<your-agent>

The plugin generates bridgeToken on first load and stores it in <vault>/.obsidian/plugins/review-bridge/data.json. Requests with a browser Origin header are rejected with ORIGIN_FORBIDDEN.

Mutating POST requests are processed through a single FIFO write queue inside the plugin. They may include Idempotency-Key: <unique-key>. Reusing the same key for the same endpoint replays the first stored response with idempotentReplay: true, which prevents duplicate marks after client retries.

Mutating bodies that write against note content must include expectedRevision and expectedSpanHash. Missing preconditions return 428 PRECONDITION_REQUIRED; stale preconditions return 409 REVISION_MISMATCH or 409 SPAN_HASH_MISMATCH. Agent responses may also include expectedAgentRequestId; if the human has cancelled or superseded that Ask request, the bridge returns 409 AGENT_REQUEST_CANCELLED or 409 AGENT_REQUEST_SUPERSEDED instead of writing stale feedback. The review-bridge CLI supplies these automatically.

The long-term protocol name is Review Bridge. Biological/Vivesca component names stay internal and do not appear in endpoint names, JSON fields, or error codes.

GET /state?path=

Returns { bridge, path, markdown, marks: ParsedMark[], threads: StateThreadSummary[], revision, activeSelection, pinnedSelection, activeAgentRequest }. New bridge marks keep CriticMarkup readable in the file and expose id, thread, and metadata from plugin sidecar data; offsets are current observations, not stable identity.

bridge reports the loaded runtime, for example { name: "review-bridge", protocolVersion: 2, vaultPath: "/Users/terry/review-bridge-testvault", capabilities: ["state.threads", "state.threadActions", "state.threadPreconditions", "state.threadConversation", "state.threadAgreement", "state.activeThread", "state.activeThreadSummary", "state.activeThreadEndpoint", "thread.activeActions", "state.persistentActiveThread", "state.activeAgentRequest", "state.activeSelection", "selection.readEndpoint", "thread.replyAgreement", "thread.replyCommentsOnly", "thread.replyReopensResolved", "thread.reopenClearsResolutionMetadata", "comments.durableBody", "suggestions.acceptedProvenance", "identity.sidecarReattach", "http.serialWrites", "ui.sidebarComposer", "ui.selectionToolbar", "ui.wordLikeThreadCards", "ui.keyboardThreadFocus", "ui.sidebarCurrentThreadFocus", "ui.threadCardShortcuts", "ui.threadShortcutHints", "ui.semanticThreadActions", "ui.editorGutterMarkers", "ui.inlineCommentWidgets", "ui.resolvedInlineComments", "ui.authoredThreadFocus", "ui.focusAdvancesAfterRemoval", "ui.focusAdvancesAfterVisibilityChange", "ui.reopenRestoresFocus", "ui.replyReopenRestoresFocus", "events.persistentReplay", "events.humanAuthoredMarks", "events.humanDeleted", "events.threadFocused", "events.threadFocusOrigin", "events.agentAttentionRequested", "events.agentAttentionInstructions", "events.agentAttentionCancelled", "ui.askAgentActions", "ui.askAgentPromptComposer", "ui.askAgentNoteActions", "ui.agreementLoopRequest", "ui.agentRequestStatus", "ui.agentHandoffSummary", "ui.agentRequestCancel", "ui.statusBarTransport", "preconditions.agentRequest", "ui.preconditionErrorCopy", "..."] }. Agents should check it when validating a scratch vault, because a built main.js can be newer than the Obsidian plugin instance currently serving localhost, and because multiple open Obsidian vault windows can otherwise make the visible UI and localhost server belong to different vaults.

threads is the agent-facing review model: one object per thread with key, title, status, statusLabel, resolved, rootId, rootKind, markIds, replyCount, current start/end, targetSnippet, flat conversation rows, agreement, availableActions, and actionPreconditions. conversation mirrors the sidebar's flat thread display with one row per mark: { id, kind, role, body, author, time, start, end, oldText, newText, acceptedSuggestion }, where role is root or reply, tracked substitutions expose both sides of the edit, and accepted-suggestion provenance rows decode the original and replacement text from durable Roughdraft attributes. agreement summarizes Roughdraft-native signals such as agreed="true", agreed="false", human_needed="true", and iteration="2" into outcome, nextAction, participant lists, and visible source signals. Codex and Claude Code should prefer threads[*].rootId or markIds for reply/accept/reject calls instead of reconstructing thread state from raw CriticMarkup. actionPreconditions contains the expectedRevision and expectedSpanHash needed to call /reply, /accept, /reject, /delete, /resolve, /reopen, or /withdraw directly from a thread summary. Bridge sidecar metadata reattaches IDs by live CriticMarkup marks rather than stored byte offsets, so current start/end can move while rootId stays the action identity. availableActions is the safe action contract for the thread: open comments expose reply, resolve, and delete; resolved comments expose reply, reopen, and delete; suggestions expose accept and reject. activeThread reports the thread key currently focused by the Obsidian sidebar for this note, or null; activeThreadSummary repeats the focused thread summary, including conversation rows and action preconditions, so an agent can answer the thread the human is looking at without searching the full thread list. The focused thread key is persisted in plugin data so reloads do not erase the review focus. When the active thread is deleted, rejected, accepted, or withdrawn, focus advances to the next remaining thread visible under the current filter, falling back to the previous visible thread or null. Resolving a visible active thread can advance focus to the next visible thread under the current filter; reopening a thread focuses the reopened thread and switches the pane back to Open. A reply to a resolved thread focuses the reopened conversation too, so the human stays on the discussion they just resumed.

If path is omitted, /state uses the active Markdown note. activeSelection reports the live editor selection when Obsidian still has one, otherwise the current pinned or recent selection: { id, path, start, end, text, spanHash, prefix, suffix, pinnedAt, revision, source }, where source is activeEditor, pinnedSelection, or recentSelection. pinnedSelection is the durable Obsidian-native target: { id, path, start, end, text, spanHash, prefix, suffix, pinnedAt }. id is stable for that pinned target; start and end are refreshed opportunistically; spanHash lets agents echo the observed selection in later write preconditions; prefix and suffix let the bridge relocate the target after edits before the selection. activeAgentRequest is the visible waiting state for the current note: { id, path, revision, attentionTarget, instruction, createdAt, targetText, markId, thread } or null. It is set when the human submits an Ask composer, cleared when a non-human agent writes a comment, suggestion, or reply for that path, and cancelled when the human clicks Cancel request. Agents should echo activeAgentRequest.id as expectedAgentRequestId when answering a request.

GET /selection?path=

Returns { bridge, path, revision, activeSelection, pinnedSelection, activeAgentRequest } without reading the full Markdown body or thread list. This is the narrow observe call for the selection-first lane: Codex or Claude Code can see the range the human selected, its span hash, and whether that target came from the live editor, pinned selection, or recent remembered selection before writing a comment or suggestion. Selection write helpers prefer activeSelection preconditions over older pinned-selection hashes.

GET /active-thread?path=

Returns { bridge, path, revision, activeThread, activeThreadSummary, activeAgentRequest } without the full Markdown body, raw mark list, or complete thread list. This is the direct observe endpoint for the Word-like lane: the human focuses a sidebar thread in Obsidian, then Codex or Claude Code reads /active-thread and uses focused-thread write endpoints against that same thread.

POST /reply-active, /accept-active, /reject-active, /delete-active, /resolve-active, /reopen-active, /withdraw-active

Act on the sidebar thread currently focused in Obsidian. Each endpoint accepts an optional { path }; when omitted, the bridge uses the active Markdown note or last active note. /reply-active also requires { text } and accepts { resolve: true } plus the same agreement fields as /reply. The bridge resolves path, id, expectedRevision, expectedSpanHash, and pending expectedAgentRequestId from activeThreadSummary, so raw HTTP clients do not need to copy thread IDs out of /state. If no thread is focused, the endpoint returns NO_ACTIVE_THREAD; if the focused thread cannot perform the requested action, it returns ACTIVE_THREAD_ACTION_UNAVAILABLE with availableActions.

POST /next-thread, /previous-thread

Move Review Bridge focus through review threads in document order and return { ok, path, direction, activeThread, activeThreadSummary, navigationLabel, activeThreadIndex, revision }. These endpoints use the same wraparound behavior as the Obsidian Go to next review thread and Go to previous review thread commands, focus the target range in the editor, update the current-comment card in the sidebar, and emit thread-focused with origin: "navigation".

Inside Obsidian, the status bar is intentionally transport-only. It reports Review Bridge: idle, Review Bridge: reviewing, Review Bridge: failed, or Review Bridge: waiting for agent; review substance stays in highlighted ranges, compact inline comment chips, gutter markers, and the sidebar thread pane. The same single pane also shows a compact agent handoff strip: direct requests say Waiting for agent, agreement-loop requests say Waiting for listener, and completed agreement marks summarize as Agreed, Needs human, or No consensus. The Markdown file still stores readable CriticMarkup, but source-mode editing does not ask the human to read raw {>>...<<} comment anchors. Adjacent reply anchors collapse to one inline chip per thread, with a count when the conversation has replies. Resolved comment chips and ranges stay visible but muted, matching the sidebar's closed-thread state.

GET /health

Returns { ok, bridge, eventId, serverOwned, serverError } without reading a note. Use this as the cheap readiness and stale-runtime check before relying on newer protocol features. bridge.vaultPath is the vault that owns localhost:9848; if it differs from REVIEW_BRIDGE_VAULT, the visible Obsidian window and the agent endpoint are probably different windows.

If /health returns 404, or if bridge.capabilities is missing state.threads, state.threadActions, state.threadPreconditions, state.threadConversation, state.threadAgreement, state.activeThread, state.activeThreadSummary, state.activeThreadEndpoint, thread.activeActions, state.persistentActiveThread, state.activeAgentRequest, state.activeSelection, selection.readEndpoint, thread.navigationEndpoint, thread.replyAgreement, thread.replyCommentsOnly, thread.replyReopensResolved, thread.reopenClearsResolutionMetadata, comments.durableBody, suggestions.acceptedProvenance, identity.sidecarReattach, http.serialWrites, ui.sidebarComposer, ui.selectionToolbar, ui.wordLikeThreadCards, ui.keyboardThreadFocus, ui.sidebarCurrentThreadFocus, ui.threadCardShortcuts, ui.threadShortcutHints, ui.semanticThreadActions, ui.editorGutterMarkers, ui.inlineCommentWidgets, ui.resolvedInlineComments, ui.authoredThreadFocus, ui.focusAdvancesAfterRemoval, ui.focusAdvancesAfterVisibilityChange, ui.reopenRestoresFocus, ui.replyReopenRestoresFocus, events.persistentReplay, events.humanAuthoredMarks, events.humanDeleted, events.threadFocused, events.threadFocusOrigin, events.agentAttentionRequested, events.agentAttentionInstructions, events.agentAttentionCancelled, ui.askAgentActions, ui.askAgentPromptComposer, ui.askAgentNoteActions, ui.agreementLoopRequest, ui.agentRequestStatus, ui.agentHandoffSummary, ui.agentRequestCancel, ui.statusBarTransport, preconditions.agentRequest, or ui.preconditionErrorCopy, the localhost server is an older loaded plugin instance. Rebuilds may have written a newer main.js to disk, but Obsidian still needs to reload the plugin before agents can rely on newer fields such as /state.threads[*].availableActions, /state.threads[*].actionPreconditions, /state.threads[*].conversation, /state.threads[*].agreement, /state.activeThread, /state.activeThreadSummary, /active-thread, active-thread write endpoints, navigation endpoints, /state.activeAgentRequest, /state.activeSelection, /selection, persisted active-thread focus, focus advancement after removal, focus advancement after filter visibility changes, focus restoration after reopening, focus restoration after replying to a resolved thread, agreement-bearing replies, comment-only replies, reply-to-resolved reopening, clean reopened-thread metadata, durable CriticMarkup comment bodies, sidecar ID reattachment after edits before a mark, serial HTTP writes, the sidebar composer, selection toolbar, Word-like thread cards, keyboard thread focus, current-thread keyboard focus in the sidebar after deliberate review navigation, active card shortcuts for navigation and review decisions, accessible shortcut hints on focused thread cards and action buttons, semantic thread-card and action-button attributes, editor gutter markers, inline comment widgets over raw CriticMarkup anchors, muted resolved comment widgets, automatic focus for authored marks, persisted event replay, semantic human-authored comment/suggestion events, semantic delete events, semantic thread-focus events, thread-focus origin metadata, explicit agent-attention request events, human-written agent instructions, agent-attention cancellation events, sidebar/selection Ask agent actions, Ask agent prompt composers, note-level Ask agent actions, agreement-loop requests, visible agent request waiting state, single-pane agent handoff summary, cancellable request cards, status-bar transport state, request-id write preconditions, or human-readable pane guidance for stale preconditions.

GET /events

Server-sent event stream. Events include an SSE id and JSON eventId. Reconnects may send Last-Event-ID or ?lastEventId=<id> to replay the last 100 retained events. Retention is persisted in the plugin data file, so agents can recover recent human actions after an Obsidian plugin reload instead of losing the replay window.

Currently emits marks-changed, selection-pinned, selection-cleared, agent-attention-requested, agent-attention-cancelled, thread-focused, human-commented, human-suggested, human-accepted, human-rejected, human-deleted, human-replied, human-resolved, and human-reopened. agent-attention-requested fires when the human submits Ask agent on the current selection, a sidebar thread, the active note, or the note-level agreement-loop action. It carries the human-written instruction plus attentionTarget as selection, thread, note, or agreement-loop; selection requests include pinnedSelection, thread requests include mark, activeThread, and activeThreadSummary, note requests carry the path/revision so agents can fetch /state for the full note, and agreement-loop requests tell the client to run review-bridge agreement-loop --adapter-template juxtacrine-glm against the note's open comment threads. It also carries activeAgentRequest, the persisted waiting-state object that the sidebar displays until a non-human agent responds for that path. agent-attention-cancelled fires when the pending request is cancelled from Obsidian or /cancel-request; it carries cancelledAgentRequest and activeAgentRequest: null, so clients can stop stale work. The bridge emits semantic thread-focus events when the human focuses a review thread from the editor, gutter marker, sidebar, navigation controls, or when active focus advances after a removal or filter change. thread-focused carries activeThread, activeThreadSummary, and origin, so agents can answer the thread the human is looking at without polling /state and can distinguish deliberate selection attention from passive cursor movement. Origins are sidebar, editor-mark, editor-cursor, editor-selection, gutter, navigation, human-authored, agent-authored, and focus-advance. The bridge focuses newly authored human and agent comments/suggestions in the sidebar; human-authored comments/suggestions additionally emit the semantic human-commented or human-suggested event. Semantic human accept/reject events are emitted for suggestions, semantic delete events for comment-thread deletion, and semantic resolve/reopen events for comment threads. It also classifies Fevol Commentator accept-all/reject-all edits as human-accepted or human-rejected when the surrounding Markdown changed exactly as the removed CriticMarkup mark implies. Ambiguous removals and edits that change surrounding text at the same time are reported only as marks-changed.

{ "eventId": 7, "type": "human-accepted", "path": "note.md", "revision": 1779390000000, "marks": [], "mark": {} }

GET /events/cursor

Returns { "eventId": 7 }, the latest emitted event id. Agents should read this before starting an interaction and then watch with lastEventId or --from-now to avoid replaying older retained events.

POST /comment

{ "path": "note.md", "quote": "exact text to anchor on", "text": "comment body" }

Inserts a CriticMarkup comment after the matched quote. The visible comment body is stored in Markdown inside {>>...<<}; plugin sidecar data stores stable ids, thread/range state, and resolution metadata for the sidebar. Fails with ANCHOR_NOT_FOUND or ANCHOR_AMBIGUOUS if the quote does not occur exactly once.

POST /suggest

{ "path": "note.md", "quote": "old text", "content": "replacement" }

Wraps the quote in {~~old~>new~~}. Accept/reject in the Review Bridge sidebar or via Fevol's plugin UI.

POST /comment-selection

{ "text": "comment body" }

Uses the active Markdown editor selection as the anchor. The bridge reads editor.getSelection(), converts editor.getCursor("from") and editor.getCursor("to") with editor.posToOffset, and appends a comment mark after that exact selected range. Returns NO_ACTIVE_MARKDOWN_VIEW or SELECTION_EMPTY instead of quote ambiguity errors.

If there is no live editor selection, the endpoint falls back to pinnedSelection. If the stored offsets are stale, the bridge searches for the selected text with its saved prefix/suffix context, refreshes the pinned offsets on a unique match, and writes there. It returns TARGET_NOT_FOUND or TARGET_AMBIGUOUS when the target cannot be recovered.

POST /suggest-selection

{ "content": "replacement" }

Uses the active Markdown editor selection as the source text and replaces that exact range with a CriticMarkup substitution. This is the preferred Obsidian-native path; /comment and /suggest remain the headless-agent fallback.

If there is no live editor selection, the endpoint falls back to pinnedSelection with the same context-aware relocation behavior.

POST /accept

{ "path": "note.md", "id": "pb_..." }

Resolves the selected mark. Substitutions become the new text, insertions become their body, deletions are removed, and highlights are unwrapped. Accepted substitutions also append a resolved provenance comment such as {>>Accepted suggestion from Codex.<<}{id="s1-accepted" by="human:obsidian" at="..." kind="accepted-suggestion" status="accepted" resolved="true" old_b64="..." new_b64="..."}; the accepted prose stays plain while /state.threads[*].conversation[*].acceptedSuggestion can recover the original and replacement text. Comments are not deleted; they are marked resolved in sidecar state so the sidebar can show them under Resolved.

POST /reject

{ "path": "note.md", "id": "pb_..." }

Rejects the selected tracked-change mark in place. Substitutions become the old text, insertions are removed, deletions become their body, and highlights are unwrapped. For backward compatibility, rejecting a comment still deletes the whole comment thread, including replies, but agents should use /delete for comments because that matches threads[*].availableActions.

POST /delete

{ "path": "note.md", "id": "pb_..." }

Deletes a comment thread, including replies, so the sidebar cannot leave orphan replies behind. Returns NOT_A_COMMENT for suggestions and tracked changes.

POST /resolve

{ "path": "note.md", "id": "pb_..." }

Marks a comment thread resolved without deleting the CriticMarkup anchor.

POST /reopen

{ "path": "note.md", "id": "pb_..." }

Reopens a resolved comment thread.

POST /reply

{ "path": "note.md", "id": "pb_...", "text": "reply body", "agreed": true, "humanNeeded": false, "iteration": 1, "resolve": true }

Appends an adjacent comment anchor after the selected comment mark. Replies preserve the selected mark's thread through Roughdraft re attributes and sidecar metadata. Agent replies may also carry agreement signals: agreed: true, agreed: false, humanNeeded: true, and iteration: N. These serialize into Markdown as Roughdraft-compatible attributes such as agreed="true", human_needed="true", and iteration="2" while /state.threads[*].agreement exposes the summarized outcome. Returns NOT_A_COMMENT for suggestions and tracked changes. If resolve is true, the bridge adds the reply and resolves the thread in one operation. If the thread was already resolved and resolve is not true, the reply explicitly reopens the thread, clears stale resolvedAt and resolvedBy metadata, focuses the reopened conversation, and emits human-reopened.

For /accept, /reject, /delete, and /reply, select marks by id from /state. Legacy selection by start or exact raw still works for old marks and debugging. If a selector matches more than once, the bridge returns MARK_AMBIGUOUS; if no mark matches, it returns MARK_NOT_FOUND.

POST /withdraw

{ "path": "note.md", "id": "pb_...", "expectedRevision": 1, "expectedSpanHash": "..." }

Retracts the calling agent's own pending mark. The bridge rejects attempts to withdraw another author's mark with CANNOT_WITHDRAW_FOREIGN_MARK.

POST /rotate-token

Rotates the local bearer token and returns the new bridgeToken. This requires the current token.

POST /clear-selection

Clears the persisted pinned selection and emits a selection-cleared event.

POST /cancel-request

{ "path": "note.md" }

Cancels the active Ask agent request for the supplied path, or the current request if path is omitted. Returns activeAgentRequest: null, includes cancelledAgentRequest, and emits agent-attention-cancelled.

POST /pin-selection

{ "path": "note.md", "start": 10, "end": 42, "text": "selected text" }

Pins a target range explicitly and emits a selection-pinned event. The supplied range must still match text; the bridge assigns a ps_... target ID and stores nearby context for later relocation. This is mainly for automation and recovery; the human path is still selecting text inside Obsidian and running Ask agent about selection.

Obsidian Commands

  • Pin current selection for agent records the active Markdown selection as pinnedSelection in /state.
  • Add comment to selection pins the selected range, opens the Review Bridge pane, and focuses an inline sidebar composer for the current target. This command is the primary human path, is always visible in the command palette, remembers the last non-empty editor selection when the palette takes focus, and can be assigned a hotkey in Obsidian.
  • Suggest replacement for selection pins the selected range, opens the Review Bridge pane, and focuses an inline replacement composer for the current target. This uses the same selection memory as comments so it still works after the command palette or popover steals editor focus.
  • Ask agent about selection pins the active selection, using the same remembered-selection fallback, opens the Review Bridge pane, and focuses an Ask agent composer. Submitting it emits agent-attention-requested with attentionTarget: "selection" and the human-written instruction.
  • Ask agent to review note opens the Review Bridge pane and focuses a note-level Ask agent composer. Submitting it emits agent-attention-requested with attentionTarget: "note" and the active note path.
  • Ask agents to reconcile note emits agent-attention-requested with attentionTarget: "agreement-loop" when the note has open comment threads that still need Codex/Claude agreement. This is the Obsidian-native launch gesture for the existing review-bridge agreement-loop --adapter-template juxtacrine-glm lane; it keeps the request visible in the same Review Bridge pane instead of opening another panel.
  • Open Review Bridge pane opens the right-side target/thread pane. The pane lists bridge threads with Open/Resolved/All filters, Word-like keyboard-focusable thread cards, stable thread/action DOM attributes for accessibility and automation, accessible shortcut hints on focused thread cards and action buttons, a current-comment card treatment with compact state indicators instead of noisy status pills, compact agreement indicators for agreed/escalated/no-consensus agent threads, a compact selected-text locator, body-first flat conversation rows with quiet author/time bylines, hover/focus-revealed card actions, a low-chrome compact reply composer, compact inline tracked-change suggestion cards, closed-conversation banner for resolved comments, and header-level previous/next navigation with position labels such as 2 of 5, plus comment/suggest/ask-on-current-target, focus, ask-agent-about-thread, ask-agent-to-review-note, resolve/reopen, delete, accept/reject, reply, and reply-and-resolve controls for the relevant thread type. When no passage is pinned, the target area stays compact and exposes note-level ask and agreement-loop actions so the thread list remains the main surface. Ask agent actions open compact prompt composers for the current target, thread, or note before emitting the request event; the pane then shows a waiting card until a non-human agent response clears it. Comments and suggestions also show editor-side comment markers: highlighted target ranges, gutter markers, and compact inline comment chips that replace raw comment anchors in source mode. Inline chips collapse adjacent reply anchors to one marker per thread and show a hover/focus preview. Clicking a marker focuses the matching sidebar thread and switches the filter when needed; clicking a marker focuses the same thread whether the marker is a gutter marker or inline comment chip. Deliberate review actions such as sidebar clicks, gutter clicks, previous/next navigation, authored marks, and focus advancement scroll the current card into view and move keyboard focus to it; passive editor cursor or selection focus does not steal focus from the editor. Once a thread card has focus, ArrowDown/ArrowRight move to the next review thread, ArrowUp/ArrowLeft move to the previous thread, r focuses the reply box for comment threads, e resolves or reopens comment threads, and a/x accept or reject suggestions. When a human submits a selected-text comment or suggestion, the consumed current target and floating toolbar are cleared so the new thread becomes the visible focus.
  • The pane's automation hooks are stable data-review-bridge-* attributes, not display text: filters expose data-review-bridge-filter, data-review-bridge-filter-count, and data-review-bridge-filter-active; thread cards expose data-review-bridge-thread, data-review-bridge-status, data-review-bridge-current, data-review-bridge-root-id, data-review-bridge-agreement-outcome, and data-review-bridge-agreement-next-action; conversation containers expose data-review-bridge-conversation-count; conversation rows expose data-review-bridge-conversation-role, data-review-bridge-conversation-index, data-review-bridge-conversation-kind, data-review-bridge-mark-id, data-review-bridge-thread, data-review-bridge-author, and, for tracked substitutions, data-review-bridge-old-text and data-review-bridge-new-text; status pills expose data-review-bridge-thread-status; navigation exposes data-review-bridge-navigation-label and data-review-bridge-active-thread-index; action buttons expose data-review-bridge-action.
  • Go to next review thread and Go to previous review thread move through review threads in document order, wrap at the ends, focus the target text, and keep the sidebar on the active thread.
  • Clear pinned selection clears the current bridge target.
  • Copy current selection context copies { id, path, start, end, text, prefix, suffix, pinnedAt } JSON for debugging or manual handoff.
  • Selecting text shows a compact in-editor Review Bridge toolbar with icon buttons for comment, suggestion, and Ask agent; comment and suggestion actions now open focused sidebar composers instead of modal dialogs.
  • The editor context menu adds note-level Ask agent to review note and Ask agents to reconcile note actions, plus Add Review Bridge comment, Suggest replacement with Review Bridge, Pin selection for Review Bridge, and Ask agent about selection when text is selected. The add-comment and suggest actions use the same sidebar composer path as the selection toolbar.
  • Moving the editor cursor into a highlighted comment or suggestion range makes the matching sidebar thread active and scrolls it into view, so the pane follows the text during normal reading and editing.
  • The floating selection toolbar appears over fresh selected text with icon-first comment, suggest, and Ask agent actions. The comment action is primary, the toolbar exposes accessible labels plus stable data-review-bridge-selection-action automation hooks, Escape dismisses it, and it is suppressed over existing review marks so focusing a thread does not offer to create another thread on top of it.
  • Sidebar composers support keyboard review flow: Cmd/Ctrl-Enter submits, Alt/Option-Cmd/Ctrl-Enter replies and resolves when that action is available, and Escape dismisses a target composer or clears the active reply draft.
  • The pane translates stale-write failures into human-readable recovery steps: refresh for REVISION_MISMATCH, re-select for SPAN_HASH_MISMATCH or relocated target ambiguity, and refresh the pane when a review thread has disappeared.

CLI

The package exposes a small review-bridge CLI for Codex, Claude Code, and other local clients:

review-bridge help
review-bridge state --path "note.md"
review-bridge threads --path "note.md"
review-bridge active-thread --path "note.md"
review-bridge next-thread --path "note.md"
review-bridge previous-thread --path "note.md"
review-bridge health
review-bridge doctor
review-bridge stack-readiness
review-bridge selection
review-bridge comment --text "comment body" --expected-agent-request-id "ar_..."
review-bridge suggest --content "replacement" --expected-agent-request-id "ar_..."
review-bridge reply --path "note.md" --id "pb_..." --text "reply body" --expected-revision 123 --expected-span-hash "..." --expected-agent-request-id "ar_..."
review-bridge reply --path "note.md" --id "pb_..." --text "agreed" --agreed --iteration 1
review-bridge reply --path "note.md" --id "pb_..." --text "needs Terry" --human-needed --iteration 2
review-bridge reply --path "note.md" --id "pb_..." --text "fixed" --resolve
review-bridge reply --active --text "reply to the focused thread"
review-bridge accept --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge accept --active
review-bridge reject --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge reject --active
review-bridge delete --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge delete --active
review-bridge resolve --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge resolve --active
review-bridge reopen --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge reopen --active
review-bridge withdraw --path "note.md" --id "pb_..." --expected-revision 123 --expected-span-hash "..."
review-bridge withdraw --active
review-bridge pin-selection --path "note.md" --start 10 --end 42 --text "selected text"
review-bridge clear-selection
review-bridge cancel-request --path "note.md"
review-bridge cursor
review-bridge watch --from-now --until human-rejected
review-bridge attention
review-bridge agreement-loop --path "note.md" --codex-command "codex-review-adapter" --claude-code-command "claude-review-adapter" --trace-file "review-loop.jsonl"
review-bridge agreement-loop --path "note.md" --adapter-template juxtacrine-glm --trace-file "review-loop.jsonl"
review-bridge listen --once --adapter-template juxtacrine-glm
review-bridge listen --trace-file "review-loop.jsonl"
review-bridge launchd-plist --vault "/path/to/vault" --out "$HOME/Library/LaunchAgents/com.terry.review-bridge.listener.plist"

review-bridge listener-status reads the JSONL observability files produced by review-bridge listen and review-bridge launchd-plist, then prints a JSON summary without contacting the Obsidian plugin or needing a token:

review-bridge listener-status
review-bridge listener-status --status-file /path/to/status.jsonl --trace-file /path/to/trace.jsonl --adapter-trace-file /path/to/adapter.jsonl

Defaults match the paths generated by launchd-plist: ~/Library/Logs/review-bridge/listener-status.jsonl, ~/Library/Logs/review-bridge/agreement-loop-trace.jsonl, and ~/Library/Logs/review-bridge/adapter-trace.jsonl. The output includes an ok boolean, per-file present/records/malformedLines counts, statusCounts and reasonCounts breakdowns of listener status records, and the latest dispatch-start, dispatch-finish, dispatch-error, agreement loop trace event, and adapter trace event — all redacted to never include markdown body, reply text, stdout/stderr body, or document text. Missing files report present: false and do not crash. ok is false when no useful observability file exists or the latest useful status is a dispatch error. Malformed JSONL lines are counted and skipped.

review-bridge validate-marks --file note.md reads a Markdown file from disk and reports any malformed CriticMarkup. It runs entirely offline — no token, no localhost runtime, no vault — so an agent, a git pre-commit hook, or a CI step can verify the review marks in a file before sending it through the bridge. Pass --stdin instead of --file to validate Markdown piped from another process, e.g. git show HEAD~1:vault/note.md | review-bridge validate-marks --stdin. The MCP equivalent is review_bridge_validate_marks, which is content-only by design: it accepts { content: "<Markdown source>" } and deliberately exposes no file-read primitive. That keeps the MCP surface scoped to markdown the calling agent already holds; agents that need to validate a file on disk should read it themselves (their own filesystem permissions apply) and pass the bytes via content. The output is { ok, path|source, marks, issues } where marks counts the valid CriticMarkup marks found outside fenced code blocks and issues lists each malformed opening with kind (one of unclosed-comment, unclosed-insertion, unclosed-deletion, unclosed-substitution, malformed-substitution, unclosed-highlight), a human-readable message, a 1-based line and column, the byte offset, and a 40-character snippet for context. Matches that would span a fenced code block boundary are treated as malformed rather than silently swallowing fenced backticks as a closing delimiter. The command exits 0 even when issues are found; pipe through jq -e .ok to get exit-status semantics in a script.

review-bridge agreement-loop --validate-first (MCP: review_bridge_agreement_loop with validateFirst: true) runs the same validator against the runtime's Markdown before invoking any agent. The preflight is bounded by --agent-timeout-ms (default 120s) so a wedged bridge cannot hang the loop indefinitely, and it fails closed when the bridge returns no markdown for the path: the loop returns { ok: false, ..., stoppedReason: "validate-first-markdown-missing", error } rather than silently treating absent content as valid. When validation runs and the note has malformed CriticMarkup, the loop returns { ok: false, path, iterations: 0, stoppedReason: "validate-first-failed", turns: [], threads, validation } where validation is the same { ok, marks, issues } shape produced by review-bridge validate-marks, with no agent ever called. The trace file records the preflight lifecycle as validate-first-start followed by a stop event whose reason is one of validate-first-failed, validate-first-markdown-missing, or validate-first-preflight-error, while keeping the redacted-body discipline of every other trace event. When validation passes, the loop continues into its normal completion path and returns { ok: true, path, iterations, stoppedReason: "no-open-threads" | "max-iterations-reached", turns, threads }stoppedReason is populated on every return shape so consumers can branch on a stable discriminator without parsing free-text messages.

review-bridge stack-readiness checks three lanes before serious review work and returns a single JSON summary. It verifies: (1) the juxtacrine/Claude Code lane via juxtacrine lane --json, expecting ok: true and budget_lane: "claude-code-max-subscription"; (2) the ribosome/GLM lane via ribosome lane --json, expecting ok: true, provider: "zhipu", budget_lane: "zhipu-coding-plan", and either models.opus or models.sonnet equal to "glm-5.1"; and (3) Review Bridge runtime readiness using the same doctor logic that compares the loaded Obsidian plugin with the installed bundle, vault ownership, and listener observability. The command runs all three checks in parallel with a 10-second timeout per lane. It degrades gracefully: if juxtacrine, ribosome, or localhost Review Bridge is unavailable, the output includes ok: false with actionable per-check status and error details, not a crash. Output contains no tokens or markdown bodies. The MCP equivalent is review_bridge_stack_readiness. Use --juxtacrine-command or --ribosome-command to override the default lane commands, or set REVIEW_BRIDGE_STACK_JUXTACRINE_COMMAND and REVIEW_BRIDGE_STACK_RIBOSOME_COMMAND environment variables. Use --lane-timeout-ms to override the per-lane timeout.

The CLI reads bridgeToken from REVIEW_BRIDGE_TOKEN, REVIEW_BRIDGE_DATA, REVIEW_BRIDGE_VAULT, or the scratch vault default.

review-bridge selection is the narrow observe call for the selected-passage lane. It returns the active editor selection when possible, then falls back to pinned or recent selection state, including source, revision, and spanHash. review-bridge threads is the compact agent path for deciding what to do next. It returns the same threads array, activeThread key, and activeThreadSummary object as /state, plus the same activeAgentRequest, activeSelection, path, revision, pinnedSelection, and bridge metadata, without the full Markdown body or raw mark list. review-bridge active-thread calls /active-thread and is narrower: it returns only activeThread, activeThreadSummary, activeAgentRequest, path, revision, and bridge metadata, so an agent can inspect the thread the human is looking at before using an --active action. Thread summaries include actionPreconditions; pass them to mark-action commands with --expected-revision and --expected-span-hash to avoid an extra state fetch. When activeAgentRequest is present, the CLI also adds expectedAgentRequestId to write calls so cancelled or superseded requests fail before changing Markdown. Selection comment/suggest commands use activeSelection preconditions before falling back to pinnedSelection.

For the Word-like lane, mark-action commands also accept --active. review-bridge reply --active --text "...", accept --active, reject --active, delete --active, resolve --active, reopen --active, and withdraw --active call the matching active-thread write endpoint, so the runtime resolves the focused sidebar thread and fails with NO_ACTIVE_THREAD or ACTIVE_THREAD_ACTION_UNAVAILABLE instead of guessing when that thread cannot perform the action. review-bridge next-thread and review-bridge previous-thread move the focused/current comment exactly like the Obsidian navigation buttons and return the new activeThreadSummary, so an agent can traverse comments without parsing Markdown.

review-bridge attention waits specifically for the next future agent-attention-requested, the event emitted by the Obsidian Ask agent composers. It returns the human instruction plus either pinnedSelection for selection requests or activeThreadSummary and mark for thread requests. The command reads /events/cursor before connecting unless --last-event-id or --replay is supplied, so older retained events do not replay by accident. review-bridge cancel-request clears the pending request and emits agent-attention-cancelled.

review-bridge agreement-loop runs the visible two-agent convergence loop over existing comment threads. It reads /state, finds threads whose agreement.nextAction is continue, and invokes the Codex and Claude Code adapter commands sequentially with a JSON context on stdin: { protocol, path, participant, iteration, maxIterations, thread, markdown }. Each adapter returns JSON such as { "text": "Agreed.", "agreed": true }, { "text": "I disagree because...", "agreed": false }, { "text": "Needs Terry.", "humanNeeded": true }, or { "skip": true }. The loop writes every non-skipped decision through /reply with the adapter's agent id, preserving Review Bridge preconditions, event emission, Obsidian focus behavior, and Roughdraft agreement attributes. When --trace-file is supplied, it appends JSONL events such as start, iteration, agent-decision, turn, agent-error, and finish without storing the full Markdown body or reply text. --adapter-template juxtacrine-glm provides named default adapter commands for both participants; explicit --codex-command or --claude-code-command override the template for that participant. When an adapter fails (parse error, non-zero exit, or timeout), the trace includes participant, threadKey, rootId, iteration, error, stdoutLength, and stderrLength without the full stdout, stderr, or reply text, and the CLI exits with a JSON error envelope carrying the same metadata.

review-bridge listen watches the Review Bridge SSE stream and automatically runs the agreement loop when the human clicks "Ask agents to reconcile note" in the Review Bridge pane. It is the non-developer alternative to running review-bridge attention followed by review-bridge agreement-loop for each request:

review-bridge listen
review-bridge listen --once
review-bridge listen --adapter-template juxtacrine-glm --max-iterations 3 --trace-file loop-trace.jsonl --status-file listener-status.jsonl

The listener connects to /events, ignores non-agreement-loop attention requests, and runs runAgreementLoop in-process when attentionTarget or activeAgentRequest.attentionTarget is "agreement-loop". --once dispatches a single agreement-loop run and exits, which is useful for testing or one-shot automation. Without --once, the listener stays connected and handles successive requests. --adapter-template juxtacrine-glm is the default; explicit --codex-command or --claude-code-command override the template. --max-iterations, --agent-timeout-ms, and --trace-file pass through to the agreement loop.

The listener deduplicates by activeAgentRequest.id when present, otherwise by eventId + path, so SSE reconnects and retained-event replays do not run the same request twice within one listener process. It prints JSONL status records to stdout and also appends them to --status-file or REVIEW_BRIDGE_LISTENER_STATUS when supplied: { "status": "ignored", "reason": "non-agreement-loop" | "duplicate" | "no-path", ... } for skipped events, { "status": "dispatch-start", "path", "eventId", "dedupKey" } when an agreement loop begins, { "status": "dispatch-finish", "path", "iterations", "turnCount" } when it completes, and { "status": "dispatch-error", "path", "error", ... } on adapter failure. Adapter stdout and stderr bodies are never included; only their lengths. When an adapter fails after selecting a thread, the listener writes one visible humanNeeded reply to that thread with redacted recovery guidance; this clears the same Obsidian waiting card and leaves the failure in Markdown rather than silently leaving the request pending. The human sets up the listener once (e.g. as a login item or launchd agent), then uses the same single Review Bridge pane and clicks "Ask agents to reconcile note" whenever they want an automatic agreement pass.

review-bridge launchd-plist generates a reviewable macOS LaunchAgent plist for the listener and does not load or install it. The generated job uses absolute paths for Node, review-bridge.js, and review-bridge-adapter.js, sets RunAtLoad and KeepAlive, writes stdout/stderr under ~/Library/Logs/review-bridge/, passes --status-file plus --trace-file, and sets REVIEW_BRIDGE_ADAPTER_TRACE so background runs stay observable from listener, loop, and adapter-lane logs. Use --vault to bake the intended vault into EnvironmentVariables; use --out to write the plist to a chosen path for review before any manual launchctl action.

review-bridge-adapter codex and review-bridge-adapter juxtacrine-glm are the default commands behind the juxtacrine-glm template. They read the same agreement-loop JSON context from stdin and emit a single decision JSON object. Set REVIEW_BRIDGE_ADAPTER_TRACE=/path/to/adapter.jsonl to record redacted adapter observability events with run id, route, participant, thread key, root id, iteration, duration, and stdout/stderr lengths, without document text or reply text. Tests and custom deployments can override the underlying calls with REVIEW_BRIDGE_ADAPTER_CODEX_COMMAND or REVIEW_BRIDGE_ADAPTER_JUXTACRINE_GLM_COMMAND.

review-bridge doctor compares the loaded localhost runtime with the installed plugin bundle, the configured vault, and background listener health. It accepts the same --status-file, --trace-file, and --adapter-trace-file options as listener-status to locate listener observability files. If the installed main.js contains newer protocol support but /health returns 404, doctor reports staleLoadedRuntime: true, meaning Obsidian needs to reload the plugin. If bridge.vaultPath differs from REVIEW_BRIDGE_VAULT, doctor reports vaultMismatch so agents do not confuse one open Obsidian vault window for another. The comparison is realpath-aware: a symlinked alias on either side does not raise a spurious mismatch, and a real mismatch returns runtime, configured, runtimeReal, and configuredReal so the diagnosis points at the actual on-disk locations. The listenerObservability key contains the same redacted summary as listener-status: per-file presence and record counts, statusCounts, reasonCounts, and the latest dispatch/trace/adapter events with all markdown, text, stdout, stderr, body, document, and content fields redacted. Missing listener files report present: false and do not crash doctor; the plugin health check proceeds regardless. If auth setup is wrong, CLI and MCP errors include the guessed vault, the data file path, and a hint to set REVIEW_BRIDGE_VAULT, REVIEW_BRIDGE_DATA, or REVIEW_BRIDGE_TOKEN.

npm run smoke:visual is the repeatable Obsidian visual smoke for the Word-like lane. With Obsidian open on the scratch vault and the bridge plugin loaded, it creates Review Bridge Selection Visual Smoke.md, selects text in the active editor, verifies the floating toolbar, clicks the floating toolbar's Add comment action, writes through the focused sidebar composer, observes the human-commented SSE event, and verifies the human-authored comment becomes the active thread. It then writes a selection-anchored comment and selection-anchored suggestion without quote matching, accepts the selected suggestion, submits a selected-text Ask agent request, observes the agent-attention-requested SSE event, answers it with a visible comment, verifies the waiting card clears, then selects an already reviewed range and verifies the sidebar focuses that thread with origin: "editor-selection". It then creates Review Bridge Visual Smoke.md, submits a note-level Ask agent request, observes the note-targeted SSE event, answers it with a visible comment, verifies the waiting card clears, submits an agreement-loop request from the same pane, verifies attentionTarget: "agreement-loop" and one Review Bridge pane, answers it through a visible reply, and runs comment -> resolve -> reply/reopen, focused-card r reply focus, focused-card e resolve/reopen, suggestion, focused-card x reject, next-thread plus focused-card ArrowUp/ArrowDown navigation, and focused-card a accept. It asserts the active-thread JSON and DOM signals, checks obsidian dev:errors, and writes screenshots to artifacts/visual-smoke-00-selection-toolbar.png, artifacts/visual-smoke-00-human-toolbar-comment.png, artifacts/visual-smoke-00-selection-comment.png, artifacts/visual-smoke-00-selection-suggestion.png, artifacts/visual-smoke-00-selection-suggestion-accepted.png, artifacts/visual-smoke-00-selection-ask-request.png, artifacts/visual-smoke-00-selection-ask-answered.png, artifacts/visual-smoke-00-selection-existing-focus.png, artifacts/visual-smoke-00-note-ask-request.png, artifacts/visual-smoke-00-note-ask-answered.png, artifacts/visual-smoke-00-agreement-loop-request.png, artifacts/visual-smoke-00-agreement-loop-answered.png, artifacts/visual-smoke-01-open-comment.png, artifacts/visual-smoke-02-resolved-comment.png, artifacts/visual-smoke-03-replied-reopened-focus.png, artifacts/visual-smoke-04-suggestion-card.png, artifacts/visual-smoke-04-navigation-current.png, and artifacts/visual-smoke-05-suggestion-accepted.png.

MCP

The package also exposes a thin stdio MCP adapter for clients that prefer typed tools over shell commands:

review-bridge-mcp

The MCP server exposes the same core verbs as the CLI: review_bridge_doctor, review_bridge_health, review_bridge_state, review_bridge_selection, review_bridge_threads, review_bridge_active_thread, review_bridge_next_thread, review_bridge_previous_thread, review_bridge_comment, review_bridge_suggest, review_bridge_reply, review_bridge_reply_active, review_bridge_agreement_loop, review_bridge_accept, review_bridge_accept_active, review_bridge_reject, review_bridge_reject_active, review_bridge_delete, review_bridge_delete_active, review_bridge_resolve, review_bridge_resolve_active, review_bridge_reopen, review_bridge_reopen_active, review_bridge_withdraw, review_bridge_withdraw_active, review_bridge_pin_selection, review_bridge_clear_selection, review_bridge_cancel_request, review_bridge_event_cursor, review_bridge_watch, review_bridge_next_attention_request, review_bridge_stack_readiness, and review_bridge_validate_marks. review_bridge_doctor also returns listenerObservability and accepts statusFile, traceFile, and adapterTraceFile to inspect non-default listener logs without shelling out. review_bridge_stack_readiness checks juxtacrine, ribosome, and Review Bridge lanes in one call; it accepts juxtacrineCommand, ribosomeCommand, laneTimeoutMs, plus the same listener observability paths as doctor.

Codex and Claude Code setup notes live in CLIENTS.md.

Example client configuration:

{
  "mcpServers": {
    "review-bridge": {
      "command": "node",
      "args": ["/Users/terry/obsidian-review-bridge/bin/review-bridge-mcp.mjs"],
      "env": {
        "REVIEW_BRIDGE_VAULT": "/Users/terry/review-bridge-testvault"
      }
    }
  }
}

review_bridge_watch waits on /events and supports lastEventId or fromNow: true. Supplying a cursor, or setting fromNow, avoids replaying retained in-memory events.

review_bridge_next_attention_request waits specifically for the next future agent-attention-requested by default, so Codex, Claude Code, or another local client can block until the human asks about a selected passage or thread, then answer using the attached instruction and target metadata. Pass lastEventId or fromNow: false only when intentionally replaying retained events.

review_bridge_agreement_loop is the typed MCP equivalent of review-bridge agreement-loop: it accepts a vault-relative path, codexCommand, claudeCodeCommand, and optional traceFile, passes each adapter the same JSON context on stdin, and writes non-skipped decisions through /reply using the adapter's Review Bridge agent id. An optional adapterTemplate: "juxtacrine-glm" provides default adapter commands for both participants; explicit codexCommand and claudeCodeCommand override the template for their respective participant.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages