pi-warden 0.26.0 → 0.28.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +56 -176
- package/dist/config.d.ts +18 -0
- package/dist/config.js +48 -0
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +148 -41
- package/dist/extension.js.bak +148 -41
- package/dist/guard.d.ts +5 -0
- package/dist/guard.js +5 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/learning.d.ts +51 -5
- package/dist/learning.js +219 -29
- package/dist/output.d.ts +31 -1
- package/dist/output.js +80 -1
- package/dist/panel.d.ts +29 -0
- package/dist/panel.js +257 -10
- package/dist/saver.d.ts +2 -0
- package/dist/saver.js +4 -0
- package/dist/shape.js +2 -1
- package/dist/trace.d.ts +2 -0
- package/dist/widget.d.ts +96 -0
- package/dist/widget.js +259 -1
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
1
2
|
import * as tuiModule from "@earendil-works/pi-tui";
|
|
2
3
|
const MouseRegion = tuiModule.MouseRegion;
|
|
3
4
|
import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
@@ -6,27 +7,27 @@ import { backendHost, disclosureFor, judgeOptions, keyAvailable, resolveBackend
|
|
|
6
7
|
import { ActionGuard } from "./action-guard.js";
|
|
7
8
|
import { ArmingTracker, unparseableArmingRules } from "./arming.js";
|
|
8
9
|
import * as configModule from "./config.js";
|
|
9
|
-
import { applyUserOverrides, defaultConfig, isMode, loadConfig, PACKAGE_NAME, projectConfigPath, readUserConfig, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
10
|
+
import { applyUserOverrides, defaultConfig, getNestedValue, isMode, loadConfig, PACKAGE_NAME, parseConfigValue, projectConfigPath, readUserConfig, setNestedValue, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
10
11
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome as recordDoneOutcome } from "./done.js";
|
|
11
|
-
import { evaluateAction, formatVerdict, higher, inertPathRules, intentSteer, offTaskSteer, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, writeSinkTargets } from "./guard.js";
|
|
12
|
+
import { evaluateAction, formatVerdict, formatVerdictTokens, higher, inertPathRules, intentSteer, offTaskSteer, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, writeSinkTargets } from "./guard.js";
|
|
12
13
|
import { commandOf } from "./tools.js";
|
|
13
14
|
import { formatHolds, HoldLedger, HoldLog, holdLogPath, outcomeNote, regretsAt, textRegrets } from "./holds.js";
|
|
14
|
-
import { initSchema, recordHold, recordOutcome, toHoldRecord } from "./learning.js";
|
|
15
|
+
import { initSchema, recordHold, recordOutcome, toHoldRecord, generateRecommendations, analyzeSteerEffectivenessReport } from "./learning.js";
|
|
15
16
|
import { evaluateProse, proseNudge, ProseTrend, RESTATE_MIN_SENTENCES, RESTATE_SHARE, RestatementWindow, substantiveSentences } from "./prose.js";
|
|
16
|
-
import { compressOutput, duplicateNote, evaluateOutput, mergeOutput, outputKey, saveOutput, securityNotice } from "./output.js";
|
|
17
|
+
import { compressOutput, duplicateNote, evaluateOutput, mergeOutput, outputKey, saveOutput, securityNotice, CompressionLearner } from "./output.js";
|
|
17
18
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
18
19
|
import { redact } from "./redact.js";
|
|
19
20
|
import { formatRules, pathNoteSteer, RulesGuard, rulesSteer } from "./rules.js";
|
|
20
21
|
import { detectNotifier, sendNotification } from "./notify.js";
|
|
21
22
|
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
22
23
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
23
|
-
import { openTracePanel } from "./panel.js";
|
|
24
|
+
import { openConfigPanel, openTracePanel } from "./panel.js";
|
|
24
25
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
25
26
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
26
27
|
import { formatWake, newReports, reportLabel, triageReport, WakePolicy } from "./subagent.js";
|
|
27
28
|
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
28
|
-
import { DEFAULT_TEMPLATES, proseTokens, renderTemplate, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
29
|
-
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
29
|
+
import { DEFAULT_TEMPLATES, LEVEL_COLOR, pickSentenceTemplate, proseTokens, renderTemplate, SENTENCE_TEMPLATES, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
30
|
+
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, reason) for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
30
31
|
const WIDGET = PACKAGE_NAME;
|
|
31
32
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
32
33
|
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, offPlan: 0, offTask: 0, slop: 0, ruleChecks: 0, ruleViolations: 0, pathNotes: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, runaway: 0, errors: 0, steers: 0, steersSkipped: 0, steerGuards: {}, subagentReports: 0, subagentWoken: 0, restatements: 0 });
|
|
@@ -201,7 +202,6 @@ export default function wardenExtension(pi) {
|
|
|
201
202
|
const rulesGuard = new RulesGuard();
|
|
202
203
|
// Hold feedback: what the user did after each judged call, the trace entry each label lands on, and the per-session log.
|
|
203
204
|
const holds = new HoldLedger();
|
|
204
|
-
initSchema();
|
|
205
205
|
const learningIds = new Map(); // holds.id -> learning.id
|
|
206
206
|
// Prune learningIds when it grows large to prevent memory leaks
|
|
207
207
|
function pruneLearningIds() {
|
|
@@ -225,9 +225,12 @@ export default function wardenExtension(pi) {
|
|
|
225
225
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
226
226
|
let evidence = emptyEvidence();
|
|
227
227
|
let doneNudged = false;
|
|
228
|
+
let warnedFallback = false;
|
|
228
229
|
const prose = new ProseTrend();
|
|
229
230
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
230
231
|
const ledger = new ContextLedger();
|
|
232
|
+
// Learns which compression strategies work best per tool, so the next call skips the judge when confident.
|
|
233
|
+
const compressionLearner = new CompressionLearner();
|
|
231
234
|
// Secrets already announced this session, by fingerprint: the same key read twice earns one banner and one steer.
|
|
232
235
|
const secretsSeen = new Set();
|
|
233
236
|
// Subagent report entries already triaged, by session entry id; the wake window outlives one scan.
|
|
@@ -328,7 +331,30 @@ export default function wardenExtension(pi) {
|
|
|
328
331
|
ctx.ui.setWidget(WIDGET, undefined);
|
|
329
332
|
return;
|
|
330
333
|
}
|
|
331
|
-
|
|
334
|
+
let entries = [...widget].map(([guard, line]) => ({ guard, line }));
|
|
335
|
+
// In live mode: only show the most recent guard, re-rendered with sentence templates.
|
|
336
|
+
if (config.widget.barMode === "live" && entries.length > 0) {
|
|
337
|
+
const lastEntry = trace.entries().at(-1);
|
|
338
|
+
if (lastEntry?.tokens) {
|
|
339
|
+
const template = pickSentenceTemplate(lastEntry.guard, lastEntry.tokens);
|
|
340
|
+
const sentence = renderTemplate(template, lastEntry.tokens);
|
|
341
|
+
const chip = lastEntry.tokens.level ?? lastEntry.tokens.status;
|
|
342
|
+
ctx.ui.setWidget(WIDGET, (_tui, theme) => {
|
|
343
|
+
const color = LEVEL_COLOR[chip ?? ""] ?? "text";
|
|
344
|
+
const chipText = chip ? `${theme.bold(theme.fg(color, chip.toUpperCase()))} ` : "";
|
|
345
|
+
const guardText = theme.fg("muted", lastEntry.guard + " ");
|
|
346
|
+
const body = { render: (width) => [chipText + guardText + sentence], invalidate: () => { } };
|
|
347
|
+
return MouseRegion ? new MouseRegion(body, event => {
|
|
348
|
+
if (event.type !== "click" || event.button !== "left")
|
|
349
|
+
return undefined;
|
|
350
|
+
togglePanel(lastUi, config);
|
|
351
|
+
return { handled: true };
|
|
352
|
+
}) : body;
|
|
353
|
+
}, { placement: config.widget.placement });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
entries = [entries.at(-1)];
|
|
357
|
+
}
|
|
332
358
|
// A custom component so the lines wrap to the pane and a click (fullscreen mode) opens the trace panel.
|
|
333
359
|
// When the host TUI lacks MouseRegion (e.g. omp 18.2.5), the extension still loads — the widget
|
|
334
360
|
// renders the same text, but a click does nothing. A missing named import is a link-time error
|
|
@@ -343,9 +369,9 @@ export default function wardenExtension(pi) {
|
|
|
343
369
|
}) : body;
|
|
344
370
|
}, { placement: config.widget.placement });
|
|
345
371
|
};
|
|
346
|
-
const record = (ctx, config, guard, line, details) => {
|
|
372
|
+
const record = (ctx, config, guard, line, details, tokens) => {
|
|
347
373
|
widget.set(guard, line);
|
|
348
|
-
const entry = { at: Date.now(), guard, line, details };
|
|
374
|
+
const entry = { at: Date.now(), guard, line, details, tokens };
|
|
349
375
|
trace.push(entry);
|
|
350
376
|
paint(ctx, config);
|
|
351
377
|
return entry;
|
|
@@ -453,8 +479,12 @@ export default function wardenExtension(pi) {
|
|
|
453
479
|
}
|
|
454
480
|
};
|
|
455
481
|
pi.on("session_start", async (_event, ctx) => {
|
|
482
|
+
if (ctx.hasUI)
|
|
483
|
+
lastUi = ctx.ui;
|
|
456
484
|
client = undefined;
|
|
457
485
|
budgetExhausted = false;
|
|
486
|
+
warnedFallback = false;
|
|
487
|
+
await initSchema(loadConfig().learning.retentionDays);
|
|
458
488
|
stats = freshStats();
|
|
459
489
|
widget.clear();
|
|
460
490
|
trace.clear();
|
|
@@ -469,7 +499,17 @@ export default function wardenExtension(pi) {
|
|
|
469
499
|
evidence = emptyEvidence();
|
|
470
500
|
doneNudged = false;
|
|
471
501
|
prose.reset();
|
|
502
|
+
// Clean up temp output dirs from the previous session.
|
|
503
|
+
for (const dir of ledger.storedPaths()) {
|
|
504
|
+
try {
|
|
505
|
+
await rm(dir, { recursive: true, force: true });
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
console.warn("pi-warden: temp cleanup failed:", err);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
472
511
|
ledger.reset();
|
|
512
|
+
compressionLearner.reset();
|
|
473
513
|
secretsSeen.clear();
|
|
474
514
|
subagentSeen.clear();
|
|
475
515
|
wakePolicy.reset();
|
|
@@ -486,10 +526,21 @@ export default function wardenExtension(pi) {
|
|
|
486
526
|
slopCounts[symptom] = 0;
|
|
487
527
|
if (ctx.hasUI)
|
|
488
528
|
ctx.ui.setWidget(WIDGET, undefined);
|
|
529
|
+
// One-time notice when confirm mode falls back to steer (headless).
|
|
530
|
+
if (!warnedFallback && loadConfig().mode === "confirm" && !ctx.hasUI) {
|
|
531
|
+
warnedFallback = true;
|
|
532
|
+
const msg = "warden: confirm mode requires a UI; falling back to steer mode for this session.";
|
|
533
|
+
if (ctx.hasUI)
|
|
534
|
+
ctx.ui.notify(msg, "warning");
|
|
535
|
+
else
|
|
536
|
+
pi.sendMessage({ customType: `${PACKAGE_NAME}-status`, content: msg, display: true });
|
|
537
|
+
}
|
|
489
538
|
});
|
|
490
539
|
// A new user prompt starts a new attempt history, a new steer budget, and a new restatement window; answering the
|
|
491
540
|
// user is never a restatement.
|
|
492
541
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
542
|
+
if (ctx.hasUI)
|
|
543
|
+
lastUi = ctx.ui;
|
|
493
544
|
const config = configFor(ctx);
|
|
494
545
|
attempts = new AttemptWindow(config.stuck.window);
|
|
495
546
|
doneNudged = false;
|
|
@@ -555,8 +606,10 @@ export default function wardenExtension(pi) {
|
|
|
555
606
|
if (storedPath) {
|
|
556
607
|
const kind = classifyRecall(event.toolName, event.input, storedPath);
|
|
557
608
|
const recalled = ledger.noteAccess(serializedInput, kind);
|
|
558
|
-
if (recalled)
|
|
609
|
+
if (recalled) {
|
|
610
|
+
compressionLearner.noteRecall(event.toolName);
|
|
559
611
|
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: `full output recalled (${kind})` }), [`the agent went back to ${recalled} (${kind === "full" ? "whole-file read" : "scoped access"})`, formatLedger(ledger.snapshot())]);
|
|
612
|
+
}
|
|
560
613
|
}
|
|
561
614
|
if (!config.action.enabled || !config.action.tools.includes(event.toolName))
|
|
562
615
|
return;
|
|
@@ -653,13 +706,14 @@ export default function wardenExtension(pi) {
|
|
|
653
706
|
noteOutcomes(config, [released]);
|
|
654
707
|
const learningId = learningIds.get(released.id);
|
|
655
708
|
if (learningId)
|
|
656
|
-
recordOutcome(learningId, "approved");
|
|
709
|
+
recordOutcome(learningId, "approved").catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
657
710
|
}
|
|
658
711
|
}
|
|
659
712
|
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
|
|
660
713
|
// SAFETY: verdict.source is a string union; the read-only → pattern rewrite above may have narrowed it in TS's view,
|
|
661
714
|
// but the field is still one of the source values at runtime when no armed hits fired.
|
|
662
|
-
const
|
|
715
|
+
const actionFmt = formatVerdictTokens(verdict, config.widget.action);
|
|
716
|
+
const entry = verdict.source !== "read-only" ? record(ctx, config, "action", actionFmt.line, actionDetails(verdict, { mode, ...(told ? { told } : {}) }), actionFmt.tokens) : undefined;
|
|
663
717
|
// What happens to this call is the label for its scores; a decision made in the dialog lands at once, a steer-mode hold waits for the user.
|
|
664
718
|
const track = (held, outcome, via) => {
|
|
665
719
|
if (!entry)
|
|
@@ -679,10 +733,11 @@ export default function wardenExtension(pi) {
|
|
|
679
733
|
noteOutcomes(config, outcome ? [item] : []);
|
|
680
734
|
// Record to SQLite for learning
|
|
681
735
|
if (held) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
736
|
+
recordHold(toHoldRecord({ at: item.at, tool: item.tool, level: item.level, reasons: item.reasons, scores: item.scores }, ctx.cwd, { task: task ? redact(task) : task, plan: verdict.plan, contextSummary: ctxSummary, agentReason: steerReason(verdict, { canApprove: judge !== undefined }) })).then(id => {
|
|
737
|
+
learningIds.set(item.id, id);
|
|
738
|
+
if (outcome)
|
|
739
|
+
recordOutcome(id, outcome).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
740
|
+
}).catch(err => console.warn("pi-warden: recordHold failed:", err));
|
|
686
741
|
pruneLearningIds();
|
|
687
742
|
}
|
|
688
743
|
};
|
|
@@ -849,7 +904,7 @@ export default function wardenExtension(pi) {
|
|
|
849
904
|
blockVerdicts.push(await evaluateOutput(event.toolName, blockText, latestUserPrompt(ctx), {
|
|
850
905
|
security: config.security, context: config.context, judge: judgeFor(config), timeoutMs: config.timeoutMs,
|
|
851
906
|
signal: ctx.signal, compressible: true, taskContext: recentTaskContext(ctx),
|
|
852
|
-
}));
|
|
907
|
+
}, compressionLearner));
|
|
853
908
|
}
|
|
854
909
|
output = mergeOutput(blockVerdicts);
|
|
855
910
|
}
|
|
@@ -857,7 +912,7 @@ export default function wardenExtension(pi) {
|
|
|
857
912
|
output = await evaluateOutput(event.toolName, text, latestUserPrompt(ctx), {
|
|
858
913
|
security: config.security, context: config.context, judge: judgeFor(config), timeoutMs: config.timeoutMs,
|
|
859
914
|
signal: ctx.signal, compressible: true, taskContext: recentTaskContext(ctx),
|
|
860
|
-
});
|
|
915
|
+
}, compressionLearner);
|
|
861
916
|
}
|
|
862
917
|
if (ctx.signal?.aborted)
|
|
863
918
|
return;
|
|
@@ -943,6 +998,7 @@ export default function wardenExtension(pi) {
|
|
|
943
998
|
replacement = body;
|
|
944
999
|
ledger.record(path, bytesSaved);
|
|
945
1000
|
storedPath = path;
|
|
1001
|
+
compressionLearner.record(event.toolName, verdict.retention, verdict.format, false);
|
|
946
1002
|
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: verdict.retention, bytesSaved: String(bytesSaved) }), [
|
|
947
1003
|
`text block ${textIndex} of ${blockVerdicts.length}: retention ${verdict.retention}; confidence ${verdict.confidence?.toFixed(2)}; format ${verdict.format ?? "generic"}${verdict.formatConfidence === undefined ? "" : ` (${verdict.formatConfidence.toFixed(2)})`}; saved ${bytesSaved} bytes; full output: ${path}`,
|
|
948
1004
|
formatLedger(ledger.snapshot()),
|
|
@@ -970,6 +1026,7 @@ export default function wardenExtension(pi) {
|
|
|
970
1026
|
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
971
1027
|
ledger.record(path, bytesSaved);
|
|
972
1028
|
storedPath = path;
|
|
1029
|
+
compressionLearner.record(event.toolName, output.retention, output.format, false);
|
|
973
1030
|
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: output.retention, bytesSaved: String(bytesSaved) }), [
|
|
974
1031
|
`retention: ${output.retention}; confidence ${output.confidence?.toFixed(2)}; format ${output.format ?? "generic"}${output.formatConfidence === undefined ? "" : ` (${output.formatConfidence.toFixed(2)})`}; ${output.model}; ${output.elapsedMs} ms`,
|
|
975
1032
|
`saved ${bytesSaved} bytes; full output: ${path}`,
|
|
@@ -1127,7 +1184,7 @@ export default function wardenExtension(pi) {
|
|
|
1127
1184
|
}
|
|
1128
1185
|
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace"];
|
|
1129
1186
|
pi.registerCommand("warden", {
|
|
1130
|
-
description: "pi-warden status, TypeSafe consent,
|
|
1187
|
+
description: "pi-warden status, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, and a synthetic guard test",
|
|
1131
1188
|
getArgumentCompletions(prefix) {
|
|
1132
1189
|
const matches = actions.filter(action => action.startsWith(prefix)).map(action => ({ value: action, label: action }));
|
|
1133
1190
|
return matches.length ? matches : null;
|
|
@@ -1153,6 +1210,7 @@ export default function wardenExtension(pi) {
|
|
|
1153
1210
|
formatSteers(stats),
|
|
1154
1211
|
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / steer ${config.action.offTask.steer} (never holds); intent mismatch ${config.action.intentMismatch} (${config.action.visibleMismatch} on a visible action); 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}.`,
|
|
1155
1212
|
formatLedger(ledger.snapshot()),
|
|
1213
|
+
...(config.learning.patternAnalysis ? [`Learning: ${(await generateRecommendations(ctx.cwd)).length} recommendations, steer effectiveness ${Math.round((await analyzeSteerEffectivenessReport(ctx.cwd)).overall * 100)}% (use /warden recommend for details)`] : []),
|
|
1156
1214
|
`${formatHolds(holds.snapshot(), config.action.feedbackLog ? holdLog?.path : undefined)}${holdLog?.lastFailure ? ` Log write failed: ${holdLog.lastFailure}.` : ""}`,
|
|
1157
1215
|
`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"}.`,
|
|
1158
1216
|
...(config.action.armingRules.length > 0 ? [`Arming: ${arming.statusLine() || "no rules armed"}.`] : []),
|
|
@@ -1172,6 +1230,49 @@ export default function wardenExtension(pi) {
|
|
|
1172
1230
|
togglePanel(ctx.ui, config);
|
|
1173
1231
|
return;
|
|
1174
1232
|
}
|
|
1233
|
+
if (action === "recommend") {
|
|
1234
|
+
// Learning-driven recommendations based on hold history
|
|
1235
|
+
const recommendations = await generateRecommendations(ctx.cwd);
|
|
1236
|
+
const steerReport = await analyzeSteerEffectivenessReport(ctx.cwd);
|
|
1237
|
+
const lines = [];
|
|
1238
|
+
lines.push("pi-warden learning recommendations:");
|
|
1239
|
+
if (recommendations.length === 0 && steerReport.suggestions.length === 0) {
|
|
1240
|
+
lines.push(`No recommendations yet. Need more hold data (current: ${holds.snapshot().holds} holds).`);
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
if (recommendations.length > 0) {
|
|
1244
|
+
lines.push("");
|
|
1245
|
+
lines.push("Threshold & pattern recommendations:");
|
|
1246
|
+
for (const rec of recommendations.slice(0, 5)) {
|
|
1247
|
+
lines.push(` [${rec.priority}] ${rec.message}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (steerReport.suggestions.length > 0) {
|
|
1251
|
+
lines.push("");
|
|
1252
|
+
lines.push("Steer effectiveness:");
|
|
1253
|
+
lines.push(` Overall effectiveness: ${(steerReport.overall * 100).toFixed(0)}%`);
|
|
1254
|
+
for (const suggestion of steerReport.suggestions.slice(0, 3)) {
|
|
1255
|
+
lines.push(` ${suggestion}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (Object.keys(steerReport.byType).length > 0) {
|
|
1259
|
+
lines.push("");
|
|
1260
|
+
lines.push("Effectiveness by type:");
|
|
1261
|
+
for (const [type, stats] of Object.entries(steerReport.byType)) {
|
|
1262
|
+
lines.push(` ${type}: ${(stats.rate * 100).toFixed(0)}% (${stats.effective}/${stats.total})`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (steerReport.topPatterns.length > 0) {
|
|
1266
|
+
lines.push("");
|
|
1267
|
+
lines.push("Top performing patterns:");
|
|
1268
|
+
for (const pattern of steerReport.topPatterns.slice(0, 3)) {
|
|
1269
|
+
lines.push(` ${pattern.pattern}: ${(pattern.effectiveness * 100).toFixed(0)}% effectiveness (${pattern.sampleSize} samples)`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
report(lines.join("\n"));
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1175
1276
|
if (action === "enable") {
|
|
1176
1277
|
if (!ctx.hasUI) {
|
|
1177
1278
|
report("Consent needs an interactive session. For headless runs set PI_WARDEN_ENABLED=1 and TYPESAFE_API_KEY explicitly.", "warning");
|
|
@@ -1206,31 +1307,36 @@ export default function wardenExtension(pi) {
|
|
|
1206
1307
|
return;
|
|
1207
1308
|
}
|
|
1208
1309
|
if (action === "config") {
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1310
|
+
if (argument && argument.startsWith("set ")) {
|
|
1311
|
+
const rest = argument.slice(4).trim();
|
|
1312
|
+
const spaceIndex = rest.indexOf(" ");
|
|
1313
|
+
if (spaceIndex === -1) {
|
|
1314
|
+
report("Usage: /warden config set <key> <value>", "warning");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const keyPath = rest.slice(0, spaceIndex).trim();
|
|
1318
|
+
const rawValue = rest.slice(spaceIndex + 1).trim();
|
|
1319
|
+
const value = parseConfigValue(rawValue);
|
|
1320
|
+
const current = readUserConfig();
|
|
1321
|
+
const updated = setNestedValue(current, keyPath, value);
|
|
1322
|
+
const savedPath = writeUserConfig(updated);
|
|
1323
|
+
client = undefined;
|
|
1324
|
+
report(`Saved ${keyPath} = ${JSON.stringify(value)}.`);
|
|
1211
1325
|
return;
|
|
1212
1326
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1327
|
+
if (argument && argument.startsWith("get ")) {
|
|
1328
|
+
const keyPath = argument.slice(4).trim();
|
|
1329
|
+
const current = readUserConfig();
|
|
1330
|
+
const value = getNestedValue(current, keyPath);
|
|
1331
|
+
const defaultValue = getNestedValue(defaultConfig(), keyPath);
|
|
1332
|
+
report(value === undefined ? `${keyPath} not set (default: ${JSON.stringify(defaultValue)})` : `${keyPath} = ${JSON.stringify(value)}`);
|
|
1217
1333
|
return;
|
|
1218
|
-
let parsed;
|
|
1219
|
-
try {
|
|
1220
|
-
parsed = JSON.parse(text);
|
|
1221
1334
|
}
|
|
1222
|
-
|
|
1223
|
-
report(
|
|
1335
|
+
if (!ctx.hasUI || !lastUi) {
|
|
1336
|
+
report(`Edit ${userConfigPath()} directly. Use /warden config set <key> <value> for quick changes.`);
|
|
1224
1337
|
return;
|
|
1225
1338
|
}
|
|
1226
|
-
|
|
1227
|
-
report("The config must be a JSON object; nothing was saved.", "error");
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
const path = writeUserConfig(parsed);
|
|
1231
|
-
const effective = applyUserOverrides(defaultConfig(), parsed);
|
|
1232
|
-
client = undefined;
|
|
1233
|
-
report(`Saved ${path}. Effective: guard ${effective.enabled && effective.action.enabled ? "on" : "off"}, mode ${effective.mode}, TypeSafe ${effective.typesafe ? "on" : "off"}, tools ${effective.action.tools.join(", ")}, irreversible hold ≥ ${effective.action.irreversible.confirm}, off-task steer ≥ ${effective.action.offTask.steer}, stuck ${effective.stuck.enabled ? "on" : "off"}, done-check ${effective.done.enabled ? "on" : "off"}, slop ${effective.slop.enabled ? "on" : "off"}.`);
|
|
1339
|
+
openConfigPanel(lastUi, config, { width: config.widget.panelWidth });
|
|
1234
1340
|
return;
|
|
1235
1341
|
}
|
|
1236
1342
|
if (action === "test") {
|
|
@@ -1238,7 +1344,8 @@ export default function wardenExtension(pi) {
|
|
|
1238
1344
|
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Clean up the demo directory") goes to ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
|
|
1239
1345
|
return;
|
|
1240
1346
|
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Clean up the demo directory" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge });
|
|
1241
|
-
|
|
1347
|
+
const fmt = formatVerdictTokens(verdict, config.widget.action);
|
|
1348
|
+
record(ctx, config, "action", fmt.line, actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }), fmt.tokens);
|
|
1242
1349
|
report(`${formatVerdict(verdict)}${verdict.reasons.length ? ` — ${verdict.reasons.join("; ")}` : ""}${judge ? "" : " (pattern checks only: TypeSafe judgments are not enabled or no key is configured)"}${verdict.error ? ` — ${verdict.error}` : ""}`);
|
|
1243
1350
|
if (verdict.level === "confirm") {
|
|
1244
1351
|
const mode = activeMode(config, ctx.hasUI);
|