* feat(opencode): restore the slash commands on OpenCode 2
OpenCode's V2 plugin API gained native command execution upstream
(anomalyco/opencode issue #2185, PR #44765): ctx.command.transform lets a
plugin add a command whose execute callback fully owns the invocation. That
shape currently ships on the beta and dev dist-tags of @opencode-ai/plugin
while next and latest still carry the older context, so the capability is
duck-typed at runtime and never imported. On a host that exposes it the V2
adapter registers /plannotator-review, /plannotator-annotate and
/plannotator-last and runs the same handleCliCommand machinery OpenCode 1
uses, passing the raw argument tail straight through to the CLI. On a host
without it nothing new is registered and behavior is byte-identical to before.
Also wires ctx.session.switchAgent (same API generation, same probe) so an
agent switch chosen in the review UI is applied instead of only warned about,
and accepts both agent.list() response shapes: the HTTP client types it as a
{ location, data } envelope while the in-process plugin domain answers with a
bare array, where reading .data threw and silently emptied the agent list.
The shared command stubs get model-mediated fallback bodies for OpenCode 2
hosts on the stale channels. They carry no shell interpolation on purpose:
OpenCode 1 evaluates a template's !`...` before the V1 plugin's
command.execute.before hook can clear the parts, so a bang template there
would launch a second Plannotator session on every OC1 invocation. A source
level test pins that.
AI-assisted (Claude) under maintainer direction.
* fix(opencode): probe the command draft and reclaim the names from the stubs
Review found the capability probe was wrong in the direction that matters.
ctx.command.transform exists on pre-#44765 hosts too: our own pinned
@opencode-ai/plugin@0.0.0-next-16775 declares CommandDraft as
{ list, get, update, remove } with no add. The probe therefore returned true on
next and latest, draft.add was undefined, and because transforms are stored and
replayed the TypeError landed in the batched reload flush and aborted it before
commit, plausibly taking every command registration on the host down with it.
Capability is now read from the draft handed to the callback, which is the only
witness, and the registration call is wrapped so no transform rejection can fail
plugin setup.
The stubs also shadowed the native definitions on new hosts. Command definitions
land in a name-keyed map where add is Map.set, transforms replay in registration
order, and OpenCode's own ConfigCommandPlugin activates in the post group after
package plugins while scanning the exact directory the installer writes the
three stubs to. A setup-time registration is therefore always overwritten on a
normal install. The plugin now re-registers the same transform once activation
settles, so its definitions are last in the replay order, and calls
ctx.command.reload() explicitly because a late registration only adds its reload
to the already-flushed boot batch. Ownership is read back from
ctx.command.list() by description, which is why the native descriptions and the
stub frontmatter are deliberately distinct. If the reclaim cannot run the stubs
keep the names and the commands still work through their fallback bodies.
Also: a failing switchAgent no longer costs the reviewer their feedback on the
command path, feedback is delivered as "queue" rather than replaying the
invocation's admission mode minutes later when a steer would land mid-turn, and
the agent-list comment no longer asserts a bare-array response that could not be
reproduced upstream (accepting both shapes is still right, since reading .data
blindly throws into a catch that degrades silently).
Tests: the real old-host draft shape registers nothing and throws nothing, the
shadowing contest is modelled against upstream's replay semantics, the OpenCode 1
parts-clearing invariant is pinned for all three commands in both plan-agent and
manual mode now that the stubs carry real instructions, and the V2 smoke asserts
the plugin did not activate as failed and that all three commands resolve. The
smoke now also installs the stubs into its sandbox config dir so the contest
actually happens there. scripts/opencode2-native-commands-smoke.sh runs the same
smoke against a dev-channel build with native commands required; CI cannot,
because it pins a next build.
AI-assisted (Claude) under maintainer direction.
* fix(opencode): keep the reclaim ticking and stop an unbuilt checkout failing setup
The reclaim ended the loop when the draft-probe flag read false, but that flag
only flips when the transform replays, which under boot batching is the flush
after every plugin has loaded. Plannotator loads before the post-group config
plugins, so the first tick legitimately reads false and the loop exited for
good: the reclaim was inert in exactly the shape production has. The tick is
skipped now instead, with a test that flips the flag between ticks.
The V1 entry called resolveBundledHtmlPath synchronously during plugin
construction, outside the .catch that was there to absorb a missing asset, so an
unbuilt checkout threw out of construction before any code path that needs the
HTML. The Test workflow runs bun test with no build step, so the new OpenCode 1
interception tests failed there. Both preloads are guarded; the lazy getters
still raise a clear error if something actually needs the file.
The smoke's failed-plugin guard read entry.state.status, but Plugin.Info carries
status and error at the top level, so a failed activation slipped through.
Reads the top level first and keeps the nested one as a fallback.
Comment corrections: State.batch clears its active flag before flushing, so a
late transform registration materializes on its own; the explicit reload() is
redundant-but-defensive rather than required. The reclaim schedule is a list of
deltas the loop awaits in turn, so the ticks land near 0.3s, 1.5s, 5.5s and
15.5s, not at the raw numbers.
AI-assisted (Claude) under maintainer direction.
* fix(plugin): consolidate system prompt injections into single array element
The plugin previously pushes planning prompts and improvement contexts as
separate elements in the output.system array. This change appends them to
output.system[0] with newline separators instead. This keeps all system
instructions within a single message block to prevent potential parsing or
formatting issues when the agent processes the context.
* refactor(opencode-plugin): extract composeSystemPrompt helper to centralize system prompt assembly and add unit tests
* style(opencode-plugin): remove extra newline before plan submission reminder heading
* fix(opencode-plugin): store composed prompt result before clearing system array to prevent data loss
Previously, `output.system` was cleared with `length = 0` before being passed into `composeSystemPrompt`, causing the function to compose from an empty array instead of the original system content. The fix stores the composition result in a variable first, then pushes it after clearing. Additionally, add `.trim()` in `stripConflictingPlanModeRules` to normalize whitespace before filtering empty entries, and include a test case for empty string collapse behavior.
* refactor(plan-mode.ts): move string trimming from stripConflictingPlanModeRules to composeSystemPrompt for centralized whitespace handling
* test(plan-mode): add test case for trimming trailing newlines in composeSystemPrompt
* fix(opencode): default agent switching to disabled
* fix(opencode): keep plan-approval build handoff; default no-switch for review feedback only
The agent switch cookie is shared by plan approval and code review, so
flipping the stored default to `disabled` also removed OpenCode's
plan-approval hand-off for every user who never configured the setting.
Make the unset default surface-aware instead: `getAgentSwitchSettings('plan')`
keeps the historical build hand-off, `getAgentSwitchSettings('review')`
stays on the current agent. An explicit user choice still applies to both
surfaces. Settings and the agent warning resolve the default from the mode
they render in, and the OpenCode "agent not available" warning now names
plan approval on the plan path.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat(annotate): add strict atomic result output
* feat(annotate): exit 2 for strict-gate usage and publication errors
Adopt the grep convention for the strict annotate gate's exit codes:
0 = approved, 1 = negative human outcome (annotated/dismissed under
--require-approval), 2 = the gate itself was misconfigured or could not
start/deliver a decision. Previously all usage/startup/validation
failures shared exit 1 with "reviewer did not approve", so callers could
not tell a denied review from a broken gate.
- parseStrictAnnotateOptions failures (bad flag combos, strict flags
outside annotate --gate --json) now exit 2
- --result-file preflight failures (missing parent, pre-existing or
dangling-symlink destination) now exit 2
- post-decision publication failures (destination raced into existence,
hard links unavailable, stdout write failure) now exit 2: they deliver
no decision record at all, so the code's own fail-closed handling
presents them as environment errors, never as a reviewer outcome --
and never approval, since only 0 means approved
- decision outcomes keep 0/1 exactly as before; signal deaths keep 128+n
- document the contract in AGENTS.md and the annotate-gates guide
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
* feat(annotate): preserve notes on structured approval
* test(pi): use exact annotate outcome import
* fix(annotate): exit 2 for strict-gate startup failures
The six startup-failure sites in the annotate path (missing path, unreachable
URL, empty folder, ambiguous name, missing/unsupported file, oversized file)
run after flag parsing and exited 1. Under --require-approval / --result-file,
1 is the "reviewer requested changes" signal, so a typo'd path made automation
misclassify a configuration error as a legitimate rejection.
Route those sites through exitAnnotateStartupFailure(), which picks its code
from the already-parsed strict options via the new pure helper
annotateStartupFailureExitCode(). Non-strict invocations still exit 1 with
byte-identical stderr; strict invocations exit STRICT_GATE_ERROR_EXIT_CODE (2).
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): emit the strict decision on stdout before publishing it
writeResultFile ran before the decision JSON reached stdout. On a filesystem
without hard links (exFAT, FAT32, most SMB/NFS, some container bind mounts)
publication fails deterministically, the catch exited 2 with nothing written
anywhere — and the reviewer's autosaved draft had already been deleted by the
feedback flow, so their completed decision was lost.
Emit the stdout record first, then publish the result file. Exit semantics are
unchanged: a publication failure still exits 2, but the decision has reached
stdout by then. Only a stdout write failure now leaves no record at all.
Correct the docs and comments that claimed exit 2 delivers no decision record:
it means the result *file* was not published. Also document the two publication
caveats: the 0600 mode is a no-op on Windows, and the atomic link/rename is not
followed by a parent-directory fsync, so publication is atomic but not
crash-durable.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): parse linked docs with the render-side frontmatter rule on export
buildCompleteAnnotateFeedback re-parsed each linked document with
parseMarkdownToBlocks(entry.markdown) — no options, so frontmatter
stripping defaulted on. The render side parses with
{ frontmatter: shouldStripFrontmatter(path) }.
For plain-text linked docs (.yaml/.json/.toml/…) a leading `---` is real
content, not frontmatter: a multi-document YAML opens with it. Stripping
it on the export side shifted every block id, so ordinary Send Feedback
and deny emitted wrong `(line N)` labels — or dropped them entirely when
the annotation's block no longer existed.
Pass the same shouldStripFrontmatter(filepath) option at the export call
site so both sides agree.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): carry the message scope through approve-with-notes
/api/feedback forwards selectedMessageId and feedbackScope; /api/approve
dropped them. Pi resolves the anchor message from those fields, so notes
delivered on the approve path anchored to the last message instead of the
one the reviewer picked in a multi-message annotate-last session — while
Send Feedback in the same session anchored correctly.
Forward both fields on the approve path in the Bun and Pi servers, and
have the client build the approval body with the same scope resolution
Send Feedback uses (extracted as getFeedbackMessageScope so the two can
no longer drift).
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* docs(annotate): tell agents an approval may carry notes
The skill and slash-command files still described `"decision": "approved"`
as "acknowledge and stop", with no mention of the feedback field the gate
can now attach — so an agent reading them would silently drop the
reviewer's approval notes.
Update the Claude core/claude skills, the Copilot commands, the Gemini
annotate command, and the annotate command reference so the approved
branch names the optional feedback field and says what to do with it:
carry it into subsequent work, do not treat it as a change request.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* docs(annotate): document the real approvedWithNotes default
The default annotate.approvedWithNotes template is
`{{contextBlock}}{{feedback}}`, not `{{context}}` on its own line, and
{{contextBlock}} was missing from the variable table entirely.
Show the actual default, add {{contextBlock}} to the variable table, and
explain why the default prefers it: it collapses to nothing for message
annotations instead of leaving a stray blank line.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Self-review hardening of e5fcc415: the hey-api SDK returns {error} for HTTP
failures (404 on pre-toast hosts is safe), but a fetch-level failure (host
server restarting) REJECTS the promise, and `void promise` doesn't catch
that. Both toast call sites now .catch(() => {}) when the result is
thenable, so the cosmetic toast path is strictly quieter than the
surrounding app.log pattern.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
QA traced the repeatedly-regressed remote-URL invariant to its root cause on
OpenCode: every URL path (logPlannotatorReady, the cli-bridge stderr
forwarder, and the ready-file poller) funneled exclusively through
client.app.log, which OpenCode documents as "write a log entry to the server
logs" — it is never rendered in the TUI. Remote users therefore never saw
the session URL. All three paths now ALSO call tui.showToast (the SDK's
visible surface), best-effort with optional chaining so older hosts without
/tui/show-toast no-op. A shared per-run toastedUrls set dedupes the stderr
and ready-file deliveries so one session never stacks two toasts.
Also: recognize the current binary's "Plannotator session ready" stderr
phrasing in formatUserFacingCliStderrLine (the old "Open this link" match no
longer fires; only the bare-URL line was being forwarded), and add
data-print-hide to the resize-handle cursor tooltip portal so print.css hides
it (QA print-clipping finding).
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Allow disabling URL sharing through ~/.plannotator/config.json
({ "share": "disabled" }) in addition to the PLANNOTATOR_SHARE
env var. Adds a resolveSharingEnabled() helper (env var > config >
default enabled) and routes all sharing checks through it across the
hook server, OpenCode plugin, and Pi extension. Docs updated.
Two agent command-surface cleanups. Both remove only the command entry points; all underlying infrastructure stays.
1. /plannotator-status (Pi): removed — it echoed phase/plan-file/progress on
demand, but that state is already shown ambiently (status bar + live
checklist widget). The phase/checklist state machine is untouched.
2. /plannotator-archive (all agents): removed the command/skill entry points
across every surface — Claude/Codex/Kiro skills, Pi, OpenCode (handler +
dispatch + cli-bridge + embedded + stub), Droid, the Kiro agent prompt, all
three installers, docs, marketing, and the CI deprecated-command guard. The
installers also gained a stale-skill cleanup so upgraders drop a previously
installed plannotator-archive skill.
Kept (infrastructure) — archive browsing stays available in-review via the
sidebar: the `plannotator archive` CLI subcommand (apps/hook/server), the
mode:"archive" server path + /api/archive endpoints, ArchiveBrowser/useArchive,
the sidebar Archive tab, sessions.ts "archive" mode, and ~/.plannotator/plans
storage.
Verified: bun test scripts/install.test.ts → 72 pass; pi-extension typecheck +
build:opencode pass; repo-wide residual scan clean; KEEP-set integrity
confirmed; one orphaned import (opencode commands.ts) caught in self-review and
removed.
* feat: message picker for annotate-last (#800)
When running /plannotator-last after /rewind, the newest transcript
entry is no longer the message the user intended to annotate, and there
was no affordance to pick a different one.
Adds a picker UI that surfaces the recent assistant messages so the
user can choose which one to annotate:
- A "Message N of M" button in the Viewer's sticky-top action bar
(alongside Copy / Global comment / Attachments), so it stays
accessible while scrolling.
- A "Messages" tab in the left sidebar with the full list
(newest-first, preview + timestamp, default ★), mirroring the
existing Files / Versions / Archive tab pattern.
Wired for Claude Code, Codex, and Droid (all share apps/hook/server).
OpenCode, Pi, and Copilot still get the original single-message
behavior — they don't emit recentMessages, so the picker affordances
hide cleanly.
Default selection (index 0) matches today's "last message" behavior,
so users who don't interact with the picker see no change.
* feat: extend annotate-last picker to Copilot and OpenCode
The picker UI from #800 was wired for Claude / Codex / Droid only. Pull
Copilot and OpenCode onto the same shape so users on those harnesses
also get the recent-messages picker when annotating the last assistant
message.
- Copilot: replace getLastCopilotMessage with getRecentCopilotMessages,
walking events.jsonl newest-first up to 25 assistant.message events.
- OpenCode: rewrite the session walk to collect up to 25 messages
(newest first) instead of bailing on the first hit; normalize the SDK
time.created (ms epoch) to ISO to match the shared picker contract.
- Both pass recentMessages to startAnnotateServer only when length > 1,
matching the existing Claude/Codex/Droid behavior.
Also trims a leftover narrating comment in MessagesBrowser and refreshes
the stale Copilot session-parser header.
Pi parity follows in the next commit (needs round-trip of the picker
selection through /api/feedback so its post-submit anchoring quotes the
right message).
* feat(pi): wire annotate-last picker with feedback round-trip
Extends the picker UI (#800) to Pi and fixes a Pi-specific anchoring bug
the picker would otherwise introduce.
Picker plumbing
- assistant-message: getRecentAssistantMessages walks the active branch
newest-first, returning { messageId, text, timestamp? } in the same
shape the other harnesses produce.
- Plumbed through plannotator-browser / plannotator-events so the Bun
server's recentMessages option is populated when the branch has more
than one assistant message.
Anchoring fix
- Pi quotes the targeted assistant message back to the agent because its
UX is async — the conversation may have moved on by feedback time.
With the picker, that target is no longer guaranteed to be the
snapshot taken when the UI opened. The editor now sends the user's
selectedMessageId with /api/feedback; Pi looks it up in the current
branch via findAssistantMessageByEntryId and quotes that message
instead. Falls back to the original snapshot if the entry is gone.
- The round-trip field is optional and only meaningful in annotate-last
mode; other harnesses (and other modes) ignore it.
Timestamp safety
- Pi's SDK currently types SessionEntryBase.timestamp as string, but the
picker contract everywhere else is ISO. Treat the value as unknown and
normalize string/number(ms)/Date to ISO; drop anything else, rather
than blind-casting and risking silent drift if the SDK changes.
* chore: strip issue-number references from comments
Comments shouldn't rely on external references — issue numbers age out
of context, link rot is a thing, and a reader shouldn't need to open
GitHub to understand why a line exists. Strip the `(#800)` and `(#570)`
parentheticals from comments and doc strings across the picker and
review-gate code; the surrounding "why" content is preserved.
* fix: prevent removeChild crash when switching annotate-last messages
Switching the picked message remounted nothing, so React reconciled new
content against DOM that web-highlighter had mutated with <mark> nodes,
throwing removeChild. Drive the Viewer key (and StickyHeaderLane's
remount token) off a shared viewerContentKey so a message switch fully
remounts the Viewer and re-anchors the sticky-header observer.
Also cap MessagesBrowser row previews via previewText() and drop the
redundant 'block' class that was overriding line-clamp-2.
* feat: persist annotate-last feedback across messages
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat: add PLANNOTATOR_DATA_DIR env var to customize data directory
* fix: update missed hardcoded paths to use PLANNOTATOR_DATA_DIR
OpenCode plugin and VS Code extension still used hardcoded
~/.plannotator paths, causing the IPC registry and plan backing
file to diverge from the server when PLANNOTATOR_DATA_DIR is set.
Also exports data-dir from @plannotator/shared and documents the
new env var in AGENTS.md.
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
* fix: vendor data-dir.ts into Pi extension and rewrite imports
The Pi extension copies shared/server modules into generated/ at
build time. Without vendoring data-dir.ts and rewriting the
parent-relative imports, typecheck fails on all generated files
that import getPlannotatorDataDir.
* refactor: eliminate duplicated data-dir logic and clean up call sites
- VS Code extension: replace inlined getPlannotatorDataDir() copy with
import from the canonical packages/shared/data-dir.ts (esbuild bundles
it, so no runtime dependency needed)
- storage.ts: hoist repeated getPlannotatorDataDir() calls to a
module-level DATA_DIR constant, matching the pattern config.ts uses
- data-dir.ts: remove inaccurate docstring claim about relative path
resolution (the code does not call resolve())
- improvement-hooks.ts: hoist to DATA_DIR constant, clarify comments
on the two-level hook lookup (hooks/ subdir vs root fallback)
* fix: resolve relative PLANNOTATOR_DATA_DIR to absolute path
A relative value like ./data would break readArchivedPlan's path
traversal guard, which compares a resolve()'d absolute path against
the still-relative planDir prefix. Always return an absolute path
so all callers get consistent path shapes.
* fix: use @plannotator/shared/data-dir imports in server package
Switch from relative ../shared/data-dir imports to the package
export, matching the convention every other server file follows.
Update Pi vendor script sed rules to match the new import style.
* fix: use package imports in server and respect data dir in compound skill
Server modules: switch from relative ../shared/data-dir imports to
@plannotator/shared/data-dir, matching the convention every other
server file follows. Update Pi vendor script sed rules to match.
Compound skill: update hardcoded ~/.plannotator paths to check
PLANNOTATOR_DATA_DIR first, so the skill reads plans and writes
the improvement hook to the correct location when users set a
custom data directory.
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
* fix: remove remaining hardcoded ~/.plannotator assumptions
- Settings UI: replace hardcoded path in label and placeholder with
generic text that doesn't assume a specific data directory
- quickLabels: update agent tip to reference PLANNOTATOR_DATA_DIR
so the agent checks the correct plans directory
- codex-review: hoist getPlannotatorDataDir() to module-level DATA_DIR
constant, eliminating redundant per-call resolution in debugLog()
- Tests: make submit-plan and storage tests resilient to
PLANNOTATOR_DATA_DIR being set in the environment
- Install scripts (sh, ps1, cmd): check PLANNOTATOR_DATA_DIR before
falling back to ~/.plannotator for config.json attestation lookup
* fix: expand tilde in install script and update test assertions
install.sh: PLANNOTATOR_DATA_DIR set to ~/... stays literal inside
double quotes, so the config file check silently failed. Add case
statement to expand ~ the same way the runtime data-dir.ts does.
install.test.ts: update three assertions that checked for hardcoded
~/.plannotator paths — now verify PLANNOTATOR_DATA_DIR awareness
instead.
* docs: add PLANNOTATOR_DATA_DIR to env var reference with VS Code note
Document the new env var on the marketing site's environment
variables reference page. Include a footnote about ensuring
VS Code inherits the variable when launched from the Dock.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
The backing file used for edit-based plan submission was stored at
.opencode/plans/_active-plan.md inside the workspace, causing it to
appear in git status and editor file trees. It is now stored at
~/.plannotator/active/{project}/_active-plan.md alongside the version
history.
- Move getPlanBackingPath to derive path from project name under
~/.plannotator/active/
- Derive project name from ctx.directory basename via sanitizeTag at
call site
- Delete backing file on approval since it is no longer needed after the
session ends
- Update tests to reflect new path contract
When a file has no content (lineCount === 0), any edit is a pure insert
and the end field is semantically irrelevant. The previous check
rejected payloads where end was present on an empty file because end >
lineCount always evaluated true, breaking first-call submit_plan
invocations from agents or frameworks that include end unconditionally.
- Skip end > lineCount validation when lineCount === 0; applyEdits
handles it via splice clamping
- Add applyEdits test: edit on empty file with start=1, end=1 produces
correct output
- Add validateEdits test: passes for empty file with start=1 and end=1
Fixes#742
* feat(submit-plan): replace text/file-path mode with edit-based interface
Switches the OpenCode submit_plan tool from a dual-mode interface
(inline text or file path) to an edit-based one. The plugin now owns a
backing file at .opencode/plans/_active-plan.md; the agent never reads
or writes it directly. On denial, the response includes the current plan
with line numbers so the agent can apply surgical edits instead of
resubmitting the entire document, reducing token waste on iterative
revisions.
- Add applyEdits, validateEdits, formatWithLineNumbers, and
getPlanBackingPath helpers to the plugin
- Validate edit ranges (bounds, overlap, size limit) before mutating the
backing file
- Return line-numbered plan in denial responses to anchor targeted edits
- Remove getPlanDirectory, validatePlanPath, and file-path auto-
detection from plan-mode.ts
- Replace plan-mode.test.ts path-validation coverage with submit-
plan.test.ts for the edit engine
- Update custom-feedback.md and opencode.md docs for edit-based
semantics
Refs #365
* chore(opencode): drop unused buildPlanFileRule import
Removed in PR #730 deny path along with the only call site, but the
import was left behind.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
OpenCode's command dispatcher appends `arguments` to the `.md` body and
runs `resolvePromptParts()` over the combined string, which auto-attaches
any file path it finds as a `FilePart`. With `/plannotator-annotate
/path/to/huge.md`, that meant the agent received the file's content as a
user message before the annotation UI even opened — blowing the context
on large files (GLM-5 auto-compact reported in #713).
Move `plannotator-annotate`, `plannotator-review`, and `plannotator-archive`
from the post-hoc `event` handler to `command.execute.before`, matching
the pattern `plannotator-last` already used. The hook clears `output.parts`
in place so the agent never receives the command turn; handlers then run
the UI and inject feedback via `client.session.prompt` as a separate turn.
Empty the bodies of the three `.md` files for defense in depth — only the
frontmatter is needed for OpenCode to register the slash command.
Also fixes a latent bug in the `plannotator-last` path: `output.parts = []`
reassigns the throwaway wrapper object's property but doesn't touch the
`parts` array the caller in `prompt.ts:1944` uses directly. Switched to
`output.parts.length = 0` to mutate in place. `plannotator-last` only
escaped notice because its parts array was always a single benign text
part.
PFM reminder & improvement hook support across Claude Code, OpenCode, and Pi.
- Add opt-in PFM reminder (pfmReminder config flag) injected on EnterPlanMode
- Wire composeImproveContext() into all three runtimes
- Fix OpenCode system.transform array reference bug (pushes were going to dead array)
- Fix install scripts silently stripping PreToolUse/EnterPlanMode hook entry
- Isolated Pi sandbox testing (--no-extensions -e)
Registers submit_plan tool and slash commands without modifying prompts
or agent permissions. Fills the gap between manual (commands only) and
plan-agent (full automation) for users who want to manage prompts and
permissions themselves.
* fix(remote): notify URL in remote mode for OpenCode and Pi (#551, #574)
PR #440 removed writeRemoteShareLink from OpenCode because the base64
share URL flooded the TUI — but no replacement was added. Remote users
got zero feedback about where the server was listening.
OpenCode: log the short localhost URL via client.app.log() in all onReady
callbacks (submit_plan, review, annotate, annotate-last, archive).
Pi: check isRemoteSession() directly instead of relying on the openBrowser
return value, which only sets isRemote when BROWSER env is unset. Users
running via Cursor (which sets BROWSER) now get the notification.
* fix(pi): use neutral URL notification for remote sessions
The previous wording ("Remote session. Open manually:") implied the
browser failed even when BROWSER env successfully opened it via port
forwarding. Use a neutral informational message instead.
Surface agent switching directly on the Approve button as a split
dropdown for OpenCode users, so they can pick which agent to switch
to (or disable switching) without hunting through Settings.
Also fix the "approved with notes" prompt to not say "Proceed with
implementation" when agent switching is disabled.
Closes#575, closes#114, closes#106, closes#159
Unified feedback pipeline for all plan approvals, plan denials, annotation
feedback, and review suffixes. Users customize messages via config.json with
{{variable}} template interpolation and per-runtime overrides.
Closes#624
Co-authored-by: Aviad Shiber <aviadshiber@users.noreply.github.com>
For provenance purposes, this commit was AI assisted.
Adds an opt-in review gate flow to annotation mode with three composable flags:
- `--gate`: 3-way UX (Approve / Send Annotations / Close)
- `--json`: structured decision output (`{"decision":"approved|annotated|dismissed"}`)
- `--silent-approve`: suppresses plaintext approve marker for naive hooks
Includes shared arg parser, @-reference handling, updated templates across all
harnesses (Claude Code, Copilot, Gemini, OpenCode, Pi), and full documentation.
Closes#570
For provenance purposes, this commit was AI assisted.
* Wire PLANNOTATOR_PASTE_URL through opencode/pi servers and Landing demo link
OpenCode plugin only read PLANNOTATOR_SHARE_URL; add a getPasteApiUrl helper
and thread it into plan/annotate/archive server starts. Pi extension's
serverReview gains the same shareBaseUrl/pasteApiUrl env-var pair already
used by serverPlan/serverAnnotate. Landing.tsx now accepts a shareBaseUrl
prop for self-hosters' demo link. Paste-service CORS defaults grow a
comment clarifying that self-hosters must override ALLOWED_ORIGINS.
* Embed custom paste origin in short URL fragment
When PLANNOTATOR_PASTE_URL is set to a non-default paste service, the
generated short link now includes a base64url-encoded paste param in the
fragment (#key=...&paste=...). The share portal and importFromShareUrl
extract it on load so they can fetch from the right paste backend without
needing a server — fixing broken short links for self-hosters who use a
custom paste service but keep the hosted share portal.
Backward compatible: links without a paste param continue to use the
default or server-provided paste API URL as before.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(opencode): reuse local server for review flows
Try the default local OpenCode server before spawning a new one, and resolve bundled assets and command paths correctly when the plugin is loaded from source during local testing.
* Fix typecheck after narrowing the opencode type to the sdk
Remove writeRemoteShareLink stderr output from the OpenCode plugin —
the base64 share URL was flooding the TUI on remote sessions. Also
strip leftover console.log debug statements from the PR viewed files
feature in the review server.
Closes#435
For provenance purposes, this commit was AI assisted.
* perf(opencode): lazy-load HTML to cut plugin startup from ~160ms to ~35ms
The two SPA HTML files (~20 MB combined) were inlined as string literals
via Bun's `with { type: "text" }` imports, forcing Bun to parse a 21 MB
bundle at module load time. Replace with lazy readFileSync getters and
background preload during plugin init, reducing the bundle to 0.81 MB.
Closes#410
For provenance purposes, this commit was AI assisted.
* test: add OpenCode plugin startup benchmark script
Measures real-world startup time across three scenarios:
no plugin, published npm, and local optimized. Uses
`opencode run` for non-interactive measurement and parses
log timing.
For provenance purposes, this commit was AI assisted.
* fix(bench): resolve project dir to repo root and auto-build before scenario 3
PROJECT_DIR pointed to tests/ instead of the repo root, so the local
plugin path was invalid and scenario 3 silently measured a no-plugin run.
Also auto-run build:opencode when dist/index.js is missing.
For provenance purposes, this commit was AI assisted.
* feat: add /plannotator-archive slash command for Claude Code and OpenCode
The archive browser was only accessible via CLI (plannotator archive) and
Pi (/plannotator-archive). This adds slash command parity so users can
browse saved plan decisions from within Claude Code and OpenCode sessions.
Relates to #362
For provenance purposes, this commit was AI assisted.
* fix: re-fetch archive plans with custom path in standalone mode
The server pre-loads plans from ~/.plannotator/plans/ (default) because
no runtime passes customPlanPath. For users with a custom save directory,
the plan list was wrong and clicking any plan 404'd. Calling fetchPlans()
after init() re-fetches with the user's cookie-based custom path setting.
For provenance purposes, this commit was AI assisted.
Instead of nuking OpenCode's STRICTLY FORBIDDEN plan mode message entirely
(which left the model with no prompt-level guardrails, causing it to go
rogue and edit code via bash+python), replace it with a tailored version
that allows markdown file writing while keeping all other restrictions.
Fixes#328
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore uniform submit_plan and add submit_plan_file for OpenCode
PR #318 replaced the original text-based submit_plan with a file-path-only
version and split prompt injection between plan/non-plan agents. This caused
regressions: the agent couldn't figure out file paths from non-plan agents,
and the 80+ line prompt with TodoWrite replacements was fragile against
OpenCode upstream changes.
This restores the original submit_plan(plan) that accepts markdown text
directly — the contract that worked uniformly across all agents — and keeps
the file-based workflow as submit_plan_file(path) for users who want
persistent plan files.
Key changes:
- Two tools: submit_plan (text) + submit_plan_file (path)
- Unified prompt for all primary agents (not just plan mode)
- Removed aggressive TodoWrite string replacements and system-reminder
- Kept adversarial stripping of OpenCode's STRICTLY FORBIDDEN rules
- Extracted shared server helpers to reduce duplication
Addresses #328
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: single submit_plan with auto-detect text/path for OpenCode
Collapses submit_plan + submit_plan_file back into one tool that
auto-detects whether the argument is plan text or a file path.
First submission: agent passes markdown text (simple, works from any agent).
On deny: response includes the history path where the plan was saved, so the
agent can Edit the file for targeted revisions and resubmit with the path.
Key changes:
- One tool: submit_plan(plan) accepts text or absolute .md file path
- Server surfaces historyPath through waitForDecision (already saved by
saveToHistory, just not returned before)
- Deny response includes file path hint for Edit-based revision workflow
- Unified prompt for all primary agents (~25 lines, no TodoWrite warfare)
- Still strips OpenCode's STRICTLY FORBIDDEN rules and suppresses plan_exit
Addresses #328 and rcdailey's feedback on PR #333
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove historyPath leak, error on missing file paths, restore two-tier prompt
- Remove historyPath from deny flow — internal history directory should not
be exposed to agents. Text submissions get text feedback; file submissions
get file feedback. No crossover.
- Error when agent passes an absolute .md path that doesn't exist instead of
silently treating it as plan text.
- Restore two-tier prompt: plan agent gets full planning instructions, other
primary agents get a minimal reminder (matching pre-v0.13.0 behavior).
- Clean up stale prompt copy referencing historyPath-based revision flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore todowrite override, strengthen planning prompt, remove noisy logs
- Override todowrite description to defer to submit_plan during active planning
- Tool description now instructs agent to explore and ask questions before submitting
- Sequenced planning prompt: explore, ask, then write
- Remove success logs from integration saves (stderr was bleeding through)
- Append resubmit reminder to OpenCode deny feedback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: OpenCode plan mode permissions and prompt conflicts
- Add per-agent edit permission (*.md allow) for the plan agent via
opencodeConfig.agent.plan.permission.edit, fixing the path.relative
worktree mismatch that caused PermissionDeniedError on plan writes
- Strip OpenCode's "STRICTLY FORBIDDEN" plan mode prompt from synthetic
user message parts via experimental.chat.messages.transform
- Replace conflicting TodoWrite/planning instructions in the base prompt
when plan agent is active (surgical replacements + global sweep)
- Override todowrite tool description to redirect to submit_plan
- Enhance submit_plan tool description with planning workflow guidance
- Add system-reminder reinforcing plan mode behavior on every turn
- Disable validatePlanPath directory restriction (plans can be written
anywhere)
- Strengthen planning prompt with explicit anti-TodoWrite language,
required workflow summary, and mkdir instruction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove debug logging from plugin hooks
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last command to annotate last assistant message
Adds a new slash command that extracts the last rendered assistant message
from Claude Code's session log and opens it in the annotation UI.
Session log parser (apps/hook/server/session-log.ts):
- Parses Claude Code JSONL logs at ~/.claude/projects/{slug}/*.jsonl
- Finds the last assistant message.id with text content blocks
- Skips noise entries (progress, system, file-history-snapshot, queue-operation)
- Filters system-generated user messages by prefix to avoid false turn boundaries
- Walks backward through empty turns when back-to-back user messages exist
- No anchoring — reads from end of log since <command-message> isn't written
until after the binary completes
New files:
- apps/hook/commands/plannotator-last.md — slash command definition
- apps/hook/server/session-log.ts — Claude-Code-specific log parser
- apps/hook/server/session-log.test.ts — 30 tests covering streaming chunks,
tool call turns, sub-agent noise, stop hooks, thinking blocks, and edge cases
Modified:
- apps/hook/server/index.ts — annotate-last subcommand
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove 3 redundant real-world scenario tests
These duplicated coverage already provided by focused unit tests:
- "full conversation" → covered by "grabs last message.id in multi-tool turn"
- "stop hook interrupted" → covered by "skips progress and system noise"
- "long tool-only sequence" → covered by "skips tool-only assistant entries"
Kept the thinking block test (unique coverage). 27 tests remain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last command to Pi extension
Uses Pi's session manager API to find the last assistant message —
walks backward through ctx.sessionManager.getEntries(), finds the
last entry with role "assistant" and text content, opens it in the
annotation UI. Reuses existing isAssistantMessage(), getTextContent(),
startAnnotateServer(), and runBrowserReview() from the extension.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last to OpenCode plugin + extract command handlers
Adds annotate-last command that fetches session messages via
client.session.messages(), finds the last assistant message with text
parts, and opens it in the annotation UI.
Refactors command handling: extracts review, annotate, and annotate-last
handlers from the inline event hook into commands.ts module. Reduces
index.ts by ~120 lines and makes adding future commands cleaner.
New files:
- apps/opencode-plugin/commands.ts — extracted command handlers
- apps/opencode-plugin/commands/plannotator-last.md — command metadata
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: context-aware UI labels for annotate-last mode
Adds "annotate-last" mode to the annotate server, passed through to the
UI via /api/plan response. The editor uses this to show "Copy message"
instead of "Copy plan", and "annotations on the message" in the
completion overlay.
- packages/server/annotate.ts: new `mode` option on AnnotateServerOptions
- packages/editor/App.tsx: annotateSource state derived from mode
- packages/ui/components/Viewer.tsx: copyLabel prop for button text
- All three harnesses pass mode: "annotate-last" in their callers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Codex support to annotate-last command
Detects Codex via CODEX_THREAD_ID env var (injected by Codex into every
spawned process). Uses the thread ID to find the rollout file in
~/.codex/sessions/, parses the Codex rollout JSONL format to extract
the last assistant message.
Also adds `plannotator last` alias for shorter usage in Codex bang
commands (!plannotator last).
New files:
- apps/hook/server/codex-session.ts — Codex rollout parser
- apps/hook/server/codex-session.test.ts — 9 tests
Modified:
- apps/hook/server/index.ts — Codex detection + `last` alias
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: context-aware feedback title + top spacing for paragraph-first content
- exportAnnotations now accepts a title param: "Message Feedback" for
annotate-last, "File Feedback" for file annotation, "Plan Feedback"
for plan review (default)
- Adds top spacer when content starts with a paragraph (not a heading)
and has no frontmatter, fixing tight spacing in annotate-last mode
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add sandbox scripts for Pi and Codex testing
- sandbox-pi.sh: builds extension, creates temp project, installs via
`pi install`, launches Pi with sample files
- sandbox-codex.sh: compiles binary, creates temp project, launches
Codex. Test with `!plannotator last`
Both follow the same pattern as sandbox-opencode.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add hook build step to opencode sandbox script
The opencode build copies HTML from hook/dist/ — without building hook
first, the sandbox could use stale HTML. Pi and Codex sandboxes already
had this step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove command body from plannotator-last to prevent agent response
The .md body was being sent to the agent as a prompt, causing it to
respond with "Opening annotation UI..." before the event handler could
fetch messages. That response became the "last message" instead of the
actual one. Empty body = agent stays silent, event handler intercepts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use command.execute.before hook for OpenCode annotate-last
Moves plannotator-last from the passive event hook to the
command.execute.before hook. This intercepts the command before the
agent sees it, clears output.parts so the agent stays silent, fetches
session messages, opens the annotation UI, then sends feedback via
client.session.prompt() — same pattern as review/annotate.
Previously the agent would respond to the command body before the
event handler could fetch messages, polluting the session history.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add Codex to origin type and agent name mapping
Origin "codex" was falling through to the default "Coding Agent" label.
Added "codex" to the origin union type across annotate server, editor,
and removed the `as any` cast in the hook.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remote share link, plan-specific prose, and codex type unions
- Add writeRemoteShareLink to annotate-last onReady callback so remote
sessions get a reachable URL
- Add subject parameter to exportAnnotations so feedback says "message"
or "file" instead of "plan" when appropriate
- Add 'codex' to origin type unions in useAgents, Settings, UpdateBanner,
and App.tsx fetch handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct JSDoc for projectSlugFromCwd (leading dash is kept, not stripped)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: use RenderedMessage type instead of inline structural type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: extract checklist utilities to shared package
Move ChecklistItem, parseChecklist, extractDoneSteps, and
markCompletedSteps from Pi extension to @plannotator/shared
for reuse by the OpenCode plugin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Pi-style iterative planning for OpenCode plugin
Rewrite the OpenCode plugin to match Pi's planning methodology:
- Inject rich iterative planning prompt when agent is "plan"
(explore → update plan → ask user loop, structured plan format)
- File-based submit_plan: reads plan from disk instead of requiring
it as a string arg. Resolves path from OpenCode's system prompt,
falls back to PLAN.md
- Suppress plan_exit via tool.definition hook (directs to submit_plan)
- Add PLANNOTATOR_ALLOW_SUBAGENTS env var for #289
- Pass planFilePath to denial feedback template
- Keep existing subagent/build/title guards intact
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review findings in OpenCode plugin
- Cache agents list (static per session, was fetched every LLM call)
- Remove TOCTOU: drop redundant file.exists() before file.text()
- Eliminate duplicate system.join() by reusing joined string
- Resolve getSharingEnabled() once in submit_plan instead of twice
- Use path.join() instead of string concatenation for file paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: session-scoped plan files and tool-neutral prompt
Replace plugin-global resolvedPlanFilePath and prompt-path parsing with
per-session plan files at ~/.plannotator/session-plans/opencode/{id}/plan.md.
Remove resolvePlanFilePath() regex (fixes spaces-in-path and cross-session
race). Make planning prompt tool-neutral (no write/edit references) so it
works with apply_patch models. Strip OpenCode's native read-only and
experimental-mode prompt lines before injecting Plannotator's planning prompt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: directory-based plan storage with path validation
Replace session-scoped plan files with a shared plan directory at
~/.plannotator/session-plans/opencode/. Agent picks the filename,
submit_plan takes a path arg and validates it (absolute, inside plan
dir, exists, readable, non-empty) with canonical path checks to
defeat traversal and symlink escapes. Remove summary and plan string
args. Planning prompt is now tool-neutral and directs the agent to
reuse the same file on feedback, not create new ones.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use OpenCode's native plans directory for permission compatibility
OpenCode's plan mode permission ruleset only allows edits to
.opencode/plans/*.md and $XDG_DATA_HOME/opencode/plans/*.md.
Our custom ~/.plannotator/session-plans/opencode/ path was blocked
by PermissionDeniedError at the tool level regardless of prompt
stripping. Switch to the XDG path that OpenCode already allows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: exploration-first planning prompt
Restructure the planning prompt as a phased workflow:
Explore → Ask → Write → Submit. The agent now explores the
codebase before creating a plan file or asking questions,
producing better-researched plans for existing codebases.
Greenfield tasks can skip straight to questions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: fix stale path in JSDoc header comment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: shared feedback templates across all integrations
The deny/feedback prompts sent to LLM agents were duplicated as inline
string templates in hook, opencode-plugin, and pi-extension — each with
different tone and framing. The hook's directive style (from #224) was
the most effective at getting agents to address feedback. This extracts
all feedback text into @plannotator/shared/feedback-templates and has
every integration import from the single source of truth.
Closes#215 follow-up (propagates fix to OpenCode and Pi).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: rewrite feedback template tests around contracts not implementation
Tests now verify: cross-integration consistency, verbatim feedback
preservation, empty input handling, and that approved messages don't
contain directive language. Wording can change freely without breaking
tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: instruct agent to preserve plan title on resubmission (#296)
Version history slugs are derived from the plan's first # heading.
When the agent renames the heading after a deny, the version chain
breaks and the user loses diffs. The deny template now tells the
agent not to change the title unless explicitly asked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: improve plan deny preamble readability
Break the dense single-paragraph preamble into structured sections:
verdict, directive, and rules list. Easier for agents to parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add missing @plannotator/shared workspace dependency
Hook and OpenCode plugin imported from @plannotator/shared/feedback-templates
without declaring it as a dependency. Worked locally but failed in CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* revert: remove code review and annotate from shared templates
Scope-crept into code review/annotate feedback which introduced a double
heading regression and dropped integration-specific strings. Reverts those
paths to their original inline strings; shared module now only covers
plan deny feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore Pi plan file hint and vendor template for source installs
Add optional planFilePath to planDenyFeedback so Pi can tell the agent
to read the plan file before editing. Check in a vendored copy of the
template so Pi source installs work without running build:pi first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't ask agent to address feedback on LGTM approval (#284)
When the reviewer approves with no annotations, send a neutral
"Code review completed — no changes requested." message instead of
the contradictory "LGTM" + "Please address this feedback."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use explicit approved flag instead of annotations.length heuristic
The previous fix inferred LGTM from an empty annotations array, but
VS Code editor annotations are carried in feedbackMarkdown without
populating the annotations array — causing real review comments to be
misclassified as approvals. Thread an explicit `approved` boolean from
the UI through the review server to all three integrations.
Closes#284
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: append assertive instruction to review feedback output
When the reviewer submits actual feedback, append "The reviewer has
identified issues above. You must address all of them." so the agent
treats annotations with urgency rather than soft-acknowledging them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: annotate unused LGTM feedback string
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add case-insensitive file resolution so users can open markdown files
without typing exact paths. Applies to both the CLI (`plannotator
annotate`) and the `/api/doc` endpoint for linked documents.
Resolution strategies (tried in order):
1. Exact path — absolute or relative to cwd (existing behavior)
2. Case-insensitive relative path — `docs/setup.md` matches `docs/SETUP.md`
3. Bare filename search — `setup.md` searches the entire project tree
Ambiguity handling:
- 1 match → opens the file, prints "Resolved: /full/path"
- 0 matches → "File not found: <input>"
- 2+ matches → "Ambiguous filename: found N matches" with full paths
Skips node_modules, .git, dist, build, .next, __pycache__, .obsidian,
.trash during search. Restricts results to .md/.mdx/.markdown files
and enforces project root boundary (no path traversal).
Extracted shared resolveMarkdownFile() into packages/server/resolve-file.ts
and refactored /api/doc handler to use it, removing ~40 lines of inline
resolution logic.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* 🐛 fix: force standard a/b diff prefix to handle mnemonic prefix config
When diff.mnemonicPrefix is enabled in git config, `git diff` uses
context-dependent prefixes (c/ for commit, w/ for worktree, i/ for
index) instead of the standard a/b. Both the internal parseDiffToFiles
and @pierre/diffs library expect a/b prefixes, causing silent parse
failures that result in an empty file list.
Add --src-prefix=a/ --dst-prefix=b/ to all git diff invocations to
ensure consistent output regardless of user git configuration.
* ✨ feat: propagate git diff errors to review UI
Previously, git diff errors were silently caught and returned as empty
patches. The UI showed "No changes" with no indication of failure.
- Add error field to DiffResult and propagate through ReviewServerOptions
- Include error in /api/diff and /api/diff/switch responses
- Show distinct error state in review UI (red icon + error message)
- Clear/set error state on diff type switch
* Fix mnemonic prefix handling in pi-extension runGitDiff
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a 10-minute timeout on waitForDecision() with proper clearTimeout cleanup, and use writeRemoteShareLink for remote URL notification instead of os.hostname()/localhost.
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add PLANNOTATOR_SHARE_URL env var so users can point share links at
their own self-hosted portal instance instead of share.plannotator.ai.
Threads the base URL through the same path as sharingEnabled: env var →
server options → API response → editor state → useSharing hook →
generateShareUrl(). Includes self-hosting guide and documentation.
Closes#12
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add agent filtering to experimental.chat.system.transform hook to prevent
injecting plan submission prompts into agents that shouldn't receive them:
- Hardcoded exclusion for "build" agent
- Dynamic exclusion for agents with mode "subagent" (e.g., general, explore)
- Skip injection if agent detection fails (safer behavior)
The plan agent and other primary agents will continue to receive the prompt.
Co-authored-by: cognitive <152830360+METAeuPHORIC@users.noreply.github.com>