Skip to content

Commit 97df41d

Browse files
committed
feat(annotate): add --silent-approve flag for naive hook compatibility
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.
1 parent acb8053 commit 97df41d

8 files changed

Lines changed: 163 additions & 43 deletions

File tree

apps/hook/server/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,29 @@ const cliNoJina = noJinaIdx !== -1;
126126
if (cliNoJina) args.splice(noJinaIdx, 1);
127127

128128
// Annotate review-gate flags (#570): --gate adds an Approve button,
129-
// --json switches stdout to structured decision output.
129+
// --json switches stdout to structured decision output, --silent-approve
130+
// suppresses the plaintext approve marker (naive hooks that treat any
131+
// stdout as a block signal opt in here to keep silence-is-permission).
130132
const gateIdx = args.indexOf("--gate");
131133
const gateFlag = gateIdx !== -1;
132134
if (gateFlag) args.splice(gateIdx, 1);
133135
const jsonIdx = args.indexOf("--json");
134136
const jsonFlag = jsonIdx !== -1;
135137
if (jsonFlag) args.splice(jsonIdx, 1);
138+
const silentApproveIdx = args.indexOf("--silent-approve");
139+
const silentApproveFlag = silentApproveIdx !== -1;
140+
if (silentApproveFlag) args.splice(silentApproveIdx, 1);
136141

137142
// Stdout matrix for annotate / annotate-last / copilot annotate-last (#570).
138143
// Plaintext mode:
139144
// - Close emits empty stdout (naive PostToolUse / Stop hooks: empty = allow).
140145
// - Approve emits "The user approved." so agents and templates can
141-
// distinguish approval from close without needing --json.
146+
// distinguish approval from close without needing --json. With
147+
// --silent-approve, Approve also emits empty stdout (hook-friendly).
142148
// - Send Annotations emits the plaintext feedback markdown.
143-
// --json switches to structured output across all three decisions.
149+
// --json switches to structured output across all three decisions;
150+
// --silent-approve has no effect in --json mode (JSON always routes by
151+
// decision field, so there's no ambiguity to silence).
144152
export const APPROVED_PLAINTEXT_MARKER = "The user approved.";
145153

146154
function emitAnnotateOutcome(result: {
@@ -160,7 +168,7 @@ function emitAnnotateOutcome(result: {
160168
}
161169
if (result.exit) return; // empty stdout on close
162170
if (result.approved) {
163-
console.log(APPROVED_PLAINTEXT_MARKER);
171+
if (!silentApproveFlag) console.log(APPROVED_PLAINTEXT_MARKER);
164172
return;
165173
}
166174
if (result.feedback) console.log(result.feedback);

apps/marketing/src/content/docs/commands/annotate-last.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ The annotation UI in `annotate-last` mode works the same as `/plannotator-annota
7575

7676
## Flags
7777

78-
`plannotator annotate-last` accepts the same `--gate` and `--json` flags as `plannotator annotate`. See [Annotate → Flags](/docs/commands/annotate/#flags) for the full matrix.
78+
`plannotator annotate-last` accepts the same `--gate`, `--json`, and `--silent-approve` flags as `plannotator annotate`. See [Annotate → Flags](/docs/commands/annotate/#flags) for the full matrix.
7979

8080
The common use case for `--gate` on annotate-last is a turn-by-turn review gate wired to a Stop hook:
8181

apps/marketing/src/content/docs/commands/annotate.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ All annotation types work identically: deletions, replacements, comments, insert
103103

104104
## Flags
105105

106-
Two opt-in flags turn annotate into a review gate for hook integrations (spec-driven frameworks, turn-by-turn review, and so on). They are orthogonal: you can use either alone or combine them.
106+
Three opt-in flags turn annotate into a review gate for hook integrations (spec-driven frameworks, turn-by-turn review, and so on). They compose: use any alone or combine them.
107107

108108
### `--gate`
109109

@@ -123,18 +123,25 @@ Switches stdout to a structured decision object so hooks can route programmatica
123123

124124
`feedback` is only present when `decision === "annotated"`.
125125

126+
### `--silent-approve`
127+
128+
Suppresses the plaintext approve marker so Approve emits empty stdout instead of `The user approved.`. Use this with naive hooks that treat any non-empty stdout as a block signal. Approve and Close both become silent, and only Send Annotations blocks with feedback (silence-is-permission).
129+
130+
`--silent-approve` only affects plaintext mode. In `--json` mode, Approve continues to emit `{"decision":"approved"}` — JSON callers route on the `decision` field, so there's no ambiguity to silence.
131+
126132
### Stdout matrix
127133

128134
| Flags | UX | Approve | Close | Send Annotations |
129135
|---|---|---|---|---|
130136
| *(none)* | 2-button | n/a | empty | feedback (plaintext) |
131137
| `--gate` | 3-button | `The user approved.` | empty | feedback (plaintext) |
138+
| `--gate --silent-approve` | 3-button | empty | empty | feedback (plaintext) |
132139
| `--json` | 2-button | n/a | `{"decision":"dismissed"}` | `{"decision":"annotated","feedback":"..."}` |
133140
| `--gate --json` | 3-button | `{"decision":"approved"}` | `{"decision":"dismissed"}` | `{"decision":"annotated","feedback":"..."}` |
134141

135-
**Key property:** `--gate` plaintext output is unambiguous across all three decisions. Close is empty, Send Annotations is feedback markdown, Approve is the exact line `The user approved.` — each case distinguishable without JSON parsing. Use `--json` when you want machine-readable decision objects instead of string matching.
142+
**Key property:** `--gate` plaintext output is unambiguous across three decisionsClose is empty, Send Annotations is feedback markdown, Approve is the line `The user approved.`. Drop the marker with `--silent-approve` when your hook treats any stdout as a block. Use `--json` when you want machine-readable decision objects instead of string matching.
136143

137-
On OpenCode and Pi, `--json` is silently accepted because those harnesses write back into the session directly rather than via stdout. The `--gate` flag behaves identically across all three harnesses.
144+
On OpenCode and Pi, `--json` and `--silent-approve` are silently accepted because those harnesses write back into the session directly rather than via stdout. The `--gate` flag behaves identically across all three harnesses.
138145

139146
See [Hook integration recipes](/docs/guides/hook-integration/) for ready-to-use PostToolUse and Stop hook examples.
140147

apps/marketing/src/content/docs/guides/annotate-gates-and-json-responses.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
---
22
title: "Annotate Gates and JSON Responses"
3-
description: "The --gate and --json flags extend plannotator annotate from a feedback tool into a structured review gate with machine-readable decisions. Use them to wire Plannotator into spec-driven workflows, Stop hooks, and agent pipelines."
3+
description: "The --gate, --json, and --silent-approve flags extend plannotator annotate from a feedback tool into a structured review gate with machine-readable decisions. Use them to wire Plannotator into spec-driven workflows, Stop hooks, and agent pipelines."
44
sidebar:
55
order: 28
66
section: "Guides"
77
---
88

9-
`plannotator annotate` and `plannotator annotate-last` accept two flags that turn markdown annotation into a full review gate with structured output.
9+
`plannotator annotate` and `plannotator annotate-last` accept three flags that turn markdown annotation into a full review gate with structured output.
1010

1111
## Capabilities
1212

1313
- **`--gate`** adds an Approve button to the annotation UI. The reviewer picks one of three decisions: approve, send annotations, or close.
1414
- **`--json`** emits every decision as a structured JSON object on stdout so hooks and plugins can route on the outcome without parsing free text.
15-
- The flags compose. Use them together, separately, or not at all.
15+
- **`--silent-approve`** suppresses the plaintext approve marker so Approve emits empty stdout. Naive hooks that treat any stdout as a block signal opt in here to keep silence-is-permission intact.
16+
- The flags compose. Use any alone or together.
1617
- Identical semantics across every supported harness: Claude Code, Copilot CLI, Gemini CLI, OpenCode, Pi, and Codex.
1718

1819
## Stdout contract
1920

2021
```
21-
Flags │ UX │ Approve │ Close │ Annotate
22-
─────────────── ┼──────────────────┼─────────────────────────┼──────────────────────────┼───────────────────────────────────────────────
23-
(none) │ 2-button │ n/a │ empty │ feedback (plaintext)
24-
--gate │ 3-button │ `The user approved.` │ empty │ feedback (plaintext)
25-
--json │ 2-button │ n/a │ {"decision":"dismissed"}│ {"decision":"annotated","feedback":"..."}
26-
--gate --json │ 3-button │ {"decision":"approved"}│ {"decision":"dismissed"}│ {"decision":"annotated","feedback":"..."}
22+
Flags │ UX │ Approve │ Close │ Annotate
23+
──────────────────────────┼──────────────────┼─────────────────────────┼──────────────────────────┼───────────────────────────────────────────────
24+
(none) │ 2-button │ n/a │ empty │ feedback (plaintext)
25+
--gate │ 3-button │ `The user approved.` │ empty │ feedback (plaintext)
26+
--gate --silent-approve │ 3-button │ empty │ empty │ feedback (plaintext)
27+
--json │ 2-button │ n/a │ {"decision":"dismissed"}│ {"decision":"annotated","feedback":"..."}
28+
--gate --json │ 3-button │ {"decision":"approved"}│ {"decision":"dismissed"}│ {"decision":"annotated","feedback":"..."}
2729
```
2830

2931
### JSON schema
@@ -68,7 +70,7 @@ A three-way review decision. The annotation UI adds an Approve button alongside
6870
- **Send Annotations.** The reviewer has specific changes. The feedback is returned verbatim.
6971
- **Close.** The session ends without a decision. Neither a signal to the agent nor an instruction set.
7072

71-
In plaintext mode, Approve emits the single line `The user approved.` on stdout so templates and agents can distinguish approval from close without needing `--json`. Close emits nothing. Send Annotations emits the feedback markdown. Hook authors who treat any non-empty stdout as a block signal need to filter the approve marker (or use `--json` for cleaner routing).
73+
In plaintext mode, Approve emits the single line `The user approved.` on stdout so templates and agents can distinguish approval from close without needing `--json`. Close emits nothing. Send Annotations emits the feedback markdown. Hook authors who treat any non-empty stdout as a block signal can add `--silent-approve` to suppress the marker, or use `--json` for structured routing.
7274

7375
## `--json`
7476

@@ -80,6 +82,18 @@ Structured stdout. Every decision is emitted as a JSON object with a `decision`
8082
- `--gate --json` unlocks all three decisions in structured form.
8183
- On OpenCode and Pi, `--json` is accepted silently. Those harnesses write back to the session directly rather than via stdout, so the flag has no effect there. Recipes remain portable.
8284

85+
## `--silent-approve`
86+
87+
Suppresses the plaintext approve marker. With `--gate --silent-approve`, Approve emits empty stdout (instead of `The user approved.`), matching Close. Send Annotations still emits feedback.
88+
89+
This is the shape naive hooks want — the ones that treat any non-empty stdout as a block signal:
90+
91+
- Approve → empty → hook passes → agent proceeds.
92+
- Close → empty → hook passes → agent proceeds.
93+
- Send Annotations → feedback → hook blocks with that feedback as the reason.
94+
95+
`--silent-approve` only affects plaintext mode. In `--json` mode, Approve still emits `{"decision":"approved"}` — JSON callers route on the `decision` field, so there's no ambiguity to silence. The flag is accepted silently on OpenCode and Pi for the same reason `--json` is: those harnesses don't use stdout as the signal channel.
96+
8397
## Primary use cases
8498

8599
### Spec-driven development frameworks

apps/marketing/src/content/docs/guides/hook-integration.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ sidebar:
66
section: "Guides"
77
---
88

9-
The `--gate` and `--json` flags on `plannotator annotate` and `plannotator annotate-last` turn annotation into a structured review decision. This guide shows how to wire them into agent hooks so a human can gate the agent at specific points in a workflow.
9+
The `--gate`, `--json`, and `--silent-approve` flags on `plannotator annotate` and `plannotator annotate-last` turn annotation into a structured review decision. This guide shows how to wire them into agent hooks so a human can gate the agent at specific points in a workflow.
1010

1111
See [Annotate → Flags](/docs/commands/annotate/#flags) for the full stdout matrix. The short version:
1212

1313
- `--gate` adds a three-button UX (Approve / Send Annotations / Close).
14-
- Without `--json`: Approve emits the line `The user approved.`, Close emits empty stdout, Send Annotations emits the feedback markdown. Three distinguishable outputs without parsing JSON.
15-
- With `--json`: every decision emits a structured `{ "decision": "approved" | "annotated" | "dismissed", "feedback": "..." }` object.
14+
- Plaintext default: Approve emits the line `The user approved.`, Close emits empty stdout, Send Annotations emits the feedback markdown. Three distinguishable outputs without parsing JSON.
15+
- `--silent-approve` collapses Approve to empty stdout, matching Close. Use this with naive "any stdout = block" hooks so silence means permission.
16+
- `--json` emits every decision as a structured `{ "decision": "approved" | "annotated" | "dismissed", "feedback": "..." }` object.
1617

1718
## Recipe 1: PostToolUse spec gate
1819

@@ -47,7 +48,21 @@ Behavior:
4748
- **Send Annotations** → feedback markdown on stdout. Claude Code reports the feedback back.
4849
- **Close** → empty stdout. Claude Code proceeds silently.
4950

50-
If your hook treats any non-empty stdout as a block signal (some scripts do), filter the approve marker explicitly, or use the `--json` recipe below to route on the parsed decision instead.
51+
### Silence-is-permission (`--silent-approve`)
52+
53+
If your hook treats any non-empty stdout as a block signal (spec-kit and similar naive PostToolUse hooks), add `--silent-approve` so Approve also emits empty stdout:
54+
55+
```json
56+
"command": "plannotator annotate \"$CLAUDE_TOOL_INPUT_file_path\" --gate --silent-approve"
57+
```
58+
59+
Behavior with the flag:
60+
61+
- **Approve** → empty stdout → hook passes → agent proceeds.
62+
- **Close** → empty stdout → hook passes → agent proceeds.
63+
- **Send Annotations** → feedback on stdout → hook blocks with feedback as the reason.
64+
65+
Approve and Close collapse into the same "silent = allow" cell, which is what this class of hook expects. Only Send Annotations carries content the agent needs to react to.
5166

5267
### Structured (`--json`)
5368

@@ -104,6 +119,8 @@ Behavior:
104119
- **Send Annotations** → feedback on stdout → Claude Code re-prompts with the feedback.
105120
- **Close** → empty stdout → turn ends.
106121

122+
Add `--silent-approve` if your Stop hook treats any stdout as a re-prompt trigger — Approve then emits empty stdout too, so only Send Annotations re-fires the turn with feedback.
123+
107124
### Structured
108125

109126
Same pattern as the PostToolUse recipe — pipe `--gate --json` through a shell wrapper if you want distinct handling per decision.
@@ -116,7 +133,7 @@ The same `--gate` flag works in OpenCode's `/plannotator-annotate` and Pi's `/pl
116133
/plannotator-annotate spec.md --gate
117134
```
118135

119-
On those harnesses there is no stdout channel back to the agent — the plugin writes back via `session.prompt` (OpenCode) or `sendUserMessage` (Pi). Approve and Close both result in no session injection; Send Annotations injects the feedback. `--json` is accepted silently on these harnesses so recipes stay portable.
136+
On those harnesses there is no stdout channel back to the agent — the plugin writes back via `session.prompt` (OpenCode) or `sendUserMessage` (Pi). Approve and Close both result in no session injection; Send Annotations injects the feedback. `--json` and `--silent-approve` are accepted silently on these harnesses so recipes stay portable.
120137

121138
Third-party Pi or OpenCode plugins that want explicit decision routing can read `approved` directly from the server's decision object:
122139

bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)