pi-warden 0.28.4 → 0.34.1
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/CHANGELOG.md +154 -0
- package/README.md +5 -1
- package/dist/audit.d.ts +22 -0
- package/dist/audit.js +110 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +9 -0
- package/dist/conscience-config.d.ts +21 -0
- package/dist/conscience-config.js +82 -0
- package/dist/conscience-loader.d.ts +26 -0
- package/dist/conscience-loader.js +82 -0
- package/dist/conscience-policy.d.ts +24 -0
- package/dist/conscience-policy.js +28 -0
- package/dist/conscience-privacy.d.ts +4 -0
- package/dist/conscience-privacy.js +11 -0
- package/dist/discover.d.ts +14 -0
- package/dist/discover.js +85 -0
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +213 -35
- package/dist/extension.js.bak +213 -35
- package/dist/guard.d.ts +127 -1
- package/dist/guard.js +447 -31
- package/dist/holds.d.ts +2 -0
- package/dist/holds.js +7 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +5 -2
- package/dist/init.d.ts +36 -0
- package/dist/init.js +199 -0
- package/dist/laya-download.d.ts +40 -0
- package/dist/laya-download.js +288 -0
- package/dist/laya-judge.d.ts +62 -0
- package/dist/laya-judge.js +495 -0
- package/dist/learning.d.ts +30 -0
- package/dist/learning.js +53 -10
- package/dist/redact.js +9 -4
- package/dist/rules-file.d.ts +18 -0
- package/dist/rules-file.js +72 -0
- package/dist/rules.d.ts +8 -0
- package/dist/rules.js +54 -3
- package/dist/shape.js +9 -1
- package/dist/tool-recommendation.d.ts +15 -0
- package/dist/tool-recommendation.js +31 -0
- package/dist/widget.d.ts +3 -1
- package/dist/widget.js +16 -13
- package/package.json +2 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { redact } from "./redact.js";
|
|
2
|
+
/** URLs are not local paths; protect them before scanning free text for filesystem spans. */
|
|
3
|
+
export function safeConscienceText(text, disclosed = "") {
|
|
4
|
+
const urls = [];
|
|
5
|
+
const redacted = redact(text);
|
|
6
|
+
const masked = redacted.replace(/https?:\/\/[^\s<>"'`]+/gi, value => `\u0001${urls.push(value) - 1}\u0002`);
|
|
7
|
+
const clean = masked.replace(/(?:file:\/\/[^\s<>"'`]+|[A-Za-z]:[\\/][^\s<>"'`]+|\\\\[^\s<>"'`]+|~\/[^\s<>"'`]+|(?<![\w/:])\/(?:[^\s<>"'`]+))/g, path => disclosed.includes(path) ? path : "[local path]");
|
|
8
|
+
return clean.replace(/\u0001(\d+)\u0002/g, (_, index) => urls[Number(index)] ?? "[URL]");
|
|
9
|
+
}
|
|
10
|
+
export const safeCapabilityName = (name) => /^[A-Za-z0-9_.:\-]{1,128}$/.test(name) && redact(name) === name;
|
|
11
|
+
export const excludedCapability = (name, patterns) => patterns.some(pattern => new RegExp(`^${pattern.split("*").map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Directories to skip during source-file discovery. */
|
|
2
|
+
export declare const SKIP_DIRS: Set<string>;
|
|
3
|
+
export declare const SOURCE_RE: RegExp;
|
|
4
|
+
/**
|
|
5
|
+
* Recursively walk `dir` and collect source-file paths (relative to `dir`).
|
|
6
|
+
* Skips directories in SKIP_DIRS and stops after `max` files.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discoverSourceFiles(dir: string, max?: number): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Find project directories inside a workspace. A project is any directory
|
|
11
|
+
* containing a known manifest file. The workspace root itself counts if it
|
|
12
|
+
* has one.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findProjects(cwd: string): string[];
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { redact } from "./redact.js";
|
|
4
|
+
/** Directories to skip during source-file discovery. */
|
|
5
|
+
export const SKIP_DIRS = new Set([
|
|
6
|
+
".git", "node_modules", "dist", "build", ".next", ".output",
|
|
7
|
+
"coverage", ".cache", "__pycache__", ".pi-warden", "vendor",
|
|
8
|
+
".turbo", ".vercel", ".netlify", ".svelte-kit",
|
|
9
|
+
]);
|
|
10
|
+
export const SOURCE_RE = /\.(ts|tsx|js|jsx|rs|py|go|vue|svelte|rb|php|java|kt|swift|cs|cpp|c|h)$/;
|
|
11
|
+
/**
|
|
12
|
+
* Recursively walk `dir` and collect source-file paths (relative to `dir`).
|
|
13
|
+
* Skips directories in SKIP_DIRS and stops after `max` files.
|
|
14
|
+
*/
|
|
15
|
+
export function discoverSourceFiles(dir, max = 200) {
|
|
16
|
+
const files = [];
|
|
17
|
+
function walk(current) {
|
|
18
|
+
if (files.length >= max)
|
|
19
|
+
return;
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.warn(`audit: could not list ${redact(current)}: ${err instanceof Error ? err.message : err}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (files.length >= max)
|
|
30
|
+
return;
|
|
31
|
+
if (entry.name.startsWith(".") && entry.name !== ".")
|
|
32
|
+
continue;
|
|
33
|
+
const fullPath = join(current, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
if (SKIP_DIRS.has(entry.name))
|
|
36
|
+
continue;
|
|
37
|
+
walk(fullPath);
|
|
38
|
+
}
|
|
39
|
+
else if (entry.isFile() && SOURCE_RE.test(entry.name)) {
|
|
40
|
+
files.push(fullPath.slice(dir.length + 1)); // relative path
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
walk(dir);
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Find project directories inside a workspace. A project is any directory
|
|
49
|
+
* containing a known manifest file. The workspace root itself counts if it
|
|
50
|
+
* has one.
|
|
51
|
+
*/
|
|
52
|
+
export function findProjects(cwd) {
|
|
53
|
+
const projects = [];
|
|
54
|
+
const manifests = ["package.json", "Cargo.toml", "pyproject.toml", "go.mod"];
|
|
55
|
+
for (const m of manifests) {
|
|
56
|
+
if (existsSync(join(cwd, m))) {
|
|
57
|
+
projects.push(cwd);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(cwd);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.warn(`audit: could not read workspace ${redact(cwd)}: ${err instanceof Error ? err.message : err}`);
|
|
67
|
+
return projects;
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry === "node_modules" || entry.startsWith("."))
|
|
71
|
+
continue;
|
|
72
|
+
const sub = join(cwd, entry);
|
|
73
|
+
try {
|
|
74
|
+
if (!existsSync(join(sub, "package.json")) && !existsSync(join(sub, "Cargo.toml")) && !existsSync(join(sub, "pyproject.toml")) && !existsSync(join(sub, "go.mod")))
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.warn(`audit: could not check ${redact(sub)}: ${err instanceof Error ? err.message : err}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!projects.includes(sub))
|
|
82
|
+
projects.push(sub);
|
|
83
|
+
}
|
|
84
|
+
return projects;
|
|
85
|
+
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
4
|
import type { ShapeResult } from "./shape.js";
|
|
5
|
-
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, with the agent's own words from the message that makes the call (its stated plan); for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; 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; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary,
|
|
5
|
+
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, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request, including calls where the rules guard is disabled; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; 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; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
6
6
|
/** Which guard spent the user's attention. The status line reports one count per guard. */
|
|
7
7
|
export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "prose" | "runaway" | "subagent";
|
|
8
8
|
/**
|
package/dist/extension.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import * as tuiModule from "@earendil-works/pi-tui";
|
|
3
5
|
const MouseRegion = tuiModule.MouseRegion;
|
|
4
6
|
import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
@@ -9,15 +11,18 @@ import { ArmingTracker, unparseableArmingRules } from "./arming.js";
|
|
|
9
11
|
import * as configModule from "./config.js";
|
|
10
12
|
import { applyUserOverrides, defaultConfig, getNestedValue, isMode, loadConfig, PACKAGE_NAME, parseConfigValue, projectConfigPath, readUserConfig, setNestedValue, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
11
13
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome as recordDoneOutcome } from "./done.js";
|
|
12
|
-
import { evaluateAction,
|
|
14
|
+
import { evaluateAction, formatVerdictTokens, higher, inertPathRules, intentSteer, offTaskSteer, shouldProceedMessage, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, writeSinkTargets } from "./guard.js";
|
|
13
15
|
import { commandOf } from "./tools.js";
|
|
14
16
|
import { formatHolds, HoldLedger, HoldLog, holdLogPath, outcomeNote, regretsAt, textRegrets } from "./holds.js";
|
|
15
|
-
import { initSchema, recordHold, recordOutcome, toHoldRecord, generateRecommendations, analyzeSteerEffectivenessReport } from "./learning.js";
|
|
17
|
+
import { initSchema, recordHold, recordOutcome, toHoldRecord, holdStats, generateRecommendations, analyzeSteerEffectivenessReport } from "./learning.js";
|
|
16
18
|
import { evaluateProse, proseNudge, ProseTrend, RESTATE_MIN_SENTENCES, RESTATE_SHARE, RestatementWindow, substantiveSentences } from "./prose.js";
|
|
17
19
|
import { compressOutput, duplicateNote, evaluateOutput, mergeOutput, outputKey, saveOutput, securityNotice, CompressionLearner } from "./output.js";
|
|
18
20
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
19
21
|
import { redact } from "./redact.js";
|
|
20
22
|
import { formatRules, pathNoteSteer, RulesGuard, rulesSteer } from "./rules.js";
|
|
23
|
+
import { checkPiWardenMissing } from "./rules-file.js";
|
|
24
|
+
import { writeStarterRules, buildInitPrompt } from "./init.js";
|
|
25
|
+
import { buildAuditPrompt, findProjects, snapshotReport, reportOutcome } from "./audit.js";
|
|
21
26
|
import { detectNotifier, sendNotification } from "./notify.js";
|
|
22
27
|
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
23
28
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
@@ -26,8 +31,8 @@ import { completeConfig, shapeWarning } from "./shape.js";
|
|
|
26
31
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
27
32
|
import { formatWake, newReports, reportLabel, triageReport, WakePolicy } from "./subagent.js";
|
|
28
33
|
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
29
|
-
import { DEFAULT_TEMPLATES, LEVEL_COLOR, pickSentenceTemplate, proseTokens, renderTemplate, SENTENCE_TEMPLATES, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
30
|
-
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, with the agent's own words from the message that makes the call (its stated plan); for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; 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; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary,
|
|
34
|
+
import { actionTokens, DEFAULT_TEMPLATES, LEVEL_COLOR, pickSentenceTemplate, proseTokens, renderTemplate, SENTENCE_TEMPLATES, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
35
|
+
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, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request, including calls where the rules guard is disabled; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; 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; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
31
36
|
const WIDGET = PACKAGE_NAME;
|
|
32
37
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
33
38
|
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, offPlan: 0, offTask: 0, slop: 0, ruleChecks: 0, ruleViolations: 0, pathNotes: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, runaway: 0, errors: 0, steers: 0, steersSkipped: 0, steerGuards: {}, subagentReports: 0, subagentWoken: 0, restatements: 0 });
|
|
@@ -126,6 +131,28 @@ function activeMode(config, hasUI) {
|
|
|
126
131
|
const mode = isMode(env) ? env : config.mode;
|
|
127
132
|
return mode === "confirm" && !hasUI ? "steer" : mode;
|
|
128
133
|
}
|
|
134
|
+
/** Keep diagnostics intact while removing structured trace-only items at the agent boundary. */
|
|
135
|
+
function agentDeliveryReasons(verdict) {
|
|
136
|
+
const indexes = [
|
|
137
|
+
verdict.offTaskTraceOnly ? verdict.offTaskTraceOnlyReasonIndex : undefined,
|
|
138
|
+
verdict.shouldProceedTraceOnly ? verdict.shouldProceedTraceOnlyReasonIndex : undefined,
|
|
139
|
+
].filter((index) => index !== undefined && index >= 0 && index < verdict.reasons.length);
|
|
140
|
+
if (!indexes.length)
|
|
141
|
+
return verdict.reasons;
|
|
142
|
+
return verdict.reasons.filter((_reason, reasonIndex) => !indexes.includes(reasonIndex));
|
|
143
|
+
}
|
|
144
|
+
/** Keep the full judgment while suppressing trace-only reasons and steer flags. */
|
|
145
|
+
function agentDeliveryVerdict(verdict) {
|
|
146
|
+
const reasons = agentDeliveryReasons(verdict);
|
|
147
|
+
if (reasons === verdict.reasons)
|
|
148
|
+
return verdict;
|
|
149
|
+
const deliveryVerdict = { ...verdict, reasons };
|
|
150
|
+
if (verdict.offTaskTraceOnly)
|
|
151
|
+
delete deliveryVerdict.offTaskSteer;
|
|
152
|
+
if (verdict.shouldProceedTraceOnly)
|
|
153
|
+
delete deliveryVerdict.shouldProceedSteer;
|
|
154
|
+
return deliveryVerdict;
|
|
155
|
+
}
|
|
129
156
|
function clip(text) {
|
|
130
157
|
return text.length <= CONFIRM_TEXT_LIMIT ? text : `${text.slice(0, CONFIRM_TEXT_LIMIT)}…`;
|
|
131
158
|
}
|
|
@@ -202,7 +229,7 @@ export default function wardenExtension(pi) {
|
|
|
202
229
|
const rulesGuard = new RulesGuard();
|
|
203
230
|
// Hold feedback: what the user did after each judged call, the trace entry each label lands on, and the per-session log.
|
|
204
231
|
const holds = new HoldLedger();
|
|
205
|
-
const learningIds = new Map(); // holds.id -> learning.id
|
|
232
|
+
const learningIds = new Map(); // holds.id -> promise of learning.id
|
|
206
233
|
// Prune learningIds when it grows large to prevent memory leaks
|
|
207
234
|
function pruneLearningIds() {
|
|
208
235
|
if (learningIds.size > 1000) {
|
|
@@ -226,6 +253,11 @@ export default function wardenExtension(pi) {
|
|
|
226
253
|
let evidence = emptyEvidence();
|
|
227
254
|
let doneNudged = false;
|
|
228
255
|
let warnedFallback = false;
|
|
256
|
+
let warnedMissingRules = false;
|
|
257
|
+
/** True while /warden init is sending a prompt and waiting for the agent to generate pi-warden.md. */
|
|
258
|
+
let initRunning = false;
|
|
259
|
+
/** True while /warden audit is sending a prompt and waiting for the agent to write the report. */
|
|
260
|
+
let auditRunning = false;
|
|
229
261
|
const prose = new ProseTrend();
|
|
230
262
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
231
263
|
const ledger = new ContextLedger();
|
|
@@ -343,7 +375,20 @@ export default function wardenExtension(pi) {
|
|
|
343
375
|
const color = LEVEL_COLOR[chip ?? ""] ?? "text";
|
|
344
376
|
const chipText = chip ? `${theme.bold(theme.fg(color, chip.toUpperCase()))} ` : "";
|
|
345
377
|
const guardText = theme.fg("muted", lastEntry.guard + " ");
|
|
346
|
-
|
|
378
|
+
// Like widgetLines: the head width is tracked, the body wraps to what remains, and a line wider than
|
|
379
|
+
// the pane trips pi's render-width guard, which aborts the session.
|
|
380
|
+
const chipWidth = chip ? chip.length + 2 : 0;
|
|
381
|
+
const headWidth = chipWidth + lastEntry.guard.length + 1;
|
|
382
|
+
const head = chipText + guardText;
|
|
383
|
+
const body = {
|
|
384
|
+
render: (width) => {
|
|
385
|
+
if (!width)
|
|
386
|
+
return [head + sentence];
|
|
387
|
+
const wrapped = tuiModule.wrapTextWithAnsi(sentence, Math.max(10, width - headWidth));
|
|
388
|
+
return [head + (wrapped[0] ?? ""), ...wrapped.slice(1).map(rest => " ".repeat(headWidth) + rest)];
|
|
389
|
+
},
|
|
390
|
+
invalidate: () => { },
|
|
391
|
+
};
|
|
347
392
|
return MouseRegion ? new MouseRegion(body, event => {
|
|
348
393
|
if (event.type !== "click" || event.button !== "left")
|
|
349
394
|
return undefined;
|
|
@@ -353,7 +398,7 @@ export default function wardenExtension(pi) {
|
|
|
353
398
|
}, { placement: config.widget.placement });
|
|
354
399
|
return;
|
|
355
400
|
}
|
|
356
|
-
entries = [entries.at(-1)];
|
|
401
|
+
entries = [lastEntry ?? entries.at(-1)];
|
|
357
402
|
}
|
|
358
403
|
// A custom component so the lines wrap to the pane and a click (fullscreen mode) opens the trace panel.
|
|
359
404
|
// When the host TUI lacks MouseRegion (e.g. omp 18.2.5), the extension still loads — the widget
|
|
@@ -370,6 +415,7 @@ export default function wardenExtension(pi) {
|
|
|
370
415
|
}, { placement: config.widget.placement });
|
|
371
416
|
};
|
|
372
417
|
const record = (ctx, config, guard, line, details, tokens) => {
|
|
418
|
+
widget.delete(guard);
|
|
373
419
|
widget.set(guard, line);
|
|
374
420
|
const entry = { at: Date.now(), guard, line, details, tokens };
|
|
375
421
|
trace.push(entry);
|
|
@@ -382,6 +428,10 @@ export default function wardenExtension(pi) {
|
|
|
382
428
|
const entry = traceOf.get(item);
|
|
383
429
|
if (entry)
|
|
384
430
|
trace.amend(entry, outcomeNote(item));
|
|
431
|
+
const idPromise = learningIds.get(item.id);
|
|
432
|
+
if (idPromise && item.outcome !== "pending") {
|
|
433
|
+
idPromise.then(id => recordOutcome(id, item.outcome)).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
434
|
+
}
|
|
385
435
|
}
|
|
386
436
|
if (config.action.feedbackLog && holds.records().length)
|
|
387
437
|
void holdLog?.save(holds.records());
|
|
@@ -484,6 +534,8 @@ export default function wardenExtension(pi) {
|
|
|
484
534
|
client = undefined;
|
|
485
535
|
budgetExhausted = false;
|
|
486
536
|
warnedFallback = false;
|
|
537
|
+
warnedMissingRules = false;
|
|
538
|
+
initRunning = false;
|
|
487
539
|
await initSchema(loadConfig().learning.retentionDays);
|
|
488
540
|
stats = freshStats();
|
|
489
541
|
widget.clear();
|
|
@@ -614,6 +666,17 @@ export default function wardenExtension(pi) {
|
|
|
614
666
|
if (!config.action.enabled || !config.action.tools.includes(event.toolName))
|
|
615
667
|
return;
|
|
616
668
|
stats.inspected++;
|
|
669
|
+
// First-run warning: pi-warden.md missing, one time per session.
|
|
670
|
+
if (!warnedMissingRules && config.rules.enabled) {
|
|
671
|
+
const { missing, fallbackSource } = checkPiWardenMissing(ctx.cwd);
|
|
672
|
+
if (missing && ctx.hasUI) {
|
|
673
|
+
warnedMissingRules = true;
|
|
674
|
+
const msg = fallbackSource
|
|
675
|
+
? `No pi-warden.md detected. Using ${fallbackSource} as active fallback rules. Run /warden init to create project-specific rules.`
|
|
676
|
+
: `No rules file detected (pi-warden.md, README.md, CLAUDE.md, or AGENTS.md). Run /warden init to create project-specific rules.`;
|
|
677
|
+
ctx.ui.notify(msg, "warning");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
617
680
|
// Arming: a write/edit to a protected path arms matching command patterns for a window. Bash redirect/tee
|
|
618
681
|
// targets that match a when.edited glob also arm. This is session state, not a per-call verdict — it runs
|
|
619
682
|
// before the action guard so the armed check on a later command sees the preparation.
|
|
@@ -683,6 +746,8 @@ export default function wardenExtension(pi) {
|
|
|
683
746
|
if (verdict.source === "read-only")
|
|
684
747
|
verdict.source = "pattern";
|
|
685
748
|
}
|
|
749
|
+
const deliveryVerdict = agentDeliveryVerdict(verdict);
|
|
750
|
+
const deliveryReasons = deliveryVerdict.reasons;
|
|
686
751
|
if (regretCandidates.length && verdict.judgment?.regretted !== undefined) {
|
|
687
752
|
settleRegret(config, { regretted: regretsAt(verdict.judgment.regretted), target: verdict.judgment.regretTarget, probability: verdict.judgment.regretted, via: "jev" });
|
|
688
753
|
}
|
|
@@ -704,12 +769,9 @@ export default function wardenExtension(pi) {
|
|
|
704
769
|
const released = holds.approved(event.toolName);
|
|
705
770
|
if (released) {
|
|
706
771
|
noteOutcomes(config, [released]);
|
|
707
|
-
const learningId = learningIds.get(released.id);
|
|
708
|
-
if (learningId)
|
|
709
|
-
recordOutcome(learningId, "approved").catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
710
772
|
}
|
|
711
773
|
}
|
|
712
|
-
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(
|
|
774
|
+
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) : undefined;
|
|
713
775
|
// SAFETY: verdict.source is a string union; the read-only → pattern rewrite above may have narrowed it in TS's view,
|
|
714
776
|
// but the field is still one of the source values at runtime when no armed hits fired.
|
|
715
777
|
const actionFmt = formatVerdictTokens(verdict, config.widget.action);
|
|
@@ -727,19 +789,17 @@ export default function wardenExtension(pi) {
|
|
|
727
789
|
if (ctxSummary)
|
|
728
790
|
recordOpts.contextSummary = ctxSummary;
|
|
729
791
|
if (held)
|
|
730
|
-
recordOpts.agentReason = steerReason(
|
|
792
|
+
recordOpts.agentReason = steerReason(deliveryVerdict, { canApprove: judge !== undefined });
|
|
731
793
|
const item = holds.record(verdict, recordOpts);
|
|
732
794
|
traceOf.set(item, entry);
|
|
795
|
+
// Record to SQLite for learning (held and judged-allowed calls).
|
|
796
|
+
// The promise and map entry must exist before noteOutcomes so that an outcome
|
|
797
|
+
// known at record time (e.g. dialog-approved) is not lost to the race.
|
|
798
|
+
const preview = redact(verdict.summary.command ?? verdict.summary.path ?? "");
|
|
799
|
+
const holdPromise = recordHold(toHoldRecord({ at: item.at, tool: item.tool, level: item.level, reasons: item.reasons, scores: item.scores, held }, ctx.cwd, { task: task ? redact(task) : task, plan: verdict.plan, contextSummary: ctxSummary, agentReason: held ? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) : undefined, preview })).catch(err => { console.warn("pi-warden: recordHold failed:", err); return -1; });
|
|
800
|
+
learningIds.set(item.id, holdPromise);
|
|
801
|
+
pruneLearningIds();
|
|
733
802
|
noteOutcomes(config, outcome ? [item] : []);
|
|
734
|
-
// Record to SQLite for learning
|
|
735
|
-
if (held) {
|
|
736
|
-
recordHold(toHoldRecord({ at: item.at, tool: item.tool, level: item.level, reasons: item.reasons, scores: item.scores }, ctx.cwd, { task: task ? redact(task) : task, plan: verdict.plan, contextSummary: ctxSummary, agentReason: steerReason(verdict, { canApprove: judge !== undefined }) })).then(id => {
|
|
737
|
-
learningIds.set(item.id, id);
|
|
738
|
-
if (outcome)
|
|
739
|
-
recordOutcome(id, outcome).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
740
|
-
}).catch(err => console.warn("pi-warden: recordHold failed:", err));
|
|
741
|
-
pruneLearningIds();
|
|
742
|
-
}
|
|
743
803
|
};
|
|
744
804
|
// The warn notice below names the mismatch to the user; the agent gets the steer with the other notes.
|
|
745
805
|
if (verdict.intentMismatch) {
|
|
@@ -749,8 +809,14 @@ export default function wardenExtension(pi) {
|
|
|
749
809
|
}
|
|
750
810
|
if (verdict.offTaskSteer) {
|
|
751
811
|
stats.offTask++;
|
|
812
|
+
if (!verdict.offTaskTraceOnly) {
|
|
813
|
+
noteGuards.add("action");
|
|
814
|
+
notes.push(offTaskSteer(verdict));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (verdict.shouldProceedSteer && !verdict.shouldProceedTraceOnly) {
|
|
752
818
|
noteGuards.add("action");
|
|
753
|
-
notes.push(
|
|
819
|
+
notes.push(shouldProceedMessage(verdict));
|
|
754
820
|
}
|
|
755
821
|
if (verdict.slopSymptoms?.length && verdict.slopReasons) {
|
|
756
822
|
stats.slop++;
|
|
@@ -800,13 +866,15 @@ export default function wardenExtension(pi) {
|
|
|
800
866
|
traceNote(delivered);
|
|
801
867
|
pathNoteTraces.length = 0;
|
|
802
868
|
}
|
|
803
|
-
const warnSteer = (
|
|
869
|
+
const warnSteer = (reasons) => reasons.length > 0 && steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${reasons.join("; ")}). Nobody sees this in a headless run, so it is on you: if the flagged risk is expected, continue; otherwise fix it or ask the user before building on it.`);
|
|
804
870
|
if (verdict.level === "warn") {
|
|
805
871
|
stats.warned++;
|
|
806
872
|
if (ctx.hasUI && config.notices)
|
|
807
873
|
ctx.ui.notify(`warden · ${event.toolName}: ${verdict.reasons.join("; ")}`, "warning");
|
|
808
|
-
else if (!ctx.hasUI)
|
|
809
|
-
|
|
874
|
+
else if (!ctx.hasUI) {
|
|
875
|
+
const otherReasons = deliveryReasons.filter(r => !r.startsWith("should-proceed "));
|
|
876
|
+
warnSteer(otherReasons);
|
|
877
|
+
}
|
|
810
878
|
track(false);
|
|
811
879
|
return undefined;
|
|
812
880
|
}
|
|
@@ -816,13 +884,14 @@ export default function wardenExtension(pi) {
|
|
|
816
884
|
if (ctx.hasUI && config.notices)
|
|
817
885
|
ctx.ui.notify(`warden · blocked ${event.toolName}: ${verdict.reasons.join("; ")}`, "error");
|
|
818
886
|
notifyDesktop(ctx, config, `Blocked ${event.toolName}: ${verdict.reasons.join("; ")}. A deny rule matched; the call never ran.`);
|
|
819
|
-
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${
|
|
887
|
+
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${deliveryReasons.join("; ")}). A deny rule matched; this command is not allowed to run. Ask the user if this is genuinely required.` };
|
|
820
888
|
}
|
|
821
889
|
if (verdict.level !== "confirm") {
|
|
822
890
|
track(false);
|
|
823
891
|
return undefined;
|
|
824
892
|
}
|
|
825
893
|
const reasons = verdict.reasons.join("; ");
|
|
894
|
+
const deliveredReasons = deliveryReasons.join("; ");
|
|
826
895
|
// A user-defined confirm rule prompts the user in every mode, advise included: the operator wrote the rule to be
|
|
827
896
|
// asked. Advise mode keeps Jev holds advisory; it does not soften a prompt the operator asked for by name.
|
|
828
897
|
// The action defaults to dialog at parse time (the reason one writes such a rule); hold restores steer semantics.
|
|
@@ -836,14 +905,14 @@ export default function wardenExtension(pi) {
|
|
|
836
905
|
}
|
|
837
906
|
stats.held++;
|
|
838
907
|
track(true, "declined", "dialog");
|
|
839
|
-
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${
|
|
908
|
+
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${deliveredReasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
840
909
|
}
|
|
841
910
|
if (mode === "advise") {
|
|
842
911
|
stats.warned++;
|
|
843
912
|
if (ctx.hasUI && config.notices)
|
|
844
913
|
ctx.ui.notify(`warden · ${event.toolName} (advise mode, not held): ${reasons}`, "warning");
|
|
845
914
|
else if (!ctx.hasUI)
|
|
846
|
-
warnSteer(
|
|
915
|
+
warnSteer(deliveryReasons);
|
|
847
916
|
track(false);
|
|
848
917
|
return undefined;
|
|
849
918
|
}
|
|
@@ -856,7 +925,7 @@ export default function wardenExtension(pi) {
|
|
|
856
925
|
}
|
|
857
926
|
stats.held++;
|
|
858
927
|
track(true, "declined", "dialog");
|
|
859
|
-
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${
|
|
928
|
+
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${deliveredReasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
860
929
|
}
|
|
861
930
|
stats.held++;
|
|
862
931
|
actionGuard.hold(task);
|
|
@@ -864,7 +933,7 @@ export default function wardenExtension(pi) {
|
|
|
864
933
|
if (ctx.hasUI && config.notices)
|
|
865
934
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
866
935
|
notifyDesktop(ctx, config, `Held ${event.toolName}: ${reasons}. The agent will re-plan or ask you in chat.`);
|
|
867
|
-
return { block: true, reason: told ?? steerReason(
|
|
936
|
+
return { block: true, reason: told ?? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) };
|
|
868
937
|
});
|
|
869
938
|
pi.on("tool_result", async (event, ctx) => {
|
|
870
939
|
const config = configFor(ctx);
|
|
@@ -1086,6 +1155,17 @@ export default function wardenExtension(pi) {
|
|
|
1086
1155
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1087
1156
|
await checkSubagentReports(ctx, configFor(ctx));
|
|
1088
1157
|
});
|
|
1158
|
+
// Block new user messages while /warden init or /warden audit is running.
|
|
1159
|
+
pi.on("input", async (event, ctx) => {
|
|
1160
|
+
if (initRunning) {
|
|
1161
|
+
ctx.ui.notify("pi-warden is generating rules. Please wait...", "warning");
|
|
1162
|
+
return { action: "handled" };
|
|
1163
|
+
}
|
|
1164
|
+
if (auditRunning) {
|
|
1165
|
+
ctx.ui.notify("pi-warden is running an audit. Please wait...", "warning");
|
|
1166
|
+
return { action: "handled" };
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1089
1169
|
pi.on("agent_end", async (event, ctx) => {
|
|
1090
1170
|
const config = configFor(ctx);
|
|
1091
1171
|
if (!config.enabled)
|
|
@@ -1182,7 +1262,7 @@ export default function wardenExtension(pi) {
|
|
|
1182
1262
|
togglePanel(ctx.ui, configFor(ctx)); },
|
|
1183
1263
|
});
|
|
1184
1264
|
}
|
|
1185
|
-
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace"];
|
|
1265
|
+
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace", "init", "audit"];
|
|
1186
1266
|
pi.registerCommand("warden", {
|
|
1187
1267
|
description: "pi-warden status, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, and a synthetic guard test",
|
|
1188
1268
|
getArgumentCompletions(prefix) {
|
|
@@ -1204,6 +1284,10 @@ export default function wardenExtension(pi) {
|
|
|
1204
1284
|
const source = consentSource(config);
|
|
1205
1285
|
const usage = client?.getUsage();
|
|
1206
1286
|
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.rules.enabled && "rules", config.context.enabled && "context", config.runaway.enabled && "runaway", config.subagent.enabled && "subagent triage", config.notify.enabled && "desktop notifications"].filter(Boolean).join(", ");
|
|
1287
|
+
const ls = await holdStats(ctx.cwd);
|
|
1288
|
+
const lifetimeLine = ls.labeled === 0
|
|
1289
|
+
? `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, not yet measurable (${ls.allowed} allowed).`
|
|
1290
|
+
: `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, ${ls.labeled} labeled, ${ls.declined + ls.replanned} stood (${ls.declined + ls.replanned}/${ls.labeled}), ${ls.allowed} allowed (${ls.accepted} accepted, ${ls.regretted} regretted).`;
|
|
1207
1291
|
report([
|
|
1208
1292
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.typesafeBackend !== "typesafe" ? ` (${config.typesafeBackend})` : ""}; ${auth.text}`,
|
|
1209
1293
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.offPlan} off plan, ${stats.offTask} off task, ${stats.slop} slop notes, ${stats.ruleViolations}/${stats.ruleChecks} rule violations, ${stats.pathNotes} sensitive-path notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.runaway} runaway stops, ${stats.subagentWoken}/${stats.subagentReports} subagent reports woken, ${stats.restatements} restatements, ${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)"}. Steer budget: ${config.steerBudget === 0 ? "off" : `${config.steerBudget} per run`}.`,
|
|
@@ -1212,6 +1296,7 @@ export default function wardenExtension(pi) {
|
|
|
1212
1296
|
formatLedger(ledger.snapshot()),
|
|
1213
1297
|
...(config.learning.patternAnalysis ? [`Learning: ${(await generateRecommendations(ctx.cwd)).length} recommendations, steer effectiveness ${Math.round((await analyzeSteerEffectivenessReport(ctx.cwd)).overall * 100)}% (use /warden recommend for details)`] : []),
|
|
1214
1298
|
`${formatHolds(holds.snapshot(), config.action.feedbackLog ? holdLog?.path : undefined)}${holdLog?.lastFailure ? ` Log write failed: ${holdLog.lastFailure}.` : ""}`,
|
|
1299
|
+
lifetimeLine,
|
|
1215
1300
|
`Rules: ${config.rules.enabled ? `${rulesGuard.describe(ctx.cwd, config.rules)}${Object.keys(config.rules.sensitivePaths).length ? `; ${Object.keys(config.rules.sensitivePaths).length} sensitive path${Object.keys(config.rules.sensitivePaths).length === 1 ? "" : "s"}` : ""}` : "off"}.`,
|
|
1216
1301
|
...(config.action.armingRules.length > 0 ? [`Arming: ${arming.statusLine() || "no rules armed"}.`] : []),
|
|
1217
1302
|
`Desktop notifications: ${config.notify.enabled ? `on (${config.notify.command.length ? `command ${config.notify.command[0]}` : (await (notifier ??= detectNotifier())) ?? "no notifier found on this machine"}; cooldown ${config.notify.cooldownMs} ms)` : "off (\"notify\": { \"enabled\": true } in the config turns them on)"}.`,
|
|
@@ -1339,14 +1424,107 @@ export default function wardenExtension(pi) {
|
|
|
1339
1424
|
openConfigPanel(lastUi, config, { width: config.widget.panelWidth });
|
|
1340
1425
|
return;
|
|
1341
1426
|
}
|
|
1427
|
+
if (action === "init") {
|
|
1428
|
+
const targetPath = join(ctx.cwd, "pi-warden.md");
|
|
1429
|
+
const force = argument === "--force";
|
|
1430
|
+
if (existsSync(targetPath)) {
|
|
1431
|
+
if (ctx.hasUI && !force) {
|
|
1432
|
+
if (!await ctx.ui.confirm("pi-warden.md already exists. Overwrite with a fresh starter template?", `This replaces ${targetPath} with a generic starter. Your current rules will be lost.`)) {
|
|
1433
|
+
report("Cancelled. Existing pi-warden.md unchanged.");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
else if (!force) {
|
|
1438
|
+
report(`pi-warden.md already exists at ${targetPath}. Pass --force to overwrite.`, "warning");
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const prompt = buildInitPrompt(ctx.cwd);
|
|
1443
|
+
initRunning = true;
|
|
1444
|
+
if (ctx.hasUI)
|
|
1445
|
+
ctx.ui.notify("pi-warden: Generating rules file...", "info");
|
|
1446
|
+
try {
|
|
1447
|
+
// sendUserMessage throws when the agent is not idle. Brief wait so a
|
|
1448
|
+
// just-closing confirm dialog does not cause a race.
|
|
1449
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1450
|
+
if (ctx.isIdle())
|
|
1451
|
+
break;
|
|
1452
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1453
|
+
}
|
|
1454
|
+
pi.sendUserMessage(prompt);
|
|
1455
|
+
await ctx.waitForIdle();
|
|
1456
|
+
}
|
|
1457
|
+
catch (err) {
|
|
1458
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1459
|
+
report(`pi-warden init failed: ${detail}. Try creating pi-warden.md manually.`, "error");
|
|
1460
|
+
}
|
|
1461
|
+
finally {
|
|
1462
|
+
initRunning = false;
|
|
1463
|
+
}
|
|
1464
|
+
const wardenExists = existsSync(join(ctx.cwd, "pi-warden.md"));
|
|
1465
|
+
report(wardenExists ? "pi-warden.md created. Review the rules and edit as needed." : "Agent did not create pi-warden.md. Create it manually or try /warden init again.");
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (action === "audit") {
|
|
1469
|
+
if (!ctx.hasUI) {
|
|
1470
|
+
report("Audit needs an interactive session to run.", "warning");
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (!await ctx.ui.confirm("Run workspace audit?", `This runs an agent-driven audit of ${redact(ctx.cwd)}. It uses the session model, reads source code, and writes a report. It may take several minutes and use real tokens. Measured comparisons need the agent's TypeSafe tool, which is enabled per session with \`/typesafe enable\`; without it findings will be marked unmeasured.`))
|
|
1474
|
+
return;
|
|
1475
|
+
const projects = findProjects(ctx.cwd);
|
|
1476
|
+
const prompt = buildAuditPrompt(ctx.cwd, projects);
|
|
1477
|
+
const reportPath = join(ctx.cwd, ".pi-warden", "audit-report.html");
|
|
1478
|
+
const preSnapshot = snapshotReport(reportPath);
|
|
1479
|
+
auditRunning = true;
|
|
1480
|
+
if (ctx.hasUI)
|
|
1481
|
+
ctx.ui.notify("pi-warden: Running workspace audit...", "info");
|
|
1482
|
+
try {
|
|
1483
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1484
|
+
if (ctx.isIdle())
|
|
1485
|
+
break;
|
|
1486
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1487
|
+
}
|
|
1488
|
+
pi.sendUserMessage(prompt);
|
|
1489
|
+
await ctx.waitForIdle();
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1493
|
+
report(`pi-warden audit failed: ${detail}.`, "error");
|
|
1494
|
+
}
|
|
1495
|
+
finally {
|
|
1496
|
+
auditRunning = false;
|
|
1497
|
+
}
|
|
1498
|
+
const outcome = reportOutcome(preSnapshot, snapshotReport(reportPath));
|
|
1499
|
+
switch (outcome) {
|
|
1500
|
+
case "written":
|
|
1501
|
+
report(`Audit report written to ${reportPath}`);
|
|
1502
|
+
break;
|
|
1503
|
+
case "stale":
|
|
1504
|
+
report("Audit finished but the report file was not updated — the agent may have reported findings in chat instead.");
|
|
1505
|
+
break;
|
|
1506
|
+
case "missing":
|
|
1507
|
+
report("Agent did not write an audit report. The model may have reported findings in chat instead.");
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1342
1512
|
if (action === "test") {
|
|
1343
1513
|
const judge = judgeFor(config);
|
|
1344
|
-
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "
|
|
1514
|
+
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Prepare the demo environment") goes to ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
|
|
1345
1515
|
return;
|
|
1346
|
-
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "
|
|
1516
|
+
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Prepare the demo environment" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge });
|
|
1517
|
+
const deliveryVerdict = ctx.hasUI ? verdict : agentDeliveryVerdict(verdict);
|
|
1518
|
+
const deliveryReasons = deliveryVerdict.reasons;
|
|
1347
1519
|
const fmt = formatVerdictTokens(verdict, config.widget.action);
|
|
1348
1520
|
record(ctx, config, "action", fmt.line, actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }), fmt.tokens);
|
|
1349
|
-
|
|
1521
|
+
const deliveryTokens = actionTokens(deliveryVerdict);
|
|
1522
|
+
if (!ctx.hasUI && verdict.offTaskTraceOnly) {
|
|
1523
|
+
// Redact only off-task presentation tokens; retain the judgment for other consumers.
|
|
1524
|
+
delete deliveryTokens.offTask;
|
|
1525
|
+
delete deliveryTokens.scope;
|
|
1526
|
+
}
|
|
1527
|
+
report(`${renderTemplate(DEFAULT_TEMPLATES.action, deliveryTokens)}${deliveryReasons.length ? ` — ${deliveryReasons.join("; ")}` : ""}${judge ? "" : " (pattern checks only: TypeSafe judgments are not enabled or no key is configured)"}${verdict.error ? ` — ${verdict.error}` : ""}`);
|
|
1350
1528
|
if (verdict.level === "confirm") {
|
|
1351
1529
|
const mode = activeMode(config, ctx.hasUI);
|
|
1352
1530
|
if (mode === "confirm" && ctx.hasUI) {
|
|
@@ -1354,7 +1532,7 @@ export default function wardenExtension(pi) {
|
|
|
1354
1532
|
report(allowed ? "Demo: you chose Yes, so a real call would have run." : "Demo: you chose No, so a real call would have been blocked and the agent told why.");
|
|
1355
1533
|
}
|
|
1356
1534
|
else {
|
|
1357
|
-
report(`In ${mode} mode a real call would ${mode === "advise" ? "run with this warning shown to you" : "be held and the agent would read"}: "${steerReason(
|
|
1535
|
+
report(`In ${mode} mode a real call would ${mode === "advise" ? "run with this warning shown to you" : "be held and the agent would read"}: "${steerReason(deliveryVerdict, { canApprove: judge !== undefined })}"`);
|
|
1358
1536
|
}
|
|
1359
1537
|
}
|
|
1360
1538
|
return;
|