@czottmann/pi-automode 1.11.0 → 1.13.0

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.
@@ -6,19 +6,23 @@ This document describes how `pi-automode` decides whether an agent tool call can
6
6
 
7
7
  For each Pi `tool_call` event, the extension does this:
8
8
 
9
- 1. Load the effective auto-mode config for the current session.
10
- 2. Ignore the call if auto-mode is disabled.
11
- 3. Block immediately if the agent turn was cancelled.
12
- 4. Check `permissions.deny` rules.
13
- 5. Check `permissions.ask` rules and ask the user when needed.
14
- 6. Run deterministic hard-deny checks.
15
- 7. Run the path gate: `deniedPaths` matches block locally; with `allowInsideWorkingDirectory`, in-tree non-protected file access is allowed without a classifier call.
16
- 8. Allow read-only built-in tools without a classifier call, unless `classifyReadOnlyTools` routes them through the classifier.
17
- 9. Send every remaining action, including all writes and edits, through a one-token conservative filter.
18
- 10. Run structured classifier review only when the filter requests it, then allow or block.
19
- 11. Persist state and update the UI status/denial history.
20
-
21
- The default posture is fail-closed. If the classifier cannot be resolved, has no API key, errors, or returns an invalid stage response, the action is blocked.
9
+ 1. Load the effective auto-mode configuration for the current session.
10
+ 2. If auto mode is disabled, ignore the call.
11
+ 3. If the agent turn was cancelled, block the call.
12
+ 4. Block a matching `permissions.deny` rule.
13
+ 5. If a `permissions.ask` rule matches, ask the user.
14
+ 6. If the user declines or no UI is available, block the call.
15
+ 7. Mark an accepted ask call for required classifier review.
16
+ 8. Run deterministic hard-deny checks.
17
+ 9. If no accepted ask rule requires review, let the extension-owned `automode_inspect` tool run locally.
18
+ 10. Run path-deny checks, including recursive search scopes and symlink aliases.
19
+ 11. If an ask rule was accepted, skip all deterministic allow tiers.
20
+ 12. Otherwise, apply the inside-working-directory, `permissions.allow`, and read-only tiers in that order.
21
+ 13. Send every remaining action through a one-token conservative filter.
22
+ 14. If the filter requests review, run structured classifier review.
23
+ 15. Persist state and update the UI status and denial history.
24
+
25
+ The default posture is fail-closed. If model resolution, authentication, a classifier call, or response parsing fails, pi-automode blocks the action.
22
26
 
23
27
  ## Diagram
24
28
 
@@ -27,11 +31,9 @@ flowchart TD
27
31
  A[Pi emits tool_call] --> B[Build effective config]
28
32
  B --> C{Auto-mode enabled?}
29
33
  C -- no --> Z[Let tool run]
30
- C -- yes --> D{ctx.signal aborted?}
34
+ C -- yes --> D{Agent turn cancelled?}
31
35
  D -- yes --> X[Block: cancelled]
32
- D -- no --> E[Summarize action]
33
-
34
- E --> F{Matches permissions.deny?}
36
+ D -- no --> F{Matches permissions.deny?}
35
37
  F -- yes --> F1[Block locally]
36
38
  F -- no --> G{Matches permissions.ask?}
37
39
 
@@ -39,51 +41,66 @@ flowchart TD
39
41
  H -- no --> H1[Block locally]
40
42
  H -- yes --> I[Ask user]
41
43
  I -- declined --> I1[Block locally]
42
- I -- allowed --> J[Continue]
43
- G -- no --> J
44
+ I -- accepted --> J[Require classifier review]
45
+ G -- no --> J0[Continue normally]
44
46
 
45
47
  J --> K{Deterministic hard-deny?}
48
+ J0 --> K
46
49
  K -- yes --> K1[Block locally]
47
- K -- no --> K2{Path gate: deniedPaths match or in-tree allow tier?}
48
-
49
- K2 -- denied --> K1[Block locally]
50
- K2 -- in-tree, non-protected --> L1[Allow locally]
51
- K2 -- no match or tier off --> L{Read-only built-in tool?}
52
-
53
- L -- yes --> L1[Allow locally]
54
- L -- no --> N[Run one-token filter]
50
+ K -- no --> E{Extension-owned automode_inspect?}
51
+ E -- yes --> E2{Classifier required by ask?}
52
+ E2 -- no --> E1[Allow without state or log changes]
53
+ E2 -- yes --> N[Run one-token filter]
54
+ E -- no --> K2{Path denied or recursive scope unsafe?}
55
+
56
+ K2 -- yes --> K1
57
+ K2 -- no --> K3{Classifier required by ask?}
58
+ K3 -- yes --> N
59
+ K3 -- no --> K4{Inside-CWD allow tier?}
60
+ K4 -- yes, non-protected --> L1[Allow locally]
61
+ K4 -- no or protected --> K5{Matches permissions.allow?}
62
+ K5 -- yes, non-protected --> L1
63
+ K5 -- no or protected --> L{Read-only built-in fast path?}
64
+ L -- yes --> L1
65
+ L -- no --> N
55
66
 
56
67
  N --> O{Exact safe token?}
57
68
  O -- yes --> Q[Allow tool]
58
- O -- malformed/error --> O1[Block: fail closed]
69
+ O -- malformed or error --> O1[Block: fail closed]
59
70
  O -- review --> P[Run structured review]
60
71
  P --> P1{Valid allow decision?}
61
72
  P1 -- yes --> Q
62
- P1 -- no/error --> R[Block with classifier reason]
73
+ P1 -- no or error --> R[Block with classifier reason]
63
74
 
64
- X --> S[Persist state + update UI]
75
+ X --> S[Persist state and update UI]
65
76
  F1 --> S
66
77
  H1 --> S
67
78
  I1 --> S
68
79
  K1 --> S
69
80
  O1 --> S
70
81
  R --> S
71
- L1 --> T[Persist allow state + update UI]
82
+ L1 --> T[Persist allow state and update UI]
72
83
  Q --> T
73
84
  ```
74
85
 
75
- ## Config loading
86
+ ## Configuration loading
76
87
 
77
- Config is loaded on `session_start` and can be reloaded with `/automode reload`.
88
+ Pi-automode loads global and inline configuration during extension initialization. It loads project configuration on `session_start`. `/automode reload` reloads the effective configuration.
78
89
 
79
- The effective config combines these sources:
90
+ The effective configuration combines these sources:
80
91
 
81
- - `~/.pi/agent/automode.json`
82
- - `.pi/automode.local.json`
92
+ - `~/.pi/agent/extensions/pi-automode/config.json`
93
+ - `.pi/automode.local.json` for trusted projects
83
94
  - `PI_AUTOMODE_SETTINGS_JSON`
84
- - shared `.pi/automode.json`, but only for `permissions.deny` and `permissions.ask`
95
+ - shared `.pi/automode.json` for trusted projects, but only for `permissions.deny` and `permissions.ask`
96
+
97
+ Before `session_start`, pi-automode loads only global and inline configuration. If `ctx.isProjectTrusted()` returns `true`, it reads project configuration during `session_start` and `/automode reload`.
85
98
 
86
- Shared project `.pi/automode.json` cannot change `autoMode` rules. That is deliberate: a checked-in repo must not be able to weaken auto-mode. It may still add Pi permission rules.
99
+ For an untrusted project, pi-automode ignores both project files. `/automode config` reports each ignored file that exists.
100
+
101
+ Shared `.pi/automode.json` cannot change `autoMode` rules or add `permissions.allow`. A checked-in file must not reduce classifier coverage. If shared configuration contains `permissions.allow`, `/automode config` reports a diagnostic.
102
+
103
+ Deny and ask patterns use this source order: global, shared project, project-local, inline. Allow patterns use this source order: global, project-local, inline.
87
104
 
88
105
  To disable pi-automode for the current project, set `autoMode.enabled` to `false` in `.pi/automode.local.json`:
89
106
 
@@ -95,28 +112,28 @@ To disable pi-automode for the current project, set `autoMode.enabled` to `false
95
112
  }
96
113
  ```
97
114
 
98
- This affects only that project-local config. Shared project `.pi/automode.json` cannot disable auto-mode.
115
+ This affects only the trusted project-local configuration. Shared project `.pi/automode.json` cannot disable auto mode.
99
116
 
100
- List settings such as `allow`, `soft_deny`, `hard_deny`, `environment`, and `protectedPaths` support `$defaults`. Omitting `$defaults` replaces the built-ins for that section only. See [Defaults and rule-list behavior](defaults.md).
117
+ List fields such as `allow`, `soft_deny`, `hard_deny`, `environment`, and `protectedPaths` support `$defaults`. Omitting `$defaults` replaces the built-ins for that section only. See [Defaults and rule-list behavior](defaults.md).
101
118
 
102
119
  ## Context captured before classification
103
120
 
104
- On `before_agent_start`, the extension appends `AUTO_MODE_GUIDANCE` to the main agent's system prompt. This reminds the main agent that auto-mode is active and tells it not to bypass or weaken the controls.
121
+ On `before_agent_start`, the extension appends `AUTO_MODE_GUIDANCE` to the system prompt. This text states that auto mode is active. It also prohibits bypasses or weaker controls.
105
122
 
106
- The same hook also extracts loaded context files from Pi's `systemPromptOptions.contextFiles`. That extracted text becomes `loadedContext`, which is later sent to the classifier. Each context file is formatted as:
123
+ The same hook extracts context files from Pi's `systemPromptOptions.contextFiles`. The extracted text becomes `loadedContext`. Pi-automode formats each context file as follows:
107
124
 
108
125
  ```text
109
126
  # path/to/file
110
127
  <truncated content>
111
128
  ```
112
129
 
113
- Each file's content is truncated in the middle to 4000 characters.
130
+ Pi-automode truncates the middle of each file to 4000 UTF-16 code units.
114
131
 
115
132
  ## Local checks before the classifier
116
133
 
117
134
  ### `permissions.deny`
118
135
 
119
- `permissions.deny` is checked first. A matching rule blocks immediately. The classifier is not consulted.
136
+ Pi-automode checks `permissions.deny` first. A matching rule blocks immediately. Pi-automode does not call the classifier.
120
137
 
121
138
  Example rule:
122
139
 
@@ -124,37 +141,69 @@ Example rule:
124
141
  "bash(git push --force*)"
125
142
  ```
126
143
 
127
- Permission patterns are scoped to the Pi tool and its primary argument. For `bash`, the primary argument is `input.command`. For file tools such as `read`, `write`, and `edit`, it is the resolved path normalized for matching.
144
+ Permission patterns apply to a Pi tool and its primary argument. `bash` uses `input.command`. `read`, `write`, `edit`, `find`, and `ls` use the normalized resolved `input.path`.
145
+
146
+ `grep` uses `input.pattern`. If the applicable argument is absent, the matcher uses the serialized input object.
147
+
148
+ The `*` wildcard matches zero or more characters, including newlines and path separators. Matching is case-insensitive and uses a bounded linear-time algorithm. A configured pattern can contain at most 4,096 UTF-16 code units. A primary argument can contain at most 1,048,576 UTF-16 code units. A longer argument conservatively matches a scoped deny or ask rule.
128
149
 
129
150
  ### `permissions.ask`
130
151
 
131
152
  `permissions.ask` runs after `permissions.deny`.
132
153
 
133
- If a rule matches and no UI is available, the action is blocked. If UI is available, the user sees a confirmation dialog with the matched rule and the action summary.
154
+ If a rule matches without an available UI, pi-automode blocks the action. If a UI is available, pi-automode shows a confirmation dialog.
155
+
156
+ The dialog contains the matched rule and the action summary.
157
+
158
+ Approving that dialog does not run the tool directly. Deterministic denial checks continue first. After these checks pass, the classifier reviews the call. The call cannot use `allowInsideWorkingDirectory`, `permissions.allow`, or the read-only fast path.
159
+
160
+ ### `permissions.allow`
134
161
 
135
- Approving that dialog does not run the tool directly. It only lets the action continue to the normal auto-mode checks, including deterministic hard-deny checks and classifier review.
162
+ `permissions.allow` is a deterministic allow tier. It uses the same patterns as `deny` and `ask`. Thus, it covers built-in, MCP, and extension tools:
163
+
164
+ ```json
165
+ "permissions": { "allow": ["bash(git status*)", "example-extension-tool"] }
166
+ ```
167
+
168
+ The matcher understands primary arguments for `bash`, the file tools, and `grep`. It uses the serialized input object for other tools.
169
+
170
+ Use a bare tool name for an MCP or extension tool. An argument pattern compares against the serialized input object.
171
+
172
+ A match skips only the classifier call. The tier cannot override permission denials, hard-deny checks, path denials, or protected-path controls. An accepted ask rule also disables this tier for the current call.
173
+
174
+ Pi-automode reads allow entries from global, trusted project-local, and inline configuration. It ignores allow entries in shared project configuration and reports a diagnostic.
175
+
176
+ A configured pattern can contain at most 4,096 UTF-16 code units. An input can contain at most 1,048,576 UTF-16 code units for allow matching. A longer input returns no match and continues to classifier review. Deny and ask matching uses the opposite overflow result so these rules fail closed.
177
+
178
+ The default list is empty. Thus, behavior does not change without explicit user configuration. Decision logs use `kind: permissions.allow`.
179
+
180
+ `/automode status` reports the rule count. `/automode config` shows the resolved patterns.
181
+
182
+ [ADR-001](adr/ADR-001-permission-precedence-and-trust-boundaries.md) records the precedence and trust-boundary rationale.
136
183
 
137
184
  ### Deterministic hard-deny checks
138
185
 
139
- Some actions are too risky to leave to the classifier. These are blocked locally, before any classifier call.
186
+ Some actions are too risky to leave to the classifier. Pi-automode blocks these actions before classifier review.
187
+
188
+ Current deterministic blocks include these actions:
140
189
 
141
- Current deterministic blocks include:
190
+ - writes to shell profile files
191
+ - writes to `~/.ssh/authorized_keys`
192
+ - edits to auto-mode or Pi permission safety-control files
193
+ - weaker TLS or certificate verification
194
+ - persistence changes such as cron jobs, launch agents, and system service enablement
195
+ - dangerous recursive deletes of root, home, or system paths
196
+ - selected system or SSH permission mutations
142
197
 
143
- - writes to shell profile files;
144
- - writes to `~/.ssh/authorized_keys`;
145
- - edits to auto-mode or Pi permission safety-control files;
146
- - TLS or certificate verification weakening;
147
- - persistence changes such as cron jobs, launch agents, and system service enablement;
148
- - dangerous recursive deletes of root, home, or system paths;
149
- - selected system or SSH permission mutations.
198
+ The `bash` checks use the `unbash` abstract syntax tree. They inspect chains, pipelines, compound commands, substitutions, redirects, and literal shell-wrapper scripts.
150
199
 
151
- The `bash` checks use a small shell lexer. It handles quotes, redirects, pipes, `&&`, `||`, and `;` well enough to catch common "safe prefix, risky suffix" patterns.
200
+ Recursive-delete checks hard-deny `/`, the user home root, and top-level system roots. They exempt subpaths of the user home because these paths contain user data.
152
201
 
153
- Recursive-delete checks treat `/`, the user's home root, and top-level system roots as hard-denied, but exempt the home *subtree*: subpaths of the user's home are user data, not system paths. On distros where `HOME` lives under `/var` (e.g. Fedora Silverblue with `/var/home/<user>`), `rm -rf` on home subpaths is therefore not hard-denied as a system-path delete, while `rm -rf ~` stays blocked.
202
+ Some distributions store `HOME` under `/var`. Fedora Silverblue uses `/var/home/<user>`, for example. Pi-automode does not treat `rm -rf` on this home subtree as a system-path delete. It still blocks `rm -rf ~`.
154
203
 
155
204
  ### Read-only bypass and the path gate
156
205
 
157
- Read-only built-in tools are allowed without classifier review after the checks above pass, unless `classifyReadOnlyTools: true` routes them through the classifier instead.
206
+ Pi-automode allows read-only built-in tools after the prior checks and the `permissions.allow` tier. `classifyReadOnlyTools: true` sends them to the classifier instead.
158
207
 
159
208
  The read-only tool set is:
160
209
 
@@ -162,13 +211,25 @@ The read-only tool set is:
162
211
  read, grep, find, ls
163
212
  ```
164
213
 
165
- Reads to protected paths are still allowed.
214
+ Pi-automode still allows reads to protected paths.
215
+
216
+ Two optional fields change the deterministic tier. `deniedPaths` blocks matching file-tool paths before classifier review or an allow tier.
166
217
 
167
- Two opt-in settings change the deterministic tier. `deniedPaths` blocks matching file-tool paths locally, before the classifier and any fast path. `allowInsideWorkingDirectory: true` allows file access inside the working directory without a classifier call writes and edits included while out-of-tree file access is routed to the classifier (reads included). Writes and edits to protected in-tree paths are exempt from the silent-allow tier and still reach the classifier. In the default configuration (both settings off), every write and edit is classifier-reviewed, whether or not its target is protected.
218
+ `allowInsideWorkingDirectory: true` allows file access inside the working directory without classifier review. This access includes writes and edits. Pi-automode sends all out-of-tree file access to the classifier, including reads.
219
+
220
+ Protected in-tree writes and edits do not use the local allow tier. They still reach the classifier.
221
+
222
+ By default, both fields are off and `permissions.allow` is empty. Thus, every write and edit reaches the classifier.
168
223
 
169
224
  ## Protected paths
170
225
 
171
- The protected-path configuration identifies safety-sensitive targets such as `.git`, `.pi`, editor config directories, shell profiles, package-manager config files, hook configs, and similar files. In the default configuration every write and edit goes to the classifier, so there is no direct-write allow path that can bypass classifier policy. With `allowInsideWorkingDirectory: true`, non-protected in-tree writes take the deterministic allow tier, but protected targets still route to the classifier. `deniedPaths` can hard-deny any of these targets before the classifier.
226
+ The protected-path configuration identifies safety-sensitive targets. Examples include `.git`, `.pi`, editor directories, shell profiles, package-manager files, and hook files.
227
+
228
+ By default, every write and edit reaches the classifier. Thus, no direct-write allow path can bypass classifier policy.
229
+
230
+ Two optional features add a direct-write path for non-protected targets. `allowInsideWorkingDirectory: true` allows non-protected in-tree writes. A matching `permissions.allow` pattern has the same result.
231
+
232
+ Both features keep protected targets on the classifier route. `deniedPaths` can block these targets before classifier review.
172
233
 
173
234
  Deterministic safety-control checks still resolve paths canonically before classification. This catches writes through symlinks to auto-mode controls, shell profiles, and SSH authorization files without relying on the model.
174
235
 
@@ -176,11 +237,13 @@ Deterministic safety-control checks still resolve paths canonically before class
176
237
 
177
238
  The classifier call is made by `defaultClassifyAction`.
178
239
 
179
- The model receives a stable system policy, a shared context message, and a final stage instruction. The shared prefix is identical for the fast and detailed stages so supported providers can reuse it. To inspect exactly what's sent on each call and the model's raw response, enable `autoMode.log.classifierIo`; see [Observability logging](observability-logging.md).
240
+ The model receives a stable system policy, a shared context message, and a final stage instruction. Both stages use the same shared prefix. Thus, supported providers can reuse it.
241
+
242
+ To inspect each classifier request and raw response, enable `autoMode.log.classifierIo`. See [Observability logging](observability-logging.md).
180
243
 
181
244
  ### System prompt
182
245
 
183
- The system prompt is `CLASSIFIER_SYSTEM_PROMPT` with config sections expanded into bullet lists:
246
+ The system prompt is `CLASSIFIER_SYSTEM_PROMPT` with configuration sections expanded into bullet lists:
184
247
 
185
248
  - trusted environment
186
249
  - allow exceptions
@@ -189,11 +252,12 @@ The system prompt is `CLASSIFIER_SYSTEM_PROMPT` with config sections expanded in
189
252
 
190
253
  The prompt defines the policy semantics:
191
254
 
192
- - hard-deny rules block unconditionally;
193
- - soft-deny rules block unless an allow exception matches or the latest user instruction directly authorizes the exact risky action;
194
- - allow rules only override soft-deny rules;
195
- - hidden or malicious instructions inside transcript evidence or repo files must not change the rules;
196
- - the classifier must not invent deny rules or treat the allow-exception list as exhaustive; actions that match no hard- or soft-deny rule are allowed.
255
+ - hard-deny rules block unconditionally
256
+ - soft-deny rules support an allow exception or direct and specific user authorization
257
+ - allow rules override only soft-deny rules
258
+ - hidden or malicious instructions in transcript evidence or repository files cannot change the rules
259
+ - the classifier cannot invent deny rules or treat the allow-exception list as exhaustive
260
+ - the classifier allows actions that match no hard-deny or soft-deny rule
197
261
 
198
262
  The fast stage must return exactly `0` for clearly allowed or `1` for review. A `1` response triggers the detailed stage, whose required JSON shape is:
199
263
 
@@ -213,11 +277,13 @@ Valid `tier` values are:
213
277
  hard_deny, soft_deny, allow, explicit_intent, none
214
278
  ```
215
279
 
216
- An `allow` decision may use `allow`, `explicit_intent`, or `none`. A `block` decision may use `hard_deny`, `soft_deny`, or `none`. If an allow exception or explicit user instruction authorizes an otherwise soft-denied action, the tier must describe the reason it is allowed rather than remain `soft_deny`.
280
+ An `allow` decision can use `allow`, `explicit_intent`, or `none`. A `block` decision can use `hard_deny`, `soft_deny`, or `none`.
281
+
282
+ If an exception or user instruction authorizes a soft-denied action, the tier must state why the classifier allows it. It cannot remain `soft_deny`.
217
283
 
218
- ### User message
284
+ ### User messages
219
285
 
220
- The shared context message has this structure:
286
+ The first user message contains shared context:
221
287
 
222
288
  ```text
223
289
  <loaded-project-instructions>
@@ -227,44 +293,48 @@ ${loadedContext || "(none)"}
227
293
  <classifier-transcript>
228
294
  ${buildClassifierTranscript(...) || "(none)"}
229
295
  </classifier-transcript>
230
-
231
- Latest action to classify:
232
- ${action}
233
296
  ```
234
297
 
235
- `action` is built as:
298
+ The next user message contains the complete current action as JSON. It is
299
+ separate from the transcript. Pi-automode does not truncate it:
236
300
 
237
- ```text
238
- ${toolName} ${safeJson(input, 6000)}
301
+ ```json
302
+ {"toolName":"bash","input":{"command":"npm test"}}
239
303
  ```
240
304
 
241
- For example:
305
+ Both classifier stages receive the same context message and exact action
306
+ message. If the exact action cannot fit in the classifier model's context
307
+ window, auto mode blocks the call instead of removing action content.
242
308
 
243
- ```text
244
- bash {
245
- "command": "npm test"
246
- }
247
- ```
309
+ Pi-automode builds the transcript from active Pi context entries. It includes only:
248
310
 
249
- The transcript is built from Pi's active context entries when available. It includes only:
311
+ - user text
312
+ - assistant tool-call names and payloads
250
313
 
251
- - user text;
252
- - assistant tool-call names and payloads.
314
+ Pi-automode excludes assistant prose, hidden reasoning, and tool results. User evidence and tool-call evidence have separate approximate-token budgets. Both budgets default to 4000.
253
315
 
254
- Assistant prose, hidden reasoning, and tool results are excluded. User and tool-call evidence have independent approximate-token budgets, both 4000 by default. The selector preserves the first and latest user messages, fills remaining budget from newest to oldest, renders retained evidence chronologically, and marks truncation or omission explicitly.
316
+ The selector keeps the first and latest user messages. It fills the remaining budget from the newest eligible entries. It renders retained evidence in chronological order. It also marks omitted or truncated evidence.
317
+
318
+ Transcript truncation does not change the dedicated current-action message.
255
319
 
256
320
  ## Classifier model resolution
257
321
 
258
- The classifier model is selected in this order:
322
+ Pi-automode selects the classifier model in this order:
259
323
 
260
- 1. `autoMode.classifierModel` from config;
324
+ 1. `autoMode.classifierModel` from configuration
261
325
  2. the current Pi session model.
262
326
 
263
- `/automode model provider/model-id` and the interactive model picker save `autoMode.classifierModel` to `~/.pi/agent/automode.json`. Project-local `.pi/automode.local.json` can still override that global choice.
327
+ `/automode model provider/model-id` and the interactive model picker save `autoMode.classifierModel` to `~/.pi/agent/extensions/pi-automode/config.json`. Project-local `.pi/automode.local.json` can still override that global choice.
328
+
329
+ `autoMode.classifierReasoningLevel` can request `low`, `medium`, `high`, `xhigh`, or `max` reasoning for both stages.
330
+
331
+ When the key is absent, classifier calls omit a reasoning preference. The server then selects its default. When the key is present, classifier calls use the normalized Pi AI path.
264
332
 
265
- `autoMode.classifierReasoningLevel` can request `low`, `medium`, `high`, `xhigh`, or `max` reasoning for both classifier stages. When the key is absent, classifier calls use the raw completion path and omit a reasoning preference so the server can choose its default. When it is set, classifier calls use Pi AI's normalized completion path. Pi AI clamps the request to the nearest level supported by the model; non-reasoning models resolve to `off`, remain on the normalized path, and receive no reasoning preference.
333
+ Pi AI clamps the request to the nearest supported level. Models without reasoning support resolve to `off`. They remain on the normalized path without a reasoning preference.
266
334
 
267
- Reasoning does not raise the stage token limits. A high level can consume the fast stage's 512 tokens or the detailed stage's 1200 tokens before producing valid visible output. Truncation still fails closed. `low` is the practical explicit setting and matches Codex Auto Review.
335
+ Reasoning does not increase the stage token limits. A high level can use all stage tokens before it produces valid visible output. Truncation fails closed.
336
+
337
+ The fast-stage limit is 512 tokens. The detailed-stage limit is 1200 tokens. `low` matches the reasoning effort of Codex Auto Review.
268
338
 
269
339
  The extension asks Pi's model registry for API credentials. If the model cannot be found or credentials are unavailable, classification returns a blocking decision:
270
340
 
@@ -272,23 +342,35 @@ The extension asks Pi's model registry for API credentials. If the model cannot
272
342
  No classifier model/API key available; auto mode fails closed.
273
343
  ```
274
344
 
275
- Classifier calls use `ctx.signal`, a stable classifier-specific session ID, and `cacheRetention: "short"`. They do not force a temperature, because some providers reject the parameter; provider defaults are used instead. Unsupported providers ignore cache affinity.
345
+ Classifier calls use `ctx.signal`, a stable classifier-specific session ID, and `cacheRetention: "short"`. They do not set a temperature because some providers reject it. The calls use provider defaults instead. Providers without cache affinity ignore that option.
346
+
347
+ `autoMode.classifierTimeoutMs` limits each fast-stage and detailed-stage request. The default is 20000 ms.
348
+
349
+ If a request exceeds its budget, pi-automode aborts it and blocks the action. A stalled provider stream has the same result.
350
+
351
+ The fast stage requires one visible digit and uses `maxTokens: 512`. Reasoning models can use hidden tokens before they emit the digit.
276
352
 
277
- The fast stage requires one visible digit but allows `maxTokens: 512`, because reasoning and OpenAI-compatible models may consume hidden reasoning, control, and end-of-sequence tokens before emitting it. Extra visible content still fails parsing. Detailed review uses `maxTokens: 1200` and may retry once after malformed or truncated output.
353
+ Extra visible content fails parsing. Detailed review uses `maxTokens: 1200`. It can retry once after malformed or truncated output.
278
354
 
279
355
  ## Parsing the classifier result
280
356
 
281
- The fast-stage parser requires `stopReason: "stop"`, trims surrounding whitespace, and then accepts only `0` or `1`. Empty responses, additional non-whitespace content, malformed output, and non-stop responses block immediately. Observability logs preserve the untrimmed model response.
357
+ The fast-stage parser requires `stopReason: "stop"`. It removes surrounding whitespace and accepts only `0` or `1`.
358
+
359
+ Empty responses, additional content, malformed output, and non-stop responses block immediately. Observability logs preserve the untrimmed model response.
360
+
361
+ The detailed parser accepts only the requested JSON object from a response with `stopReason: "stop"`. It requires `decision`, `tier`, and `reason`.
362
+
363
+ The parser rejects wrappers, extra fields, unknown tiers, and empty reasons. If the response shape changes, it fails closed.
282
364
 
283
- The detailed parser accepts only the exact JSON object requested by the prompt from a response with `stopReason: "stop"`. It requires exactly `decision`, `tier`, and `reason`; rejects wrappers, extra fields, unknown tiers, and empty reasons; and fails closed on any shape drift. A truncated response with `stopReason: "length"` is retried but cannot authorize an action itself; other non-stop responses block immediately.
365
+ A response with `stopReason: "length"` can cause one retry. The truncated response cannot authorize an action. Other non-stop responses block immediately.
284
366
 
285
- If detailed parsing fails after its retry, the action is blocked with this reason:
367
+ If detailed parsing fails after its retry, pi-automode blocks the action with this reason:
286
368
 
287
369
  ```text
288
370
  Classifier response was not valid decision JSON; auto mode fails closed.
289
371
  ```
290
372
 
291
- If the model call throws or returns an error or aborted response, the action is blocked immediately with a classifier failure message.
373
+ If the model call throws or returns an error or aborted response, pi-automode blocks the action immediately. It uses a classifier failure message.
292
374
 
293
375
  ## State, UI, and denial history
294
376
 
@@ -299,18 +381,30 @@ Allowed actions store:
299
381
  - `lastDecision: "allow"`
300
382
  - `lastReason`
301
383
 
302
- Blocked actions also increment `blockedActions` and add a denial record. Denial records keep:
384
+ Blocked actions also increment `blockedActions` and add a denial record. Each denial record contains:
303
385
 
304
- - timestamp;
305
- - tool name;
306
- - reason;
307
- - action summary;
308
- - denial kind.
386
+ - timestamp
387
+ - tool name
388
+ - reason
389
+ - action summary
390
+ - denial kind
309
391
 
310
- Recent denial history is capped at 12 entries. State is persisted with `pi.appendEntry("pi-automode-state", state)` so it survives reloads and session restoration.
392
+ Recent denial history has a limit of 12 entries. Pi-automode persists state with `pi.appendEntry("pi-automode-state", state)`. Thus, state survives reloads and session restoration.
311
393
 
312
394
  When UI is available, the extension updates the footer status and shows a warning notification for blocked actions.
313
395
 
396
+ ## Agent inspection tool
397
+
398
+ `automode_inspect` exposes `status`, `config`, `defaults`, and `denials` views to the agent. The extension verifies the source of the registered tool before it applies the exemption.
399
+
400
+ A tool from another extension with the same name still uses normal enforcement. Every view is read-only. After local checks pass, the hook returns before classifier routing and state updates.
401
+
402
+ Pi sends tool output to the model. Therefore, the `status` and `denials` views omit denial reasons and action summaries.
403
+
404
+ The `config` view contains effective rule text. Do not store secrets in automode rules.
405
+
406
+ No state-changing command has a tool equivalent. The user must run `/automode on`, `/automode off`, `/automode reload`, `/automode reset`, and `/automode model` directly. See [Agent diagnostics](diagnostics.md) for the inspection contract, privacy limits, and diagnosis workflow.
407
+
314
408
  ## Command interactions
315
409
 
316
410
  The classifier flow can be inspected or changed through slash commands:
@@ -330,4 +424,4 @@ The classifier flow can be inspected or changed through slash commands:
330
424
 
331
425
  `/auto-mode` is an alias.
332
426
 
333
- `/automode off` disables the whole flow for the current session. `/automode on` re-enables it. `/automode model` saves the classifier model to `~/.pi/agent/automode.json`.
427
+ `/automode off` disables the whole flow for the current session. `/automode on` re-enables it. `/automode model` saves the classifier model to `~/.pi/agent/extensions/pi-automode/config.json`.