feat(annotate): approve/annotate/dismiss flow (#570)#606
Draft
backnotprop wants to merge 13 commits intomainfrom
Draft
feat(annotate): approve/annotate/dismiss flow (#570)#606backnotprop wants to merge 13 commits intomainfrom
backnotprop wants to merge 13 commits intomainfrom
Conversation
Draft PR placeholder. Design in description. For provenance purposes, this commit was AI assisted.
This was referenced Apr 23, 2026
Parses --gate and --json flags from a raw args string for the OpenCode plugin and Pi extension (both receive pre-joined arg strings from their harness slash-command dispatchers). Claude Code's binary continues to use argv indexOf/splice directly. Part of #570. For provenance purposes, this commit was AI assisted.
- Add gate?: boolean to AnnotateServerOptions (Bun) and the Pi server's options type so the UI can render the Approve button. - Add approved?: boolean to the annotate decision type in both servers. - Include gate in the /api/plan response so the client knows which UX variant to render. - New /api/approve endpoint that resolves the decision with approved=true and empty feedback (mirror of /api/exit but semantically distinct). Route parity test (tests/parity/route-parity.test.ts) stays green. Part of #570. For provenance purposes, this commit was AI assisted.
When the server sets gate=true on /api/plan (#570), render a third Approve button alongside Close + Send Annotations. New handler POSTs to /api/approve. Completion overlay copy differentiates 'Approved' (annotate-mode approve) from 'Plan Approved' (plan-mode approve). For provenance purposes, this commit was AI assisted.
- Add --gate and --json argv parsing next to --no-jina. Both are orthogonal; matrix is documented in #570 and in the draft PR body. - Thread gate through all three startAnnotateServer call sites (annotate, annotate-last, copilot-cli annotate-last). - Replace three identical stdout blocks with a shared emitAnnotateOutcome helper that implements the 4-case matrix. - Drop the "Annotation session closed without feedback." line — Close now emits truly empty stdout so naive PostToolUse hooks (empty = allow, non-empty = block) work out of the box. - Update plannotator-annotate.md and plannotator-last.md templates so the agent handles empty stdout gracefully. For provenance purposes, this commit was AI assisted.
Both handlers now call parseAnnotateArgs on the slash-command args string to separate --gate and --json from the target path. Gate flag threads through to startAnnotateServer so the editor renders the three-button UX on request. --json is silently accepted: OpenCode writes back to the session via client.session.prompt, not stdout, so there's no channel for JSON. Accepting it without error keeps hook recipes portable across harnesses. Session-injection logic now treats approved the same as exit: skip the prompt injection. Annotate feedback still injects as before. Part of #570. For provenance purposes, this commit was AI assisted.
- vendor.sh: add annotate-args to the list of vendored shared modules so Pi gets its own generated/annotate-args.ts at build time. - index.ts: parseAnnotateArgs splits --gate / --json from the path on both /plannotator-annotate and /plannotator-last. --json is silently accepted (Pi writes via sendUserMessage, not stdout). Both handlers branch on result.approved (notify only) vs result.exit vs result.feedback (inject as user message). - plannotator-browser.ts: openMarkdownAnnotation and openLastMessageAnnotation take an optional `gate` param and return approved?: boolean in the decision. - plannotator-events.ts: extend PlannotatorAnnotatePayload with gate? and PlannotatorAnnotationResult with approved? for the outbound third-party consumer API. Internal event handlers thread gate through to openMarkdownAnnotation / openLastMessageAnnotation. Part of #570. For provenance purposes, this commit was AI assisted.
- Annotate: full Flags section with stdout matrix + key-property callout + note on --json semantics across harnesses. Server API table gains /api/approve. - Annotate-last: short Flags section pointing back to annotate's matrix + Stop-hook usage teaser. - New guide: hook-integration.md with two ready-to-copy recipes (PostToolUse spec gate + Stop-hook turn gate) in both plaintext and --json variants, plus OpenCode/Pi notes and gotchas. For provenance purposes, this commit was AI assisted.
… recipe Two self-review fixes: 1. editor: when --gate is on and the user presses Cmd/Ctrl+Enter with no annotations, call handleAnnotateApprove instead of handleAnnotateFeedback. Previously this would POST empty annotations, which produces the "User reviewed the document and has no feedback." boilerplate string on stdout — blocking naive PostToolUse hooks on a meaningless signal. Now the keyboard shortcut matches the visible primary action (Approve button) in the no-annotations case. 2. docs: the --json hook recipe previously used `exit 2` to signal "block" for the annotated case, but Claude Code PostToolUse treats stderr (not stdout) as the block-reason channel on exit 2. The binary's native plaintext --gate mode blocks via stdout + exit 0, and the --json recipe should mirror that contract exactly. Rewrote the case handler to always exit 0 and signal via stdout presence. For provenance purposes, this commit was AI assisted.
…prove
Three parallel structures merged into their existing counterparts:
1. Remove the second <ApproveButton> render I had added inside the
annotate branch. Widen the existing plan-mode render's condition
from {!annotateMode} to {(!annotateMode || gate)} and branch the
onClick on annotateMode. One render site, two modes.
2. Reuse the existing showExitWarning ConfirmDialog for the
annotate-gate Approve guardrail. New exitWarningAction state
('close' | 'approve') carries which button opened it; dialog's
onConfirm routes to handleAnnotateExit vs handleAnnotateApprove
and confirmText/message swap accordingly. Single dialog serves
both destructive actions.
3. Extract hasAnyAnnotations useMemo. Replaces the four-term inline
check in the annotate-mode Close button, Send Annotations render,
and Cmd+Enter handler.
No behavior change vs the previous commit; this is pure consolidation.
For provenance purposes, this commit was AI assisted.
The Claude Code slash command template for plannotator-last was invoking `plannotator annotate-last` without forwarding the user's arguments, so `/plannotator-last --gate` and `/plannotator-last --json` were silently ignored on Claude Code even though the binary parses both flags, the annotate-last binary path threads gate through to the server, and the docs explicitly cover the Stop-hook recipe. Parity with /plannotator-annotate (forwards $ARGUMENTS) and /plannotator-review (forwards $ARGUMENTS). One-line fix. For provenance purposes, this commit was AI assisted.
When a user passes --json via /plannotator-annotate or /plannotator-last,
the binary emits structured decision objects like {"decision":"approved"}
or {"decision":"dismissed"}. The existing template prose only distinguished
empty vs non-empty stdout, so approved and dismissed JSON markers were
falling into the "address the feedback" branch and confusing the agent.
Both templates now recognize approved/dismissed JSON as equivalent to
empty stdout ("no changes requested, stop"), and tell the agent to pull
the feedback field when the decision is annotated.
For provenance purposes, this commit was AI assisted.
The command.execute.before dispatch was synthesizing a fake event with only sessionID, dropping the raw argument string before it reached handleAnnotateLastCommand's parseAnnotateArgs. Result: /plannotator-last --gate and --json silently no-op'd on OpenCode even though the docs and handler claimed support. Confirmed via OpenCode source that input.arguments on command.execute.before carries the raw tail string (the hook isn't in the typed plugin API but is populated at runtime — see packages/plugin/src/index.ts in opencode-ai/opencode). Also: document parseAnnotateArgs's known whitespace-tokenizer limitations (double-spaces in paths, literal --gate/--json in path names) in a comment. Fix not pursued — dev-context paths with those shapes are too rare to justify a full shell-style tokenizer. For provenance purposes, this commit was AI assisted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #570.
Design
Adds a
--gateflag toplannotator annotateandplannotator annotate-lastthat enables a three-way review UX (Approve / Annotate / Close), plus an orthogonal--jsonflag for structured hook-friendly output. Ships with full parity across Claude Code, OpenCode, and Pi.Stdout contract
Key property: in
--gatemode without--json, Approve and Close are both empty stdout, so naive PostToolUse and Stop hooks work out of the box. Only Annotate blocks. Add--jsononly when you need explicit approved-vs-dismissed telemetry.Parity across harnesses
--gateis wired--gatefrom args, passesgate: truetostartAnnotateServer. Decision object flows through plugin's injection path: approved/dismissed skip session prompt injection, annotated injects feedback.Shipped slash commands stay 2-button by default in all three harnesses. Users opt in per invocation:
/plannotator-annotate spec.md --gate.What ships (unconditional)
plannotator-annotate.md+plannotator-last.md) to handle empty-stdout gracefully.What ships (behind
--gate)/api/approveendpoint inpackages/server/annotate.ts.approvedfield in annotate decision object.packages/editor/App.tsx).gateprop passed through/api/planto client.What ships (behind
--json){"decision":"approved"|"annotated"|"dismissed","feedback":"..."}.--gate.Recipes (ships as docs)
PostToolUse spec gate (Claude Code). For spec-driven frameworks (spec-kit, kiro, openspec):
```bash
plannotator annotate --gate
```
Stop-hook turn gate (Claude Code). For turn-by-turn review:
```bash
plannotator annotate-last --gate
```
Future (not in this PR)
--gateper invocation.For provenance purposes, this PR was AI assisted.