@erichll/pi-auto-review 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,307 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## 0.15.1 - 2026-08-31
4
-
5
- - Adapt the policy audit to `@gotgenes/pi-permission-system` v28.0.0. Ask
6
- decisions made by a registered authorizer chain link — including
7
- pi-auto-review's own model review — now broadcast `authorizer_allowed` /
8
- `authorizer_denied` instead of `user_approved` / `user_denied`. The audit
9
- whitelist records both new values verbatim instead of collapsing them into
10
- `unknown`, and approval sources surface them under their own name.
11
- - Keep suggestion evidence strictly human/session-based: `authorizer_allowed`
12
- deliberately does not join `SUCCESS_RESOLUTIONS`, because counting the
13
- extension's own machine review as evidence would create a feedback loop
14
- (model review allows → evidence → suggested auto-allow rule → future review
15
- bypassed). Authorizer-resolved rows still count as "reviewed" for the
16
- display-only low-risk candidate list so those surfaces stay visible.
17
- - Bump the `@gotgenes/pi-permission-system` peer range to `^28.0.0`.
18
-
19
- ## 0.15.0 - 2026-08-29
20
-
21
- - Fix a deterministic reviewer-unavailable denial for large CJK tool inputs.
22
- The reviewer input estimator counted one token per UTF-8 byte, tripling the
23
- estimate for CJK text, so multi-kilobyte Chinese payloads (e.g. a long plan
24
- document submitted through a tool call) synchronously failed the
25
- `reviewer_input_budget_exceeded` preflight before the reviewer model was
26
- ever called, and the request was denied as "Automatic review is
27
- unavailable." CJK code points now estimate at one token each and remaining
28
- code points at their UTF-8 byte length over three (estimator
29
- `conservative:cjk-aware`).
30
- - Record the denial when a reviewer-unavailable review fails closed, so
31
- `/auto-review-approve` — the recovery command that verdict recommends —
32
- actually has a matching denial to approve. Previously the recommendation
33
- was dead text for this failure class.
34
- - Let an explicit human override (`/auto-review-approve` trusted exact retry)
35
- bypass the reviewer input-budget preflight failure. A sizing estimate is
36
- not a safety verdict and must not veto an already-authorized retry; the
37
- review proceeds with truncated evidence.
38
- - Show "waiting for you" in the review widget while a `ctx.ui` prompt blocks
39
- the session during an active review (pi >= 0.84.4 `ui_prompt_start` /
40
- `ui_prompt_end`). The widget previously kept claiming "Waiting for
41
- <model>…" while pi was actually waiting on the user (permission dialog,
42
- `/review-denials`, break-glass challenge). Completed outcome summaries and
43
- sessions without a live widget are unaffected.
44
- - Fix `/auto-review-approve` and `/auto-review-break-glass` never applying in
45
- real TUI sessions. The exact-match request hash included `toolCallId`, but a
46
- retry is always a brand-new model tool call with a fresh tool call ID, so
47
- the authorization could never match the retried request. The hash now
48
- excludes both retry-minted identifiers (`id` was already excluded).
49
- - Fix break-glass authorization being discarded on every retry. The stored
50
- authorization was bound to the denial-time turn scope, but the command flow
51
- appends a user message (`pi.sendUserMessage`) before the agent retries, so
52
- the retry always lands in a later turn whose scope differs. Consumption now
53
- binds on session and request hash plus the confirmation TTL instead of the
54
- turn scope. The non-critical approve path already behaved this way.
55
- - Reset the turn circuit breaker when a break-glass allow is consumed, so the
56
- human-authorized scope can continue instead of staying blocked for the rest
57
- of the turn. Fresh repeated denials still trip it again after three
58
- consecutive failures.
59
- - Clean up break-glass authorization bookkeeping: an expired authorization
60
- that was never consumed no longer blocks re-approving the same action, and
61
- a fresh denial of an action whose approval was already consumed re-arms
62
- one-shot approval (one approval per denial).
63
-
64
- ## 0.14.0 - 2026-08-28
65
-
66
- - Upgrade `/auto-review-policy-audit` from redacted statistics to an
67
- evidence-driven permission-configuration optimizer. Reports now separate
68
- suggested allow rules, keep-ask rules, and insufficient evidence, and emit a
69
- copyable JSON fragment targeting the selected project or global
70
- permission-system configuration. Suggestions remain advisory and never read
71
- or modify permission configuration.
72
- - Discover candidates from actual audit data without built-in tool, executable,
73
- Git, RTK, package-manager, or read-only command catalogs. Valid observed
74
- surfaces and sanitized Bash templates—including unknown tools and write
75
- operations—can qualify after the configured minimum number of successful
76
- ask-path approvals. Denials, gate failures, unavailable confirmations,
77
- forwarded requests, and structurally unsafe Bash variants block a suggestion.
78
- - Migrate the policy-audit database transactionally to schema v2. Existing
79
- aggregate counts and `collecting_since` are preserved, while the new
80
- `recommendations_since` boundary prevents pre-migration data from producing
81
- allow suggestions. Valid custom tool names and sanitized Bash templates are
82
- retained for reporting. Templates replace paths, URLs, assignments, quoted
83
- values, and option values with wildcards; raw commands and non-Bash tool
84
- arguments or inputs remain excluded.
85
-
86
- ## 0.13.0 - 2026-08-28
87
-
88
- - Add compatibility with `@gotgenes/pi-permission-system` 27.1 directional
89
- `path_*` and `external_directory_*` surfaces while retaining the original
90
- surface in authorization evidence, UI binding, logs, and policy audits.
91
- - Keep the permission-system peer range at `^27.0.0` so existing 27.x bare
92
- `path` and `external_directory` integrations remain supported.
93
-
94
- ## 0.12.0 - 2026-08-27
95
-
96
- - Add the private policy audit
97
- compare current-project or anonymous global surface/command hotspots, denial
98
- rates, approval sources, and rule-hit fingerprints; then review repeatedly
99
- approved read-only operations as candidates for narrowly scoped `allow`
100
- rules. Mutation, project execution, network, arbitrary-shell, unknown, and
101
- custom-tool activity is called out as keep-`ask` evidence rather than an
102
- automatic relaxation. The report does not edit policy or claim that an
103
- unobserved rule is unused.
104
- - Add default-on, 180-day permission-policy auditing backed by built-in
105
- `node:sqlite`. Permission decisions are immediately reduced to fixed command,
106
- risk, path-class, source, and anonymous rule-hit aggregates; raw commands,
107
- paths, URLs, credentials, request IDs, and project names are never stored.
108
- - Add `/auto-review-policy-audit` as an extension-owned custom-entry report.
109
- It is not registered as an Agent tool or packaged skill, so audit reporting
110
- adds no tool schema or skill metadata to the model context. Reports default
111
- to the current project and can explicitly show an anonymous global aggregate.
112
- - Require Node.js 22.13.0 or newer. No SQLite CLI, system library, npm SQLite
113
- package, RTK, or historical permission-log import is required.
114
-
115
- ## 0.11.1 - 2026-08-25
116
-
117
- - Align the package version with `@erichll/pi-sandbox` `0.11.1`. No
118
- pi-auto-review runtime or public API changes.
119
-
120
- ## 0.11.0 - 2026-08-24
121
-
122
- - **Structured network destination.** Network approval requests now carry
123
- separate host, port, and protocol fields alongside the existing combined
124
- destination string. One-shot grants bind each component independently, so a
125
- grant issued for one host-port-protocol combination cannot be reused for a
126
- different one.
127
- - **Deferred reviews are more visible.** The review widget now highlights
128
- deferred verdicts with a warning accent instead of the previous muted style,
129
- making pending human decisions easier to spot.
130
-
131
- ## 0.10.0 - 2026-08-22
132
-
133
- - Replace the review footer and newly appended transcript entries with one
134
- above-editor `pi-auto-review` widget. Each permission check independently
135
- replaces the previous display, shows the configured reviewer model while it
136
- runs, and updates in place with its final outcome and metadata. The latest
137
- result remains until the next check and is cleared on session change or
138
- shutdown.
139
- - Prevent late concurrent completions and permission decisions from overwriting
140
- a newer check. A current local denial adds `Local confirmation · denied` to
141
- that result only. Widget failures fall back to notifications without changing
142
- authorization behavior.
143
- - Remove tool-call grouping, timers, capacity eviction, and tool/turn flush
144
- wiring for new feedback while retaining the historical entry renderer. Bound
145
- target and rationale text with visible ellipses, preserve width-aware wrapping,
146
- and keep token, duration, and call-count numbers in the normal metadata color.
147
-
148
- ## 0.9.0 - 2026-08-22
149
-
150
- - Require `@gotgenes/pi-permission-system` `^27.0.0` and register through the
151
- session-keyed `getPermissionsService(sessionId)` locator. Repeated ready
152
- broadcasts are idempotent; session changes, shutdown, and late asynchronous
153
- imports cannot retain or revive a stale authorizer registration. In-process
154
- child nodes register against their own service without replacing the
155
- parent's process-local broker capability.
156
- - Group local TUI review results by `(sessionId, toolCallId)` into one
157
- transcript entry while preserving independent asks, model calls, verdicts,
158
- grants, confirmations, and audit records. Denials flush immediately; tool
159
- start, terminal permission denial, turn/agent/session end, a 60-second TTL,
160
- and bounded-capacity eviction provide lossless fallback flushes. Forwarded,
161
- uncorrelated, and non-TUI reviews remain immediate.
162
- - Add `toolCallId` to each `pi_auto_review_decision` audit entry and retain
163
- compatibility rendering for historical single-result transcript entries.
164
- - Remove the review entry's left quote bar and combine each target with its
165
- rationale and member surface/outcome on one line, keeping model/token/duration
166
- metadata separate.
167
- - Label every displayed model token count with `toks` in both grouped and
168
- single-result output.
169
-
170
- ## 0.8.2 - 2026-08-21
171
-
172
- - Structure interactive review results as headline, target, rationale, and a
173
- meta line with the reviewer model name, input/output tokens, duration, and extra
174
- calls. The TUI renders them as markdown-style quote rows (`│` + `mdQuote`),
175
- with allow/deny verbs and token counts tinted from the theme. The model id is
176
- shown without a provider prefix; audit still records the full `provider/id`.
177
- Local hard denies omit model and token fields because no model ran.
178
-
179
- ## 0.8.1 - 2026-08-21
180
-
181
- - Restore TUI auto-confirm for capped `path` / `external_directory` allows on
182
- permission-system 26.x. `permissions:ui_prompt` no longer carries `message`;
183
- the bridge now binds `requestId` plus `request.value` (falling back to the
184
- display `value`, then the legacy `message`) and fingerprints the live dialog
185
- with those fields. Top-level display `surface` is not treated as the gate.
186
-
187
- ## 0.8.0 - 2026-08-21
188
-
189
- - Add `/auto-review-approve` for exact non-critical denial retries and remove
190
- the unprefixed `/approve` command without a compatibility alias.
191
- - Add interactive `/auto-review-break-glass` for a recent critical model deny.
192
- The high-friction confirmation is bound to the full request hash, session,
193
- scope, and original request ID, expires after 60 seconds, permits one direct
194
- retry, and never bypasses deterministic or protected-write hard denies.
195
- - Add tighten-only `breakGlassEnabled` (default `true`), structured break-glass
196
- allow provenance, and challenge/authorization/consumption/rejection audit
197
- events. Challenge phrases are never included in audit events.
198
-
199
- ## 0.7.0 - 2026-08-20
200
-
201
- - **Named `/home` paths are not a home wipe.** The reviewer prompt now matches
202
- the deterministic hard-deny: recursive forced wipe of `/`, `~`, `$HOME`, or
203
- the home directory itself remains forbidden. A narrow, user-requested delete
204
- of named files or directories under `/home/...` is high-risk, not critical.
205
- High risk without medium/high user authorization defers to the human instead
206
- of denying as if it were a hard deny. The fixed prompt is compacted without
207
- changing those rules.
208
- - **Clearer agent-facing denials.** Authorizer denies still fail closed, but the
209
- teaching reason now states that automatic policy denied the request (not a
210
- human click), forbids rephrasing or circumvention, and points to the exact
211
- retry command
212
- for one exact retry when the user already requested that action. The
213
- permission-system wrapper may still say `User denied`; the reason suffix is
214
- the correction this package can own.
215
-
216
- ## 0.6.0 - 2026-08-20
217
-
218
- - **Remove host user-constraint overlay.** Stop rewriting model allows from
219
- regex matches on user text (`constraintEffect` / `vagueContinuation` /
220
- authorization ceilings). Older messages enter evidence only via an exact
221
- request reference or trusted exact retry. Compaction summaries remain
222
- excluded from user intent. Hard denies, grant hashing, and the reviewer
223
- model are unchanged.
224
-
225
- ## 0.5.0 - 2026-08-20
226
-
227
- - **Safer evidence selection.** Review only request-linked tool calls, paired
228
- results, current-task user intent, and bounded security evidence. Compaction
229
- summaries cannot restore authorization, raw revocations override model
230
- allows, and missing mandatory evidence fails closed before a model call.
231
- - **Independent, smaller reviewer requests.** Use one compact canonical request
232
- envelope and a byte-stable policy prefix, reducing fixed input from 2,011 to
233
- 1,396 characters. Independent SSE calls prevent prior review state from
234
- carrying into later approvals while preserving cache/routing identity.
235
- - **Explicit token limits.** Add tighten-only `maxReviewerInputTokens` with an
236
- 8,192 default and conservative UTF-8 accounting across the complete prompt.
237
- Lower the default `maxTokens` from 1,600 to 256 after sequential real-model
238
- validation at 384 and 256; the legal range remains 256–4,096.
239
- - **Typed, bounded retries.** Share one deadline across each review, disable
240
- provider-internal retries, and allow at most two actual model calls. Only
241
- format errors, recognized connection/5xx failures, and bounded 429 responses
242
- retry; deterministic and unknown failures remain terminal and fail closed.
243
- - **Privacy-bounded telemetry.** Record per-attempt usage and per-review
244
- summaries for both reviewer entry points using stable status/error codes and
245
- prompt-part counts, without logging evidence text, provider errors,
246
- credentials, headers, or URL query values.
247
-
248
- ## 0.4.0 - 2026-08-19
249
-
250
- - **Credential-exfiltration hardening.** Expand deterministic terminal denies to cover
251
- `.env` variants, common credential files, additional file readers and encoders,
252
- shell substitutions, staged variables, redirects, and multi-stage pipelines that
253
- upload credentials to network sinks. Template files such as `.env.example` and
254
- `.env.sample` remain reviewable.
255
- - **Reviewer refresh safety.** Re-resolve reviewer model/provider metadata for each
256
- review, reacquire authentication for each attempt, and bind the reviewer session
257
- identity to the endpoint and authentication fingerprint so provider or OAuth
258
- refreshes cannot reuse a stale connection.
259
- - **Compatibility.** Require Pi 0.84.1+ and permission-system 25.1.0+ to match the
260
- current reviewer and forwarded-permission contracts.
261
-
262
- ## 0.3.5 - 2026-08-13
263
-
264
- - Raise the default model-review `timeoutMs` from 45s to 90s and the default
265
- `retries` from 1 to 2. Approval now tolerates real-world model latency better
266
- while keeping the same bounded transcript surface.
267
-
268
- ## 0.3.4 - 2026-08-08
269
-
270
- - Normalize direct and permission-system v24 forwarded evidence before policy
271
- checks, model review, grant hashing, and audit logging. Forwarded Bash
272
- commands, canonical path boundaries, agent names, and requester session IDs
273
- now remain bound to the exact reviewed request.
274
- - Add an opt-in `PI_AUTO_REVIEW_AUDIT_FILE` JSONL sink for isolated release
275
- verification without changing the normal event-based audit surface.
276
-
277
- ## 0.3.3 - 2026-08-06
278
-
279
- - Fix config validation rejecting multi-segment model ids (`provider/group/model`).
280
- `validateConfig` no longer restricts a model to exactly one `/`; it only rejects
281
- ids with empty segments. Segments are resolved as before: the first segment is
282
- the provider and the rest is the model id.
283
-
284
- ## 0.3.2 - 2026-07-29
285
-
286
- - Load an optional user-global trusted config from
287
- `~/.pi/agent/extensions/pi-auto-review/config.json` as a full overlay on the
288
- package defaults (model, `autoConfirmBoundedAllows`, and other legal keys).
289
- - Keep project `.pi/pi-auto-review.json` tighten-only on top of that trusted
290
- layer.
291
- - Deterministically deny writes to the user-global reviewer config directory.
292
- - Default reviewer model is the bare id `codex-auto-review`; `provider/model`
293
- remains supported. Bare ids resolve against registered available models.
294
- - Show user-facing review feedback in interactive UI: a footer status while the
295
- model is reviewing, then a short toast for allow / deny / defer / auto-confirm
296
- / circuit-breaker outcomes (separate from agent denial reasons).
297
-
298
- ## 0.3.1 - 2026-07-29
299
-
300
- - Bind one-shot approval grants to matched policy evidence as well as the
301
- command, working directory, destination, paths, and other request fields.
302
- - Prevent a grant issued for one Host-IPC trigger from authorizing a request
303
- with different trigger evidence.
304
-
305
- ## 0.3.0 - 2026-07-28
306
-
307
- - Initial public release.
3
+ ## 0.15.3 - 2026-09-03
4
+
5
+ - Tolerate a single enclosing ```` ```json ```` or bare Markdown code fence
6
+ around reviewer decisions while preserving strict decision-schema
7
+ validation (fixes reviewer models that fence JSON despite the prompt,
8
+ notably when routed through Claude Code).
9
+ - Verify compatibility with `@gotgenes/pi-permission-system` 30.2.0 and
10
+ 31.0.0, widen the peer range through 31.x, and move the development baseline
11
+ to 31.0.0.
12
+ - Keep permission-system 31 statement-operand audit classification aligned for
13
+ `for`/`select` word lists and `case` subjects without treating case patterns
14
+ as accessed paths.
15
+ - Confirm that model auto-confirm stays one-shot and cannot select
16
+ permission-system 30.2's wider both-directions session grant.
17
+
18
+ ## 0.15.2 - 2026-09-02
19
+
20
+ - No behavior changes. Verified against `@gotgenes/pi-permission-system`
21
+ 29.x with a development baseline of `29.3.0`; the peer range now accepts
22
+ 29.x alongside 28.x.
package/README.md CHANGED
@@ -31,7 +31,8 @@ that dependency is a hard prerequisite (see [Install and enable](#install-and-en
31
31
  > **Prerequisite:** pi-auto-review is an authorizer inside
32
32
  > `@gotgenes/pi-permission-system`. Pi does not auto-install peer packages, so
33
33
  > install the permission system separately (once per machine) before this
34
- > extension. Version 0.10.0 requires permission-system 27.x:
34
+ > extension. This release line supports permission-system 29.3.0 through the
35
+ > latest 31.x release:
35
36
 
36
37
  Node.js 22.13.0 or newer is required. Permission auditing uses Node's built-in
37
38
  `node:sqlite`; it does not require a SQLite CLI, system SQLite library, or npm
@@ -76,6 +77,12 @@ following recognized permission dialog. The bridge is request-ID-bound,
76
77
  expires after ten seconds, and is consumed once. Mode, component, request, or
77
78
  event-order mismatches leave the original human dialog in place.
78
79
 
80
+ Permission-system 31.x also projects paths named directly by `for`/`select`
81
+ word lists and `case` subjects onto the existing `path` and
82
+ `external_directory` surfaces. Those additional requests follow the same
83
+ bounded review and one-shot auto-confirm rules; no new surface is implicitly
84
+ trusted.
85
+
79
86
  Automatic review stops for the current turn after three consecutive denials or
80
87
  ten denials in the last fifty reviews. An explicit denial tells the agent that
81
88
  automatic policy denied the request (not a human click), not to rephrase or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-auto-review",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "Fail-closed, model-backed approval broker for Pi: deterministically hard-denies dangerous operations, reviews dangerous boundaries with a reviewer model, and issues one-shot expiring grants that OS sandbox adapters must consume before retrying.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,12 +49,12 @@
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-ai": "^0.84.4",
51
51
  "@earendil-works/pi-coding-agent": "^0.84.4",
52
- "@gotgenes/pi-permission-system": "^28.0.0"
52
+ "@gotgenes/pi-permission-system": ">=29.3.0 <32.0.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@earendil-works/pi-ai": "^0.84.4",
56
56
  "@earendil-works/pi-coding-agent": "^0.84.4",
57
- "@gotgenes/pi-permission-system": "28.0.0",
57
+ "@gotgenes/pi-permission-system": "31.0.0",
58
58
  "@types/node": "^26.2.0",
59
59
  "typescript": "^6.0.3"
60
60
  }
@@ -75,6 +75,62 @@ function shellTokens(command: string): string[] {
75
75
  return command.trim().split(/\s+/u);
76
76
  }
77
77
 
78
+ function normalizedPathToken(token: string): string {
79
+ let value = token.replace(/^@/, "").replace(/[;)]+$/u, "");
80
+ if (
81
+ value.length >= 2 &&
82
+ ((value.startsWith("\"") && value.endsWith("\"")) ||
83
+ (value.startsWith("'") && value.endsWith("'")))
84
+ ) {
85
+ value = value.slice(1, -1);
86
+ }
87
+ return value;
88
+ }
89
+
90
+ function firstExplicitPathToken(tokens: readonly string[]): string | undefined {
91
+ return tokens
92
+ .map(normalizedPathToken)
93
+ .find((token) =>
94
+ token.startsWith("/") ||
95
+ token.startsWith("./") ||
96
+ token.startsWith("../") ||
97
+ token.startsWith("~/")
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Recover the path-bearing statement operands added to permission-system 31.
103
+ * This remains deliberately conservative rather than pretending to be a shell
104
+ * parser: case-arm patterns are excluded, while the actual path gate's
105
+ * separate decision event still records paths found in arm/body commands.
106
+ */
107
+ function statementOperandPathToken(
108
+ tokens: readonly string[],
109
+ index: number,
110
+ ): { handled: boolean; token?: string } {
111
+ const statement = tokens[index]?.replace(/^["']|["']$/gu, "");
112
+ if (statement !== "for" && statement !== "select" && statement !== "case") {
113
+ return { handled: false };
114
+ }
115
+ const inIndex = tokens.findIndex((token, tokenIndex) =>
116
+ tokenIndex > index && token === "in"
117
+ );
118
+ if (inIndex < 0) return { handled: true };
119
+ if (statement === "case") {
120
+ return {
121
+ handled: true,
122
+ token: firstExplicitPathToken(tokens.slice(index + 1, inIndex)),
123
+ };
124
+ }
125
+ const operands: string[] = [];
126
+ for (const token of tokens.slice(inIndex + 1)) {
127
+ if (token === "do") break;
128
+ operands.push(token);
129
+ if (/[;]$/u.test(token)) break;
130
+ }
131
+ return { handled: true, token: firstExplicitPathToken(operands) };
132
+ }
133
+
78
134
  function executableIndex(tokens: readonly string[]): number {
79
135
  let index = 0;
80
136
  if (tokens[index] === "env") index++;
@@ -161,9 +217,10 @@ export function classifyBash(command: unknown, cwd: string): ClassifiedPermissio
161
217
  eligible: blocker === undefined,
162
218
  ...(blocker ? { blocker } : {}),
163
219
  } : undefined;
164
- const pathToken = tokens.slice(index + 1)
165
- .map((token) => token.replace(/^@/, ""))
166
- .find((token) => token.startsWith("/") || token.startsWith("./") || token.startsWith("../") || token.startsWith("~/"));
220
+ const statementOperand = statementOperandPathToken(tokens, index);
221
+ const pathToken = statementOperand.handled
222
+ ? statementOperand.token
223
+ : firstExplicitPathToken(tokens.slice(index + 1));
167
224
  return {
168
225
  surface: "bash",
169
226
  signature: template ?? "<command>",
package/src/policy.ts CHANGED
@@ -202,10 +202,20 @@ function exactKeys(
202
202
  );
203
203
  }
204
204
 
205
+ // Some reviewer models (notably any routed through Claude Code, which always
206
+ // markdown-fences JSON) wrap the decision object in a ```json ... ``` block
207
+ // despite the prompt forbidding markdown. Strip a single enclosing fence before
208
+ // parsing; the strict shape validation below is unchanged.
209
+ function stripReviewerFence(text: string): string {
210
+ const t = text.trim();
211
+ const fenced = t.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
212
+ return (fenced ? fenced[1] : t).trim();
213
+ }
214
+
205
215
  export function parseDecision(text: string): ModelDecision {
206
216
  let value: unknown;
207
217
  try {
208
- value = JSON.parse(text.trim());
218
+ value = JSON.parse(stripReviewerFence(text));
209
219
  } catch {
210
220
  throw new Error("reviewer returned non-JSON output");
211
221
  }