pi-warden 0.28.4 → 0.35.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 +158 -1
- package/README.md +5 -1
- package/dist/audit.d.ts +22 -0
- package/dist/audit.js +110 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +12 -1
- package/dist/conscience-config.d.ts +21 -0
- package/dist/conscience-config.js +82 -0
- package/dist/conscience-loader.d.ts +26 -0
- package/dist/conscience-loader.js +82 -0
- package/dist/conscience-policy.d.ts +24 -0
- package/dist/conscience-policy.js +28 -0
- package/dist/conscience-privacy.d.ts +4 -0
- package/dist/conscience-privacy.js +11 -0
- package/dist/discover.d.ts +14 -0
- package/dist/discover.js +85 -0
- package/dist/extension.d.ts +1 -1
- package/dist/extension.js +261 -38
- package/dist/extension.js.bak +213 -35
- package/dist/guard.d.ts +127 -1
- package/dist/guard.js +447 -31
- package/dist/holds.d.ts +2 -0
- package/dist/holds.js +7 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +5 -2
- package/dist/init.d.ts +36 -0
- package/dist/init.js +199 -0
- package/dist/laya-download.d.ts +40 -0
- package/dist/laya-download.js +288 -0
- package/dist/laya-judge.d.ts +62 -0
- package/dist/laya-judge.js +495 -0
- package/dist/learning.d.ts +30 -0
- package/dist/learning.js +53 -10
- package/dist/redact.js +9 -4
- package/dist/rules-file.d.ts +18 -0
- package/dist/rules-file.js +72 -0
- package/dist/rules.d.ts +8 -0
- package/dist/rules.js +54 -3
- package/dist/shape.js +10 -2
- package/dist/stuck.d.ts +9 -0
- package/dist/stuck.js +101 -0
- package/dist/tool-recommendation.d.ts +15 -0
- package/dist/tool-recommendation.js +31 -0
- package/dist/widget.d.ts +3 -1
- package/dist/widget.js +16 -13
- package/package.json +2 -2
package/dist/extension.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import * as tuiModule from "@earendil-works/pi-tui";
|
|
3
5
|
const MouseRegion = tuiModule.MouseRegion;
|
|
4
6
|
import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
@@ -9,25 +11,28 @@ import { ArmingTracker, unparseableArmingRules } from "./arming.js";
|
|
|
9
11
|
import * as configModule from "./config.js";
|
|
10
12
|
import { applyUserOverrides, defaultConfig, getNestedValue, isMode, loadConfig, PACKAGE_NAME, parseConfigValue, projectConfigPath, readUserConfig, setNestedValue, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
11
13
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome as recordDoneOutcome } from "./done.js";
|
|
12
|
-
import { evaluateAction,
|
|
14
|
+
import { evaluateAction, formatVerdictTokens, higher, inertPathRules, intentSteer, offTaskSteer, shouldProceedMessage, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, writeSinkTargets } from "./guard.js";
|
|
13
15
|
import { commandOf } from "./tools.js";
|
|
14
16
|
import { formatHolds, HoldLedger, HoldLog, holdLogPath, outcomeNote, regretsAt, textRegrets } from "./holds.js";
|
|
15
|
-
import { initSchema, recordHold, recordOutcome, toHoldRecord, generateRecommendations, analyzeSteerEffectivenessReport } from "./learning.js";
|
|
17
|
+
import { initSchema, recordHold, recordOutcome, toHoldRecord, holdStats, generateRecommendations, analyzeSteerEffectivenessReport } from "./learning.js";
|
|
16
18
|
import { evaluateProse, proseNudge, ProseTrend, RESTATE_MIN_SENTENCES, RESTATE_SHARE, RestatementWindow, substantiveSentences } from "./prose.js";
|
|
17
19
|
import { compressOutput, duplicateNote, evaluateOutput, mergeOutput, outputKey, saveOutput, securityNotice, CompressionLearner } from "./output.js";
|
|
18
20
|
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
19
21
|
import { redact } from "./redact.js";
|
|
20
22
|
import { formatRules, pathNoteSteer, RulesGuard, rulesSteer } from "./rules.js";
|
|
23
|
+
import { checkPiWardenMissing } from "./rules-file.js";
|
|
24
|
+
import { writeStarterRules, buildInitPrompt } from "./init.js";
|
|
25
|
+
import { buildAuditPrompt, findProjects, snapshotReport, reportOutcome } from "./audit.js";
|
|
21
26
|
import { detectNotifier, sendNotification } from "./notify.js";
|
|
22
27
|
import { formatRunaway, RunawayMonitor, runawayNudge } from "./runaway.js";
|
|
23
|
-
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
28
|
+
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckDiff, stuckNudge } from "./stuck.js";
|
|
24
29
|
import { openConfigPanel, openTracePanel } from "./panel.js";
|
|
25
30
|
import { completeConfig, shapeWarning } from "./shape.js";
|
|
26
31
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
27
32
|
import { formatWake, newReports, reportLabel, triageReport, WakePolicy } from "./subagent.js";
|
|
28
33
|
import { actionDetails, doneDetails, proseDetails, rulesDetails, runawayDetails, stuckDetails, Trace } from "./trace.js";
|
|
29
|
-
import { DEFAULT_TEMPLATES, LEVEL_COLOR, pickSentenceTemplate, proseTokens, renderTemplate, SENTENCE_TEMPLATES, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
30
|
-
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary,
|
|
34
|
+
import { actionTokens, DEFAULT_TEMPLATES, LEVEL_COLOR, pickSentenceTemplate, proseTokens, renderTemplate, SENTENCE_TEMPLATES, statusWidget, TOKEN_NAMES } from "./widget.js";
|
|
35
|
+
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, with the agent's own words from the message that makes the call (its stated plan); the resolved active rules file content (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback, token-aware truncated at ~4000 tokens) sent with every action request, including calls where the rules guard is disabled; for a write or edit in a project with a rules file (pi-warden.md, the configured files, or README/CLAUDE/AGENTS as fallback), a larger redacted sample of the written content with the current file around each edit and the rule text; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; redacted tool-output samples for security and context saving (retention and output format); a redacted sample of an async subagent report that names a failure, a stop, or a question, with your latest request, when warden decides whether that report should wake the agent; and, on the first guarded call after your reply, the redacted summaries of the calls allowed in the previous turn, so Jev can say whether your reply regrets one of them. Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine; the hold feedback log stores tool names, pattern ids, scores, and outcomes (never commands) in an owner-only file under Pi's agent directory; an owner-only SQLite database under Pi's agent directory stores redacted hold context (plan, summary, redacted command preview, outcomes) for held and judged-allowed calls, for learning and retention (configurable, default 365 days). Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
31
36
|
const WIDGET = PACKAGE_NAME;
|
|
32
37
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
33
38
|
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, offPlan: 0, offTask: 0, slop: 0, ruleChecks: 0, ruleViolations: 0, pathNotes: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, runaway: 0, errors: 0, steers: 0, steersSkipped: 0, steerGuards: {}, subagentReports: 0, subagentWoken: 0, restatements: 0 });
|
|
@@ -126,6 +131,28 @@ function activeMode(config, hasUI) {
|
|
|
126
131
|
const mode = isMode(env) ? env : config.mode;
|
|
127
132
|
return mode === "confirm" && !hasUI ? "steer" : mode;
|
|
128
133
|
}
|
|
134
|
+
/** Keep diagnostics intact while removing structured trace-only items at the agent boundary. */
|
|
135
|
+
function agentDeliveryReasons(verdict) {
|
|
136
|
+
const indexes = [
|
|
137
|
+
verdict.offTaskTraceOnly ? verdict.offTaskTraceOnlyReasonIndex : undefined,
|
|
138
|
+
verdict.shouldProceedTraceOnly ? verdict.shouldProceedTraceOnlyReasonIndex : undefined,
|
|
139
|
+
].filter((index) => index !== undefined && index >= 0 && index < verdict.reasons.length);
|
|
140
|
+
if (!indexes.length)
|
|
141
|
+
return verdict.reasons;
|
|
142
|
+
return verdict.reasons.filter((_reason, reasonIndex) => !indexes.includes(reasonIndex));
|
|
143
|
+
}
|
|
144
|
+
/** Keep the full judgment while suppressing trace-only reasons and steer flags. */
|
|
145
|
+
function agentDeliveryVerdict(verdict) {
|
|
146
|
+
const reasons = agentDeliveryReasons(verdict);
|
|
147
|
+
if (reasons === verdict.reasons)
|
|
148
|
+
return verdict;
|
|
149
|
+
const deliveryVerdict = { ...verdict, reasons };
|
|
150
|
+
if (verdict.offTaskTraceOnly)
|
|
151
|
+
delete deliveryVerdict.offTaskSteer;
|
|
152
|
+
if (verdict.shouldProceedTraceOnly)
|
|
153
|
+
delete deliveryVerdict.shouldProceedSteer;
|
|
154
|
+
return deliveryVerdict;
|
|
155
|
+
}
|
|
129
156
|
function clip(text) {
|
|
130
157
|
return text.length <= CONFIRM_TEXT_LIMIT ? text : `${text.slice(0, CONFIRM_TEXT_LIMIT)}…`;
|
|
131
158
|
}
|
|
@@ -202,7 +229,7 @@ export default function wardenExtension(pi) {
|
|
|
202
229
|
const rulesGuard = new RulesGuard();
|
|
203
230
|
// Hold feedback: what the user did after each judged call, the trace entry each label lands on, and the per-session log.
|
|
204
231
|
const holds = new HoldLedger();
|
|
205
|
-
const learningIds = new Map(); // holds.id -> learning.id
|
|
232
|
+
const learningIds = new Map(); // holds.id -> promise of learning.id
|
|
206
233
|
// Prune learningIds when it grows large to prevent memory leaks
|
|
207
234
|
function pruneLearningIds() {
|
|
208
235
|
if (learningIds.size > 1000) {
|
|
@@ -223,9 +250,16 @@ export default function wardenExtension(pi) {
|
|
|
223
250
|
// Allowed calls of the turn the user just replied to; the regret question about them rides the next action request.
|
|
224
251
|
let regretCandidates = [];
|
|
225
252
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
253
|
+
/** Full output text and saved path keyed by attempt call key, for stuck-loop diffs. */
|
|
254
|
+
let fullOutputs = new Map();
|
|
226
255
|
let evidence = emptyEvidence();
|
|
227
256
|
let doneNudged = false;
|
|
228
257
|
let warnedFallback = false;
|
|
258
|
+
let warnedMissingRules = false;
|
|
259
|
+
/** True while /warden init is sending a prompt and waiting for the agent to generate pi-warden.md. */
|
|
260
|
+
let initRunning = false;
|
|
261
|
+
/** True while /warden audit is sending a prompt and waiting for the agent to write the report. */
|
|
262
|
+
let auditRunning = false;
|
|
229
263
|
const prose = new ProseTrend();
|
|
230
264
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
231
265
|
const ledger = new ContextLedger();
|
|
@@ -343,7 +377,20 @@ export default function wardenExtension(pi) {
|
|
|
343
377
|
const color = LEVEL_COLOR[chip ?? ""] ?? "text";
|
|
344
378
|
const chipText = chip ? `${theme.bold(theme.fg(color, chip.toUpperCase()))} ` : "";
|
|
345
379
|
const guardText = theme.fg("muted", lastEntry.guard + " ");
|
|
346
|
-
|
|
380
|
+
// Like widgetLines: the head width is tracked, the body wraps to what remains, and a line wider than
|
|
381
|
+
// the pane trips pi's render-width guard, which aborts the session.
|
|
382
|
+
const chipWidth = chip ? chip.length + 2 : 0;
|
|
383
|
+
const headWidth = chipWidth + lastEntry.guard.length + 1;
|
|
384
|
+
const head = chipText + guardText;
|
|
385
|
+
const body = {
|
|
386
|
+
render: (width) => {
|
|
387
|
+
if (!width)
|
|
388
|
+
return [head + sentence];
|
|
389
|
+
const wrapped = tuiModule.wrapTextWithAnsi(sentence, Math.max(10, width - headWidth));
|
|
390
|
+
return [head + (wrapped[0] ?? ""), ...wrapped.slice(1).map(rest => " ".repeat(headWidth) + rest)];
|
|
391
|
+
},
|
|
392
|
+
invalidate: () => { },
|
|
393
|
+
};
|
|
347
394
|
return MouseRegion ? new MouseRegion(body, event => {
|
|
348
395
|
if (event.type !== "click" || event.button !== "left")
|
|
349
396
|
return undefined;
|
|
@@ -353,7 +400,7 @@ export default function wardenExtension(pi) {
|
|
|
353
400
|
}, { placement: config.widget.placement });
|
|
354
401
|
return;
|
|
355
402
|
}
|
|
356
|
-
entries = [entries.at(-1)];
|
|
403
|
+
entries = [lastEntry ?? entries.at(-1)];
|
|
357
404
|
}
|
|
358
405
|
// A custom component so the lines wrap to the pane and a click (fullscreen mode) opens the trace panel.
|
|
359
406
|
// When the host TUI lacks MouseRegion (e.g. omp 18.2.5), the extension still loads — the widget
|
|
@@ -370,6 +417,7 @@ export default function wardenExtension(pi) {
|
|
|
370
417
|
}, { placement: config.widget.placement });
|
|
371
418
|
};
|
|
372
419
|
const record = (ctx, config, guard, line, details, tokens) => {
|
|
420
|
+
widget.delete(guard);
|
|
373
421
|
widget.set(guard, line);
|
|
374
422
|
const entry = { at: Date.now(), guard, line, details, tokens };
|
|
375
423
|
trace.push(entry);
|
|
@@ -382,6 +430,10 @@ export default function wardenExtension(pi) {
|
|
|
382
430
|
const entry = traceOf.get(item);
|
|
383
431
|
if (entry)
|
|
384
432
|
trace.amend(entry, outcomeNote(item));
|
|
433
|
+
const idPromise = learningIds.get(item.id);
|
|
434
|
+
if (idPromise && item.outcome !== "pending") {
|
|
435
|
+
idPromise.then(id => recordOutcome(id, item.outcome)).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
436
|
+
}
|
|
385
437
|
}
|
|
386
438
|
if (config.action.feedbackLog && holds.records().length)
|
|
387
439
|
void holdLog?.save(holds.records());
|
|
@@ -484,6 +536,8 @@ export default function wardenExtension(pi) {
|
|
|
484
536
|
client = undefined;
|
|
485
537
|
budgetExhausted = false;
|
|
486
538
|
warnedFallback = false;
|
|
539
|
+
warnedMissingRules = false;
|
|
540
|
+
initRunning = false;
|
|
487
541
|
await initSchema(loadConfig().learning.retentionDays);
|
|
488
542
|
stats = freshStats();
|
|
489
543
|
widget.clear();
|
|
@@ -543,6 +597,7 @@ export default function wardenExtension(pi) {
|
|
|
543
597
|
lastUi = ctx.ui;
|
|
544
598
|
const config = configFor(ctx);
|
|
545
599
|
attempts = new AttemptWindow(config.stuck.window);
|
|
600
|
+
fullOutputs = new Map();
|
|
546
601
|
doneNudged = false;
|
|
547
602
|
steersThisRun = 0;
|
|
548
603
|
finals.reset();
|
|
@@ -614,6 +669,17 @@ export default function wardenExtension(pi) {
|
|
|
614
669
|
if (!config.action.enabled || !config.action.tools.includes(event.toolName))
|
|
615
670
|
return;
|
|
616
671
|
stats.inspected++;
|
|
672
|
+
// First-run warning: pi-warden.md missing, one time per session.
|
|
673
|
+
if (!warnedMissingRules && config.rules.enabled) {
|
|
674
|
+
const { missing, fallbackSource } = checkPiWardenMissing(ctx.cwd);
|
|
675
|
+
if (missing && ctx.hasUI) {
|
|
676
|
+
warnedMissingRules = true;
|
|
677
|
+
const msg = fallbackSource
|
|
678
|
+
? `No pi-warden.md detected. Using ${fallbackSource} as active fallback rules. Run /warden init to create project-specific rules.`
|
|
679
|
+
: `No rules file detected (pi-warden.md, README.md, CLAUDE.md, or AGENTS.md). Run /warden init to create project-specific rules.`;
|
|
680
|
+
ctx.ui.notify(msg, "warning");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
617
683
|
// Arming: a write/edit to a protected path arms matching command patterns for a window. Bash redirect/tee
|
|
618
684
|
// targets that match a when.edited glob also arm. This is session state, not a per-call verdict — it runs
|
|
619
685
|
// before the action guard so the armed check on a later command sees the preparation.
|
|
@@ -683,6 +749,8 @@ export default function wardenExtension(pi) {
|
|
|
683
749
|
if (verdict.source === "read-only")
|
|
684
750
|
verdict.source = "pattern";
|
|
685
751
|
}
|
|
752
|
+
const deliveryVerdict = agentDeliveryVerdict(verdict);
|
|
753
|
+
const deliveryReasons = deliveryVerdict.reasons;
|
|
686
754
|
if (regretCandidates.length && verdict.judgment?.regretted !== undefined) {
|
|
687
755
|
settleRegret(config, { regretted: regretsAt(verdict.judgment.regretted), target: verdict.judgment.regretTarget, probability: verdict.judgment.regretted, via: "jev" });
|
|
688
756
|
}
|
|
@@ -704,12 +772,9 @@ export default function wardenExtension(pi) {
|
|
|
704
772
|
const released = holds.approved(event.toolName);
|
|
705
773
|
if (released) {
|
|
706
774
|
noteOutcomes(config, [released]);
|
|
707
|
-
const learningId = learningIds.get(released.id);
|
|
708
|
-
if (learningId)
|
|
709
|
-
recordOutcome(learningId, "approved").catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
710
775
|
}
|
|
711
776
|
}
|
|
712
|
-
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(
|
|
777
|
+
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) : undefined;
|
|
713
778
|
// SAFETY: verdict.source is a string union; the read-only → pattern rewrite above may have narrowed it in TS's view,
|
|
714
779
|
// but the field is still one of the source values at runtime when no armed hits fired.
|
|
715
780
|
const actionFmt = formatVerdictTokens(verdict, config.widget.action);
|
|
@@ -727,19 +792,17 @@ export default function wardenExtension(pi) {
|
|
|
727
792
|
if (ctxSummary)
|
|
728
793
|
recordOpts.contextSummary = ctxSummary;
|
|
729
794
|
if (held)
|
|
730
|
-
recordOpts.agentReason = steerReason(
|
|
795
|
+
recordOpts.agentReason = steerReason(deliveryVerdict, { canApprove: judge !== undefined });
|
|
731
796
|
const item = holds.record(verdict, recordOpts);
|
|
732
797
|
traceOf.set(item, entry);
|
|
798
|
+
// Record to SQLite for learning (held and judged-allowed calls).
|
|
799
|
+
// The promise and map entry must exist before noteOutcomes so that an outcome
|
|
800
|
+
// known at record time (e.g. dialog-approved) is not lost to the race.
|
|
801
|
+
const preview = redact(verdict.summary.command ?? verdict.summary.path ?? "");
|
|
802
|
+
const holdPromise = recordHold(toHoldRecord({ at: item.at, tool: item.tool, level: item.level, reasons: item.reasons, scores: item.scores, held }, ctx.cwd, { task: task ? redact(task) : task, plan: verdict.plan, contextSummary: ctxSummary, agentReason: held ? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) : undefined, preview })).catch(err => { console.warn("pi-warden: recordHold failed:", err); return -1; });
|
|
803
|
+
learningIds.set(item.id, holdPromise);
|
|
804
|
+
pruneLearningIds();
|
|
733
805
|
noteOutcomes(config, outcome ? [item] : []);
|
|
734
|
-
// Record to SQLite for learning
|
|
735
|
-
if (held) {
|
|
736
|
-
recordHold(toHoldRecord({ at: item.at, tool: item.tool, level: item.level, reasons: item.reasons, scores: item.scores }, ctx.cwd, { task: task ? redact(task) : task, plan: verdict.plan, contextSummary: ctxSummary, agentReason: steerReason(verdict, { canApprove: judge !== undefined }) })).then(id => {
|
|
737
|
-
learningIds.set(item.id, id);
|
|
738
|
-
if (outcome)
|
|
739
|
-
recordOutcome(id, outcome).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
|
|
740
|
-
}).catch(err => console.warn("pi-warden: recordHold failed:", err));
|
|
741
|
-
pruneLearningIds();
|
|
742
|
-
}
|
|
743
806
|
};
|
|
744
807
|
// The warn notice below names the mismatch to the user; the agent gets the steer with the other notes.
|
|
745
808
|
if (verdict.intentMismatch) {
|
|
@@ -749,8 +812,14 @@ export default function wardenExtension(pi) {
|
|
|
749
812
|
}
|
|
750
813
|
if (verdict.offTaskSteer) {
|
|
751
814
|
stats.offTask++;
|
|
815
|
+
if (!verdict.offTaskTraceOnly) {
|
|
816
|
+
noteGuards.add("action");
|
|
817
|
+
notes.push(offTaskSteer(verdict));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (verdict.shouldProceedSteer && !verdict.shouldProceedTraceOnly) {
|
|
752
821
|
noteGuards.add("action");
|
|
753
|
-
notes.push(
|
|
822
|
+
notes.push(shouldProceedMessage(verdict));
|
|
754
823
|
}
|
|
755
824
|
if (verdict.slopSymptoms?.length && verdict.slopReasons) {
|
|
756
825
|
stats.slop++;
|
|
@@ -800,13 +869,15 @@ export default function wardenExtension(pi) {
|
|
|
800
869
|
traceNote(delivered);
|
|
801
870
|
pathNoteTraces.length = 0;
|
|
802
871
|
}
|
|
803
|
-
const warnSteer = (
|
|
872
|
+
const warnSteer = (reasons) => reasons.length > 0 && steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${reasons.join("; ")}). Nobody sees this in a headless run, so it is on you: if the flagged risk is expected, continue; otherwise fix it or ask the user before building on it.`);
|
|
804
873
|
if (verdict.level === "warn") {
|
|
805
874
|
stats.warned++;
|
|
806
875
|
if (ctx.hasUI && config.notices)
|
|
807
876
|
ctx.ui.notify(`warden · ${event.toolName}: ${verdict.reasons.join("; ")}`, "warning");
|
|
808
|
-
else if (!ctx.hasUI)
|
|
809
|
-
|
|
877
|
+
else if (!ctx.hasUI) {
|
|
878
|
+
const otherReasons = deliveryReasons.filter(r => !r.startsWith("should-proceed "));
|
|
879
|
+
warnSteer(otherReasons);
|
|
880
|
+
}
|
|
810
881
|
track(false);
|
|
811
882
|
return undefined;
|
|
812
883
|
}
|
|
@@ -816,13 +887,14 @@ export default function wardenExtension(pi) {
|
|
|
816
887
|
if (ctx.hasUI && config.notices)
|
|
817
888
|
ctx.ui.notify(`warden · blocked ${event.toolName}: ${verdict.reasons.join("; ")}`, "error");
|
|
818
889
|
notifyDesktop(ctx, config, `Blocked ${event.toolName}: ${verdict.reasons.join("; ")}. A deny rule matched; the call never ran.`);
|
|
819
|
-
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${
|
|
890
|
+
return { block: true, reason: `pi-warden blocked this ${event.toolName} call (${deliveryReasons.join("; ")}). A deny rule matched; this command is not allowed to run. Ask the user if this is genuinely required.` };
|
|
820
891
|
}
|
|
821
892
|
if (verdict.level !== "confirm") {
|
|
822
893
|
track(false);
|
|
823
894
|
return undefined;
|
|
824
895
|
}
|
|
825
896
|
const reasons = verdict.reasons.join("; ");
|
|
897
|
+
const deliveredReasons = deliveryReasons.join("; ");
|
|
826
898
|
// A user-defined confirm rule prompts the user in every mode, advise included: the operator wrote the rule to be
|
|
827
899
|
// asked. Advise mode keeps Jev holds advisory; it does not soften a prompt the operator asked for by name.
|
|
828
900
|
// The action defaults to dialog at parse time (the reason one writes such a rule); hold restores steer semantics.
|
|
@@ -836,14 +908,14 @@ export default function wardenExtension(pi) {
|
|
|
836
908
|
}
|
|
837
909
|
stats.held++;
|
|
838
910
|
track(true, "declined", "dialog");
|
|
839
|
-
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${
|
|
911
|
+
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${deliveredReasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
840
912
|
}
|
|
841
913
|
if (mode === "advise") {
|
|
842
914
|
stats.warned++;
|
|
843
915
|
if (ctx.hasUI && config.notices)
|
|
844
916
|
ctx.ui.notify(`warden · ${event.toolName} (advise mode, not held): ${reasons}`, "warning");
|
|
845
917
|
else if (!ctx.hasUI)
|
|
846
|
-
warnSteer(
|
|
918
|
+
warnSteer(deliveryReasons);
|
|
847
919
|
track(false);
|
|
848
920
|
return undefined;
|
|
849
921
|
}
|
|
@@ -856,7 +928,7 @@ export default function wardenExtension(pi) {
|
|
|
856
928
|
}
|
|
857
929
|
stats.held++;
|
|
858
930
|
track(true, "declined", "dialog");
|
|
859
|
-
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${
|
|
931
|
+
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${deliveredReasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
860
932
|
}
|
|
861
933
|
stats.held++;
|
|
862
934
|
actionGuard.hold(task);
|
|
@@ -864,7 +936,7 @@ export default function wardenExtension(pi) {
|
|
|
864
936
|
if (ctx.hasUI && config.notices)
|
|
865
937
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
866
938
|
notifyDesktop(ctx, config, `Held ${event.toolName}: ${reasons}. The agent will re-plan or ask you in chat.`);
|
|
867
|
-
return { block: true, reason: told ?? steerReason(
|
|
939
|
+
return { block: true, reason: told ?? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) };
|
|
868
940
|
});
|
|
869
941
|
pi.on("tool_result", async (event, ctx) => {
|
|
870
942
|
const config = configFor(ctx);
|
|
@@ -1060,11 +1132,53 @@ export default function wardenExtension(pi) {
|
|
|
1060
1132
|
`output sample: ${redact(text).slice(0, 300)}`, delivered ? `agent told: ${notice}` : `steer recorded, not delivered (a repeat or the per-run budget): ${notice}`,
|
|
1061
1133
|
]);
|
|
1062
1134
|
}
|
|
1135
|
+
// Stuck-loop diff: when the verdict is stuck on a failed result and a previous failed output
|
|
1136
|
+
// for the same call is available, replace the tool result with a short diff note. Compute
|
|
1137
|
+
// bytesSaved against the current content (after duplicate detection and compression), so the
|
|
1138
|
+
// diff never grows the result.
|
|
1139
|
+
const verdict = await stuckCheck;
|
|
1140
|
+
if (verdict && verdict.stuck && failed && !verdict.successRepeat && !verdict.churn && text.length > 0) {
|
|
1141
|
+
const currentKey = attempts.attempts.at(-1)?.key;
|
|
1142
|
+
const prevEntry = currentKey ? fullOutputs.get(currentKey) : undefined;
|
|
1143
|
+
if (currentKey && prevEntry && prevEntry.text !== text) {
|
|
1144
|
+
// Differing outputs: save the current output and diff against the previous one.
|
|
1145
|
+
try {
|
|
1146
|
+
const saved = await saveOutput(text);
|
|
1147
|
+
const diffNote = stuckDiff(prevEntry.text, text, { diffLimit: config.stuck.diffLimit, tailLimit: config.stuck.tailLimit, fullPath: saved });
|
|
1148
|
+
const currentLen = Buffer.byteLength(content.map(part => part.type === "text" ? part.text : "").join("\n"));
|
|
1149
|
+
if (Buffer.byteLength(diffNote) < currentLen) {
|
|
1150
|
+
content = [{ type: "text", text: diffNote }, ...content.filter(part => part.type !== "text")];
|
|
1151
|
+
}
|
|
1152
|
+
fullOutputs.set(currentKey, { text, path: saved });
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
noteError(ctx, "Could not store full output for stuck diff; keeping it unchanged.", undefined);
|
|
1156
|
+
fullOutputs.set(currentKey, { text });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
else if (currentKey && prevEntry && prevEntry.text === text && prevEntry.path) {
|
|
1160
|
+
// Byte-identical outputs: one-line note reusing the existing saved path.
|
|
1161
|
+
const diffNote = stuckDiff(text, text, { diffLimit: config.stuck.diffLimit, tailLimit: config.stuck.tailLimit, fullPath: prevEntry.path });
|
|
1162
|
+
const currentLen = Buffer.byteLength(content.map(part => part.type === "text" ? part.text : "").join("\n"));
|
|
1163
|
+
if (Buffer.byteLength(diffNote) < currentLen) {
|
|
1164
|
+
content = [{ type: "text", text: diffNote }, ...content.filter(part => part.type !== "text")];
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
else if (currentKey) {
|
|
1168
|
+
// First failed output for this call, or no previous entry yet: store for future diffs.
|
|
1169
|
+
fullOutputs.set(currentKey, { text });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else if (failed && text.length > 0) {
|
|
1173
|
+
// Not stuck (or no verdict yet): store the output so a future repeat can diff against it.
|
|
1174
|
+
const currentKey = attempts.attempts.at(-1)?.key;
|
|
1175
|
+
if (currentKey)
|
|
1176
|
+
fullOutputs.set(currentKey, { text });
|
|
1177
|
+
}
|
|
1063
1178
|
const patch = content === event.content ? undefined : { content };
|
|
1064
1179
|
// Checks use the original result, not the excerpts or security banner.
|
|
1065
1180
|
if (config.done.enabled)
|
|
1066
1181
|
recordDoneOutcome(evidence, classifyToolResult(event.toolName, event.input, failed, text), event.input, event.toolName);
|
|
1067
|
-
const verdict = await stuckCheck;
|
|
1068
1182
|
if (!verdict)
|
|
1069
1183
|
return patch;
|
|
1070
1184
|
if (verdict.error)
|
|
@@ -1077,7 +1191,7 @@ export default function wardenExtension(pi) {
|
|
|
1077
1191
|
return patch;
|
|
1078
1192
|
stats.stuck++;
|
|
1079
1193
|
if (ctx.hasUI && config.notices)
|
|
1080
|
-
ctx.ui.notify(`warden
|
|
1194
|
+
ctx.ui.notify(`warden \u00b7 stuck: ${verdict.reasons.join("; ")}${nudge ? " (agent nudged)" : ""}`, "warning");
|
|
1081
1195
|
if (nudge)
|
|
1082
1196
|
steer(config, "stuck", nudge);
|
|
1083
1197
|
return patch;
|
|
@@ -1086,6 +1200,17 @@ export default function wardenExtension(pi) {
|
|
|
1086
1200
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
1087
1201
|
await checkSubagentReports(ctx, configFor(ctx));
|
|
1088
1202
|
});
|
|
1203
|
+
// Block new user messages while /warden init or /warden audit is running.
|
|
1204
|
+
pi.on("input", async (event, ctx) => {
|
|
1205
|
+
if (initRunning) {
|
|
1206
|
+
ctx.ui.notify("pi-warden is generating rules. Please wait...", "warning");
|
|
1207
|
+
return { action: "handled" };
|
|
1208
|
+
}
|
|
1209
|
+
if (auditRunning) {
|
|
1210
|
+
ctx.ui.notify("pi-warden is running an audit. Please wait...", "warning");
|
|
1211
|
+
return { action: "handled" };
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1089
1214
|
pi.on("agent_end", async (event, ctx) => {
|
|
1090
1215
|
const config = configFor(ctx);
|
|
1091
1216
|
if (!config.enabled)
|
|
@@ -1182,7 +1307,7 @@ export default function wardenExtension(pi) {
|
|
|
1182
1307
|
togglePanel(ctx.ui, configFor(ctx)); },
|
|
1183
1308
|
});
|
|
1184
1309
|
}
|
|
1185
|
-
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace"];
|
|
1310
|
+
const actions = ["status", "enable", "disable", "mode", "config", "test", "trace", "init", "audit"];
|
|
1186
1311
|
pi.registerCommand("warden", {
|
|
1187
1312
|
description: "pi-warden status, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, and a synthetic guard test",
|
|
1188
1313
|
getArgumentCompletions(prefix) {
|
|
@@ -1204,6 +1329,10 @@ export default function wardenExtension(pi) {
|
|
|
1204
1329
|
const source = consentSource(config);
|
|
1205
1330
|
const usage = client?.getUsage();
|
|
1206
1331
|
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.rules.enabled && "rules", config.context.enabled && "context", config.runaway.enabled && "runaway", config.subagent.enabled && "subagent triage", config.notify.enabled && "desktop notifications"].filter(Boolean).join(", ");
|
|
1332
|
+
const ls = await holdStats(ctx.cwd);
|
|
1333
|
+
const lifetimeLine = ls.labeled === 0
|
|
1334
|
+
? `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, not yet measurable (${ls.allowed} allowed).`
|
|
1335
|
+
: `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, ${ls.labeled} labeled, ${ls.declined + ls.replanned} stood (${ls.declined + ls.replanned}/${ls.labeled}), ${ls.allowed} allowed (${ls.accepted} accepted, ${ls.regretted} regretted).`;
|
|
1207
1336
|
report([
|
|
1208
1337
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.typesafeBackend !== "typesafe" ? ` (${config.typesafeBackend})` : ""}; ${auth.text}`,
|
|
1209
1338
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.offPlan} off plan, ${stats.offTask} off task, ${stats.slop} slop notes, ${stats.ruleViolations}/${stats.ruleChecks} rule violations, ${stats.pathNotes} sensitive-path notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.runaway} runaway stops, ${stats.subagentWoken}/${stats.subagentReports} subagent reports woken, ${stats.restatements} restatements, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}. Steer budget: ${config.steerBudget === 0 ? "off" : `${config.steerBudget} per run`}.`,
|
|
@@ -1212,6 +1341,7 @@ export default function wardenExtension(pi) {
|
|
|
1212
1341
|
formatLedger(ledger.snapshot()),
|
|
1213
1342
|
...(config.learning.patternAnalysis ? [`Learning: ${(await generateRecommendations(ctx.cwd)).length} recommendations, steer effectiveness ${Math.round((await analyzeSteerEffectivenessReport(ctx.cwd)).overall * 100)}% (use /warden recommend for details)`] : []),
|
|
1214
1343
|
`${formatHolds(holds.snapshot(), config.action.feedbackLog ? holdLog?.path : undefined)}${holdLog?.lastFailure ? ` Log write failed: ${holdLog.lastFailure}.` : ""}`,
|
|
1344
|
+
lifetimeLine,
|
|
1215
1345
|
`Rules: ${config.rules.enabled ? `${rulesGuard.describe(ctx.cwd, config.rules)}${Object.keys(config.rules.sensitivePaths).length ? `; ${Object.keys(config.rules.sensitivePaths).length} sensitive path${Object.keys(config.rules.sensitivePaths).length === 1 ? "" : "s"}` : ""}` : "off"}.`,
|
|
1216
1346
|
...(config.action.armingRules.length > 0 ? [`Arming: ${arming.statusLine() || "no rules armed"}.`] : []),
|
|
1217
1347
|
`Desktop notifications: ${config.notify.enabled ? `on (${config.notify.command.length ? `command ${config.notify.command[0]}` : (await (notifier ??= detectNotifier())) ?? "no notifier found on this machine"}; cooldown ${config.notify.cooldownMs} ms)` : "off (\"notify\": { \"enabled\": true } in the config turns them on)"}.`,
|
|
@@ -1339,14 +1469,107 @@ export default function wardenExtension(pi) {
|
|
|
1339
1469
|
openConfigPanel(lastUi, config, { width: config.widget.panelWidth });
|
|
1340
1470
|
return;
|
|
1341
1471
|
}
|
|
1472
|
+
if (action === "init") {
|
|
1473
|
+
const targetPath = join(ctx.cwd, "pi-warden.md");
|
|
1474
|
+
const force = argument === "--force";
|
|
1475
|
+
if (existsSync(targetPath)) {
|
|
1476
|
+
if (ctx.hasUI && !force) {
|
|
1477
|
+
if (!await ctx.ui.confirm("pi-warden.md already exists. Overwrite with a fresh starter template?", `This replaces ${targetPath} with a generic starter. Your current rules will be lost.`)) {
|
|
1478
|
+
report("Cancelled. Existing pi-warden.md unchanged.");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
else if (!force) {
|
|
1483
|
+
report(`pi-warden.md already exists at ${targetPath}. Pass --force to overwrite.`, "warning");
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const prompt = buildInitPrompt(ctx.cwd);
|
|
1488
|
+
initRunning = true;
|
|
1489
|
+
if (ctx.hasUI)
|
|
1490
|
+
ctx.ui.notify("pi-warden: Generating rules file...", "info");
|
|
1491
|
+
try {
|
|
1492
|
+
// sendUserMessage throws when the agent is not idle. Brief wait so a
|
|
1493
|
+
// just-closing confirm dialog does not cause a race.
|
|
1494
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1495
|
+
if (ctx.isIdle())
|
|
1496
|
+
break;
|
|
1497
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1498
|
+
}
|
|
1499
|
+
pi.sendUserMessage(prompt);
|
|
1500
|
+
await ctx.waitForIdle();
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1504
|
+
report(`pi-warden init failed: ${detail}. Try creating pi-warden.md manually.`, "error");
|
|
1505
|
+
}
|
|
1506
|
+
finally {
|
|
1507
|
+
initRunning = false;
|
|
1508
|
+
}
|
|
1509
|
+
const wardenExists = existsSync(join(ctx.cwd, "pi-warden.md"));
|
|
1510
|
+
report(wardenExists ? "pi-warden.md created. Review the rules and edit as needed." : "Agent did not create pi-warden.md. Create it manually or try /warden init again.");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (action === "audit") {
|
|
1514
|
+
if (!ctx.hasUI) {
|
|
1515
|
+
report("Audit needs an interactive session to run.", "warning");
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (!await ctx.ui.confirm("Run workspace audit?", `This runs an agent-driven audit of ${redact(ctx.cwd)}. It uses the session model, reads source code, and writes a report. It may take several minutes and use real tokens. Measured comparisons need the agent's TypeSafe tool, which is enabled per session with \`/typesafe enable\`; without it findings will be marked unmeasured.`))
|
|
1519
|
+
return;
|
|
1520
|
+
const projects = findProjects(ctx.cwd);
|
|
1521
|
+
const prompt = buildAuditPrompt(ctx.cwd, projects);
|
|
1522
|
+
const reportPath = join(ctx.cwd, ".pi-warden", "audit-report.html");
|
|
1523
|
+
const preSnapshot = snapshotReport(reportPath);
|
|
1524
|
+
auditRunning = true;
|
|
1525
|
+
if (ctx.hasUI)
|
|
1526
|
+
ctx.ui.notify("pi-warden: Running workspace audit...", "info");
|
|
1527
|
+
try {
|
|
1528
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
1529
|
+
if (ctx.isIdle())
|
|
1530
|
+
break;
|
|
1531
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1532
|
+
}
|
|
1533
|
+
pi.sendUserMessage(prompt);
|
|
1534
|
+
await ctx.waitForIdle();
|
|
1535
|
+
}
|
|
1536
|
+
catch (err) {
|
|
1537
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1538
|
+
report(`pi-warden audit failed: ${detail}.`, "error");
|
|
1539
|
+
}
|
|
1540
|
+
finally {
|
|
1541
|
+
auditRunning = false;
|
|
1542
|
+
}
|
|
1543
|
+
const outcome = reportOutcome(preSnapshot, snapshotReport(reportPath));
|
|
1544
|
+
switch (outcome) {
|
|
1545
|
+
case "written":
|
|
1546
|
+
report(`Audit report written to ${reportPath}`);
|
|
1547
|
+
break;
|
|
1548
|
+
case "stale":
|
|
1549
|
+
report("Audit finished but the report file was not updated — the agent may have reported findings in chat instead.");
|
|
1550
|
+
break;
|
|
1551
|
+
case "missing":
|
|
1552
|
+
report("Agent did not write an audit report. The model may have reported findings in chat instead.");
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1342
1557
|
if (action === "test") {
|
|
1343
1558
|
const judge = judgeFor(config);
|
|
1344
|
-
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "
|
|
1559
|
+
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Prepare the demo environment") goes to ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
|
|
1345
1560
|
return;
|
|
1346
|
-
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "
|
|
1561
|
+
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Prepare the demo environment" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge });
|
|
1562
|
+
const deliveryVerdict = ctx.hasUI ? verdict : agentDeliveryVerdict(verdict);
|
|
1563
|
+
const deliveryReasons = deliveryVerdict.reasons;
|
|
1347
1564
|
const fmt = formatVerdictTokens(verdict, config.widget.action);
|
|
1348
1565
|
record(ctx, config, "action", fmt.line, actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }), fmt.tokens);
|
|
1349
|
-
|
|
1566
|
+
const deliveryTokens = actionTokens(deliveryVerdict);
|
|
1567
|
+
if (!ctx.hasUI && verdict.offTaskTraceOnly) {
|
|
1568
|
+
// Redact only off-task presentation tokens; retain the judgment for other consumers.
|
|
1569
|
+
delete deliveryTokens.offTask;
|
|
1570
|
+
delete deliveryTokens.scope;
|
|
1571
|
+
}
|
|
1572
|
+
report(`${renderTemplate(DEFAULT_TEMPLATES.action, deliveryTokens)}${deliveryReasons.length ? ` — ${deliveryReasons.join("; ")}` : ""}${judge ? "" : " (pattern checks only: TypeSafe judgments are not enabled or no key is configured)"}${verdict.error ? ` — ${verdict.error}` : ""}`);
|
|
1350
1573
|
if (verdict.level === "confirm") {
|
|
1351
1574
|
const mode = activeMode(config, ctx.hasUI);
|
|
1352
1575
|
if (mode === "confirm" && ctx.hasUI) {
|
|
@@ -1354,7 +1577,7 @@ export default function wardenExtension(pi) {
|
|
|
1354
1577
|
report(allowed ? "Demo: you chose Yes, so a real call would have run." : "Demo: you chose No, so a real call would have been blocked and the agent told why.");
|
|
1355
1578
|
}
|
|
1356
1579
|
else {
|
|
1357
|
-
report(`In ${mode} mode a real call would ${mode === "advise" ? "run with this warning shown to you" : "be held and the agent would read"}: "${steerReason(
|
|
1580
|
+
report(`In ${mode} mode a real call would ${mode === "advise" ? "run with this warning shown to you" : "be held and the agent would read"}: "${steerReason(deliveryVerdict, { canApprove: judge !== undefined })}"`);
|
|
1358
1581
|
}
|
|
1359
1582
|
}
|
|
1360
1583
|
return;
|