Skip to content

feat(annotate): approve/annotate/dismiss flow (#570)#606

Merged
backnotprop merged 26 commits intomainfrom
feat/annotate-approve
Apr 24, 2026
Merged

feat(annotate): approve/annotate/dismiss flow (#570)#606
backnotprop merged 26 commits intomainfrom
feat/annotate-approve

Conversation

@backnotprop
Copy link
Copy Markdown
Owner

@backnotprop backnotprop commented Apr 23, 2026

Resolves #570.

Design

Adds a --gate flag to plannotator annotate and plannotator annotate-last that enables a three-way review UX (Approve / Annotate / Close), plus an orthogonal --json flag for structured hook-friendly output. Ships with full parity across Claude Code, OpenCode, and Pi.

Stdout contract

     Flags      │        UX        │         Approve         │          Close           │                 Annotate
─────────────── ┼──────────────────┼─────────────────────────┼──────────────────────────┼───────────────────────────────────────────────
 (none)         │  2-button        │  n/a                    │  empty                   │  feedback (plaintext)
 --gate         │  3-button        │  empty                  │  empty                   │  feedback (plaintext)
 --json         │  2-button        │  n/a                    │  {"decision":"dismissed"}│  {"decision":"annotated","feedback":"..."}
 --gate --json  │  3-button        │  {"decision":"approved"}│  {"decision":"dismissed"}│  {"decision":"annotated","feedback":"..."}

Key property: in --gate mode without --json, Approve and Close are both empty stdout, so naive PostToolUse and Stop hooks work out of the box. Only Annotate blocks. Add --json only when you need explicit approved-vs-dismissed telemetry.

Parity across harnesses

Harness How --gate is wired
Claude Code Binary CLI flag. Stdout contract per matrix. Works with PostToolUse and Stop hooks.
OpenCode Plugin's slash-command handler parses --gate from args, passes gate: true to startAnnotateServer. Decision object flows through plugin's injection path: approved/dismissed skip session prompt injection, annotated injects feedback.
Pi Same as OpenCode. Event handler parses arg, passes through to server.

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)

  • Drop `"Annotation session closed without feedback."` stdout line. Close truly emits empty.
  • Slash-command template rewrite (Claude Code plannotator-annotate.md + plannotator-last.md) to handle empty-stdout gracefully.

What ships (behind --gate)

  • New /api/approve endpoint in packages/server/annotate.ts.
  • New approved field in annotate decision object.
  • Approve button in 3-button annotate UI (packages/editor/App.tsx).
  • gate prop passed through /api/plan to client.

What ships (behind --json)

  • Structured JSON stdout mode: {"decision":"approved"|"annotated"|"dismissed","feedback":"..."}.
  • Works with or without --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)

  • User-level setting to always show the gate UI without needing --gate per invocation.

For provenance purposes, this PR was AI assisted.

@punk-dev-robot
Copy link
Copy Markdown

This is huge improvment and can't wait to start using it! The --gate flow feels very natural for use of annotate in hooks, and the stdout contract also works great here.

I did a careful pass on the current branch and the main behavior looks good from my side:

  • plaintext --gate: approve/close stay silent, annotate emits feedback
  • --json: approved/dismissed/annotated all produce the expected structured decisions

I also really appreciate the extra polish here, especially around plannotator-last arguments and cmd/ctrl+enter for approving

I did hit one regression that still reproduces for me on the current branch tip:

apps/opencode-plugin/commands.ts seems to have lost some newer main behavior while adding --gate support.

Repro:

  • /plannotator-annotate docs logs Opening annotation UI for docs... and then File not found: docs
  • /plannotator-annotate docs/plan.md also fails when the file exists under ctx.directory but not under process.cwd()

So it looks like handleAnnotateCommand() is no longer using deps.directory, and the folder-annotation path from current main is no longer preserved in the OpenCode handler.

Everything else I checked around the new gate flow looks solid, so this feels more like stale-branch drift than a problem with the feature itself.

Happy to help if useful: i can add some targeted tests arount that path, fix restoring the OpenCode path/folder behavior, or both :)

@backnotprop
Copy link
Copy Markdown
Owner Author

Quick amendment to the matrix above.

--gate plaintext Approve now emits the line The user approved. instead of empty stdout. Reason: slash command templates needed to distinguish Approve from Close in plaintext without forcing --json everywhere. Empty-on-both collapsed those two decisions for any agent reading stdout.

Added --silent-approve as the opt-out so naive hooks (any stdout = block) still work. For spec-kit style PostToolUse:

plannotator annotate "$FILE" --gate --silent-approve

Approve and Close both emit empty; only Send Annotations blocks with feedback. That's the silence-is-permission shape originally agreed on.

Updated matrix:

Flags Approve Close Annotate
--gate The user approved. empty feedback
--gate --silent-approve empty empty feedback
--json n/a {"decision":"dismissed"} {"decision":"annotated","feedback":"..."}
--gate --json {"decision":"approved"} {"decision":"dismissed"} {"decision":"annotated","feedback":"..."}

--json behavior unchanged. --silent-approve is a no-op in --json mode (JSON always routes by decision field).

Sorry for the late amendment. Landing in the next release.

@backnotprop backnotprop marked this pull request as ready for review April 23, 2026 22:56
@backnotprop backnotprop requested a review from Copilot April 23, 2026 22:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in “review gate” flow to annotation mode so users (and hook-driven agent workflows) can explicitly Approve / Annotate / Close, with optional structured decision output for automation across Claude Code, OpenCode, and Pi.

Changes:

  • Introduces --gate (3-way review UX) plus decision plumbing (approved field, /api/approve, gate propagated via /api/plan).
  • Adds --json decision stdout mode and --silent-approve to support “silence-is-permission” hook patterns.
  • Centralizes harness parsing and path normalization via new shared helpers (parseAnnotateArgs, stripAtPrefix / resolveAtReference) and updates docs/templates.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/shared/resolve-file.ts Exports stripWrappingQuotes for reuse by new argument/path helpers.
packages/shared/package.json Exposes new shared entrypoints (annotate-args, at-reference).
packages/shared/at-reference.ts Adds consistent @path normalization + literal fallback logic.
packages/shared/at-reference.test.ts Unit tests for @-reference handling (including quote stripping).
packages/shared/annotate-args.ts Adds slash-command-style arg parsing for --gate/--json/--silent-approve while preserving internal whitespace.
packages/shared/annotate-args.test.ts Extensive tests pinning parsing behavior (whitespace, quotes, @ behavior, flags).
packages/server/annotate.ts Adds gate option, propagates it to UI via /api/plan, and adds /api/approve.
packages/editor/App.tsx Adds gate-mode approve behavior/UX, state routing for exit warnings, and reuses /api/approve.
apps/opencode-plugin/index.ts Passes raw command tail through so --gate/--json can be parsed downstream.
apps/opencode-plugin/commands.ts Parses annotate args, implements @ literal fallback semantics, and treats approved like exit (no injection).
apps/pi-extension/vendor.sh Vendors new shared helper modules into the extension build.
apps/pi-extension/server/serverAnnotate.ts Adds gate option and approved decision plumbing in Pi’s server wrapper.
apps/pi-extension/plannotator-events.ts Extends Pi event payload/result types to include gate and approved.
apps/pi-extension/plannotator-browser.ts Threads gate through to startAnnotateServer and returns approved.
apps/pi-extension/index.ts Parses args for slash commands, resolves @ references with fallback, and handles approved result.
apps/hook/server/index.ts Implements stdout decision matrix (--gate/--json/--silent-approve), adds /api/approve flow, and improves @ handling.
apps/hook/commands/plannotator-annotate.md Updates command template to pass $ARGUMENTS and documents decision outcomes.
apps/hook/commands/plannotator-last.md Same as above for annotate-last template.
apps/copilot/commands/plannotator-annotate.md Updates Copilot command template to pass $ARGUMENTS and documents outcomes.
apps/copilot/commands/plannotator-last.md Same as above for copilot-last template.
apps/gemini/commands/plannotator-annotate.toml Updates Gemini command prompt to document decision outcomes.
apps/marketing/src/content/docs/commands/annotate.md Documents new flags, stdout matrix, and /api/approve endpoint.
apps/marketing/src/content/docs/commands/annotate-last.md Documents new flags for annotate-last.
apps/marketing/src/content/docs/guides/hook-integration.md Adds hook recipes for gating workflows, including --silent-approve patterns.
apps/marketing/src/content/docs/guides/annotate-gates-and-json-responses.md Adds detailed guide for gate + JSON decision semantics.
AGENTS.md Updates server API contract docs to include gate and /api/approve.
bun.lock Bumps package versions for the release.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/copilot/commands/plannotator-annotate.md
Comment on lines +15 to +16
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
3. Plaintext annotation feedback, OR a JSON object with `"decision": "annotated"` and a `"feedback"` field. Address the feedback. The user has reviewed your last message and provided specific annotations and comments.
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This instruction treats empty stdout as definitely dismissed, but with --silent-approve enabled plaintext approval is also empty stdout (indistinguishable from Close). The guidance should reflect that empty output may mean either dismissed or silently approved, and recommend a neutral acknowledgement/stop behavior accordingly.

Suggested change
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
3. Plaintext annotation feedback, OR a JSON object with `"decision": "annotated"` and a `"feedback"` field. Address the feedback. The user has reviewed your last message and provided specific annotations and comments.
2. A JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
3. Empty. This may mean the user approved silently, or that they closed the session without requesting changes. Acknowledge neutrally with a single sentence ("Acknowledged.") and stop. Do not begin any work.
4. Plaintext annotation feedback, OR a JSON object with `"decision": "annotated"` and a `"feedback"` field. Address the feedback. The user has reviewed your last message and provided specific annotations and comments.

Copilot uses AI. Check for mistakes.
Comment thread apps/gemini/commands/plannotator-annotate.toml
Comment thread apps/hook/server/index.ts
Comment on lines +169 to +172
if (result.exit) return; // empty stdout on close
if (result.approved) {
if (!silentApproveFlag) console.log(APPROVED_PLAINTEXT_MARKER);
return;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description’s stdout matrix says --gate (without --json) makes both Approve and Close emit empty stdout, but emitAnnotateOutcome prints the plaintext marker on approval unless --silent-approve is set. Please align the PR description/matrix with the implemented behavior, or adjust this branch to match the described contract.

Copilot uses AI. Check for mistakes.
Comment thread apps/hook/server/index.ts Outdated
The output above will be one of:

1. The exact text `The user approved.`, OR a JSON object with `"decision": "approved"`. The user approved the markdown file(s). Acknowledge with a single sentence ("Approved.") and stop. Do not begin any work.
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This instruction treats empty stdout as definitely dismissed, but with --silent-approve enabled plaintext approval is also empty stdout (indistinguishable from Close). The guidance should reflect that empty output may mean either dismissed or silently approved, and recommend a neutral acknowledgement/stop behavior accordingly.

Suggested change
2. Empty, OR a JSON object with `"decision": "dismissed"`. The user closed the session without requesting changes. Acknowledge with a single sentence ("Annotation session closed.") and stop. Do not begin any work.
2. Empty, OR a JSON object with `"decision": "dismissed"`. Empty output is ambiguous: the user may have closed the session without requesting changes, or approval may have been recorded silently. Do not infer which occurred. Acknowledge with a single neutral sentence ("No further action needed.") and stop. Do not begin any work.

Copilot uses AI. Check for mistakes.
Comment thread apps/hook/commands/plannotator-last.md
Draft PR placeholder. Design in description.

For provenance purposes, this commit was AI assisted.
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.
The split/join tokenizer in parseAnnotateArgs was collapsing multi-whitespace
runs in file paths (`My  Notes.md` → `My Notes.md`, tabs → single space).
Before this branch, OpenCode and Pi passed the raw args string straight
to resolveUserPath; my own commit 53b87b2 introduced the regression by
inserting a naive parser between them.

Replace with a segment-preserving scanner: walk the string once, keep
whitespace runs and non-whitespace tokens as separate segments. Remove
only `--gate` / `--json` tokens plus one adjacent whitespace run. Final
trim handles the edge where two adjacent flags both claim the same
inter-flag whitespace.

Tests added for the regression cases (double-space, tab, flag-at-start,
no-flags multi-whitespace passthrough). Full test suite: 688/688 pass.

For provenance purposes, this commit was AI assisted.
The runtime result object from openMarkdownAnnotation and
openLastMessageAnnotation has always included exit? (pre-existing
gap, not introduced by this PR), but the exported event-channel
type was missing it. The guides/hook-integration.md doc I added
explicitly advertises exit? as part of the annotation return shape,
so the type contract and the public docs were disagreeing.

Add exit?: boolean to the interface. Typecheck clean.

For provenance purposes, this commit was AI assisted.
The Claude Code slash-command templates were updated in 0892088 to
handle the new stdout shapes (empty on Approve/Close, JSON decision
markers with --json). The Copilot and Gemini copies of those
templates were missed — they still instructed the agent to "address
the annotation feedback above" unconditionally, which hallucinates
responses when the user passes --gate (empty output) or --json
(JSON decision marker).

Also: apps/copilot/commands/plannotator-last.md invoked
`plannotator copilot-last` with no $ARGUMENTS forwarding, so --gate
/ --json couldn't reach the binary via that slash command on Copilot.
Same bug pattern as apps/hook/commands/plannotator-last.md which was
fixed in c55efb4 — this is the Copilot copy.

Parity claim in the PR docs now actually holds across harnesses.

For provenance purposes, this commit was AI assisted.
Introduces packages/shared/at-reference.ts with two pure functions:

  stripAtPrefix(input): string
    Primary reference-mode resolution. Removes a leading `@`.

  resolveAtReference(input, exists): string | null
    Try stripped first (reference convention, first-class), fall back to
    literal if stripped doesn't resolve. Filesystem predicate is injected
    so the helper itself stays pure and trivially testable.

Both functions replace scattered inline strip logic so every harness —
Claude Code binary, OpenCode plugin, Pi extension — uses one source of
truth. Currently wired:

- parseAnnotateArgs (packages/shared/annotate-args.ts) calls stripAtPrefix
- Claude Code binary (apps/hook/server/index.ts) calls stripAtPrefix
- OpenCode + Pi inherit via parseAnnotateArgs

resolveAtReference is exported for future use in HTML/folder branches
that want literal-@ fallback (scoped-package-style names). Not wired yet.

Pi vendor.sh updated so the helper ships into apps/pi-extension/generated
on next build. Full test suite: 702/702 pass.

For provenance purposes, this commit was AI assisted.
parseAnnotateArgs now returns rawFilePath alongside the stripped
filePath so dispatch code can fall back to the literal form when
the stripped form doesn't resolve — the node_modules / scoped-
package case (e.g. @plannotator/ui/README.md).

Wired in every dispatch site:

  apps/hook/server/index.ts          (binary: folder, HTML, markdown)
  apps/opencode-plugin/commands.ts   (OpenCode plugin: HTML, markdown)
  apps/pi-extension/index.ts         (Pi extension: all three)

Folder and HTML branches use resolveAtReference(rawFilePath, existsSync-
style predicate). Markdown branches use a small inline fallback against
resolveMarkdownFile since that resolver has its own fuzzy-match and
ambiguous-case handling we want to preserve.

Reference-mode priority (strip wins when both exist) is preserved end-
to-end — the helper tries stripped first on every call. #488's existing
resolver tests pass unchanged. Full suite: 702/702.

For provenance purposes, this commit was AI assisted.
Dedicated capability-reference page for the --gate and --json flags.
Shows the stdout matrix at the top, explains what each flag does,
covers the three primary use cases (spec-driven frameworks, Stop-hook
turn review, programmatic decision routing), and cross-links to
hook-integration.md for copy-paste recipes.

Declarative tone, no em dashes.

For provenance purposes, this commit was AI assisted.
Shows the actual shape of each decision object (approved, dismissed,
annotated) plus the annotated example with a real feedback payload.
Clarifies that each invocation emits a single line of JSON.

For provenance purposes, this commit was AI assisted.
OpenCode and Pi don't go through a shell, so quoted paths arrive at
the parser with literal quote characters intact. stripAtPrefix was
checking startsWith("@") against the quote character, never seeing
the @, and the reference-mode strip silently skipped.

Export stripWrappingQuotes from resolve-file.ts (already existed as
a private helper). Use it in stripAtPrefix before the @ check, and
in parseAnnotateArgs when building rawFilePath so every downstream
caller gets a clean string without tokenization artifacts.

Covers `"@foo.md"`, `'@foo.md'`, `"@my Notes.md"`, and `"foo.md"`
(quotes without @). Mismatched quotes are left alone. Terminal CLI
usage is unchanged because the shell already unwraps quotes before
the binary receives argv.

Tests: 711/711 pass.

For provenance purposes, this commit was AI assisted.
Two changes:

1. CLAUDE.md Annotate Server API table now lists /api/approve and
   /api/exit, plus the gate field on the /api/plan response. The
   internal developer reference matches the public marketing docs
   and the actual server code.

2. All five annotate slash-command templates (Claude Code annotate
   and last, Copilot annotate and last, Gemini annotate) split the
   "approved or dismissed" branch into two. With --json on, the agent
   now acknowledges approvals with "Approved." and dismissals with
   "Annotation session closed." The explicit approval signal from the
   reviewer reaches the agent instead of being collapsed into a
   neutral close.

Plaintext mode (no --json) is unchanged since approve and close both
emit empty stdout by design.

For provenance purposes, this commit was AI assisted.
Follow-up to 345b42a which intended to include this file but staged
the symlink (CLAUDE.md) instead of the target (AGENTS.md). The table
now lists /api/approve, /api/exit, and the gate field on /api/plan
so the internal developer reference matches the public marketing
docs and the actual server code.

For provenance purposes, this commit was AI assisted.
Previously --gate Approve emitted empty stdout, identical to Close,
to preserve a specific naive-hook pattern where any stdout blocks.
That made plaintext --gate approvals indistinguishable from closes
for every other consumer — slash command templates especially, which
ended up collapsing approval into a "session closed" acknowledgement.

Now Approve emits the exact line "The user approved." on stdout in
plaintext --gate mode. Close stays empty. Send Annotations stays
feedback markdown. All three outputs are unambiguous without JSON.

Slash command templates (Claude Code, Copilot, Gemini) updated to
recognize either the plaintext marker or the JSON form. Stdout
matrix updated in every doc that references it. Hook integration
recipes note the behavior change for users whose hook scripts treat
any stdout as a block signal — the fix is trivial (filter the
marker, or use --json).

JSON mode unchanged. OpenCode and Pi unaffected (they route on the
decision object directly, not stdout).

For provenance purposes, this commit was AI assisted.
Hooks that treat any non-empty stdout as a block signal (spec-kit
PostToolUse pattern) need Approve to emit empty stdout. The default
--gate behavior emits "The user approved." so slash command templates
can distinguish approve from close in plaintext. --silent-approve
suppresses that marker, restoring silence-is-permission for hook
authors. No effect in --json mode.

Includes: shared parser update (FLAG_MAP refactor), 4 new tests,
docs for annotate, annotate-last, hook-integration, and gates guide.

For provenance purposes, this commit was AI assisted.
@backnotprop backnotprop force-pushed the feat/annotate-approve branch from 3718e98 to 97df41d Compare April 23, 2026 23:41
For provenance purposes, this commit was AI assisted.
@backnotprop backnotprop merged commit d102c5f into main Apr 24, 2026
7 checks passed
@backnotprop
Copy link
Copy Markdown
Owner Author

Post-merge follow-up: --silent-approve is being replaced with --hook in #610.

After reviewing the Claude Code and Codex hook documentation, we found that neither system treats plain text stdout as a block signal. Both require {"decision":"block","reason":"..."} JSON to actually block a hook. The --silent-approve flag was solving a problem that doesn't exist in either hook protocol.

--hook emits hook-native JSON directly — no wrapper script needed. See #610 for details.

backnotprop added a commit that referenced this pull request Apr 24, 2026
…protocol

Replaces `--silent-approve` with `--hook`, which emits hook-native JSON
that works directly with Claude Code and Codex PostToolUse/Stop hooks.

- Approve/Close: empty stdout (hook passes)
- Annotate: `{"decision":"block","reason":"<feedback>"}` (hook blocks)
- `--hook` implies `--gate` (three-button UX), applied in the parser
  so all harnesses (binary, OpenCode, Pi) get it automatically
- Rewrites hook integration guide to be agent-agnostic

Follow-up to #606. Refs #570.

For provenance purposes, this commit was AI assisted.
HeikoAtGitHub pushed a commit to HeikoAtGitHub/plannotator that referenced this pull request Apr 26, 2026
…protocol

Replaces `--silent-approve` with `--hook`, which emits hook-native JSON
that works directly with Claude Code and Codex PostToolUse/Stop hooks.

- Approve/Close: empty stdout (hook passes)
- Annotate: `{"decision":"block","reason":"<feedback>"}` (hook blocks)
- `--hook` implies `--gate` (three-button UX), applied in the parser
  so all harnesses (binary, OpenCode, Pi) get it automatically
- Rewrites hook integration guide to be agent-agnostic

Follow-up to backnotprop#606. Refs backnotprop#570.

For provenance purposes, this commit was AI assisted.
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.

feat(annotate): approve/reject flow for spec-driven development frameworks

3 participants