pi-warden 0.71.0 → 0.74.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/adaptive.d.ts +3 -2
  3. package/dist/adaptive.js +4 -3
  4. package/dist/backend.d.ts +33 -14
  5. package/dist/backend.js +27 -14
  6. package/dist/config.d.ts +13 -8
  7. package/dist/config.js +20 -20
  8. package/dist/conscience.d.ts +1 -1
  9. package/dist/extension.d.ts +14 -3
  10. package/dist/extension.js +410 -64
  11. package/dist/guard.d.ts +2 -1
  12. package/dist/guard.js +4 -4
  13. package/dist/holds.d.ts +1 -1
  14. package/dist/holds.js +3 -2
  15. package/dist/host-compat.d.ts +2 -0
  16. package/dist/host-compat.js +132 -0
  17. package/dist/host-dirs.d.ts +10 -0
  18. package/dist/host-dirs.js +21 -0
  19. package/dist/index-cmd.d.ts +6 -5
  20. package/dist/index-cmd.js +16 -24
  21. package/dist/index.d.ts +12 -4
  22. package/dist/index.js +6 -2
  23. package/dist/init.js +31 -3
  24. package/dist/learning.d.ts +12 -11
  25. package/dist/learning.js +60 -38
  26. package/dist/loops.d.ts +2 -1
  27. package/dist/loops.js +3 -2
  28. package/dist/panel.d.ts +4 -1
  29. package/dist/panel.js +11 -6
  30. package/dist/prefs.d.ts +1 -1
  31. package/dist/prefs.js +3 -2
  32. package/dist/rules-audit.d.ts +208 -0
  33. package/dist/rules-audit.js +417 -0
  34. package/dist/rules-calibrate.d.ts +196 -0
  35. package/dist/rules-calibrate.js +474 -0
  36. package/dist/rules-lint.d.ts +2 -0
  37. package/dist/rules-lint.js +7 -2
  38. package/dist/rules-log.d.ts +6 -1
  39. package/dist/rules-log.js +9 -3
  40. package/dist/rules-report.d.ts +3 -0
  41. package/dist/rules-report.js +5 -0
  42. package/dist/rules.d.ts +72 -5
  43. package/dist/rules.js +116 -54
  44. package/dist/shape.js +8 -2
  45. package/dist/turn-rules.d.ts +86 -0
  46. package/dist/turn-rules.js +232 -0
  47. package/dist/widget-render.d.ts +23 -0
  48. package/dist/widget-render.js +82 -0
  49. package/dist/widget.d.ts +5 -22
  50. package/dist/widget.js +3 -80
  51. package/package.json +5 -3
package/dist/learning.js CHANGED
@@ -2,10 +2,11 @@
2
2
  // Records full context with each hold and predicts outcomes using history.
3
3
  import { createHash } from "crypto";
4
4
  import { mkdirSync } from "fs";
5
- import { homedir } from "os";
6
5
  import { dirname, join } from "path";
6
+ import { defaultHostDirs } from "./host-dirs.js";
7
+ import { userConfigPath } from "./config.js";
7
8
  import { redact } from "./redact.js";
8
- let db;
9
+ const dbs = new Map();
9
10
  let sqliteAvailable;
10
11
  // --- Schema (shared constant) ---
11
12
  export const HOLDS_SCHEMA = `
@@ -38,31 +39,52 @@ const NOOP_DB = {
38
39
  prepare() { return { run() { return { changes: 0, lastInsertRowid: 0 }; }, get() { return undefined; }, all() { return []; } }; },
39
40
  pragma() { },
40
41
  };
41
- async function getDb() {
42
- if (db)
43
- return db;
44
- if (sqliteAvailable === false)
45
- return NOOP_DB;
42
+ /** Open one connection; a failure is remembered per path so later calls neither warn again nor retry forever. */
43
+ async function openDb(dbPath) {
44
+ let opened;
46
45
  try {
47
- const { DatabaseSync } = await import("node:sqlite");
46
+ // Static import cannot work: node:sqlite is flagged experimental and loads lazily so a missing
47
+ // or broken build of it disables learning features instead of failing the whole process.
48
+ const sqlite = await import("node:sqlite").catch(err => ({ importFailed: err }));
49
+ if ("importFailed" in sqlite) {
50
+ sqliteAvailable = false;
51
+ console.warn("pi-warden: node:sqlite unavailable, learning features disabled:", sqlite.importFailed);
52
+ return NOOP_DB;
53
+ }
54
+ const { DatabaseSync } = sqlite;
48
55
  sqliteAvailable = true;
49
- const dbPath = process.env.PI_WARDEN_DB ?? join(homedir(), ".pi", "agent", "pi-warden", "holds.db");
50
56
  // DatabaseSync does not create parent directories; on a fresh machine the folder may not exist yet.
51
57
  mkdirSync(dirname(dbPath), { recursive: true, mode: 0o700 });
52
- db = new DatabaseSync(dbPath);
53
- db.exec("PRAGMA journal_mode = WAL");
54
- db.exec("PRAGMA busy_timeout = 10000");
55
- return db;
58
+ opened = new DatabaseSync(dbPath);
59
+ opened.exec("PRAGMA journal_mode = WAL");
60
+ opened.exec("PRAGMA busy_timeout = 10000");
61
+ return opened;
56
62
  }
57
63
  catch (err) {
58
- sqliteAvailable = false;
59
- console.warn("pi-warden: node:sqlite unavailable, learning features disabled:", err);
64
+ // Import failures are handled above; this is a path open or PRAGMA failure for this path only.
65
+ // Close the half-opened handle and leave the resolved NOOP promise cached: one warning,
66
+ // and later calls neither reopen nor retry an unusable file.
67
+ try {
68
+ opened?.close();
69
+ }
70
+ catch { /* already closed */ }
71
+ console.warn(`pi-warden: could not open ${dbPath}:`, err);
60
72
  return NOOP_DB;
61
73
  }
62
74
  }
63
- export async function initSchema(retentionDays = 365) {
75
+ /** One connection per resolved path; concurrent first calls share one open. */
76
+ async function getDb(dirs = defaultHostDirs()) {
77
+ const dbPath = process.env.PI_WARDEN_DB ?? join(dirname(userConfigPath(dirs)), "holds.db");
78
+ if (sqliteAvailable === false)
79
+ return NOOP_DB;
80
+ let opening = dbs.get(dbPath);
81
+ if (!opening)
82
+ dbs.set(dbPath, opening = openDb(dbPath));
83
+ return opening;
84
+ }
85
+ export async function initSchema(retentionDays = 365, dirs = defaultHostDirs()) {
64
86
  try {
65
- const d = await getDb();
87
+ const d = await getDb(dirs);
66
88
  d.exec(HOLDS_SCHEMA);
67
89
  // Migrate: add columns that may be missing from older databases.
68
90
  try {
@@ -135,8 +157,8 @@ export function toHoldRecord(item, projectRoot, ctx) {
135
157
  return result;
136
158
  }
137
159
  // --- Recording ---
138
- export async function recordHold(hold) {
139
- const d = await getDb();
160
+ export async function recordHold(hold, dirs = defaultHostDirs()) {
161
+ const d = await getDb(dirs);
140
162
  const hash = signatureHash(hold.tool, hold.scores);
141
163
  const stmt = d.prepare(`
142
164
  INSERT INTO holds
@@ -148,9 +170,9 @@ export async function recordHold(hold) {
148
170
  const result = stmt.run(hold.timestamp, hold.projectRoot, hold.tool, hash, hold.commandPreview, hold.task ?? null, hold.plan ?? null, hold.contextSummary ?? null, hold.precedingActions ?? null, JSON.stringify(hold.scores), hold.level, hold.held ? 1 : 0, JSON.stringify(hold.reasons), hold.agentReason ?? null, hold.confidence ?? null);
149
171
  return Number(result.lastInsertRowid);
150
172
  }
151
- export async function recordOutcome(id, outcome) {
173
+ export async function recordOutcome(id, outcome, dirs = defaultHostDirs()) {
152
174
  try {
153
- (await getDb()).prepare("UPDATE holds SET outcome = ?, outcome_at = ? WHERE id = ?").run(outcome, Date.now(), id);
175
+ (await getDb(dirs)).prepare("UPDATE holds SET outcome = ?, outcome_at = ? WHERE id = ?").run(outcome, Date.now(), id);
154
176
  }
155
177
  catch (err) {
156
178
  console.warn("pi-warden: could not record hold outcome:", err);
@@ -158,15 +180,15 @@ export async function recordOutcome(id, outcome) {
158
180
  }
159
181
  // --- Querying ---
160
182
  /** Query holds by project and held value. Used by tests and future analytics. */
161
- export async function queryHoldsForProject(projectRoot, options) {
162
- const d = await getDb();
183
+ export async function queryHoldsForProject(projectRoot, options, dirs = defaultHostDirs()) {
184
+ const d = await getDb(dirs);
163
185
  if (options?.held !== undefined) {
164
186
  return d.prepare("SELECT id, tool, held, outcome, command_preview FROM holds WHERE project_root = ? AND held = ? ORDER BY timestamp").all(projectRoot, options.held ? 1 : 0);
165
187
  }
166
188
  return d.prepare("SELECT id, tool, held, outcome, command_preview FROM holds WHERE project_root = ? ORDER BY timestamp").all(projectRoot);
167
189
  }
168
- export async function holdStats(projectRoot) {
169
- const d = await getDb();
190
+ export async function holdStats(projectRoot, dirs = defaultHostDirs()) {
191
+ const d = await getDb(dirs);
170
192
  const held = d.prepare(`SELECT
171
193
  COUNT(*) AS total,
172
194
  COUNT(CASE WHEN outcome IN ('approved','declined','replanned') THEN 1 END) AS labeled,
@@ -194,8 +216,8 @@ export async function holdStats(projectRoot) {
194
216
  newest: held.newest ?? 0,
195
217
  };
196
218
  }
197
- export async function querySmartHistory(tool, scores, projectRoot) {
198
- const d = await getDb();
219
+ export async function querySmartHistory(tool, scores, projectRoot, dirs = defaultHostDirs()) {
220
+ const d = await getDb(dirs);
199
221
  const hash = signatureHash(tool, scores);
200
222
  const exact = d.prepare(`
201
223
  SELECT task, plan, outcome, scores, agent_reason, timestamp
@@ -236,8 +258,8 @@ export function calculateSmartConfidence(history) {
236
258
  };
237
259
  }
238
260
  // --- Integration ---
239
- export async function shouldSkipHold(tool, scores, projectRoot) {
240
- const history = await querySmartHistory(tool, scores, projectRoot);
261
+ export async function shouldSkipHold(tool, scores, projectRoot, dirs = defaultHostDirs()) {
262
+ const history = await querySmartHistory(tool, scores, projectRoot, dirs);
241
263
  const { confidence, reason } = calculateSmartConfidence(history);
242
264
  const isDestructive = scores.reasons.some(r => r.startsWith("destructive:"));
243
265
  if (isDestructive)
@@ -248,8 +270,8 @@ export async function shouldSkipHold(tool, scores, projectRoot) {
248
270
  return { skip: false, confidence, reason };
249
271
  }
250
272
  /** Analyze hold outcomes to suggest threshold adjustments. */
251
- export async function analyzeThresholds(projectRoot) {
252
- const d = await getDb();
273
+ export async function analyzeThresholds(projectRoot, dirs = defaultHostDirs()) {
274
+ const d = await getDb(dirs);
253
275
  const adjustments = [];
254
276
  // Analyze action guard: look at holds vs approvals
255
277
  const actionHolds = d.prepare(`
@@ -295,8 +317,8 @@ export async function analyzeThresholds(projectRoot) {
295
317
  }
296
318
  return adjustments;
297
319
  }
298
- export async function analyzePatterns(projectRoot) {
299
- const d = await getDb();
320
+ export async function analyzePatterns(projectRoot, dirs = defaultHostDirs()) {
321
+ const d = await getDb(dirs);
300
322
  const insights = [];
301
323
  // Get pattern outcomes
302
324
  const patterns = d.prepare(`
@@ -340,9 +362,9 @@ export async function analyzePatterns(projectRoot) {
340
362
  return insights.sort((a, b) => b.falsePositiveRate - a.falsePositiveRate);
341
363
  }
342
364
  /** Generate recommendations based on learning data. */
343
- export async function generateRecommendations(projectRoot) {
365
+ export async function generateRecommendations(projectRoot, dirs = defaultHostDirs()) {
344
366
  const recommendations = [];
345
- const thresholdAdjustments = await analyzeThresholds(projectRoot);
367
+ const thresholdAdjustments = await analyzeThresholds(projectRoot, dirs);
346
368
  for (const adj of thresholdAdjustments) {
347
369
  if (adj.confidence > 0.5) {
348
370
  recommendations.push({
@@ -352,7 +374,7 @@ export async function generateRecommendations(projectRoot) {
352
374
  });
353
375
  }
354
376
  }
355
- const patternInsights = await analyzePatterns(projectRoot);
377
+ const patternInsights = await analyzePatterns(projectRoot, dirs);
356
378
  for (const insight of patternInsights.slice(0, 3)) {
357
379
  if (insight.falsePositiveRate > 0.6) {
358
380
  recommendations.push({
@@ -368,8 +390,8 @@ export async function generateRecommendations(projectRoot) {
368
390
  });
369
391
  }
370
392
  /** Analyze steer effectiveness from hold outcomes. */
371
- export async function analyzeSteerEffectivenessReport(projectRoot) {
372
- const d = await getDb();
393
+ export async function analyzeSteerEffectivenessReport(projectRoot, dirs = defaultHostDirs()) {
394
+ const d = await getDb(dirs);
373
395
  const suggestions = [];
374
396
  // Get steer outcomes (inferred from hold outcomes)
375
397
  const steerOutcomes = d.prepare(`
package/dist/loops.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { HostDirs } from "./host-dirs.js";
1
2
  export declare const LOOP_CHARS = 160;
2
3
  /** Open loops shown in the appendix, the reminder, and the resume message. */
3
4
  export declare const LOOPS_SHOWN = 8;
@@ -25,7 +26,7 @@ export declare function emptyLoopStore(): LoopStore;
25
26
  * One file per session, in a folder per project: the working directory's hash keeps another project's session with the
26
27
  * same id apart, and the id keeps another session of this project apart.
27
28
  */
28
- export declare function loopsPath(cwd: string, session: string): string;
29
+ export declare function loopsPath(cwd: string, session: string, dirs?: HostDirs): string;
29
30
  /** A missing file is an empty store; an unreadable or malformed one throws, so a write never replaces loops it could not read. */
30
31
  export declare function readLoops(path: string): Promise<LoopStore>;
31
32
  /** Written whole to a temporary file and renamed, so a crash never leaves half a file. */
package/dist/loops.js CHANGED
@@ -10,6 +10,7 @@ import { readFile } from "node:fs/promises";
10
10
  import { dirname, join } from "node:path";
11
11
  import { serialize, writeFileAtomic } from "./atomic.js";
12
12
  import { userConfigPath } from "./config.js";
13
+ import { defaultHostDirs } from "./host-dirs.js";
13
14
  import { redact } from "./redact.js";
14
15
  export const LOOP_CHARS = 160;
15
16
  /** Open loops shown in the appendix, the reminder, and the resume message. */
@@ -25,9 +26,9 @@ export function emptyLoopStore() {
25
26
  * One file per session, in a folder per project: the working directory's hash keeps another project's session with the
26
27
  * same id apart, and the id keeps another session of this project apart.
27
28
  */
28
- export function loopsPath(cwd, session) {
29
+ export function loopsPath(cwd, session, dirs = defaultHostDirs()) {
29
30
  const project = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
30
- return join(dirname(userConfigPath()), "loops", project, `${session.replace(/[^\w.-]/g, "_")}.json`);
31
+ return join(dirname(userConfigPath(dirs)), "loops", project, `${session.replace(/[^\w.-]/g, "_")}.json`);
31
32
  }
32
33
  const isLoop = (value) => {
33
34
  const loop = value;
package/dist/panel.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Component, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
2
2
  import type { WardenConfig } from "./config.js";
3
+ import type { HostDirs } from "./host-dirs.js";
3
4
  import type { Trace } from "./trace.js";
4
5
  import type { ThemeLike } from "./widget.js";
5
6
  export interface PanelActions {
@@ -69,6 +70,7 @@ export declare class ConfigPanel implements Component {
69
70
  private readonly theme;
70
71
  private readonly actions;
71
72
  private readonly requestRender;
73
+ private readonly dirs;
72
74
  focused: boolean;
73
75
  private cursor;
74
76
  private scroll;
@@ -77,7 +79,7 @@ export declare class ConfigPanel implements Component {
77
79
  private editBuffer;
78
80
  private readonly edits;
79
81
  private readonly entries;
80
- constructor(config: WardenConfig, theme: ThemeLike, actions: PanelActions, requestRender: () => void);
82
+ constructor(config: WardenConfig, theme: ThemeLike, actions: PanelActions, requestRender: () => void, dirs?: HostDirs);
81
83
  invalidate(): void;
82
84
  private lines;
83
85
  render(width: number): string[];
@@ -89,4 +91,5 @@ export declare class ConfigPanel implements Component {
89
91
  }
90
92
  export declare function openConfigPanel(ui: PanelUi, config: WardenConfig, options?: {
91
93
  width?: string | number;
94
+ dirs?: HostDirs;
92
95
  }): PanelController;
package/dist/panel.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { applyUserOverrides, defaultConfig, readUserConfig, writeUserConfig } from "./config.js";
3
+ import { defaultHostDirs } from "./host-dirs.js";
3
4
  import { LEVEL_COLOR, parseVerdictLine, renderSegment } from "./widget.js";
4
5
  /**
5
6
  * Sidebar listing the trace newest-first, live-updating while open. It opens without taking keyboard input: the editor
@@ -169,7 +170,8 @@ export function openTracePanel(ui, trace, options = {}) {
169
170
  }, {
170
171
  overlay: true,
171
172
  overlayOptions: { anchor: "right-center", width: options.width ?? "40%", minWidth: 44, maxHeight: "100%", nonCapturing: true },
172
- onHandle: (handle) => { unfocus = () => handle.unfocus(); },
173
+ // A handle without `unfocus` belongs to a host whose overlays always capture input; close instead.
174
+ onHandle: (handle) => { unfocus = typeof handle.unfocus === "function" ? () => handle.unfocus() : () => close(); },
173
175
  });
174
176
  return { closed, close: () => close(), built: () => built };
175
177
  }
@@ -195,6 +197,7 @@ export class ConfigPanel {
195
197
  theme;
196
198
  actions;
197
199
  requestRender;
200
+ dirs;
198
201
  focused = false;
199
202
  cursor = 0;
200
203
  scroll = 0;
@@ -203,11 +206,12 @@ export class ConfigPanel {
203
206
  editBuffer = "";
204
207
  edits = new Map();
205
208
  entries;
206
- constructor(config, theme, actions, requestRender) {
209
+ constructor(config, theme, actions, requestRender, dirs = defaultHostDirs()) {
207
210
  this.config = config;
208
211
  this.theme = theme;
209
212
  this.actions = actions;
210
213
  this.requestRender = requestRender;
214
+ this.dirs = dirs;
211
215
  this.entries = flattenConfig(config);
212
216
  }
213
217
  invalidate() { }
@@ -369,7 +373,7 @@ export class ConfigPanel {
369
373
  save() {
370
374
  if (this.edits.size === 0)
371
375
  return;
372
- const raw = readUserConfig();
376
+ const raw = readUserConfig(this.dirs);
373
377
  const obj = (typeof raw === "object" && raw !== null && !Array.isArray(raw)) ? raw : {};
374
378
  for (const [path, value] of this.edits) {
375
379
  const keys = path.split(".");
@@ -382,7 +386,7 @@ export class ConfigPanel {
382
386
  }
383
387
  current[keys.at(-1)] = value;
384
388
  }
385
- writeUserConfig(obj);
389
+ writeUserConfig(obj, this.dirs);
386
390
  this.edits.clear();
387
391
  }
388
392
  }
@@ -391,7 +395,7 @@ export function openConfigPanel(ui, config, options = {}) {
391
395
  let unfocus = () => { };
392
396
  const closed = ui.custom((tui, theme, _keybindings, done) => {
393
397
  close = () => done();
394
- const panel = new ConfigPanel(config, theme, { close, unfocus: () => unfocus() }, () => tui.requestRender());
398
+ const panel = new ConfigPanel(config, theme, { close, unfocus: () => unfocus() }, () => tui.requestRender(), options.dirs);
395
399
  const rows = tui.terminal?.rows;
396
400
  if (typeof rows === "number")
397
401
  panel.setViewport(rows - 2);
@@ -399,7 +403,8 @@ export function openConfigPanel(ui, config, options = {}) {
399
403
  }, {
400
404
  overlay: true,
401
405
  overlayOptions: { anchor: "right-center", width: options.width ?? "40%", minWidth: 44, maxHeight: "100%", nonCapturing: true },
402
- onHandle: (handle) => { unfocus = () => handle.unfocus(); },
406
+ // A handle without `unfocus` belongs to a host whose overlays always capture input; close instead.
407
+ onHandle: (handle) => { unfocus = typeof handle.unfocus === "function" ? () => handle.unfocus() : () => close(); },
403
408
  });
404
409
  return { closed, close: () => close() };
405
410
  }
package/dist/prefs.d.ts CHANGED
@@ -184,7 +184,7 @@ export interface PrefsStore {
184
184
  forgotten: Forgotten[];
185
185
  }
186
186
  export declare function emptyPrefsStore(): PrefsStore;
187
- export declare function prefsStorePath(project: string): string;
187
+ export declare function prefsStorePath(project: string, dirs?: import("./host-dirs.js").HostDirs): string;
188
188
  /**
189
189
  * A missing file is an empty store. An unreadable or malformed one throws: without the forgotten list, a group the
190
190
  * user dropped could reach the agent again, so the caller injects nothing and says why.
package/dist/prefs.js CHANGED
@@ -14,6 +14,7 @@ import { open, readdir, readFile, stat } from "node:fs/promises";
14
14
  import { basename, dirname, isAbsolute, join, relative } from "node:path";
15
15
  import { serialize, writeFileAtomic } from "./atomic.js";
16
16
  import { userConfigPath } from "./config.js";
17
+ import { defaultHostDirs } from "./host-dirs.js";
17
18
  import { redact } from "./redact.js";
18
19
  /**
19
20
  * Short automated sessions fill a project's directory quickly: on real data the newest 50 sessions covered three to six
@@ -502,9 +503,9 @@ export async function scanPreferences(options) {
502
503
  export function emptyPrefsStore() {
503
504
  return { lessons: [], confirmations: [], forgotten: [] };
504
505
  }
505
- export function prefsStorePath(project) {
506
+ export function prefsStorePath(project, dirs = defaultHostDirs()) {
506
507
  const hash = createHash("sha256").update(project).digest("hex").slice(0, 12);
507
- return join(dirname(userConfigPath()), "prefs", `${hash}.json`);
508
+ return join(dirname(userConfigPath(dirs)), "prefs", `${hash}.json`);
508
509
  }
509
510
  const isRecord = (value) => typeof value === "object" && value !== null && typeof value.session === "string" && typeof value.at === "number";
510
511
  const isLesson = (value) => typeof value === "object" && value !== null && typeof value.text === "string" && Array.isArray(value.records) && value.records.every(isRecord);
@@ -0,0 +1,208 @@
1
+ import type { Judge } from "pi-typesafe";
2
+ import type { RulesConfig } from "./config.js";
3
+ import type { RuleSet, RulesVerdict } from "./rules.js";
4
+ /**
5
+ * `/warden rules audit` and `/warden bench`: two plain functions over the rules guard.
6
+ *
7
+ * The audit judges existing files against the project rules as if each had just been written, one `write` request per
8
+ * file. Nothing leaves the machine without the confirm dialog (or `--yes` in a headless run), the results never reach
9
+ * the rules log, and the only file written is the Markdown copy under `.pi-warden/`. The bench measures what one of
10
+ * those requests costs on this machine with a fixed built-in sample, so no project content is sent for it at all.
11
+ */
12
+ /** Files the audit judges unless `--max` says otherwise. */
13
+ export declare const AUDIT_DEFAULT_MAX = 50;
14
+ /** Requests in flight at once; small so a big audit cannot burst the backend. */
15
+ export declare const AUDIT_CONCURRENCY = 4;
16
+ /** Source files the discovery walk collects before it stops; the cap counts what it found. */
17
+ export declare const AUDIT_DISCOVERY_MAX = 5000;
18
+ export declare const AUDIT_REPORT_FILE = ".pi-warden/rules-audit.md";
19
+ export declare const RULES_AUDIT_USAGE = "Usage: /warden rules audit [paths...] [--max N] [--yes], where N is a positive integer (default 50) and paths default to the project.";
20
+ export declare const BENCH_USAGE = "Usage: /warden bench [--runs N], where N is a positive integer (default 10).";
21
+ export interface RulesAuditArgs {
22
+ /** Project-relative paths to audit; `["."]` when none were given. */
23
+ paths: string[];
24
+ max: number;
25
+ yes: boolean;
26
+ error?: string;
27
+ }
28
+ export declare function parseRulesAuditArgs(tokens: readonly string[]): RulesAuditArgs;
29
+ export interface BenchArgs {
30
+ runs: number;
31
+ error?: string;
32
+ }
33
+ export declare function parseBenchArgs(tokens: readonly string[]): BenchArgs;
34
+ export interface AuditFilePlan {
35
+ /** Project-relative paths to judge, in sorted order, capped at `max`. */
36
+ files: string[];
37
+ /** Files that passed every filter (git, `rules.exclude`, `rules.skip`, and at least one applicable rule). */
38
+ matched: number;
39
+ /** Matched files left out at the cap. */
40
+ leftOut: number;
41
+ /** Given paths that are not a file or directory inside the project. */
42
+ missing: string[];
43
+ }
44
+ /**
45
+ * The source files under `paths` the audit will judge: found by `discoverSourceFiles`, inside the project, not ignored
46
+ * by git, not kept out by `rules.exclude` or `rules.skip`, and with at least one rule that applies to them. Capped in
47
+ * sorted order so the same command judges the same files twice.
48
+ */
49
+ export declare function collectAuditFiles(cwd: string, paths: readonly string[], set: RuleSet | undefined, config: RulesConfig, max: number): AuditFilePlan;
50
+ export interface RulesAuditFileResult {
51
+ /** Project-relative path. */
52
+ path: string;
53
+ verdict: RulesVerdict;
54
+ }
55
+ export interface RulesAuditRow {
56
+ id: string;
57
+ name: string;
58
+ /** Files whose request asked about this rule. */
59
+ judged: number;
60
+ /** Files where the rule scored at or above its cutoff. */
61
+ flagged: number;
62
+ /** Mean P(violation) over the files judged. */
63
+ mean: number;
64
+ }
65
+ export interface RulesAuditOutcome {
66
+ paths: string[];
67
+ max: number;
68
+ /** Files selected for judgment. */
69
+ selected: number;
70
+ judged: number;
71
+ leftOut: number;
72
+ missing: string[];
73
+ /** Selected files that could not be read or were empty. */
74
+ unreadable: string[];
75
+ /** Files whose judgment came back as an error. */
76
+ errors: string[];
77
+ results: RulesAuditFileResult[];
78
+ rows: RulesAuditRow[];
79
+ /** Files with at least one finding. */
80
+ flagged: number;
81
+ sources: string[];
82
+ /** Distinct rules asked across the audit. */
83
+ rules: number;
84
+ aggregate: boolean;
85
+ reportPath: string;
86
+ elapsedMs: number;
87
+ }
88
+ export type RulesAuditResult = {
89
+ status: "no-judge";
90
+ } | {
91
+ status: "no-files";
92
+ reason: string;
93
+ } | {
94
+ status: "needs-yes";
95
+ files: number;
96
+ leftOut: number;
97
+ } | {
98
+ status: "cancelled";
99
+ } | {
100
+ status: "done";
101
+ outcome: RulesAuditOutcome;
102
+ };
103
+ export interface RulesAuditOptions {
104
+ cwd: string;
105
+ paths: readonly string[];
106
+ max: number;
107
+ /** Headless runs pass `--yes`; without it and without a dialog nothing is sent. */
108
+ yes: boolean;
109
+ config: RulesConfig;
110
+ set: RuleSet | undefined;
111
+ judge?: Judge | undefined;
112
+ timeoutMs: number;
113
+ signal?: AbortSignal | undefined;
114
+ /** Where the samples go, named in the confirm dialog. */
115
+ destination: string;
116
+ confirm?: ((title: string, body: string) => boolean | Promise<boolean>) | undefined;
117
+ }
118
+ /**
119
+ * One `write` request per selected file, at most AUDIT_CONCURRENCY at a time, after the confirm dialog (or `--yes`).
120
+ * With no judge nothing is sent and no dialog is shown. The Markdown copy is written to `AUDIT_REPORT_FILE`; the rules
121
+ * log is never touched.
122
+ */
123
+ export declare function runRulesAudit(options: RulesAuditOptions): Promise<RulesAuditResult>;
124
+ /** The per-rule table body, flagged rules first. */
125
+ export declare function formatRulesAuditRows(rows: readonly RulesAuditRow[]): string;
126
+ export interface RulesAuditFlag {
127
+ name: string;
128
+ score: number;
129
+ }
130
+ export interface RulesAuditFileFlags {
131
+ path: string;
132
+ findings: RulesAuditFlag[];
133
+ soft: RulesAuditFlag[];
134
+ }
135
+ /** Files with findings, worst first (most findings, then the strongest score). */
136
+ export declare function flaggedAuditFiles(results: readonly RulesAuditFileResult[]): RulesAuditFileFlags[];
137
+ /** One line per file, rule names with scores indented under the path. */
138
+ export declare function formatRulesAuditFiles(results: readonly RulesAuditFileResult[]): string;
139
+ /** The terminal output: summary, the table by rule, the file list, and where the Markdown copy went. */
140
+ export declare function formatRulesAudit(outcome: RulesAuditOutcome): string;
141
+ /** The Markdown copy of one audit run. */
142
+ export declare function rulesAuditMarkdown(outcome: RulesAuditOutcome, date?: Date): string;
143
+ /** Write the Markdown copy and return the path it went to. */
144
+ export declare function writeRulesAuditReport(cwd: string, markdown: string): string;
145
+ /** One row per rule asked anywhere in the audit: files judged, files flagged, mean score; flagged first. */
146
+ export declare function ruleAuditRows(results: readonly RulesAuditFileResult[]): RulesAuditRow[];
147
+ /**
148
+ * The one fixed sample the bench judges. It is built in, so no project content is sent for a bench run; only the
149
+ * active rule text travels with it.
150
+ */
151
+ export declare const BENCH_SAMPLE: {
152
+ path: string;
153
+ content: string;
154
+ };
155
+ /** The session counters a bench run diffs; what pi-typesafe's `getUsage()` returns. */
156
+ export interface BenchUsage {
157
+ requestsStarted: number;
158
+ inputTokens: number;
159
+ outputTokens: number;
160
+ estimatedUsd: number;
161
+ }
162
+ export interface BenchOptions {
163
+ runs: number;
164
+ cwd: string;
165
+ config: RulesConfig;
166
+ set: RuleSet | undefined;
167
+ judge?: Judge | undefined;
168
+ getUsage?: (() => BenchUsage) | undefined;
169
+ timeoutMs: number;
170
+ signal?: AbortSignal | undefined;
171
+ /** Clock for the latency measurements; injected by tests. */
172
+ now?: () => number;
173
+ }
174
+ export type BenchResult = {
175
+ status: "no-judge";
176
+ } | {
177
+ status: "skipped";
178
+ reason: string;
179
+ } | {
180
+ status: "done";
181
+ runs: number;
182
+ /** Rules asked per check. */
183
+ asked: number;
184
+ aggregate: boolean;
185
+ /** Checks with at least one finding. */
186
+ flagged: number;
187
+ /** Checks that came back as an error. */
188
+ errors: number;
189
+ /** Requests sent, from `getUsage()`; the checks themselves without it. */
190
+ requests: number;
191
+ p50: number;
192
+ p95: number;
193
+ meanInputTokens?: number;
194
+ costPerCheck?: number;
195
+ costPer100?: number;
196
+ };
197
+ /** Nearest-rank percentile of a sorted, non-empty list of numbers. */
198
+ export declare function percentile(sorted: readonly number[], p: number): number;
199
+ /**
200
+ * Judge the built-in sample `runs` times against the active rules, one request at a time so the latency numbers mean
201
+ * something, and report what the checks cost on this machine. With no judge nothing is sent; a sample the rules keep
202
+ * out (`rules.exclude`, `rules.skip`) is reported and never sent either.
203
+ */
204
+ export declare function runBench(options: BenchOptions): Promise<BenchResult>;
205
+ /** The bench output: the numbers, and the line that says no project content was sent. */
206
+ export declare function formatBench(result: Extract<BenchResult, {
207
+ status: "done";
208
+ }>): string;