pi-warden 0.2.1 → 0.4.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 +61 -13
- package/dist/config.d.ts +19 -4
- package/dist/config.js +35 -6
- package/dist/done.d.ts +2 -2
- package/dist/done.js +11 -13
- package/dist/extension.d.ts +3 -1
- package/dist/extension.js +125 -28
- package/dist/guard.d.ts +20 -11
- package/dist/guard.js +53 -38
- package/dist/index.d.ts +11 -2
- package/dist/index.js +5 -0
- package/dist/panel.d.ts +44 -0
- package/dist/panel.js +121 -0
- package/dist/prose.d.ts +57 -0
- package/dist/prose.js +98 -0
- package/dist/stuck.d.ts +6 -3
- package/dist/stuck.js +11 -14
- package/dist/tools.d.ts +16 -0
- package/dist/tools.js +37 -0
- package/dist/trace.d.ts +31 -0
- package/dist/trace.js +93 -0
- package/dist/widget.d.ts +40 -0
- package/dist/widget.js +120 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# pi-warden
|
|
2
2
|
|
|
3
|
-
A second pair of eyes for [Pi](https://pi.dev) that makes the agent smarter instead of interrupting you. Before the agent runs a `bash`, `write`, or `edit` call, pi-warden asks [Jev](https://typesafe.ai) two questions about it in ~250 ms: *would this destroy something that cannot be recovered?* and *is this what the user actually asked for?* The answers are probabilities, so `rm -rf dist` after "rebuild from scratch" sails through while `npm run db:reset` after "add a column" is held, and the agent is told why so it re-plans or asks you in chat. Three more guards watch the run: a **stuck-loop** detector, a **done-check** for completion claims that no test backed up, and a **slop** note for stubs and filler. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
|
|
3
|
+
A second pair of eyes for [Pi](https://pi.dev) that makes the agent smarter instead of interrupting you. Before the agent runs a `bash`, `write`, or `edit` call (or a shell command through [context-mode](https://www.npmjs.com/package/context-mode)'s `ctx_execute`), pi-warden asks [Jev](https://typesafe.ai) two questions about it in ~250 ms: *would this destroy something that cannot be recovered?* and *is this what the user actually asked for?* The answers are probabilities, so `rm -rf dist` after "rebuild from scratch" sails through while `npm run db:reset` after "add a column" is held, and the agent is told why so it re-plans or asks you in chat. Three more guards watch the run: a **stuck-loop** detector, a **done-check** for completion claims that no test backed up, and a **slop** note for stubs and filler. Built on [pi-typesafe](https://github.com/DevMortimer/pi-typesafe).
|
|
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.
|
|
8
8
|
|
|
9
|
+
## Philosophy
|
|
10
|
+
|
|
11
|
+
pi-warden is a harness for the agent, not a gate for the user. It watches every tool call, tool result, and reply, asks Jev small typed questions about them, and acts through the agent's own context — a held call, a steer message, a compressed output — rather than through dialogs. Jev decides; code applies the decision; the LLM is never asked to judge itself. Everything warden does is visible in the trace panel, and nothing is rewritten in your session file.
|
|
12
|
+
|
|
13
|
+
Three jobs, in order of maturity:
|
|
14
|
+
|
|
15
|
+
| Pillar | What it means | Status |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| **Security** | Irreversible or off-task actions are held with a reason the agent can act on; credential files, force pushes, destructive SQL, and remote-script execution are caught offline; secrets are redacted before anything leaves the machine. Next: injected instructions inside tool output (web pages, issues, files) flagged and quarantined; insecure patterns in written code steered. | built · extending |
|
|
18
|
+
| **Deslopify** | Written code is scored per symptom — stubs, restating comments, dead code, hedging — and the agent gets a nudge naming the symptom and its fix; repeats become a standing rule. Replies are scored for wordiness, assistant clichés, and jargon against an `audience` setting, and a trend across replies shapes the next one. | built |
|
|
19
|
+
| **Context saving** | Jev judges which tool outputs are worth keeping; code compresses them. Only at moments that cannot cost a prompt-cache miss: when an output arrives (before it enters the cache), when the cache is measurably cold, or when Pi compacts anyway. Works alongside prompt-cache optimizers because it never touches the system prompt or a warm prefix. | planned |
|
|
20
|
+
|
|
9
21
|
## Why a coding agent needs this
|
|
10
22
|
|
|
11
23
|
Agents are good at picking the next command and bad at noticing when that command is out of proportion to the request. Pattern lists catch `rm -rf /` and force pushes; they cannot tell `db:reset` when you asked for a reset from `db:reset` when you asked for a column. A generative model can, but a second LLM call per tool call is slow and expensive. Jev is a System One model: it returns calibrated probabilities instead of text, in a quarter of a second, for a fraction of a cent. That makes it cheap enough to sit in front of **every** guarded call:
|
|
@@ -14,7 +26,7 @@ Agents are good at picking the next command and bad at noticing when that comman
|
|
|
14
26
|
- **Scope drift gets flagged.** Poems in a bugfix, refactors nobody asked for, dependency installs unrelated to the task: warned to you at 0.6, held at 0.85 when Jev also calls the action `unrelated`.
|
|
15
27
|
- **Loops get broken.** Three failures with the same strategy (same command with cosmetic changes, same error after each edit) earn the agent a steer: re-read the error, form a new hypothesis, or report the blocker. Exact repeats are caught offline; Jev tells "investigating between failures" from "flailing" — 0.32 vs 0.93 in the smoke run.
|
|
16
28
|
- **"Done" gets checked.** When the final message reports completion after file changes and no test, build, or lint passed in that run, the agent is asked to verify before you read a false "all green". A claim that tests passed when none ran is called out as such. Once per prompt, so it cannot loop.
|
|
17
|
-
- **Slop gets a
|
|
29
|
+
- **Slop gets a name.** Written code is scored per symptom on the same request as the action guard (zero extra latency): stubs and fake-data mocks, comments that restate the code, dead or duplicated code, hedging notes. The agent is told which symptom and how to fix it, never held; the third repeat in a session becomes a standing rule. Replies are scored too — wordy, clichéd, or too technical for the configured audience — and a trend across replies earns a nudge that shapes the next one without spending a turn.
|
|
18
30
|
- **Nothing slows down the boring calls.** Read-only shell lines are skipped without a request; anything else costs one request of ~600 input tokens.
|
|
19
31
|
- **It degrades gracefully.** Offline pattern checks and exact-repeat detection run with no account at all. On an API timeout or outage the call is allowed with a warning (configurable), and reasons never include your command text or upstream error bodies.
|
|
20
32
|
|
|
@@ -38,7 +50,7 @@ A `TYPESAFE_API_KEY` environment variable takes precedence over the stored key.
|
|
|
38
50
|
|
|
39
51
|
### Action guard (`tool_call`)
|
|
40
52
|
|
|
41
|
-
1. **Skip** read-only tools, and read-only shell lines such as `git status && ls` (no network, no widget).
|
|
53
|
+
1. **Skip** read-only tools, and read-only shell lines such as `git status && ls` (no network, no widget). Guarded by default: `bash`, `powershell`, `write`, `edit`, and context-mode's `ctx_execute`, `ctx_execute_file`, `ctx_batch_execute` (their `code`/`commands` fields are read as the command; non-shell code such as JavaScript is judged but never skipped as read-only). Add other tools with `action.tools`; unknown tools are judged from their JSON input.
|
|
42
54
|
2. **Pattern pass** (offline): force pushes, `git reset --hard`, `git clean -f`, recursive `rm` on absolute/home/variable/parent paths, SQL `DROP`/`TRUNCATE`/`DELETE FROM`, block-device writes, `chmod -R 777`, fork bombs, `curl | sh`, `kill -1`, shutdown, package publishing, infrastructure destroys → **hold**. `rm -rf` on a project path, `git checkout -- .`, `git branch -D`, `git stash drop`, `find -delete`, `sudo` → **warn**. Reads or writes of `.env`, SSH, AWS, npm, kube, and other credential files → **warn**. A `write` that overwrites an existing file outside the project → **hold**; creating or editing outside the project → **warn**.
|
|
43
55
|
3. **Jev judgment** (with consent): one request with the state `{ task, action }` and three questions — `irreversible` (Noul), `off_task` (Noul), `scope` (Choice: expected step, plausible side step, unrelated, unclear). Defaults: irreversible ≥ 0.5 warns, ≥ 0.7 holds; off-task ≥ 0.6 warns, ≥ 0.85 with `unrelated` holds. Pattern results set the floor; Jev can only raise it. For `write`/`edit`, two slop questions ride on the same request.
|
|
44
56
|
4. **Act**, by mode:
|
|
@@ -54,17 +66,25 @@ Keeps the last 12 tool results for the current prompt. When the latest result fa
|
|
|
54
66
|
|
|
55
67
|
### Done-check (`agent_end`)
|
|
56
68
|
|
|
57
|
-
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/fail. When a run ends with a normal assistant message after code changes and no passing check, one Jev request judges the message: `claims_done`, `claims_verified`, `verification_applies` (Noul), `outcome` (Choice: complete / partial / blocked / other). A completion claim ≥ 0.7 that is not a blocker or question, for a task where checks would mean something (≥ 0.5; prose and file housekeeping score ~0.05), is reported as unverified; a verification claim with no check run is reported as a false claim. With `nudge: true` (default) the agent receives one follow-up turn asking it to run the checks or say plainly that nothing was verified — once per user prompt.
|
|
69
|
+
Tracks each run's evidence: code changes (`write`, `edit`) and check commands (`npm test`, `pytest`, `cargo test`, `tsc`, `eslint`, `go test`, `make test`, and similar, whether run through `bash` or `ctx_execute`) with pass/fail; context-mode's inline `Command exited with code N` counts as a failure. When a run ends with a normal assistant message after code changes and no passing check, one Jev request judges the message: `claims_done`, `claims_verified`, `verification_applies` (Noul), `outcome` (Choice: complete / partial / blocked / other). A completion claim ≥ 0.7 that is not a blocker or question, for a task where checks would mean something (≥ 0.5; prose and file housekeeping score ~0.05), is reported as unverified; a verification claim with no check run is reported as a false claim. With `nudge: true` (default) the agent receives one follow-up turn asking it to run the checks or say plainly that nothing was verified — once per user prompt.
|
|
58
70
|
|
|
59
|
-
### Slop (`write`/`edit`, same request as the action guard)
|
|
71
|
+
### Slop in code (`write`/`edit`, same request as the action guard)
|
|
60
72
|
|
|
61
73
|
pi-warden cannot rewrite code — Jev only judges — so slop is handled as a feedback loop through the agent:
|
|
62
74
|
|
|
63
|
-
1. When the agent calls `write` or `edit`,
|
|
64
|
-
2.
|
|
65
|
-
3. The
|
|
75
|
+
1. When the agent calls `write` or `edit`, four Noul questions ride on the action guard's request (no extra latency), one per symptom, each with concrete yes/no criteria: `slop_stub` (placeholder, mock, or "implement later" code where `task` needs a working implementation), `slop_comments` (explanatory comments that restate what the adjacent code shows), `slop_dead` (commented-out code, unused imports or variables, duplicated logic, unreachable branches), `slop_hedging` ("should work", "for now", TODOs without a plan). Jev sees a head/middle/tail sample of a `write` (1500 characters) or the first three replacement texts of an `edit`, plus your request.
|
|
76
|
+
2. Any symptom ≥ `slop.threshold` (0.7) trips the note. The write **goes through** — holding it would leave a half-written file.
|
|
77
|
+
3. The agent receives a steer message naming the symptoms and their fixes, e.g. *"pi-warden: the content just written to `src/x.ts` has stub or placeholder code where a working implementation is needed; hedging or vague notes. Fix it in your next edit: replace stubs, placeholders, and hard-coded fake data with the working implementation, or state in your reply exactly what is left unimplemented and why; replace \"should work\", \"for now\", and TODOs without a plan with a definite statement or a concrete follow-up."* The third time a symptom appears in a session, the note says so and asks the agent to treat it as a standing rule.
|
|
78
|
+
|
|
79
|
+
Per-symptom scoring is what makes this precise. From the tuning set (`scripts/slop-cases.mjs`, 16 code cases): a `// TODO: implement later` stub scores stub 0.99; restating comments score comments 0.97 while an explanatory why-comment scores 0.08; commented-out code scores dead 0.96 and comments 0.43; a mock inside a *test file* scores stub 0.51 (below threshold, correctly); a stub explicitly outside the task scores 0.24. Not covered: code written through shell heredocs and content past the sample limits.
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
### Slop in replies (`agent_end`)
|
|
82
|
+
|
|
83
|
+
The run's final reply (≥ 200 characters) is judged against `slop.prose.audience`: `wordy` (preamble, restating the request, closing summary, filler), `cliches` ("Great question", "I hope this helps", "it's worth noting", unrequested caveat lists, emoji headings), `jargon` (unexplained terms for the audience). `audience` is `technical` (default), `plain`, or any free-text description such as *"a founder without programming background"*. One long answer to a long question is not punished: a symptom must appear in `trend` (2) of the last 3 replies before the agent is nudged, the nudge is queued for the next user prompt so it shapes the next reply without spending a turn, and two replies pass before the same nudge can fire again. From the tuning set: a padded reply scores wordy 0.97 / clichés 0.99; a dense three-point summary 0.25 / 0.05; the same status update scores jargon 0.94 for a plain audience and 0.24 for a developer.
|
|
84
|
+
|
|
85
|
+
### Steer messages
|
|
86
|
+
|
|
87
|
+
Nudges from the stuck, done, slop, and prose guards are custom messages in the agent's context. By default they are **hidden from the transcript** (`steerVisible: false`) so the conversation stays yours; the notification tells you a nudge happened and the trace panel shows the exact text. Set `steerVisible: true` to see them inline.
|
|
68
88
|
|
|
69
89
|
If TypeSafe cannot answer (timeout after 5 s, outage, budget), an action-guard call is allowed with a warning (`failOpen: true`; set it to `false` to hold instead), and the other guards simply skip. When the per-session request budget is spent, pi-warden says so once and continues with offline checks. Consent in headless runs comes from `PI_WARDEN_ENABLED=1`; `PI_WARDEN_MODE` overrides the mode.
|
|
70
90
|
|
|
@@ -78,6 +98,27 @@ If TypeSafe cannot answer (timeout after 5 s, outage, budget), an action-guard c
|
|
|
78
98
|
| `/warden mode steer\|confirm\|advise` | Choose how holds are handled; without an argument, show the current mode |
|
|
79
99
|
| `/warden config` | Edit the user config JSON in Pi's editor and save it |
|
|
80
100
|
| `/warden test` | Evaluate one synthetic destructive action and show the verdict and what the agent would be told |
|
|
101
|
+
| `/warden trace` | Open the trace panel (or print the last 20 events without a UI) |
|
|
102
|
+
|
|
103
|
+
## Status line and trace panel
|
|
104
|
+
|
|
105
|
+
The line above the editor shows the latest verdict per guard, for example `warden · bash · irreversible 0.84 · off-task 0.86 · unrelated · confirm`. Every verdict is also kept in a session trace with what was inspected (redacted command or path), the pattern hits, Jev's scores with model and latency, the reasons, and the exact text the agent was told. Open the panel with `/warden trace`, `ctrl+shift+w`, or by clicking the status line (clicks need Pi's fullscreen mode: `tuiMode: "fullscreen"` in settings, because regular mode leaves the mouse to the terminal). The panel is a right-hand overlay, newest first, live-updating while the agent works; ↑↓/PgUp/PgDn scroll, `c` clears, Esc closes.
|
|
106
|
+
|
|
107
|
+
Templates in `config.widget` control the text. Segments are separated by ` · `; a segment whose token has no value for that verdict is dropped, so optional information disappears together with its label:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
"widget": {
|
|
111
|
+
"enabled": true,
|
|
112
|
+
"placement": "aboveEditor",
|
|
113
|
+
"shortcut": "ctrl+shift+w",
|
|
114
|
+
"action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
|
|
115
|
+
"stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
|
|
116
|
+
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
|
|
117
|
+
"prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Tokens — action: `tool level source irreversible offTask scope approved slop slopStub slopComments slopDead slopHedging patterns reasons path model ms flags time`; prose: `wordy cliches jargon status reasons model ms flags time`; stuck: `failures sameStrategy approachChange progress status source reasons model ms flags time`; done: `changes checks checksPassed claimsDone claimsVerified checksApply outcome status reasons model ms flags time`. A minimal line: `"action": "⚔ {tool} {level} · {irreversible}/{offTask}"`. Set `"enabled": false` to hide the line (the trace and panel keep working); `"shortcut": ""` disables the keybinding.
|
|
81
122
|
|
|
82
123
|
## Configuration
|
|
83
124
|
|
|
@@ -92,14 +133,20 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
92
133
|
"maxRequests": 500,
|
|
93
134
|
"action": {
|
|
94
135
|
"enabled": true,
|
|
95
|
-
"tools": ["bash", "write", "edit"],
|
|
136
|
+
"tools": ["bash", "powershell", "ctx_execute", "ctx_batch_execute", "ctx_execute_file", "write", "edit"],
|
|
96
137
|
"failOpen": true,
|
|
97
138
|
"irreversible": { "warn": 0.5, "confirm": 0.7 },
|
|
98
139
|
"offTask": { "warn": 0.6, "confirm": 0.85 }
|
|
99
140
|
},
|
|
100
141
|
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
|
|
101
142
|
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
|
|
102
|
-
"slop": {
|
|
143
|
+
"slop": {
|
|
144
|
+
"enabled": true,
|
|
145
|
+
"threshold": 0.7,
|
|
146
|
+
"prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
|
|
147
|
+
},
|
|
148
|
+
"widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w" },
|
|
149
|
+
"steerVisible": false
|
|
103
150
|
}
|
|
104
151
|
```
|
|
105
152
|
|
|
@@ -107,7 +154,7 @@ A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (f
|
|
|
107
154
|
|
|
108
155
|
## Data handling
|
|
109
156
|
|
|
110
|
-
- With consent, each guarded call sends to `https://api.typesafe.ai`: your latest prompt (truncated to 1500 characters), the tool name, the command (truncated to 2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character
|
|
157
|
+
- With consent, each guarded call sends to `https://api.typesafe.ai`: your latest prompt (truncated to 1500 characters), the tool name, the command (truncated to 2000 characters) or the file path (relative inside the project, `~`-shortened outside), whether the file exists, a 1500-character head/middle/tail sample for `write`, and the first three edit pairs (400 characters each) for `edit`. The stuck detector sends the last 12 tool calls (300 characters each) with 400-character output tails; the done-check and the prose check send the agent's final message (2000 and 2500 characters) with the run's check commands and the audience description. No other files, history, or telemetry.
|
|
111
158
|
- Obvious credentials in the action (`Authorization` headers, `TOKEN=`/`SECRET=` assignments, `sk-`, `ghp_`, `AKIA`, JWTs, URL passwords, PEM blocks) are replaced with `[redacted]` before sending. This is best-effort; do not rely on it for prompts that contain secrets.
|
|
112
159
|
- Text returned or steered to the agent names the tool, the reasons, and the scores, not the command text. UI errors never include upstream response bodies or keys.
|
|
113
160
|
- Judgments are model output. Thresholds are yours to tune; a hold is information for the agent and for you, not a verdict on either.
|
|
@@ -137,8 +184,9 @@ Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`,
|
|
|
137
184
|
```bash
|
|
138
185
|
npm install
|
|
139
186
|
npm run check # typecheck, offline tests (mocked transport), build
|
|
140
|
-
npm run test:live #
|
|
187
|
+
npm run test:live # 31 billable synthetic judgments across all guards (key from .env or the stored login); pass action|slop|approval|stuck|done for one group
|
|
141
188
|
npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
|
|
189
|
+
node scripts/slop-cases.mjs # 22 billable cases that tune the slop and prose questions
|
|
142
190
|
npm run preview # re-render docs/preview.png from the recorded live verdicts (needs a Chrome binary)
|
|
143
191
|
```
|
|
144
192
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WidgetConfig } from "./widget.js";
|
|
1
2
|
export interface Threshold {
|
|
2
3
|
/** P(yes) at or above this shows a warning and continues. */
|
|
3
4
|
warn: number;
|
|
@@ -35,12 +36,22 @@ export interface DoneGuardConfig {
|
|
|
35
36
|
/** Also send the agent a follow-up asking it to verify. Triggers one more LLM turn. */
|
|
36
37
|
nudge: boolean;
|
|
37
38
|
}
|
|
39
|
+
export interface ProseConfig {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/** Who reads the agent's replies: "technical", "plain", or a free-text description. Drives the jargon question. */
|
|
42
|
+
audience: string;
|
|
43
|
+
/** P(symptom) at or above this counts as a hit. */
|
|
44
|
+
threshold: number;
|
|
45
|
+
/** A symptom must hit in this many of the last three replies before the agent is nudged. */
|
|
46
|
+
trend: number;
|
|
47
|
+
/** Replies with fewer characters are not judged. */
|
|
48
|
+
minChars: number;
|
|
49
|
+
}
|
|
38
50
|
export interface SlopGuardConfig {
|
|
39
51
|
enabled: boolean;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
placeholder: number;
|
|
52
|
+
/** P(symptom) at or above this is reported for written code: stub, comments, dead, hedging. */
|
|
53
|
+
threshold: number;
|
|
54
|
+
prose: ProseConfig;
|
|
44
55
|
}
|
|
45
56
|
export type WardenMode = "steer" | "confirm" | "advise";
|
|
46
57
|
export interface WardenConfig {
|
|
@@ -62,6 +73,10 @@ export interface WardenConfig {
|
|
|
62
73
|
stuck: StuckGuardConfig;
|
|
63
74
|
done: DoneGuardConfig;
|
|
64
75
|
slop: SlopGuardConfig;
|
|
76
|
+
/** The status line above the editor and the trace panel. */
|
|
77
|
+
widget: WidgetConfig;
|
|
78
|
+
/** Show steer messages in the transcript. They are always visible in the trace panel. */
|
|
79
|
+
steerVisible: boolean;
|
|
65
80
|
}
|
|
66
81
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
67
82
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { COMMAND_TOOLS } from "./tools.js";
|
|
5
|
+
import { defaultWidgetConfig } from "./widget.js";
|
|
4
6
|
export const PACKAGE_NAME = "pi-warden";
|
|
5
7
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
6
8
|
export function defaultConfig() {
|
|
@@ -12,7 +14,7 @@ export function defaultConfig() {
|
|
|
12
14
|
maxRequests: 500,
|
|
13
15
|
action: {
|
|
14
16
|
enabled: true,
|
|
15
|
-
tools: [
|
|
17
|
+
tools: [...COMMAND_TOOLS, "write", "edit"],
|
|
16
18
|
failOpen: true,
|
|
17
19
|
timeoutMs: 5000,
|
|
18
20
|
irreversible: { warn: 0.5, confirm: 0.7 },
|
|
@@ -20,7 +22,9 @@ export function defaultConfig() {
|
|
|
20
22
|
},
|
|
21
23
|
stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, nudge: true },
|
|
22
24
|
done: { enabled: true, claimsDone: 0.7, nudge: true },
|
|
23
|
-
slop: { enabled: true,
|
|
25
|
+
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
26
|
+
widget: defaultWidgetConfig(),
|
|
27
|
+
steerVisible: false,
|
|
24
28
|
};
|
|
25
29
|
}
|
|
26
30
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
|
@@ -65,9 +69,6 @@ function positiveInteger(value, fallback) {
|
|
|
65
69
|
export function isMode(value) {
|
|
66
70
|
return value === "steer" || value === "confirm" || value === "advise";
|
|
67
71
|
}
|
|
68
|
-
function score(value, fallback) {
|
|
69
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 2 ? value : fallback;
|
|
70
|
-
}
|
|
71
72
|
function applyAction(base, raw, timeoutMs) {
|
|
72
73
|
const withTimeout = { ...base, timeoutMs };
|
|
73
74
|
if (!isObject(raw))
|
|
@@ -100,10 +101,36 @@ function applyDone(base, raw) {
|
|
|
100
101
|
return base;
|
|
101
102
|
return { enabled: boolean(raw.enabled, base.enabled), claimsDone: probability(raw.claimsDone, base.claimsDone), nudge: boolean(raw.nudge, base.nudge) };
|
|
102
103
|
}
|
|
104
|
+
function applyProse(base, raw) {
|
|
105
|
+
if (!isObject(raw))
|
|
106
|
+
return base;
|
|
107
|
+
return {
|
|
108
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
109
|
+
audience: typeof raw.audience === "string" && raw.audience.trim() ? raw.audience.trim() : base.audience,
|
|
110
|
+
threshold: probability(raw.threshold, base.threshold),
|
|
111
|
+
trend: Math.min(3, positiveInteger(raw.trend, base.trend)),
|
|
112
|
+
minChars: positiveInteger(raw.minChars, base.minChars),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
103
115
|
function applySlop(base, raw) {
|
|
104
116
|
if (!isObject(raw))
|
|
105
117
|
return base;
|
|
106
|
-
|
|
118
|
+
// 0.2.x used `placeholder` for the stub threshold; it still sets the shared threshold.
|
|
119
|
+
return { enabled: boolean(raw.enabled, base.enabled), threshold: probability(raw.threshold ?? raw.placeholder, base.threshold), prose: applyProse(base.prose, raw.prose) };
|
|
120
|
+
}
|
|
121
|
+
function applyWidget(base, raw) {
|
|
122
|
+
if (!isObject(raw))
|
|
123
|
+
return base;
|
|
124
|
+
const template = (value, fallback) => (typeof value === "string" && value.trim() ? value : fallback);
|
|
125
|
+
return {
|
|
126
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
127
|
+
placement: raw.placement === "belowEditor" || raw.placement === "aboveEditor" ? raw.placement : base.placement,
|
|
128
|
+
shortcut: typeof raw.shortcut === "string" ? raw.shortcut.trim() : base.shortcut,
|
|
129
|
+
action: template(raw.action, base.action),
|
|
130
|
+
stuck: template(raw.stuck, base.stuck),
|
|
131
|
+
done: template(raw.done, base.done),
|
|
132
|
+
prose: template(raw.prose, base.prose),
|
|
133
|
+
};
|
|
107
134
|
}
|
|
108
135
|
/** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
|
|
109
136
|
function applyShared(base, raw) {
|
|
@@ -132,6 +159,8 @@ export function applyUserOverrides(base, raw) {
|
|
|
132
159
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
133
160
|
...shared,
|
|
134
161
|
...applyGuards(base, raw, shared.timeoutMs),
|
|
162
|
+
widget: applyWidget(base.widget, raw.widget),
|
|
163
|
+
steerVisible: boolean(raw.steerVisible, base.steerVisible),
|
|
135
164
|
};
|
|
136
165
|
}
|
|
137
166
|
/** Project files may tune the guards but cannot grant TypeSafe consent, change the mode, or raise budgets. */
|
package/dist/done.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface RunEvidence {
|
|
|
14
14
|
}>;
|
|
15
15
|
}
|
|
16
16
|
export declare function emptyEvidence(): RunEvidence;
|
|
17
|
-
export declare function recordOutcome(evidence: RunEvidence, outcome: ToolOutcome, input: Record<string, unknown
|
|
17
|
+
export declare function recordOutcome(evidence: RunEvidence, outcome: ToolOutcome, input: Record<string, unknown>, tool?: string): void;
|
|
18
18
|
interface MessageLike {
|
|
19
19
|
role: string;
|
|
20
20
|
content?: unknown;
|
|
@@ -82,5 +82,5 @@ export interface DoneOptions {
|
|
|
82
82
|
export declare function evaluateDone(task: string | undefined, finalMessage: string, evidence: RunEvidence, options: DoneOptions): Promise<DoneVerdict>;
|
|
83
83
|
/** Follow-up for the agent: verify or say plainly that nothing was verified. */
|
|
84
84
|
export declare function doneNudge(verdict: DoneVerdict): string;
|
|
85
|
-
export declare function formatDone(verdict: DoneVerdict): string;
|
|
85
|
+
export declare function formatDone(verdict: DoneVerdict, template?: string): string;
|
|
86
86
|
export {};
|
package/dist/done.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { choice, noul, TypeSafeIntegrationError } from "pi-typesafe";
|
|
2
2
|
import { isReadOnlyCommand } from "./guard.js";
|
|
3
3
|
import { redact } from "./redact.js";
|
|
4
|
+
import { commandOf } from "./tools.js";
|
|
5
|
+
import { DEFAULT_TEMPLATES, doneTokens, renderTemplate } from "./widget.js";
|
|
4
6
|
/** Commands whose success is evidence that the work was verified. */
|
|
5
7
|
const CHECK_COMMAND = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|check|lint|typecheck|build|verify|ci)\b|(?:npx|pnpm|bunx)\s+(?:tsc|jest|vitest|mocha|eslint|biome|prettier\s+--check)\b|pytest|jest|vitest|mocha|tsc|eslint|biome\s+check|ruff|mypy|flake8|pylint|black\s+--check|cargo\s+(?:test|check|build|clippy)|go\s+(?:test|vet|build)|make\s+(?:test|check|lint|build)|mvn\s+(?:test|verify)|gradle\w*\s+(?:test|check|build)|dotnet\s+(?:test|build)|node\s+--test|deno\s+(?:test|check|lint)|rspec|rake\s+test|mix\s+test|phpunit|swift\s+(?:test|build)|xcodebuild\s+test|ctest|zig\s+(?:test|build))\b/;
|
|
6
8
|
/**
|
|
@@ -12,20 +14,22 @@ export function classifyToolResult(tool, input, failed) {
|
|
|
12
14
|
return "mutation";
|
|
13
15
|
if (tool === "read" || tool === "grep" || tool === "find" || tool === "ls")
|
|
14
16
|
return "read";
|
|
15
|
-
|
|
17
|
+
const view = commandOf(tool, input);
|
|
18
|
+
if (!view)
|
|
16
19
|
return "unknown";
|
|
17
|
-
if (CHECK_COMMAND.test(
|
|
20
|
+
if (CHECK_COMMAND.test(view.command))
|
|
18
21
|
return failed ? "check-fail" : "check-pass";
|
|
19
|
-
return isReadOnlyCommand(
|
|
22
|
+
return view.shell && isReadOnlyCommand(view.command) ? "read" : "unknown";
|
|
20
23
|
}
|
|
21
24
|
export function emptyEvidence() {
|
|
22
25
|
return { mutations: 0, checks: [] };
|
|
23
26
|
}
|
|
24
|
-
export function recordOutcome(evidence, outcome, input) {
|
|
27
|
+
export function recordOutcome(evidence, outcome, input, tool = "bash") {
|
|
25
28
|
if (outcome === "mutation")
|
|
26
29
|
evidence.mutations++;
|
|
27
30
|
if (outcome === "check-pass" || outcome === "check-fail") {
|
|
28
|
-
const
|
|
31
|
+
const command = commandOf(tool, input)?.command;
|
|
32
|
+
const call = command !== undefined ? redact(command.length > 200 ? `${command.slice(0, 200)}…` : command) : "check";
|
|
29
33
|
evidence.checks.push({ call, passed: outcome === "check-pass" });
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -110,12 +114,6 @@ export function doneNudge(verdict) {
|
|
|
110
114
|
const detail = failed.length ? `The last check that ran failed: ${failed.at(-1).call}. Fix that first.` : "Run the project's tests, build, or lint (whatever exists) on what you changed.";
|
|
111
115
|
return `pi-warden: ${verdict.reasons.join("; ")}. ${detail} Then report the actual result. If no check exists or can run, say so explicitly instead of presenting the work as done.`;
|
|
112
116
|
}
|
|
113
|
-
export function formatDone(verdict) {
|
|
114
|
-
|
|
115
|
-
if (verdict.judgment)
|
|
116
|
-
parts.push(`claims done ${verdict.judgment.claimsDone.toFixed(2)}`, `claims verified ${verdict.judgment.claimsVerified.toFixed(2)}`, `checks apply ${verdict.judgment.verificationApplies.toFixed(2)}`, verdict.judgment.outcome);
|
|
117
|
-
if (verdict.error)
|
|
118
|
-
parts.push("typesafe error");
|
|
119
|
-
parts.push(verdict.falseClaim ? "false claim" : verdict.unverified ? "unverified" : "ok");
|
|
120
|
-
return parts.join(" · ");
|
|
117
|
+
export function formatDone(verdict, template = DEFAULT_TEMPLATES.done) {
|
|
118
|
+
return renderTemplate(template, doneTokens(verdict));
|
|
121
119
|
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Verdict } from "./guard.js";
|
|
2
|
+
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
3
3
|
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; the last few tool calls and output tails when the agent keeps failing; and the agent's final message when it reports completion without running checks. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
4
4
|
export declare function confirmMessage(verdict: Verdict): string;
|
|
5
|
+
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
6
|
+
export declare function slopSteer(where: string, symptoms: readonly SlopSymptom[], counts: Record<SlopSymptom, number>): string;
|
|
5
7
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
6
8
|
export default function wardenExtension(pi: ExtensionAPI): void;
|