pi-warden 0.64.1 → 0.71.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/CHANGELOG.md +95 -0
- package/README.md +49 -93
- package/dist/adaptive.d.ts +132 -0
- package/dist/adaptive.js +274 -0
- package/dist/atomic.d.ts +10 -0
- package/dist/atomic.js +51 -0
- package/dist/config.d.ts +40 -1
- package/dist/config.js +37 -9
- package/dist/evidence.d.ts +93 -0
- package/dist/evidence.js +409 -0
- package/dist/extension.js +213 -40
- package/dist/extension.js.bak +668 -102
- package/dist/index.d.ts +10 -2
- package/dist/index.js +5 -1
- package/dist/loops.d.ts +10 -0
- package/dist/loops.js +15 -5
- package/dist/prefs.d.ts +8 -0
- package/dist/prefs.js +15 -5
- package/dist/rules-lint.d.ts +84 -0
- package/dist/rules-lint.js +191 -0
- package/dist/rules-log.d.ts +98 -0
- package/dist/rules-log.js +190 -0
- package/dist/rules-report.d.ts +52 -0
- package/dist/rules-report.js +83 -0
- package/dist/rules.d.ts +24 -2
- package/dist/rules.js +154 -33
- package/dist/shape.d.ts +1 -1
- package/dist/shape.js +14 -3
- package/dist/stuck.d.ts +34 -11
- package/dist/stuck.js +20 -8
- package/dist/trace.d.ts +1 -1
- package/dist/waste.d.ts +98 -0
- package/dist/waste.js +582 -0
- package/package.json +4 -2
package/dist/extension.js
CHANGED
|
@@ -8,6 +8,7 @@ import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
|
8
8
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
9
9
|
import { backendHost, disclosureFor, judgeOptions, keyEnvFor, loginStoresKey, resolveBackend } from "./backend.js";
|
|
10
10
|
import { ActionGuard } from "./action-guard.js";
|
|
11
|
+
import { assistantView, formatMuted, NEVER_MUTED, STEER_KINDS, SteerStats, SteerWatch } from "./adaptive.js";
|
|
11
12
|
import { ArmingTracker, unparseableArmingRules } from "./arming.js";
|
|
12
13
|
import * as configModule from "./config.js";
|
|
13
14
|
import { applyUserOverrides, defaultConfig, getNestedValue, isMode, loadConfig, PACKAGE_NAME, parseConfigValue, projectConfigPath, readUserConfig, setNestedValue, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
@@ -22,12 +23,16 @@ import { compressOutput, duplicateNote, evaluateOutput, mergeOutput, outputKey,
|
|
|
22
23
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
23
24
|
import { maskSecrets, redact } from "./redact.js";
|
|
24
25
|
import { formatRules, pathNoteSteer, projectPath, RulesGuard, rulesSteer, RULES_FILE, FALLBACK_FILES } from "./rules.js";
|
|
26
|
+
import { checkRules, formatRulesCheck } from "./rules-lint.js";
|
|
27
|
+
import { readRulesLog, RulesLog, rulesLogPath } from "./rules-log.js";
|
|
28
|
+
import { buildRulesReport, formatRulesReport, REPORT_DEFAULT_DAYS } from "./rules-report.js";
|
|
25
29
|
import { checkPiWardenMissing } from "./rules-file.js";
|
|
26
30
|
import { writeStarterRules, buildInitPrompt } from "./init.js";
|
|
27
31
|
import { buildAuditPrompt, findProjects, snapshotReport, reportOutcome } from "./audit.js";
|
|
28
32
|
import { detectNotifier, sendNotification } from "./notify.js";
|
|
29
33
|
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
30
34
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, quickRepeatNudge, resultFailed, stuckDiff, stuckNudge } from "./stuck.js";
|
|
35
|
+
import { WasteTracker, WASTE_TIP, withWasteTip } from "./waste.js";
|
|
31
36
|
import { assess } from "./conscience.js";
|
|
32
37
|
import { loadSkillBody, buildLoadMessage, policyMatches, CONSCIENCE_BETA_POLICY, recordFileIdentity, clearFileIdentityCache } from "./load.js";
|
|
33
38
|
import { buildIndexPrompt, readIndex, writeIndex, validateIndex, indexStats, indexPath, ensureIndexDir } from "./index-cmd.js";
|
|
@@ -38,8 +43,8 @@ import { completeConfig, shapeWarning, taskSpine } from "./shape.js";
|
|
|
38
43
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
39
44
|
import { SeenText, collapseRuns, seenItem } from "./dedupe.js";
|
|
40
45
|
import { buildCompactSnapshot, compactAppendix, recallText } from "./compact.js";
|
|
41
|
-
import { applyLoopAction, formatLoopsForUser, formatOpenLoops, LOOP_ACTIONS, LOOP_CHARS, loopsFingerprint, loopsPath, openLoops, readLoops,
|
|
42
|
-
import { emptyWordCounts, evaluatePrefs, forgetPref, formatPrefs, isCorrection, LESSON_CHARS, LESSON_TURNS, NO_LESSON_SIGNAL, prefsMessage, prefsStorePath, readPrefsStore, recordLesson, scanPreferences
|
|
46
|
+
import { applyLoopAction, formatLoopsForUser, formatOpenLoops, LOOP_ACTIONS, LOOP_CHARS, loopsFingerprint, loopsPath, openLoops, readLoops, updateLoops } from "./loops.js";
|
|
47
|
+
import { changePrefsStore, emptyWordCounts, evaluatePrefs, forgetPref, formatPrefs, isCorrection, LESSON_CHARS, LESSON_TURNS, NO_LESSON_SIGNAL, prefsMessage, prefsStorePath, readPrefsStore, recordLesson, scanPreferences } from "./prefs.js";
|
|
43
48
|
import { formatWake, newReports, reportLabel, triageReport, WakePolicy } from "./subagent.js";
|
|
44
49
|
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
45
50
|
import { TraceFile, judgmentsState, traceDir, traceFilePath } from "./trace-file.js";
|
|
@@ -49,7 +54,7 @@ const WIDGET = PACKAGE_NAME;
|
|
|
49
54
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
50
55
|
/** Rules requests one bash command may start; the other files it writes are recorded as skipped. */
|
|
51
56
|
const SHELL_RULES_CHECKS = 5;
|
|
52
|
-
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, offPlan: 0, offPlanTraceOnly: 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, cooldownSkips: 0, steers: 0, steersSkipped: 0, steerGuards: {}, subagentReports: 0, subagentWoken: 0, restatements: 0 });
|
|
57
|
+
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, offPlan: 0, offPlanTraceOnly: 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, cooldownSkips: 0, steers: 0, steersSkipped: 0, steersMuted: 0, steerGuards: {}, subagentReports: 0, subagentWoken: 0, restatements: 0 });
|
|
53
58
|
/**
|
|
54
59
|
* One steer message can carry notes from more than one guard, so the per-guard numbers may add up to more than the
|
|
55
60
|
* message count; the line says so instead of hiding it. Worst offender first: that is the number worth acting on.
|
|
@@ -272,7 +277,7 @@ export function guardCurrentSections(result) {
|
|
|
272
277
|
if (typeof config.rules !== "object" || config.rules === null || !Array.isArray(config.rules.exclude)) {
|
|
273
278
|
if (!missing.includes("rules"))
|
|
274
279
|
missing.push("rules");
|
|
275
|
-
config.rules = { enabled: false, threshold: 1, files: [], fallback: false, maxChars: 500, exclude: [], skip: [], sensitivePaths: {} };
|
|
280
|
+
config.rules = { enabled: false, threshold: 1, softThreshold: 0, files: [], fallback: false, maxChars: 500, exclude: [], skip: [], sensitivePaths: {} };
|
|
276
281
|
}
|
|
277
282
|
if (typeof config.widget !== "object" || config.widget === null) {
|
|
278
283
|
if (!missing.includes("widget"))
|
|
@@ -345,9 +350,14 @@ export default function wardenExtension(pi) {
|
|
|
345
350
|
}
|
|
346
351
|
const traceOf = new WeakMap();
|
|
347
352
|
let holdLog;
|
|
353
|
+
// Local rules verdict log for this session; a write failure is silent to the agent and shown once in the trace.
|
|
354
|
+
let rulesLog;
|
|
348
355
|
// Allowed calls of the turn the user just replied to; the regret question about them rides the next action request.
|
|
349
356
|
let regretCandidates = [];
|
|
350
357
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
358
|
+
// Call-waste notes for this session; the tip is offered once, the notes are rate limited per detector.
|
|
359
|
+
const waste = new WasteTracker();
|
|
360
|
+
let wasteTipOffered = false;
|
|
351
361
|
/** Full output text and saved path keyed by attempt call key, for stuck-loop diffs. */
|
|
352
362
|
let fullOutputs = new Map();
|
|
353
363
|
let evidence = emptyEvidence();
|
|
@@ -746,6 +756,9 @@ export default function wardenExtension(pi) {
|
|
|
746
756
|
/** Guards whose notices gate the run itself; they deliver even when the per-run steer budget is spent. */
|
|
747
757
|
const CRITICAL_STEER_GUARDS = new Set(["stuck", "done", "runaway", "subagent"]);
|
|
748
758
|
let steersThisRun = 0;
|
|
759
|
+
/** Follow and dispute counts per (model, steer kind), kept across sessions; and the sent steers still waiting for a reply. */
|
|
760
|
+
const steerStats = new SteerStats();
|
|
761
|
+
const steerWatch = new SteerWatch();
|
|
749
762
|
/** Session generation counter for conscience invalidation. Incremented on steer, abort, reload, switch, or config change. */
|
|
750
763
|
let conscienceGeneration = 0;
|
|
751
764
|
/** Per-prompt error categories already warned about (one console.warn per category per prompt). */
|
|
@@ -807,6 +820,39 @@ export default function wardenExtension(pi) {
|
|
|
807
820
|
wardenContinuation = true;
|
|
808
821
|
return true;
|
|
809
822
|
};
|
|
823
|
+
const modelKey = (ctx) => ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
824
|
+
const KIND_GUARD = { rules: "rules", "sensitive-path": "rules", "security-write": "security", stuck: "stuck", repeat: "stuck", prose: "prose", conscience: "conscience" };
|
|
825
|
+
/**
|
|
826
|
+
* False when steers of this kind are trace-only for the current model; the skipped note gets a trace line. Holds and
|
|
827
|
+
* judgments are not affected: only whether the note reaches the agent.
|
|
828
|
+
*/
|
|
829
|
+
const adaptiveSend = (ctx, config, kind) => {
|
|
830
|
+
const model = modelKey(ctx);
|
|
831
|
+
if (!model)
|
|
832
|
+
return true;
|
|
833
|
+
const decision = steerStats.request(model, kind, config.steers);
|
|
834
|
+
if (decision !== "trace-only")
|
|
835
|
+
return true;
|
|
836
|
+
const pair = steerStats.pair(model, kind);
|
|
837
|
+
const rates = pair ? `${pair.sent} steers observed, ${Math.round((pair.followed / Math.max(1, pair.sent)) * 100)}% followed, ${Math.round((pair.disputed / Math.max(1, pair.sent)) * 100)}% disputed` : "no counts";
|
|
838
|
+
stats.steersMuted++;
|
|
839
|
+
record(ctx, config, KIND_GUARD[kind] ?? "action", `warden · steer trace-only · ${kind} · ${model}`, [
|
|
840
|
+
`${kind} steers are trace-only for ${model} (${rates}); 1 in ${config.steers.probeEvery} is still sent for the re-check`,
|
|
841
|
+
`/warden unmute ${kind} sends them again`,
|
|
842
|
+
]);
|
|
843
|
+
return false;
|
|
844
|
+
};
|
|
845
|
+
/** A delivered steer waits for the agent's next two messages; its outcome is counted for the (model, kind) pair. */
|
|
846
|
+
const watchSteer = (ctx, config, kinds, subject = {}, nextTurn = false) => {
|
|
847
|
+
const model = modelKey(ctx);
|
|
848
|
+
if (!model || !config.steers.adaptive)
|
|
849
|
+
return;
|
|
850
|
+
steerWatch.add(model, kinds.filter(kind => !NEVER_MUTED.has(kind) || kind === "done"), subject, { nextTurn });
|
|
851
|
+
};
|
|
852
|
+
const settleSteers = (config, outcomes) => {
|
|
853
|
+
for (const outcome of outcomes)
|
|
854
|
+
steerStats.observe(outcome.model, outcome.kind, outcome, config.steers);
|
|
855
|
+
};
|
|
810
856
|
/**
|
|
811
857
|
* Desktop notification for a moment that needs the user back at the terminal. Interactive sessions only: a headless run
|
|
812
858
|
* or a subagent has nobody to call, and several of them would flood the desktop. Fire-and-forget; failures are silent.
|
|
@@ -878,6 +924,7 @@ export default function wardenExtension(pi) {
|
|
|
878
924
|
initRunning = false;
|
|
879
925
|
await initSchema(loadConfig().learning.retentionDays);
|
|
880
926
|
stats = freshStats();
|
|
927
|
+
steerWatch.clear();
|
|
881
928
|
widget.clear();
|
|
882
929
|
trace.clear();
|
|
883
930
|
panel?.close();
|
|
@@ -888,6 +935,7 @@ export default function wardenExtension(pi) {
|
|
|
888
935
|
regretCandidates = [];
|
|
889
936
|
const sessionId = typeof ctx.sessionManager.getSessionId === "function" ? ctx.sessionManager.getSessionId() : String(process.pid);
|
|
890
937
|
holdLog = new HoldLog(holdLogPath(sessionId));
|
|
938
|
+
rulesLog = new RulesLog(ctx.cwd, sessionId, { onFailure: message => record(ctx, loadConfig(), "rules", "rules log write failed", [message]) });
|
|
891
939
|
unsubscribeTraceFile?.();
|
|
892
940
|
unsubscribeTraceFile = undefined;
|
|
893
941
|
traceFile = undefined;
|
|
@@ -910,6 +958,8 @@ export default function wardenExtension(pi) {
|
|
|
910
958
|
}
|
|
911
959
|
arming.reset();
|
|
912
960
|
attempts.reset();
|
|
961
|
+
waste.reset();
|
|
962
|
+
wasteTipOffered = false;
|
|
913
963
|
evidence = emptyEvidence();
|
|
914
964
|
doneNudged = false;
|
|
915
965
|
wardenContinuation = false;
|
|
@@ -991,6 +1041,22 @@ export default function wardenExtension(pi) {
|
|
|
991
1041
|
noticeUi = ctx.hasUI ? ctx.ui : undefined;
|
|
992
1042
|
const config = configFor(ctx);
|
|
993
1043
|
attempts = new AttemptWindow(config.stuck.window);
|
|
1044
|
+
// ── Call waste: the session tip ──
|
|
1045
|
+
// The tip is appended to the prompt, so every other part of the prompt stays where it was and the host records the
|
|
1046
|
+
// change as a prompt-section delta. It is offered once per session; the trace line says so, and the next runs
|
|
1047
|
+
// re-append the same text, which is what keeps it in the prompt instead of being diffed away.
|
|
1048
|
+
const wasteOptions = event.systemPromptOptions;
|
|
1049
|
+
if (config.enabled && config.waste.enabled && config.waste.tip && wasteOptions) {
|
|
1050
|
+
wasteOptions.appendSystemPrompt = withWasteTip(wasteOptions.appendSystemPrompt);
|
|
1051
|
+
if (!wasteTipOffered) {
|
|
1052
|
+
wasteTipOffered = true;
|
|
1053
|
+
record(ctx, config, "waste", "warden · waste · session tip · instructions_supplied", [
|
|
1054
|
+
"trigger: before_agent_start",
|
|
1055
|
+
"delivery: appended to the system prompt (appendSystemPrompt)",
|
|
1056
|
+
`tip: ${WASTE_TIP}`,
|
|
1057
|
+
]);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
994
1060
|
fullOutputs = new Map();
|
|
995
1061
|
doneNudged = false;
|
|
996
1062
|
wardenContinuation = false;
|
|
@@ -1490,6 +1556,7 @@ export default function wardenExtension(pi) {
|
|
|
1490
1556
|
// Notes for the agent about the content it just wrote: slop, rule violations, and sensitive paths arrive as one message.
|
|
1491
1557
|
const notes = [];
|
|
1492
1558
|
const noteGuards = new Set();
|
|
1559
|
+
const noteKinds = [];
|
|
1493
1560
|
/** Slop and rule findings say "just written", so they wait until the call proceeds. A held, denied, or declined write
|
|
1494
1561
|
* never lands and drops them; an approved retry is judged again and brings its own. They go between the action notes
|
|
1495
1562
|
* and the sensitive-path notes, the order of the combined message. */
|
|
@@ -1497,8 +1564,9 @@ export default function wardenExtension(pi) {
|
|
|
1497
1564
|
const pathNotes = [];
|
|
1498
1565
|
/** Sensitive-path notes ride with the combined steer for this call; their trace waits for the delivery result. */
|
|
1499
1566
|
const pathNoteTraces = [];
|
|
1500
|
-
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold) {
|
|
1501
|
-
steer(config, "security", "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.")
|
|
1567
|
+
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold && adaptiveSend(ctx, config, "security-write")) {
|
|
1568
|
+
if (steer(config, "security", "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."))
|
|
1569
|
+
watchSteer(ctx, config, ["security-write"], verdict.summary.path ? { path: verdict.summary.path } : {});
|
|
1502
1570
|
}
|
|
1503
1571
|
if (verdict.judgment)
|
|
1504
1572
|
stats.judged++;
|
|
@@ -1547,25 +1615,29 @@ export default function wardenExtension(pi) {
|
|
|
1547
1615
|
stats.offPlan++;
|
|
1548
1616
|
if (verdict.intentTraceOnly)
|
|
1549
1617
|
stats.offPlanTraceOnly++;
|
|
1550
|
-
else {
|
|
1618
|
+
else if (adaptiveSend(ctx, config, "intent-mismatch")) {
|
|
1551
1619
|
noteGuards.add("action");
|
|
1620
|
+
noteKinds.push("intent-mismatch");
|
|
1552
1621
|
notes.push(intentSteer(verdict));
|
|
1553
1622
|
}
|
|
1554
1623
|
}
|
|
1555
1624
|
if (verdict.offTaskSteer) {
|
|
1556
1625
|
stats.offTask++;
|
|
1557
|
-
if (!verdict.offTaskTraceOnly) {
|
|
1626
|
+
if (!verdict.offTaskTraceOnly && adaptiveSend(ctx, config, "off-task")) {
|
|
1558
1627
|
noteGuards.add("action");
|
|
1628
|
+
noteKinds.push("off-task");
|
|
1559
1629
|
notes.push(offTaskSteer(verdict));
|
|
1560
1630
|
}
|
|
1561
1631
|
}
|
|
1562
|
-
if (verdict.shouldProceedSteer && !verdict.shouldProceedTraceOnly) {
|
|
1632
|
+
if (verdict.shouldProceedSteer && !verdict.shouldProceedTraceOnly && adaptiveSend(ctx, config, "should-proceed")) {
|
|
1563
1633
|
noteGuards.add("action");
|
|
1634
|
+
noteKinds.push("should-proceed");
|
|
1564
1635
|
notes.push(shouldProceedMessage(verdict));
|
|
1565
1636
|
}
|
|
1566
1637
|
const largeOutput = largeOutputNotice(verdict, largeOutputSteered);
|
|
1567
|
-
if (largeOutput) {
|
|
1638
|
+
if (largeOutput && adaptiveSend(ctx, config, "large-output")) {
|
|
1568
1639
|
noteGuards.add("action");
|
|
1640
|
+
noteKinds.push("large-output");
|
|
1569
1641
|
notes.push(largeOutput);
|
|
1570
1642
|
}
|
|
1571
1643
|
if (verdict.slopSymptoms?.length && verdict.slopReasons) {
|
|
@@ -1575,11 +1647,11 @@ export default function wardenExtension(pi) {
|
|
|
1575
1647
|
if (ctx.hasUI && config.notices)
|
|
1576
1648
|
ctx.ui.notify(`warden · slop · ${where}: ${verdict.slopReasons.join("; ")}`, "warning");
|
|
1577
1649
|
contentNotes.push(proceeds => {
|
|
1578
|
-
if (!proceeds)
|
|
1650
|
+
if (!proceeds || !adaptiveSend(ctx, config, "slop"))
|
|
1579
1651
|
return undefined;
|
|
1580
1652
|
for (const symptom of symptoms)
|
|
1581
1653
|
slopCounts[symptom]++;
|
|
1582
|
-
return { guard: "action", text: slopSteer(where, symptoms, slopCounts) };
|
|
1654
|
+
return { guard: "action", kind: "slop", text: slopSteer(where, symptoms, slopCounts) };
|
|
1583
1655
|
});
|
|
1584
1656
|
}
|
|
1585
1657
|
for (const { check, shellWrite } of rulesChecks) {
|
|
@@ -1596,18 +1668,26 @@ export default function wardenExtension(pi) {
|
|
|
1596
1668
|
if (capNotice)
|
|
1597
1669
|
ctx.ui.notify(capNotice, "warning");
|
|
1598
1670
|
holds.recordRules({ source: rules.source, path: rules.path, findings: rules.findings.map(f => ({ name: f.name, violation: f.violation })), ...(rules.error ? { error: rules.error } : {}) });
|
|
1671
|
+
// A later judgment below the threshold clears an earlier finding of the same rule on this path: one trace line, no steer.
|
|
1672
|
+
if (rules.source === "typesafe") {
|
|
1673
|
+
for (const observation of rulesLog?.record(rules, config.rules.threshold, Date.now(), config.rules.softThreshold) ?? []) {
|
|
1674
|
+
if (observation.cleared)
|
|
1675
|
+
record(ctx, config, "rules", `rules: ${observation.name} now clear on ${rules.path}`, [`clear: a later ${rules.tool} on this path scored ${observation.violation.toFixed(2)}, below the ${config.rules.threshold} threshold`]);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1599
1678
|
if (rules.findings.length) {
|
|
1600
1679
|
stats.ruleViolations++;
|
|
1601
1680
|
if (ctx.hasUI && config.notices)
|
|
1602
1681
|
ctx.ui.notify(`warden · rules · ${rules.path}: ${rules.findings.map(finding => `${finding.name} (${finding.violation.toFixed(2)})`).join("; ")}`, "warning");
|
|
1603
1682
|
}
|
|
1604
1683
|
contentNotes.push(proceeds => {
|
|
1605
|
-
const
|
|
1684
|
+
const hasRules = rules.findings.length > 0 || (rules.softFindings?.length ?? 0) > 0;
|
|
1685
|
+
const told = proceeds && hasRules && adaptiveSend(ctx, config, "rules") ? rulesSteer(rules, rulesGuard.count(rules)) : undefined;
|
|
1606
1686
|
const details = [...shellNote, ...rulesDetails(rules, told)];
|
|
1607
1687
|
if (!proceeds && rules.findings.length)
|
|
1608
1688
|
details.push("agent not told: the write was held");
|
|
1609
1689
|
record(ctx, config, "rules", rulesLine, details);
|
|
1610
|
-
return told ? { guard: "rules", text: told } : undefined;
|
|
1690
|
+
return told ? { guard: "rules", kind: "rules", text: told } : undefined;
|
|
1611
1691
|
});
|
|
1612
1692
|
}
|
|
1613
1693
|
else {
|
|
@@ -1620,7 +1700,7 @@ export default function wardenExtension(pi) {
|
|
|
1620
1700
|
}
|
|
1621
1701
|
if (config.rules.enabled && (event.toolName === "write" || event.toolName === "edit")) {
|
|
1622
1702
|
const hits = rulesGuard.notesFor(verdict.summary.location === "inside_project" ? verdict.summary.path : undefined, config.rules.sensitivePaths);
|
|
1623
|
-
if (hits.length && verdict.summary.path) {
|
|
1703
|
+
if (hits.length && verdict.summary.path && adaptiveSend(ctx, config, "sensitive-path")) {
|
|
1624
1704
|
stats.pathNotes++;
|
|
1625
1705
|
const told = pathNoteSteer(verdict.summary.path, hits);
|
|
1626
1706
|
// A repeat or a spent steer budget records the note without the agent reading it again, so the trace waits
|
|
@@ -1631,6 +1711,7 @@ export default function wardenExtension(pi) {
|
|
|
1631
1711
|
ctx.ui.notify(`warden · sensitive path · ${verdict.summary.path} (${hits.map(hit => hit.glob).join(", ")}); the agent was given the note`, "warning");
|
|
1632
1712
|
});
|
|
1633
1713
|
noteGuards.add("rules");
|
|
1714
|
+
noteKinds.push("sensitive-path");
|
|
1634
1715
|
pathNotes.push(told);
|
|
1635
1716
|
}
|
|
1636
1717
|
}
|
|
@@ -1641,17 +1722,28 @@ export default function wardenExtension(pi) {
|
|
|
1641
1722
|
if (!note)
|
|
1642
1723
|
continue;
|
|
1643
1724
|
noteGuards.add(note.guard);
|
|
1725
|
+
noteKinds.push(note.kind);
|
|
1644
1726
|
notes.push(note.text);
|
|
1645
1727
|
}
|
|
1646
1728
|
notes.push(...pathNotes);
|
|
1647
1729
|
if (notes.length) {
|
|
1648
1730
|
const delivered = steer(config, [...noteGuards], notes.join("\n\n"));
|
|
1731
|
+
const notePath = verdict.summary.path ?? shell?.writes[0]?.path;
|
|
1732
|
+
if (delivered)
|
|
1733
|
+
watchSteer(ctx, config, noteKinds, notePath ? { path: notePath } : {});
|
|
1649
1734
|
for (const traceNote of pathNoteTraces)
|
|
1650
1735
|
traceNote(delivered);
|
|
1651
1736
|
pathNoteTraces.length = 0;
|
|
1652
1737
|
}
|
|
1653
1738
|
};
|
|
1654
|
-
const warnSteer = (reasons) =>
|
|
1739
|
+
const warnSteer = (reasons) => {
|
|
1740
|
+
if (!reasons.length || !adaptiveSend(ctx, config, "warn-headless"))
|
|
1741
|
+
return false;
|
|
1742
|
+
const sent = steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${reasons.join("; ")}). Nobody sees this in a headless run, so it is on you: if the flagged risk is expected, continue; otherwise fix it or ask the user before building on it.`);
|
|
1743
|
+
if (sent)
|
|
1744
|
+
watchSteer(ctx, config, ["warn-headless"]);
|
|
1745
|
+
return sent;
|
|
1746
|
+
};
|
|
1655
1747
|
if (verdict.level === "warn") {
|
|
1656
1748
|
deliverNotes(true);
|
|
1657
1749
|
stats.warned++;
|
|
@@ -1751,6 +1843,10 @@ export default function wardenExtension(pi) {
|
|
|
1751
1843
|
pruneScratch(sessionScratch);
|
|
1752
1844
|
// Repeat detection uses the original result, so its request goes out together with the output check.
|
|
1753
1845
|
const failed = resultFailed(event.isError, event.details, event.content);
|
|
1846
|
+
// ── Call waste ──
|
|
1847
|
+
// Every finished call enters the window; a note, when one fires, is attached to this result below. Nothing here
|
|
1848
|
+
// reads the call's fate, and the note never reaches a hold, the judge, or the steer budget.
|
|
1849
|
+
const wasteNudge = waste.record({ tool: event.toolName, input: event.input, cwd: ctx.cwd, failed, result: text }, config.waste);
|
|
1754
1850
|
// ── Conscience: track tool_result for the pending capability ──
|
|
1755
1851
|
if (config.conscience.enabled && pendingCapability && !triggerConsumed) {
|
|
1756
1852
|
if (pendingCapability.kind === "tool" && event.toolName === pendingCapability.id) {
|
|
@@ -2035,6 +2131,32 @@ export default function wardenExtension(pi) {
|
|
|
2035
2131
|
fullOutputs.set(currentKey, { text });
|
|
2036
2132
|
}
|
|
2037
2133
|
const patch = content === event.content ? undefined : { content };
|
|
2134
|
+
/** The note rides the result the model is about to read; the call already ran, so this only adds text to it. */
|
|
2135
|
+
const withWaste = (current) => {
|
|
2136
|
+
if (!wasteNudge)
|
|
2137
|
+
return current;
|
|
2138
|
+
const base = current?.content ?? event.content;
|
|
2139
|
+
const lastText = base.reduce((last, part, index) => part.type === "text" ? index : last, -1);
|
|
2140
|
+
if (lastText < 0)
|
|
2141
|
+
return current;
|
|
2142
|
+
const next = base.map((part, index) => {
|
|
2143
|
+
if (part.type !== "text" || index !== lastText)
|
|
2144
|
+
return part;
|
|
2145
|
+
return { ...part, text: `${part.text}\n\n${wasteNudge.text}` };
|
|
2146
|
+
});
|
|
2147
|
+
// Redaction runs before the cut: a 200-character slice can leave half a credential the redactor no longer knows.
|
|
2148
|
+
const triggerCommand = event.toolName === "bash" && typeof event.input.command === "string"
|
|
2149
|
+
? ` ${redact(event.input.command).replace(/\s+/g, " ").slice(0, 200)}`
|
|
2150
|
+
: "";
|
|
2151
|
+
record(ctx, config, "waste", `warden · waste · ${wasteNudge.detector} · ${wasteNudge.subject} · agent told`, [
|
|
2152
|
+
`trigger: ${event.toolName}${triggerCommand}`,
|
|
2153
|
+
`detector: ${wasteNudge.detector}`,
|
|
2154
|
+
`subject: ${redact(wasteNudge.subject)}`,
|
|
2155
|
+
`nudge: ${wasteNudge.text}`,
|
|
2156
|
+
`rate limit: at most one ${wasteNudge.detector} note per ${config.waste.every} calls; no steer, no request`,
|
|
2157
|
+
]);
|
|
2158
|
+
return { content: next };
|
|
2159
|
+
};
|
|
2038
2160
|
// Checks use the original result, not the excerpts or security banner.
|
|
2039
2161
|
if (config.done.enabled)
|
|
2040
2162
|
recordDoneOutcome(evidence, classifyToolResult(event.toolName, event.input, failed, text), event.input, event.toolName);
|
|
@@ -2047,34 +2169,38 @@ export default function wardenExtension(pi) {
|
|
|
2047
2169
|
// A stuck verdict on the same call carries its own steer; the quick one would say the same thing twice.
|
|
2048
2170
|
if (quickRepeat && !verdict?.stuck) {
|
|
2049
2171
|
const nudge = quickRepeatNudge(quickRepeat);
|
|
2050
|
-
const sent = steer(config, "repeat", nudge);
|
|
2172
|
+
const sent = adaptiveSend(ctx, config, "repeat") && steer(config, "repeat", nudge);
|
|
2173
|
+
if (sent)
|
|
2174
|
+
watchSteer(ctx, config, ["repeat"], { call: { name: event.toolName, input: event.input } });
|
|
2051
2175
|
record(ctx, config, "stuck", `warden · stuck · repeat · ${quickRepeat.attempt.tool} · ${quickRepeat.attempt.failed ? "same failure" : "same output"} · ${sent ? "agent nudged" : "recorded only"}`, [
|
|
2052
|
-
`call: ${quickRepeat.attempt.call}`, `previous identical call: ${quickRepeat.callsAgo} calls ago, nothing changed between`, `agent told: ${sent ? nudge : "nothing (repeat of a recent steer
|
|
2176
|
+
`call: ${quickRepeat.attempt.call}`, `previous identical call: ${quickRepeat.callsAgo} calls ago, nothing changed between`, `agent told: ${sent ? nudge : "nothing (repeat of a recent steer, over the steer budget, or trace-only for this model)"}`,
|
|
2053
2177
|
]);
|
|
2054
2178
|
if (sent && ctx.hasUI && config.notices)
|
|
2055
2179
|
ctx.ui.notify(`warden \u00b7 repeat: ${quickRepeat.attempt.failed ? "same call failed the same way" : "same output re-read"} (agent nudged)`, "warning");
|
|
2056
2180
|
}
|
|
2057
2181
|
if (!verdict)
|
|
2058
|
-
return patch;
|
|
2182
|
+
return withWaste(patch);
|
|
2059
2183
|
if (verdict.error)
|
|
2060
2184
|
noteError(ctx, verdict.error, verdict.errorCode);
|
|
2061
2185
|
if (verdict.source === "repeat" && !verdict.stuck)
|
|
2062
|
-
return patch;
|
|
2186
|
+
return withWaste(patch);
|
|
2063
2187
|
const nudge = verdict.stuck && config.stuck.nudge ? stuckNudge(verdict) : undefined;
|
|
2064
2188
|
record(ctx, config, "stuck", formatStuck(verdict, config.widget.stuck), stuckDetails(verdict, attempts.attempts, nudge));
|
|
2065
2189
|
if (!verdict.stuck)
|
|
2066
|
-
return patch;
|
|
2190
|
+
return withWaste(patch);
|
|
2067
2191
|
stats.stuck++;
|
|
2068
2192
|
if (ctx.hasUI && config.notices)
|
|
2069
2193
|
ctx.ui.notify(`warden \u00b7 stuck: ${verdict.reasons.join("; ")}${nudge ? " (agent nudged)" : ""}`, "warning");
|
|
2070
|
-
if (nudge)
|
|
2071
|
-
|
|
2072
|
-
return patch;
|
|
2194
|
+
if (nudge && adaptiveSend(ctx, config, "stuck") && steer(config, "stuck", nudge))
|
|
2195
|
+
watchSteer(ctx, config, ["stuck"], { call: { name: event.toolName, input: event.input } });
|
|
2196
|
+
return withWaste(patch);
|
|
2073
2197
|
});
|
|
2074
2198
|
// Opt-in: a relayed report that pastes earlier turns again keeps only its new part. Pi persists the replaced message.
|
|
2075
2199
|
pi.on("message_end", async (event, ctx) => {
|
|
2076
2200
|
const config = configFor(ctx);
|
|
2077
2201
|
const message = event.message;
|
|
2202
|
+
if (message.role === "assistant")
|
|
2203
|
+
settleSteers(config, steerWatch.assistant(assistantView(message.content)));
|
|
2078
2204
|
if (!config.enabled || !config.context.enabled || !config.context.dedupeRuns || !config.context.dedupeMessages)
|
|
2079
2205
|
return;
|
|
2080
2206
|
if (message.role !== "user" && message.role !== "custom")
|
|
@@ -2151,6 +2277,7 @@ export default function wardenExtension(pi) {
|
|
|
2151
2277
|
}
|
|
2152
2278
|
// Operator input invalidates in-flight conscience assessments
|
|
2153
2279
|
conscienceGeneration++;
|
|
2280
|
+
settleSteers(configFor(ctx), steerWatch.user());
|
|
2154
2281
|
});
|
|
2155
2282
|
/**
|
|
2156
2283
|
* One short reminder of the open loops when a run ends, for the next turn: it starts no turn of its own, counts against
|
|
@@ -2166,8 +2293,10 @@ export default function wardenExtension(pi) {
|
|
|
2166
2293
|
if (!open.length || fingerprint === loopsReminded)
|
|
2167
2294
|
return;
|
|
2168
2295
|
const notice = `pi-warden: ${open.length} open loop${open.length === 1 ? "" : "s"} you promised in this session:\n${formatOpenLoops(open)}\nClose each with warden_loops done or drop once it is finished or no longer needed.`;
|
|
2169
|
-
if (steer(config, "loops", notice, { deliverAs: "nextTurn" }))
|
|
2296
|
+
if (adaptiveSend(ctx, config, "loops") && steer(config, "loops", notice, { deliverAs: "nextTurn" })) {
|
|
2170
2297
|
loopsReminded = fingerprint;
|
|
2298
|
+
watchSteer(ctx, config, ["loops"], {}, true);
|
|
2299
|
+
}
|
|
2171
2300
|
}
|
|
2172
2301
|
catch (error) {
|
|
2173
2302
|
record(ctx, config, "action", "loops reminder error", [`reading the loops file failed: ${error instanceof Error ? error.message : String(error)}`]);
|
|
@@ -2239,14 +2368,15 @@ export default function wardenExtension(pi) {
|
|
|
2239
2368
|
// Same activation gate as the first recommendation: selectedCapability is set before that gate, so without this a
|
|
2240
2369
|
// capability held for no_policy would be delivered here.
|
|
2241
2370
|
if (result.selected && result.selected.kind === selectedCapability.kind && result.selected.id === selectedCapability.id &&
|
|
2242
|
-
result.disposition !== "awaiting_user" && policyMatches(consciencePolicy, result.questionHash, judgeModel)) {
|
|
2371
|
+
result.disposition !== "awaiting_user" && policyMatches(consciencePolicy, result.questionHash, judgeModel) && adaptiveSend(ctx, config, "conscience")) {
|
|
2243
2372
|
reminderSent = true;
|
|
2244
2373
|
nudgesThisPrompt++;
|
|
2245
2374
|
spendBudgetUnit();
|
|
2246
2375
|
const msg = result.selected.kind === "skill"
|
|
2247
2376
|
? `Reminder: consider using the \"${result.selected.id}\" skill. ${result.selected.description}`
|
|
2248
2377
|
: `Reminder: consider using the \"${result.selected.id}\" tool. ${result.selected.description}`;
|
|
2249
|
-
steer(config, "conscience", msg, { deliverAs: "followUp" })
|
|
2378
|
+
if (steer(config, "conscience", msg, { deliverAs: "followUp" }))
|
|
2379
|
+
watchSteer(ctx, config, ["conscience"], { capability: { kind: result.selected.kind === "skill" ? "skill" : "tool", id: result.selected.id } });
|
|
2250
2380
|
}
|
|
2251
2381
|
}
|
|
2252
2382
|
catch (err) {
|
|
@@ -2285,7 +2415,8 @@ export default function wardenExtension(pi) {
|
|
|
2285
2415
|
if (nudge) {
|
|
2286
2416
|
if (ctx.hasUI && config.notices)
|
|
2287
2417
|
ctx.ui.notify(`warden · prose: ${due.join(", ")} in ${config.slop.prose.trend} of the last 3 replies (agent nudged for the next reply)`, "warning");
|
|
2288
|
-
steer(config, "prose", nudge, { deliverAs: "nextTurn" })
|
|
2418
|
+
if (adaptiveSend(ctx, config, "prose") && steer(config, "prose", nudge, { deliverAs: "nextTurn" }))
|
|
2419
|
+
watchSteer(ctx, config, ["prose"], {}, true);
|
|
2289
2420
|
}
|
|
2290
2421
|
}
|
|
2291
2422
|
if (!doneCheck)
|
|
@@ -2304,7 +2435,8 @@ export default function wardenExtension(pi) {
|
|
|
2304
2435
|
ctx.ui.notify(`warden · done-check: ${verdict.reasons.join("; ")}${nudge ? " (agent asked to verify)" : ""}`, "warning");
|
|
2305
2436
|
if (nudge) {
|
|
2306
2437
|
doneNudged = true;
|
|
2307
|
-
steer(config, "done", nudge, { deliverAs: "followUp", triggerTurn: true })
|
|
2438
|
+
if (steer(config, "done", nudge, { deliverAs: "followUp", triggerTurn: true }))
|
|
2439
|
+
watchSteer(ctx, config, ["done"]);
|
|
2308
2440
|
}
|
|
2309
2441
|
});
|
|
2310
2442
|
// Shortcuts are registered once at load; a shape check keeps a typo in the config file from being registered.
|
|
@@ -2337,10 +2469,11 @@ export default function wardenExtension(pi) {
|
|
|
2337
2469
|
const session = typeof manager.getSessionId === "function" ? manager.getSessionId() : "unknown";
|
|
2338
2470
|
const scan = await standingPrefs(ctx);
|
|
2339
2471
|
const path = prefsStorePath(scan.project);
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2472
|
+
const reply = await changePrefsStore(path, store => {
|
|
2473
|
+
const result = recordLesson({ lesson, session, now: Date.now(), signal, scan, store });
|
|
2474
|
+
return { value: result.reply, ...(result.store ? { store: result.store } : {}) };
|
|
2475
|
+
});
|
|
2476
|
+
return toolReply(reply);
|
|
2344
2477
|
},
|
|
2345
2478
|
});
|
|
2346
2479
|
pi.registerTool({
|
|
@@ -2360,10 +2493,8 @@ export default function wardenExtension(pi) {
|
|
|
2360
2493
|
const file = loopsFile(ctx);
|
|
2361
2494
|
if (!file)
|
|
2362
2495
|
return toolReply("not available: this session has no id to keep loops under");
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
await writeLoops(file, result.store);
|
|
2366
|
-
return toolReply(result.reply);
|
|
2496
|
+
const reply = await updateLoops(file, store => applyLoopAction(store, params, Date.now()));
|
|
2497
|
+
return toolReply(reply);
|
|
2367
2498
|
},
|
|
2368
2499
|
});
|
|
2369
2500
|
pi.registerTool({
|
|
@@ -2377,11 +2508,14 @@ export default function wardenExtension(pi) {
|
|
|
2377
2508
|
return toolReply(recallText(evidenceSnapshot(ctx)));
|
|
2378
2509
|
},
|
|
2379
2510
|
});
|
|
2380
|
-
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace", "init", "audit", "index", "prefs", "loops", "recommend"];
|
|
2511
|
+
const actions = ["status", "rules", "report", "enable", "disable", "mode", "config", "test", "trace", "init", "audit", "index", "prefs", "loops", "recommend", "unmute"];
|
|
2381
2512
|
pi.registerCommand("warden", {
|
|
2382
|
-
description: "pi-warden status, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, standing preferences, open loops, and a synthetic guard test",
|
|
2513
|
+
description: "pi-warden status, active rules, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, standing preferences, open loops, and a synthetic guard test",
|
|
2383
2514
|
getArgumentCompletions(prefix) {
|
|
2384
2515
|
const matches = actions.filter(action => action.startsWith(prefix)).map(action => ({ value: action, label: action }));
|
|
2516
|
+
// `rules check` is the one two-word action: `rules` on its own stays the local list.
|
|
2517
|
+
if ("rules check".startsWith(prefix))
|
|
2518
|
+
matches.push({ value: "rules check", label: "rules check", description: "which of the active rules the guard cannot judge well" });
|
|
2385
2519
|
return matches.length ? matches : null;
|
|
2386
2520
|
},
|
|
2387
2521
|
async handler(args, ctx) {
|
|
@@ -2412,6 +2546,7 @@ export default function wardenExtension(pi) {
|
|
|
2412
2546
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.typesafeBackend !== "typesafe" ? ` (${config.typesafeBackend})` : ""}; ${auth.text}`,
|
|
2413
2547
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.offPlan} off plan (${stats.offPlanTraceOnly} trace-only), ${stats.offTask} off task, ${stats.slop} slop notes, ${stats.ruleViolations}/${stats.ruleChecks} rule violations, ${stats.pathNotes} sensitive-path notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.runaway} runaway stops, ${stats.subagentWoken}/${stats.subagentReports} subagent reports woken, ${stats.restatements} restatements, ${stats.errors} TypeSafe errors, ${stats.cooldownSkips} checks without Jev during a judge cooldown; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}. Steer budget: ${config.steerBudget === 0 ? "off" : `${config.steerBudget} per run`}.`,
|
|
2414
2548
|
formatSteers(stats),
|
|
2549
|
+
`${formatMuted(steerStats.muted(), config.steers)}${stats.steersMuted ? ` This session: ${stats.steersMuted} steer${stats.steersMuted === 1 ? "" : "s"} kept in the trace only.` : ""}`,
|
|
2415
2550
|
`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, trace-only: ${config.action.intentTraceOnly}); 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}.`,
|
|
2416
2551
|
formatLedger(ledger.snapshot()),
|
|
2417
2552
|
...(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)`] : []),
|
|
@@ -2426,6 +2561,33 @@ export default function wardenExtension(pi) {
|
|
|
2426
2561
|
].join(" "));
|
|
2427
2562
|
return;
|
|
2428
2563
|
}
|
|
2564
|
+
if (action === "rules") {
|
|
2565
|
+
if (argument === "check") {
|
|
2566
|
+
const off = judgmentsOffReason(config);
|
|
2567
|
+
const result = await checkRules({ set: rulesGuard.store.load(ctx.cwd, config.rules), judge: judgeFor(config), timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
2568
|
+
if (result.source === "skipped") {
|
|
2569
|
+
// No key, no consent, or a spent budget: say why and send nothing.
|
|
2570
|
+
report(`Rules check sent nothing: Jev judgments are off${off === "budget" ? " (the request budget is used up)" : ""}.${off && off !== "budget" ? ` ${judgmentsOffText(off, config.typesafeBackend, !ctx.hasUI)}` : ""} /warden rules shows the rule set locally.`);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
report(formatRulesCheck(result));
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
report(rulesGuard.details(ctx.cwd, config.rules));
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
if (action === "report") {
|
|
2580
|
+
const match = /--days\s+(\d+)/.exec(tail);
|
|
2581
|
+
const days = match ? Number(match[1]) : REPORT_DEFAULT_DAYS;
|
|
2582
|
+
if (match && days < 1) {
|
|
2583
|
+
report("Usage: /warden report [--days N], where N is at least 1. Default: 30 days.", "warning");
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
const records = await readRulesLog(rulesLogPath(ctx.cwd));
|
|
2587
|
+
const set = rulesGuard.store.load(ctx.cwd, config.rules);
|
|
2588
|
+
report(formatRulesReport(buildRulesReport(records, { days, currentRules: set?.rules.map(rule => ({ id: rule.id, name: rule.name })) ?? [] })));
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2429
2591
|
if (action === "trace") {
|
|
2430
2592
|
if (!ctx.hasUI) {
|
|
2431
2593
|
report(traceText());
|
|
@@ -2441,6 +2603,17 @@ export default function wardenExtension(pi) {
|
|
|
2441
2603
|
void opened?.closed.then(fallback, fallback);
|
|
2442
2604
|
return;
|
|
2443
2605
|
}
|
|
2606
|
+
if (action === "unmute") {
|
|
2607
|
+
const kind = STEER_KINDS.find(name => name === argument);
|
|
2608
|
+
if (!kind) {
|
|
2609
|
+
report(`Usage: /warden unmute <kind> [model]. Kinds: ${STEER_KINDS.filter(name => !NEVER_MUTED.has(name)).join(", ")}.`, "warning");
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const models = tokens[2] ? [tokens[2]] : modelKey(ctx) ? [modelKey(ctx)] : steerStats.modelsWith(kind);
|
|
2613
|
+
const reset = models.filter(model => steerStats.unmute(model, kind));
|
|
2614
|
+
report(reset.length ? `Reset ${kind} for ${reset.join(", ")}: its steers are sent again and counted from zero.` : `No counts for ${kind} on ${models.join(", ") || "any model"}; nothing to reset.`);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2444
2617
|
if (action === "loops") {
|
|
2445
2618
|
const file = loopsFile(ctx);
|
|
2446
2619
|
report(file ? formatLoopsForUser(await readLoops(file)) : "No loops: this session has no id to keep them under.");
|
|
@@ -2459,7 +2632,7 @@ export default function wardenExtension(pi) {
|
|
|
2459
2632
|
report(`Usage: /warden prefs forget <n>, where n is an item number from /warden prefs (1 to ${items.length}).`, "warning");
|
|
2460
2633
|
return;
|
|
2461
2634
|
}
|
|
2462
|
-
await
|
|
2635
|
+
await changePrefsStore(path, store => ({ value: undefined, store: forgetPref(store, item) }));
|
|
2463
2636
|
report(`Forgotten for this project: "${item.text}". It is not listed or injected again.`);
|
|
2464
2637
|
return;
|
|
2465
2638
|
}
|