pi-warden 0.28.4 → 0.34.1

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +5 -1
  3. package/dist/audit.d.ts +22 -0
  4. package/dist/audit.js +110 -0
  5. package/dist/config.d.ts +10 -0
  6. package/dist/config.js +9 -0
  7. package/dist/conscience-config.d.ts +21 -0
  8. package/dist/conscience-config.js +82 -0
  9. package/dist/conscience-loader.d.ts +26 -0
  10. package/dist/conscience-loader.js +82 -0
  11. package/dist/conscience-policy.d.ts +24 -0
  12. package/dist/conscience-policy.js +28 -0
  13. package/dist/conscience-privacy.d.ts +4 -0
  14. package/dist/conscience-privacy.js +11 -0
  15. package/dist/discover.d.ts +14 -0
  16. package/dist/discover.js +85 -0
  17. package/dist/extension.d.ts +1 -1
  18. package/dist/extension.js +213 -35
  19. package/dist/extension.js.bak +213 -35
  20. package/dist/guard.d.ts +127 -1
  21. package/dist/guard.js +447 -31
  22. package/dist/holds.d.ts +2 -0
  23. package/dist/holds.js +7 -1
  24. package/dist/index.d.ts +9 -3
  25. package/dist/index.js +5 -2
  26. package/dist/init.d.ts +36 -0
  27. package/dist/init.js +199 -0
  28. package/dist/laya-download.d.ts +40 -0
  29. package/dist/laya-download.js +288 -0
  30. package/dist/laya-judge.d.ts +62 -0
  31. package/dist/laya-judge.js +495 -0
  32. package/dist/learning.d.ts +30 -0
  33. package/dist/learning.js +53 -10
  34. package/dist/redact.js +9 -4
  35. package/dist/rules-file.d.ts +18 -0
  36. package/dist/rules-file.js +72 -0
  37. package/dist/rules.d.ts +8 -0
  38. package/dist/rules.js +54 -3
  39. package/dist/shape.js +9 -1
  40. package/dist/tool-recommendation.d.ts +15 -0
  41. package/dist/tool-recommendation.js +31 -0
  42. package/dist/widget.d.ts +3 -1
  43. package/dist/widget.js +16 -13
  44. package/package.json +2 -2
@@ -0,0 +1,11 @@
1
+ import { redact } from "./redact.js";
2
+ /** URLs are not local paths; protect them before scanning free text for filesystem spans. */
3
+ export function safeConscienceText(text, disclosed = "") {
4
+ const urls = [];
5
+ const redacted = redact(text);
6
+ const masked = redacted.replace(/https?:\/\/[^\s<>"'`]+/gi, value => `\u0001${urls.push(value) - 1}\u0002`);
7
+ const clean = masked.replace(/(?:file:\/\/[^\s<>"'`]+|[A-Za-z]:[\\/][^\s<>"'`]+|\\\\[^\s<>"'`]+|~\/[^\s<>"'`]+|(?<![\w/:])\/(?:[^\s<>"'`]+))/g, path => disclosed.includes(path) ? path : "[local path]");
8
+ return clean.replace(/\u0001(\d+)\u0002/g, (_, index) => urls[Number(index)] ?? "[URL]");
9
+ }
10
+ export const safeCapabilityName = (name) => /^[A-Za-z0-9_.:\-]{1,128}$/.test(name) && redact(name) === name;
11
+ export const excludedCapability = (name, patterns) => patterns.some(pattern => new RegExp(`^${pattern.split("*").map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(name));
@@ -0,0 +1,14 @@
1
+ /** Directories to skip during source-file discovery. */
2
+ export declare const SKIP_DIRS: Set<string>;
3
+ export declare const SOURCE_RE: RegExp;
4
+ /**
5
+ * Recursively walk `dir` and collect source-file paths (relative to `dir`).
6
+ * Skips directories in SKIP_DIRS and stops after `max` files.
7
+ */
8
+ export declare function discoverSourceFiles(dir: string, max?: number): string[];
9
+ /**
10
+ * Find project directories inside a workspace. A project is any directory
11
+ * containing a known manifest file. The workspace root itself counts if it
12
+ * has one.
13
+ */
14
+ export declare function findProjects(cwd: string): string[];
@@ -0,0 +1,85 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { redact } from "./redact.js";
4
+ /** Directories to skip during source-file discovery. */
5
+ export const SKIP_DIRS = new Set([
6
+ ".git", "node_modules", "dist", "build", ".next", ".output",
7
+ "coverage", ".cache", "__pycache__", ".pi-warden", "vendor",
8
+ ".turbo", ".vercel", ".netlify", ".svelte-kit",
9
+ ]);
10
+ export const SOURCE_RE = /\.(ts|tsx|js|jsx|rs|py|go|vue|svelte|rb|php|java|kt|swift|cs|cpp|c|h)$/;
11
+ /**
12
+ * Recursively walk `dir` and collect source-file paths (relative to `dir`).
13
+ * Skips directories in SKIP_DIRS and stops after `max` files.
14
+ */
15
+ export function discoverSourceFiles(dir, max = 200) {
16
+ const files = [];
17
+ function walk(current) {
18
+ if (files.length >= max)
19
+ return;
20
+ let entries;
21
+ try {
22
+ entries = readdirSync(current, { withFileTypes: true });
23
+ }
24
+ catch (err) {
25
+ console.warn(`audit: could not list ${redact(current)}: ${err instanceof Error ? err.message : err}`);
26
+ return;
27
+ }
28
+ for (const entry of entries) {
29
+ if (files.length >= max)
30
+ return;
31
+ if (entry.name.startsWith(".") && entry.name !== ".")
32
+ continue;
33
+ const fullPath = join(current, entry.name);
34
+ if (entry.isDirectory()) {
35
+ if (SKIP_DIRS.has(entry.name))
36
+ continue;
37
+ walk(fullPath);
38
+ }
39
+ else if (entry.isFile() && SOURCE_RE.test(entry.name)) {
40
+ files.push(fullPath.slice(dir.length + 1)); // relative path
41
+ }
42
+ }
43
+ }
44
+ walk(dir);
45
+ return files;
46
+ }
47
+ /**
48
+ * Find project directories inside a workspace. A project is any directory
49
+ * containing a known manifest file. The workspace root itself counts if it
50
+ * has one.
51
+ */
52
+ export function findProjects(cwd) {
53
+ const projects = [];
54
+ const manifests = ["package.json", "Cargo.toml", "pyproject.toml", "go.mod"];
55
+ for (const m of manifests) {
56
+ if (existsSync(join(cwd, m))) {
57
+ projects.push(cwd);
58
+ break;
59
+ }
60
+ }
61
+ let entries;
62
+ try {
63
+ entries = readdirSync(cwd);
64
+ }
65
+ catch (err) {
66
+ console.warn(`audit: could not read workspace ${redact(cwd)}: ${err instanceof Error ? err.message : err}`);
67
+ return projects;
68
+ }
69
+ for (const entry of entries) {
70
+ if (entry === "node_modules" || entry.startsWith("."))
71
+ continue;
72
+ const sub = join(cwd, entry);
73
+ try {
74
+ if (!existsSync(join(sub, "package.json")) && !existsSync(join(sub, "Cargo.toml")) && !existsSync(join(sub, "pyproject.toml")) && !existsSync(join(sub, "go.mod")))
75
+ continue;
76
+ }
77
+ catch (err) {
78
+ console.warn(`audit: could not check ${redact(sub)}: ${err instanceof Error ? err.message : err}`);
79
+ continue;
80
+ }
81
+ if (!projects.includes(sub))
82
+ projects.push(sub);
83
+ }
84
+ return projects;
85
+ }
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import type { ToolCallRef } from "./action-guard.js";
3
3
  import type { SlopSymptom, Verdict } from "./guard.js";
4
4
  import type { ShapeResult } from "./shape.js";
5
- export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, 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.";
5
+ export declare const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs, 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.";
6
6
  /** Which guard spent the user's attention. The status line reports one count per guard. */
7
7
  export type SteerGuard = "action" | "rules" | "security" | "stuck" | "done" | "prose" | "runaway" | "subagent";
8
8
  /**
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,15 +11,18 @@ 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, formatVerdict, formatVerdictTokens, higher, inertPathRules, intentSteer, offTaskSteer, SLOP_LABELS, SteerRepeatWindow, steerReason, stripDataText, unknownExemptIds, writeSinkTargets } from "./guard.js";
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
28
  import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
@@ -26,8 +31,8 @@ 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, 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.";
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) {
@@ -226,6 +253,11 @@ export default function wardenExtension(pi) {
226
253
  let evidence = emptyEvidence();
227
254
  let doneNudged = false;
228
255
  let warnedFallback = false;
256
+ let warnedMissingRules = false;
257
+ /** True while /warden init is sending a prompt and waiting for the agent to generate pi-warden.md. */
258
+ let initRunning = false;
259
+ /** True while /warden audit is sending a prompt and waiting for the agent to write the report. */
260
+ let auditRunning = false;
229
261
  const prose = new ProseTrend();
230
262
  const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
231
263
  const ledger = new ContextLedger();
@@ -343,7 +375,20 @@ export default function wardenExtension(pi) {
343
375
  const color = LEVEL_COLOR[chip ?? ""] ?? "text";
344
376
  const chipText = chip ? `${theme.bold(theme.fg(color, chip.toUpperCase()))} ` : "";
345
377
  const guardText = theme.fg("muted", lastEntry.guard + " ");
346
- const body = { render: (width) => [chipText + guardText + sentence], invalidate: () => { } };
378
+ // Like widgetLines: the head width is tracked, the body wraps to what remains, and a line wider than
379
+ // the pane trips pi's render-width guard, which aborts the session.
380
+ const chipWidth = chip ? chip.length + 2 : 0;
381
+ const headWidth = chipWidth + lastEntry.guard.length + 1;
382
+ const head = chipText + guardText;
383
+ const body = {
384
+ render: (width) => {
385
+ if (!width)
386
+ return [head + sentence];
387
+ const wrapped = tuiModule.wrapTextWithAnsi(sentence, Math.max(10, width - headWidth));
388
+ return [head + (wrapped[0] ?? ""), ...wrapped.slice(1).map(rest => " ".repeat(headWidth) + rest)];
389
+ },
390
+ invalidate: () => { },
391
+ };
347
392
  return MouseRegion ? new MouseRegion(body, event => {
348
393
  if (event.type !== "click" || event.button !== "left")
349
394
  return undefined;
@@ -353,7 +398,7 @@ export default function wardenExtension(pi) {
353
398
  }, { placement: config.widget.placement });
354
399
  return;
355
400
  }
356
- entries = [entries.at(-1)];
401
+ entries = [lastEntry ?? entries.at(-1)];
357
402
  }
358
403
  // A custom component so the lines wrap to the pane and a click (fullscreen mode) opens the trace panel.
359
404
  // When the host TUI lacks MouseRegion (e.g. omp 18.2.5), the extension still loads — the widget
@@ -370,6 +415,7 @@ export default function wardenExtension(pi) {
370
415
  }, { placement: config.widget.placement });
371
416
  };
372
417
  const record = (ctx, config, guard, line, details, tokens) => {
418
+ widget.delete(guard);
373
419
  widget.set(guard, line);
374
420
  const entry = { at: Date.now(), guard, line, details, tokens };
375
421
  trace.push(entry);
@@ -382,6 +428,10 @@ export default function wardenExtension(pi) {
382
428
  const entry = traceOf.get(item);
383
429
  if (entry)
384
430
  trace.amend(entry, outcomeNote(item));
431
+ const idPromise = learningIds.get(item.id);
432
+ if (idPromise && item.outcome !== "pending") {
433
+ idPromise.then(id => recordOutcome(id, item.outcome)).catch(err => console.warn("pi-warden: recordOutcome failed:", err));
434
+ }
385
435
  }
386
436
  if (config.action.feedbackLog && holds.records().length)
387
437
  void holdLog?.save(holds.records());
@@ -484,6 +534,8 @@ export default function wardenExtension(pi) {
484
534
  client = undefined;
485
535
  budgetExhausted = false;
486
536
  warnedFallback = false;
537
+ warnedMissingRules = false;
538
+ initRunning = false;
487
539
  await initSchema(loadConfig().learning.retentionDays);
488
540
  stats = freshStats();
489
541
  widget.clear();
@@ -614,6 +666,17 @@ export default function wardenExtension(pi) {
614
666
  if (!config.action.enabled || !config.action.tools.includes(event.toolName))
615
667
  return;
616
668
  stats.inspected++;
669
+ // First-run warning: pi-warden.md missing, one time per session.
670
+ if (!warnedMissingRules && config.rules.enabled) {
671
+ const { missing, fallbackSource } = checkPiWardenMissing(ctx.cwd);
672
+ if (missing && ctx.hasUI) {
673
+ warnedMissingRules = true;
674
+ const msg = fallbackSource
675
+ ? `No pi-warden.md detected. Using ${fallbackSource} as active fallback rules. Run /warden init to create project-specific rules.`
676
+ : `No rules file detected (pi-warden.md, README.md, CLAUDE.md, or AGENTS.md). Run /warden init to create project-specific rules.`;
677
+ ctx.ui.notify(msg, "warning");
678
+ }
679
+ }
617
680
  // Arming: a write/edit to a protected path arms matching command patterns for a window. Bash redirect/tee
618
681
  // targets that match a when.edited glob also arm. This is session state, not a per-call verdict — it runs
619
682
  // before the action guard so the armed check on a later command sees the preparation.
@@ -683,6 +746,8 @@ export default function wardenExtension(pi) {
683
746
  if (verdict.source === "read-only")
684
747
  verdict.source = "pattern";
685
748
  }
749
+ const deliveryVerdict = agentDeliveryVerdict(verdict);
750
+ const deliveryReasons = deliveryVerdict.reasons;
686
751
  if (regretCandidates.length && verdict.judgment?.regretted !== undefined) {
687
752
  settleRegret(config, { regretted: regretsAt(verdict.judgment.regretted), target: verdict.judgment.regretTarget, probability: verdict.judgment.regretted, via: "jev" });
688
753
  }
@@ -704,12 +769,9 @@ export default function wardenExtension(pi) {
704
769
  const released = holds.approved(event.toolName);
705
770
  if (released) {
706
771
  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
772
  }
711
773
  }
712
- const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
774
+ const told = verdict.level === "confirm" && mode === "steer" ? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) : undefined;
713
775
  // SAFETY: verdict.source is a string union; the read-only → pattern rewrite above may have narrowed it in TS's view,
714
776
  // but the field is still one of the source values at runtime when no armed hits fired.
715
777
  const actionFmt = formatVerdictTokens(verdict, config.widget.action);
@@ -727,19 +789,17 @@ export default function wardenExtension(pi) {
727
789
  if (ctxSummary)
728
790
  recordOpts.contextSummary = ctxSummary;
729
791
  if (held)
730
- recordOpts.agentReason = steerReason(verdict, { canApprove: judge !== undefined });
792
+ recordOpts.agentReason = steerReason(deliveryVerdict, { canApprove: judge !== undefined });
731
793
  const item = holds.record(verdict, recordOpts);
732
794
  traceOf.set(item, entry);
795
+ // Record to SQLite for learning (held and judged-allowed calls).
796
+ // The promise and map entry must exist before noteOutcomes so that an outcome
797
+ // known at record time (e.g. dialog-approved) is not lost to the race.
798
+ const preview = redact(verdict.summary.command ?? verdict.summary.path ?? "");
799
+ 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; });
800
+ learningIds.set(item.id, holdPromise);
801
+ pruneLearningIds();
733
802
  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
803
  };
744
804
  // The warn notice below names the mismatch to the user; the agent gets the steer with the other notes.
745
805
  if (verdict.intentMismatch) {
@@ -749,8 +809,14 @@ export default function wardenExtension(pi) {
749
809
  }
750
810
  if (verdict.offTaskSteer) {
751
811
  stats.offTask++;
812
+ if (!verdict.offTaskTraceOnly) {
813
+ noteGuards.add("action");
814
+ notes.push(offTaskSteer(verdict));
815
+ }
816
+ }
817
+ if (verdict.shouldProceedSteer && !verdict.shouldProceedTraceOnly) {
752
818
  noteGuards.add("action");
753
- notes.push(offTaskSteer(verdict));
819
+ notes.push(shouldProceedMessage(verdict));
754
820
  }
755
821
  if (verdict.slopSymptoms?.length && verdict.slopReasons) {
756
822
  stats.slop++;
@@ -800,13 +866,15 @@ export default function wardenExtension(pi) {
800
866
  traceNote(delivered);
801
867
  pathNoteTraces.length = 0;
802
868
  }
803
- const warnSteer = (label) => steer(config, "action", `pi-warden: this ${event.toolName} call ran with a warning (${label}). 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.`);
869
+ 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
870
  if (verdict.level === "warn") {
805
871
  stats.warned++;
806
872
  if (ctx.hasUI && config.notices)
807
873
  ctx.ui.notify(`warden · ${event.toolName}: ${verdict.reasons.join("; ")}`, "warning");
808
- else if (!ctx.hasUI)
809
- warnSteer(verdict.reasons.join("; "));
874
+ else if (!ctx.hasUI) {
875
+ const otherReasons = deliveryReasons.filter(r => !r.startsWith("should-proceed "));
876
+ warnSteer(otherReasons);
877
+ }
810
878
  track(false);
811
879
  return undefined;
812
880
  }
@@ -816,13 +884,14 @@ export default function wardenExtension(pi) {
816
884
  if (ctx.hasUI && config.notices)
817
885
  ctx.ui.notify(`warden · blocked ${event.toolName}: ${verdict.reasons.join("; ")}`, "error");
818
886
  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 (${verdict.reasons.join("; ")}). A deny rule matched; this command is not allowed to run. Ask the user if this is genuinely required.` };
887
+ 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
888
  }
821
889
  if (verdict.level !== "confirm") {
822
890
  track(false);
823
891
  return undefined;
824
892
  }
825
893
  const reasons = verdict.reasons.join("; ");
894
+ const deliveredReasons = deliveryReasons.join("; ");
826
895
  // A user-defined confirm rule prompts the user in every mode, advise included: the operator wrote the rule to be
827
896
  // asked. Advise mode keeps Jev holds advisory; it does not soften a prompt the operator asked for by name.
828
897
  // The action defaults to dialog at parse time (the reason one writes such a rule); hold restores steer semantics.
@@ -836,14 +905,14 @@ export default function wardenExtension(pi) {
836
905
  }
837
906
  stats.held++;
838
907
  track(true, "declined", "dialog");
839
- return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${reasons}). Do not retry it unchanged; ask the user how to proceed.` };
908
+ 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
909
  }
841
910
  if (mode === "advise") {
842
911
  stats.warned++;
843
912
  if (ctx.hasUI && config.notices)
844
913
  ctx.ui.notify(`warden · ${event.toolName} (advise mode, not held): ${reasons}`, "warning");
845
914
  else if (!ctx.hasUI)
846
- warnSteer(reasons);
915
+ warnSteer(deliveryReasons);
847
916
  track(false);
848
917
  return undefined;
849
918
  }
@@ -856,7 +925,7 @@ export default function wardenExtension(pi) {
856
925
  }
857
926
  stats.held++;
858
927
  track(true, "declined", "dialog");
859
- return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${reasons}). Do not retry it unchanged; ask the user how to proceed.` };
928
+ 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
929
  }
861
930
  stats.held++;
862
931
  actionGuard.hold(task);
@@ -864,7 +933,7 @@ export default function wardenExtension(pi) {
864
933
  if (ctx.hasUI && config.notices)
865
934
  ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
866
935
  notifyDesktop(ctx, config, `Held ${event.toolName}: ${reasons}. The agent will re-plan or ask you in chat.`);
867
- return { block: true, reason: told ?? steerReason(verdict, { canApprove: judge !== undefined }) };
936
+ return { block: true, reason: told ?? steerReason(deliveryVerdict, { canApprove: judge !== undefined }) };
868
937
  });
869
938
  pi.on("tool_result", async (event, ctx) => {
870
939
  const config = configFor(ctx);
@@ -1086,6 +1155,17 @@ export default function wardenExtension(pi) {
1086
1155
  pi.on("agent_settled", async (_event, ctx) => {
1087
1156
  await checkSubagentReports(ctx, configFor(ctx));
1088
1157
  });
1158
+ // Block new user messages while /warden init or /warden audit is running.
1159
+ pi.on("input", async (event, ctx) => {
1160
+ if (initRunning) {
1161
+ ctx.ui.notify("pi-warden is generating rules. Please wait...", "warning");
1162
+ return { action: "handled" };
1163
+ }
1164
+ if (auditRunning) {
1165
+ ctx.ui.notify("pi-warden is running an audit. Please wait...", "warning");
1166
+ return { action: "handled" };
1167
+ }
1168
+ });
1089
1169
  pi.on("agent_end", async (event, ctx) => {
1090
1170
  const config = configFor(ctx);
1091
1171
  if (!config.enabled)
@@ -1182,7 +1262,7 @@ export default function wardenExtension(pi) {
1182
1262
  togglePanel(ctx.ui, configFor(ctx)); },
1183
1263
  });
1184
1264
  }
1185
- const actions = ["status", "enable", "disable", "mode", "config", "test", "trace"];
1265
+ const actions = ["status", "enable", "disable", "mode", "config", "test", "trace", "init", "audit"];
1186
1266
  pi.registerCommand("warden", {
1187
1267
  description: "pi-warden status, config (set/get/editor), TypeSafe consent, mode, trace panel, recommend, and a synthetic guard test",
1188
1268
  getArgumentCompletions(prefix) {
@@ -1204,6 +1284,10 @@ export default function wardenExtension(pi) {
1204
1284
  const source = consentSource(config);
1205
1285
  const usage = client?.getUsage();
1206
1286
  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(", ");
1287
+ const ls = await holdStats(ctx.cwd);
1288
+ const lifetimeLine = ls.labeled === 0
1289
+ ? `Lifetime here: ${ls.held} hold${ls.held === 1 ? "" : "s"}, not yet measurable (${ls.allowed} allowed).`
1290
+ : `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
1291
  report([
1208
1292
  `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
1293
  `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 +1296,7 @@ export default function wardenExtension(pi) {
1212
1296
  formatLedger(ledger.snapshot()),
1213
1297
  ...(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
1298
  `${formatHolds(holds.snapshot(), config.action.feedbackLog ? holdLog?.path : undefined)}${holdLog?.lastFailure ? ` Log write failed: ${holdLog.lastFailure}.` : ""}`,
1299
+ lifetimeLine,
1215
1300
  `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
1301
  ...(config.action.armingRules.length > 0 ? [`Arming: ${arming.statusLine() || "no rules armed"}.`] : []),
1217
1302
  `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 +1424,107 @@ export default function wardenExtension(pi) {
1339
1424
  openConfigPanel(lastUi, config, { width: config.widget.panelWidth });
1340
1425
  return;
1341
1426
  }
1427
+ if (action === "init") {
1428
+ const targetPath = join(ctx.cwd, "pi-warden.md");
1429
+ const force = argument === "--force";
1430
+ if (existsSync(targetPath)) {
1431
+ if (ctx.hasUI && !force) {
1432
+ 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.`)) {
1433
+ report("Cancelled. Existing pi-warden.md unchanged.");
1434
+ return;
1435
+ }
1436
+ }
1437
+ else if (!force) {
1438
+ report(`pi-warden.md already exists at ${targetPath}. Pass --force to overwrite.`, "warning");
1439
+ return;
1440
+ }
1441
+ }
1442
+ const prompt = buildInitPrompt(ctx.cwd);
1443
+ initRunning = true;
1444
+ if (ctx.hasUI)
1445
+ ctx.ui.notify("pi-warden: Generating rules file...", "info");
1446
+ try {
1447
+ // sendUserMessage throws when the agent is not idle. Brief wait so a
1448
+ // just-closing confirm dialog does not cause a race.
1449
+ for (let attempt = 0; attempt < 40; attempt++) {
1450
+ if (ctx.isIdle())
1451
+ break;
1452
+ await new Promise(resolve => setTimeout(resolve, 250));
1453
+ }
1454
+ pi.sendUserMessage(prompt);
1455
+ await ctx.waitForIdle();
1456
+ }
1457
+ catch (err) {
1458
+ const detail = err instanceof Error ? err.message : String(err);
1459
+ report(`pi-warden init failed: ${detail}. Try creating pi-warden.md manually.`, "error");
1460
+ }
1461
+ finally {
1462
+ initRunning = false;
1463
+ }
1464
+ const wardenExists = existsSync(join(ctx.cwd, "pi-warden.md"));
1465
+ 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.");
1466
+ return;
1467
+ }
1468
+ if (action === "audit") {
1469
+ if (!ctx.hasUI) {
1470
+ report("Audit needs an interactive session to run.", "warning");
1471
+ return;
1472
+ }
1473
+ 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.`))
1474
+ return;
1475
+ const projects = findProjects(ctx.cwd);
1476
+ const prompt = buildAuditPrompt(ctx.cwd, projects);
1477
+ const reportPath = join(ctx.cwd, ".pi-warden", "audit-report.html");
1478
+ const preSnapshot = snapshotReport(reportPath);
1479
+ auditRunning = true;
1480
+ if (ctx.hasUI)
1481
+ ctx.ui.notify("pi-warden: Running workspace audit...", "info");
1482
+ try {
1483
+ for (let attempt = 0; attempt < 40; attempt++) {
1484
+ if (ctx.isIdle())
1485
+ break;
1486
+ await new Promise(resolve => setTimeout(resolve, 250));
1487
+ }
1488
+ pi.sendUserMessage(prompt);
1489
+ await ctx.waitForIdle();
1490
+ }
1491
+ catch (err) {
1492
+ const detail = err instanceof Error ? err.message : String(err);
1493
+ report(`pi-warden audit failed: ${detail}.`, "error");
1494
+ }
1495
+ finally {
1496
+ auditRunning = false;
1497
+ }
1498
+ const outcome = reportOutcome(preSnapshot, snapshotReport(reportPath));
1499
+ switch (outcome) {
1500
+ case "written":
1501
+ report(`Audit report written to ${reportPath}`);
1502
+ break;
1503
+ case "stale":
1504
+ report("Audit finished but the report file was not updated — the agent may have reported findings in chat instead.");
1505
+ break;
1506
+ case "missing":
1507
+ report("Agent did not write an audit report. The model may have reported findings in chat instead.");
1508
+ break;
1509
+ }
1510
+ return;
1511
+ }
1342
1512
  if (action === "test") {
1343
1513
  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 "Clean up the demo directory") goes to ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
1514
+ 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
1515
  return;
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 });
1516
+ 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 });
1517
+ const deliveryVerdict = ctx.hasUI ? verdict : agentDeliveryVerdict(verdict);
1518
+ const deliveryReasons = deliveryVerdict.reasons;
1347
1519
  const fmt = formatVerdictTokens(verdict, config.widget.action);
1348
1520
  record(ctx, config, "action", fmt.line, actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }), fmt.tokens);
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}` : ""}`);
1521
+ const deliveryTokens = actionTokens(deliveryVerdict);
1522
+ if (!ctx.hasUI && verdict.offTaskTraceOnly) {
1523
+ // Redact only off-task presentation tokens; retain the judgment for other consumers.
1524
+ delete deliveryTokens.offTask;
1525
+ delete deliveryTokens.scope;
1526
+ }
1527
+ 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
1528
  if (verdict.level === "confirm") {
1351
1529
  const mode = activeMode(config, ctx.hasUI);
1352
1530
  if (mode === "confirm" && ctx.hasUI) {
@@ -1354,7 +1532,7 @@ export default function wardenExtension(pi) {
1354
1532
  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
1533
  }
1356
1534
  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(verdict, { canApprove: judge !== undefined })}"`);
1535
+ 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
1536
  }
1359
1537
  }
1360
1538
  return;