pi-warden 0.7.0 → 0.9.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 +168 -100
- package/dist/config.d.ts +44 -1
- package/dist/config.js +53 -4
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +126 -8
- package/dist/guard.d.ts +13 -0
- package/dist/guard.js +122 -6
- package/dist/index.d.ts +11 -5
- package/dist/index.js +6 -3
- package/dist/notify.d.ts +42 -0
- package/dist/notify.js +99 -0
- package/dist/panel.js +1 -1
- package/dist/rules.d.ts +164 -0
- package/dist/rules.js +479 -0
- package/dist/runaway.d.ts +66 -0
- package/dist/runaway.js +115 -0
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +11 -2
- package/dist/trace.d.ts +5 -1
- package/dist/trace.js +24 -0
- package/dist/widget.d.ts +10 -0
- package/dist/widget.js +32 -0
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -9,7 +9,7 @@ export function isRecallTool(value) {
|
|
|
9
9
|
}
|
|
10
10
|
export const PACKAGE_NAME = "pi-warden";
|
|
11
11
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
12
|
-
export const CONFIG_SCHEMA =
|
|
12
|
+
export const CONFIG_SCHEMA = 5;
|
|
13
13
|
export const PROJECT_CONFIG_FILE = `${PACKAGE_NAME}.json`;
|
|
14
14
|
export function defaultConfig() {
|
|
15
15
|
return {
|
|
@@ -30,7 +30,10 @@ export function defaultConfig() {
|
|
|
30
30
|
done: { enabled: true, claimsDone: 0.7, nudge: true },
|
|
31
31
|
slop: { enabled: true, threshold: 0.7, prose: { enabled: true, audience: "technical", threshold: 0.7, trend: 2, minChars: 200 } },
|
|
32
32
|
security: { enabled: true, threshold: 0.7 },
|
|
33
|
+
rules: { enabled: true, threshold: 0.7, files: [], fallback: true, maxChars: 8000, exclude: [], skip: [], sensitivePaths: {} },
|
|
33
34
|
context: { enabled: true, tailMinChars: 12000, confidence: 0.8, duplicateMinChars: 2000, recallTool: "auto", formatConfidence: 0.7 },
|
|
35
|
+
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
36
|
+
notify: { enabled: true, cooldownMs: 10000, command: [] },
|
|
34
37
|
widget: defaultWidgetConfig(),
|
|
35
38
|
steerVisible: false,
|
|
36
39
|
};
|
|
@@ -104,6 +107,26 @@ function applyStuck(base, raw) {
|
|
|
104
107
|
nudge: boolean(raw.nudge, base.nudge),
|
|
105
108
|
};
|
|
106
109
|
}
|
|
110
|
+
function applyRunaway(base, raw) {
|
|
111
|
+
if (!isObject(raw))
|
|
112
|
+
return base;
|
|
113
|
+
return {
|
|
114
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
115
|
+
// One occurrence is not a repeat, so the floor is 2.
|
|
116
|
+
repeats: Math.max(2, positiveInteger(raw.repeats, base.repeats)),
|
|
117
|
+
thinkingRepeats: Math.max(2, positiveInteger(raw.thinkingRepeats, base.thinkingRepeats)),
|
|
118
|
+
minChars: positiveInteger(raw.minChars, base.minChars),
|
|
119
|
+
recover: boolean(raw.recover, base.recover),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function applyNotify(base, raw, allowCommand) {
|
|
123
|
+
if (!isObject(raw))
|
|
124
|
+
return base;
|
|
125
|
+
const cooldown = typeof raw.cooldownMs === "number" && Number.isSafeInteger(raw.cooldownMs) && raw.cooldownMs >= 0 ? raw.cooldownMs : base.cooldownMs;
|
|
126
|
+
const command = allowCommand && Array.isArray(raw.command) && raw.command.every((arg) => typeof arg === "string") && (raw.command.length === 0 || raw.command[0].trim())
|
|
127
|
+
? [...raw.command] : base.command;
|
|
128
|
+
return { enabled: boolean(raw.enabled, base.enabled), cooldownMs: cooldown, command };
|
|
129
|
+
}
|
|
107
130
|
function applyDone(base, raw) {
|
|
108
131
|
if (!isObject(raw))
|
|
109
132
|
return base;
|
|
@@ -126,6 +149,26 @@ function applySlop(base, raw) {
|
|
|
126
149
|
// 0.2.x used `placeholder` for the stub threshold; it still sets the shared threshold.
|
|
127
150
|
return { enabled: boolean(raw.enabled, base.enabled), threshold: probability(raw.threshold ?? raw.placeholder, base.threshold), prose: applyProse(base.prose, raw.prose) };
|
|
128
151
|
}
|
|
152
|
+
function globList(value, fallback) {
|
|
153
|
+
return Array.isArray(value) ? value.filter((glob) => typeof glob === "string" && glob.trim().length > 0).map(glob => glob.trim()) : fallback;
|
|
154
|
+
}
|
|
155
|
+
function applyRules(base, raw) {
|
|
156
|
+
if (!isObject(raw))
|
|
157
|
+
return base;
|
|
158
|
+
const notes = isObject(raw.sensitivePaths)
|
|
159
|
+
? Object.fromEntries(Object.entries(raw.sensitivePaths).filter((entry) => entry[0].trim().length > 0 && typeof entry[1] === "string" && entry[1].trim().length > 0))
|
|
160
|
+
: base.sensitivePaths;
|
|
161
|
+
return {
|
|
162
|
+
enabled: boolean(raw.enabled, base.enabled),
|
|
163
|
+
threshold: probability(raw.threshold, base.threshold),
|
|
164
|
+
files: globList(raw.files, base.files),
|
|
165
|
+
fallback: boolean(raw.fallback, base.fallback),
|
|
166
|
+
maxChars: Math.max(500, positiveInteger(raw.maxChars, base.maxChars)),
|
|
167
|
+
exclude: globList(raw.exclude, base.exclude),
|
|
168
|
+
skip: globList(raw.skip, base.skip),
|
|
169
|
+
sensitivePaths: notes,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
129
172
|
function applyWidget(base, raw) {
|
|
130
173
|
if (!isObject(raw))
|
|
131
174
|
return base;
|
|
@@ -142,6 +185,8 @@ function applyWidget(base, raw) {
|
|
|
142
185
|
prose: template(raw.prose, base.prose),
|
|
143
186
|
security: template(raw.security, base.security),
|
|
144
187
|
context: template(raw.context, base.context),
|
|
188
|
+
runaway: template(raw.runaway, base.runaway),
|
|
189
|
+
rules: template(raw.rules, base.rules),
|
|
145
190
|
};
|
|
146
191
|
}
|
|
147
192
|
/** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
|
|
@@ -152,8 +197,12 @@ function applyShared(base, raw) {
|
|
|
152
197
|
maxRequests: positiveInteger(raw.maxRequests ?? legacy.maxRequests, base.maxRequests),
|
|
153
198
|
};
|
|
154
199
|
}
|
|
155
|
-
function applyGuards(base, raw, timeoutMs) {
|
|
200
|
+
function applyGuards(base, raw, timeoutMs, source) {
|
|
156
201
|
return {
|
|
202
|
+
rules: applyRules(base.rules, raw.rules),
|
|
203
|
+
runaway: applyRunaway(base.runaway, raw.runaway),
|
|
204
|
+
// A project file may switch notifications off or on, but never names a command to run.
|
|
205
|
+
notify: applyNotify(base.notify, raw.notify, source === "user"),
|
|
157
206
|
action: applyAction(base.action, raw.action, timeoutMs),
|
|
158
207
|
stuck: applyStuck(base.stuck, raw.stuck),
|
|
159
208
|
done: applyDone(base.done, raw.done),
|
|
@@ -182,7 +231,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
182
231
|
typesafe: boolean(raw.typesafe, base.typesafe),
|
|
183
232
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
184
233
|
...shared,
|
|
185
|
-
...applyGuards(base, raw, shared.timeoutMs),
|
|
234
|
+
...applyGuards(base, raw, shared.timeoutMs, "user"),
|
|
186
235
|
widget: applyWidget(base.widget, raw.widget),
|
|
187
236
|
steerVisible: boolean(raw.steerVisible, base.steerVisible),
|
|
188
237
|
};
|
|
@@ -191,7 +240,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
191
240
|
export function applyProjectOverrides(base, raw) {
|
|
192
241
|
if (!isObject(raw))
|
|
193
242
|
return base;
|
|
194
|
-
return { ...base, enabled: boolean(raw.enabled, base.enabled), ...applyGuards(base, raw, base.timeoutMs) };
|
|
243
|
+
return { ...base, enabled: boolean(raw.enabled, base.enabled), ...applyGuards(base, raw, base.timeoutMs, "project") };
|
|
195
244
|
}
|
|
196
245
|
export function loadConfig(options = {}) {
|
|
197
246
|
let config = applyUserOverrides(defaultConfig(), readJson(userConfigPath()));
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ToolCallRef } from "./action-guard.js";
|
|
3
3
|
import type { SlopSymptom, Verdict } from "./guard.js";
|
|
4
|
-
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 context saving (retention and output format). Compression and duplicate notes store 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
|
+
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; 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; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store 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.";
|
|
5
5
|
/**
|
|
6
6
|
* Tool calls of the assistant message being preflighted. Pi runs `tool_call` hooks for sibling calls one after another,
|
|
7
7
|
* so judging them one request at a time costs one round trip per call; judging them together costs one round trip.
|
package/dist/extension.js
CHANGED
|
@@ -10,16 +10,19 @@ import { evaluateProse, proseNudge, ProseTrend } from "./prose.js";
|
|
|
10
10
|
import { compressOutput, duplicateNote, evaluateOutput, outputKey, saveOutput, securityNotice } from "./output.js";
|
|
11
11
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
12
12
|
import { redact } from "./redact.js";
|
|
13
|
+
import { formatRules, pathNoteSteer, RulesGuard, rulesSteer } from "./rules.js";
|
|
14
|
+
import { detectNotifier, sendNotification } from "./notify.js";
|
|
15
|
+
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
13
16
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
14
17
|
import { openTracePanel } from "./panel.js";
|
|
15
18
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
16
19
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
17
|
-
import { actionDetails, doneDetails, proseDetails, stuckDetails, Trace } from "./trace.js";
|
|
20
|
+
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
18
21
|
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
19
|
-
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 context saving (retention and output format). Compression and duplicate notes store 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.";
|
|
22
|
+
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; 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; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store 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.";
|
|
20
23
|
const WIDGET = PACKAGE_NAME;
|
|
21
24
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
22
|
-
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 });
|
|
25
|
+
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 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 });
|
|
23
26
|
function latestUserPrompt(ctx) {
|
|
24
27
|
const entries = ctx.sessionManager.getBranch();
|
|
25
28
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -122,14 +125,22 @@ export default function wardenExtension(pi) {
|
|
|
122
125
|
let panel;
|
|
123
126
|
let lastUi;
|
|
124
127
|
const actionGuard = new ActionGuard();
|
|
128
|
+
const rulesGuard = new RulesGuard();
|
|
125
129
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
126
130
|
let evidence = emptyEvidence();
|
|
127
131
|
let doneNudged = false;
|
|
128
132
|
const prose = new ProseTrend();
|
|
129
133
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
130
134
|
const ledger = new ContextLedger();
|
|
135
|
+
const runaway = new RunawayMonitor();
|
|
136
|
+
// Runs stopped by the runaway guard for the current user prompt; the first one gets a recovery turn, later ones wait for the user.
|
|
137
|
+
let runawayStops = 0;
|
|
138
|
+
let pendingRunaway;
|
|
131
139
|
// Probed once per session, outside any tool_result handler; the footer under excerpts names this command.
|
|
132
140
|
let searchTool;
|
|
141
|
+
// Desktop notifier, probed on first use; undefined after the probe means this machine has no route to the desktop.
|
|
142
|
+
let notifier;
|
|
143
|
+
let lastNotifiedAt = 0;
|
|
133
144
|
// A partially updated module graph can hand this build a config without the sections it expects; see shape.ts.
|
|
134
145
|
let shapeReported = false;
|
|
135
146
|
const configFor = (ctx) => {
|
|
@@ -195,6 +206,24 @@ export default function wardenExtension(pi) {
|
|
|
195
206
|
paint(ctx, config);
|
|
196
207
|
};
|
|
197
208
|
const steer = (config, content, options = { deliverAs: "steer" }) => pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content, display: config.steerVisible }, options);
|
|
209
|
+
/**
|
|
210
|
+
* Desktop notification for a moment that needs the user back at the terminal. Interactive sessions only: a headless run
|
|
211
|
+
* or a subagent has nobody to call, and several of them would flood the desktop. Fire-and-forget; failures are silent.
|
|
212
|
+
*/
|
|
213
|
+
const notifyDesktop = (ctx, config, body) => {
|
|
214
|
+
if (!ctx.hasUI || !config.notify.enabled)
|
|
215
|
+
return;
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
if (now - lastNotifiedAt < config.notify.cooldownMs)
|
|
218
|
+
return;
|
|
219
|
+
lastNotifiedAt = now;
|
|
220
|
+
if (config.notify.command.length) {
|
|
221
|
+
void sendNotification(config.notify.command, { title: "pi-warden", body }).catch(() => false);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
notifier ??= detectNotifier();
|
|
225
|
+
void notifier.then(name => (name ? sendNotification(name, { title: "pi-warden", body }) : false)).catch(() => false);
|
|
226
|
+
};
|
|
198
227
|
pi.on("session_start", async (_event, ctx) => {
|
|
199
228
|
client = undefined;
|
|
200
229
|
budgetExhausted = false;
|
|
@@ -203,12 +232,18 @@ export default function wardenExtension(pi) {
|
|
|
203
232
|
trace.clear();
|
|
204
233
|
panel?.close();
|
|
205
234
|
actionGuard.reset();
|
|
235
|
+
rulesGuard.reset();
|
|
206
236
|
attempts.reset();
|
|
207
237
|
evidence = emptyEvidence();
|
|
208
238
|
doneNudged = false;
|
|
209
239
|
prose.reset();
|
|
210
240
|
ledger.reset();
|
|
241
|
+
runaway.reset();
|
|
242
|
+
runawayStops = 0;
|
|
243
|
+
pendingRunaway = undefined;
|
|
211
244
|
searchTool = undefined;
|
|
245
|
+
notifier = undefined;
|
|
246
|
+
lastNotifiedAt = 0;
|
|
212
247
|
for (const symptom of Object.keys(slopCounts))
|
|
213
248
|
slopCounts[symptom] = 0;
|
|
214
249
|
if (ctx.hasUI)
|
|
@@ -218,7 +253,39 @@ export default function wardenExtension(pi) {
|
|
|
218
253
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
219
254
|
attempts = new AttemptWindow(configFor(ctx).stuck.window);
|
|
220
255
|
doneNudged = false;
|
|
256
|
+
runaway.reset();
|
|
257
|
+
runawayStops = 0;
|
|
258
|
+
pendingRunaway = undefined;
|
|
221
259
|
actionGuard.turnEnd();
|
|
260
|
+
rulesGuard.turnEnd();
|
|
261
|
+
});
|
|
262
|
+
// Each assistant message is judged on its own; Pi does not forward the stream's own "start" event, so this is the reset.
|
|
263
|
+
pi.on("message_start", async (event) => {
|
|
264
|
+
if (event.message.role === "assistant")
|
|
265
|
+
runaway.reset();
|
|
266
|
+
});
|
|
267
|
+
// Per token this only appends to a buffer; every 256 characters the buffer is checked for identical blocks, with code only.
|
|
268
|
+
pi.on("message_update", async (event, ctx) => {
|
|
269
|
+
const kind = runaway.feed(event.assistantMessageEvent);
|
|
270
|
+
if (!kind || runaway.stopped)
|
|
271
|
+
return;
|
|
272
|
+
const config = configFor(ctx);
|
|
273
|
+
if (!config.enabled || !config.runaway.enabled)
|
|
274
|
+
return;
|
|
275
|
+
const verdict = runaway.check(kind, config.runaway);
|
|
276
|
+
if (!verdict)
|
|
277
|
+
return;
|
|
278
|
+
stats.runaway++;
|
|
279
|
+
runawayStops++;
|
|
280
|
+
const recover = config.runaway.recover && runawayStops === 1;
|
|
281
|
+
const nudge = runawayNudge(verdict, recover);
|
|
282
|
+
pendingRunaway = { nudge, recover };
|
|
283
|
+
record(ctx, config, "runaway", formatRunaway(verdict, recover, config.widget.runaway), runawayDetails(verdict, nudge, recover));
|
|
284
|
+
if (ctx.hasUI)
|
|
285
|
+
ctx.ui.notify(`warden · runaway: the same ${verdict.kind} block repeated ${verdict.count} times in ${verdict.chars} chars; run stopped${recover ? " (agent gets one follow-up turn)" : " (not restarted: second time for this prompt)"}`, "error");
|
|
286
|
+
notifyDesktop(ctx, config, `Runaway stopped: the same ${verdict.kind} block repeated ${verdict.count} times. ${recover ? "The agent gets one recovery turn." : "Second time for this prompt; the agent is waiting for you."}`);
|
|
287
|
+
// Interactive Pi restores the user's queued messages to the editor before aborting; the follow-up is queued in agent_end, after that.
|
|
288
|
+
ctx.abort();
|
|
222
289
|
});
|
|
223
290
|
// Each low-level run collects its own evidence of changes and checks.
|
|
224
291
|
pi.on("agent_start", async () => {
|
|
@@ -228,6 +295,7 @@ export default function wardenExtension(pi) {
|
|
|
228
295
|
pi.on("turn_end", async () => {
|
|
229
296
|
ledger.turnEnd();
|
|
230
297
|
actionGuard.turnEnd();
|
|
298
|
+
rulesGuard.turnEnd();
|
|
231
299
|
});
|
|
232
300
|
pi.on("tool_call", async (event, ctx) => {
|
|
233
301
|
const config = configFor(ctx);
|
|
@@ -248,9 +316,18 @@ export default function wardenExtension(pi) {
|
|
|
248
316
|
stats.inspected++;
|
|
249
317
|
const task = latestUserPrompt(ctx);
|
|
250
318
|
const judge = judgeFor(config);
|
|
251
|
-
const
|
|
319
|
+
const siblings = siblingToolCalls(ctx);
|
|
320
|
+
const call = { id: event.toolCallId, tool: event.toolName, input: event.input };
|
|
321
|
+
// The rules request carries the written content and the rule text, so it goes out beside the action request, not inside it.
|
|
322
|
+
const rulesCheck = config.rules.enabled && (event.toolName === "write" || event.toolName === "edit")
|
|
323
|
+
? rulesGuard.inspect(call, siblings, { cwd: ctx.cwd, config: config.rules, judge, timeoutMs: config.timeoutMs, signal: ctx.signal })
|
|
324
|
+
: undefined;
|
|
325
|
+
rulesCheck?.catch(() => undefined);
|
|
326
|
+
const verdict = await actionGuard.inspect(call, { task, context: recentTaskContext(ctx), siblings }, { config: config.action, cwd: ctx.cwd, judge, signal: ctx.signal, slop: config.slop, security: config.security });
|
|
252
327
|
if (verdict.source === "skipped")
|
|
253
328
|
return;
|
|
329
|
+
// Notes for the agent about the content it just wrote: slop, rule violations, and sensitive paths arrive as one message.
|
|
330
|
+
const notes = [];
|
|
254
331
|
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold) {
|
|
255
332
|
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.");
|
|
256
333
|
}
|
|
@@ -271,8 +348,35 @@ export default function wardenExtension(pi) {
|
|
|
271
348
|
const where = verdict.summary.path ?? event.toolName;
|
|
272
349
|
if (ctx.hasUI)
|
|
273
350
|
ctx.ui.notify(`warden · slop · ${where}: ${verdict.slopReasons.join("; ")}`, "warning");
|
|
274
|
-
|
|
351
|
+
notes.push(slopSteer(where, verdict.slopSymptoms, slopCounts));
|
|
275
352
|
}
|
|
353
|
+
if (rulesCheck) {
|
|
354
|
+
const rules = await rulesCheck;
|
|
355
|
+
if (rules.source !== "skipped") {
|
|
356
|
+
stats.ruleChecks++;
|
|
357
|
+
if (rules.error)
|
|
358
|
+
noteError(ctx, rules.error, rules.errorCode);
|
|
359
|
+
const told = rules.findings.length ? rulesSteer(rules, rulesGuard.count(rules)) : undefined;
|
|
360
|
+
record(ctx, config, "rules", formatRules(rules, config.widget.rules), rulesDetails(rules, told));
|
|
361
|
+
if (told) {
|
|
362
|
+
stats.ruleViolations++;
|
|
363
|
+
if (ctx.hasUI)
|
|
364
|
+
ctx.ui.notify(`warden · rules · ${rules.path}: ${rules.findings.map(finding => `${finding.name} (${finding.violation.toFixed(2)})`).join("; ")}`, "warning");
|
|
365
|
+
notes.push(told);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const hits = rulesGuard.notesFor(verdict.summary.location === "inside_project" ? verdict.summary.path : undefined, config.rules.sensitivePaths);
|
|
369
|
+
if (hits.length && verdict.summary.path) {
|
|
370
|
+
stats.pathNotes++;
|
|
371
|
+
const told = pathNoteSteer(verdict.summary.path, hits);
|
|
372
|
+
record(ctx, config, "rules", renderTemplate(config.widget.rules, { tool: event.toolName, path: verdict.summary.path, violations: `sensitive path ${hits.map(hit => hit.glob).join(", ")}`, status: "note" }), [`${event.toolName} ${verdict.summary.path} matches ${hits.map(hit => hit.glob).join(", ")} in rules.sensitivePaths`, `agent told: ${told}`]);
|
|
373
|
+
if (ctx.hasUI)
|
|
374
|
+
ctx.ui.notify(`warden · sensitive path · ${verdict.summary.path} (${hits.map(hit => hit.glob).join(", ")}); the agent was given the note`, "warning");
|
|
375
|
+
notes.push(told);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (notes.length)
|
|
379
|
+
steer(config, notes.join("\n\n"));
|
|
276
380
|
if (verdict.level === "warn") {
|
|
277
381
|
stats.warned++;
|
|
278
382
|
if (ctx.hasUI)
|
|
@@ -289,6 +393,7 @@ export default function wardenExtension(pi) {
|
|
|
289
393
|
return undefined;
|
|
290
394
|
}
|
|
291
395
|
if (mode === "confirm") {
|
|
396
|
+
notifyDesktop(ctx, config, `Waiting for you: allow this ${event.toolName} call? ${reasons}`);
|
|
292
397
|
const allowed = await ctx.ui.confirm(`warden: allow this ${event.toolName} call?`, confirmMessage(verdict), ctx.signal ? { signal: ctx.signal } : {});
|
|
293
398
|
if (allowed)
|
|
294
399
|
return undefined;
|
|
@@ -299,6 +404,7 @@ export default function wardenExtension(pi) {
|
|
|
299
404
|
actionGuard.hold(task);
|
|
300
405
|
if (ctx.hasUI)
|
|
301
406
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
407
|
+
notifyDesktop(ctx, config, `Held ${event.toolName}: ${reasons}. The agent will re-plan or ask you in chat.`);
|
|
302
408
|
return { block: true, reason: told ?? steerReason(verdict, { canApprove: judge !== undefined }) };
|
|
303
409
|
});
|
|
304
410
|
pi.on("tool_result", async (event, ctx) => {
|
|
@@ -422,6 +528,16 @@ export default function wardenExtension(pi) {
|
|
|
422
528
|
const config = configFor(ctx);
|
|
423
529
|
if (!config.enabled)
|
|
424
530
|
return;
|
|
531
|
+
if (pendingRunaway) {
|
|
532
|
+
const { nudge, recover } = pendingRunaway;
|
|
533
|
+
pendingRunaway = undefined;
|
|
534
|
+
// A follow-up queued here continues the run once the aborted message is in place; without recovery the note is appended only.
|
|
535
|
+
if (recover)
|
|
536
|
+
pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content: nudge, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
537
|
+
else
|
|
538
|
+
pi.sendMessage({ customType: `${PACKAGE_NAME}-steer`, content: nudge, display: true }, { triggerTurn: false });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
425
541
|
const finalMessage = finalAssistantText(event.messages);
|
|
426
542
|
const judge = judgeFor(config);
|
|
427
543
|
if (!finalMessage || !judge)
|
|
@@ -503,12 +619,14 @@ export default function wardenExtension(pi) {
|
|
|
503
619
|
const key = resolveApiKey();
|
|
504
620
|
const source = consentSource(config);
|
|
505
621
|
const usage = client?.getUsage();
|
|
506
|
-
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"].filter(Boolean).join(", ");
|
|
622
|
+
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.notify.enabled && "desktop notifications"].filter(Boolean).join(", ");
|
|
507
623
|
report([
|
|
508
624
|
`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)"}.`,
|
|
509
|
-
`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)"}.`,
|
|
510
|
-
`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}.`,
|
|
625
|
+
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${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.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)"}.`,
|
|
626
|
+
`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}, rules ${config.rules.threshold}, prose ${config.slop.prose.threshold} in ${config.slop.prose.trend}/3 replies; runaway ${config.runaway.repeats} repeats (thinking ${config.runaway.thinkingRepeats}), recover ${config.runaway.recover}; failOpen ${config.action.failOpen}.`,
|
|
511
627
|
formatLedger(ledger.snapshot()),
|
|
628
|
+
`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"}.`,
|
|
629
|
+
`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"}.`,
|
|
512
630
|
`Config: ${userConfigPath()}${ctx.isProjectTrusted() ? ` and ${projectConfigPath(ctx.cwd)}` : ""}.`,
|
|
513
631
|
widget.size ? `Last: ${[...widget.values()].join(" | ")}` : "No guarded activity yet this session.",
|
|
514
632
|
`Trace: ${trace.entries().length} events (/warden trace${shortcut ? `, ${shortcut}` : ""}, or click the status line in fullscreen mode; each toggles the sidebar). Widget templates in config.widget: action tokens ${TOKEN_NAMES.action.map(name => `{${name}}`).join(" ")}.`,
|
package/dist/guard.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface ActionSummary {
|
|
|
37
37
|
newText: string;
|
|
38
38
|
}>;
|
|
39
39
|
input?: string;
|
|
40
|
+
/** Present when part of the command is data (a heredoc body, a quoted message), so a destructive string inside it is payload. */
|
|
41
|
+
dataText?: string;
|
|
40
42
|
}
|
|
41
43
|
export type ScopeLabel = "expected_step" | "plausible_side_step" | "unrelated" | "unclear";
|
|
42
44
|
export interface Judgment {
|
|
@@ -91,6 +93,17 @@ export interface EvaluateOptions {
|
|
|
91
93
|
/** This exact call was held earlier and the user has replied since: ask whether the reply approves it. */
|
|
92
94
|
retryAfterHold?: boolean | undefined;
|
|
93
95
|
}
|
|
96
|
+
export interface ScannedCommand {
|
|
97
|
+
/** The command with data text blanked; what the pattern rules read. */
|
|
98
|
+
text: string;
|
|
99
|
+
/** True when a heredoc body or quoted data was removed. */
|
|
100
|
+
stripped: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Removes heredoc bodies that are not fed to a shell and quoted arguments of data commands. Interpreter heredocs
|
|
104
|
+
* (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
|
|
105
|
+
*/
|
|
106
|
+
export declare function stripDataText(command: string): ScannedCommand;
|
|
94
107
|
export declare function matchPatterns(tool: string, input: Record<string, unknown>, cwd?: string): PatternHit[];
|
|
95
108
|
export declare function isReadOnlyCommand(command: string): boolean;
|
|
96
109
|
export declare function describeAction(tool: string, input: Record<string, unknown>, cwd: string): ActionSummary;
|
package/dist/guard.js
CHANGED
|
@@ -52,12 +52,123 @@ const SENSITIVE_PATH = /(?:^|[\s/"'=:(])\.env(?:\.(?!example\b|sample\b|template
|
|
|
52
52
|
function splitShell(command) {
|
|
53
53
|
return command.split(/\n|;|&&|\|\||\||&/).map(part => part.trim()).filter(Boolean);
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Data text: a heredoc body written to a file, a quoted message, or a search pattern is not a command. Pattern rules skip
|
|
57
|
+
// it so a test fixture or a commit message that mentions `git push --force` is not held. A shell sink anywhere in the
|
|
58
|
+
// command (sh, eval, bash -c, command substitution) keeps every byte in scope, because the payload is executed.
|
|
59
|
+
const WRAPPERS = new Set(["sudo", "nohup", "time", "env", "command", "builtin", "exec", "nice", "timeout", "doas"]);
|
|
60
|
+
const SHELL_SINKS = new Set(["sh", "bash", "zsh", "dash", "ksh", "fish", "eval", "source", ".", "xargs", "su"]);
|
|
61
|
+
/** Commands whose quoted arguments are text they print, search, or record. */
|
|
62
|
+
const DATA_HEADS = new Set(["echo", "printf", "grep", "egrep", "fgrep", "rg", "ag", "ugrep", "jq", "cat", "tee", "head", "tail", "wc", "sort", "uniq", "cut", "tr", "less", "more", "test", "["]);
|
|
63
|
+
const GIT_MESSAGE_SUBCOMMANDS = new Set(["commit", "tag", "notes", "merge", "stash"]);
|
|
64
|
+
/** Interpreters whose stdin script can still run shell commands; their heredoc bodies stay in scope when they do. */
|
|
65
|
+
const INTERPRETERS = /^(?:python[\d.]*|node|ruby|perl|php|deno|bun|tsx|Rscript|lua[\d.]*)$/;
|
|
66
|
+
const EXEC_CALLS = /\b(?:os\.system|os\.popen|os\.exec\w*|subprocess|child_process|execSync|spawnSync|execFileSync|spawn\(|exec\(|system\(|popen\(|shell_exec|passthru|proc_open|Open3|IO\.popen|Deno\.run|Deno\.Command|Bun\.spawn|Bun\.\$|%x[\[{(]|`[^`\n]*\b(?:rm|git|dd|mkfs|kubectl|terraform)\b)/;
|
|
67
|
+
const HEREDOC = /<<-?\s*(?:"(\w+)"|'(\w+)'|\\?(\w+))/;
|
|
68
|
+
function headOf(segment) {
|
|
69
|
+
const tokens = segment.trim().split(/\s+/);
|
|
70
|
+
let index = 0;
|
|
71
|
+
while (index < tokens.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index]) || WRAPPERS.has(tokens[index])))
|
|
72
|
+
index++;
|
|
73
|
+
const head = tokens[index];
|
|
74
|
+
return head ? head.replace(/^.*\//, "") : undefined;
|
|
75
|
+
}
|
|
76
|
+
/** Quoted strings replaced by a placeholder; escapes inside double quotes are honoured, single quotes take everything. */
|
|
77
|
+
function blankQuotes(segment) {
|
|
78
|
+
let out = "";
|
|
79
|
+
for (let index = 0; index < segment.length; index++) {
|
|
80
|
+
const char = segment[index];
|
|
81
|
+
if (char !== "'" && char !== "\"") {
|
|
82
|
+
out += char;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let end = index + 1;
|
|
86
|
+
while (end < segment.length && segment[end] !== char)
|
|
87
|
+
end += char === "\"" && segment[end] === "\\" ? 2 : 1;
|
|
88
|
+
if (end >= segment.length) {
|
|
89
|
+
out += segment.slice(index);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
out += `${char}[text]${char}`;
|
|
93
|
+
index = end;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function isDataSegment(segment) {
|
|
98
|
+
const head = headOf(segment);
|
|
99
|
+
if (!head)
|
|
100
|
+
return false;
|
|
101
|
+
if (head === "git") {
|
|
102
|
+
const sub = segment.trim().split(/\s+/).find(token => !token.startsWith("-") && token !== "git" && !WRAPPERS.has(token));
|
|
103
|
+
return sub !== undefined && GIT_MESSAGE_SUBCOMMANDS.has(sub) && !/\s-c\s|--config/.test(segment);
|
|
104
|
+
}
|
|
105
|
+
if (head === "gh")
|
|
106
|
+
return /\s--(?:body|title|notes)\b/.test(segment) || /\s-[bt]\s/.test(segment);
|
|
107
|
+
return DATA_HEADS.has(head);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Removes heredoc bodies that are not fed to a shell and quoted arguments of data commands. Interpreter heredocs
|
|
111
|
+
* (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
|
|
112
|
+
*/
|
|
113
|
+
export function stripDataText(command) {
|
|
114
|
+
if (/\$\(|`/.test(command))
|
|
115
|
+
return { text: command, stripped: false };
|
|
116
|
+
const lines = command.split("\n");
|
|
117
|
+
const out = [];
|
|
118
|
+
let stripped = false;
|
|
119
|
+
for (let index = 0; index < lines.length; index++) {
|
|
120
|
+
const line = lines[index];
|
|
121
|
+
const heredoc = HEREDOC.exec(line);
|
|
122
|
+
if (!heredoc) {
|
|
123
|
+
out.push(line);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const delimiter = heredoc[1] ?? heredoc[2] ?? heredoc[3];
|
|
127
|
+
const consumer = headOf(line.slice(0, heredoc.index)) ?? "";
|
|
128
|
+
const body = [];
|
|
129
|
+
let close = index + 1;
|
|
130
|
+
while (close < lines.length && lines[close].replace(/^\t+/, "") !== delimiter)
|
|
131
|
+
body.push(lines[close]), close++;
|
|
132
|
+
const bodyText = body.join("\n");
|
|
133
|
+
const executed = SHELL_SINKS.has(consumer) || (INTERPRETERS.test(consumer) && EXEC_CALLS.test(bodyText));
|
|
134
|
+
out.push(line);
|
|
135
|
+
if (executed)
|
|
136
|
+
out.push(...body);
|
|
137
|
+
else if (body.length) {
|
|
138
|
+
out.push(`[heredoc body: ${body.length} lines of data]`);
|
|
139
|
+
stripped = true;
|
|
140
|
+
}
|
|
141
|
+
if (close < lines.length)
|
|
142
|
+
out.push(lines[close]);
|
|
143
|
+
index = close;
|
|
144
|
+
}
|
|
145
|
+
const joined = out.join("\n");
|
|
146
|
+
const segments = splitShell(joined);
|
|
147
|
+
if (segments.some(segment => { const head = headOf(segment); return head !== undefined && SHELL_SINKS.has(head); }))
|
|
148
|
+
return { text: joined, stripped };
|
|
149
|
+
if (/\b(?:ba|z|da|k)?sh\s+-[a-zA-Z]*c\b/.test(joined))
|
|
150
|
+
return { text: joined, stripped };
|
|
151
|
+
let text = joined;
|
|
152
|
+
for (const segment of segments) {
|
|
153
|
+
if (!isDataSegment(segment) || !/["']/.test(segment))
|
|
154
|
+
continue;
|
|
155
|
+
const blanked = blankQuotes(segment);
|
|
156
|
+
if (blanked === segment)
|
|
157
|
+
continue;
|
|
158
|
+
text = text.replace(segment, blanked);
|
|
159
|
+
stripped = true;
|
|
160
|
+
}
|
|
161
|
+
return { text, stripped };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* rm with both recursive and force flags. Absolute, home, variable, or wildcard targets are destructive; relative ones are
|
|
165
|
+
* risky. A quote before `rm` is allowed so `bash -c "rm -rf /"` is read; data quotes were blanked before this runs.
|
|
166
|
+
*/
|
|
56
167
|
function classifyRm(segment, cwd) {
|
|
57
|
-
const match = /(
|
|
168
|
+
const match = /(?:^|[\s"'])rm\s+(.*)$/.exec(segment);
|
|
58
169
|
if (!match)
|
|
59
170
|
return undefined;
|
|
60
|
-
const tokens = match[1].split(/\s+/).filter(Boolean);
|
|
171
|
+
const tokens = match[1].split(/\s+/).filter(Boolean).map(token => token.replace(/["']$/, ""));
|
|
61
172
|
const flags = tokens.filter(token => token.startsWith("-"));
|
|
62
173
|
const targets = tokens.filter(token => !token.startsWith("-"));
|
|
63
174
|
const recursive = flags.some(flag => flag === "--recursive" || (/^-[a-zA-Z]+$/.test(flag) && /[rR]/.test(flag)));
|
|
@@ -86,8 +197,9 @@ export function matchPatterns(tool, input, cwd) {
|
|
|
86
197
|
const hits = new Map();
|
|
87
198
|
const add = (hit) => { if (hit && !hits.has(hit.id))
|
|
88
199
|
hits.set(hit.id, hit); };
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
200
|
+
const raw = commandOf(tool, input)?.command;
|
|
201
|
+
if (raw) {
|
|
202
|
+
const command = stripDataText(raw).text;
|
|
91
203
|
for (const rule of SHELL_RULES)
|
|
92
204
|
if (rule.test.test(command))
|
|
93
205
|
add({ id: rule.id, severity: rule.severity, label: rule.label });
|
|
@@ -174,8 +286,12 @@ function displayPath(target, cwd) {
|
|
|
174
286
|
export function describeAction(tool, input, cwd) {
|
|
175
287
|
const summary = { tool };
|
|
176
288
|
const view = commandOf(tool, input);
|
|
177
|
-
if (view)
|
|
289
|
+
if (view) {
|
|
178
290
|
summary.command = redact(truncate(view.command, COMMAND_LIMIT));
|
|
291
|
+
// Jev sees the full text; this names the part of it that is written or printed rather than executed.
|
|
292
|
+
if (stripDataText(view.command).stripped)
|
|
293
|
+
summary.dataText = "heredoc bodies and quoted arguments of echo/printf/grep/git commit in this command are text that is written, printed, searched, or recorded, not executed";
|
|
294
|
+
}
|
|
179
295
|
if (typeof input.path === "string" && input.path.trim() && tool !== "ctx_execute_file") {
|
|
180
296
|
const shown = displayPath(input.path, cwd);
|
|
181
297
|
summary.path = shown.path;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
2
|
-
export type { ActionInput, ActionSummary, EvaluateOptions, Judge, Judgment, SlopJudgment, SlopSymptom, Level, PatternHit, ScopeLabel, Severity, Verdict } from "./guard.js";
|
|
1
|
+
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, stripDataText, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
2
|
+
export type { ActionInput, ActionSummary, EvaluateOptions, Judge, Judgment, SlopJudgment, SlopSymptom, Level, PatternHit, ScannedCommand, ScopeLabel, Severity, Verdict } from "./guard.js";
|
|
3
3
|
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
4
4
|
export { ActionGuard } from "./action-guard.js";
|
|
5
5
|
export type { Conversation, InspectOptions, ToolCallRef } from "./action-guard.js";
|
|
@@ -16,15 +16,21 @@ export type { OutputFormat } from "./excerpt.js";
|
|
|
16
16
|
export { detectSearchTool, recallInstruction, classifyRecall } from "./recall.js";
|
|
17
17
|
export type { SearchTool } from "./recall.js";
|
|
18
18
|
export { redact } from "./redact.js";
|
|
19
|
+
export { detectNotifier, notifierCommand, sendNotification, candidates as notifierCandidates, clipBody } from "./notify.js";
|
|
20
|
+
export type { NotifierName, NotifierTarget, Notification, NotifierCommand, Runner } from "./notify.js";
|
|
21
|
+
export { RunawayMonitor, repeatedBlock, repeatedTail, findRepeats, runawayNudge, formatRunaway, BLOCK_MIN_CHARS, PHRASE_CHARS, CHECK_EVERY } from "./runaway.js";
|
|
22
|
+
export type { RunawayVerdict, RepeatSignal, StreamKind } from "./runaway.js";
|
|
19
23
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
20
24
|
export { ContextLedger, formatLedger } from "./saver.js";
|
|
21
25
|
export type { ContextLedgerSnapshot, RecallKind } from "./saver.js";
|
|
22
26
|
export type { ShapeResult } from "./shape.js";
|
|
23
27
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
24
|
-
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, ContextConfig, RecallTool, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
28
|
+
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, RulesConfig, ContextConfig, RunawayConfig, NotifyConfig, RecallTool, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
25
29
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
26
30
|
export type { CommandView } from "./tools.js";
|
|
27
|
-
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
31
|
+
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, runawayTokens, rulesTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
28
32
|
export type { WidgetConfig, WidgetPlacement, Tokens } from "./widget.js";
|
|
29
|
-
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails } from "./trace.js";
|
|
33
|
+
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails, runawayDetails, rulesDetails } from "./trace.js";
|
|
30
34
|
export type { TraceEntry, GuardName } from "./trace.js";
|
|
35
|
+
export { parseRules, condense, globToRegExp, matchGlob, projectPath, RuleStore, rulesFor, describeRuleSet, describeTarget, buildRulesRequest, ruleQuestion, evaluateRules, skipReason, rulesSteer, formatRules, pathNotes, pathNoteSteer, RulesGuard, RULES_FILE, FALLBACK_FILES, MAX_RULES, AGGREGATE_QUESTION, LOCATOR_QUESTION } from "./rules.js";
|
|
36
|
+
export type { Rule, RuleSet, RuleOutcome, RuleScore, RuleFinding, RulesVerdict, RulesOptions, RulesTarget, EditView, PathNote, RulesCallRef } from "./rules.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
1
|
+
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, stripDataText, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
2
2
|
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
3
3
|
export { ActionGuard } from "./action-guard.js";
|
|
4
4
|
export { evaluateProse, buildProseRequest, proseQuestions, proseNudge, describeAudience, ProseTrend, PROSE_SYMPTOMS, PROSE_LABELS } from "./prose.js";
|
|
@@ -8,9 +8,12 @@ export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, sa
|
|
|
8
8
|
export { formatExcerpt, formatQuestion } from "./excerpt.js";
|
|
9
9
|
export { detectSearchTool, recallInstruction, classifyRecall } from "./recall.js";
|
|
10
10
|
export { redact } from "./redact.js";
|
|
11
|
+
export { detectNotifier, notifierCommand, sendNotification, candidates as notifierCandidates, clipBody } from "./notify.js";
|
|
12
|
+
export { RunawayMonitor, repeatedBlock, repeatedTail, findRepeats, runawayNudge, formatRunaway, BLOCK_MIN_CHARS, PHRASE_CHARS, CHECK_EVERY } from "./runaway.js";
|
|
11
13
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
12
14
|
export { ContextLedger, formatLedger } from "./saver.js";
|
|
13
15
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
14
16
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
15
|
-
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
16
|
-
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails } from "./trace.js";
|
|
17
|
+
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, runawayTokens, rulesTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
18
|
+
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails, runawayDetails, rulesDetails } from "./trace.js";
|
|
19
|
+
export { parseRules, condense, globToRegExp, matchGlob, projectPath, RuleStore, rulesFor, describeRuleSet, describeTarget, buildRulesRequest, ruleQuestion, evaluateRules, skipReason, rulesSteer, formatRules, pathNotes, pathNoteSteer, RulesGuard, RULES_FILE, FALLBACK_FILES, MAX_RULES, AGGREGATE_QUESTION, LOCATOR_QUESTION } from "./rules.js";
|