pi-warden 0.12.0 → 0.14.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,325 +1,203 @@
1
1
  # pi-warden
2
2
 
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.
3
+ Makes the [Pi](https://pi.dev) agent follow your project's rules. An `AGENTS.md` asks the model to remember; pi-warden checks. On every write and edit, [Jev](https://typesafe.ai) judges the change against the rules in your `pi-warden.md` (about 250 ms, a fraction of a cent) and the violated rule is quoted back into the agent's own context. The same channel names slop, breaks retry loops, calls out "done" when no test ran, keeps large tool output small, and holds the rare destructive command. You keep working; the agent gets a nudge instead of you getting a dialog.
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
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
- Default configs and a starter rules file are in [`examples/`](examples/).
9
+ ## In one minute
10
10
 
11
- ## What it does
11
+ ```bash
12
+ pi install npm:pi-warden
13
+ ```
14
+
15
+ Then, inside Pi:
16
+
17
+ 1. `/warden enable`. Read the data notice and confirm. If no key is stored yet, paste one from [console.typesafe.ai](https://console.typesafe.ai) (hidden input, saved owner-only, shared with pi-typesafe).
18
+ 2. `/warden test` shows one synthetic verdict and what the agent would be told.
19
+ 3. Work as usual. The line above the editor shows the latest verdict; `ctrl+shift+w` opens the trace with the scores and the exact text the agent received.
20
+
21
+ Requires Pi 0.85 or newer and Node.js 22.19 or newer. Without a key, the offline parts still run: the pattern list, the runaway stop, duplicate-output notes, sensitive-path notes, and the credential-shape warnings. The [examples folder](examples/README.md) has a starter rules file and both config files.
22
+
23
+ ## What it watches
12
24
 
13
25
  | Guard | Watches | Does |
14
26
  | --- | --- | --- |
15
- | **Action** | every `bash`, `write`, `edit` (and context-mode's `ctx_execute*`) before it runs | holds irreversible calls and tells the agent why, so it re-plans or asks you; steers it back when a change is unrelated to your request |
16
27
  | **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 |
17
28
  | **Slop** | written code and final replies | names stubs, restating comments, dead code, hedging, padded replies; the agent fixes them in the next edit |
18
- | **Security** | written code and tool output | flags hardcoded secrets, disabled TLS, unsafe interpolation; marks injected instructions in tool output |
19
29
  | **Stuck** | tool results | notices three failures with the same strategy and asks for a new hypothesis |
20
- | **Runaway** | the reply stream | stops a reply that repeats the same block over and over (code only, no request) |
21
30
  | **Done-check** | the final message | catches "done" claims after code changes when no test, build, or lint passed |
22
31
  | **Context saver** | large or repeated tool output | keeps the exact lines that matter, stores the rest in a file, points recalls at a search |
32
+ | **Action** | every `bash`, `write`, `edit` (and context-mode's `ctx_execute*`) before it runs | holds irreversible calls and tells the agent why, so it re-plans or asks you; steers it back when a change is unrelated to your request |
33
+ | **Security** | written code and tool output | flags hardcoded secrets, disabled TLS, unsafe interpolation; marks injected instructions in tool output; fixture and documentation stand-ins are traced, never announced |
34
+ | **Runaway** | the reply stream | stops a reply that repeats the same block over and over (code only, no request) |
35
+ | **Subagent triage** | async subagent reports | Jev decides whether a child report is worth waking the agent for; routine progress stays in context and costs no request ([detail](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md#subagent-triage)) |
23
36
  | **Notifications** (opt-in) | moments that need you | desktop notification for a held call, a confirm dialog, a runaway stop |
24
37
 
25
- 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.
26
-
27
- ## Why Jev
28
-
29
- 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.
30
-
31
- ## Install
32
-
33
- ```bash
34
- pi install npm:pi-warden
35
- ```
36
-
37
- 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).
38
-
39
- ## Setup
40
-
41
- 1. Get a key at [console.typesafe.ai](https://console.typesafe.ai) (API Keys).
42
- 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`.
43
- 3. Optional: `/warden test` shows one synthetic verdict.
44
-
45
- `TYPESAFE_API_KEY` in the environment takes precedence over the stored key. Headless runs give consent with `PI_WARDEN_ENABLED=1`.
46
-
47
- The [examples README](examples/README.md) walks through the starter `pi-warden.md`, the project `.pi/pi-warden.json`, and the user `config.json`.
38
+ Three jobs, one channel: keep the agent on your rules, keep slop out of the code and the replies, keep the context small. The action guard is the seatbelt underneath: on 17,000 recorded calls it held 42 times. Everything arrives through the agent's own context, so the agent gets smarter and you are not the approval button. Full detail per guard, with thresholds and tuning numbers, is in [docs/guards.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md).
48
39
 
49
- ## Philosophy
40
+ ## Does it improve the code?
50
41
 
51
- pi-warden is a harness for the agent, not a gate for you. Three jobs:
42
+ **Short answer: yes, on a model that breaks rules.** The same five tasks through glm-5.3-flash broke a rule in **7 of 25 runs without pi-warden and 1 of 25 with it**, at no cost in passing tests. On a model that already writes clean code (deepseek-v4.1-flash) it changed nothing measurable: 0 rule-breaking runs in either cell.
52
43
 
53
- | Pillar | What it means |
54
- | --- | --- |
55
- | **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. |
56
- | **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. |
57
- | **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. |
44
+ `npm run eval:ab` runs the same fixture tasks through headless Pi twice. The control cell gets the five rules as prose in `AGENTS.md`; the warden cell gets the same file plus pi-warden enforcing `pi-warden.md`. A mechanical checker scores every final diff and shares no code with the guard, so no guard verdict can influence a score; each task ships a shortcut that passes the provided tests while breaking a rule, so only the checker can tell the cells apart. Run it on your own model: `npm run eval:ab -- --repeats 5 --model <model>` (about 45 minutes and a few hundred thousand tokens for 50 runs).
58
45
 
59
- ## The guards
46
+ Results so far, 50 runs per model (5 tasks, both cells, 5 repeats):
60
47
 
61
- ### Action guard
48
+ | Model | Cell | Rule violations in final diffs | Tests fully passing | Median run time |
49
+ | --- | --- | --- | --- | --- |
50
+ | glm-5.3-flash | control | **7 of 25 runs** | 50 of 50 | 34 s |
51
+ | glm-5.3-flash | warden | **1 of 25 runs** | 47 of 50 | 44 s |
52
+ | deepseek-v4.1-flash | control | 0 of 25 runs | 25 of 25 | 15 s |
53
+ | deepseek-v4.1-flash | warden | 0 of 25 runs | 25 of 25 | 20 s |
62
54
 
63
- Runs on `tool_call`, before the tool executes.
55
+ The clipping task shows the effect best. Without the warden, 6 of 10 glm runs cut user-visible text to fit. With it, 1 of 10, and every run that received a clip steer ended clean. On deepseek, a model that already wrote clean code, the warden changed nothing worth measuring and cost a few seconds per run.
64
56
 
65
- 1. **Skip** read-only tools and read-only shell lines (`git status && ls`): no request, no widget line.
66
- 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`, `--no-verify` or signing switched off on a git command, and `gh pr merge` 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.
57
+ The honest footnotes. The glm test failures in the warden cell are model variance, not guard interference: a 20-run retry-only batch put both cells at 8 of 10, and the guard stayed silent in every failure. Credential steer noise was the one measured waste: 30 of deepseek's 34 steers were two fixture tokens the agent read from the test file it was editing. 0.14 traces those stand-ins instead, which replays those 50 runs at 2 credential steers instead of 30, both of them a real-shaped token an environment dump exposed. All numbers so far come from one machine and two models; per-batch reports live in [eval/reports/](eval/reports/README.md), and the claim has to survive your re-run too.
67
58
 
68
- 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.
69
- 3. **Jev**, with consent: one request with `{ task, context, plan, 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 never holds: at 0.6 it warns, and at 0.85 with `unrelated` on a call that can change something the agent is also steered back to your request (an unrelated `grep` is warned about only). Patterns set the floor; Jev can only raise it.
59
+ ## Two moments from real sessions
70
60
 
71
- `plan` is the agent's own words in the message that makes the call (or its latest text since your prompt, 500 redacted characters). It tells Jev which step this is, so a verification fixture the agent just announced is not judged unrelated; it never authorizes anything. When there is a plan, a fifth question `intent_mismatch` asks whether the call does something materially different from it: a delete where the plan said list, a force push where it said push. At `action.intentMismatch` (0.9) on a call that can change something, the call is warned about and the agent is told to keep its words and its calls in step. A command whose effect is visible outside the working tree (`visible`: a commit, push, merge, publish, message, install, launched program) needs only `action.visibleMismatch` (0.8): on recorded sessions that pair is what users objected to. Never held on that alone. The trace shows the plan under each verdict.
72
- 4. **Act**, by mode:
73
- - `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).
74
- - `confirm`: a `ctx.ui.confirm` dialog. No blocks with a short reason. Falls back to `steer` without a UI.
75
- - `advise`: never holds, reports only.
61
+ Both are from my own work sessions, taken from the session logs. Project names are redacted, paths shortened, secrets replaced. The other two, a placeholder the agent talked itself out of removing and a retry loop broken mid-run, are in [docs/examples.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/examples.md).
76
62
 
77
- 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).
63
+ ### It quoted a project rule back, and the agent fixed it in the next turn
78
64
 
79
- **Hold feedback.** What you do next labels each judgment, so hold precision is measured on your sessions rather than assumed. A hold your reply releases (or the confirm dialog allows) was a false positive; a hold you decline, or that nobody approves after you replied and the next turn ended, stood. An allowed call your next message tells the agent to stop, undo, or revert was a miss: one `regretted` question rides the first action request after your reply, with the redacted summaries of last turn's allowed calls (a locator names the one when there are several); offline, a stop-word heuristic stands in. `/warden status` shows the counts and the precision, the trace entry of each call gets its outcome, and every judged call is written with its scores and outcome to an owner-only per-session file under `~/.pi/agent/pi-warden/holds/` (tool, pattern ids, scores, level, mode, outcome; never the command). `"action": { "feedbackLog": false }` keeps the counts and skips the file.
65
+ The project's `pi-warden.md` rule "No hardcoded secrets" says connection URLs come from settings or the environment. The agent wrote a demo script with the local database DSN as a fallback default.
80
66
 
81
- #### Calibration
67
+ ```text
68
+ assistant
69
+ [write] scripts/demo_rail_decisions.py (186 lines)
70
+ DSN = os.getenv("SUPABASE_DB_URL", "postgresql://[local default]")
82
71
 
83
- `node scripts/calibrate-action.mjs --all` replays every guarded call in your recorded Pi sessions through the guard (one request per call) and asks Jev, once per turn, whether your next message regrets one of the calls that ran, approves each held call, and how it receives the turn (continues, corrects, rejects, unrelated). Run on 321 sessions from this machine (1,085 labelled turns, 17,160 guarded calls, 14,903 judged):
72
+ pi-warden (steer message to the agent)
73
+ pi-warden: the content just written to scripts/demo_rail_decisions.py violates
74
+ project rule from pi-warden.md: "No hardcoded secrets" (0.88): Source and config
75
+ code must not contain passwords, API keys, tokens, or connection URLs (including
76
+ Supabase DSNs and `FLY_API_TOKEN` values). They come from settings
77
+ (`utils/config.py`) or the environ… [77 more chars]. Fix it in your next edit.
84
78
 
85
- - Regret is rare: 20 calls (2% of turns). None of them was about data loss: their `irreversible` scores were 0.04 to 0.57, median 0.07. They were scope and permission complaints: a commit the user did not want, an edit to a personal `CLAUDE.md`, a merge, a test run when conflicts were the job, a program launched at night. The hold rule catches none of them at any threshold that holds fewer than 3% of calls, so the hold defaults stay where they are; they are a checkpoint for destructive actions, and regret is the wrong yardstick for those.
86
- - Signal ranking against regret (AUC): `mutates` 0.74, `irreversible` 0.71, `intent_mismatch` 0.57, `off_task` 0.51. Off-task alone caused 56 of the 139 replay holds and none of them drew a complaint, so since 0.12 off-task warns and steers but never holds (`offTask.steer`; a `confirm` key in an older config file still sets it).
87
- - The intent steer earned its threshold here. At 0.8 it fires on 11% of calls that can change something and 14% of those sit in a turn the user rejects (base rate 5%); at 0.9 it fires on 4% and 33% of those are in a rejected turn, 54% in one the user rejects or corrects (base rate 24%). The default is 0.9.
88
- - A second pass asked four candidate questions on the same calls (`scripts/action-candidates.mjs`, `--extra`). None separates rejected turns on its own: "would a careful engineer ask first", "is this unrequested", "did the user ask to pause", and "is the effect visible outside the working tree" all sit at the 4–5% base rate. `visible` has the best recall on regret (AUC 0.82, 10 of 19 regretted calls) but a commit or push is usually what was asked. Paired with the plan it works: `visible >= 0.8` and `intent_mismatch >= 0.8` flags 1.1% of calls with 18% in a rejected turn, so that pair steers at `visibleMismatch` 0.8. Two deterministic patterns came from the regretted list: a git command with hooks or signing switched off, and `gh pr merge`.
89
- - Of 42 holds pi-warden made in those sessions, the user's next message approved 5.
90
-
91
- The output stays under `.local/calibration/` (owner-only, never committed); `--report FILE` recomputes the tables without requests, `--project DIR` limits the run to one project's sessions, `--dry-run` prints the request count.
92
-
93
- ### Rules
94
-
95
- Write your project's rules as Markdown headings in `pi-warden.md` at the project root (a starter file with a dozen rules is in [`examples/pi-warden.md`](examples/pi-warden.md)):
96
-
97
- ```markdown
98
- # No console statements
99
- Code must not contain `console.log` or `console.debug`. Use the logger.
100
-
101
- # Exported functions must have explicit return types
102
- paths: src/**/*.ts
103
- Every exported function declares its return type.
104
-
105
- # TODO comments need a reference
106
- A `TODO` or `FIXME` must name a ticket, for example `TODO(APP-123): ...`.
79
+ assistant
80
+ The warden flags the hardcoded local DSN. Right call. Making the script require
81
+ `SUPABASE_DB_URL` from the environment instead:
82
+ DSN = os.getenv("SUPABASE_DB_URL")
83
+ if not DSN:
84
+ sys.exit("Set SUPABASE_DB_URL to the local stack's DB_URL (supabase status)")
107
85
  ```
108
86
 
109
- 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.
110
-
111
- 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.
87
+ The rule is three lines of Markdown in the repo. No linter knows what a Supabase DSN looks like in a default argument; Jev does, and the agent got the rule text, not a red X.
112
88
 
113
- 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.
89
+ ### It held a database reset, and the agent finished everything else first
114
90
 
115
- 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.
91
+ A feature branch with a migration. The agent decided to replay the local database. `supabase db reset` is legitimate in that repo, but it wipes local state and nobody had asked for it.
116
92
 
117
- 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.
93
+ ```text
94
+ assistant
95
+ [bash] cd .claude/worktrees/con-142 && supabase db reset 2>&1 | tail -6
118
96
 
119
- 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.
97
+ tool result (written by pi-warden; the command did not run)
98
+ pi-warden held this bash call before it ran: irreversible 0.85. Do not retry it
99
+ unchanged. Either (1) reach the goal with a recoverable alternative that stays
100
+ inside the project (a targeted path, a dry run, a move instead of a delete, a
101
+ normal push), or (2) if this exact action is genuinely required, stop and tell
102
+ the user in one or two sentences what it does, what cannot be undone, and why
103
+ it is needed, then wait for their reply. [...the template continues]
120
104
 
121
- 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).
105
+ assistant
106
+ The warden held `supabase db reset`. It's the sanctioned local-stack replay
107
+ (loopback docker only, no dev/prod reach), but I'll pause that step and ask at
108
+ the end of this update. Continuing with lanes that don't need it:
109
+ [bash] uv run pytest tests/canon tests/services/messaging ... 92 passed in 24.80s
122
110
 
123
- ### Slop
111
+ assistant (final message, about 16 tool calls later)
112
+ Draft PR is up, work committed and pushed. One decision is pending for you.
113
+ PR: redacted#2640 (draft) ...
114
+ ```
124
115
 
125
- **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.
116
+ No dialog, no interruption. The agent moved the risky step to the end and asked in one sentence.
126
117
 
127
- 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).
118
+ ## How it decides
128
119
 
129
- **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.
120
+ Every guarded call (`bash`, `write`, `edit`, and shell-like tools such as context-mode's `ctx_execute`) goes through four steps:
130
121
 
131
- ### Security
122
+ 1. **Read-only? Skip.** `git status && ls` or a `read`: no request, no line in the trace.
123
+ 2. **Pattern list, offline.** Force push, `git reset --hard`, recursive `rm` outside the project, SQL `DROP`/`TRUNCATE`/`DELETE FROM`, `curl | sh`, publishing, infrastructure destroys: the call is held. Risky but recoverable things (`rm -rf` on a project path, `sudo`, `--no-verify`, `gh pr merge`, touching `.env`) warn. Text that is data, such as a commit message that mentions `git push --force`, is not a command.
124
+ 3. **Jev, with your consent.** One request carries your request, up to eight earlier messages, the agent's own words right before the call (its plan), and the call itself. Jev answers fixed questions with probabilities: irreversible, off task, does it change anything, how it relates to the task, does it contradict the plan, is the effect visible outside the working tree. On a write the slop and security questions ride along, and the rules check is a second request in parallel. About 250 ms before the tool runs.
125
+ 4. **Act.** Only two things hold a call: a destructive pattern, or `irreversible` at 0.7 or above. Everything else (off task, plan mismatch, slop, a rule violation, a security smell) tells the agent and lets the call run.
132
126
 
133
- 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.
127
+ In the default `steer` mode a hold goes back to the agent as its tool result with two acceptable next moves: find a recoverable way, or explain the action to you in a sentence and wait. If your reply approves it, the retry goes through. `confirm` mode shows you a dialog instead; `advise` never holds.
134
128
 
135
- 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.
129
+ Three rules hold all of this together. Patterns set the floor and Jev can only raise it. The agent's plan can add a nudge but never remove a hold. Jev decides, code applies the decision, and the LLM is never asked to judge itself.
136
130
 
137
- ### Stuck
131
+ **Why Jev and not a second LLM call.** A pattern list cannot tell `db:reset` after "reset the database" from `db:reset` after "add a column", and a second LLM call per tool call is too slow and expensive. Jev returns calibrated probabilities to fixed questions in about 250 ms for a fraction of a cent, cheap enough for every guarded call. The full argument, and how large a request gets, is in [docs/guards.md#why-jev](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md#why-jev-and-not-a-second-llm-call).
138
132
 
139
- 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.
133
+ ## What the numbers say
140
134
 
141
- ### Runaway
135
+ `node scripts/calibrate-action.mjs --all` replays every guarded call in your recorded Pi sessions through the guard and asks Jev, once per turn, whether your next message approved a hold, regretted a call that ran, or rejected the turn. On 321 of my own sessions (17,160 guarded calls): **42 holds, and my next message approved 5**, so 37 stood; **20 regretted calls (2% of turns)**, all scope rather than data loss; and off-task caused 56 of the 139 replay holds without drawing a complaint, so it now steers and never holds.
142
136
 
143
- 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.
137
+ The labelling never stops: what you do after each hold (approve, decline, redirect, complain) becomes a label, and `/warden status` shows hold precision for your own sessions. The signal ranking, the intent threshold, the candidate questions that lost, and what 67 live steers did to the agent are in [docs/guards.md#calibration](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md#calibration).
144
138
 
145
- ### Done-check
139
+ ## Questions people asked
146
140
 
147
- 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.
141
+ **How is this different from putting the rules in AGENTS.md?**
142
+ An `AGENTS.md` is read once and asks the model to remember. Long sessions, compaction, and cheap models forget. pi-warden re-reads your rules file when it changes and judges every write against it at the moment it happens, then quotes the rule the agent broke. The rules can be things a linter cannot check: "exported functions declare return types", "a TODO must name a ticket", "comments must not restate the code". Keep the AGENTS.md; this is the check behind it.
148
143
 
149
- ### Context saver
144
+ **Do I need a TypeSafe key?**
145
+ Only for Jev judgments. Without one you still get the pattern list, the runaway stop, duplicate-output notes, sensitive-path notes, and credential-shape warnings. Keys come through a waitlist at [console.typesafe.ai](https://console.typesafe.ai); install now and run `/warden enable` when yours arrives.
150
146
 
151
- 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.
147
+ The other answers (regex, Claude Code's auto mode, cost, permissions, what is sent to TypeSafe, project overrides, headless CI, and the size of a Jev request) are in [docs/faq.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/faq.md).
152
148
 
153
- - **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.
154
- - **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.
155
- - **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`.
156
- - **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.
149
+ ## Rules for your project
157
150
 
158
- Set `context.enabled: false` to turn it off. Full-output files can contain secrets and stay in the OS temporary directory until removed.
151
+ Put `pi-warden.md` at the project root. Each heading is one rule; the text under it is the specification. An optional `paths:` line limits the rule to matching files.
159
152
 
160
- ### Desktop notifications
153
+ ```markdown
154
+ # No console statements
155
+ Code must not contain `console.log` or `console.debug`. Use the logger.
161
156
 
162
- Off by default. With `"notify": { "enabled": true }` in your config, 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. `"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.
157
+ # Exported functions must have explicit return types
158
+ paths: src/**/*.ts
159
+ Every exported function declares its return type.
160
+ ```
163
161
 
164
- ### Steer messages
162
+ On every write and edit, Jev judges the change against each rule: compliant, violation, not applicable, or insufficient context. A violation at 0.7 or above is quoted back to the agent (heading, rule text, and which edit); the write goes through, because a held write would leave a half-written file. The third hit of one rule in a session is called out as a standing rule, and files are re-read when they change.
165
163
 
166
- 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.
164
+ Without a `pi-warden.md`, the first of `README.md`, `CLAUDE.md`, `AGENTS.md` is judged as one document instead, so existing context files work from day one. Starter file with a dozen rules: [`examples/pi-warden.md`](examples/pi-warden.md). Path globs, sensitive-path notes, and the tuning set: [docs/guards.md#rules](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md#rules).
167
165
 
168
166
  ## Commands
169
167
 
170
- | Command | Effect |
171
- | --- | --- |
172
- | `/warden status` | Guard state, consent and key source, session counts, thresholds, hold precision and feedback log, rules source, config paths, last verdicts |
173
- | `/warden enable` | Data notice, key prompt if none is stored, consent saved |
174
- | `/warden disable` | Stop Jev judgments; pattern checks continue |
175
- | `/warden mode steer\|confirm\|advise` | How holds are handled; without an argument, show the current mode |
176
- | `/warden config` | Edit the user config JSON in Pi's editor |
177
- | `/warden test` | One synthetic destructive action, its verdict, and what the agent would be told |
178
- | `/warden trace` | Toggle the trace sidebar (or print the last 20 events without a UI) |
179
-
180
- ## Status line and trace sidebar
181
-
182
- 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.
183
-
184
- Clicks and the wheel need Pi's fullscreen mode (`tuiMode: "fullscreen"` in `/settings`). In macOS Terminal.app enable View → Allow Mouse Reporting.
185
-
186
- Templates in `config.widget` control the text. Segments are separated by ` · `; a segment whose token has no value is dropped:
187
-
188
- ```json
189
- "widget": {
190
- "action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
191
- "rules": "warden · rules · {tool} {path} · {asked} rules · {violations} · {status}",
192
- "stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
193
- "done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
194
- "prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
195
- "security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
196
- "context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes",
197
- "runaway": "warden · runaway · {kind} · {count}× repeated · {chars} chars · {signal} · {status}"
198
- }
199
- ```
200
-
201
- Tokens per guard: action `tool level source irreversible offTask scope approved intent visible plan 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.
168
+ Seven commands: `/warden status`, `enable`, `disable`, `mode steer|confirm|advise`, `config`, `test`, `trace`. `status` reports guard state, session counts, steer counts per guard, thresholds, hold precision, and config paths; `trace` toggles the sidebar with the exact text the agent received. The full table is in [docs/commands.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/commands.md).
202
169
 
203
170
  ## Configuration
204
171
 
205
- User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use these defaults:
206
-
207
- ```json
208
- {
209
- "enabled": true,
210
- "typesafe": false,
211
- "mode": "steer",
212
- "timeoutMs": 5000,
213
- "maxRequests": 500,
214
- "action": {
215
- "enabled": true,
216
- "tools": ["bash", "powershell", "ctx_execute", "ctx_batch_execute", "ctx_execute_file", "write", "edit"],
217
- "failOpen": true,
218
- "irreversible": { "warn": 0.5, "confirm": 0.7 },
219
- "offTask": { "warn": 0.6, "steer": 0.85 },
220
- "intentMismatch": 0.9,
221
- "visibleMismatch": 0.8,
222
- "feedbackLog": true
223
- },
224
- "rules": {
225
- "enabled": true,
226
- "threshold": 0.7,
227
- "files": [],
228
- "fallback": true,
229
- "maxChars": 8000,
230
- "exclude": [],
231
- "skip": [],
232
- "sensitivePaths": {}
233
- },
234
- "slop": {
235
- "enabled": true,
236
- "threshold": 0.7,
237
- "prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
238
- },
239
- "security": { "enabled": true, "threshold": 0.7 },
240
- "stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
241
- "done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
242
- "context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
243
- "runaway": { "enabled": true, "repeats": 4, "thinkingRepeats": 10, "minChars": 400, "recover": true },
244
- "notify": { "enabled": false, "cooldownMs": 10000, "command": [] },
245
- "widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w", "panelWidth": "40%" },
246
- "steerVisible": false
247
- }
248
- ```
249
-
250
- 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`.
251
-
252
- A wince-style setup for a backend repo (the full version is [`examples/pi-warden.json`](examples/pi-warden.json)):
253
-
254
- ```json
255
- {
256
- "rules": {
257
- "skip": ["tests/**", "**/*.test.*", "docs/**", "**/*.md"],
258
- "exclude": ["secrets/**", "**/*.pem"],
259
- "sensitivePaths": {
260
- "migrations/**": "This touches a migration: tell the user and add a rollback path",
261
- "**/permissions*": "Access control changed: ask the user for a security review before merging"
262
- }
263
- }
264
- }
265
- ```
266
-
267
- ## Data handling
268
-
269
- With consent, requests go to `https://api.typesafe.ai`. What is sent:
270
-
271
- - **Action guard**: your latest prompt (1500 characters), up to eight earlier user and assistant messages (750 redacted characters each), the agent's text from the message that makes the call (500 redacted characters), 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`. On the first guarded call after your reply, the tool names and commands (300 characters) or paths of up to six calls allowed in the previous turn, for the regret question.
272
- - **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.
273
- - **Stuck**: the last 12 tool calls (300 characters each) with 400-character output tails.
274
- - **Done-check and prose**: the agent's final message (2000 and 2500 characters), the run's check commands, the audience description.
275
- - **Output checks**: a redacted head/tail sample up to 6000 characters plus size, line counts, and tool name.
276
- - **Nothing** for duplicate detection, the runaway guard, sensitive-path notes, or pattern checks.
172
+ Two optional files: your defaults in `~/.pi/agent/pi-warden/config.json` (`/warden config` opens it) and per-repo overrides in `.pi/pi-warden.json`. Common tweaks are `mode`, `steerVisible`, `notify.enabled`, `slop.prose.audience`, `rules.skip`, and `context.enabled`. Every key with its default, what a project file may and may not change, and the environment variables: [docs/configuration.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/configuration.md). Worked examples of all three files: [examples/README.md](examples/README.md).
277
173
 
278
- The hold feedback log under `~/.pi/agent/pi-warden/holds/` stays on this machine, owner-only: one JSON line per judged call with tool, pattern ids, scores, level, mode, outcome, and the length of the agent's stated plan; never the command, path, prompt, or plan text.
174
+ ## Status line and trace
279
175
 
280
- 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.
281
-
282
- ## After updating the package
283
-
284
- Restart Pi after an update; `/reload` re-imports the entry module but can leave older modules of the same package in memory. Since 0.5.2 the extension checks the shape of the config it receives; a section that an older module does not know (the symptom of two package versions in one process) switches that guard off and prints one warning naming the sections and the schema numbers. 0.9.0 crashed instead when the shape-check module itself was the stale one; since 0.9.1 the extension guards the sections it reads in its own module, so the warning appears and everything else keeps working. If you see the warning, restart Pi.
176
+ 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`, or a click on the line opens a right-hand sidebar with the full trace, newest first. Clicks need Pi's fullscreen mode (`tuiMode: "fullscreen"` in `/settings`). Widget templates and placement: [docs/configuration.md#status-line-and-trace-sidebar](https://github.com/DevMortimer/pi-warden/blob/main/docs/configuration.md#status-line-and-trace-sidebar).
285
177
 
286
178
  ## For extension authors
287
179
 
288
- Every guard is a plain function you can call with any object that has pi-typesafe's `evaluate` method as the judge:
180
+ Every guard is a plain function you can call with any object that has pi-typesafe's `evaluate` method as the judge, and the library has no dependency on Pi's runtime, so it is safe in tests:
289
181
 
290
182
  ```ts
291
- import { evaluateAction, evaluateRules, RuleStore, defaultConfig } from "pi-warden";
183
+ import { evaluateAction, defaultConfig } from "pi-warden";
292
184
  import { createTypeSafe } from "pi-typesafe";
293
185
 
294
- const judge = createTypeSafe();
295
186
  const verdict = await evaluateAction(
296
187
  { tool: "bash", input: { command: "git push --force" }, cwd: process.cwd(), task: "push my branch" },
297
- { config: defaultConfig().action, judge }, // omit judge for pattern checks only
188
+ { config: defaultConfig().action, judge: createTypeSafe() }, // omit judge for pattern checks only
298
189
  );
299
- verdict.level; // "allow" | "warn" | "confirm" (confirm = hold in steer mode)
300
- verdict.reasons; // ["destructive: git force push", "irreversible 0.91"]
301
-
302
- const set = new RuleStore().load(process.cwd(), defaultConfig().rules);
303
- const rules = await evaluateRules("write", { path: "src/a.ts", content: "console.log(1)" }, { cwd: process.cwd(), config: defaultConfig().rules, set, judge, timeoutMs: 5000 });
304
- rules.findings; // [{ id: "no-console-statements", name, violation: 0.97, body }]
305
190
  ```
306
191
 
307
- 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.
192
+ The rules guard, the session-level `ActionGuard` and `RulesGuard`, and the other exports are in [docs/extension-authors.md](https://github.com/DevMortimer/pi-warden/blob/main/docs/extension-authors.md).
308
193
 
309
194
  ## Development
310
195
 
311
- ```bash
312
- npm install
313
- npm run check # typecheck, offline tests (mocked transport), build
314
- npm run test:live # billable synthetic judgments across the guards; pass action|intent|slop|approval|regret|stuck|done|security|context for one group
315
- node scripts/rules-cases.mjs # 13 billable cases against an 8-rule fixture file
316
- node scripts/slop-cases.mjs # 22 billable cases for the slop and prose questions
317
- node scripts/security-cases.mjs # 9 output-security and task-continuity cases
318
- node scripts/context-cases.mjs # 15 labelled outputs for retention and format
319
- node scripts/calibrate-action.mjs --dry-run # action-guard calibration on your recorded sessions; drop --dry-run to spend the requests
320
- npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
321
- npm run preview # re-render docs/preview.png (needs a Chrome binary)
322
- ```
196
+ `npm install`, then `npm run check` (typecheck, offline tests on a mocked transport, build). `npm run test:live` spends real judgments across the guards; `node scripts/calibrate-action.mjs --dry-run` estimates a calibration run on your own sessions; `npm run dev:pi` starts Pi with this working tree. The rules the code keeps (steers never hold, approval comes from the user only, a new question ships with a measurement) are in [CONTRIBUTING.md](CONTRIBUTING.md).
197
+
198
+ ## Credits
199
+
200
+ Rules as headings and the four outcomes come from [jevrealtimecodecheck](https://github.com/MrDesjardins/jevrealtimecodecheck); path globs, sensitive paths, and "judge the change, not its story" from [wince](https://github.com/TinyFrontier/wince).
323
201
 
324
202
  ## License
325
203
 
package/dist/config.d.ts CHANGED
@@ -123,6 +123,16 @@ export interface NotifyConfig {
123
123
  */
124
124
  command: string[];
125
125
  }
126
+ export interface SubagentConfig {
127
+ /** Read async subagent reports at all. Off: warden ignores them, as before 0.14. */
128
+ enabled: boolean;
129
+ /** Ask Jev whether a report that names trouble deserves a wake. Off keeps the offline layer, which never wakes. */
130
+ wake: boolean;
131
+ /** P(this report needs the agent awake) at or above this value wakes it. Conservative on purpose. */
132
+ threshold: number;
133
+ /** At most one wake per this window, so several children finishing together cost one interruption. */
134
+ cooldownMs: number;
135
+ }
126
136
  export type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
127
137
  export declare function isRecallTool(value: unknown): value is RecallTool;
128
138
  export type WardenMode = "steer" | "confirm" | "advise";
@@ -150,6 +160,8 @@ export interface WardenConfig {
150
160
  context: ContextConfig;
151
161
  runaway: RunawayConfig;
152
162
  notify: NotifyConfig;
163
+ /** Triage of async subagent reports: Jev separates what needs the agent awake from what is only context. */
164
+ subagent: SubagentConfig;
153
165
  /** The status line above the editor and the trace panel. */
154
166
  widget: WidgetConfig;
155
167
  /** Show steer messages in the transcript. They are always visible in the trace panel. */
@@ -157,7 +169,7 @@ export interface WardenConfig {
157
169
  }
158
170
  export declare const PACKAGE_NAME = "pi-warden";
159
171
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
160
- export declare const CONFIG_SCHEMA = 5;
172
+ export declare const CONFIG_SCHEMA = 6;
161
173
  export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
162
174
  export declare function defaultConfig(): WardenConfig;
163
175
  /** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
package/dist/config.js CHANGED
@@ -9,7 +9,7 @@ export function isRecallTool(value) {
9
9
  }
10
10
  export const PACKAGE_NAME = "pi-warden";
11
11
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
12
- export const CONFIG_SCHEMA = 5;
12
+ export const CONFIG_SCHEMA = 6;
13
13
  export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
14
14
  export function defaultConfig() {
15
15
  return {
@@ -37,6 +37,7 @@ export function defaultConfig() {
37
37
  context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7 },
38
38
  runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
39
39
  notify: { enabled: false, cooldownMs: 10000, command: [] },
40
+ subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
40
41
  widget: defaultWidgetConfig(),
41
42
  steerVisible: false,
42
43
  };
@@ -141,6 +142,12 @@ function applyNotify(base, raw, allowCommand) {
141
142
  ? [...raw.command] : base.command;
142
143
  return { enabled: boolean(raw.enabled, base.enabled), cooldownMs: cooldown, command };
143
144
  }
145
+ function applySubagent(base, raw) {
146
+ if (!isObject(raw))
147
+ return base;
148
+ const cooldown = typeof raw.cooldownMs === "number" && Number.isSafeInteger(raw.cooldownMs) && raw.cooldownMs >= 0 ? raw.cooldownMs : base.cooldownMs;
149
+ return { enabled: boolean(raw.enabled, base.enabled), wake: boolean(raw.wake, base.wake), threshold: probability(raw.threshold, base.threshold), cooldownMs: cooldown };
150
+ }
144
151
  function applyDone(base, raw) {
145
152
  if (!isObject(raw))
146
153
  return base;
@@ -201,6 +208,7 @@ function applyWidget(base, raw) {
201
208
  context: template(raw.context, base.context),
202
209
  runaway: template(raw.runaway, base.runaway),
203
210
  rules: template(raw.rules, base.rules),
211
+ subagent: template(raw.subagent, base.subagent),
204
212
  };
205
213
  }
206
214
  /** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
@@ -215,6 +223,7 @@ function applyGuards(base, raw, timeoutMs, source) {
215
223
  return {
216
224
  rules: applyRules(base.rules, raw.rules),
217
225
  runaway: applyRunaway(base.runaway, raw.runaway),
226
+ subagent: applySubagent(base.subagent, raw.subagent),
218
227
  // A project file may switch notifications off or on, but never names a command to run.
219
228
  notify: applyNotify(base.notify, raw.notify, source === "user"),
220
229
  action: applyAction(base.action, raw.action, timeoutMs),
package/dist/done.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface RunEvidence {
18
18
  call: string;
19
19
  passed: boolean;
20
20
  }>;
21
+ checksBeforeMutation?: number;
21
22
  }
22
23
  export declare function emptyEvidence(): RunEvidence;
23
24
  export declare function recordOutcome(evidence: RunEvidence, outcome: ToolOutcome, input: Record<string, unknown>, tool?: string): void;
@@ -28,7 +29,9 @@ interface MessageLike {
28
29
  }
29
30
  /** Text of the run's final assistant message, when it ended normally with text (not a tool call, error, or abort). */
30
31
  export declare function finalAssistantText(messages: ReadonlyArray<MessageLike>): string | undefined;
31
- /** The check only makes sense when something changed and nothing proved it works. */
32
+ /** The checks that ran after the latest change: only those ran on the code as it stands now. Earlier ones stay as history. */
33
+ export declare function freshChecks(evidence: RunEvidence): RunEvidence["checks"];
34
+ /** The check only makes sense when something changed and nothing proved that change works. */
32
35
  export declare function needsDoneCheck(evidence: RunEvidence): boolean;
33
36
  export declare const doneQuestions: {
34
37
  claims_done: import("pi-typesafe").NoulQuestion;