pi-warden 0.3.0 → 0.5.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 +55 -17
- package/dist/config.d.ts +30 -4
- package/dist/config.js +30 -5
- package/dist/extension.d.ts +4 -2
- package/dist/extension.js +140 -19
- package/dist/guard.d.ts +45 -18
- package/dist/guard.js +68 -29
- package/dist/index.d.ts +10 -5
- package/dist/index.js +6 -3
- package/dist/output.d.ts +61 -0
- package/dist/output.js +121 -0
- package/dist/panel.js +1 -1
- package/dist/prose.d.ts +57 -0
- package/dist/prose.js +98 -0
- package/dist/trace.d.ts +3 -1
- package/dist/trace.js +15 -1
- package/dist/widget.d.ts +13 -2
- package/dist/widget.js +27 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,15 +8,15 @@ The verdicts above are real output from `npm run test:live`. Independent project
|
|
|
8
8
|
|
|
9
9
|
## Philosophy
|
|
10
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.
|
|
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. Guard decisions are visible in the trace panel. Existing session entries are never rewritten; output warnings and excerpts are applied before a new tool result enters the session.
|
|
12
12
|
|
|
13
13
|
Three jobs, in order of maturity:
|
|
14
14
|
|
|
15
15
|
| Pillar | What it means | Status |
|
|
16
16
|
| --- | --- | --- |
|
|
17
|
-
| **Security** |
|
|
18
|
-
| **Deslopify** |
|
|
19
|
-
| **Context saving** | Jev
|
|
17
|
+
| **Security** | Action holds and offline patterns protect risky operations. Tool-output checks flag possible injected instructions and credential shapes; insecure written code gets a targeted steer. Output notices are advisory, not a sandbox or a guarantee that an attack is contained. | built |
|
|
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 selects a retention policy for large new tool results; code keeps deterministic excerpts and an exact local copy. Only tail compression is implemented. Cold-cache pruning and compaction integration remain planned. The system prompt and existing history are untouched. | tail MVP |
|
|
20
20
|
|
|
21
21
|
## Why a coding agent needs this
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ Agents are good at picking the next command and bad at noticing when that comman
|
|
|
26
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`.
|
|
27
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.
|
|
28
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.
|
|
29
|
-
- **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.
|
|
30
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.
|
|
31
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.
|
|
32
32
|
|
|
@@ -68,18 +68,40 @@ Keeps the last 12 tool results for the current prompt. When the latest result fa
|
|
|
68
68
|
|
|
69
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.
|
|
70
70
|
|
|
71
|
-
### Slop (`write`/`edit`, same request as the action guard)
|
|
71
|
+
### Slop in code (`write`/`edit`, same request as the action guard)
|
|
72
72
|
|
|
73
73
|
pi-warden cannot rewrite code — Jev only judges — so slop is handled as a feedback loop through the agent:
|
|
74
74
|
|
|
75
|
-
1. When the agent calls `write` or `edit`,
|
|
76
|
-
2.
|
|
77
|
-
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
78
|
|
|
79
|
-
From the
|
|
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.
|
|
80
|
+
|
|
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.
|
|
80
88
|
|
|
81
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.
|
|
82
90
|
|
|
91
|
+
### Tool-output security and tail compression
|
|
92
|
+
|
|
93
|
+
With consent, content-bearing tools (`read`, fetch/search tools and named MCP equivalents) are checked for instructions that redirect the assistant or request private data. Other tools, including shell/context-mode tools, are checked from 2048 characters. Jev receives a redacted 6000-character head/tail sample, so attacks in an unsampled middle can be missed. A score at or above `security.threshold` adds an untrusted-data notice around the text and a hidden steer. Local credential-shape checks work without network access and warn not to echo or commit possible secrets. Images, result details, error flags, and usage are preserved.
|
|
94
|
+
|
|
95
|
+
Written code gets a `security_risk` question in the existing action request: hardcoded credentials, disabled TLS checks, unsafe shell/SQL interpolation, broad permissions, or bypassed verification. A threshold crossing warns and steers; it does not authorize an action or automatically block otherwise safe edits.
|
|
96
|
+
|
|
97
|
+
For a single text block of at least `context.tailMinChars`, the same output request asks Jev to choose `all`, `errors_and_summary`, or `summary_only`. The full result stays when P(keep all) exceeds `1 - context.confidence` (default: keep when Jev gives `all` more than 0.2), when the probability is missing, or on any request error. Code—not Jev—keeps bounded diagnostic/head/tail excerpts. An exact copy is written to an owner-only temporary directory before replacement; the excerpt links to it. If storage fails, the original stays. Multiple text blocks keep their layout and are not compressed. Trace and widget show the actual bytes saved. No extra steer is persisted for compression alone.
|
|
98
|
+
|
|
99
|
+
This MVP is always tail-only: set `context.enabled: false` to stop compression. It never prunes a warm prefix, changes the system prompt, or edits historical session entries. Full-output files can contain secrets; they remain in the OS temporary directory until you or the OS remove them. Excerpts can omit relevant evidence; read the linked file before making a decision that needs it.
|
|
100
|
+
|
|
101
|
+
### Task continuity
|
|
102
|
+
|
|
103
|
+
The action guard receives the latest user message plus up to eight previous user/assistant text messages, limited to 750 redacted characters each. This keeps follow-ups and side comments from replacing the task. New user instructions override earlier ones; assistant text supplies context, not permission. Approval of a held action still uses only the latest user message. `unclear` scope alone no longer triggers an off-task warning; irreversible-action checks are unchanged. Long handoffs outside that bounded context can still require the user to restate the task.
|
|
104
|
+
|
|
83
105
|
## Commands
|
|
84
106
|
|
|
85
107
|
| Command | Effect |
|
|
@@ -103,13 +125,16 @@ Templates in `config.widget` control the text. Segments are separated by ` · `;
|
|
|
103
125
|
"enabled": true,
|
|
104
126
|
"placement": "aboveEditor",
|
|
105
127
|
"shortcut": "ctrl+shift+w",
|
|
106
|
-
"action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop {
|
|
128
|
+
"action": "warden · {tool} · irreversible {irreversible} · off-task {offTask} · {scope} · slop: {slop} · patterns: {patterns} · {flags} · {level}",
|
|
107
129
|
"stuck": "warden · stuck · {failures} failures · same strategy {sameStrategy} · change {approachChange} · progress {progress} · {flags} · {status}",
|
|
108
|
-
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}"
|
|
130
|
+
"done": "warden · done-check · {changes} changes · {checksPassed}/{checks} checks passed · claims done {claimsDone} · claims verified {claimsVerified} · checks apply {checksApply} · {outcome} · {status}",
|
|
131
|
+
"prose": "warden · prose · wordy {wordy} · clichés {cliches} · jargon {jargon} · {flags} · {status}",
|
|
132
|
+
"security": "warden · security · {tool} · injection {injection} · exfiltration {exfiltration} · {status}",
|
|
133
|
+
"context": "warden · context · {tool} · {retention} · saved {bytesSaved} bytes"
|
|
109
134
|
}
|
|
110
135
|
```
|
|
111
136
|
|
|
112
|
-
Tokens — action: `tool level source irreversible offTask scope approved
|
|
137
|
+
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`. Security tokens: `tool injection exfiltration status`; context tokens: `tool retention bytesSaved`. 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.
|
|
113
138
|
|
|
114
139
|
## Configuration
|
|
115
140
|
|
|
@@ -131,8 +156,15 @@ User file `~/.pi/agent/pi-warden/config.json` (owner-only). Missing keys use the
|
|
|
131
156
|
},
|
|
132
157
|
"stuck": { "enabled": true, "window": 12, "minFailures": 3, "cooldown": 3, "sameStrategy": 0.7, "nudge": true },
|
|
133
158
|
"done": { "enabled": true, "claimsDone": 0.7, "nudge": true },
|
|
134
|
-
"
|
|
135
|
-
"
|
|
159
|
+
"security": { "enabled": true, "threshold": 0.7 },
|
|
160
|
+
"context": { "enabled": true, "tailMinChars": 12000, "confidence": 0.8 },
|
|
161
|
+
"slop": {
|
|
162
|
+
"enabled": true,
|
|
163
|
+
"threshold": 0.7,
|
|
164
|
+
"prose": { "enabled": true, "audience": "technical", "threshold": 0.7, "trend": 2, "minChars": 200 }
|
|
165
|
+
},
|
|
166
|
+
"widget": { "enabled": true, "placement": "aboveEditor", "shortcut": "ctrl+shift+w" },
|
|
167
|
+
"steerVisible": false
|
|
136
168
|
}
|
|
137
169
|
```
|
|
138
170
|
|
|
@@ -140,11 +172,15 @@ A project may add `.pi/pi-warden.json` with `enabled` and per-guard overrides (f
|
|
|
140
172
|
|
|
141
173
|
## Data handling
|
|
142
174
|
|
|
143
|
-
- 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
|
|
175
|
+
- 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. Scope and retention requests also carry up to eight prior user/assistant text messages (750 redacted characters each). Output checks send a redacted head/tail sample up to 6000 characters plus size and tool name. No unrelated files or telemetry.
|
|
144
176
|
- 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.
|
|
145
177
|
- 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.
|
|
146
178
|
- Judgments are model output. Thresholds are yours to tune; a hold is information for the agent and for you, not a verdict on either.
|
|
147
179
|
|
|
180
|
+
### After updating the package
|
|
181
|
+
|
|
182
|
+
Run `/reload` in each existing Pi session after an update. If an existing session still reports `Cannot read properties of undefined (reading 'enabled')` while a fresh session works, restart that session to replace loaded modules. Version labels alone do not prove that all in-memory modules match the files on disk. Current config loading merges missing legacy fields with defaults; regression tests cover malformed and pre-prose config files. A persistent crash after restart needs its new stack trace and a redacted config example.
|
|
183
|
+
|
|
148
184
|
## For extension authors
|
|
149
185
|
|
|
150
186
|
The guard is a plain function you can call from your own extension or tests, with any object that has pi-typesafe's `evaluate` method as the judge:
|
|
@@ -170,8 +206,10 @@ Also exported: `matchPatterns`, `isReadOnlyCommand`, `describeAction`, `redact`,
|
|
|
170
206
|
```bash
|
|
171
207
|
npm install
|
|
172
208
|
npm run check # typecheck, offline tests (mocked transport), build
|
|
173
|
-
npm run test:live #
|
|
209
|
+
npm run test:live # 43 billable synthetic judgments across all guards (key from .env or the stored login); pass action|slop|approval|stuck|done|security for one group
|
|
210
|
+
node scripts/security-cases.mjs [context] # the 12-case output-security, task-continuity, and retention set on its own
|
|
174
211
|
npm run dev:pi # start Pi with this working tree plus an installed pi-typesafe
|
|
212
|
+
node scripts/slop-cases.mjs # 22 billable cases that tune the slop and prose questions
|
|
175
213
|
npm run preview # re-render docs/preview.png from the recorded live verdicts (needs a Chrome binary)
|
|
176
214
|
```
|
|
177
215
|
|
package/dist/config.d.ts
CHANGED
|
@@ -36,12 +36,34 @@ export interface DoneGuardConfig {
|
|
|
36
36
|
/** Also send the agent a follow-up asking it to verify. Triggers one more LLM turn. */
|
|
37
37
|
nudge: boolean;
|
|
38
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
|
+
}
|
|
39
50
|
export interface SlopGuardConfig {
|
|
40
51
|
enabled: boolean;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
/** P(symptom) at or above this is reported for written code: stub, comments, dead, hedging. */
|
|
53
|
+
threshold: number;
|
|
54
|
+
prose: ProseConfig;
|
|
55
|
+
}
|
|
56
|
+
export interface SecurityConfig {
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
/** P(injection or exfiltration) at or above this adds an untrusted-output notice. */
|
|
59
|
+
threshold: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ContextConfig {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
/** Only new tool output is compressed; warm history and system prompts are never changed. */
|
|
64
|
+
tailMinChars: number;
|
|
65
|
+
/** Minimum P(the full output is not needed), 1 - P(all), before code removes output. */
|
|
66
|
+
confidence: number;
|
|
45
67
|
}
|
|
46
68
|
export type WardenMode = "steer" | "confirm" | "advise";
|
|
47
69
|
export interface WardenConfig {
|
|
@@ -63,8 +85,12 @@ export interface WardenConfig {
|
|
|
63
85
|
stuck: StuckGuardConfig;
|
|
64
86
|
done: DoneGuardConfig;
|
|
65
87
|
slop: SlopGuardConfig;
|
|
88
|
+
security: SecurityConfig;
|
|
89
|
+
context: ContextConfig;
|
|
66
90
|
/** The status line above the editor and the trace panel. */
|
|
67
91
|
widget: WidgetConfig;
|
|
92
|
+
/** Show steer messages in the transcript. They are always visible in the trace panel. */
|
|
93
|
+
steerVisible: boolean;
|
|
68
94
|
}
|
|
69
95
|
export declare const PACKAGE_NAME = "pi-warden";
|
|
70
96
|
export declare const PROJECT_CONFIG_FILE = "pi-warden.json";
|
package/dist/config.js
CHANGED
|
@@ -22,8 +22,11 @@ export function defaultConfig() {
|
|
|
22
22
|
},
|
|
23
23
|
stuck: { enabled: true, window: 12, minFailures: 3, cooldown: 3, sameStrategy: 0.7, nudge: true },
|
|
24
24
|
done: { enabled: true, claimsDone: 0.7, nudge: true },
|
|
25
|
-
slop: { enabled: true,
|
|
25
|
+
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
26
|
+
security: { enabled: true, threshold: 0.7 },
|
|
27
|
+
context: { enabled: true, tailMinChars: 12000, confidence: 0.8 },
|
|
26
28
|
widget: defaultWidgetConfig(),
|
|
29
|
+
steerVisible: false,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
/** Mirrors Pi's agent directory rule so the file sits next to pi-typesafe's auth.json. */
|
|
@@ -68,9 +71,6 @@ function positiveInteger(value, fallback) {
|
|
|
68
71
|
export function isMode(value) {
|
|
69
72
|
return value === "steer" || value === "confirm" || value === "advise";
|
|
70
73
|
}
|
|
71
|
-
function score(value, fallback) {
|
|
72
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 2 ? value : fallback;
|
|
73
|
-
}
|
|
74
74
|
function applyAction(base, raw, timeoutMs) {
|
|
75
75
|
const withTimeout = { ...base, timeoutMs };
|
|
76
76
|
if (!isObject(raw))
|
|
@@ -103,10 +103,22 @@ function applyDone(base, raw) {
|
|
|
103
103
|
return base;
|
|
104
104
|
return { enabled: boolean(raw.enabled, base.enabled), claimsDone: probability(raw.claimsDone, base.claimsDone), nudge: boolean(raw.nudge, base.nudge) };
|
|
105
105
|
}
|
|
106
|
+
function applyProse(base, raw) {
|
|
107
|
+
if (!isObject(raw))
|
|
108
|
+
return base;
|
|
109
|
+
return {
|
|
110
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
111
|
+
audience: typeof raw.audience === "string" && raw.audience.trim() ? raw.audience.trim() : base.audience,
|
|
112
|
+
threshold: probability(raw.threshold, base.threshold),
|
|
113
|
+
trend: Math.min(3, positiveInteger(raw.trend, base.trend)),
|
|
114
|
+
minChars: positiveInteger(raw.minChars, base.minChars),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
106
117
|
function applySlop(base, raw) {
|
|
107
118
|
if (!isObject(raw))
|
|
108
119
|
return base;
|
|
109
|
-
|
|
120
|
+
// 0.2.x used `placeholder` for the stub threshold; it still sets the shared threshold.
|
|
121
|
+
return { enabled: boolean(raw.enabled, base.enabled), threshold: probability(raw.threshold ?? raw.placeholder, base.threshold), prose: applyProse(base.prose, raw.prose) };
|
|
110
122
|
}
|
|
111
123
|
function applyWidget(base, raw) {
|
|
112
124
|
if (!isObject(raw))
|
|
@@ -119,6 +131,9 @@ function applyWidget(base, raw) {
|
|
|
119
131
|
action: template(raw.action, base.action),
|
|
120
132
|
stuck: template(raw.stuck, base.stuck),
|
|
121
133
|
done: template(raw.done, base.done),
|
|
134
|
+
prose: template(raw.prose, base.prose),
|
|
135
|
+
security: template(raw.security, base.security),
|
|
136
|
+
context: template(raw.context, base.context),
|
|
122
137
|
};
|
|
123
138
|
}
|
|
124
139
|
/** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
|
|
@@ -135,6 +150,15 @@ function applyGuards(base, raw, timeoutMs) {
|
|
|
135
150
|
stuck: applyStuck(base.stuck, raw.stuck),
|
|
136
151
|
done: applyDone(base.done, raw.done),
|
|
137
152
|
slop: applySlop(base.slop, raw.slop),
|
|
153
|
+
security: isObject(raw.security) ? {
|
|
154
|
+
enabled: boolean(raw.security.enabled, base.security.enabled),
|
|
155
|
+
threshold: probability(raw.security.threshold, base.security.threshold),
|
|
156
|
+
} : base.security,
|
|
157
|
+
context: isObject(raw.context) ? {
|
|
158
|
+
enabled: boolean(raw.context.enabled, base.context.enabled),
|
|
159
|
+
tailMinChars: positiveInteger(raw.context.tailMinChars, base.context.tailMinChars),
|
|
160
|
+
confidence: probability(raw.context.confidence, base.context.confidence),
|
|
161
|
+
} : base.context,
|
|
138
162
|
};
|
|
139
163
|
}
|
|
140
164
|
/** Unknown keys and invalid values fall back to the base; nothing throws on a malformed file. */
|
|
@@ -149,6 +173,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
149
173
|
...shared,
|
|
150
174
|
...applyGuards(base, raw, shared.timeoutMs),
|
|
151
175
|
widget: applyWidget(base.widget, raw.widget),
|
|
176
|
+
steerVisible: boolean(raw.steerVisible, base.steerVisible),
|
|
152
177
|
};
|
|
153
178
|
}
|
|
154
179
|
/** Project files may tune the guards but cannot grant TypeSafe consent, change the mode, or raise budgets. */
|
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";
|
|
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;
|
|
2
|
+
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
3
|
+
export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, 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; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and tail compression. Compression stores an exact, owner-only copy in a temporary file on this machine. 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;
|
package/dist/extension.js
CHANGED
|
@@ -3,15 +3,18 @@ import { createTypeSafe, resolveApiKey } from "pi-typesafe";
|
|
|
3
3
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
4
4
|
import { applyUserOverrides, defaultConfig, isMode, loadConfig, PACKAGE_NAME, projectConfigPath, readUserConfig, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
5
5
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome } from "./done.js";
|
|
6
|
-
import { evaluateAction, formatVerdict, steerReason, textApproves } from "./guard.js";
|
|
6
|
+
import { evaluateAction, formatVerdict, SLOP_LABELS, steerReason, textApproves } from "./guard.js";
|
|
7
|
+
import { evaluateProse, proseNudge, ProseTrend } from "./prose.js";
|
|
8
|
+
import { compressOutput, evaluateOutput, saveOutput, securityNotice } from "./output.js";
|
|
9
|
+
import { redact } from "./redact.js";
|
|
7
10
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
8
11
|
import { openTracePanel } from "./panel.js";
|
|
9
|
-
import { actionDetails, doneDetails, stuckDetails, Trace } from "./trace.js";
|
|
10
|
-
import { TOKEN_NAMES } from "./widget.js";
|
|
11
|
-
export 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;
|
|
12
|
+
import { actionDetails, doneDetails, proseDetails, stuckDetails, Trace } from "./trace.js";
|
|
13
|
+
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
14
|
+
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, 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; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and tail compression. Compression stores an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
12
15
|
const WIDGET = PACKAGE_NAME;
|
|
13
16
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
14
|
-
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, slop: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, errors: 0 });
|
|
17
|
+
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, slop: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, errors: 0 });
|
|
15
18
|
function latestUserPrompt(ctx) {
|
|
16
19
|
const entries = ctx.sessionManager.getBranch();
|
|
17
20
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -25,6 +28,28 @@ function latestUserPrompt(ctx) {
|
|
|
25
28
|
}
|
|
26
29
|
return undefined;
|
|
27
30
|
}
|
|
31
|
+
/** Scope context only: approval still comes from latestUserPrompt, never from this history. */
|
|
32
|
+
function recentTaskContext(ctx) {
|
|
33
|
+
const entries = ctx.sessionManager.getBranch();
|
|
34
|
+
const messages = [];
|
|
35
|
+
let skippedLatestUser = false;
|
|
36
|
+
for (let index = entries.length - 1; index >= 0 && messages.length < 8; index--) {
|
|
37
|
+
const entry = entries[index];
|
|
38
|
+
if (entry?.type !== "message")
|
|
39
|
+
continue;
|
|
40
|
+
if (entry.message.role !== "user" && entry.message.role !== "assistant")
|
|
41
|
+
continue;
|
|
42
|
+
const { role, content } = entry.message;
|
|
43
|
+
if (role === "user" && !skippedLatestUser) {
|
|
44
|
+
skippedLatestUser = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const text = typeof content === "string" ? content : content.filter(part => part.type === "text").map(part => part.text).join("\n");
|
|
48
|
+
if (text.trim())
|
|
49
|
+
messages.push({ role, text: redact(text).slice(0, 750) });
|
|
50
|
+
}
|
|
51
|
+
return messages.reverse();
|
|
52
|
+
}
|
|
28
53
|
function activeMode(config, hasUI) {
|
|
29
54
|
const env = process.env.PI_WARDEN_MODE?.trim();
|
|
30
55
|
const mode = isMode(env) ? env : config.mode;
|
|
@@ -49,6 +74,19 @@ export function confirmMessage(verdict) {
|
|
|
49
74
|
return lines.join("\n");
|
|
50
75
|
}
|
|
51
76
|
const callKey = (tool, input) => `${tool}\0${JSON.stringify(input)}`;
|
|
77
|
+
const SLOP_FIXES = {
|
|
78
|
+
stub: "replace stubs, placeholders, and hard-coded fake data with the working implementation, or state in your reply exactly what is left unimplemented and why",
|
|
79
|
+
comments: "delete comments that restate the code; keep only those that explain intent, constraints, or non-obvious behaviour",
|
|
80
|
+
dead: "remove commented-out code, unused imports and variables, duplicated logic, and unreachable branches",
|
|
81
|
+
hedging: "replace \"should work\", \"for now\", and TODOs without a plan with a definite statement or a concrete follow-up",
|
|
82
|
+
};
|
|
83
|
+
/** Names each symptom and its fix; repeats in the session turn the note into a standing rule. */
|
|
84
|
+
export function slopSteer(where, symptoms, counts) {
|
|
85
|
+
const named = symptoms.map(symptom => `${SLOP_LABELS[symptom]}${counts[symptom] >= 3 ? ` (${counts[symptom]}th time this session)` : ""}`).join("; ");
|
|
86
|
+
const fixes = symptoms.map(symptom => SLOP_FIXES[symptom]).join("; ");
|
|
87
|
+
const standing = symptoms.some(symptom => counts[symptom] >= 3) ? " Treat this as a standing rule for the rest of the session." : "";
|
|
88
|
+
return `pi-warden: the content just written to ${where} has ${named}. Fix it in your next edit: ${fixes}.${standing}`;
|
|
89
|
+
}
|
|
52
90
|
/** Native Pi registration; importing the root library does not load this module. */
|
|
53
91
|
export default function wardenExtension(pi) {
|
|
54
92
|
let client;
|
|
@@ -63,6 +101,8 @@ export default function wardenExtension(pi) {
|
|
|
63
101
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
64
102
|
let evidence = emptyEvidence();
|
|
65
103
|
let doneNudged = false;
|
|
104
|
+
const prose = new ProseTrend();
|
|
105
|
+
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
66
106
|
const configFor = (ctx) => loadConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
|
|
67
107
|
const consentGiven = (config) => config.typesafe || process.env.PI_WARDEN_ENABLED === "1";
|
|
68
108
|
const consentSource = (config) => config.typesafe ? "/warden enable" : process.env.PI_WARDEN_ENABLED === "1" ? "PI_WARDEN_ENABLED" : undefined;
|
|
@@ -106,7 +146,7 @@ export default function wardenExtension(pi) {
|
|
|
106
146
|
trace.push({ at: Date.now(), guard, line, details });
|
|
107
147
|
paint(ctx, config);
|
|
108
148
|
};
|
|
109
|
-
const steer = (content) => pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display:
|
|
149
|
+
const steer = (config, content, options = { deliverAs: "steer" }) => pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display: config.steerVisible }, options);
|
|
110
150
|
pi.on("session_start", async (_event, ctx) => {
|
|
111
151
|
client = undefined;
|
|
112
152
|
budgetExhausted = false;
|
|
@@ -117,6 +157,9 @@ export default function wardenExtension(pi) {
|
|
|
117
157
|
attempts.reset();
|
|
118
158
|
evidence = emptyEvidence();
|
|
119
159
|
doneNudged = false;
|
|
160
|
+
prose.reset();
|
|
161
|
+
for (const symptom of Object.keys(slopCounts))
|
|
162
|
+
slopCounts[symptom] = 0;
|
|
120
163
|
if (ctx.hasUI)
|
|
121
164
|
ctx.ui.setWidget(WIDGET, undefined);
|
|
122
165
|
});
|
|
@@ -139,9 +182,12 @@ export default function wardenExtension(pi) {
|
|
|
139
182
|
const heldAt = held.get(key);
|
|
140
183
|
const retryAfterHold = held.has(key) && heldAt !== task;
|
|
141
184
|
const judge = judgeFor(config);
|
|
142
|
-
const verdict = await evaluateAction({ tool: event.toolName, input: event.input, cwd: ctx.cwd, task }, { config: config.action, judge, signal: ctx.signal, slop: config.slop, retryAfterHold });
|
|
185
|
+
const verdict = await evaluateAction({ tool: event.toolName, input: event.input, cwd: ctx.cwd, task, context: recentTaskContext(ctx) }, { config: config.action, judge, signal: ctx.signal, slop: config.slop, security: config.security, retryAfterHold });
|
|
143
186
|
if (verdict.source === "skipped")
|
|
144
187
|
return;
|
|
188
|
+
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold) {
|
|
189
|
+
steer(config, "pi-warden: the proposed write may introduce a security weakness. Check for embedded credentials, disabled TLS, unsafe command/SQL interpolation, broad permissions, or bypassed verification; use a safe implementation instead.");
|
|
190
|
+
}
|
|
145
191
|
if (verdict.judgment)
|
|
146
192
|
stats.judged++;
|
|
147
193
|
if (verdict.source === "error")
|
|
@@ -160,12 +206,14 @@ export default function wardenExtension(pi) {
|
|
|
160
206
|
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
|
|
161
207
|
if (verdict.source !== "read-only")
|
|
162
208
|
record(ctx, config, "action", formatVerdict(verdict, config.widget.action), actionDetails(verdict, { mode, ...(told ? { told } : {}) }));
|
|
163
|
-
if (verdict.
|
|
209
|
+
if (verdict.slopSymptoms?.length && verdict.slopReasons) {
|
|
164
210
|
stats.slop++;
|
|
211
|
+
for (const symptom of verdict.slopSymptoms)
|
|
212
|
+
slopCounts[symptom]++;
|
|
165
213
|
const where = verdict.summary.path ?? event.toolName;
|
|
166
214
|
if (ctx.hasUI)
|
|
167
215
|
ctx.ui.notify(`warden · slop · ${where}: ${verdict.slopReasons.join("; ")}`, "warning");
|
|
168
|
-
steer(
|
|
216
|
+
steer(config, slopSteer(where, verdict.slopSymptoms, slopCounts));
|
|
169
217
|
}
|
|
170
218
|
if (verdict.level === "warn") {
|
|
171
219
|
stats.warned++;
|
|
@@ -199,38 +247,111 @@ export default function wardenExtension(pi) {
|
|
|
199
247
|
const config = configFor(ctx);
|
|
200
248
|
if (!config.enabled)
|
|
201
249
|
return;
|
|
250
|
+
const textBlocks = event.content.filter(part => part.type === "text");
|
|
251
|
+
const text = textBlocks.map(part => part.text).join("\n");
|
|
252
|
+
const output = await evaluateOutput(event.toolName, text, latestUserPrompt(ctx), {
|
|
253
|
+
security: config.security, context: config.context, judge: judgeFor(config), timeoutMs: config.timeoutMs,
|
|
254
|
+
signal: ctx.signal, compressible: textBlocks.length === 1, taskContext: recentTaskContext(ctx),
|
|
255
|
+
});
|
|
256
|
+
if (ctx.signal?.aborted)
|
|
257
|
+
return;
|
|
258
|
+
if (output.error)
|
|
259
|
+
noteError(ctx, output.error, output.errorCode);
|
|
260
|
+
let content = event.content;
|
|
261
|
+
const notice = securityNotice(output);
|
|
262
|
+
const excerpt = compressOutput(text, output.retention);
|
|
263
|
+
if (excerpt && !ctx.signal?.aborted) {
|
|
264
|
+
try {
|
|
265
|
+
const path = await saveOutput(text);
|
|
266
|
+
const replacement = `${excerpt}\n\nFull output: ${path}`;
|
|
267
|
+
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement) - (notice ? Buffer.byteLength(notice) * 2 + 4 : 0);
|
|
268
|
+
if (bytesSaved > 0) {
|
|
269
|
+
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
270
|
+
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: output.retention, bytesSaved: String(bytesSaved) }), [
|
|
271
|
+
`retention: ${output.retention}; confidence ${output.confidence?.toFixed(2)}; ${output.model}; ${output.elapsedMs} ms`,
|
|
272
|
+
`saved ${bytesSaved} bytes; full output: ${path}`,
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
noteError(ctx, "Could not store full output; keeping it unchanged.", undefined);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (notice && textBlocks.length) {
|
|
281
|
+
let index = 0;
|
|
282
|
+
content = content.map(part => {
|
|
283
|
+
if (part.type !== "text")
|
|
284
|
+
return part;
|
|
285
|
+
index++;
|
|
286
|
+
return { ...part, text: `${index === 1 ? `${notice}\n\n` : ""}${part.text}${index === textBlocks.length ? `\n\n${notice}` : ""}` };
|
|
287
|
+
});
|
|
288
|
+
steer(config, notice);
|
|
289
|
+
record(ctx, config, "security", renderTemplate(config.widget.security, {
|
|
290
|
+
tool: event.toolName, injection: output.injection?.toFixed(2), exfiltration: output.exfiltration?.toFixed(2),
|
|
291
|
+
status: [output.suspicious && "untrusted instructions", output.secret && "possible credentials"].filter(Boolean).join(", "),
|
|
292
|
+
}), [
|
|
293
|
+
`jev: injection ${output.injection?.toFixed(2) ?? "not judged"}; exfiltration ${output.exfiltration?.toFixed(2) ?? "not judged"}`,
|
|
294
|
+
`output sample: ${redact(text).slice(0, 300)}`, `agent told: ${notice}`,
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
const patch = content === event.content ? undefined : { content };
|
|
298
|
+
// Checks and repeat detection use the original result, not the excerpts or security banner.
|
|
202
299
|
const failed = resultFailed(event.isError, event.details, event.content);
|
|
203
300
|
if (config.done.enabled)
|
|
204
301
|
recordOutcome(evidence, classifyToolResult(event.toolName, event.input, failed), event.input, event.toolName);
|
|
205
302
|
if (!config.stuck.enabled)
|
|
206
|
-
return;
|
|
303
|
+
return patch;
|
|
207
304
|
attempts.push(makeAttempt(event.toolName, event.input, event.content, failed));
|
|
208
305
|
if (!attempts.shouldJudge(config.stuck))
|
|
209
|
-
return;
|
|
306
|
+
return patch;
|
|
210
307
|
stats.stuckChecks++;
|
|
211
308
|
const verdict = await evaluateStuck(attempts, latestUserPrompt(ctx), { config: config.stuck, judge: judgeFor(config), timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
212
309
|
if (verdict.error)
|
|
213
310
|
noteError(ctx, verdict.error, undefined);
|
|
214
311
|
if (verdict.source === "repeat" && !verdict.stuck)
|
|
215
|
-
return;
|
|
312
|
+
return patch;
|
|
216
313
|
const nudge = verdict.stuck && config.stuck.nudge ? stuckNudge(verdict) : undefined;
|
|
217
314
|
record(ctx, config, "stuck", formatStuck(verdict, config.widget.stuck), stuckDetails(verdict, attempts.attempts, nudge));
|
|
218
315
|
if (!verdict.stuck)
|
|
219
|
-
return;
|
|
316
|
+
return patch;
|
|
220
317
|
stats.stuck++;
|
|
221
318
|
if (ctx.hasUI)
|
|
222
319
|
ctx.ui.notify(`warden · stuck: ${verdict.reasons.join("; ")}${nudge ? " (agent nudged)" : ""}`, "warning");
|
|
223
320
|
if (nudge)
|
|
224
|
-
steer(nudge);
|
|
321
|
+
steer(config, nudge);
|
|
322
|
+
return patch;
|
|
225
323
|
});
|
|
226
324
|
pi.on("agent_end", async (event, ctx) => {
|
|
227
325
|
const config = configFor(ctx);
|
|
228
|
-
if (!config.enabled
|
|
326
|
+
if (!config.enabled)
|
|
229
327
|
return;
|
|
230
328
|
const finalMessage = finalAssistantText(event.messages);
|
|
231
329
|
const judge = judgeFor(config);
|
|
232
330
|
if (!finalMessage || !judge)
|
|
233
331
|
return;
|
|
332
|
+
if (config.slop.enabled && config.slop.prose.enabled && finalMessage.length >= config.slop.prose.minChars) {
|
|
333
|
+
stats.proseChecks++;
|
|
334
|
+
const verdict = await evaluateProse(latestUserPrompt(ctx), finalMessage, { config: config.slop.prose, judge, timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
335
|
+
if (verdict.error)
|
|
336
|
+
noteError(ctx, verdict.error, undefined);
|
|
337
|
+
else
|
|
338
|
+
prose.record(verdict.flagged);
|
|
339
|
+
const due = verdict.error ? [] : prose.due(config.slop.prose.trend);
|
|
340
|
+
const nudge = due.length ? proseNudge(due, config.slop.prose.audience, prose.counts) : undefined;
|
|
341
|
+
if (nudge) {
|
|
342
|
+
verdict.nudged = true;
|
|
343
|
+
prose.markNudged();
|
|
344
|
+
stats.proseNudges++;
|
|
345
|
+
}
|
|
346
|
+
record(ctx, config, "prose", renderTemplate(config.widget.prose, proseTokens(verdict)), proseDetails(verdict, finalMessage, config.slop.prose.audience, nudge));
|
|
347
|
+
if (nudge) {
|
|
348
|
+
if (ctx.hasUI)
|
|
349
|
+
ctx.ui.notify(`warden · prose: ${due.join(", ")} in ${config.slop.prose.trend} of the last 3 replies (agent nudged for the next reply)`, "warning");
|
|
350
|
+
steer(config, nudge, { deliverAs: "nextTurn" });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!config.done.enabled || doneNudged || !needsDoneCheck(evidence))
|
|
354
|
+
return;
|
|
234
355
|
stats.doneChecks++;
|
|
235
356
|
const verdict = await evaluateDone(latestUserPrompt(ctx), finalMessage, evidence, { config: config.done, judge, timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
236
357
|
if (verdict.error)
|
|
@@ -244,7 +365,7 @@ export default function wardenExtension(pi) {
|
|
|
244
365
|
ctx.ui.notify(`warden · done-check: ${verdict.reasons.join("; ")}${nudge ? " (agent asked to verify)" : ""}`, "warning");
|
|
245
366
|
if (nudge) {
|
|
246
367
|
doneNudged = true;
|
|
247
|
-
|
|
368
|
+
steer(config, nudge, { deliverAs: "followUp", triggerTurn: true });
|
|
248
369
|
}
|
|
249
370
|
});
|
|
250
371
|
// Shortcuts are registered once at load; a shape check keeps a typo in the config file from being registered.
|
|
@@ -277,11 +398,11 @@ export default function wardenExtension(pi) {
|
|
|
277
398
|
const key = resolveApiKey();
|
|
278
399
|
const source = consentSource(config);
|
|
279
400
|
const usage = client?.getUsage();
|
|
280
|
-
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop"].filter(Boolean).join(", ");
|
|
401
|
+
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.context.enabled && "context (tail-only)"].filter(Boolean).join(", ");
|
|
281
402
|
report([
|
|
282
403
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `enabled via ${source}` : "disabled (run /warden enable)"}; key ${key ? key.source === "stored" ? "stored (shared with pi-typesafe)" : "from TYPESAFE_API_KEY" : "missing (run /warden enable)"}.`,
|
|
283
|
-
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests.`,
|
|
284
|
-
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / hold ${config.action.offTask.confirm}; stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop
|
|
404
|
+
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}.`,
|
|
405
|
+
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / hold ${config.action.offTask.confirm}; stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop ${config.slop.threshold}, prose ${config.slop.prose.threshold} in ${config.slop.prose.trend}/3 replies; failOpen ${config.action.failOpen}.`,
|
|
285
406
|
`Config: ${userConfigPath()}${ctx.isProjectTrusted() ? ` and ${projectConfigPath(ctx.cwd)}` : ""}.`,
|
|
286
407
|
widget.size ? `Last: ${[...widget.values()].join(" | ")}` : "No guarded activity yet this session.",
|
|
287
408
|
`Trace: ${trace.entries().length} events (/warden trace${shortcut ? `, ${shortcut}` : ""}, or click the status line in fullscreen mode). Widget templates in config.widget: action tokens ${TOKEN_NAMES.action.map(name => `{${name}}`).join(" ")}.`,
|