pi-warden 0.14.0 → 0.23.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/CHANGELOG.md ADDED
@@ -0,0 +1,155 @@
1
+ # Changelog
2
+
3
+ Notable changes to pi-warden, newest first. Versions follow semver. The published surface is `dist/` plus `README.md`; changes under `eval/`, `scripts/`, and `docs/` are repo tooling and ride along with the next release.
4
+
5
+ How to keep this current: add the entry in the same pull request as the change, under `Unreleased`. The release commit renames `Unreleased` to the version it ships and adds its own notes. Entries before 0.10.0 are one-line summaries taken from the release commit headers; the detail for those is in `git log`.
6
+
7
+ ## 0.23.0
8
+
9
+ ### Added
10
+
11
+ - Same-target churn detection (WARDEN-LOOP-2). The stuck guard now catches repeated calls to the same tool and input where the output changes each time — polling a command that returns a different result every run, or cycling through slight variations of the same call. `churnThreshold` (default 5) sets how many calls to the same target trigger the verdict; it is part of `StuckGuardConfig` and configurable like the other stuck-guard fields. The widget shows `churn · stuck`, and the nudge tells the agent to act on the latest result or switch targets.
12
+
13
+ ## 0.22.0
14
+
15
+ ### Changed
16
+
17
+ - Status line design pass (WARDEN-TUI-1, the widget half). The verdict leads each line as a bold colored chip (`WARN`, `STUCK`, `UNVERIFIED`) instead of hiding at the end; the redundant `warden ·` prefix is gone; the guard follows in muted and the body reads as data (subject in the text tone, labels muted, numeric values bright) — the same palette and vocabulary as the 0.19.0 sidebar pass.
18
+ - Verdicts the guard found nothing in (`ok`, `allow`, `skipped`) fold into one line per verdict, naming the guards that spoke: six guards firing in one turn cost one line, not six, and the folded scores stay in the sidebar. A quiet verdict keeps its own line when the line names a finding or a caveat — `typesafe error`, `user approved`, `slop: <symptom>`, `patterns: <id>` — because folding it would report a verdict the guard did not give. The worst verdict sits last, nearest the editor.
19
+ - The folded line is a display choice, not data loss: `/warden status` prints the raw line per guard under `Last:`, and the trace sidebar keeps every event with its scores. Templates still work; a template that keeps `{level}` or `{status}` mid-line has no verdict to lead with, so the guard name leads the line instead.
20
+
21
+ ## 0.21.0
22
+
23
+ ### Added
24
+
25
+ - Per-block retention for multi-block tool results (WARDEN-CTX-10). A result made of several parts (text plus images, or several text blocks) used to skip compression entirely; now each text block at or above `context.tailMinChars` earns its own retention request and its own excerpt, with its full text stored separately. Block order and non-text parts are untouched, and a credential or injection banner lands on the block that earned it instead of wrapping the first and last text block. Blocks below the threshold keep their text and still get the offline credential scan; `mergeOutput` gives the session bookkeeping (secret dedup, trace scores) the worst signal across blocks.
26
+
27
+ ## 0.20.0
28
+
29
+ ### The end-of-task restatement loop
30
+
31
+ A closing run used to collect a notice per guarded call (intent mismatch, credentials, off-task), and each delivered notice cost the agent one more LLM turn, which it filled by restating the final status. Six notices, six "CON-375 is complete" replies. The 0.16 wording caps and the 0.17 stuck-guard repeats could not touch this: the disease is not one reply, and not one tool call.
32
+
33
+ ### Added
34
+
35
+ - `"steerBudget": 3` (default): steers delivered to the agent per run before further non-critical ones are recorded in the trace only. A notice skipped for the budget keeps its chance: the same notice can deliver on the next run. Critical guards (stuck, done, runaway recovery, subagent wake) always deliver, because their message starts the turn it asks for. `0` disables the budget.
36
+ - Restatement measurement (code only, no request): at the end of a run the final message is compared with the run's earlier final messages (`RestatementWindow`, `restatedShare` in `src/prose.ts`). A reply whose substantive sentences mostly restate an earlier reply of the same run is counted in the trace, the widget, and `/warden status`; it is never steered, because a nudge cannot retract the reply and would cost the turn it warns against. The window resets with each user prompt, so answering you is never a restatement.
37
+ - `/warden status` reports the run's restatement count and the steer budget next to the steer counts.
38
+
39
+ ### Changed
40
+
41
+ - A repeated steer is no longer re-sent, not even as the one-line reminder: the reminder itself cost the accounting turn it forbade. The first copy is already in the agent's context; the trace says `steer recorded, not delivered` and carries the text. The delivery result is now known to the trace, so a notice the budget or a repeat swallowed no longer claims `agent told:`.
42
+ - `repeatSteer` is removed from the public API (`src/index.ts`); `SteerRepeatWindow` and `steerFingerprint` stay.
43
+
44
+ ### Tests
45
+
46
+ - `tests/prose.test.ts` pins the restatement share (paraphrase restates, fresh information does not, one-line acknowledgements never count, the window resets per prompt).
47
+ - `tests/extension.test.ts` pins: a repeated notice is recorded only; the per-run budget records further notices and refills on the next prompt; critical guards deliver past the spent budget; a restating final reply is counted without steering.
48
+
49
+ ## 0.19.0
50
+
51
+ ### Changed
52
+
53
+ - Trace sidebar design pass. The verdict leads each entry as a bold colored chip (`ALLOW`, `STUCK`, `UNVERIFIED`) instead of hiding at the end of the line; the redundant `warden ·` prefix is gone; the body reads as data (subject in text tone, labels muted, numeric values bright); details stay dim under the entry; the header rule uses the border color. Same palette, same separators, same rail — hierarchy instead of new primitives.
54
+
55
+ ## 0.18.0
56
+
57
+ ### Changed
58
+
59
+ - A warn-level verdict no longer dies with the UI. In a headless run (`ctx.hasUI` false) the warn reasons go to the agent as a steer — the only reader available — instead of reaching nobody. Interactive sessions keep the UI notice with no extra steer. Decision on WARDEN-ACT-1: delivery to the agent, not gating on `steerVisible`, which is a display option.
60
+
61
+ ## 0.17.0
62
+
63
+ ### Changed
64
+
65
+ - The stuck guard now sees successful loops. A call that succeeds but prints the same normalised output `minFailures` times (the poll that keeps answering "all checks passed") reaches a repeat verdict in code, with its own nudge: act on the answer already in hand instead of re-running. `AttemptWindow.successRepeats` drives it; the widget flags the verdict `successful repeat`.
66
+
67
+ ## 0.16.0
68
+
69
+ ### Added
70
+
71
+ - `"notices": false` (default): the per-call yellow warnings (`warden · …`) no longer print in the transcript. The widget, the trace sidebar, and `/warden trace` still show every event; set `"notices": true` in the user config to print them again.
72
+
73
+ ### Changed
74
+
75
+ - Steers bound the reply they ask for. The intent-mismatch and off-task steers now cap the acknowledgement at one short sentence and say not to restate session state, so a notice costs a line instead of an end-of-task essay.
76
+ - An unchanged steer is sent once. When the same notice (scores and counts ignored) repeats inside the last three steers, the agent gets a one-line reminder that nothing new is owed instead of the full text.
77
+ - Jev calls go through pi-typesafe's `ask`. `src/jev.ts` is removed: it was the same function plus a usage field. `tests/ask.test.ts` pins what the guards rely on, one signal for the per-guard timeout and the caller's abort, and failures that carry no upstream text.
78
+ - `/warden status` and the judge path consult `authState().usable` and `describeAuth()` instead of `resolveApiKey()`. An enabled warden with no usable key used to report "enabled" while silently running pattern checks only.
79
+ - `scripts/calibrate-action.mjs` takes AUC, the threshold grids, and the recommendation from `pi-typesafe/calibrate`, counters from `getSpend()`, and the price from `DEFAULT_USD_PER_MTOK`. Its session replay, labelling, and the two-parameter hold-rule report stay local.
80
+ - Dependency: `pi-typesafe` 0.3.0 to 0.5.0.
81
+
82
+ ### Benchmarks (repo tooling, not in the npm package)
83
+
84
+ - `eval/fixture` v2 with ten rules, five of which no model can know from training, and every scored rule announced to both cells. `bare-todo` and `console-log` were scored before without ever being told.
85
+ - 15 tasks in four families (rules, project-only, verification, blast-radius) plus a 12-turn decay arc. Each task ships the files that make its shortcut tempting.
86
+ - `eval/verify.mjs` scores two axes independently of the guard: the final reply's claims against the checks the runner itself runs, and unasked visible actions from the tool calls plus the run's git state.
87
+ - `eval/env.mjs` drops credential-named variables whose value pi-warden itself would flag, so an environment dump cannot reach a model. Two of fifty deepseek runs had leaked a real-format token before this.
88
+ - Runner: `--concurrency` (each run has its own temp project and agent dir), `--turns N` for an arc in one session, a local bare `origin` so a push is observable, and a refusal to start without an explicit `--model` unless `--yes` is passed, because a batch spends real tokens.
89
+ - `scripts/eval-rescore.mjs` re-scores a finished batch against its own evidence when a checker is corrected, so a fix costs no model spend.
90
+ - Measured over 150 paired runs: 6 control runs with a rule violation against 0 warden runs; no false claims in either cell; no hold delta on blast radius; no rule-violation growth over the 12-turn arc. Reports and the checker corrections: [eval/reports/](eval/reports/README.md).
91
+
92
+ ### Docs
93
+
94
+ - README leads with the measured numbers, adds "why a strong model does not make this pointless", and publishes what the benchmark does not show yet. No em-dashes.
95
+
96
+ ## 0.14.0
97
+
98
+ - Credential notices trace fixture-shaped values instead of announcing them (`syntheticish`), while a real-shaped value is still reported once.
99
+ - `/warden status` counts steers per guard, so the cost of the guard is visible per session.
100
+ - Subagent triage: offline first, only a report that names trouble reaches Jev, one batched wake per `subagent.cooldownMs` (120 s) sent as a pointer that names the report, and a failed request stays quiet.
101
+ - README restructured around the guard table; the eval report generator stopped writing em-dashes.
102
+
103
+ ## 0.13.0
104
+
105
+ - `npm run eval:ab`: a repeatable with/without benchmark. Each run gets an isolated `PI_CODING_AGENT_DIR` with exactly one extension, so the cells differ by pi-warden alone; a mechanical checker over the final diff shares no code with the guard.
106
+ - Rules observability: rules verdicts, silent allows included, land in the hold log with per-rule scores.
107
+ - Per-value credential dedup: warnings keyed on the fingerprint of the set of secrets in an output.
108
+ - Published negatives: the hermetic task showed no reliable effect, and the warden-cell test failures were model variance on the retry task where the guard stayed silent.
109
+
110
+ ## 0.12.0
111
+
112
+ - Off-task never holds. On 17k recorded calls it made 40% of the holds and caught nothing the irreversible score missed.
113
+ - A visible action (commit, push, merge, publish, launch) that departs from the agent's plan warns; git bypass flags and PR merges warn.
114
+ - The credentials notice needs a value shape, so `secret: boolean` and bare env-var names no longer fire.
115
+ - Desktop notifications are off by default.
116
+ - `scripts/calibrate-action.mjs --extra` measures candidate questions on your recorded sessions.
117
+
118
+ ## 0.11.1
119
+
120
+ - The intent steer threshold moves from 0.8 to 0.9, chosen on 17k recorded calls: a quarter of the volume with two to three times the precision.
121
+ - `scripts/calibrate-action.mjs` replays recorded sessions through the guard and labels each turn by what the user did next.
122
+
123
+ ## 0.11.0
124
+
125
+ - The agent's own words before a call travel with the request as `plan`. A call that differs materially from the plan steers at `intent_mismatch` 0.8 when it is visible.
126
+ - Trace shows the plan and the score; the hold log records `planChars`.
127
+
128
+ ## 0.10.0
129
+
130
+ - Hold feedback loop: every held call is labelled by what the user does next (approved, declined, redirected, complained), and `/warden status` shows holds per outcome with precision over the labelled ones.
131
+ - Live regret cases: 5 of 5 as expected.
132
+
133
+ ## Earlier releases
134
+
135
+ One line each, from the release commit headers.
136
+
137
+ - 0.9.1 survive a stale shape module after an update; data text with substitutions elsewhere is still data
138
+ - 0.9.0 project rules judge written code; destructive text that is data no longer holds a command
139
+ - 0.8.0 runaway guard stops a looping reply; desktop notifications when the agent needs you
140
+ - 0.7.0 context saver drops duplicate results, keeps exact lines per output format, and points recalls at a search
141
+ - 0.6.0 a budget error from any guard stops further requests; the Action guard owns hold and approval
142
+ - 0.5.5 judge sibling tool calls together; overlap the end-of-turn checks
143
+ - 0.5.4 approval applies to the action, not the exact command string
144
+ - 0.5.3 measure the context saver
145
+ - 0.5.2 off-task holds only actions that change something; survive a mixed module graph
146
+ - 0.5.1 non-modal trace sidebar
147
+ - 0.5.0 tool-output security, tail compression, task context for scope
148
+ - 0.4.0 per-symptom code slop, prose slop against an audience, hidden steers, repeat escalation
149
+ - 0.3.0 configurable status line templates, session trace, and a live trace panel
150
+ - 0.2.2 guard context-mode `ctx_execute*` and powershell, and count checks run through them
151
+ - 0.2.1 README explains the slop feedback loop
152
+ - 0.2.0 steer mode by default, stuck-loop detector, done-check, slop notes, chat approval of held calls
153
+ - 0.1.2 README leads with real verdicts and the generated preview image
154
+ - 0.1.1 `/warden enable` prompts for and stores a key; `/warden test` shows the confirm dialog as a demo
155
+ - 0.1.0 first release: the action guard with pattern checks plus Jev judgments before bash, write, and edit
package/README.md CHANGED
@@ -1,24 +1,14 @@
1
1
  # pi-warden
2
2
 
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.
3
+ Your `AGENTS.md` is a request. pi-warden is the check behind it.
4
4
 
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
-
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
-
9
- ## In one minute
10
-
11
- ```bash
12
- pi install npm:pi-warden
13
- ```
5
+ On every write and edit, [Jev](https://typesafe.ai) judges the change against the rules in your `pi-warden.md` and quotes the broken rule 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, triages async subagent reports, and holds the rare destructive command. You keep working. The agent gets a nudge instead of you getting a dialog.
14
6
 
15
- Then, inside Pi:
7
+ Measured: 150 paired headless runs broke a project rule **6 times with pi-warden off and 0 times with it on**. On 321 of my own sessions (17,160 guarded calls) the action guard held 42 calls and my next message approved 5, so 37 stood. A judgment costs about 1,000 input tokens (roughly $0.00004) and lands in about 0.3 s, which is why it can fire on every guarded call.
16
8
 
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.
9
+ ![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)
20
10
 
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.
11
+ 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).
22
12
 
23
13
  ## What it watches
24
14
 
@@ -32,29 +22,57 @@ Requires Pi 0.85 or newer and Node.js 22.19 or newer. Without a key, the offline
32
22
  | **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
23
  | **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
24
  | **Runaway** | the reply stream | stops a reply that repeats the same block over and over (code only, no request) |
25
+ | **Steer budget** | the agent's attention | bounds the steers one run can demand: repeats and notices past the budget are recorded in the trace only, and a final reply that mostly restates an earlier reply of the same run is counted, never steered |
35
26
  | **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)) |
36
27
  | **Notifications** (opt-in) | moments that need you | desktop notification for a held call, a confirm dialog, a runaway stop |
37
28
 
38
29
  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).
39
30
 
40
- ## Does it improve the code?
31
+ ## In one minute
32
+
33
+ ```bash
34
+ pi install npm:pi-warden
35
+ ```
36
+
37
+ Then, inside Pi:
38
+
39
+ 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).
40
+ 2. `/warden test` shows one synthetic verdict and what the agent would be told.
41
+ 3. Work as usual. The line above the editor shows the latest verdict, folding the guards that found nothing into one line; `ctrl+shift+w` opens the trace with the scores and the exact text the agent received.
42
+
43
+ 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.
41
44
 
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.
45
+ ## Does it improve the code?
43
46
 
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).
47
+ **Short answer: the warden cell did not break a rule in 150 paired runs. The control cells did, in 6 of them.**
45
48
 
46
- Results so far, 50 runs per model (5 tasks, both cells, 5 repeats):
49
+ `npm run eval:ab` runs the same fixture tasks through headless Pi twice. The control cell gets the 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.
47
50
 
48
- | Model | Cell | Rule violations in final diffs | Tests fully passing | Median run time |
51
+ | Model | Runs per cell | Control runs with a rule violation | Warden runs with a rule violation | Tests fully passing (control / warden) |
49
52
  | --- | --- | --- | --- | --- |
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 |
53
+ | glm-5.3-flash | 60 | **5** | **0** | 54 of 60 / 55 of 60 |
54
+ | deepseek-v4.1-flash | 90 | **1** | **0** | 84 of 90 / 82 of 90 |
55
+
56
+ The rules the traps break are the ones no linter can check and no model knows from training: an endpoint pasted inline instead of imported from the config module, a `TODO` without an issue, `console.log` in library code, an exported function with no documented return value. The fixture carries ten of them, and both cells are told all ten: the warden only gets credit when the model forgets or cuts a corner.
57
+
58
+ The clipping task shows the shape of the effect. 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.
59
+
60
+ Run it on your own model: `npm run eval:ab -- --repeats 3 --model <model>` (it asks you to pick a model on purpose, because a full batch spends real tokens). Per-batch reports, including the runs where nothing changed, live in [eval/reports/](eval/reports/README.md).
61
+
62
+ ### Why a strong model does not make this pointless
63
+
64
+ A guard like this is a smoke alarm. Most days it says nothing, and that is the design: it only pays off on the run where the model is tired, compacted, four hours in, or confident about a shortcut you would have caught in review. Four of its ten guards do not depend on the model being weak at all:
65
+
66
+ - **Context saver**: large tool output is replaced by the lines that matter plus a file pointer. That is your token bill, on any model.
67
+ - **Runaway stop** and **stuck** break a loop the model cannot see itself repeating, on any model.
68
+ - **Done-check** reads the final message against the checks that actually ran, so "all tests pass" gets checked, not trusted.
69
+ - **Subagent triage** scans async child reports so routine progress does not wake the parent agent or cost a request.
70
+
71
+ The rules side is calibrated for the tail, not the average. Judge it on the runs where it fires, which is what the [trace panel](https://github.com/DevMortimer/pi-warden/blob/main/docs/guards.md) and `/warden status` hold precision are for.
54
72
 
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.
73
+ ### What it does not do yet
56
74
 
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.
75
+ Published because the negative is as useful as the positive. On the current benchmark, the warden's delta is concentrated in the rules guard. Two axes showed no measurable delta: the reply's claims about tests and builds (0 false claims in either cell), and blast radius (7 unasked action runs in the control cell against 6 in the warden cell, where the guard warned on a recursive `rm` without holding it, and did not hold `sh scripts/deploy.sh` on a "deploy it" prompt). A 12-turn decay arc also found no rule-violation growth in either cell. Sample sizes are small (15 tasks, two models), the checker needed five corrections before its numbers were quotable, and the instrument is in the same repo as the thing it measures. Re-run it and quote the folder.
58
76
 
59
77
  ## Two moments from real sessions
60
78
 
@@ -121,14 +139,14 @@ Every guarded call (`bash`, `write`, `edit`, and shell-like tools such as contex
121
139
 
122
140
  1. **Read-only? Skip.** `git status && ls` or a `read`: no request, no line in the trace.
123
141
  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.
142
+ 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 0.3 s before the tool runs.
125
143
  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.
126
144
 
127
145
  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.
128
146
 
129
147
  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.
130
148
 
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).
149
+ **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, 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).
132
150
 
133
151
  ## What the numbers say
134
152
 
@@ -169,15 +187,15 @@ Seven commands: `/warden status`, `enable`, `disable`, `mode steer|confirm|advis
169
187
 
170
188
  ## Configuration
171
189
 
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).
190
+ 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`, `notices`, `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).
173
191
 
174
192
  ## Status line and trace
175
193
 
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).
194
+ The line above the editor shows the latest verdict per guard, the verdict leading as a chip: `WARN action write · irreversible 0.09 · off-task 0.95 · unrelated · off task`. The guards that found nothing fold into one line per verdict, so a quiet turn costs one line. `/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).
177
195
 
178
196
  ## For extension authors
179
197
 
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:
198
+ 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. Calls go through pi-typesafe's `ask`, so timeouts, aborts, and error shapes are pi-typesafe's contract:
181
199
 
182
200
  ```ts
183
201
  import { evaluateAction, defaultConfig } from "pi-warden";
@@ -193,7 +211,7 @@ The rules guard, the session-level `ActionGuard` and `RulesGuard`, and the other
193
211
 
194
212
  ## Development
195
213
 
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).
214
+ `npm install`, then `npm run check` (typecheck, 185 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 eval:ab -- --dry-run` lists a benchmark batch without spending anything; `npm run dev:pi` starts Pi with this working tree. Release history, including the measured numbers behind each change: [CHANGELOG.md](CHANGELOG.md). 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
215
 
198
216
  ## Credits
199
217
 
@@ -1,6 +1,6 @@
1
1
  import type { ActionGuardConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
2
2
  import type { PreviousAction, TaskMessage, Verdict } from "./guard.js";
3
- import type { Judge } from "./jev.js";
3
+ import type { Judge } from "pi-typesafe";
4
4
  /** One tool call as the agent proposed it. `id` is Pi's tool call id, stable across hooks and retries. */
5
5
  export interface ToolCallRef {
6
6
  id: string;
package/dist/config.d.ts CHANGED
@@ -38,6 +38,8 @@ export interface StuckGuardConfig {
38
38
  cooldown: number;
39
39
  /** P(same strategy) at or above this reports the agent as stuck. */
40
40
  sameStrategy: number;
41
+ /** Calls to the same target (same tool + input key) that trigger churn detection. */
42
+ churnThreshold: number;
41
43
  /** Also steer the agent with a short message, not only the user. */
42
44
  nudge: boolean;
43
45
  }
@@ -166,6 +168,12 @@ export interface WardenConfig {
166
168
  widget: WidgetConfig;
167
169
  /** Show steer messages in the transcript. They are always visible in the trace panel. */
168
170
  steerVisible: boolean;
171
+ /** Per-call warning notices ("warden · …") in the transcript. Off by default; the widget and trace panel always show them. */
172
+ notices: boolean;
173
+ /** Steers delivered to the agent per run before further non-critical ones are recorded in the trace only. Every delivered
174
+ * steer costs at least one LLM turn, and a closing run that collects six notices collects six restatements of the final
175
+ * status. 0 disables the budget. Critical guards (stuck, done, runaway, subagent wake) always deliver. */
176
+ steerBudget: number;
169
177
  }
170
178
  export declare const PACKAGE_NAME = "pi-warden";
171
179
  /** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
package/dist/config.js CHANGED
@@ -29,7 +29,7 @@ export function defaultConfig() {
29
29
  visibleMismatch: 0.8,
30
30
  feedbackLog: true,
31
31
  },
32
- stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, nudge: true },
32
+ stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, churnThreshold: 5, nudge: true },
33
33
  done: { enabled: true, claimsDone: 0.7, nudge: true },
34
34
  slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
35
35
  security: { enabled: true, threshold: 0.7 },
@@ -40,6 +40,8 @@ export function defaultConfig() {
40
40
  subagent: { enabled: true, wake: true, threshold: 0.8, cooldownMs: 120000 },
41
41
  widget: defaultWidgetConfig(),
42
42
  steerVisible: false,
43
+ notices: false,
44
+ steerBudget: 3,
43
45
  };
44
46
  }
45
47
  /** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
@@ -119,6 +121,7 @@ function applyStuck(base, raw) {
119
121
  minFailures: Math.min(window, positiveInteger(raw.minFailures, base.minFailures)),
120
122
  cooldown: positiveInteger(raw.cooldown, base.cooldown),
121
123
  sameStrategy: probability(raw.sameStrategy, base.sameStrategy),
124
+ churnThreshold: Math.min(window, positiveInteger(raw.churnThreshold, base.churnThreshold)),
122
125
  nudge: boolean(raw.nudge, base.nudge),
123
126
  };
124
127
  }
@@ -257,6 +260,8 @@ export function applyUserOverrides(base, raw) {
257
260
  ...applyGuards(base, raw, shared.timeoutMs, "user"),
258
261
  widget: applyWidget(base.widget, raw.widget),
259
262
  steerVisible: boolean(raw.steerVisible, base.steerVisible),
263
+ notices: boolean(raw.notices, base.notices),
264
+ steerBudget: typeof raw.steerBudget === "number" && Number.isInteger(raw.steerBudget) && raw.steerBudget >= 0 ? raw.steerBudget : base.steerBudget,
260
265
  };
261
266
  }
262
267
  /** Project files may tune the guards but cannot grant TypeSafe consent, change the mode, or raise budgets. */
package/dist/done.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { IntegrationErrorCode } from "pi-typesafe";
1
+ import type { IntegrationErrorCode, Judge } from "pi-typesafe";
2
2
  import type { DoneGuardConfig } from "./config.js";
3
- import type { Judge } from "./jev.js";
4
3
  export type ToolOutcome = "read" | "mutation" | "check-pass" | "check-fail" | "unknown";
5
4
  /**
6
5
  * What a finished tool call contributes to the run's evidence. Only write/edit count as code changes: shell side effects
package/dist/done.js CHANGED
@@ -1,6 +1,5 @@
1
- import { choice, noul } from "pi-typesafe";
1
+ import { ask, choice, noul } from "pi-typesafe";
2
2
  import { isReadOnlyCommand } from "./guard.js";
3
- import { askJev } from "./jev.js";
4
3
  import { redact } from "./redact.js";
5
4
  import { commandOf } from "./tools.js";
6
5
  import { DEFAULT_TEMPLATES, doneTokens, renderTemplate } from "./widget.js";
@@ -116,7 +115,7 @@ export function buildDoneRequest(task, finalMessage, evidence) {
116
115
  }
117
116
  const APPLIES_THRESHOLD = 0.5;
118
117
  export async function evaluateDone(task, finalMessage, evidence, options) {
119
- const result = await askJev(options.judge, buildDoneRequest(task, finalMessage, evidence), { timeoutMs: options.timeoutMs, signal: options.signal });
118
+ const result = await ask(options.judge, buildDoneRequest(task, finalMessage, evidence), { timeoutMs: options.timeoutMs, ...(options.signal ? { signal: options.signal } : {}) });
120
119
  if (!result.ok)
121
120
  return { unverified: false, falseClaim: false, reasons: [], evidence, error: result.error, ...(result.errorCode ? { errorCode: result.errorCode } : {}) };
122
121
  const judgment = {
@@ -11,6 +11,7 @@ export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "p
11
11
  */
12
12
  export declare function formatSteers(stats: {
13
13
  steers: number;
14
+ steersSkipped: number;
14
15
  steerGuards: Partial<Record<SteerGuard, number>>;
15
16
  }): string;
16
17
  /**