pi-warden 0.5.3 → 0.5.4
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/dist/done.d.ts +6 -1
- package/dist/done.js +28 -1
- package/dist/extension.js +17 -10
- package/dist/guard.js +17 -0
- package/package.json +1 -1
package/dist/done.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ export type ToolOutcome = "read" | "mutation" | "check-pass" | "check-fail" | "u
|
|
|
5
5
|
* What a finished tool call contributes to the run's evidence. Only write/edit count as code changes: shell side effects
|
|
6
6
|
* (deleting a temp dir, installing a package) are too varied to demand a test run for. Custom tools are unknown.
|
|
7
7
|
*/
|
|
8
|
-
export declare function classifyToolResult(tool: string, input: Record<string, unknown>, failed: boolean): ToolOutcome;
|
|
8
|
+
export declare function classifyToolResult(tool: string, input: Record<string, unknown>, failed: boolean, output?: string): ToolOutcome;
|
|
9
|
+
/**
|
|
10
|
+
* Recognise a test/type-check runner's own summary in tool output: node:test, jest/vitest, pytest, cargo, go test,
|
|
11
|
+
* tsc. Returns the outcome the summary reports, or undefined when no runner summary is present.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkSummary(output: string): "pass" | "fail" | undefined;
|
|
9
14
|
export interface RunEvidence {
|
|
10
15
|
mutations: number;
|
|
11
16
|
checks: Array<{
|
package/dist/done.js
CHANGED
|
@@ -9,7 +9,7 @@ const CHECK_COMMAND = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|check|lin
|
|
|
9
9
|
* What a finished tool call contributes to the run's evidence. Only write/edit count as code changes: shell side effects
|
|
10
10
|
* (deleting a temp dir, installing a package) are too varied to demand a test run for. Custom tools are unknown.
|
|
11
11
|
*/
|
|
12
|
-
export function classifyToolResult(tool, input, failed) {
|
|
12
|
+
export function classifyToolResult(tool, input, failed, output) {
|
|
13
13
|
if (tool === "write" || tool === "edit")
|
|
14
14
|
return "mutation";
|
|
15
15
|
if (tool === "read" || tool === "grep" || tool === "find" || tool === "ls")
|
|
@@ -19,8 +19,35 @@ export function classifyToolResult(tool, input, failed) {
|
|
|
19
19
|
return "unknown";
|
|
20
20
|
if (CHECK_COMMAND.test(view.command))
|
|
21
21
|
return failed ? "check-fail" : "check-pass";
|
|
22
|
+
// A test runner launched from inside a script (ctx_execute JavaScript, a Python wrapper) leaves no runner name in the
|
|
23
|
+
// command text, but its output still carries the runner's summary. Judge that summary instead.
|
|
24
|
+
const summary = output === undefined ? undefined : checkSummary(output);
|
|
25
|
+
if (summary)
|
|
26
|
+
return summary === "fail" || failed ? "check-fail" : "check-pass";
|
|
22
27
|
return view.shell && isReadOnlyCommand(view.command) ? "read" : "unknown";
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Recognise a test/type-check runner's own summary in tool output: node:test, jest/vitest, pytest, cargo, go test,
|
|
31
|
+
* tsc. Returns the outcome the summary reports, or undefined when no runner summary is present.
|
|
32
|
+
*/
|
|
33
|
+
export function checkSummary(output) {
|
|
34
|
+
const tail = output.slice(-6000);
|
|
35
|
+
const nodeTest = /\u2139 (?:tests|pass|fail) \d+/.test(tail) && /\u2139 fail (\d+)/.exec(tail);
|
|
36
|
+
if (nodeTest)
|
|
37
|
+
return Number(nodeTest[1]) > 0 ? "fail" : "pass";
|
|
38
|
+
const jest = /^Tests:\s+(?:(\d+) failed, )?.*?\d+ total/m.exec(tail);
|
|
39
|
+
if (jest)
|
|
40
|
+
return jest[1] && Number(jest[1]) > 0 ? "fail" : "pass";
|
|
41
|
+
const pytest = /^=+ .*?(?:(\d+) failed|(\d+) error).*?in [\d.]+s/m.exec(tail) ?? /^=+ (\d+) passed.*? in [\d.]+s =+$/m.exec(tail);
|
|
42
|
+
if (pytest)
|
|
43
|
+
return /\d+ (?:failed|error)/.test(pytest[0]) ? "fail" : "pass";
|
|
44
|
+
const cargoOrGo = /^test result: (ok|FAILED)\./m.exec(tail) ?? /^(ok|FAIL)\s+\S+\s+[\d.]+s$/m.exec(tail);
|
|
45
|
+
if (cargoOrGo)
|
|
46
|
+
return cargoOrGo[1] === "ok" ? "pass" : "fail";
|
|
47
|
+
if (/\berror TS\d{4}:/.test(tail))
|
|
48
|
+
return "fail";
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
24
51
|
export function emptyEvidence() {
|
|
25
52
|
return { mutations: 0, checks: [] };
|
|
26
53
|
}
|
package/dist/extension.js
CHANGED
|
@@ -76,7 +76,6 @@ export function confirmMessage(verdict) {
|
|
|
76
76
|
lines.push("Judgments are model output, not authorization. Yes runs the tool; No blocks it and tells the agent.");
|
|
77
77
|
return lines.join("\n");
|
|
78
78
|
}
|
|
79
|
-
const callKey = (tool, input) => `${tool}\0${JSON.stringify(input)}`;
|
|
80
79
|
const SLOP_FIXES = {
|
|
81
80
|
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",
|
|
82
81
|
comments: "delete comments that restate the code; keep only those that explain intent, constraints, or non-obvious behaviour",
|
|
@@ -99,8 +98,13 @@ export default function wardenExtension(pi) {
|
|
|
99
98
|
const trace = new Trace();
|
|
100
99
|
let panel;
|
|
101
100
|
let lastUi;
|
|
102
|
-
/**
|
|
103
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Steer-mode hold state. After a hold, the next guarded call that runs under a *new* user prompt asks Jev whether that
|
|
103
|
+
* prompt approves it. The retry rarely repeats the held string byte for byte (a `command -v` dropped, a different
|
|
104
|
+
* timeout), so approval is judged against the action itself, not matched against the earlier command text.
|
|
105
|
+
*/
|
|
106
|
+
let lastHoldPrompt;
|
|
107
|
+
let holdPending = false;
|
|
104
108
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
105
109
|
let evidence = emptyEvidence();
|
|
106
110
|
let doneNudged = false;
|
|
@@ -179,7 +183,8 @@ export default function wardenExtension(pi) {
|
|
|
179
183
|
widget.clear();
|
|
180
184
|
trace.clear();
|
|
181
185
|
panel?.close();
|
|
182
|
-
|
|
186
|
+
lastHoldPrompt = undefined;
|
|
187
|
+
holdPending = false;
|
|
183
188
|
attempts.reset();
|
|
184
189
|
evidence = emptyEvidence();
|
|
185
190
|
doneNudged = false;
|
|
@@ -215,9 +220,8 @@ export default function wardenExtension(pi) {
|
|
|
215
220
|
return;
|
|
216
221
|
stats.inspected++;
|
|
217
222
|
const task = latestUserPrompt(ctx);
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
const retryAfterHold = held.has(key) && heldAt !== task;
|
|
223
|
+
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
224
|
+
const retryAfterHold = holdPending && lastHoldPrompt !== task;
|
|
221
225
|
const judge = judgeFor(config);
|
|
222
226
|
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 });
|
|
223
227
|
if (verdict.source === "skipped")
|
|
@@ -237,7 +241,7 @@ export default function wardenExtension(pi) {
|
|
|
237
241
|
}
|
|
238
242
|
if (verdict.approvedByUser) {
|
|
239
243
|
stats.approved++;
|
|
240
|
-
|
|
244
|
+
holdPending = false;
|
|
241
245
|
}
|
|
242
246
|
const mode = activeMode(config, ctx.hasUI);
|
|
243
247
|
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
|
|
@@ -275,7 +279,10 @@ export default function wardenExtension(pi) {
|
|
|
275
279
|
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${reasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
276
280
|
}
|
|
277
281
|
stats.held++;
|
|
278
|
-
|
|
282
|
+
// A re-hold under the user's reply keeps the original reference prompt; otherwise the reply could never approve anything.
|
|
283
|
+
if (!holdPending)
|
|
284
|
+
lastHoldPrompt = task;
|
|
285
|
+
holdPending = true;
|
|
279
286
|
if (ctx.hasUI)
|
|
280
287
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
281
288
|
return { block: true, reason: told ?? steerReason(verdict, { canApprove: judge !== undefined }) };
|
|
@@ -339,7 +346,7 @@ export default function wardenExtension(pi) {
|
|
|
339
346
|
// Checks and repeat detection use the original result, not the excerpts or security banner.
|
|
340
347
|
const failed = resultFailed(event.isError, event.details, event.content);
|
|
341
348
|
if (config.done.enabled)
|
|
342
|
-
recordOutcome(evidence, classifyToolResult(event.toolName, event.input, failed), event.input, event.toolName);
|
|
349
|
+
recordOutcome(evidence, classifyToolResult(event.toolName, event.input, failed, text), event.input, event.toolName);
|
|
343
350
|
if (!config.stuck.enabled)
|
|
344
351
|
return patch;
|
|
345
352
|
attempts.push(makeAttempt(event.toolName, event.input, event.content, failed));
|
package/dist/guard.js
CHANGED
|
@@ -287,7 +287,12 @@ export async function evaluateAction(action, options) {
|
|
|
287
287
|
const patterns = matchPatterns(action.tool, action.input, action.cwd);
|
|
288
288
|
const reasons = [];
|
|
289
289
|
let level = "allow";
|
|
290
|
+
// A shell command that merely mentions a secrets file (grep for key names, cat .env.example) is decided after Jev
|
|
291
|
+
// says whether it can write; write/edit on such a path, and offline runs, keep the immediate warning.
|
|
292
|
+
const deferSensitive = judge !== undefined && (action.tool !== "write" && action.tool !== "edit");
|
|
290
293
|
for (const hit of patterns) {
|
|
294
|
+
if (hit.severity === "sensitive" && deferSensitive)
|
|
295
|
+
continue;
|
|
291
296
|
level = higher(level, hit.severity === "destructive" ? "confirm" : "warn");
|
|
292
297
|
reasons.push(`${hit.severity}: ${hit.label}`);
|
|
293
298
|
}
|
|
@@ -323,6 +328,18 @@ export async function evaluateAction(action, options) {
|
|
|
323
328
|
judgment.approved = answers.approved.noul;
|
|
324
329
|
if (typeof answers.mutates?.noul === "number")
|
|
325
330
|
judgment.mutates = answers.mutates.noul;
|
|
331
|
+
if (deferSensitive) {
|
|
332
|
+
for (const hit of patterns) {
|
|
333
|
+
if (hit.severity !== "sensitive")
|
|
334
|
+
continue;
|
|
335
|
+
if ((judgment.mutates ?? 1) >= 0.5) {
|
|
336
|
+
level = higher(level, "warn");
|
|
337
|
+
reasons.push(`${hit.severity}: ${hit.label}`);
|
|
338
|
+
}
|
|
339
|
+
else
|
|
340
|
+
reasons.push(`${hit.label} (read-only, not warned)`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
326
343
|
// write/edit always change something; a command that Jev judges read-only is warned about, never held, for scope alone.
|
|
327
344
|
const canChange = summary.tool === "write" || summary.tool === "edit" || (judgment.mutates ?? 1) >= 0.5;
|
|
328
345
|
if (judgment.irreversible >= config.irreversible.confirm) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Guardrails for Pi built on pi-typesafe that steer the agent instead of interrupting you: Jev judges irreversible and off-task tool calls before they run, detects stuck loops, checks unverified done claims, and flags slop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|