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 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
  ![Real verdicts from pi-warden: the same command gets a different verdict depending on what the user asked for](https://raw.githubusercontent.com/DevMortimer/pi-warden/main/docs/preview.png)
6
6
 
7
7
  The verdicts above are real output from `npm run test:live`. Independent project; not affiliated with TypeSafe AI or the Pi authors.
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 note.** Stub functions, `TODO: implement later`, mocks returning fake data, filler comments: scored on the same request as the action guard (zero extra latency) and steered back to the agent after the write, never held.
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`, two questions ride on the action guard's request (no extra latency): `slop_quality`, a Score over three described levels — *focused* (does what the task needs, comments only where they add information) / *some filler* (comments restating the code, minor dead code, hedging) / *sloppy* (stub or placeholder code, TODO where a working implementation is needed, duplicated or commented-out logic, vague text) — and `slop_placeholder`, a Noul: does the content leave placeholder, stub, mock, or "implement later" code where `task` needs a working implementation? Jev sees the first 1500 characters of a `write` or the first three replacement texts of an `edit`, plus your request, so "needed" is judged against what you asked for.
64
- 2. Quality ≥ 1.5 or placeholder ≥ 0.7 (config `slop`) trips the note. Below that, nothing happens.
65
- 3. The write **goes through** — holding it would leave a half-written file. Instead the agent receives a steer message before its next LLM call: *"pi-warden: the content just written to `src/x.ts` reads as quality 1.90/2 (filler or sloppy) and placeholder or stub code 0.92. Replace stubs and placeholders with working code, remove comments that restate the code, and keep only what the request needs. If something is intentionally left unimplemented, say so in your reply instead of leaving it in the code."* The agent fixes the file on its next turn; you see a notification and `slop 1.9/2 · stub 0.92` in the widget.
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
- From the live smoke: a `// TODO: implement later` stub scored quality 2.00 / placeholder 0.99, a mock returning fake user data 2.00 / 0.98, a focused implementation 0.00 / 0.02, and a function with only restating comments 0.87 / 0.02 — deliberately below the threshold, because the guard is for stubs and filler that change behaviour, not for style. Not covered: code written through bash heredocs, content past the excerpt limits, and the agent's prose replies.
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": { "enabled": true, "quality": 1.5, "placeholder": 0.7 }
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 content excerpt 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 sends the agent's final message (2000 characters) and the run's check commands. No other files, history, or telemetry.
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 # 28 billable synthetic judgments across all guards (key from .env or the stored login); pass action|slop|approval|stuck|done for one group
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
- /** Score (0 focused … 2 sloppy) at or above this warns. */
41
- quality: number;
42
- /** P(placeholder or stub code) at or above this warns. */
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: ["bash", "write", "edit"],
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, quality: 1.5, placeholder: 0.7 },
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
- return { enabled: boolean(raw.enabled, base.enabled), quality: score(raw.quality, base.quality), placeholder: probability(raw.placeholder, base.placeholder) };
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>): void;
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
- if (tool !== "bash" || typeof input.command !== "string")
17
+ const view = commandOf(tool, input);
18
+ if (!view)
16
19
  return "unknown";
17
- if (CHECK_COMMAND.test(input.command))
20
+ if (CHECK_COMMAND.test(view.command))
18
21
  return failed ? "check-fail" : "check-pass";
19
- return isReadOnlyCommand(input.command) ? "read" : "unknown";
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 call = typeof input.command === "string" ? redact(input.command.length > 200 ? `${input.command.slice(0, 200)}…` : input.command) : "check";
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
- const parts = [`warden · done-check · ${verdict.evidence.mutations} changes · ${verdict.evidence.checks.filter(check => check.passed).length}/${verdict.evidence.checks.length} checks passed`];
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
  }
@@ -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;