pi-warden 0.8.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 +162 -105
- package/dist/config.d.ts +20 -1
- package/dist/config.js +24 -1
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +50 -8
- package/dist/guard.d.ts +13 -0
- package/dist/guard.js +122 -6
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/panel.js +1 -1
- package/dist/rules.d.ts +164 -0
- package/dist/rules.js +479 -0
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +5 -2
- package/dist/trace.d.ts +3 -1
- package/dist/trace.js +14 -0
- package/dist/widget.d.ts +5 -0
- package/dist/widget.js +18 -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,6 +30,7 @@ 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 },
|
|
34
35
|
runaway: { enabled: true, repeats: 4, thinkingRepeats: 10, minChars: 400, recover: true },
|
|
35
36
|
notify: { enabled: true, cooldownMs: 10000, command: [] },
|
|
@@ -148,6 +149,26 @@ function applySlop(base, raw) {
|
|
|
148
149
|
// 0.2.x used `placeholder` for the stub threshold; it still sets the shared threshold.
|
|
149
150
|
return { enabled: boolean(raw.enabled, base.enabled), threshold: probability(raw.threshold ?? raw.placeholder, base.threshold), prose: applyProse(base.prose, raw.prose) };
|
|
150
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
|
+
}
|
|
151
172
|
function applyWidget(base, raw) {
|
|
152
173
|
if (!isObject(raw))
|
|
153
174
|
return base;
|
|
@@ -165,6 +186,7 @@ function applyWidget(base, raw) {
|
|
|
165
186
|
security: template(raw.security, base.security),
|
|
166
187
|
context: template(raw.context, base.context),
|
|
167
188
|
runaway: template(raw.runaway, base.runaway),
|
|
189
|
+
rules: template(raw.rules, base.rules),
|
|
168
190
|
};
|
|
169
191
|
}
|
|
170
192
|
/** Shared request settings; `action.timeoutMs`/`action.maxRequests` from 0.1.x files are still honoured. */
|
|
@@ -177,6 +199,7 @@ function applyShared(base, raw) {
|
|
|
177
199
|
}
|
|
178
200
|
function applyGuards(base, raw, timeoutMs, source) {
|
|
179
201
|
return {
|
|
202
|
+
rules: applyRules(base.rules, raw.rules),
|
|
180
203
|
runaway: applyRunaway(base.runaway, raw.runaway),
|
|
181
204
|
// A project file may switch notifications off or on, but never names a command to run.
|
|
182
205
|
notify: applyNotify(base.notify, raw.notify, source === "user"),
|
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,18 +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";
|
|
13
14
|
import { detectNotifier, sendNotification } from "./notify.js";
|
|
14
15
|
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
15
16
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
16
17
|
import { openTracePanel } from "./panel.js";
|
|
17
18
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
18
19
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
19
|
-
import { actionDetails, doneDetails, proseDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
20
|
+
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
20
21
|
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
21
|
-
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.";
|
|
22
23
|
const WIDGET = PACKAGE_NAME;
|
|
23
24
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
24
|
-
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, runaway: 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 });
|
|
25
26
|
function latestUserPrompt(ctx) {
|
|
26
27
|
const entries = ctx.sessionManager.getBranch();
|
|
27
28
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
@@ -124,6 +125,7 @@ export default function wardenExtension(pi) {
|
|
|
124
125
|
let panel;
|
|
125
126
|
let lastUi;
|
|
126
127
|
const actionGuard = new ActionGuard();
|
|
128
|
+
const rulesGuard = new RulesGuard();
|
|
127
129
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
128
130
|
let evidence = emptyEvidence();
|
|
129
131
|
let doneNudged = false;
|
|
@@ -230,6 +232,7 @@ export default function wardenExtension(pi) {
|
|
|
230
232
|
trace.clear();
|
|
231
233
|
panel?.close();
|
|
232
234
|
actionGuard.reset();
|
|
235
|
+
rulesGuard.reset();
|
|
233
236
|
attempts.reset();
|
|
234
237
|
evidence = emptyEvidence();
|
|
235
238
|
doneNudged = false;
|
|
@@ -254,6 +257,7 @@ export default function wardenExtension(pi) {
|
|
|
254
257
|
runawayStops = 0;
|
|
255
258
|
pendingRunaway = undefined;
|
|
256
259
|
actionGuard.turnEnd();
|
|
260
|
+
rulesGuard.turnEnd();
|
|
257
261
|
});
|
|
258
262
|
// Each assistant message is judged on its own; Pi does not forward the stream's own "start" event, so this is the reset.
|
|
259
263
|
pi.on("message_start", async (event) => {
|
|
@@ -291,6 +295,7 @@ export default function wardenExtension(pi) {
|
|
|
291
295
|
pi.on("turn_end", async () => {
|
|
292
296
|
ledger.turnEnd();
|
|
293
297
|
actionGuard.turnEnd();
|
|
298
|
+
rulesGuard.turnEnd();
|
|
294
299
|
});
|
|
295
300
|
pi.on("tool_call", async (event, ctx) => {
|
|
296
301
|
const config = configFor(ctx);
|
|
@@ -311,9 +316,18 @@ export default function wardenExtension(pi) {
|
|
|
311
316
|
stats.inspected++;
|
|
312
317
|
const task = latestUserPrompt(ctx);
|
|
313
318
|
const judge = judgeFor(config);
|
|
314
|
-
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 });
|
|
315
327
|
if (verdict.source === "skipped")
|
|
316
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 = [];
|
|
317
331
|
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold) {
|
|
318
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.");
|
|
319
333
|
}
|
|
@@ -334,8 +348,35 @@ export default function wardenExtension(pi) {
|
|
|
334
348
|
const where = verdict.summary.path ?? event.toolName;
|
|
335
349
|
if (ctx.hasUI)
|
|
336
350
|
ctx.ui.notify(`warden · slop · ${where}: ${verdict.slopReasons.join("; ")}`, "warning");
|
|
337
|
-
|
|
351
|
+
notes.push(slopSteer(where, verdict.slopSymptoms, slopCounts));
|
|
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
|
+
}
|
|
338
377
|
}
|
|
378
|
+
if (notes.length)
|
|
379
|
+
steer(config, notes.join("\n\n"));
|
|
339
380
|
if (verdict.level === "warn") {
|
|
340
381
|
stats.warned++;
|
|
341
382
|
if (ctx.hasUI)
|
|
@@ -578,12 +619,13 @@ export default function wardenExtension(pi) {
|
|
|
578
619
|
const key = resolveApiKey();
|
|
579
620
|
const source = consentSource(config);
|
|
580
621
|
const usage = client?.getUsage();
|
|
581
|
-
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", config.runaway.enabled && "runaway", config.notify.enabled && "desktop notifications"].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(", ");
|
|
582
623
|
report([
|
|
583
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)"}.`,
|
|
584
|
-
`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.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)"}.`,
|
|
585
|
-
`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; runaway ${config.runaway.repeats} repeats (thinking ${config.runaway.thinkingRepeats}), recover ${config.runaway.recover}; 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}.`,
|
|
586
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"}.`,
|
|
587
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"}.`,
|
|
588
630
|
`Config: ${userConfigPath()}${ctx.isProjectTrusted() ? ` and ${projectConfigPath(ctx.cwd)}` : ""}.`,
|
|
589
631
|
widget.size ? `Last: ${[...widget.values()].join(" | ")}` : "No guarded activity yet this session.",
|
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";
|
|
@@ -25,10 +25,12 @@ export { ContextLedger, formatLedger } from "./saver.js";
|
|
|
25
25
|
export type { ContextLedgerSnapshot, RecallKind } from "./saver.js";
|
|
26
26
|
export type { ShapeResult } from "./shape.js";
|
|
27
27
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
28
|
-
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, ContextConfig, RunawayConfig, NotifyConfig, 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";
|
|
29
29
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
30
30
|
export type { CommandView } from "./tools.js";
|
|
31
|
-
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, runawayTokens, 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";
|
|
32
32
|
export type { WidgetConfig, WidgetPlacement, Tokens } from "./widget.js";
|
|
33
|
-
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails, runawayDetails } from "./trace.js";
|
|
33
|
+
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails, runawayDetails, rulesDetails } from "./trace.js";
|
|
34
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";
|
|
@@ -14,5 +14,6 @@ export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
|
14
14
|
export { ContextLedger, formatLedger } from "./saver.js";
|
|
15
15
|
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
16
16
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
17
|
-
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, runawayTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
18
|
-
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails, runawayDetails } 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";
|
package/dist/panel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
const LEVEL_COLOR = { allow: "success", ok: "success", warn: "warning", unverified: "warning", nudged: "warning", confirm: "error", stuck: "error", "false claim": "error", stopped: "error", "stopped, recovering": "error" };
|
|
2
|
+
const LEVEL_COLOR = { allow: "success", ok: "success", warn: "warning", unverified: "warning", nudged: "warning", confirm: "error", stuck: "error", "false claim": "error", stopped: "error", "stopped, recovering": "error", violation: "warning", skipped: "muted" };
|
|
3
3
|
/**
|
|
4
4
|
* Sidebar listing the trace newest-first, live-updating while open. It opens without taking keyboard input: the editor
|
|
5
5
|
* keeps working while it is visible. A click inside it (fullscreen mode) focuses it; then ↑/↓/j/k scroll a line,
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { IntegrationErrorCode } from "pi-typesafe";
|
|
2
|
+
import type { RulesConfig } from "./config.js";
|
|
3
|
+
import type { Judge } from "./jev.js";
|
|
4
|
+
/**
|
|
5
|
+
* Project rules: Markdown headings become rules, Jev judges each write or edit against every rule on its own request, and the
|
|
6
|
+
* agent is steered, never held, with the violated rule. Path scoping and sensitive-path notes are code only.
|
|
7
|
+
*
|
|
8
|
+
* Sources, in order: `pi-warden.md` at the project root; else the files in `rules.files`; else, with `rules.fallback`, the
|
|
9
|
+
* first of README.md, CLAUDE.md, AGENTS.md as one aggregate rule set. Files are re-read when their mtime or size changes.
|
|
10
|
+
*/
|
|
11
|
+
export interface Rule {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** Rule text under the heading, fences included, `paths:` line removed. */
|
|
15
|
+
body: string;
|
|
16
|
+
/** Globs the rule applies to; empty means every file. */
|
|
17
|
+
paths: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface RuleSet {
|
|
20
|
+
/** Project-relative file names the rules came from. */
|
|
21
|
+
sources: string[];
|
|
22
|
+
rules: Rule[];
|
|
23
|
+
/** Fallback documents have no rule headings: one question judges the content against this whole text. */
|
|
24
|
+
aggregate?: string;
|
|
25
|
+
/** Rules past the request cap, dropped in file order. */
|
|
26
|
+
dropped: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const RULES_FILE = "pi-warden.md";
|
|
29
|
+
export declare const FALLBACK_FILES: string[];
|
|
30
|
+
/** TypeSafe answers at most 32 questions per request; one is kept for the edit locator. */
|
|
31
|
+
export declare const MAX_RULES = 31;
|
|
32
|
+
/**
|
|
33
|
+
* A fallback document cut to `limit` characters while keeping its shape: every heading stays and each section keeps its
|
|
34
|
+
* head, so a rule stated in the last section of a very long AGENTS.md is still seen. Code inside fences is treated as text.
|
|
35
|
+
*/
|
|
36
|
+
export declare function condense(text: string, limit: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Headings at the highest level present outside code fences delimit rules (`#` in a jev-rules style file, `##` under a
|
|
39
|
+
* document title). Text before the first such heading is not a rule. A `paths:` line at the top of a body scopes the rule.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseRules(markdown: string): Rule[];
|
|
42
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
43
|
+
/** The first pattern that matches the path, or undefined. */
|
|
44
|
+
export declare function matchGlob(path: string, patterns: readonly string[]): string | undefined;
|
|
45
|
+
/** Project-relative path with forward slashes, or undefined when the target lies outside the project. */
|
|
46
|
+
export declare function projectPath(target: string, cwd: string): string | undefined;
|
|
47
|
+
export declare class RuleStore {
|
|
48
|
+
private readonly cache;
|
|
49
|
+
private read;
|
|
50
|
+
/** The active rule set for a project, or undefined when no source exists. Never throws. */
|
|
51
|
+
load(cwd: string, config: Pick<RulesConfig, "files" | "fallback" | "maxChars">): RuleSet | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Rules that apply to a path: unscoped rules plus those whose `paths` match. */
|
|
54
|
+
export declare function rulesFor(set: RuleSet, path: string): Rule[];
|
|
55
|
+
export declare function describeRuleSet(set: RuleSet | undefined): string;
|
|
56
|
+
export interface EditView {
|
|
57
|
+
id: string;
|
|
58
|
+
/** Current file text around the replaced text, when the file exists and the text was found. */
|
|
59
|
+
before?: string;
|
|
60
|
+
newText: string;
|
|
61
|
+
}
|
|
62
|
+
export interface RulesTarget {
|
|
63
|
+
tool: "write" | "edit";
|
|
64
|
+
/** Project-relative path. */
|
|
65
|
+
path: string;
|
|
66
|
+
content?: string;
|
|
67
|
+
edits?: EditView[];
|
|
68
|
+
/** Edits beyond MAX_EDITS, not shown. */
|
|
69
|
+
moreEdits?: number;
|
|
70
|
+
}
|
|
71
|
+
/** The redacted content of a write or edit as Jev sees it, or undefined when the call carries nothing to judge. */
|
|
72
|
+
export declare function describeTarget(tool: string, input: Record<string, unknown>, cwd: string): RulesTarget | undefined;
|
|
73
|
+
export type RuleOutcome = "compliant" | "violation" | "not_applicable" | "insufficient_context";
|
|
74
|
+
export declare const AGGREGATE_QUESTION = "rules";
|
|
75
|
+
export declare const LOCATOR_QUESTION = "which_edit";
|
|
76
|
+
export declare function ruleQuestion(rule: Rule): import("pi-typesafe").ChoiceQuestion<Record<RuleOutcome, string>>;
|
|
77
|
+
export declare function buildRulesRequest(target: RulesTarget, set: RuleSet): {
|
|
78
|
+
state: {
|
|
79
|
+
rules?: string;
|
|
80
|
+
moreEdits?: number;
|
|
81
|
+
edits?: {
|
|
82
|
+
newText: string;
|
|
83
|
+
before?: string;
|
|
84
|
+
id: string;
|
|
85
|
+
}[];
|
|
86
|
+
content?: string;
|
|
87
|
+
path: string;
|
|
88
|
+
};
|
|
89
|
+
questions: Record<string, import("pi-typesafe").ChoiceQuestion<import("@typesafe-ai/sdk").ChoiceCriteria>>;
|
|
90
|
+
applicable: Rule[];
|
|
91
|
+
};
|
|
92
|
+
export interface RuleScore {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
outcome: RuleOutcome;
|
|
96
|
+
violation: number;
|
|
97
|
+
}
|
|
98
|
+
export interface RuleFinding extends RuleScore {
|
|
99
|
+
body: string;
|
|
100
|
+
}
|
|
101
|
+
export interface RulesVerdict {
|
|
102
|
+
source: "skipped" | "typesafe" | "error";
|
|
103
|
+
path: string;
|
|
104
|
+
tool: "write" | "edit";
|
|
105
|
+
sources: string[];
|
|
106
|
+
/** Rules asked about, after path scoping; 1 for an aggregate document. */
|
|
107
|
+
asked: number;
|
|
108
|
+
aggregate: boolean;
|
|
109
|
+
scores?: RuleScore[];
|
|
110
|
+
/** Violations at or above the threshold, strongest first. */
|
|
111
|
+
findings: RuleFinding[];
|
|
112
|
+
/** The edit Jev points at when several edits were judged and something was flagged. */
|
|
113
|
+
editId?: string;
|
|
114
|
+
editPreview?: string;
|
|
115
|
+
model?: string;
|
|
116
|
+
elapsedMs?: number;
|
|
117
|
+
skippedReason?: string;
|
|
118
|
+
error?: string;
|
|
119
|
+
errorCode?: IntegrationErrorCode;
|
|
120
|
+
}
|
|
121
|
+
export interface RulesOptions {
|
|
122
|
+
cwd: string;
|
|
123
|
+
config: RulesConfig;
|
|
124
|
+
set: RuleSet | undefined;
|
|
125
|
+
judge?: Judge | undefined;
|
|
126
|
+
timeoutMs: number;
|
|
127
|
+
signal?: AbortSignal | undefined;
|
|
128
|
+
}
|
|
129
|
+
/** Why a write or edit is not judged against the rules: nothing to judge, no rules, or a path the config keeps out. */
|
|
130
|
+
export declare function skipReason(target: RulesTarget | undefined, set: RuleSet | undefined, config: RulesConfig): string | undefined;
|
|
131
|
+
export declare function evaluateRules(tool: string, input: Record<string, unknown>, options: RulesOptions): Promise<RulesVerdict>;
|
|
132
|
+
/** Names each violated rule with a short quote of its text; the third hit of one rule in a session makes it a standing rule. */
|
|
133
|
+
export declare function rulesSteer(verdict: RulesVerdict, counts: ReadonlyMap<string, number>): string;
|
|
134
|
+
export declare function formatRules(verdict: RulesVerdict, template?: string): string;
|
|
135
|
+
export interface PathNote {
|
|
136
|
+
glob: string;
|
|
137
|
+
note: string;
|
|
138
|
+
}
|
|
139
|
+
export declare function pathNotes(path: string | undefined, notes: Readonly<Record<string, string>>): PathNote[];
|
|
140
|
+
export declare function pathNoteSteer(path: string, hits: readonly PathNote[]): string;
|
|
141
|
+
export interface RulesCallRef {
|
|
142
|
+
id: string;
|
|
143
|
+
tool: string;
|
|
144
|
+
input: Record<string, unknown>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The Rules guard for one session: loads and caches the rule sources, judges each write or edit on its own request (fired
|
|
148
|
+
* together with the action guard's), prejudges sibling writes so their requests overlap, and counts hits per rule so a repeat
|
|
149
|
+
* becomes a standing rule.
|
|
150
|
+
*/
|
|
151
|
+
export declare class RulesGuard {
|
|
152
|
+
readonly store: RuleStore;
|
|
153
|
+
private readonly prejudged;
|
|
154
|
+
private readonly counts;
|
|
155
|
+
private readonly noted;
|
|
156
|
+
inspect(call: RulesCallRef, siblings: readonly RulesCallRef[], options: Omit<RulesOptions, "set">): Promise<RulesVerdict>;
|
|
157
|
+
/** Records the findings and returns the per-rule session counts the steer text uses. */
|
|
158
|
+
count(verdict: RulesVerdict): ReadonlyMap<string, number>;
|
|
159
|
+
/** Sensitive-path notes not yet given for this path in this session. */
|
|
160
|
+
notesFor(path: string | undefined, notes: Readonly<Record<string, string>>): PathNote[];
|
|
161
|
+
describe(cwd: string, config: Pick<RulesConfig, "files" | "fallback" | "maxChars">): string;
|
|
162
|
+
turnEnd(): void;
|
|
163
|
+
reset(): void;
|
|
164
|
+
}
|