pi-warden 0.11.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 +118 -226
- package/dist/config.d.ts +23 -3
- package/dist/config.js +24 -5
- package/dist/done.d.ts +4 -1
- package/dist/done.js +15 -7
- package/dist/extension.d.ts +11 -1
- package/dist/extension.js +132 -18
- package/dist/guard.d.ts +25 -1
- package/dist/guard.js +57 -11
- package/dist/holds.d.ts +15 -0
- package/dist/holds.js +31 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/output.d.ts +7 -0
- package/dist/output.js +10 -2
- package/dist/redact.d.ts +24 -0
- package/dist/redact.js +123 -0
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +12 -4
- package/dist/subagent.d.ts +83 -0
- package/dist/subagent.js +118 -0
- package/dist/trace.d.ts +1 -1
- package/dist/trace.js +1 -1
- package/dist/widget.d.ts +4 -1
- package/dist/widget.js +9 -3
- package/examples/pi-warden.json +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,311 +1,203 @@
|
|
|
1
1
|
# pi-warden
|
|
2
2
|
|
|
3
|
-
|
|
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
|

|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
## What it does
|
|
12
|
-
|
|
13
|
-
| Guard | Watches | Does |
|
|
14
|
-
| --- | --- | --- |
|
|
15
|
-
| **Action** | every `bash`, `write`, `edit` (and context-mode's `ctx_execute*`) before it runs | holds irreversible or off-task calls and tells the agent why, so it re-plans or asks you |
|
|
16
|
-
| **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
|
-
| **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
|
-
| **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
|
-
| **Done-check** | the final message | catches "done" claims after code changes when no test, build, or lint passed |
|
|
22
|
-
| **Context saver** | large or repeated tool output | keeps the exact lines that matter, stores the rest in a file, points recalls at a search |
|
|
23
|
-
| **Notifications** | moments that need you | desktop notification for a held call, a confirm dialog, a runaway stop |
|
|
24
|
-
|
|
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
|
|
9
|
+
## In one minute
|
|
32
10
|
|
|
33
11
|
```bash
|
|
34
12
|
pi install npm:pi-warden
|
|
35
13
|
```
|
|
36
14
|
|
|
37
|
-
|
|
15
|
+
Then, inside Pi:
|
|
38
16
|
|
|
39
|
-
|
|
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.
|
|
40
20
|
|
|
41
|
-
|
|
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.
|
|
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.
|
|
44
22
|
|
|
45
|
-
|
|
23
|
+
## What it watches
|
|
46
24
|
|
|
47
|
-
|
|
25
|
+
| Guard | Watches | Does |
|
|
26
|
+
| --- | --- | --- |
|
|
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 |
|
|
28
|
+
| **Slop** | written code and final replies | names stubs, restating comments, dead code, hedging, padded replies; the agent fixes them in the next edit |
|
|
29
|
+
| **Stuck** | tool results | notices three failures with the same strategy and asks for a new hypothesis |
|
|
30
|
+
| **Done-check** | the final message | catches "done" claims after code changes when no test, build, or lint passed |
|
|
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)) |
|
|
36
|
+
| **Notifications** (opt-in) | moments that need you | desktop notification for a held call, a confirm dialog, a runaway stop |
|
|
48
37
|
|
|
49
|
-
|
|
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).
|
|
50
39
|
|
|
51
|
-
|
|
40
|
+
## Does it improve the code?
|
|
52
41
|
|
|
53
|
-
|
|
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. |
|
|
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.
|
|
58
43
|
|
|
59
|
-
|
|
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).
|
|
60
45
|
|
|
61
|
-
|
|
46
|
+
Results so far, 50 runs per model (5 tasks, both cells, 5 repeats):
|
|
62
47
|
|
|
63
|
-
|
|
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 |
|
|
64
54
|
|
|
65
|
-
|
|
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` 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.
|
|
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.
|
|
67
56
|
|
|
68
|
-
|
|
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 at 0.6 warns and at 0.85 with `unrelated` holds, but only when the action can change something. An unrelated `grep` is warned about, never held. Patterns set the floor; Jev can only raise it.
|
|
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.
|
|
70
58
|
|
|
71
|
-
|
|
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.
|
|
59
|
+
## Two moments from real sessions
|
|
76
60
|
|
|
77
|
-
|
|
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).
|
|
78
62
|
|
|
79
|
-
|
|
63
|
+
### It quoted a project rule back, and the agent fixed it in the next turn
|
|
80
64
|
|
|
81
|
-
|
|
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.
|
|
82
66
|
|
|
83
|
-
|
|
67
|
+
```text
|
|
68
|
+
assistant
|
|
69
|
+
[write] scripts/demo_rail_decisions.py (186 lines)
|
|
70
|
+
DSN = os.getenv("SUPABASE_DB_URL", "postgresql://[local default]")
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)")
|
|
95
85
|
```
|
|
96
86
|
|
|
97
|
-
|
|
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.
|
|
98
88
|
|
|
99
|
-
|
|
89
|
+
### It held a database reset, and the agent finished everything else first
|
|
100
90
|
|
|
101
|
-
|
|
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.
|
|
102
92
|
|
|
103
|
-
|
|
93
|
+
```text
|
|
94
|
+
assistant
|
|
95
|
+
[bash] cd .claude/worktrees/con-142 && supabase db reset 2>&1 | tail -6
|
|
104
96
|
|
|
105
|
-
|
|
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]
|
|
106
104
|
|
|
107
|
-
|
|
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
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
```
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
No dialog, no interruption. The agent moved the risky step to the end and asked in one sentence.
|
|
114
117
|
|
|
115
|
-
|
|
118
|
+
## How it decides
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
Every guarded call (`bash`, `write`, `edit`, and shell-like tools such as context-mode's `ctx_execute`) goes through four steps:
|
|
118
121
|
|
|
119
|
-
|
|
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.
|
|
120
126
|
|
|
121
|
-
|
|
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.
|
|
122
128
|
|
|
123
|
-
|
|
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.
|
|
124
130
|
|
|
125
|
-
|
|
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).
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
## What the numbers say
|
|
128
134
|
|
|
129
|
-
|
|
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.
|
|
130
136
|
|
|
131
|
-
|
|
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).
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
## Questions people asked
|
|
134
140
|
|
|
135
|
-
|
|
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.
|
|
136
143
|
|
|
137
|
-
|
|
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.
|
|
138
146
|
|
|
139
|
-
|
|
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).
|
|
140
148
|
|
|
141
|
-
|
|
142
|
-
- **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.
|
|
143
|
-
- **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`.
|
|
144
|
-
- **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
|
|
145
150
|
|
|
146
|
-
|
|
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.
|
|
147
152
|
|
|
148
|
-
|
|
153
|
+
```markdown
|
|
154
|
+
# No console statements
|
|
155
|
+
Code must not contain `console.log` or `console.debug`. Use the logger.
|
|
149
156
|
|
|
150
|
-
|
|
157
|
+
# Exported functions must have explicit return types
|
|
158
|
+
paths: src/**/*.ts
|
|
159
|
+
Every exported function declares its return type.
|
|
160
|
+
```
|
|
151
161
|
|
|
152
|
-
|
|
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.
|
|
153
163
|
|
|
154
|
-
|
|
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).
|
|
155
165
|
|
|
156
166
|
## Commands
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
| --- | --- |
|
|
160
|
-
| `/warden status` | Guard state, consent and key source, session counts, thresholds, hold precision and feedback log, rules source, config paths, last verdicts |
|
|
161
|
-
| `/warden enable` | Data notice, key prompt if none is stored, consent saved |
|
|
162
|
-
| `/warden disable` | Stop Jev judgments; pattern checks continue |
|
|
163
|
-
| `/warden mode steer\|confirm\|advise` | How holds are handled; without an argument, show the current mode |
|
|
164
|
-
| `/warden config` | Edit the user config JSON in Pi's editor |
|
|
165
|
-
| `/warden test` | One synthetic destructive action, its verdict, and what the agent would be told |
|
|
166
|
-
| `/warden trace` | Toggle the trace sidebar (or print the last 20 events without a UI) |
|
|
167
|
-
|
|
168
|
-
## Status line and trace sidebar
|
|
169
|
-
|
|
170
|
-
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.
|
|
171
|
-
|
|
172
|
-
Clicks and the wheel need Pi's fullscreen mode (`tuiMode: "fullscreen"` in `/settings`). In macOS Terminal.app enable View → Allow Mouse Reporting.
|
|
173
|
-
|
|
174
|
-
Templates in `config.widget` control the text. Segments are separated by ` · `; a segment whose token has no value is dropped:
|
|
175
|
-
|
|
176
|
-
```json
|
|
177
|
-
"widget": {
|
|
178
|
-
"action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
|
|
179
|
-
"rules": "warden · rules · {tool} {path} · {asked} rules · {violations} · {status}",
|
|
180
|
-
"stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
|
|
181
|
-
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
|
|
182
|
-
"prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
|
|
183
|
-
"security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
|
|
184
|
-
"context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes",
|
|
185
|
-
"runaway": "warden · runaway · {kind} · {count}× repeated · {chars} chars · {signal} · {status}"
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Tokens per guard: action `tool level source irreversible offTask scope approved intent 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).
|
|
190
169
|
|
|
191
170
|
## Configuration
|
|
192
171
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
```json
|
|
196
|
-
{
|
|
197
|
-
"enabled": true,
|
|
198
|
-
"typesafe": false,
|
|
199
|
-
"mode": "steer",
|
|
200
|
-
"timeoutMs": 5000,
|
|
201
|
-
"maxRequests": 500,
|
|
202
|
-
"action": {
|
|
203
|
-
"enabled": true,
|
|
204
|
-
"tools": ["bash", "powershell", "ctx_execute", "ctx_batch_execute", "ctx_execute_file", "write", "edit"],
|
|
205
|
-
"failOpen": true,
|
|
206
|
-
"irreversible": { "warn": 0.5, "confirm": 0.7 },
|
|
207
|
-
"offTask": { "warn": 0.6, "confirm": 0.85 },
|
|
208
|
-
"intentMismatch": 0.8,
|
|
209
|
-
"feedbackLog": true
|
|
210
|
-
},
|
|
211
|
-
"rules": {
|
|
212
|
-
"enabled": true,
|
|
213
|
-
"threshold": 0.7,
|
|
214
|
-
"files": [],
|
|
215
|
-
"fallback": true,
|
|
216
|
-
"maxChars": 8000,
|
|
217
|
-
"exclude": [],
|
|
218
|
-
"skip": [],
|
|
219
|
-
"sensitivePaths": {}
|
|
220
|
-
},
|
|
221
|
-
"slop": {
|
|
222
|
-
"enabled": true,
|
|
223
|
-
"threshold": 0.7,
|
|
224
|
-
"prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
|
|
225
|
-
},
|
|
226
|
-
"security": { "enabled": true, "threshold": 0.7 },
|
|
227
|
-
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
|
|
228
|
-
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
|
|
229
|
-
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8, "duplicateMinChars": 2000, "recallTool": "auto", "formatConfidence": 0.7 },
|
|
230
|
-
"runaway": { "enabled": true, "repeats": 4, "thinkingRepeats": 10, "minChars": 400, "recover": true },
|
|
231
|
-
"notify": { "enabled": true, "cooldownMs": 10000, "command": [] },
|
|
232
|
-
"widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w", "panelWidth": "40%" },
|
|
233
|
-
"steerVisible": false
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
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`.
|
|
238
|
-
|
|
239
|
-
A wince-style setup for a backend repo (the full version is [`examples/pi-warden.json`](examples/pi-warden.json)):
|
|
240
|
-
|
|
241
|
-
```json
|
|
242
|
-
{
|
|
243
|
-
"rules": {
|
|
244
|
-
"skip": ["tests/**", "**/*.test.*", "docs/**", "**/*.md"],
|
|
245
|
-
"exclude": ["secrets/**", "**/*.pem"],
|
|
246
|
-
"sensitivePaths": {
|
|
247
|
-
"migrations/**": "This touches a migration: tell the user and add a rollback path",
|
|
248
|
-
"**/permissions*": "Access control changed: ask the user for a security review before merging"
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Data handling
|
|
255
|
-
|
|
256
|
-
With consent, requests go to `https://api.typesafe.ai`. What is sent:
|
|
257
|
-
|
|
258
|
-
- **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.
|
|
259
|
-
- **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.
|
|
260
|
-
- **Stuck**: the last 12 tool calls (300 characters each) with 400-character output tails.
|
|
261
|
-
- **Done-check and prose**: the agent's final message (2000 and 2500 characters), the run's check commands, the audience description.
|
|
262
|
-
- **Output checks**: a redacted head/tail sample up to 6000 characters plus size, line counts, and tool name.
|
|
263
|
-
- **Nothing** for duplicate detection, the runaway guard, sensitive-path notes, or pattern checks.
|
|
264
|
-
|
|
265
|
-
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.
|
|
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).
|
|
266
173
|
|
|
267
|
-
|
|
174
|
+
## Status line and trace
|
|
268
175
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
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).
|
|
272
177
|
|
|
273
178
|
## For extension authors
|
|
274
179
|
|
|
275
|
-
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:
|
|
276
181
|
|
|
277
182
|
```ts
|
|
278
|
-
import { evaluateAction,
|
|
183
|
+
import { evaluateAction, defaultConfig } from "pi-warden";
|
|
279
184
|
import { createTypeSafe } from "pi-typesafe";
|
|
280
185
|
|
|
281
|
-
const judge = createTypeSafe();
|
|
282
186
|
const verdict = await evaluateAction(
|
|
283
187
|
{ tool: "bash", input: { command: "git push --force" }, cwd: process.cwd(), task: "push my branch" },
|
|
284
|
-
{ config: defaultConfig().action, judge }, // omit judge for pattern checks only
|
|
188
|
+
{ config: defaultConfig().action, judge: createTypeSafe() }, // omit judge for pattern checks only
|
|
285
189
|
);
|
|
286
|
-
verdict.level; // "allow" | "warn" | "confirm" (confirm = hold in steer mode)
|
|
287
|
-
verdict.reasons; // ["destructive: git force push", "irreversible 0.91"]
|
|
288
|
-
|
|
289
|
-
const set = new RuleStore().load(process.cwd(), defaultConfig().rules);
|
|
290
|
-
const rules = await evaluateRules("write", { path: "src/a.ts", content: "console.log(1)" }, { cwd: process.cwd(), config: defaultConfig().rules, set, judge, timeoutMs: 5000 });
|
|
291
|
-
rules.findings; // [{ id: "no-console-statements", name, violation: 0.97, body }]
|
|
292
190
|
```
|
|
293
191
|
|
|
294
|
-
|
|
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).
|
|
295
193
|
|
|
296
194
|
## Development
|
|
297
195
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
node scripts/slop-cases.mjs # 22 billable cases for the slop and prose questions
|
|
304
|
-
node scripts/security-cases.mjs # 9 output-security and task-continuity cases
|
|
305
|
-
node scripts/context-cases.mjs # 15 labelled outputs for retention and format
|
|
306
|
-
npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
|
|
307
|
-
npm run preview # re-render docs/preview.png (needs a Chrome binary)
|
|
308
|
-
```
|
|
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).
|
|
309
201
|
|
|
310
202
|
## License
|
|
311
203
|
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ export interface Threshold {
|
|
|
5
5
|
/** P(yes) at or above this asks the user before the tool runs. */
|
|
6
6
|
confirm: number;
|
|
7
7
|
}
|
|
8
|
+
export interface OffTaskThreshold {
|
|
9
|
+
/** P(off-task) at or above this, with a scope other than unclear, shows a warning. */
|
|
10
|
+
warn: number;
|
|
11
|
+
/** P(off-task) at or above this, with scope unrelated on a call that can change something, also steers the agent back to the task. Never holds: on 17k recorded calls off-task holds caught nothing the user regretted. */
|
|
12
|
+
steer: number;
|
|
13
|
+
}
|
|
8
14
|
export interface ActionGuardConfig {
|
|
9
15
|
enabled: boolean;
|
|
10
16
|
/** Tool names inspected before execution. Read-only tools are skipped to keep latency low. */
|
|
@@ -14,9 +20,11 @@ export interface ActionGuardConfig {
|
|
|
14
20
|
/** Per-request TypeSafe timeout. The call is judged as an error after this. */
|
|
15
21
|
timeoutMs: number;
|
|
16
22
|
irreversible: Threshold;
|
|
17
|
-
offTask:
|
|
23
|
+
offTask: OffTaskThreshold;
|
|
18
24
|
/** P(the call differs from the agent's own stated plan) at or above this warns and tells the agent; never holds on its own. */
|
|
19
25
|
intentMismatch: number;
|
|
26
|
+
/** The same, for a command whose effect is visible outside the working tree (commit, push, merge, publish, launch): less mismatch is enough. */
|
|
27
|
+
visibleMismatch: number;
|
|
20
28
|
/** Write each judged call and what the user did next (approved, declined, re-planned, regretted) to an owner-only per-session file under the agent directory; redacted, never the command. */
|
|
21
29
|
feedbackLog: boolean;
|
|
22
30
|
}
|
|
@@ -105,7 +113,7 @@ export interface RunawayConfig {
|
|
|
105
113
|
recover: boolean;
|
|
106
114
|
}
|
|
107
115
|
export interface NotifyConfig {
|
|
108
|
-
/** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. */
|
|
116
|
+
/** Desktop notification when the agent needs you: a held call it will ask about, a confirm dialog, a stopped runaway. Off by default; opt in per user or project. */
|
|
109
117
|
enabled: boolean;
|
|
110
118
|
/** Sibling holds in one turn produce one notification; a second within this many milliseconds is skipped. */
|
|
111
119
|
cooldownMs: number;
|
|
@@ -115,6 +123,16 @@ export interface NotifyConfig {
|
|
|
115
123
|
*/
|
|
116
124
|
command: string[];
|
|
117
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
|
+
}
|
|
118
136
|
export type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
|
|
119
137
|
export declare function isRecallTool(value: unknown): value is RecallTool;
|
|
120
138
|
export type WardenMode = "steer" | "confirm" | "advise";
|
|
@@ -142,6 +160,8 @@ export interface WardenConfig {
|
|
|
142
160
|
context: ContextConfig;
|
|
143
161
|
runaway: RunawayConfig;
|
|
144
162
|
notify: NotifyConfig;
|
|
163
|
+
/** Triage of async subagent reports: Jev separates what needs the agent awake from what is only context. */
|
|
164
|
+
subagent: SubagentConfig;
|
|
145
165
|
/** The status line above the editor and the trace panel. */
|
|
146
166
|
widget: WidgetConfig;
|
|
147
167
|
/** Show steer messages in the transcript. They are always visible in the trace panel. */
|
|
@@ -149,7 +169,7 @@ export interface WardenConfig {
|
|
|
149
169
|
}
|
|
150
170
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
151
171
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
152
|
-
export declare const CONFIG_SCHEMA =
|
|
172
|
+
export declare const CONFIG_SCHEMA = 6;
|
|
153
173
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
|
154
174
|
export declare function defaultConfig(): WardenConfig;
|
|
155
175
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|