Files
Michael Ramos 990f3e8905 feat(server): durable feedback archive for every submitted review (#1438)
* feat(server): archive every submitted review to a durable local feedback store

Submitted feedback was only as durable as the agent session that asked for
it. Code review persisted nothing at all: /api/feedback deleted the draft,
settled the decision promise, and if the invoking agent had already timed
out the review existed nowhere (the failure #678 fixed for annotate). Plan
decisions only reached plans/ while the client-side planSave setting was on,
and repeat decisions on one plan overwrote each other. Annotate kept the
#678 record for single local files only.

Every submission now appends one record to
${PLANNOTATOR_DATA_DIR}/feedback/{project}/index.jsonl, plus a
records/{stamp}-{surface}-{decision}.md sidecar when it carries content,
written at decision settlement time inside the servers so all nine agent
frontends are covered by two implementations.

Surfaces wired in both runtimes: plan approve and deny, code review
/api/feedback (Send Feedback, Approve, LGTM) and /api/exit, annotate submit,
approve and exit. Bare approvals, LGTMs and dismissals are decision-only
JSONL lines with no sidecar.

Records are cheap by design. Code review carries diff identity (vcsType,
diffType, base, gitRef, snapshotId, cwd, PR metadata, changed-file count,
patch byte count) and never the patch bytes; plan records carry the decision
text plus a reference to the history/{project}/{slug}/NNN.md version the
decision was made on rather than a second copy of the plan. Annotation
provenance (source, author) is preserved, so external, review-agent and
WebMCP findings stay tagged and source == null selects the reviewer's own
comments.

The shared module never throws: an archive failure is logged, degrades
silently for the user, and keeps the annotation draft as the recovery copy.
The append happens before deleteDraft, generalizing the #678 ordering.

Controlled by PLANNOTATOR_FEEDBACK_HISTORY / feedbackHistory (default on).
PLANNOTATOR_ANNOTATE_HISTORY=0 additionally suppresses records for every
annotate surface, so the documented stateless-annotate promise still holds.
"feedback" is added to PURGE_OWNED_TOP_LEVEL so uninstall purge removes it.

AI-assisted (Claude) under maintainer direction.

* fix(server): stop the feedback archive from writing into the real data dir in tests

Review findings on the durable feedback archive.

1. The archive is default-on, and most server tests boot a real plan, review,
   or annotate server without redirecting PLANNOTATOR_DATA_DIR, so `bun test`
   deposited records in the contributor's own ~/.plannotator/feedback (24 files
   across 12 buckets from two test files alone) on CI and every machine. A new
   bunfig test preload, tests/setup/feedback-archive-off.ts, turns the archive
   off for the suite; the archive's own tests opt back in inside their test
   bodies, which is also how they exercise the opt-out. Those tests now use
   distinctive project names and remove the annotate history they leave in the
   real data dir, since storage.ts fixes its data directory at import time.

2. PR reviews bucketed under feedback/pr-<n>/. PR mode never sets gitContext
   and --local points agentCwd at a pool/pr-<n> checkout, so deriving the
   project from the review cwd was wrong. ReviewServerOptions now takes a
   `project` option, mirroring the annotate server, preferred over the cwd
   derivation on both runtimes; the Claude Code, OpenCode, and Pi entry points
   pass their already-computed detectProjectName() result.

3. changedFiles overcounted renames: extractChangedFiles unions the a/ and b/
   sides so a reader can resolve either path. The record now counts b-side
   paths through countChangedFiles, so a rename is one file.

4. Docs: the feedback archive is added to the privacy page and
   PLANNOTATOR_FEEDBACK_HISTORY (plus PLANNOTATOR_ANNOTATE_HISTORY) to the
   environment variables reference. The overclaim that every submitted review
   is archived is corrected: a review posted straight to GitHub or GitLab
   through /api/pr-action is not archived locally yet. Three behaviors are now
   written down: O_APPEND is not atomic on NFS or SMB and a genuine interleave
   damages both records that raced, folder-session records carry the folder
   path rather than the open document, and URL-session records store the full
   URL including its query string.

5. Pi parity: the Node mirror now has the failed-archive-write test (the one
   invariant its handler copies by hand) and the PR-mode bucketing test.

Comments only, no behavior change: the pool checkout recorded in
target.review.cwd can be cleaned up before anyone reads the record, and
getPlanVersionPath resolves the data directory storage.ts captured at import
while the archive resolves it per call.

AI-assisted (Claude) under maintainer direction.

* docs(server): make the feedback index an explicit multi-client contract

plannotator-tui will append to the same feedback/{project}/index.jsonl with
client "plannotator-tui", so the module's stance of "a client tool may emit
this shape under its own clients/ namespace" is out of date. The index is one
shared source of records, labeled by client.

1. The module docstring and the FEEDBACK_RECORD_CLIENT comment now describe the
   shared index: several tools append to the same file, separated by `client`;
   plannotator-tui is a known second writer, herdr-annotate is reserved, and
   `client` is an open set rather than an enum to validate against.

2. Two optional fields are declared so v1 reserves their names across clients:
   target.agent ({ host, session, transcript }) for surfaces whose subject is
   an agent session rather than a file or a diff, and top-level clientVersion.
   Neither is populated here. clientVersion stays unset deliberately: there is
   no runtime-agnostic version constant in packages/shared, and reading
   package.json from a vendored module would be a new filesystem dependency
   for cosmetic data.

3. Sidecar naming is documented at the naming site and in AGENTS.md: other
   clients suffix their id ({stamp}-{surface}-{decision}-plannotator-tui.md),
   so recordFile values carrying such suffixes are valid and nothing may parse
   a sidecar name. Nothing in this repo did: every consumer treats recordFile
   as an opaque handle and no test pins a filename pattern. A new test appends
   a foreign line (unknown client, unknown fields, suffixed recordFile) and
   pins that the reader keeps it.

4. Honesty fix to the atomicity comments, in code and in AGENTS.md:
   appendFileSync loops internally, so "one write syscall" was wrong even on a
   local filesystem. The real model is that a line-sized buffer handed to a
   single append-mode write completes without interleaving in practice
   locally, with the reader's skip-unparsable tolerance as the backstop and
   the NFS/SMB caveat unchanged.

5. Exhausting the sidecar collision counter now throws a named error instead of
   re-throwing a bare EEXIST, so the server log says what actually happened:
   100 taken names in one millisecond means a stopped clock or a runaway
   writer, not a transient disk problem.

6. AGENTS.md and the parseFeedbackIndex doc state the reader contract: lines
   are gated on a numeric `v` and unparsable ones are skipped, so analyzers
   that depend on v1 semantics should filter v <= 1 themselves. Fields are
   added, never repurposed, so a v2 would mean a real shape change.

AI-assisted (Claude) under maintainer direction.
2026-09-01 10:56:39 -07:00
..
2026-08-31 14:13:25 -07:00

@plannotator/opencode

Annotate plans. Not in the terminal.

Interactive Plan Review for OpenCode. Select the exact parts of the plan you want to change—mark for deletion, add a comment, or suggest a replacement. Feedback flows back to your agent automatically.

Obsidian users can auto-save approved plans to Obsidian as well. See details

Watch Demo

Watch Demo

Install

OpenCode 2 beta

Install OpenCode 2 from npm's next tag, then add Plannotator to the V2 plugins field:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": [
    {
      "package": "@plannotator/opencode@latest",
      "options": {
        "workflow": "plan-agent",
        "planningAgents": ["plan"]
      }
    }
  ]
}

Restart OpenCode 2 and verify that plannotator appears in opencode2 plugin list.

OpenCode 2 support is experimental while its plugin API is in beta. The core submit_plan review flow works everywhere. Two newer capabilities depend on which plugin API your OpenCode build ships with, and Plannotator detects both at runtime rather than requiring a particular channel:

  • Slash commands. Native command execution landed upstream in @opencode-ai/plugin (anomalyco/opencode issue #2185, PR #44765) and currently ships on the beta and dev dist-tags; the next and latest tags still carry the older API. Capability is detected from the command draft OpenCode hands the plugin, not from the plugin API's shape: ctx.command.transform exists on both generations, and only the newer draft has add. On a host that has it, Plannotator registers /plannotator-review, /plannotator-annotate, and /plannotator-last itself and runs the same machinery OpenCode 1 uses, so your raw arguments reach the CLI unchanged and nothing is routed through the model. On an older host it registers nothing and the commands run from their markdown definitions, which ask the agent to run the plannotator CLI and relay its output; that path works but costs a model turn and depends on the agent following the instruction.
  • Command precedence. OpenCode activates its own config-command loader after package plugins, and the last definition to claim a name wins, so the markdown stubs the installer writes to ~/.config/opencode/commands would otherwise shadow the native definitions on every normal install. Plannotator re-registers the three names shortly after startup so its own definitions are the ones that run. If that reclaim cannot run, the stubs keep the names and the commands still work through the model-mediated fallback.
  • Agent switching. ctx.session.switchAgent arrived with the same plugin API generation. On a host that exposes it, an agent switch chosen in the review UI is applied to the session. On an older host the plan is still approved and a warning is written to the server log; switch to build manually before implementation.
  • Abort signal. V2 tool execution still exposes no abort signal. Cancelling a turn cannot stop a running review server or CLI child immediately.
  • Session URLs. OpenCode 2 has a TUI plugin entry point, but it is separate from the server plugin Plannotator registers, so there is no toast to show and the plugin's own console output is discarded by the host unless you start it with OPENCODE_PRINT_LOGS=1. Instead, on a host whose plugin API exposes session.synthetic, Plannotator posts the URL into the session transcript as a Plannotator session ready: <url> notice, injected with resume: false so it appears without waking a model turn. This covers every way a session opens: the three slash commands and the submit_plan plan review, whether the review runs on the embedded runtime or the CLI. That is the link to open for a remote session, which gets no browser opened for it. On an older host without session.synthetic the URL only reaches that discarded console output, so run with OPENCODE_PRINT_LOGS=1 there.

OpenCode 1

Add to your opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@plannotator/opencode@latest"]
}

Restart OpenCode. By default, the submit_plan tool is available to OpenCode's plan agent, not to build or other primary agents.

OpenCode 1 slash commands: Run the install script to get /plannotator-review, /plannotator-annotate, and /plannotator-last:

curl -fsSL https://plannotator.ai/install.sh | bash

This also clears any cached plugin versions.

Workflow Modes

The examples below use the OpenCode 1 config shape. OpenCode 2 places the same option keys under the plugin entry's options object shown above. In V2, manual registers no tool, so it leaves only the slash commands: useful on a host with native command execution, inactive on one without it.

  • plan-agent (default): submit_plan is available to OpenCode's built-in plan agent plus any extra agents listed in planningAgents. This keeps Plannotator integrated with OpenCode plan mode without nudging build to call it.
  • manual: submit_plan is not registered. Use /plannotator-last, /plannotator-annotate, and /plannotator-review when you want Plannotator.
  • user-managed: submit_plan is registered but no prompts or agent permissions are modified. You manage which agents can call submit_plan via OpenCode's native agent configuration.
  • all-agents: legacy broad behavior. Primary agents can see and call submit_plan.

Default config:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "workflow": "plan-agent",
      "planningAgents": ["plan"]
    }]
  ]
}

Runtime selection is automatic. In Bun-hosted OpenCode, Plannotator uses the embedded server bundled with the plugin. In Node-hosted or wrapped OpenCode environments, the plugin falls back to the installed plannotator CLI and sends the result back through OpenCode. You can force the fallback while debugging:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "runtime": "cli"
    }]
  ]
}

If you use other OpenCode plugins, keep everything in one plugin array and attach Plannotator's options directly to the Plannotator entry:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "workflow": "plan-agent",
      "planningAgents": ["plan", "sisyphus"]
    }],
    "@tarquinen/opencode-dcp@latest",
    "octto",
    "oh-my-opencode-slim"
  ]
}

Do not put { "workflow": "plan-agent" } as its own item in the plugin array. OpenCode plugin entries must be either a plugin string or a two-item array like [pluginName, options].

Restore the old broad behavior:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "workflow": "all-agents"
    }]
  ]
}

Use commands only:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "workflow": "manual"
    }]
  ]
}

Register the tool but manage prompts and permissions yourself:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    ["@plannotator/opencode@latest", {
      "workflow": "user-managed"
    }]
  ]
}

How It Works

  1. The configured planning agent calls submit_plan → Plannotator opens in your browser
  2. Select text → annotate (delete, replace, comment)
  3. Approve → Agent proceeds with implementation
  4. Request changes → Annotations sent back as structured feedback

Features

  • Visual annotations: Select text, choose an action, see feedback in the sidebar
  • Local by default: Plans, annotations, drafts, history, and configuration stay local. Every app load checks GitHub for updates without sending plan content, and there is currently no opt-out setting; URL annotation, hosted PR review, AI, sharing, and Workspaces use the network when selected.
  • Legacy link sharing: Small markdown shares use compressed, unencrypted URL fragments. Larger and raw HTML shares can use client-encrypted short links. Workspaces is the primary direction for team sharing.
  • Plan Diff: See what changed when the agent revises a plan after feedback
  • Annotate last message: Run /plannotator-last to annotate the agent's most recent response
  • Annotate files, folders, and URLs: Run /plannotator-annotate when you want manual review of an artifact
  • Obsidian integration: Auto-save approved plans to your vault with frontmatter and tags

Environment Variables

Variable Description
PLANNOTATOR_REMOTE Set to 1 / true for remote mode, 0 / false for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment.
PLANNOTATOR_PORT Fixed port to use. Default: random locally, 19432 for remote sessions.
PLANNOTATOR_BROWSER Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path.
PLANNOTATOR_SHARE_URL Custom share portal URL for self-hosting. Default: https://share.plannotator.ai.
PLANNOTATOR_PASTE_URL Custom paste service URL for self-hosting. Default: https://plannotator-paste.plannotator.workers.dev.
PLANNOTATOR_PLAN_TIMEOUT_SECONDS Timeout for submit_plan review wait. Default: 345600 (96h). Set 0 to disable timeout.
PLANNOTATOR_BIN Override the CLI path used by the OpenCode plugin's CLI runtime fallback. Default: plannotator on PATH.

Devcontainer / Docker

Works in containerized environments. Set the env vars and forward the port:

{
  "containerEnv": {
    "PLANNOTATOR_REMOTE": "1",
    "PLANNOTATOR_PORT": "9999"
  },
  "forwardPorts": [9999]
}

If nothing opens automatically, open http://localhost:9999 when submit_plan is called.

See devcontainer.md for full setup details.

Obsidian Integration

Save approved plans directly to your Obsidian vault.

  1. Open Settings in Plannotator UI
  2. Enable "Obsidian Integration" and select your vault
  3. Approved plans save automatically with:
    • Human-readable filenames: Title - Jan 2, 2026 2-30pm.md
    • YAML frontmatter (created, source, tags)
    • Auto-extracted tags from plan title and code languages
    • Backlink to [[Plannotator Plans]] for graph view
image

License

Copyright 2025 backnotprop Licensed under MIT or Apache-2.0.