@ibartel74/pi-automode-ext 1.0.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.
@@ -0,0 +1,226 @@
1
+ # Configuration
2
+
3
+ The extension follows the documented Claude Code configuration model where Pi supports it.
4
+
5
+ It reads `autoMode` only from Pi-owned configuration sources:
6
+
7
+ - `~/.pi/agent/extensions/pi-automode/config.json`
8
+ - `.pi/automode.local.json` for trusted projects
9
+ - `PI_AUTOMODE_SETTINGS_JSON`
10
+
11
+ At startup, pi-automode moves a legacy `~/.pi/agent/automode.json` file to the new global path. If both files exist, it uses the new file and reports the conflict. If migration fails, it uses the legacy file for that session and reports the error.
12
+
13
+ It does not read project configuration until Pi trusts the project. For an untrusted project, it ignores `.pi/automode.local.json` and `.pi/automode.json`. `/automode config` reports each ignored file that exists.
14
+
15
+ Shared project `.pi/automode.json` cannot weaken auto mode. For a trusted project, it can add `permissions.deny` and `permissions.ask` rules.
16
+
17
+ The shared file cannot set `autoMode` or add `permissions.allow` rules. If the file contains `permissions.allow`, `/automode config` reports a diagnostic.
18
+
19
+ To disable pi-automode for the current project, create or edit `.pi/automode.local.json`:
20
+
21
+ ```json
22
+ {
23
+ "autoMode": {
24
+ "enabled": false
25
+ }
26
+ }
27
+ ```
28
+
29
+ This file is project-local. Pi reads it only after project trust. Do not commit this file. Shared project `.pi/automode.json` cannot disable auto mode.
30
+
31
+ Set a global default classifier model in `~/.pi/agent/extensions/pi-automode/config.json`. For a trusted project, override it in `.pi/automode.local.json`.
32
+
33
+ `classifierReasoningLevel` requests `low`, `medium`, `high`, `xhigh`, or `max` reasoning for both classifier stages. If the key is absent, pi-automode sends no reasoning preference. The server then selects the level.
34
+
35
+ Pi AI clamps an unsupported value to the nearest level that the selected model supports. A model without reasoning support resolves to `off`. `low` matches the reasoning effort of Codex Auto Review.
36
+
37
+ Higher levels can use all 512 or 1200 stage tokens before they produce visible output. In this case, the classifier fails closed. If truncation occurs before the required `0` or `1` digit, increase `fastClassifierMaxTokens`. The default is 512, and the minimum is 16.
38
+
39
+ `classifierTimeoutMs` limits each classifier request in milliseconds. The default is 20000, and the minimum is 1000. The fast and detailed stages have separate budgets.
40
+
41
+ If a request stalls or exceeds its budget, pi-automode aborts it. Then auto mode fails closed and blocks the action.
42
+
43
+ `allowInsideWorkingDirectory` adds a deterministic allow tier for the file tools. The default value is `false`. The file tools are `read`, `write`, `edit`, `grep`, `find`, and `ls`.
44
+
45
+ The value `allowInsideWorkingDirectory: true` allows access to paths inside the working directory without classifier review. Pi-automode sends access to outside paths to the classifier. This rule also applies to read calls.
46
+
47
+ This tier takes precedence over `classifyReadOnlyTools`. If both configuration fields are enabled, pi-automode still allows in-tree file access locally. `classifyReadOnlyTools: true` does not change this behavior.
48
+
49
+ Protected in-tree targets do not use this allow tier. Writes and edits to `.git/hooks`, `.pi` controls, shell profiles, and configuration files still reach the classifier.
50
+
51
+ `interactiveConfirm` turns classifier blocks into an interactive user confirmation when a UI is available. The default value is `true`. The dialog shows the block tier, the classifier's reason, and the action summary, and offers allow-once, always-allow (global or project), and block. The always-allow choices persist an exact-match `permissions.allow` rule — `bash(<command>)` or `<tool>(<path>)` — to the global config or `.pi/automode.local.json` and reload the effective config immediately. A project rule written in an untrusted project stays inert until the project is trusted. Persisted rules skip classifier review for future matching actions, including classifier `hard_deny` rules. Rules are not generated for targets containing wildcards or pattern syntax, and tools without a patternable argument only offer allow-once and block. This applies to every classifier block tier, including `hard_deny` and fail-closed errors such as an unavailable classifier. Deterministic denials (`permissions.deny`, deterministic hard-deny checks, `deniedPaths`) never prompt. Without a UI, or with `interactiveConfirm: false`, classifier blocks stand unchanged. Approved actions are logged as `user-confirmed` allow decisions and counted in the `uc:` status-line segment.
52
+
53
+ `deniedPaths` is a list of path glob patterns. The default list is `[]`. A matching pattern blocks a file-tool call before classifier review or an allow tier.
54
+
55
+ Patterns support `~`, `$HOME`, and `${HOME}` expansion. The `*` wildcard matches all characters, including `/`. Thus, `**/id_rsa` matches a private key at any depth.
56
+
57
+ Each pattern can contain at most 4,096 UTF-16 code units. Pi-automode matches the typed path and its symlink-resolved form. It also resolves the fixed path prefix of each pattern. Thus, a symlink alias cannot bypass a denied target.
58
+
59
+ If a recursive `grep` or `find` scope can contain a denied path, pi-automode blocks the call. A broad pattern such as `*.env` blocks these tools for every directory scope.
60
+
61
+ A matching path blocks the call without classifier review or an override. The list applies only to file tools. The classifier governs `bash` path access. Both keys use the normal scalar and array precedence.
62
+
63
+ `allowInsideWorkingDirectory` and `interactiveConfirm` use scalar precedence: global, then project-local, then `PI_AUTOMODE_SETTINGS_JSON`. `deniedPaths` entries accumulate across these configuration sources.
64
+
65
+ Shared project `.pi/automode.json` cannot set any of these fields. Omitting one of them at a higher-precedence source does not clear a lower-source value.
66
+
67
+ Example:
68
+
69
+ ```json
70
+ {
71
+ "autoMode": {
72
+ "classifierModel": "provider/model-id",
73
+ "classifierReasoningLevel": "low",
74
+ "classifyReadOnlyTools": false,
75
+ "interactiveConfirm": true,
76
+ "fastClassifierMaxTokens": 512,
77
+ "classifierTimeoutMs": 20000,
78
+ "allowInsideWorkingDirectory": false,
79
+ "deniedPaths": [],
80
+ "maxUserTranscriptTokens": 4000,
81
+ "maxToolTranscriptTokens": 4000,
82
+ "environment": [
83
+ "$defaults",
84
+ "Source control: github.example.com/acme-corp and all repos under it",
85
+ "Trusted internal domains: *.corp.example.com, git.example.com",
86
+ "Trusted cloud buckets: s3://acme-dev-artifacts, gs://acme-ci-cache",
87
+ "Key internal services: staging deploy API at deploy.corp.example.com"
88
+ ],
89
+ "allow": ["$defaults"],
90
+ "protectedPaths": ["$defaults"],
91
+ "soft_deny": ["$defaults"],
92
+ "hard_deny": [
93
+ "$defaults",
94
+ "Never send repository contents to third-party code-review APIs"
95
+ ]
96
+ },
97
+ "permissions": {
98
+ "deny": ["bash(rm -rf *)"],
99
+ "ask": ["bash(git push *)"],
100
+ "allow": ["bash(git status*)", "example-extension-tool"]
101
+ }
102
+ }
103
+ ```
104
+
105
+ `maxUserTranscriptTokens` and `maxToolTranscriptTokens` are approximate budgets for each category. Both default to 4000 and accept integers of at least 32.
106
+
107
+ Pi-automode does not support the former `maxTranscriptLines` field. Evidence selection now uses token budgets instead of line counts.
108
+
109
+ ## Ask-user tools and explicit authorization
110
+
111
+ Classifier evidence includes normal user messages and assistant tool-call inputs. It excludes assistant prose and all tool results. This exclusion includes answers from ask-user tools such as `@vanillagreen/pi-questions`.
112
+
113
+ Selecting "Yes" in that tool helps the agent select its next action. Pi-automode does not treat the result as authorization to override a soft deny.
114
+
115
+ Send the authorization as a normal chat message. Then the agent can retry the action. Tool results remain excluded because they can contain untrusted or prompt-injected content.
116
+
117
+ ## `$defaults`
118
+
119
+ See [Defaults and rule-list behavior](defaults.md) for built-in `environment`, `allow`, `protectedPaths`, `soft_deny`, and `hard_deny` entries. The document also explains replacement behavior after omission of `$defaults`.
120
+
121
+ ## Observability logging
122
+
123
+ Auto mode can write a JSONL observability log for decisions and classifier usage. Persisted sessions use a sidecar next to the Pi session file. In-memory sessions use a global application directory. Logging is off by default.
124
+
125
+ ```json
126
+ {
127
+ "autoMode": {
128
+ "log": {
129
+ "enabled": true,
130
+ "classifierIo": false
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ With logging enabled, persisted-session sidecars also contain ccusage-compatible entries for every classifier response. When `classifierIo` is off, `ccusage pi` still reports a separate `-pi-automode` session. In-memory logs use the same entry shape but live outside the normal Pi session tree.
137
+
138
+ See [Observability logging](observability-logging.md) for the log file location, entry schema, and the `classifierIo` privacy tradeoff. Run `/automode config` to see the resolved log file path.
139
+
140
+ ## Permission patterns
141
+
142
+ Permission patterns use Pi tool names. Examples include `bash(...)`, `write(...)`, `edit(...)`, and `read(...)`. The parser accepts capitalized names such as `Bash(...)`. The documented form is lowercase because Pi tool names are lowercase.
143
+
144
+ `permissions.allow` is a deterministic allow tier. The default list is `[]`. A matching rule skips classifier review.
145
+
146
+ Use this tier for a narrow command such as `bash(git status*)`. You can also use it for a side-effect-free extension or MCP tool.
147
+
148
+ The matcher understands primary arguments for `bash`, the file tools, and `grep`. For file tools, it uses the resolved `input.path`. For `grep`, it uses `input.pattern`.
149
+
150
+ For `bash`, pi-automode parses `input.command` with `unbash`. Deny and ask rules inspect each executable command in the Bash syntax tree. This includes pipelines, logical chains, compound commands, substitutions, and literal scripts passed to `bash -c`, `sh -c`, or `eval`. The analysis also follows these literal shell scripts through transparent `command`, `exec`, and `env` dispatch.
151
+
152
+ The matcher normalizes whitespace between Bash tokens. It preserves whitespace and quoting inside each token. Thus, `bash(git push*)` matches `git push origin main`. Quoted operators do not create extra commands.
153
+
154
+ A Bash allow decision requires coverage for each executable command. A multi-command pattern must match the same AST structure and operators. This structure check also applies to one command inside a group, wrapper, control structure, or background statement. Separate single-command patterns only cover top-level foreground chains and plain pipelines. Other supported structure requires one matching structural pattern. A multi-command pattern must match the same number of commands in the same order. Each pattern command must match its corresponding input command. One wildcard cannot hide an additional command or a different operator.
155
+
156
+ A redirect requires explicit coverage in the allow pattern. The redirect operator, file descriptor, variable name, and target pattern must match. Here-documents and dynamic redirect targets continue to the classifier. Parser errors, dynamic command names, and dynamic wrapper scripts cannot use `permissions.allow`.
157
+
158
+ Control nodes with unrepresented semantic values cannot use `permissions.allow`. This includes loops, functions, coprocesses, case statements, test commands, and arithmetic commands. These scripts continue to the classifier.
159
+
160
+ Pi-automode does not execute shell expansions. It cannot resolve aliases, variables, generated scripts, or dynamic `eval` input. These calls continue to the classifier unless a deterministic rule blocks them.
161
+
162
+ For other tools, the matcher uses the serialized input object. Use a bare tool name for an MCP or extension tool. For example, `example-extension-tool` matches every call to that tool.
163
+
164
+ The providing extension or MCP server defines the Pi tool name. Pi-automode does not need a predefined list.
165
+
166
+ A match skips only the classifier call. It cannot skip `permissions.deny`, deterministic hard-deny checks, `deniedPaths`, or protected-path controls. An accepted `permissions.ask` rule also takes precedence. After confirmation, the call continues through deterministic checks and then reaches the classifier. It cannot use `permissions.allow`, the inside-working-directory tier, or the read-only fast path.
167
+
168
+ Pi-automode reads `permissions.allow` only from global configuration, trusted `.pi/automode.local.json`, and `PI_AUTOMODE_SETTINGS_JSON`. Shared `.pi/automode.json` cannot add allow rules.
169
+
170
+ A pattern can contain at most 4,096 UTF-16 code units. Bash analysis accepts at most 1,048,576 UTF-16 code units. A longer Bash input is blocked before parsing.
171
+
172
+ For other allow matching, an input can contain at most 1,048,576 UTF-16 code units. A longer input returns no match. Deny and ask patterns match the same oversized input so that they fail closed.
173
+
174
+ `write` and `edit` calls whose resolved target is a protected path are never covered by `permissions.allow`. This includes protected targets reached through symlink aliases.
175
+
176
+ ## Custom models (like OpenRouter's presets)
177
+
178
+ Pi-automode can only select models that Pi exposes through its model registry. Add unlisted models to `~/.pi/agent/models.json`.
179
+
180
+ For example, register an OpenRouter preset in the built-in `openrouter` provider:
181
+
182
+ ```json
183
+ {
184
+ "providers": {
185
+ "openrouter": {
186
+ "models": [
187
+ {
188
+ "id": "@preset/nvidia-nemotron-3-nano-30b-a3b-fast",
189
+ "name": "NVIDIA: Nemotron 3 Nano 30B A3B Fast (Preset)",
190
+ "reasoning": true,
191
+ "input": ["text"],
192
+ "contextWindow": 262144,
193
+ "maxTokens": 235929,
194
+ "cost": {
195
+ "input": 0.05,
196
+ "output": 0.2,
197
+ "cacheRead": 0.03,
198
+ "cacheWrite": 0
199
+ }
200
+ }
201
+ ]
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ Restart Pi (or run `/reload`), then select the model with `/automode model openrouter/@preset/nvidia-nemotron-3-nano-30b-a3b-fast`.
208
+
209
+ ## Jev classifier (OpenRouter)
210
+
211
+ Set `classifierModel` to `openrouter/typesafe/jev-1.13` (pinned) or `openrouter/~typesafe/jev-latest` (alias) to classify with TypeSafe's Jev model through OpenRouter's Decisions API instead of an LLM. The API key comes from `OPENROUTER_API_KEY` in Pi's environment, or — when unset — from any `openrouter` provider key registered in Pi's model registry (for example one configured through OMP); if neither is present, classification fails closed. `/automode model openrouter/typesafe/jev-1.13` saves the spec without a model-registry lookup.
212
+
213
+ With a TypeSafe API key, `typesafe/jev-latest` (or a pinned release such as `typesafe/jev-1.13.0`) calls TypeSafe's System One API directly instead of OpenRouter. The key comes from `TYPESAFE_API_KEY` in Pi's environment, or — when unset — from any `typesafe` provider key registered in Pi's model registry. `/automode model typesafe/jev-latest` saves the spec the same way. GUI-launched sessions do not inherit shell profile variables; set the variable where the host application sees it.
214
+
215
+ Jev is a decision model, not a chat model: it is not reachable through Pi's model registry or `models.json`. Other `openrouter/...` classifier models keep using the registry path.
216
+
217
+ Jev replaces both LLM stages with one call. The configured `hard_deny` and `soft_deny` rules become the options of a Choice question. Two Noul questions check for an ALLOW exception and direct user authorization. Pi-automode then decides locally, with one risk tolerance of 0.2:
218
+
219
+ - Allow when the probability of "no deny rule" is at least 0.8.
220
+ - Block with `hard_deny` when a hard-deny rule is the top rule match, or when hard-deny rules together hold at least 0.2 probability. Exceptions never override hard-deny.
221
+ - Otherwise, the top soft-deny rule blocks unless the ALLOW-exception or user-authorization probability is at least 0.8.
222
+ - When rule probabilities tie at zero, the more severe tier wins.
223
+
224
+ The denial reason names the matched rule. `classifierReasoningLevel` and `fastClassifierMaxTokens` do not apply; `classifierTimeoutMs` does. Missing keys, request errors, timeouts, and malformed answers fail closed. The Choice question holds at most 254 deny rules.
225
+
226
+ The Decisions API is an OpenRouter alpha endpoint. Responses are schema-validated; anything unexpected fails closed.
@@ -0,0 +1,178 @@
1
+ # Defaults and rule-list behavior
2
+
3
+ ## `$defaults`
4
+
5
+ `$defaults` expands to the built-in entries for its section. It is section-local. In `allow`, it means the built-in allow rules, not the built-in hard-deny rules.
6
+
7
+ ### `environment`
8
+
9
+ `$defaults` expands to:
10
+
11
+ - trusted repo: the repository Pi started in and its configured git remotes
12
+ - source control: the trusted repo and its configured remotes only
13
+ - trusted internal domains: none configured
14
+ - trusted cloud buckets: none configured
15
+ - key internal services: none configured
16
+
17
+ If the classifier needs information about trusted company infrastructure, add entries such as these:
18
+
19
+ ```json
20
+ {
21
+ "autoMode": {
22
+ "environment": [
23
+ "$defaults",
24
+ "Trusted internal domains: *.corp.example.com, git.example.com",
25
+ "Trusted cloud buckets: s3://acme-dev-artifacts, gs://acme-ci-cache",
26
+ "Key internal services: staging deploy API at deploy.corp.example.com"
27
+ ]
28
+ }
29
+ }
30
+ ```
31
+
32
+ These entries give the classifier context. They do not bypass `hard_deny` or automatically allow every action involving those services.
33
+
34
+ ### `allow`
35
+
36
+ `$defaults` expands to allow exceptions for:
37
+
38
+ - read-only operations: inspecting files, listing directories, searching, GET requests, and state queries that do not expose secrets
39
+ - local development inside the working tree: creating, editing, building, testing, linting, formatting, and deleting files inside the current repository or worktree, including files that existed before the session started
40
+ - modifying or deleting pre-existing local files outside the repository or worktree with bounded direct user authorization
41
+ - installing dependencies already declared in package manifests or lockfiles
42
+ - using standard credentials only with their intended configured providers
43
+ - pushing to the current non-default working branch or a new branch created for the task
44
+ - bootstrapping language/toolchain installers from official sources
45
+
46
+ These are exceptions to `soft_deny`, not to `hard_deny`.
47
+
48
+ ### `protectedPaths`
49
+
50
+ `$defaults` expands to safety-sensitive paths. In the default configuration, every `write` and `edit` call goes to the classifier. Thus, `protectedPaths` does not change classifier routing by default. Pi-automode retains the list for compatibility and inspection.
51
+
52
+ The value `allowInsideWorkingDirectory: true` sends non-protected in-tree file access to the deterministic allow tier. Writes and edits to protected paths still reach the classifier. Classifier `allow` rules cannot override a classifier hard-deny decision.
53
+
54
+ Protected directories: `.git`, `.config/git`, `.vscode`, `.idea`, `.husky`, `.cargo`, `.devcontainer`, `.yarn`, `.mvn`, `.pi`.
55
+
56
+ Protected files include:
57
+
58
+ - Git files: `.gitconfig`, `.gitmodules`, `.gitignore`, and `.gitattributes`
59
+ - Bash files: `.bashrc`, `.bash_profile`, `.bash_login`, `.bash_aliases`, and `.bash_logout`
60
+ - Zsh files: `.zshrc`, `.zprofile`, `.zshenv`, `.zlogin`, and `.zlogout`
61
+ - other shell files: `.profile` and `.envrc`
62
+ - package-manager files: `.npmrc`, `.yarnrc`, `.yarnrc.yml`, `.pnp.cjs`, `.pnp.loader.mjs`, and `.pnpmfile.cjs`
63
+ - Bun files: `bunfig.toml` and `.bunfig.toml`
64
+ - Bazel files: `.bazelrc`, `.bazelversion`, and `.bazeliskrc`
65
+ - hook files: `.pre-commit-config.yaml`, `lefthook.yml`, `lefthook.yaml`, `.lefthook.yml`, and `.lefthook.yaml`
66
+ - wrapper files: `gradle-wrapper.properties` and `maven-wrapper.properties`
67
+ - other control files: `.devcontainer.json`, `.ripgreprc`, `pyrightconfig.json`, and `.mcp.json`
68
+
69
+ Read-only tools remain locally allowed after permission and deterministic checks. These tools are `read`, `grep`, `find`, and `ls`.
70
+
71
+ In the default configuration, all writes and edits require classification. The value `allowInsideWorkingDirectory: true` allows non-protected in-tree access locally.
72
+
73
+ Protected in-tree writes and edits still require classification. All out-of-tree file access also requires classification.
74
+
75
+ ### `deniedPaths`
76
+
77
+ `deniedPaths` is an optional list of path glob patterns. The default list is empty, and it has no built-in entries. Thus, `$defaults` has no effect in this list.
78
+
79
+ A matching pattern blocks a file-tool call before classifier review or an allow tier. The file tools are `read`, `write`, `edit`, `grep`, `find`, and `ls`.
80
+
81
+ Use this list for secrets and system paths that file tools must not send to the model. The classifier continues to govern `bash` access.
82
+
83
+ Patterns support `~`, `$HOME`, and `${HOME}` expansion. The `*` wildcard matches all characters, including `/`. Pi-automode matches the typed path and its symlink-resolved form. `deniedPaths` can only restrict access.
84
+
85
+ ### `soft_deny`
86
+
87
+ `$defaults` expands to soft blocks for:
88
+
89
+ - force push, remote branch deletion, remote history rewrite, or direct push to `main`, `master`, or the default branch
90
+ - downloading and executing external code, including `curl | bash` and `wget | sh`
91
+ - production deploys, production database migrations, or production infrastructure changes
92
+ - deleting, truncating, or overwriting pre-existing local files outside the current repository or worktree without a bounded user authorization
93
+ - mass modification/deletion in cloud storage, ticketing systems, collaboration tools, or external APIs
94
+ - writing through remote shells such as `ssh`, `kubectl exec`, or `docker exec` on shared or production hosts
95
+ - changing shared or live infrastructure without its normal preview, plan, dry run, or review
96
+ - killing other users' processes, cancelling shared jobs, or disrupting shared infrastructure
97
+ - exposing local files, sockets, credentials, or internal services through a network service
98
+ - outbound messages, purchases, bookings, subscriptions, or other real-world transactions
99
+
100
+ Soft blocks can be overridden by a matching `allow` exception or direct user intent. For a pre-existing local file outside the current repository or worktree, direct user intent must name the task, one repository or location, permitted path scopes, and an allowed operation. The action must stay inside those bounds. Files inside the current repository or worktree are covered by the local-development allow exception. Other soft blocks need direct user intent but do not require these file bounds. A later user instruction that narrows or revokes authorization controls.
101
+
102
+ ### `hard_deny`
103
+
104
+ `$defaults` expands to unconditional blocks for:
105
+
106
+ - weakening TLS, certificate validation, authentication, authorization, audit logging, or other security controls
107
+ - adding persistence such as SSH keys, shell profile edits, cron jobs, launch agents, login items, or background agents
108
+ - granting admin, owner, IAM, RBAC, repository, or production access
109
+ - exfiltrating repository contents, secrets, credentials, private keys, tokens, or internal data to untrusted places
110
+ - scanning for credentials, tokens, private keys, or secrets beyond what the task requires
111
+ - tampering with logs, audit trails, security monitoring, permission rules, auto-mode configuration, or safety-control files
112
+ - creating services, endpoints, workflows, or autonomous agents that execute arbitrary code without meaningful approval
113
+ - posting or updating public/external content that is fabricated, misleading, impersonating a user, or claiming approval/action that did not happen
114
+
115
+ Hard-deny rules cannot be overridden by `allow` or by user intent.
116
+
117
+ ### Classifier transcript budgets
118
+
119
+ Classifier evidence has separate approximate-token budgets for user messages and assistant tool-call payloads:
120
+
121
+ - `maxUserTranscriptTokens`: 4000
122
+ - `maxToolTranscriptTokens`: 4000
123
+
124
+ The selector keeps the first and latest user messages as intent anchors. It fills the remaining space from the newest eligible entries.
125
+
126
+ The selector limits individual entries. It marks omitted or truncated evidence in the classifier transcript. It excludes assistant prose and tool results.
127
+
128
+ These limits use approximate character counts. They do not guarantee the same result as a provider tokenizer.
129
+
130
+ To change a limit, set an integer of at least 32 in a Pi-owned `autoMode` configuration source. Pi-automode does not support the former `maxTranscriptLines` field.
131
+
132
+ ### Classifier request timeout
133
+
134
+ `classifierTimeoutMs` limits each classifier request in milliseconds. The fast and detailed stages have separate budgets. The default is 20000.
135
+
136
+ If a request exceeds its budget, pi-automode aborts the attempt. Then auto mode fails closed and blocks the action.
137
+
138
+ To change the timeout, set an integer of at least 1000 in a Pi-owned `autoMode` configuration source.
139
+
140
+ ### Replacement behavior
141
+
142
+ If you want to keep the built-ins and add entries, use `$defaults`:
143
+
144
+ ```json
145
+ {
146
+ "autoMode": {
147
+ "allow": [
148
+ "$defaults",
149
+ "Running the staging deploy script is allowed."
150
+ ]
151
+ }
152
+ }
153
+ ```
154
+
155
+ This configuration uses all built-in `allow` entries and the staging rule.
156
+
157
+ If you omit `$defaults`, you replace the built-ins for that section:
158
+
159
+ ```json
160
+ {
161
+ "autoMode": {
162
+ "allow": [
163
+ "Running the staging deploy script is allowed."
164
+ ]
165
+ }
166
+ }
167
+ ```
168
+
169
+ This configuration uses only the staging rule. It does not use the built-in `allow` entries.
170
+
171
+ Replacing `allow` does not replace `soft_deny`, `hard_deny`, `protectedPaths`, or `environment`.
172
+
173
+ `permissions.deny`, `permissions.ask`, and `permissions.allow` do not support `$defaults`. These lists contain only explicit Pi tool patterns. All three lists default to `[]`.
174
+
175
+ Global, project-local, and inline configuration can add all three permission lists. Shared project configuration can add only deny and ask rules.
176
+
177
+ `autoMode.allow` and `permissions.allow` are different. `autoMode.allow` contains prose exceptions that the classifier weighs against soft-deny rules. `permissions.allow` contains tool patterns that skip classifier review.
178
+
@@ -0,0 +1,90 @@
1
+ # Agent diagnostics
2
+
3
+ `automode_inspect` is a model-callable, read-only tool. An agent can use it to inspect active pi-automode state without copied slash-command output.
4
+
5
+ Use this tool to investigate an unexpected allow or block. Do not use it to change a safety control or to bypass a denial.
6
+
7
+ ## Inspection views
8
+
9
+ The tool accepts one `action` value.
10
+
11
+ | Action | Purpose | Result |
12
+ | --- | --- | --- |
13
+ | `status` | Inspect active state and counters. | A readable status summary and the enabled override, last decision, and action counters. |
14
+ | `config` | Inspect the effective configuration. | The active configuration, the resolved observability log path, and configuration diagnostics. |
15
+ | `defaults` | Inspect built-in rule lists. | The built-in environment, allow, protected-path, soft-deny, and hard-deny lists. |
16
+ | `denials` | Find recent rejected actions. | Reverse-chronological timestamps, enforcement kinds, and tool names. |
17
+
18
+ The tool reads the same in-memory configuration and state that pi-automode enforces. Configuration changes take effect after session start or `/automode reload`.
19
+
20
+ ## Enforcement behavior
21
+
22
+ An inspection call is not an unrestricted escape hatch.
23
+
24
+ 1. `permissions.deny` runs first.
25
+ 2. `permissions.ask` runs next.
26
+ 3. Deterministic hard-deny checks run next.
27
+ 4. The extension bypasses classifier routing only for its own registered `automode_inspect` tool.
28
+
29
+ If a local check blocks the call, pi-automode records it as a blocked action. If the call passes, inspection does not change counters, state, or logs.
30
+
31
+ The extension verifies the registered tool source before it applies the bypass. A tool from another extension with the same name does not receive this exemption.
32
+
33
+ The tool cannot enable or disable auto mode, reload configuration, reset state, select a model, or edit configuration. The user must run the related `/automode` command directly.
34
+
35
+ ## Model-visible data
36
+
37
+ Pi sends tool output to the current model. The output has deliberate privacy limits:
38
+
39
+ - `status` does not include the last decision reason.
40
+ - `denials` does not include denial reasons or action payloads.
41
+ - `config` removes the parser detail from invalid JSON diagnostics.
42
+
43
+ The `config` view returns effective rule text. Do not put credentials, tokens, private keys, signed URLs, or other secrets in pi-automode rules or configuration.
44
+
45
+ The tool serializes output defensively. Arrays longer than 30 entries become an object with these fields:
46
+
47
+ ```json
48
+ {
49
+ "$truncatedArray": true,
50
+ "items": ["first entries"],
51
+ "omittedEntries": 18,
52
+ "totalEntries": 48
53
+ }
54
+ ```
55
+
56
+ String values and the complete serialized result also have size limits. Treat a truncated result as incomplete. An omitted rule can still exist.
57
+
58
+ ## Observability log path
59
+
60
+ Use the `config` view to get the log path for the current session. Its `logFile` value uses the same resolution as observability logging:
61
+
62
+ - A persisted session uses a sidecar beside its Pi session file.
63
+ - An in-memory session uses the application-owned log directory for the effective session working directory.
64
+
65
+ For in-memory sessions, pi-automode uses this default location:
66
+
67
+ ```text
68
+ ~/.pi/agent/extensions/pi-automode/logs/<encoded-session-cwd>/YYYY-MM-DD/<session-id>-pi-automode.jsonl
69
+ ```
70
+
71
+ This includes `--no-session` runs and non-persisted subagents. The path does not use the launching process working directory.
72
+
73
+ See [Observability logging](observability-logging.md) for log configuration and entry schemas.
74
+
75
+ ## Diagnosis workflow
76
+
77
+ Classifier rules cannot override permission or deterministic denials.
78
+
79
+ 1. Call `automode_inspect` with `status`, `config`, and `denials`.
80
+ 2. If observability logging is enabled, use the reported log path.
81
+ 3. Read a matching decision entry before you propose a rule change.
82
+ 4. Identify the enforcement layer before you change a rule.
83
+ 5. If a configuration change is necessary, explain the change.
84
+ 6. Ask the user to run `/automode off`.
85
+ 7. Make only the requested configuration change during that maintenance window.
86
+ 8. Ask the user to run `/automode reload` and `/automode on`.
87
+ 9. Use `automode_inspect` to make sure that auto mode is enabled.
88
+ 10. Retry only an action that you know is safe.
89
+
90
+ Do not replay an unsafe action because auto mode is off. Do not print sensitive tool input or log content in a diagnosis report.
@@ -0,0 +1,160 @@
1
+ # Observability logging
2
+
3
+ Auto mode can write a JSONL observability log for decisions and classifier usage. Persisted sessions use a sidecar next to the Pi session file. In-memory sessions use a global application directory.
4
+
5
+ Logging is off by default. Logging fails open, so a write error never changes an allow or block decision.
6
+
7
+ ## Enabling
8
+
9
+ Set `autoMode.log` in a Pi-owned configuration source. These sources include `~/.pi/agent/extensions/pi-automode/config.json`, trusted `.pi/automode.local.json`, and `PI_AUTOMODE_SETTINGS_JSON`.
10
+
11
+ ```json
12
+ {
13
+ "autoMode": {
14
+ "log": {
15
+ "enabled": true,
16
+ "classifierIo": false
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ - `enabled` — write one `decision` line per tool-call decision and one ccusage-compatible `message` line per classifier model response.
23
+ - `classifierIo` — also write the classifier prompt, raw responses, and parsed decision for classifier-routed actions. The default is `false`. See [Privacy](#privacy).
24
+
25
+ Fields merge independently across configuration sources. For example, set `enabled` globally and set `classifierIo` for a trusted project.
26
+
27
+ Shared project `.pi/automode.json` cannot set `log`. The shared file cannot set any `autoMode` field. `/automode config` reports an invalid shape.
28
+
29
+ Pi-automode writes log entries only while auto mode is **enabled**. With auto mode off, no tool calls reach the hook. Thus, pi-automode writes no entries.
30
+
31
+ ## Log file location
32
+
33
+ Pi-automode stores the log next to the current Pi session file. It inserts `-pi-automode` before the extension:
34
+
35
+ ```text
36
+ <session-file> → <dir>/<id>.jsonl
37
+ <session-file>-pi-automode.jsonl → <dir>/<id>-pi-automode.jsonl
38
+ ```
39
+
40
+ For example, pi-automode can use `~/.pi/agent/sessions/<slug>/<id>-pi-automode.jsonl`.
41
+
42
+ If a custom session manager provides an absolute session directory without a session file, pi-automode uses `<sessionDir>/<sessionId>-pi-automode.jsonl`.
43
+
44
+ Pi in-memory sessions have no session file or session directory. These sessions include `--no-session` and non-persisted subagents. Their logs use this absolute application path:
45
+
46
+ ```text
47
+ ~/.pi/agent/extensions/pi-automode/logs/<encoded-session-cwd>/YYYY-MM-DD/<session-id>-pi-automode.jsonl
48
+ ```
49
+
50
+ The project directory uses the same `--path-with-dashes--` encoding as normal Pi session directories. The date partition uses UTC.
51
+
52
+ A custom session manager can supply an absolute `sessionDir` without a session file. In this case, pi-automode continues to use that directory. Run `/automode config` to see the resolved path.
53
+
54
+ Persisted sessions use one combined file per session. In-memory sessions use one file for each session ID and UTC day.
55
+
56
+ If a session crosses midnight, pi-automode continues in the file for the next day. Each line contains one JSON object with a `type` discriminator. Entries for the same tool call share a `decisionId`.
57
+
58
+ For persisted sessions, `ccusage pi` reports the sidecar as a separate `-pi-automode` session. Inspect in-memory logs directly because they are outside the normal Pi session tree.
59
+
60
+ ## Entry types
61
+
62
+ ### `decision`
63
+
64
+ Pi-automode writes one `decision` entry for each tool-call decision. Each allowed or blocked call has exactly one entry.
65
+
66
+ | field | meaning |
67
+ | --- | --- |
68
+ | `ts` | ISO timestamp |
69
+ | `decisionId` | links to the related `classifier` entry. Local decisions have no related entry. |
70
+ | `sessionId` | Pi session id |
71
+ | `cwd` | working directory |
72
+ | `tool` | tool name, for example `bash` or `write` |
73
+ | `summary` | `actionSummary` — tool name + input JSON (truncated) |
74
+ | `kind` | enforcement path: `permissions.deny`, `permissions.ask`, `deterministic-hard-deny`, `deterministic-path-deny`, `permissions.allow`, `inside-working-directory`, `classifier`, `user-confirmed`, `read-only`, or `setup` |
75
+ | `outcome` | `allow` or `block` |
76
+ | `reason` | the reason string (classifier reason, or the deterministic/permission reason) |
77
+ | `classifierModel` | the configured classifier model for a classifier-routed decision |
78
+ | `reasoning` | classifier reasoning mode and requested or effective level. See the examples below. |
79
+
80
+ The reasoning field records either server-default mode:
81
+
82
+ ```json
83
+ {"mode":"server-default"}
84
+ ```
85
+
86
+ or an explicit request after model-level clamping:
87
+
88
+ ```json
89
+ {"mode":"explicit","requestedLevel":"max","effectiveLevel":"xhigh"}
90
+ ```
91
+
92
+ Classifier-routed decisions contain the effective level after model resolution. If `classifierIo` is off or authentication fails later, this field still exists.
93
+
94
+ If pi-automode cannot resolve the model, the entry contains `requestedLevel` without `effectiveLevel`. In this case, no model-supported level exists.
95
+
96
+ Local permission, deterministic, `permissions.allow`, and read-only decisions do not run the classifier. These decisions can omit `effectiveLevel`. Pi-automode cannot observe or infer the server-selected level in `server-default` mode.
97
+
98
+ ### `message` (classifier usage)
99
+
100
+ Pi-automode writes one `message` entry for each classifier response. This includes a malformed response that causes a retry.
101
+
102
+ When logging is enabled, pi-automode writes this entry before the matching `decision` line. The entry shape is compatible with `ccusage pi`:
103
+
104
+ | field | meaning |
105
+ | --- | --- |
106
+ | `timestamp` | ISO timestamp from the classifier response |
107
+ | `message.role` | always `assistant` |
108
+ | `message.model` | model ID returned by the classifier provider |
109
+ | `message.usage` | provider-reported input, output, cache, total-token, and cost fields |
110
+
111
+ For persisted sessions, `ccusage` reports this sidecar as a separate `-pi-automode` session. This entry contains no prompt or response text. When `classifierIo` is off, pi-automode still writes it.
112
+
113
+ ### `classifier`
114
+
115
+ If `classifierIo: true`, pi-automode writes a `classifier` entry. It writes this entry only for classifier-routed actions.
116
+
117
+ The entry follows all related classifier-usage `message` entries. It precedes the matching `decision` entry.
118
+
119
+ | field | meaning |
120
+ | --- | --- |
121
+ | `ts` | ISO timestamp |
122
+ | `decisionId` | matches the `decision` entry for the same call |
123
+ | `model` | classifier model, for example `anthropic/claude-haiku-4` |
124
+ | `reasoning` | `server-default`, or the explicit requested and effective model-supported level |
125
+ | `prompt.system` | the full system policy with `environment`/`allow`/`soft_deny`/`hard_deny` rules interpolated |
126
+ | `prompt.context` | the shared context message: loaded project instructions + classifier transcript |
127
+ | `prompt.action` | the complete, untruncated current tool action JSON |
128
+ | `prompt.fastInstruction` | the exact one-token filter instruction |
129
+ | `prompt.detailedInstruction` | the exact structured-review instruction |
130
+ | `attempts` | one entry per classifier model call (see below) |
131
+ | `durationMs` | total classifier time |
132
+ | `parsed` | the final decision that was acted on (`{ decision, tier, reason }`) |
133
+
134
+ Each `attempts[]` entry is `{ stage, attempt, response?, parsed?, error?, durationMs }`:
135
+
136
+ - `stage` — `fast` for the one-token filter or `detailed` for structured review.
137
+ - `response` — `{ stopReason, text, model, timestamp, usage, errorMessage? }`, the raw model output and provider-reported usage for that call, including provider-reported errors and aborted requests.
138
+ - `parsed` — the decision parsed from the response, or absent after a parse failure.
139
+ - `error` — present after a network or authentication error. In this case, `response` is absent.
140
+
141
+ The array records both stages, retries, and fail-closed cases. A fast allow has one entry. A review with one successful retry has three entries.
142
+
143
+ ## Privacy
144
+
145
+ `prompt.context` contains project instructions and transcript evidence for both classifier stages. `prompt.action` contains the separate, untruncated current action. Pi-automode logs the final stage instructions separately.
146
+
147
+ See [Auto-mode classifier flow → What is sent to the classifier](automode-classifier-flow.md#what-is-sent-to-the-classifier) for the evidence assembly rules.
148
+
149
+ The log records this payload locally. Pi-automode also sends the same data to the classifier endpoint for each classifier-routed call.
150
+
151
+ If `classifierModel` uses a cloud provider, the payload leaves the machine. Enable `classifierIo` only for classifier diagnosis or rule tuning. Leave it off for routine outcome logging.
152
+
153
+ ## Sizing
154
+
155
+ - `decision` line: ~0.4–2 KB (driven by `summary`, which carries the tool input, capped at 6 KB).
156
+ - `classifier` line: ~5 KB fixed policy plus loaded project instructions, selected transcript evidence, stage instructions, and recorded responses.
157
+
158
+ `autoMode.maxUserTranscriptTokens` and `autoMode.maxToolTranscriptTokens` limit transcript evidence separately. Both fields default to approximately 4000 tokens.
159
+
160
+ Pi-automode excludes assistant prose and tool results. Provider cache hits can reduce processed or billed input. The log still records the full classifier payload.