Skip to content

feat(annotate): preserve notes on structured approval#1092

Open
rNoz wants to merge 5 commits into
backnotprop:mainfrom
rNoz:rnoz/feat-annotate-approve-with-notes
Open

feat(annotate): preserve notes on structured approval#1092
rNoz wants to merge 5 commits into
backnotprop:mainfrom
rNoz:rnoz/feat-annotate-approve-with-notes

Conversation

@rNoz

@rNoz rNoz commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Built on top of #1091

Important/Disclaimer (same note in all my PRs): This is not spam (I created 4 PRs after the weekend of hard work + 2 from previous weeks), just my willing to help: I have been stress testing this tool (thank you for Plannotator!), finding some enhancements, proposing a couple of improvements and fixing some issues. I keep all these features/fixes in topic branches (to be able to publish them, now here as PRs), but I have been using all them in a local integration branch that I use to run plannotator with all these contributions (I use it in all projects and CLI harnesses I own). Therefore, apart from the tests per branch and ad hoc manual tests, I have been using it flawlessly. I decided to create the PRs, in case they are useful for you. Feel free to drop, update, extend.... anything you want. I am willing to contribute, so if you prefer that I tackle something or rebase/solve merge conflicts in the future (At 2026-07-20: all is ready!), please, don't hesitate to ping me. I am sharing this amount of work in case it is useful for you all.
The plannotator "binary" that I use many times per day, it is based on the integration branch that has these PRs (branches cherrypicked) in order:

image

Summary

Annotate gates now preserve complete reviewer feedback when their outcome transport can deliver notes with approval (saving another unnecessary turn with the Agent).

Bun and Pi advertise an explicit approvalNotesSupported capability. Direct gated JSON, active OpenCode sessions, and Pi follow-up delivery opt in. When capable feedback exists, the UI presents Approve with Notes and confirms that the artifact will be approved, the notes are non-blocking guidance rather than a revision request, and the gate will close.

Plaintext and hook-native output remain unchanged. Unsupported approval paths retain the existing warning and Send Feedback action. ;)

Fixes #930

Why

Annotate approval previously posted no body and resolved an empty approved decision. A reviewer who left small non-blocking notes had to choose between silently losing them and using Send Feedback, which means revise and reopen.

Structured transports can carry an approved decision and feedback together. Making that capability explicit lets the editor preserve notes without changing transports whose approval protocol has no feedback channel.

Changes

  • Add approvalNotesSupported to Bun and Pi annotate server options and /api/plan payloads with a default of false.
  • Enable approval notes for direct --gate --json without --hook, OpenCode embedded sessions with a session ID, gated OpenCode CLI bridge launches with a session ID, and Pi annotation sessions.
  • Require an active OpenCode session before launching a gated capable CLI bridge review.
  • Accept optional validated JSON on /api/approve, preserve feedback and annotations, keep bodyless approval compatible, and reject malformed or wrong-type bodies with 400 without resolving.
  • Add packages/editor/annotateSubmission.ts to compose complete feedback and build capability-aware approval requests.
  • Include text annotations and images, global attachments, code annotations, direct edits, linked-document annotations, editor annotations, and saved-file changes in capable approval notes.
  • Change the primary action and confirmation title to Approve with Notes only when capable feedback exists.
  • Use the tested confirmation copy: “This approves the artifact, sends your notes as non-blocking guidance, and closes the gate. Unlike Send Feedback, it does not request changes.”
  • Keep unsupported transports on the existing warning and Send Feedback path rather than silently dropping notes.
  • Add a configurable annotate.approvedWithNotes prompt while preserving the existing file or message context.
  • Make the default prompt explicitly say that the artifact is approved, the notes are non-blocking and not a revision request, the agent must not revise or reopen solely because of the notes unless the user explicitly asks, and the guidance should carry into subsequent work where applicable.
  • Deliver approved feedback through the existing OpenCode file/message prompt paths.
  • Log OpenCode delivery failures and surface them as typed errors.
  • Keep auto mode from classifying a delivery failure as a startup failure and relaunching a duplicate gate.
  • Deliver Pi notes through its follow-up channel before sending the approval notification.
  • Keep approval without feedback as a no-op for the agent.
  • Extract direct annotate output to apps/hook/server/annotate-output.ts, preserve plaintext and hook output byte-for-byte, and include nonempty feedback only in direct JSON approval.
  • Document capability-based approval behavior and why --hook remains unchanged.

Validation

Focused rebased proof

  • Approval-note tests: 108 passed with 253 assertions across eight files.
  • Coverage includes prompts, prompt-config integration, editor submission and presentation policy, OpenCode commands, CLI bridge, embedded delivery and error classification, and Pi outcomes.
  • The editor presentation test pins the exact non-blocking confirmation copy.
  • bun run typecheck: passed.
  • bun run build:hook: passed; Vite transformed 5,582 modules.
  • bun run build:opencode: passed; bundled 690 embedded modules and 90 Node modules.
  • bun run build:pi: passed; the build rebuilt the review app with 3,511 modules and the hook app with 5,582 modules.
  • git diff --check: passed.

In the integration branch combining the main PRs in-flight:

  • Serialized full suite: 2,221 passed, 99 skipped, 0 failed, and 6,125 assertions across 193 files.
  • Typecheck passed.
  • Marketing build passed with 40 pages.
  • Hook, OpenCode, and Pi builds listed above passed.

Live proof (ad hoc tests for this branch)

However, I have been using it for days as part of the integration branch, in real sessions (multi-cli harness, multi-project).

  • Factory strict direct flow approved with a non-empty note after an annotated/revision round.
  • Copilot strict direct flow approved with Some fake note, exited 0, and its authoritative result was consumed.
  • In the final rebuilt integration UI gate, the user verified the Approve with Notes label and exact confirmation wording, then approved with Global note!. Retained validation/live/approval-ui-v2.stdout.log and
    validation/live/approval-ui-v2.result.json contain the same JSON line. No final-UI process exit code was captured; automated tests pin the confirmation copy and prove approved exit 0 plus exact stdout/result bytes.

These runs validate the direct structured transport and rebuilt UI. They do not validate native OpenCode or Pi delivery.

Example of usage E2E with the integration branch:

Screen.Recording.2026-07-20.at.15.44.17.mov
image

Manual testing gaps

  • No live native OpenCode agent session was used.
  • No live native Pi agent session was used.
  • No Windows or Linux run was performed.
  • Hook and plaintext compatibility are covered by automated formatter tests rather than a live hook execution.
  • The hook intentionally cannot carry approval notes.

I normally don't use plan mode / exitPlanMode hook so I relied on the tests (hooks are intentionally not modified), but if you are interested in this contribution and you want that I expand the manual E2E tests, I'll be happy to run in normal plan mode to have another check of how it works as expected (not being affected).

Compatibility

The server capability defaults to false, and bodyless /api/approve remains valid. Existing callers that do not opt in retain their current approval behavior.

Direct plaintext approval still emits The user approved.. Hook approval and close still emit empty stdout, while hook feedback still emits the existing blocking JSON. --hook is unchanged because its native approval path has no feedback channel.

Send Feedback continues to mean revise and reopen. Approval notes are used only when the transport can represent "approved, with non-blocking guidance." The default approval-note prompt tells the receiving agent not to revise or reopen solely because notes were supplied unless the user explicitly requests it.

Prior art and overlap

GitHub overlap was refreshed on 2026-07-20. Open issue #930, “Approve with annotations,” directly requests this annotate approval-notes behavior. This PR fixes #930; no duplicate issue should be opened.

Issue #570 established annotate gate decisions, but approved results still lack notes. Closed issue #30 requested "Approve with comments"; the maintainer explicitly requested confirmation and shipped OpenCode plan support in 0.4.8.

#30 is plan-mode prior art, while #570 established gate decisions. This contribution implements #930 and provides annotate-gate transport parity for direct structured output, OpenCode, and Pi. It does not add Claude hook support.

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
Maintainer-decided follow-ups on the approve-with-notes feature:

- Revert the Mod+Enter rebind: with annotations present, Mod+Enter means
  Send Feedback exactly as before, on every transport. Approve-with-Notes
  stays available via the header button and its confirmation dialog. The
  shortcut registry description ("approves when there are no annotations,
  sends feedback when there are") is accurate again.
- Approve now includes feedback based on hasFeedbackToSend (not
  hasFeedbackContent), so notes already delivered through the annotate
  agent terminal are not re-sent as approval notes.
- getAnnotateApprovedWithNotesPrompt: spread vars before the context
  defaults so the message-annotation path's `context: undefined` cannot
  clobber them and leave a literal {{context}} in custom templates; add a
  test pinning the message case with a custom template.
- copilot-last now passes approvalNotesSupported like the annotate and
  annotate-last sites, so `copilot-last --gate --json` supports
  notes-on-approve.
- Dedupe AnnotateOutcome: declared once in strict-annotate-result.ts,
  re-exported by annotate-output.ts.

Also merges the updated strict-result branch (exit 2 for gate errors).

Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Approve with annotations

2 participants