pi-warden 0.7.0 → 0.9.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.
package/README.md CHANGED
@@ -1,34 +1,30 @@
1
1
  # pi-warden
2
2
 
3
- A second pair of eyes for [Pi](https://pi.dev) that makes the agent smarter instead of interrupting you. Before the agent runs a `bash`, `write`, or `edit` call (or a shell command through [context-mode](https://www.npmjs.com/package/context-mode)'s `ctx_execute`), pi-warden asks [Jev](https://typesafe.ai) two questions about it in ~250 ms: *would this destroy something that cannot be recovered?* and *is this what the user actually asked for?* The answers are probabilities, so `rm -rf dist` after "rebuild from scratch" sails through while `npm run db:reset` after "add a column" is held, and the agent is told why so it re-plans or asks you in chat. More guards watch the run: a **stuck-loop** detector, a **done-check** for completion claims that no test backed up, **slop** notes for stubs, filler, and padded replies, a check for **injected instructions** in tool output, and a **context saver** that replaces repeated tool results with a note, keeps exact failing tests and errors from large runner output, and points recalls at a search instead of a whole-file read. Every verdict lands on a status line above the editor; **click it** (fullscreen mode), press `ctrl+shift+w`, or run `/warden trace` to open a live trace sidebar with the scores, the reasons, and exactly what the agent was told. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
3
+ A second pair of eyes for [Pi](https://pi.dev). It watches what the agent does, asks [Jev](https://typesafe.ai) small typed questions about it (about 250 ms each), and acts through the agent's own context: a held call, a short steer message, a compressed output. You keep working; the agent gets smarter.
4
4
 
5
5
  ![Real verdicts from pi-warden: the same command gets a different verdict depending on what the user asked for](https://raw.githubusercontent.com/DevMortimer/pi-warden/main/docs/preview.png)
6
6
 
7
- The verdicts above are real output from `npm run test:live`. Independent project; not affiliated with TypeSafe AI or the Pi authors.
7
+ The verdicts above are real output from `npm run test:live`. Independent project; not affiliated with TypeSafe AI or the Pi authors. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
8
8
 
9
- ## Philosophy
10
-
11
- pi-warden is a harness for the agent, not a gate for the user. It watches every tool call, tool result, and reply, asks Jev small typed questions about them, and acts through the agent's own context — a held call, a steer message, a compressed output — rather than through dialogs. Jev decides; code applies the decision; the LLM is never asked to judge itself. Guard decisions are visible in the trace panel. Existing session entries are never rewritten; output warnings and excerpts are applied before a new tool result enters the session.
12
-
13
- Three jobs, in order of maturity:
9
+ ## What it does
14
10
 
15
- | Pillar | What it means | Status |
11
+ | Guard | Watches | Does |
16
12
  | --- | --- | --- |
17
- | **Security** | Action holds and offline patterns protect risky operations. Tool-output checks flag possible injected instructions and credential shapes; insecure written code gets a targeted steer. Output notices are advisory, not a sandbox or a guarantee that an attack is contained. | built |
18
- | **Deslopify** | Written code is scored per symptom — stubs, restating comments, dead code, hedging — and the agent gets a nudge naming the symptom and its fix; repeats become a standing rule. Replies are scored for wordiness, assistant clichés, and jargon against an `audience` setting, and a trend across replies shapes the next one. | built |
19
- | **Context saving** | Only the newest tool result is ever touched, so the prompt cache prefix stays byte-identical. An identical repeat of an earlier result becomes a short note (code only). For large new results Jev selects a retention policy and names the output format; code keeps exact lines through a format parser (failing tests, errors with file:line, changed files, package notices) or a deterministic head/diagnostic/tail excerpt, stores an exact copy, and tells the agent how to search it instead of re-reading it. Task-aware segment selection and compaction support remain planned. | built |
13
+ | **Action** | every `bash`, `write`, `edit` (and context-mode's `ctx_execute*`) before it runs | holds irreversible or off-task calls and tells the agent why, so it re-plans or asks you |
14
+ | **Rules** | every `write` and `edit` | judges the written code against your project's Markdown rules (`pi-warden.md`) and names the violated rule to the agent |
15
+ | **Slop** | written code and final replies | names stubs, restating comments, dead code, hedging, padded replies; the agent fixes them in the next edit |
16
+ | **Security** | written code and tool output | flags hardcoded secrets, disabled TLS, unsafe interpolation; marks injected instructions in tool output |
17
+ | **Stuck** | tool results | notices three failures with the same strategy and asks for a new hypothesis |
18
+ | **Runaway** | the reply stream | stops a reply that repeats the same block over and over (code only, no request) |
19
+ | **Done-check** | the final message | catches "done" claims after code changes when no test, build, or lint passed |
20
+ | **Context saver** | large or repeated tool output | keeps the exact lines that matter, stores the rest in a file, points recalls at a search |
21
+ | **Notifications** | moments that need you | desktop notification for a held call, a confirm dialog, a runaway stop |
20
22
 
21
- ## Why a coding agent needs this
23
+ Every verdict lands on a status line above the editor. Click it (fullscreen mode), press `ctrl+shift+w`, or run `/warden trace` for a live sidebar with the scores, the reasons, and exactly what the agent was told.
22
24
 
23
- Agents are good at picking the next command and bad at noticing when that command is out of proportion to the request. Pattern lists catch `rm -rf /` and force pushes; they cannot tell `db:reset` when you asked for a reset from `db:reset` when you asked for a column. A generative model can, but a second LLM call per tool call is slow and expensive. Jev is a System One model: it returns calibrated probabilities instead of text, in a quarter of a second, for a fraction of a cent. That makes it cheap enough to sit in front of **every** guarded call:
25
+ ## Why Jev
24
26
 
25
- - **Irreversible actions are held, not run**: history rewrites, deleted untracked work, dropped tables, overwritten files outside the project, publishes and deploys. The agent receives the judgment as its tool result — what was flagged, the scores, and two acceptable next moves: find a recoverable alternative, or explain the action to you and wait. If you approve in chat, the retry goes through; Jev reads your reply as the approval. No modal dialog unless you ask for one (`/warden mode confirm`).
26
- - **Scope drift gets flagged.** Poems in a bugfix, refactors nobody asked for, dependency installs unrelated to the task: warned to you at 0.6, held at 0.85 when Jev also calls the action `unrelated`.
27
- - **Loops get broken.** Three failures with the same strategy (same command with cosmetic changes, same error after each edit) earn the agent a steer: re-read the error, form a new hypothesis, or report the blocker. Exact repeats are caught offline; Jev tells "investigating between failures" from "flailing" — 0.32 vs 0.93 in the smoke run.
28
- - **"Done" gets checked.** When the final message reports completion after file changes and no test, build, or lint passed in that run, the agent is asked to verify before you read a false "all green". A claim that tests passed when none ran is called out as such. Once per prompt, so it cannot loop.
29
- - **Slop gets a name.** Written code is scored per symptom on the same request as the action guard (zero extra latency): stubs and fake-data mocks, comments that restate the code, dead or duplicated code, hedging notes. The agent is told which symptom and how to fix it, never held; the third repeat in a session becomes a standing rule. Replies are scored too — wordy, clichéd, or too technical for the configured audience — and a trend across replies earns a nudge that shapes the next one without spending a turn.
30
- - **Nothing slows down the boring calls.** Read-only shell lines are skipped without a request; anything else costs one request of ~600 input tokens.
31
- - **It degrades gracefully.** Offline pattern checks and exact-repeat detection run with no account at all. On an API timeout or outage the call is allowed with a warning (configurable), and reasons never include your command text or upstream error bodies.
27
+ Agents pick the next command well and notice badly when that command is out of proportion to the request. Pattern lists catch `rm -rf /` and force pushes; they cannot tell `db:reset` after "reset the database" from `db:reset` after "add a column". A generative model can, but a second LLM call per tool call is slow and expensive. Jev is a System One model: it returns calibrated probabilities instead of text, in a quarter of a second, for a fraction of a cent. That is cheap enough to sit in front of every guarded call. Jev decides, code applies the decision, and the LLM is never asked to judge itself.
32
28
 
33
29
  ## Install
34
30
 
@@ -36,116 +32,153 @@ Agents are good at picking the next command and bad at noticing when that comman
36
32
  pi install npm:pi-warden
37
33
  ```
38
34
 
39
- Requires Pi 0.85 or newer and Node.js 22.19 or newer. Pattern checks work with no account. Jev judgments need a TypeSafe API key and are billed to your TypeSafe account (a judgment is roughly 600 input tokens; output is free).
35
+ Requires Pi 0.85 or newer and Node.js 22.19 or newer. Pattern checks work with no account. Jev judgments need a TypeSafe API key and are billed to your TypeSafe account (a judgment is roughly 600 input tokens; a rules request with 8 rules is about 2000; output is free).
40
36
 
41
37
  ## Setup
42
38
 
43
39
  1. Get a key at [console.typesafe.ai](https://console.typesafe.ai) (API Keys).
44
- 2. Run `/warden enable`. Read the data notice and confirm; if no key is configured yet, paste it at the hidden prompt. The key is verified against the API and saved to `~/.pi/agent/pi-typesafe/auth.json` with owner-only permissions, shared with [pi-typesafe](https://github.com/DevMortimer/pi-typesafe) and anything else built on it. Consent is saved to `~/.pi/agent/pi-warden/config.json`, so judgments stay on in every new session until you run `/warden disable`.
45
- 3. Optionally run `/warden test` to see one synthetic verdict.
40
+ 2. Run `/warden enable`. Read the data notice and confirm. If no key is stored yet, paste it at the hidden prompt. The key is verified and saved to `~/.pi/agent/pi-typesafe/auth.json` (owner-only, shared with pi-typesafe). Consent is saved to `~/.pi/agent/pi-warden/config.json` and stays on until `/warden disable`.
41
+ 3. Optional: `/warden test` shows one synthetic verdict.
42
+
43
+ `TYPESAFE_API_KEY` in the environment takes precedence over the stored key. Headless runs give consent with `PI_WARDEN_ENABLED=1`.
44
+
45
+ ## Philosophy
46
46
 
47
- A `TYPESAFE_API_KEY` environment variable takes precedence over the stored key. Without step 2, pi-warden still guards with offline pattern checks only.
47
+ pi-warden is a harness for the agent, not a gate for you. Three jobs:
48
48
 
49
- ## How the guards work
49
+ | Pillar | What it means |
50
+ | --- | --- |
51
+ | **Security** | Holds and offline patterns protect risky operations. Output checks flag injected instructions and credential shapes. Insecure written code gets a targeted steer. Advisory, not a sandbox. |
52
+ | **Deslopify** | Written code is scored per symptom and judged against your project rules. Replies are scored for wordiness, clichés, and jargon. Repeats become a standing rule. |
53
+ | **Context saving** | Only the newest tool result is ever touched, so the prompt cache prefix stays byte-identical. Large output is cut to exact lines and stored; recalls go to a search, not a whole-file read. |
54
+
55
+ ## The guards
56
+
57
+ ### Action guard
50
58
 
51
- ### Action guard (`tool_call`)
59
+ Runs on `tool_call`, before the tool executes.
52
60
 
53
- 1. **Skip** read-only tools, and read-only shell lines such as `git status && ls` (no network, no widget). Guarded by default: `bash`, `powershell`, `write`, `edit`, and context-mode's `ctx_execute`, `ctx_execute_file`, `ctx_batch_execute` (their `code`/`commands` fields are read as the command; non-shell code such as JavaScript is judged but never skipped as read-only). Add other tools with `action.tools`; unknown tools are judged from their JSON input.
54
- 2. **Pattern pass** (offline): force pushes, `git reset --hard`, `git clean -f`, recursive `rm` on absolute/home/variable/parent paths, SQL `DROP`/`TRUNCATE`/`DELETE FROM`, block-device writes, `chmod -R 777`, fork bombs, `curl | sh`, `kill -1`, shutdown, package publishing, infrastructure destroys → **hold**. `rm -rf` on a project path, `git checkout -- .`, `git branch -D`, `git stash drop`, `find -delete`, `sudo` → **warn**. Reads or writes of `.env`, SSH, AWS, npm, kube, and other credential files → **warn**. A `write` that overwrites an existing file outside the project → **hold**; creating or editing outside the project → **warn**.
55
- 3. **Jev judgment** (with consent): one request with the state `{ task, context, action }` and four questions — `irreversible` (Noul), `off_task` (Noul), `mutates` (Noul: does it change anything), `scope` (Choice: expected step, plausible side step, unrelated, unclear). Defaults: irreversible ≥ 0.5 warns, ≥ 0.7 holds; off-task ≥ 0.6 warns, ≥ 0.85 with `unrelated` holds **only when the action can change something** (`write`/`edit`, or `mutates` ≥ 0.5). An unrelated `cat`/`grep`/`node -e` inspection or a read-only `ctx_execute` script is warned about, never held: scope alone is not worth blocking a command that cannot do damage. Pattern results set the floor; Jev can only raise it. For `write`/`edit`, the slop and `security_risk` questions ride on the same request.
61
+ 1. **Skip** read-only tools and read-only shell lines (`git status && ls`): no request, no widget line.
62
+ 2. **Patterns**, offline: force pushes, `git reset --hard`, `git clean`, recursive `rm` on absolute, home, variable, or parent paths, SQL `DROP`/`TRUNCATE`/`DELETE FROM`, block-device writes, `chmod -R 777`, fork bombs, `curl | sh`, `kill -1`, shutdown, package publishing, infrastructure destroys hold the call. `rm -rf` on a project path, `git checkout -- .`, `git branch -D`, `git stash drop`, `find -delete`, `sudo` warn. Reads or writes of `.env`, SSH, AWS, npm, kube, and other credential files warn. A `write` that overwrites a file outside the project holds; creating or editing outside the project warns.
63
+
64
+ Text that is data is not a command. A heredoc body written to a file, a quoted `echo`/`printf` argument, a `grep` pattern, or a `git commit -m` message can mention `git push --force` without a hold. The same text fed to `sh`, `bash -c`, `eval`, `xargs`, or a `python3 - <<EOF` script that calls `os.system` keeps every hit.
65
+ 3. **Jev**, with consent: one request with `{ task, context, action }` and four questions. `irreversible` (yes/no), `off_task` (yes/no), `mutates` (does it change anything), `scope` (expected step, plausible side step, unrelated, unclear). Defaults: irreversible at 0.5 warns and at 0.7 holds; off-task at 0.6 warns and at 0.85 with `unrelated` holds, but only when the action can change something. An unrelated `grep` is warned about, never held. Patterns set the floor; Jev can only raise it.
56
66
  4. **Act**, by mode:
57
- - `steer` (default): a hold blocks the call and returns the judgment to the agent as the tool result, with the two acceptable next moves. You see a notification and the widget line; the agent keeps working. When the agent asks you and your reply approves the action (Jev: `approved` ≥ 0.7, or an offline yes/go-ahead heuristic without consent), the identical retry is allowed once.
58
- - `confirm`: a hold opens a `ctx.ui.confirm` dialog; No blocks with a short reason. Without a UI this falls back to `steer`.
59
- - `advise`: never holds; every judgment is reported to you only.
67
+ - `steer` (default): a hold blocks the call and returns the judgment to the agent as its tool result, with the two acceptable next moves: find a recoverable alternative, or explain the action to you and wait. If your reply approves it, the retry goes through (Jev reads your reply; offline, a yes/go-ahead heuristic does).
68
+ - `confirm`: a `ctx.ui.confirm` dialog. No blocks with a short reason. Falls back to `steer` without a UI.
69
+ - `advise`: never holds, reports only.
60
70
 
61
- Warn-level verdicts notify you and continue in every mode. The widget above the editor shows the last verdict of each guard, for example `warden · bash · irreversible 0.84 · off-task 0.86 · unrelated · confirm`.
71
+ The action guard receives your latest message plus up to eight earlier user and assistant messages (750 redacted characters each) so follow-ups and side comments do not replace the task. Sibling tool calls in one assistant message are judged together in one round trip. If TypeSafe cannot answer, the call is allowed with a warning (`failOpen: true`; set it to `false` to hold instead).
62
72
 
63
- ### Stuck detector (`tool_result`)
73
+ ### Rules
64
74
 
65
- Keeps the last 12 tool results for the current prompt. When the latest result failed and at least 3 failures have accumulated, it first checks for exact repeats offline (same call, same output with timings and addresses normalised). Otherwise one Jev request judges the sequence: `same_strategy` (Noul), `approach_change` (Score: identical / cosmetic / meaningfully different), `progress` (Noul). Same-strategy ≥ 0.7 counts as stuck: you get a notification, and with `nudge: true` (default) the agent gets a steer message asking for a new hypothesis or a blocker report. At most one Jev check per 3 results.
75
+ Write your project's rules as Markdown headings in `pi-warden.md` at the project root:
66
76
 
67
- ### Done-check (`agent_end`)
77
+ ```markdown
78
+ # No console statements
79
+ Code must not contain `console.log` or `console.debug`. Use the logger.
68
80
 
69
- Tracks each run's evidence: code changes (`write`, `edit`) and check commands (`npm test`, `pytest`, `cargo test`, `tsc`, `eslint`, `go test`, `make test`, and similar, whether run through `bash` or `ctx_execute`) with pass/fail; context-mode's inline `Command exited with code N` counts as a failure. When a run ends with a normal assistant message after code changes and no passing check, one Jev request judges the message: `claims_done`, `claims_verified`, `verification_applies` (Noul), `outcome` (Choice: complete / partial / blocked / other). A completion claim ≥ 0.7 that is not a blocker or question, for a task where checks would mean something (≥ 0.5; prose and file housekeeping score ~0.05), is reported as unverified; a verification claim with no check run is reported as a false claim. With `nudge: true` (default) the agent receives one follow-up turn asking it to run the checks or say plainly that nothing was verified — once per user prompt.
81
+ # Exported functions must have explicit return types
82
+ paths: src/**/*.ts
83
+ Every exported function declares its return type.
70
84
 
71
- ### Slop in code (`write`/`edit`, same request as the action guard)
85
+ # TODO comments need a reference
86
+ A `TODO` or `FIXME` must name a ticket, for example `TODO(APP-123): ...`.
87
+ ```
72
88
 
73
- pi-warden cannot rewrite code — Jev only judges — so slop is handled as a feedback loop through the agent:
89
+ Each heading is one rule; the text under it is the specification. A `paths:` line right under the heading limits the rule to matching files (`**` matches any depth, `*` stays within one segment). Content inside code fences is never read as a heading. The highest heading level present in the file delimits rules, so `##` rules under a `#` title work too.
74
90
 
75
- 1. When the agent calls `write` or `edit`, four Noul questions ride on the action guard's request (no extra latency), one per symptom, each with concrete yes/no criteria: `slop_stub` (placeholder, mock, or "implement later" code where `task` needs a working implementation), `slop_comments` (explanatory comments that restate what the adjacent code shows), `slop_dead` (commented-out code, unused imports or variables, duplicated logic, unreachable branches), `slop_hedging` ("should work", "for now", TODOs without a plan). Jev sees a head/middle/tail sample of a `write` (1500 characters) or the first three replacement texts of an `edit`, plus your request.
76
- 2. Any symptom ≥ `slop.threshold` (0.7) trips the note. The write **goes through** — holding it would leave a half-written file.
77
- 3. The agent receives a steer message naming the symptoms and their fixes, e.g. *"pi-warden: the content just written to `src/x.ts` has stub or placeholder code where a working implementation is needed; hedging or vague notes. Fix it in your next edit: replace stubs, placeholders, and hard-coded fake data with the working implementation, or state in your reply exactly what is left unimplemented and why; replace \"should work\", \"for now\", and TODOs without a plan with a definite statement or a concrete follow-up."* The third time a symptom appears in a session, the note says so and asks the agent to treat it as a standing rule.
91
+ On every `write` and `edit`, pi-warden sends its own request, in parallel with the action guard's, carrying the written content and one question per rule: `compliant`, `violation`, `not_applicable`, or `insufficient_context`. A `write` is sampled at 6000 characters (head, middle, tail); an `edit` sends each new text plus about 40 lines of the current file around the replaced text. With two or more edits, one more question asks which edit contains the violation.
78
92
 
79
- Per-symptom scoring is what makes this precise. From the tuning set (`scripts/slop-cases.mjs`, 16 code cases): a `// TODO: implement later` stub scores stub 0.99; restating comments score comments 0.97 while an explanatory why-comment scores 0.08; commented-out code scores dead 0.96 and comments 0.43; a mock inside a *test file* scores stub 0.51 (below threshold, correctly); a stub explicitly outside the task scores 0.24. Not covered: code written through shell heredocs and content past the sample limits.
93
+ Any rule with P(violation) at or above `rules.threshold` (0.7) is named to the agent: the heading, up to 200 characters of the rule text, and the edit when located. The write goes through; a held write would leave a half-written file. The third hit of one rule in a session says so and asks the agent to treat it as a standing rule. When slop also fires on the same write, both arrive as one message.
80
94
 
81
- ### Slop in replies (`agent_end`)
95
+ Sources, in order: `pi-warden.md` at the root; else the files listed in `rules.files` (all sent in one request); else, with `rules.fallback` (default true), the first of `README.md`, `CLAUDE.md`, `AGENTS.md`, judged as one document with one question. A fallback document is cut to `rules.maxChars` (8000) with every heading and the head of each section kept, so a very long AGENTS.md still fits. Files are re-read when they change; no restart needed. At most 31 rules are asked per request (TypeSafe's cap is 32); the rest are ignored and `/warden status` says how many.
82
96
 
83
- The run's final reply (≥ 200 characters) is judged against `slop.prose.audience`: `wordy` (preamble, restating the request, closing summary, filler), `cliches` ("Great question", "I hope this helps", "it's worth noting", unrequested caveat lists, emoji headings), `jargon` (unexplained terms for the audience). `audience` is `technical` (default), `plain`, or any free-text description such as *"a founder without programming background"*. One long answer to a long question is not punished: a symptom must appear in `trend` (2) of the last 3 replies before the agent is nudged, the nudge is queued for the next user prompt so it shapes the next reply without spending a turn, and two replies pass before the same nudge can fire again. From the tuning set: a padded reply scores wordy 0.97 / clichés 0.99; a dense three-point summary 0.25 / 0.05; the same status update scores jargon 0.94 for a plain audience and 0.24 for a developer.
97
+ Path scoping in config: `rules.exclude` globs are never sent to Jev (secrets, generated, vendored files); `rules.skip` globs are files the rules do not apply to (tests, docs); a per-rule `paths:` line narrows one rule. `rules.sensitivePaths` maps a glob to a note, for example `"migrations/**": "Tell the user this touches a migration and add a rollback"`; a write or edit under a matching path gives the agent that note once per path, offline, with no request.
84
98
 
85
- ### Steer messages
99
+ From the tuning set (`scripts/rules-cases.mjs`, 8 rules, 13 cases, all as expected): `console.log` in code scores 1.00, a bare TODO 1.00 and a `TODO(QUEUE-41)` 0.00, an empty catch 0.99, a `switch` without `default` 0.96, a hardcoded token 0.88, a missing return type 0.99 with a bad boolean name 0.96 on the same write. A compliant module, a test that mentions `console.log` in a string, and a Markdown doc about `console.log` score nothing. The locator points at the right one of two edits. Not covered: code written through shell heredocs and content past the sample limits.
86
100
 
87
- Nudges from the stuck, done, slop, and prose guards are custom messages in the agent's context. By default they are **hidden from the transcript** (`steerVisible: false`) so the conversation stays yours; the notification tells you a nudge happened and the trace panel shows the exact text. Set `steerVisible: true` to see them inline.
101
+ Ideas borrowed with thanks from [jevrealtimecodecheck](https://github.com/MrDesjardins/jevrealtimecodecheck) (rules as headings, the four outcomes) and [wince](https://github.com/TinyFrontier/wince) (path globs, sensitive paths, judge the change and not its story, the locator question).
88
102
 
89
- If TypeSafe cannot answer (timeout after 5 s, outage, budget), an action-guard call is allowed with a warning (`failOpen: true`; set it to `false` to hold instead), and the other guards simply skip. When the per-session request budget is spent, pi-warden says so once and continues with offline checks. Consent in headless runs comes from `PI_WARDEN_ENABLED=1`; `PI_WARDEN_MODE` overrides the mode.
103
+ ### Slop
90
104
 
91
- ### Tool-output security and context saving
105
+ **In code.** When the agent calls `write` or `edit`, four yes/no questions ride on the action guard's request (no extra latency), one per symptom: `slop_stub` (placeholder or fake-data code where a working implementation is needed), `slop_comments` (comments that restate the code), `slop_dead` (commented-out code, unused imports, duplicated logic, unreachable branches), `slop_hedging` ("should work", "for now", TODOs without a plan). Jev sees a 1500-character head/middle/tail sample of a `write` or the first three replacement texts of an `edit`. Any symptom at or above `slop.threshold` (0.7) sends the agent a steer naming the symptom and its fix. The write is never held. The third repeat of a symptom becomes a standing rule.
92
106
 
93
- With consent, content-bearing tools (`read`, fetch/search tools and named MCP equivalents) are checked for instructions that redirect the assistant or request private data. Other tools, including shell/context-mode tools, are checked from 2048 characters. Jev receives a redacted 6000-character head/tail sample, so attacks in an unsampled middle can be missed. A score at or above `security.threshold` adds an untrusted-data notice around the text and a hidden steer. Local credential-shape checks work without network access and warn not to echo or commit possible secrets. Images, result details, error flags, and usage are preserved.
107
+ From the tuning set (`scripts/slop-cases.mjs`): a `// TODO: implement later` stub scores stub 0.99; restating comments 0.97 while an explanatory why-comment scores 0.08; commented-out code scores dead 0.96; a mock inside a test file scores stub 0.51 (below threshold, correctly).
94
108
 
95
- Written code gets a `security_risk` question in the existing action request: hardcoded credentials, disabled TLS checks, unsafe shell/SQL interpolation, broad permissions, or bypassed verification. A threshold crossing warns and steers; it does not authorize an action or automatically block otherwise safe edits.
109
+ **In replies.** The final reply (200 characters or more) is judged against `slop.prose.audience`: `wordy`, `cliches`, `jargon`. `audience` is `technical` (default), `plain`, or free text such as "a founder without programming background". A symptom must appear in `trend` (2) of the last 3 replies before the agent is nudged; the nudge is queued for your next prompt so it shapes the next reply without spending a turn. A padded reply scores wordy 0.97 / clichés 0.99; a dense three-point summary 0.25 / 0.05.
96
110
 
97
- **Duplicates (code only).** A single-text-block result of at least `context.duplicateMinChars` (2000) whose content, ignoring colour codes and trailing whitespace, is identical to an earlier result of this session is replaced by a note naming the earlier tool and the size, plus a recall footer. The exact text is stored once; later copies reuse the file. No Jev request is made. Re-running the same failing test command is the typical case: nothing changed, and the note says so.
111
+ ### Security
98
112
 
99
- **Retention and format (Jev decides, code applies).** For a single text block of at least `context.tailMinChars`, the same output request asks Jev to choose `all`, `errors_and_summary`, or `summary_only`, and which tool format produced the output: `vitest_jest`, `node_test`, `tsc`, `eslint`, `pytest`, `git_diff`, `git_log`, `npm_install`, or `other`. The full result stays when P(keep all) exceeds `1 - context.confidence` (default: keep when Jev gives `all` more than 0.2), when the probability is missing, or on any request error. When a format has at least `context.formatConfidence` (0.7) and its markers are present, a deterministic parser keeps the exact lines that matter for that format: failing tests with their assertion lines, compiler and linter errors with file and line, changed files with +/- counts, package manager notices, and the final summary line. Otherwise code keeps bounded head/diagnostic/tail excerpts. Nothing is paraphrased. An exact copy is written to an owner-only temporary directory before replacement; the excerpt links to it. If storage fails, the original stays. Multiple text blocks keep their layout and are not compressed. Trace and widget show the actual bytes saved. No extra steer is persisted for compression alone.
113
+ Written code gets a `security_risk` question on the action request: hardcoded credentials, disabled TLS checks, unsafe shell or SQL interpolation, broad permissions, bypassed verification. A threshold crossing (0.7) warns you and steers the agent; it does not block.
100
114
 
101
- **Recall through search.** The footer under every excerpt or duplicate note names the file and a search command that exists on this machine, probed once per session in this order: `rg`, `ag`, `ugrep`, `git grep --no-index`, `grep`, PowerShell `Select-String`, `findstr`. It asks for a scoped search or a ranged `read` and says not to read the whole file. `context.recallTool` pins one of those names or `none`; `auto` probes. Recalls are counted by kind: a whole-file read undoes the saving, a scoped access keeps it.
115
+ Tool output from content-bearing tools (`read`, fetch and search tools, named MCP equivalents) is checked for instructions that redirect the assistant or ask for private data; other tools from 2048 characters. Jev receives a redacted 6000-character head/tail sample. A score at or above `security.threshold` wraps the text in an untrusted-data notice and steers the agent. Credential-shape checks work offline and warn not to echo or commit possible secrets. This is advisory, not a sandbox.
102
116
 
103
- **Measuring it.** `/warden status` keeps a session ledger so the saver's value is a number, not a claim: how many outputs were large enough to consider, how many Jev compressed, how many duplicates were dropped, the bytes removed (≈ tokens at 4 bytes each), the *token-turns spared* (removed tokens × every later turn, which is what a prompt cache would otherwise have re-read), and **recalls** — how often the agent went back to a stored full output, split into whole-file reads and scoped accesses. A recall means the excerpt was not enough; a recall rate above about 10% says `context.confidence` is too low for your work, near 0% says it could go lower. Whole-file recalls also give back the saving; scoped ones do not. Every compression and recall also appears in the trace with the running ledger. The quality effect (less context rot) does not show in a counter; compare a scripted task with `context.enabled` on and off if you want that number.
117
+ ### Stuck
104
118
 
105
- Set `context.enabled: false` to stop all of it. The saver never prunes a warm prefix, changes the system prompt, changes the tool list after load, or edits historical session entries: every change is to the newest tool result before it enters the session, so the prompt cache prefix is unaffected. Full-output files can contain secrets; they remain in the OS temporary directory until you or the OS remove them. Excerpts can omit relevant evidence; read the linked file before making a decision that needs it.
119
+ Keeps the last 12 tool results for the current prompt. When the latest result failed and at least 3 failures have accumulated, exact repeats are caught offline (same call, same output with timings and addresses normalised). Otherwise one request judges the sequence: `same_strategy`, `approach_change` (identical / cosmetic / meaningfully different), `progress`. Same strategy at 0.7 counts as stuck: you get a notification and the agent gets a steer asking for a new hypothesis or a blocker report. At most one check per 3 results. In the smoke run, "investigating between failures" scored 0.32 and "flailing" 0.93.
106
120
 
107
- ### Task continuity
121
+ ### Runaway
122
+
123
+ A model that degenerates mid-reply repeats the same lines until the token limit or you press Esc; no tool runs and no turn ends, so nothing else stops it. pi-warden reads the stream as it arrives: per token it appends to a buffer; every 256 characters it counts identical paragraphs (24 characters or more) and checks for one unit repeated back to back at the end. A block repeated `repeats` (4) times in the reply, or `thinkingRepeats` (10) times in thinking, aborts the run. Code only: nothing is sent anywhere. With `recover: true` (default) the agent gets one follow-up turn that names the repeat and asks for the one next step; a second runaway for the same prompt is stopped and left for you. Calibrated on 43,000 local assistant messages: ordinary replies repeat a paragraph twice at most, thinking up to seven times, and the one real runaway repeated its block 28 times. The guard stopped only that one, at half its length.
124
+
125
+ ### Done-check
126
+
127
+ Tracks each run's evidence: code changes (`write`, `edit`) and check commands (`npm test`, `pytest`, `cargo test`, `tsc`, `eslint`, `go test`, `make test`, and similar) with pass or fail; context-mode's inline `Command exited with code N` counts as a failure. When a run ends with a normal message after code changes and no passing check, one request judges the message: `claims_done`, `claims_verified`, `verification_applies`, `outcome`. A completion claim at 0.7 or above for a task where checks mean something is reported as unverified; a claim that tests passed when none ran is called a false claim. With `nudge: true` the agent gets one follow-up turn asking it to run the checks or say plainly that nothing was verified. Once per prompt.
128
+
129
+ ### Context saver
130
+
131
+ Only the newest tool result is ever changed, before it enters the session, so the prompt cache prefix and all earlier entries stay as they were.
132
+
133
+ - **Duplicates** (code only). A text result of at least `context.duplicateMinChars` (2000) that is identical to an earlier result of this session becomes a short note naming the earlier tool and the size, plus a recall footer. Re-running the same failing test is the typical case.
134
+ - **Retention and format** (Jev decides, code applies). For a single text block of at least `context.tailMinChars` (12000), Jev picks `all`, `errors_and_summary`, or `summary_only`, and names the format (`vitest_jest`, `node_test`, `tsc`, `eslint`, `pytest`, `git_diff`, `git_log`, `npm_install`, `other`). When a format is confident (0.7) and its markers are present, a parser keeps the exact lines that matter: failing tests with their assertions, compiler and linter errors with file and line, changed files with counts, package notices, the summary line. Otherwise bounded head, diagnostic, and tail excerpts. Nothing is paraphrased. The full output is written to an owner-only temporary file first; the excerpt links to it. If storage fails, the original stays. Multiple text blocks are not compressed.
135
+ - **Recall through search.** The footer names the file and a search command that exists on this machine (probed once: `rg`, `ag`, `ugrep`, `git grep --no-index`, `grep`, `Select-String`, `findstr`). `context.recallTool` pins one or `none`.
136
+ - **Measuring it.** `/warden status` shows how many outputs were candidates, how many were compressed or dropped as duplicates, the bytes removed, the token-turns spared, and the recalls, split into whole-file reads (which give the saving back) and scoped accesses. A recall rate above about 10% means `context.confidence` is too low for your work.
137
+
138
+ Set `context.enabled: false` to turn it off. Full-output files can contain secrets and stay in the OS temporary directory until removed.
139
+
140
+ ### Desktop notifications
141
+
142
+ A held call the agent will ask you about, a confirm dialog waiting for an answer, and a runaway stop reach the desktop. macOS uses `osascript`; Linux tries `notify-send`, `dunstify`, `gdbus`, `kdialog`, `zenity`, then `powershell.exe` for WSL; Windows shows a toast through PowerShell. Interactive sessions only, one notification per `cooldownMs` (10 s), the reason but never the command. `"notify": { "enabled": false }` turns it off; `"command": ["curl", "-d", "{body}", "https://ntfy.sh/your-topic"]` in the user file replaces the desktop tool with your own relay (no shell; `{title}` and `{body}` are replaced and set as `PI_WARDEN_TITLE` / `PI_WARDEN_BODY`). A project file may switch notifications off but never names a command.
143
+
144
+ ### Steer messages
108
145
 
109
- The action guard receives the latest user message plus up to eight previous user/assistant text messages, limited to 750 redacted characters each. This keeps follow-ups and side comments from replacing the task. New user instructions override earlier ones; assistant text supplies context, not permission. Approval of a held action still uses only the latest user message. `unclear` scope alone no longer triggers an off-task warning; irreversible-action checks are unchanged. Long handoffs outside that bounded context can still require the user to restate the task.
146
+ Nudges from the rules, slop, stuck, done, and prose guards are custom messages in the agent's context. By default they are hidden from the transcript (`steerVisible: false`); the notification tells you a nudge happened and the trace panel shows the exact text. When the per-session request budget is spent, pi-warden says so once and continues with offline checks.
110
147
 
111
148
  ## Commands
112
149
 
113
150
  | Command | Effect |
114
151
  | --- | --- |
115
- | `/warden status` | Guard state, consent source, key source, session counts, thresholds, config paths, last verdict |
116
- | `/warden enable` | Show the data notice, prompt for a key if none is stored, and save consent for Jev judgments |
152
+ | `/warden status` | Guard state, consent and key source, session counts, thresholds, rules source, config paths, last verdicts |
153
+ | `/warden enable` | Data notice, key prompt if none is stored, consent saved |
117
154
  | `/warden disable` | Stop Jev judgments; pattern checks continue |
118
- | `/warden mode steer\|confirm\|advise` | Choose how holds are handled; without an argument, show the current mode |
119
- | `/warden config` | Edit the user config JSON in Pi's editor and save it |
120
- | `/warden test` | Evaluate one synthetic destructive action and show the verdict and what the agent would be told |
155
+ | `/warden mode steer\|confirm\|advise` | How holds are handled; without an argument, show the current mode |
156
+ | `/warden config` | Edit the user config JSON in Pi's editor |
157
+ | `/warden test` | One synthetic destructive action, its verdict, and what the agent would be told |
121
158
  | `/warden trace` | Toggle the trace sidebar (or print the last 20 events without a UI) |
122
159
 
123
160
  ## Status line and trace sidebar
124
161
 
125
- The line above the editor shows the latest verdict per guard, for example `warden · bash · irreversible 0.84 · off-task 0.86 · unrelated · confirm`. Every verdict is also kept in a session trace with what was inspected (redacted command or path), the pattern hits, Jev's scores with model and latency, the reasons, and the exact text the agent was told.
162
+ The line above the editor shows the latest verdict per guard, for example `warden · bash · irreversible 0.84 · off-task 0.86 · unrelated · confirm`. `/warden trace`, `ctrl+shift+w`, and a click on the line each toggle a right-hand sidebar with the full trace, newest first, live. The sidebar does not take the keyboard; click inside it for arrow keys and PgUp/PgDn, `c` clears, Esc hands input back, `q` closes. `widget.panelWidth` sets its width.
126
163
 
127
- `/warden trace`, `ctrl+shift+w`, and a click on the status line each **toggle a right-hand sidebar** with that trace, newest first, live-updating while the agent works. The sidebar does not take the keyboard: you keep typing in the editor while it is open. Click inside it to give it the keys (↑↓/PgUp/PgDn scroll, `c` clears, Esc hands input back to the editor, `q` closes); the wheel scrolls it without focus. `widget.panelWidth` sets its width (`"40%"` or a column count).
164
+ Clicks and the wheel need Pi's fullscreen mode (`tuiMode: "fullscreen"` in `/settings`). In macOS Terminal.app enable View → Allow Mouse Reporting.
128
165
 
129
- Clicks and the wheel need Pi's fullscreen mode (`tuiMode: "fullscreen"` in `/settings`); regular mode leaves the mouse to the terminal for scrollback. If the mouse does nothing in fullscreen, the terminal is not reporting it: in macOS Terminal.app enable View → Allow Mouse Reporting (⌘R). Pi's extension API offers floating overlays but no side dock that narrows the transcript, so the sidebar covers the right part of the screen instead of pushing the transcript aside.
130
-
131
- Templates in `config.widget` control the text. Segments are separated by ` · `; a segment whose token has no value for that verdict is dropped, so optional information disappears together with its label:
166
+ Templates in `config.widget` control the text. Segments are separated by ` · `; a segment whose token has no value is dropped:
132
167
 
133
168
  ```json
134
169
  "widget": {
135
- "enabled": true,
136
- "placement": "aboveEditor",
137
- "shortcut": "ctrl+shift+w",
138
- "panelWidth": "40%",
139
170
  "action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
171
+ "rules": "warden · rules · {tool} {path} · {asked} rules · {violations} · {status}",
140
172
  "stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
141
173
  "done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
142
174
  "prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
143
175
  "security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
144
- "context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes"
176
+ "context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes",
177
+ "runaway": "warden · runaway · {kind} · {count}× repeated · {chars} chars · {signal} · {status}"
145
178
  }
146
179
  ```
147
180
 
148
- Tokens — action: `tool level source irreversible offTask scope approved slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time`; prose: `wordy cliches jargon status reasons model ms flags time`; stuck: `failures sameStrategy approachChange progress status source reasons model ms flags time`; done: `changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time`. Security tokens: `tool injection exfiltration status`; context tokens: `tool retention bytesSaved`. A minimal line: `"action": "⚔ {tool} {level} · {irreversible}/{offTask}"`. Set `"enabled": false` to hide the line (the trace and panel keep working); `"shortcut": ""` disables the keybinding.
181
+ Tokens per guard: action `tool level source irreversible offTask scope approved slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time`; rules `tool path asked violations status source reasons model ms flags time`; prose `wordy cliches jargon status reasons model ms flags time`; stuck `failures sameStrategy approachChange progress status source reasons model ms flags time`; done `changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time`; security `tool injection exfiltration status`; context `tool retention bytesSaved`; runaway `kind count chars signal block status time`. `"enabled": false` hides the line; `"shortcut": ""` disables the keybinding.
149
182
 
150
183
  ## Configuration
151
184
 
@@ -165,66 +198,101 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
165
198
  "irreversible": { "warn": 0.5, "confirm": 0.7 },
166
199
  "offTask": { "warn": 0.6, "confirm": 0.85 }
167
200
  },
168
- "stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
169
- "done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
170
- "security": { "enabled": true, "threshold": 0.7 },
171
- "context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
201
+ "rules": {
202
+ "enabled": true,
203
+ "threshold": 0.7,
204
+ "files": [],
205
+ "fallback": true,
206
+ "maxChars": 8000,
207
+ "exclude": [],
208
+ "skip": [],
209
+ "sensitivePaths": {}
210
+ },
172
211
  "slop": {
173
212
  "enabled": true,
174
213
  "threshold": 0.7,
175
214
  "prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
176
215
  },
216
+ "security": { "enabled": true, "threshold": 0.7 },
217
+ "stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
218
+ "done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
219
+ "context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
220
+ "runaway": { "enabled": true, "repeats": 4, "thinkingRepeats": 10, "minChars": 400, "recover": true },
221
+ "notify": { "enabled": true, "cooldownMs": 10000, "command": [] },
177
222
  "widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w", "panelWidth": "40%" },
178
223
  "steerVisible": false
179
224
  }
180
225
  ```
181
226
 
182
- A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (for example stricter thresholds, extra guarded tools, or `"done": { "enabled": false }`). Project files are read only when Pi trusts the project, and they can never grant `typesafe` consent, change `mode`, or raise `timeoutMs`/`maxRequests`. Environment: `PI_WARDEN_ENABLED=1` (consent), `PI_WARDEN_MODE=steer|confirm|advise`. Files from 0.1.x that set `action.timeoutMs`/`action.maxRequests` keep working.
227
+ A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides: stricter thresholds, extra guarded tools, `rules.files`, `rules.skip`, `rules.sensitivePaths`, or `"done": { "enabled": false }`. Project files are read only when Pi trusts the project. They can never grant `typesafe` consent, change `mode`, raise `timeoutMs` or `maxRequests`, or set `notify.command`. Environment: `PI_WARDEN_ENABLED=1` (consent), `PI_WARDEN_MODE=steer|confirm|advise`.
228
+
229
+ A wince-style setup for a backend repo:
230
+
231
+ ```json
232
+ {
233
+ "rules": {
234
+ "skip": ["tests/**", "**/*.test.*", "docs/**", "**/*.md"],
235
+ "exclude": ["secrets/**", "**/*.pem"],
236
+ "sensitivePaths": {
237
+ "migrations/**": "This touches a migration: tell the user and add a rollback path",
238
+ "**/permissions*": "Access control changed: ask the user for a security review before merging"
239
+ }
240
+ }
241
+ }
242
+ ```
183
243
 
184
244
  ## Data handling
185
245
 
186
- - With consent, each guarded call sends to `https://api.typesafe.ai`: your latest prompt (truncated to 1500 characters), the tool name, the command (truncated to 2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character head/middle/tail sample for `write`, and the first three edit pairs (400 characters each) for `edit`. The stuck detector sends the last 12 tool calls (300 characters each) with 400-character output tails; the done-check and the prose check send the agent's final message (2000 and 2500 characters) with the run's check commands and the audience description. Scope and retention requests also carry up to eight prior user/assistant text messages (750 redacted characters each). Output checks send a redacted head/tail sample up to 6000 characters plus size, line counts, and tool name; the format question uses that same sample. Duplicate detection sends nothing. No unrelated files or telemetry.
187
- - Obvious credentials in the action (`Authorization` headers, `TOKEN=`/`SECRET=` assignments, `sk-`, `ghp_`, `AKIA`, JWTs, URL passwords, PEM blocks) are replaced with `[redacted]` before sending. This is best-effort; do not rely on it for prompts that contain secrets.
188
- - Text returned or steered to the agent names the tool, the reasons, and the scores, not the command text. UI errors never include upstream response bodies or keys.
189
- - Judgments are model output. Thresholds are yours to tune; a hold is information for the agent and for you, not a verdict on either.
246
+ With consent, requests go to `https://api.typesafe.ai`. What is sent:
247
+
248
+ - **Action guard**: your latest prompt (1500 characters), up to eight earlier user and assistant messages (750 redacted characters each), the tool name, the command (2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character head/middle/tail sample of a `write`, the first three edit pairs (400 characters each) of an `edit`.
249
+ - **Rules**: the project-relative path, a 6000-character sample of a `write` or each edit's new text (1500 characters) with about 40 lines of the current file around the replaced text, and the rule text from your rules file or the condensed fallback document (`rules.maxChars`). No task text. Files under `rules.exclude` are never sent.
250
+ - **Stuck**: the last 12 tool calls (300 characters each) with 400-character output tails.
251
+ - **Done-check and prose**: the agent's final message (2000 and 2500 characters), the run's check commands, the audience description.
252
+ - **Output checks**: a redacted head/tail sample up to 6000 characters plus size, line counts, and tool name.
253
+ - **Nothing** for duplicate detection, the runaway guard, sensitive-path notes, or pattern checks.
254
+
255
+ Obvious credentials (`Authorization` headers, `TOKEN=` and `SECRET=` assignments, `sk-`, `ghp_`, `AKIA`, JWTs, URL passwords, PEM blocks) are replaced with `[redacted]` before sending. Best-effort; do not rely on it for prompts that contain secrets. Text steered to the agent names the tool, the reasons, and the scores, not the command. UI errors never include upstream response bodies or keys. Judgments are model output; thresholds are yours to tune.
190
256
 
191
- ### After updating the package
257
+ ## After updating the package
192
258
 
193
- Run `/reload` in each existing Pi session after an update. If an existing session still reports `Cannot read properties of undefined (reading 'enabled')` while a fresh session works, restart that session to replace loaded modules. Version labels alone do not prove that all in-memory modules match the files on disk. Current config loading merges missing legacy fields with defaults; regression tests cover malformed and pre-prose config files. Since 0.5.2 the extension also checks the shape of the config it receives: if a section is missing (the symptom of an extension module and a config module from different package versions in one process), that guard is switched off and one warning names the missing sections and the schema numbers instead of throwing inside Pi. If you see that warning, restart Pi. A crash that survives a restart needs its stack trace and a redacted config example.
259
+ Run `/reload` in each open Pi session after an update. If a session reports `Cannot read properties of undefined (reading 'enabled')` while a fresh session works, restart it: the loaded modules do not match the files on disk. Since 0.5.2 the extension checks the shape of the config it receives; a missing section (an extension module and a config module from different versions in one process) switches that guard off and prints one warning naming the sections and the schema numbers. If you see it, restart Pi.
194
260
 
195
261
  ## For extension authors
196
262
 
197
- The guard is a plain function you can call from your own extension or tests, with any object that has pi-typesafe's `evaluate` method as the judge:
263
+ Every guard is a plain function you can call with any object that has pi-typesafe's `evaluate` method as the judge:
198
264
 
199
265
  ```ts
200
- import { evaluateAction, defaultConfig } from "pi-warden";
266
+ import { evaluateAction, evaluateRules, RuleStore, defaultConfig } from "pi-warden";
201
267
  import { createTypeSafe } from "pi-typesafe";
202
268
 
269
+ const judge = createTypeSafe();
203
270
  const verdict = await evaluateAction(
204
271
  { tool: "bash", input: { command: "git push --force" }, cwd: process.cwd(), task: "push my branch" },
205
- { config: defaultConfig().action, judge: createTypeSafe() }, // omit judge for pattern checks only
272
+ { config: defaultConfig().action, judge }, // omit judge for pattern checks only
206
273
  );
207
274
  verdict.level; // "allow" | "warn" | "confirm" (confirm = hold in steer mode)
208
275
  verdict.reasons; // ["destructive: git force push", "irreversible 0.91"]
209
- verdict.judgment; // { irreversible, offTask, scope, scopeConfidence, approved?, model, elapsedMs }
210
- steerReason(verdict, { canApprove: true }); // the text the agent receives for a hold
211
- ```
212
276
 
213
- `evaluateAction` judges one call. What spans calls in a session, the hold → reply → retry approval and the batched judging of sibling tool calls, is `ActionGuard`: `inspect(call, conversation, options)` returns the same `Verdict`, `hold(task)` records a hold, `turnEnd()` and `reset()` follow Pi's turn and session.
277
+ const set = new RuleStore().load(process.cwd(), defaultConfig().rules);
278
+ const rules = await evaluateRules("write", { path: "src/a.ts", content: "console.log(1)" }, { cwd: process.cwd(), config: defaultConfig().rules, set, judge, timeoutMs: 5000 });
279
+ rules.findings; // [{ id: "no-console-statements", name, violation: 0.97, body }]
280
+ ```
214
281
 
215
- Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`, `formatVerdict`, the question sets, the stuck detector (`AttemptWindow`, `makeAttempt`, `evaluateStuck`, `stuckNudge`), the done-check (`classifyToolResult`, `recordOutcome`, `needsDoneCheck`, `evaluateDone`, `doneNudge`), and the config helpers.
282
+ What spans calls in a session lives in `ActionGuard` (hold, reply, retry approval, sibling batching) and `RulesGuard` (rule cache, sibling prejudging, repeat counts, sensitive-path notes). Also exported: `matchPatterns`, `isReadOnlyCommand`, `stripDataText`, `describeAction`, `parseRules`, `matchGlob`, `redact`, `formatVerdict`, the question sets, the stuck detector, the runaway guard, the notifier, the done-check, and the config helpers.
216
283
 
217
284
  ## Development
218
285
 
219
286
  ```bash
220
287
  npm install
221
- npm run check # typecheck, offline tests (mocked transport), build
222
- npm run test:live # 55 billable synthetic judgments across all guards (key from .env or the stored login); pass action|slop|approval|stuck|done|security|context for one group
223
- node scripts/security-cases.mjs # the 9-case output-security and task-continuity set on its own
224
- node scripts/context-cases.mjs # 15 labelled outputs that tune retention and format for the context saver
225
- npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
226
- node scripts/slop-cases.mjs # 22 billable cases that tune the slop and prose questions
227
- npm run preview # re-render docs/preview.png from the recorded live verdicts (needs a Chrome binary)
288
+ npm run check # typecheck, offline tests (mocked transport), build
289
+ npm run test:live # billable synthetic judgments across the guards; pass action|slop|approval|stuck|done|security|context for one group
290
+ node scripts/rules-cases.mjs # 13 billable cases against an 8-rule fixture file
291
+ node scripts/slop-cases.mjs # 22 billable cases for the slop and prose questions
292
+ node scripts/security-cases.mjs # 9 output-security and task-continuity cases
293
+ node scripts/context-cases.mjs # 15 labelled outputs for retention and format
294
+ npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
295
+ npm run preview # re-render docs/preview.png (needs a Chrome binary)
228
296
  ```
229
297
 
230
298
  ## License
package/dist/config.d.ts CHANGED
@@ -58,6 +58,24 @@ export interface SecurityConfig {
58
58
  /** P(injection or exfiltration) at or above this adds an untrusted-output notice. */
59
59
  threshold: number;
60
60
  }
61
+ export interface RulesConfig {
62
+ /** Judge each write and edit against the project's Markdown rules on its own Jev request; steer, never hold. */
63
+ enabled: boolean;
64
+ /** P(violation) at or above this names the rule to the agent. */
65
+ threshold: number;
66
+ /** Project-relative Markdown rule files, used when the root pi-warden.md is absent. All are sent in one request. */
67
+ files: string[];
68
+ /** With no rules file, README.md, CLAUDE.md, or AGENTS.md (first found) is judged as one document. */
69
+ fallback: boolean;
70
+ /** Characters of a fallback document sent per request; every heading and the head of each section are kept within it. */
71
+ maxChars: number;
72
+ /** Globs of files whose content is never sent to Jev for rules (secrets, generated, vendored). */
73
+ exclude: string[];
74
+ /** Globs of files the rules do not apply to, for example tests or docs. */
75
+ skip: string[];
76
+ /** Glob → note. A write or edit under a matching path steers the agent with the note once per path; code only. */
77
+ sensitivePaths: Record<string, string>;
78
+ }
61
79
  export interface ContextConfig {
62
80
  enabled: boolean;
63
81
  /** Only new tool output is compressed; warm history and system prompts are never changed. */
@@ -71,6 +89,28 @@ export interface ContextConfig {
71
89
  /** Minimum P(format) before a format-specific parser builds the excerpt instead of the generic head/tail one. */
72
90
  formatConfidence: number;
73
91
  }
92
+ export interface RunawayConfig {
93
+ enabled: boolean;
94
+ /** Identical text paragraphs in one streaming reply before the run is stopped. Ordinary replies repeat a paragraph twice at most. */
95
+ repeats: number;
96
+ /** The same limit for thinking, where code drafting repeats paragraphs legitimately. */
97
+ thinkingRepeats: number;
98
+ /** Characters streamed before the first check. */
99
+ minChars: number;
100
+ /** After stopping, start one follow-up turn that names the repeat and asks for the one next step; once per user prompt. */
101
+ recover: boolean;
102
+ }
103
+ export interface NotifyConfig {
104
+ /** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. */
105
+ enabled: boolean;
106
+ /** Sibling holds in one turn produce one notification; a second within this many milliseconds is skipped. */
107
+ cooldownMs: number;
108
+ /**
109
+ * Your own notifier as an argv (no shell), for ssh sessions or a phone relay: `{title}` and `{body}` in an argument are
110
+ * replaced, and both are in PI_WARDEN_TITLE / PI_WARDEN_BODY. Empty: detect the desktop's own tool. User file only.
111
+ */
112
+ command: string[];
113
+ }
74
114
  export type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
75
115
  export declare function isRecallTool(value: unknown): value is RecallTool;
76
116
  export type WardenMode = "steer" | "confirm" | "advise";
@@ -94,7 +134,10 @@ export interface WardenConfig {
94
134
  done: DoneGuardConfig;
95
135
  slop: SlopGuardConfig;
96
136
  security: SecurityConfig;
137
+ rules: RulesConfig;
97
138
  context: ContextConfig;
139
+ runaway: RunawayConfig;
140
+ notify: NotifyConfig;
98
141
  /** The status line above the editor and the trace panel. */
99
142
  widget: WidgetConfig;
100
143
  /** Show steer messages in the transcript. They are always visible in the trace panel. */
@@ -102,7 +145,7 @@ export interface WardenConfig {
102
145
  }
103
146
  export declare const PACKAGE_NAME = "pi-warden";
104
147
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
105
- export declare const CONFIG_SCHEMA = 3;
148
+ export declare const CONFIG_SCHEMA = 5;
106
149
  export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
107
150
  export declare function defaultConfig(): WardenConfig;
108
151
  /** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */