05fbc6019e790ed9f6937ac6bc1d9465624c842e
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.prettierrc b/.prettierrc
2new file mode 100644
3index 0000000000000000000000000000000000000000..b48917cda44fac62bbd9838c9b91cf0f7cd1abea
4--- /dev/null
5+++ b/.prettierrc
6@@ -0,0 +1,7 @@
7+{
8+ "useTabs": false,
9+ "singleQuote": false,
10+ "trailingComma": "all",
11+ "printWidth": 120,
12+ "plugins": []
13+}
14diff --git a/AGENTS.md b/AGENTS.md
15index f3f312530f39d8b8b594ae48461a768a3624dcaa..cd996c6ae85fb84f51ad12e31ce196619ea40e9f 100644
16--- a/AGENTS.md
17+++ b/AGENTS.md
18@@ -41,6 +41,7 @@ When changing deterministic patterns or matching behavior:
19
20 ```sh
21 bun test
22+bun prettier -w .
23 bunx eslint src test
24 bunx tsc --noEmit
25 ```
26diff --git a/README.md b/README.md
27index 3f4f9b2f631e412146fb4f8aeae87730f9af58bf..55c26ecae1b4958cb000ad86fb3d3f8ff7774d54 100644
28--- a/README.md
29+++ b/README.md
30@@ -32,13 +32,13 @@ Use a Pi-configured model. This uses Pi's provider transport and authentication,
31
32 ```json
33 {
34- "reviewer": {
35- "kind": "pi",
36- "provider": "openai-codex",
37- "model": "gpt-5.6-luna",
38- "reasoningEffort": "minimal",
39- "promptCacheKey": "policy-engine-v1"
40- }
41+ "reviewer": {
42+ "kind": "pi",
43+ "provider": "openai-codex",
44+ "model": "gpt-5.6-luna",
45+ "reasoningEffort": "minimal",
46+ "promptCacheKey": "policy-engine-v1"
47+ }
48 }
49 ```
50
51@@ -50,12 +50,12 @@ Use local llama.cpp. This backend does not use authentication.
52
53 ```json
54 {
55- "reviewer": {
56- "kind": "llama.cpp",
57- "baseUrl": "http://127.0.0.1:9931",
58- "model": "reviewer",
59- "enableThinking": true
60- }
61+ "reviewer": {
62+ "kind": "llama.cpp",
63+ "baseUrl": "http://127.0.0.1:9931",
64+ "model": "reviewer",
65+ "enableThinking": true
66+ }
67 }
68 ```
69
70@@ -74,9 +74,7 @@ Pi uses a root-level `tools` map and `externalDirectories` list. Any tool name c
71 "read": "allow",
72 "my_extension_tool": "allow"
73 },
74- "externalDirectories": [
75- "/tmp/pi"
76- ]
77+ "externalDirectories": ["/tmp/pi"]
78 }
79 ```
80
81@@ -114,7 +112,6 @@ POLICY_EVAL_REASONING_EFFORT=minimal \
82 bun test test/core/llm.test.ts
83 ```
84
85-
86 Pi evaluations using Pi's OpenAI Responses APIs use `policy-engine-evaluation-v1` as the cache key; set `POLICY_EVAL_PROMPT_CACHE_KEY` to override it.
87
88 Tests redirect Pi state to a temporary directory, except opt-in Pi OAuth evaluations, which use the existing Pi OAuth session.
89diff --git a/bun.lock b/bun.lock
90index a2298473651561f90a6c4f14cf9c3915dd551850..5609fefbe2dae566eaadddb743a2300a80f91b35 100644
91--- a/bun.lock
92+++ b/bun.lock
93@@ -13,6 +13,7 @@
94 "@eslint/js": "^10.0.1",
95 "bun-types": "latest",
96 "eslint": "^10.1.0",
97+ "prettier": "3.9.6",
98 "typescript": "^6.0.2",
99 "typescript-eslint": "^8.58.0",
100 },
101@@ -384,6 +385,8 @@
102
103 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
104
105+ "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
106+
107 "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
108
109diff --git a/eslint.config.js b/eslint.config.js
110index b519dda02dd81259febb00781320261b091a692c..fc1f8d5ef22b9efc2d3d6f339cb3f15821c5006c 100644
111--- a/eslint.config.js
112+++ b/eslint.config.js
113@@ -1,5 +1,5 @@
114-import eslint from "@eslint/js"
115-import tseslint from "typescript-eslint"
116+import eslint from "@eslint/js";
117+import tseslint from "typescript-eslint";
118
119 export default tseslint.config(
120 eslint.configs.recommended,
121@@ -14,4 +14,4 @@ export default tseslint.config(
122 "no-empty": ["error", { allowEmptyCatch: true }],
123 },
124 },
125-)
126+);
127diff --git a/package.json b/package.json
128index ae2566315559f0718a2170b77083f9e09302ba22..2ae90170e191e4c8b4ed44e1caa0600257307d7b 100644
129--- a/package.json
130+++ b/package.json
131@@ -24,6 +24,7 @@
132 "@eslint/js": "^10.0.1",
133 "bun-types": "latest",
134 "eslint": "^10.1.0",
135+ "prettier": "3.9.6",
136 "typescript": "^6.0.2",
137 "typescript-eslint": "^8.58.0"
138 },
139diff --git a/src/core/audit.ts b/src/core/audit.ts
140index 9cd7bd1aaafebbbb3de7684bcf26828df20c9789..46cfc5e9e18a308cb56f2bd1e3707638e23e2840 100644
141--- a/src/core/audit.ts
142+++ b/src/core/audit.ts
143@@ -1,19 +1,18 @@
144-import { appendFile, mkdir } from "node:fs/promises"
145-import { dirname } from "node:path"
146-import type { DecisionAudit } from "./types"
147+import { appendFile, mkdir } from "node:fs/promises";
148+import { dirname } from "node:path";
149+import type { DecisionAudit } from "./types";
150
151 export function createJsonlDecisionAudit(file: string): DecisionAudit {
152 return {
153 async record(entry): Promise<void> {
154 try {
155- await mkdir(dirname(file), { recursive: true })
156- await appendFile(file, `${JSON.stringify({ recordedAt: new Date().toISOString(), ...entry })}\n`)
157- } catch {
158- }
159+ await mkdir(dirname(file), { recursive: true });
160+ await appendFile(file, `${JSON.stringify({ recordedAt: new Date().toISOString(), ...entry })}\n`);
161+ } catch {}
162 },
163- }
164+ };
165 }
166
167 export const disabledDecisionAudit: DecisionAudit = {
168 record: async () => {},
169-}
170+};
171diff --git a/src/core/cache.ts b/src/core/cache.ts
172index be7ac7d8185b9d848418affe4e44ea78ca5cb08e..e85e9ed26d7d437de67e2b0b9644229ba540b473 100644
173--- a/src/core/cache.ts
174+++ b/src/core/cache.ts
175@@ -1,41 +1,38 @@
176-import { appendFile, mkdir, readFile } from "node:fs/promises"
177-import { dirname } from "node:path"
178-import { POLICY_VERSION } from "./rules"
179-import type { CacheEntry, Decision, DecisionCache } from "./types"
180+import { appendFile, mkdir, readFile } from "node:fs/promises";
181+import { dirname } from "node:path";
182+import { POLICY_VERSION } from "./rules";
183+import type { CacheEntry, Decision, DecisionCache } from "./types";
184
185 type StoredCacheEntry = CacheEntry & {
186- policyVersion: number
187-}
188+ policyVersion: number;
189+};
190
191 export function createJsonlDecisionCache(file: string): DecisionCache {
192 return {
193 async lookup(key: string): Promise<Decision | undefined> {
194 try {
195- const content = await readFile(file, "utf8")
196+ const content = await readFile(file, "utf8");
197 for (const line of content.split("\n")) {
198- if (!line) continue
199+ if (!line) continue;
200 try {
201- const entry = JSON.parse(line) as StoredCacheEntry
202- if (entry.key === key && entry.policyVersion === POLICY_VERSION) return entry.decision
203- } catch {
204- }
205+ const entry = JSON.parse(line) as StoredCacheEntry;
206+ if (entry.key === key && entry.policyVersion === POLICY_VERSION) return entry.decision;
207+ } catch {}
208 }
209- } catch {
210- }
211- return undefined
212+ } catch {}
213+ return undefined;
214 },
215 async write(entry: CacheEntry): Promise<void> {
216 try {
217- await mkdir(dirname(file), { recursive: true })
218- const stored: StoredCacheEntry = { ...entry, policyVersion: POLICY_VERSION }
219- await appendFile(file, `${JSON.stringify(stored)}\n`)
220- } catch {
221- }
222+ await mkdir(dirname(file), { recursive: true });
223+ const stored: StoredCacheEntry = { ...entry, policyVersion: POLICY_VERSION };
224+ await appendFile(file, `${JSON.stringify(stored)}\n`);
225+ } catch {}
226 },
227- }
228+ };
229 }
230
231 export const disabledDecisionCache: DecisionCache = {
232 lookup: async () => undefined,
233 write: async () => {},
234-}
235+};
236diff --git a/src/core/config.ts b/src/core/config.ts
237index 45a6b36099f7c3d2879f377d5267f96a00396756..8c585217cb6fe564de3c345e65bd68896317d13f 100644
238--- a/src/core/config.ts
239+++ b/src/core/config.ts
240@@ -1,59 +1,65 @@
241-import type { PiReasoningEffort, ReviewerConfig } from "./types"
242+import type { PiReasoningEffort, ReviewerConfig } from "./types";
243
244 function objectValue(value: unknown): Record<string, unknown> | undefined {
245- return value && typeof value === "object" && !Array.isArray(value)
246- ? value as Record<string, unknown>
247- : undefined
248+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
249 }
250
251 function stringValue(value: unknown): string | undefined {
252- return typeof value === "string" && value.trim() ? value.trim() : undefined
253+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
254 }
255
256 function booleanValue(value: unknown, field: string, fallback: boolean): boolean {
257- if (value === undefined) return fallback
258- if (typeof value === "boolean") return value
259- throw new Error(`${field} must be a boolean`)
260+ if (value === undefined) return fallback;
261+ if (typeof value === "boolean") return value;
262+ throw new Error(`${field} must be a boolean`);
263 }
264
265 function reasoningEffort(value: unknown): PiReasoningEffort | undefined {
266- if (value === undefined) return undefined
267- if (value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") {
268- return value
269+ if (value === undefined) return undefined;
270+ if (
271+ value === "none" ||
272+ value === "minimal" ||
273+ value === "low" ||
274+ value === "medium" ||
275+ value === "high" ||
276+ value === "xhigh" ||
277+ value === "max"
278+ ) {
279+ return value;
280 }
281- throw new Error('reviewer.reasoningEffort must be "none", "minimal", "low", "medium", "high", "xhigh", or "max"')
282+ throw new Error('reviewer.reasoningEffort must be "none", "minimal", "low", "medium", "high", "xhigh", or "max"');
283 }
284
285 export function parseReviewerConfig(value: unknown): ReviewerConfig {
286- const data = objectValue(value)
287- const reviewer = data && objectValue(data.reviewer)
288+ const data = objectValue(value);
289+ const reviewer = data && objectValue(data.reviewer);
290 if (!reviewer) {
291 return {
292 kind: "pi",
293 provider: "openai-codex",
294 model: "gpt-5.6-luna",
295 reasoningEffort: "minimal",
296- }
297+ };
298 }
299- const model = stringValue(reviewer.model)
300- if (!model) throw new Error("reviewer.model must be a non-empty string")
301+ const model = stringValue(reviewer.model);
302+ if (!model) throw new Error("reviewer.model must be a non-empty string");
303 if (reviewer.kind === "llama.cpp") {
304- const baseUrl = stringValue(reviewer.baseUrl)
305- if (!baseUrl) throw new Error("reviewer.baseUrl must be a non-empty string")
306+ const baseUrl = stringValue(reviewer.baseUrl);
307+ if (!baseUrl) throw new Error("reviewer.baseUrl must be a non-empty string");
308 return {
309 kind: "llama.cpp",
310 baseUrl,
311 model,
312 enableThinking: booleanValue(reviewer.enableThinking, "reviewer.enableThinking", false),
313- }
314+ };
315 }
316- if (reviewer.kind !== "pi") throw new Error('reviewer.kind must be "pi" or "llama.cpp"')
317+ if (reviewer.kind !== "pi") throw new Error('reviewer.kind must be "pi" or "llama.cpp"');
318
319- const provider = stringValue(reviewer.provider)
320- if (!provider) throw new Error("reviewer.provider must be a non-empty string")
321- const promptCacheKey = stringValue(reviewer.promptCacheKey)
322+ const provider = stringValue(reviewer.provider);
323+ if (!provider) throw new Error("reviewer.provider must be a non-empty string");
324+ const promptCacheKey = stringValue(reviewer.promptCacheKey);
325 if (reviewer.promptCacheKey !== undefined && !promptCacheKey) {
326- throw new Error("reviewer.promptCacheKey must be a non-empty string")
327+ throw new Error("reviewer.promptCacheKey must be a non-empty string");
328 }
329 return {
330 kind: "pi",
331@@ -61,5 +67,5 @@ export function parseReviewerConfig(value: unknown): ReviewerConfig {
332 model,
333 reasoningEffort: reasoningEffort(reviewer.reasoningEffort),
334 promptCacheKey,
335- }
336+ };
337 }
338diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
339index ae1dadcff4a135053ccf2d098b92cfbd21da1456..6f6616772b15e643860372e7b9fb0c7637c3faa7 100644
340--- a/src/core/deterministic.ts
341+++ b/src/core/deterministic.ts
342@@ -1,4 +1,4 @@
343-import type { Decision } from "./types"
344+import type { Decision } from "./types";
345 import {
346 HARD_ALLOW_PATTERNS,
347 CONFIG_ALLOW_PATTERNS,
348@@ -6,165 +6,233 @@ import {
349 SHELL_CONTROL_RE,
350 STDERR_REDIRECT_RE,
351 DEVNULL_REDIRECT_RE,
352-} from "./rules"
353-import { stripRtkPrefix, expandHome } from "./normalize"
354+} from "./rules";
355+import { stripRtkPrefix, expandHome } from "./normalize";
356
357 function hasShellControl(cmd: string): boolean {
358- return SHELL_CONTROL_RE.test(cmd)
359+ return SHELL_CONTROL_RE.test(cmd);
360 }
361
362 function checkHardAllow(command: string): Decision | undefined {
363- const cmd = command.trim()
364- if (!cmd || hasShellControl(cmd)) return undefined
365+ const cmd = command.trim();
366+ if (!cmd || hasShellControl(cmd)) return undefined;
367 for (const pat of HARD_ALLOW_PATTERNS) {
368 if (pat.test(cmd)) {
369 return {
370 decision: "allow",
371 reason: `Matched safe pattern: ${pat.source.slice(0, 50)}`,
372 category: "read_only",
373- }
374+ };
375 }
376 }
377- return undefined
378+ return undefined;
379 }
380
381 function shellWords(command: string): string[] {
382 return (command.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]+/g) ?? []).map((word) => {
383 if ((word.startsWith('"') && word.endsWith('"')) || (word.startsWith("'") && word.endsWith("'"))) {
384- return word.slice(1, -1)
385+ return word.slice(1, -1);
386 }
387- return word
388- })
389+ return word;
390+ });
391 }
392
393 function isSecretPath(word: string): boolean {
394- const path = word.replace(/\/+$/, "")
395- if (/(?:^|\/)\.env[A-Za-z0-9._-]*(?:\/|$)/i.test(path)) return true
396- if (/(?:^|\/)(?:credentials?|tokens?)(?:[._-][A-Za-z0-9_-]+)*(?:\/|$)/i.test(path)) return true
397+ const path = word.replace(/\/+$/, "");
398+ if (/(?:^|\/)\.env[A-Za-z0-9._-]*(?:\/|$)/i.test(path)) return true;
399+ if (/(?:^|\/)(?:credentials?|tokens?)(?:[._-][A-Za-z0-9_-]+)*(?:\/|$)/i.test(path)) return true;
400
401- const home = process.env.HOME
402- const homePrefix = home ? `(?:~|${home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})` : "~"
403+ const home = process.env.HOME;
404+ const homePrefix = home ? `(?:~|${home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})` : "~";
405 return new RegExp(
406 `^${homePrefix}/(?:\\.(?:aws|ssh|azure|kube|gnupg|oci)(?:/|$)|\\.config/(?:gcloud|gh|hub|azure|doctl|rclone|sops|containers)(?:/|$)|\\.local/share/keyrings(?:/|$)|\\.docker/config\\.json$|\\.(?:netrc|npmrc|pypirc|git-credentials)$)`,
407 "i",
408- ).test(path)
409+ ).test(path);
410 }
411
412 function positionalWords(words: string[], valueOptions: ReadonlySet<string> = new Set()): string[] {
413- const positional: string[] = []
414- let options = true
415+ const positional: string[] = [];
416+ let options = true;
417 for (let index = 0; index < words.length; index += 1) {
418- const word = words[index]
419+ const word = words[index];
420 if (options && word === "--") {
421- options = false
422- continue
423+ options = false;
424+ continue;
425 }
426 if (options && word.startsWith("-")) {
427- if (valueOptions.has(word)) index += 1
428- continue
429+ if (valueOptions.has(word)) index += 1;
430+ continue;
431 }
432- positional.push(word)
433+ positional.push(word);
434 }
435- return positional
436+ return positional;
437 }
438
439 function hasSecretOptionValue(arguments_: string[], optionNames: ReadonlySet<string>): boolean {
440 for (let index = 0; index < arguments_.length; index += 1) {
441- const word = arguments_[index]
442diff --git a/src/core/index.ts b/src/core/index.ts
443index deace70d68115317dbe36f258cc97e81bfe49577..2215d6055fc05215847bc512ef036684e8be4229 100644
444--- a/src/core/index.ts
445+++ b/src/core/index.ts
446@@ -1,10 +1,10 @@
447-export * from "./audit"
448-export * from "./cache"
449-export * from "./config"
450-export * from "./deterministic"
451-export * from "./normalize"
452-export * from "./pipeline"
453-export * from "./providers"
454-export * from "./review"
455-export * from "./rules"
456-export * from "./types"
457+export * from "./audit";
458+export * from "./cache";
459+export * from "./config";
460+export * from "./deterministic";
461+export * from "./normalize";
462+export * from "./pipeline";
463+export * from "./providers";
464+export * from "./review";
465+export * from "./rules";
466+export * from "./types";
467diff --git a/src/core/normalize.ts b/src/core/normalize.ts
468index e075b3413b92a34d70572d7c24230ee238388085..adb10b497067578f98038613766115bcf0081be6 100644
469--- a/src/core/normalize.ts
470+++ b/src/core/normalize.ts
471@@ -1,89 +1,92 @@
472-import { createHash } from "node:crypto"
473-import { POLICY_VERSION } from "./rules"
474-import type { PolicyRequest } from "./types"
475+import { createHash } from "node:crypto";
476+import { POLICY_VERSION } from "./rules";
477+import type { PolicyRequest } from "./types";
478
479-const RTK_PREFIX_RE = /^rtk\s+/
480-const SENSITIVE_KEY_RE = /(?:api[-_]?key|auth(?:entication|orization)?|credential|cookie|password|secret|token)/i
481-const SENSITIVE_ARGUMENT_RE = /((?:^|\s)(?:-u|-H)(?:=|\s*)|(?:^|\s)--(?:api[-_]?key|authorization|cookie|password|secret|token|user|proxy-user|header)(?:=|\s+))(?:(?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*')|\S+)/gi
482-const URL_USERINFO_RE = /([a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/gi
483-const SENSITIVE_QUERY_RE = /([?&](?:api[-_]?key|authorization|credential|cookie|password|secret|token)=)[^&\s]*/gi
484+const RTK_PREFIX_RE = /^rtk\s+/;
485+const SENSITIVE_KEY_RE = /(?:api[-_]?key|auth(?:entication|orization)?|credential|cookie|password|secret|token)/i;
486+const SENSITIVE_ARGUMENT_RE =
487+ /((?:^|\s)(?:-u|-H)(?:=|\s*)|(?:^|\s)--(?:api[-_]?key|authorization|cookie|password|secret|token|user|proxy-user|header)(?:=|\s+))(?:(?:"(?:[^"\\]|\\.)*")|(?:'(?:[^'\\]|\\.)*')|\S+)/gi;
488+const URL_USERINFO_RE = /([a-z][a-z\d+.-]*:\/\/)[^/\s@]+@/gi;
489+const SENSITIVE_QUERY_RE = /([?&](?:api[-_]?key|authorization|credential|cookie|password|secret|token)=)[^&\s]*/gi;
490
491 export function stripRtkPrefix(command: string): string {
492- return command.replace(RTK_PREFIX_RE, "")
493+ return command.replace(RTK_PREFIX_RE, "");
494 }
495
496 export function expandHome(command: string): string {
497- const home = process.env.HOME
498- if (!home) return command
499+ const home = process.env.HOME;
500+ if (!home) return command;
501 const unwrapped = command.replace(
502 /(["'])\$HOME((?:[^"'\\]|\\.)*)\1/g,
503 (_, _quote: string, rest: string) => home + rest,
504- )
505- return unwrapped.replaceAll("$HOME", home)
506+ );
507+ return unwrapped.replaceAll("$HOME", home);
508 }
509
510 function normalizeBashCommand(command: string): string {
511- let normalized = command.split(/\s+/).join(" ").trim()
512- const home = process.env.HOME ?? "~"
513- normalized = normalized.replaceAll("~", home)
514- normalized = expandHome(normalized)
515- normalized = normalized.replace(/;+\s*$/, "").trim()
516- return stripRtkPrefix(normalized)
517+ let normalized = command.split(/\s+/).join(" ").trim();
518+ const home = process.env.HOME ?? "~";
519+ normalized = normalized.replaceAll("~", home);
520+ normalized = expandHome(normalized);
521+ normalized = normalized.replace(/;+\s*$/, "").trim();
522+ return stripRtkPrefix(normalized);
523 }
524
525 function stableJson(value: unknown): string {
526- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`
527+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
528 if (value && typeof value === "object") {
529 const entries = Object.entries(value as Record<string, unknown>)
530 .sort(([left], [right]) => left.localeCompare(right))
531- .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
532- return `{${entries.join(",")}}`
533+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`);
534+ return `{${entries.join(",")}}`;
535 }
536- return JSON.stringify(value)
537+ return JSON.stringify(value);
538 }
539
540 export function normalizeRequest(toolName: string, input: Record<string, unknown>): string {
541 switch (toolName) {
542 case "bash": {
543- const command = typeof input.command === "string" ? input.command : ""
544- return `Bash:${normalizeBashCommand(command)}`
545+ const command = typeof input.command === "string" ? input.command : "";
546+ return `Bash:${normalizeBashCommand(command)}`;
547 }
548 case "webfetch":
549- return `WebFetch:${input.url ?? ""}`
550+ return `WebFetch:${input.url ?? ""}`;
551 default:
552- return `${toolName}:${stableJson(input)}`
553+ return `${toolName}:${stableJson(input)}`;
554 }
555 }
556
557 export function cacheKey(normalized: string): string {
558- const payload = `${POLICY_VERSION}\n${normalized}`
559- return createHash("sha256").update(payload).digest("hex").slice(0, 16)
560+ const payload = `${POLICY_VERSION}\n${normalized}`;
561+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
562 }
563
564 function redactText(value: string): string {
565 return value
566 .replace(SENSITIVE_ARGUMENT_RE, "$1[REDACTED]")
567 .replace(URL_USERINFO_RE, "$1[REDACTED]@")
568- .replace(SENSITIVE_QUERY_RE, "$1[REDACTED]")
569+ .replace(SENSITIVE_QUERY_RE, "$1[REDACTED]");
570 }
571diff --git a/src/core/pipeline.ts b/src/core/pipeline.ts
572index eb753e9eb56fe6ab26ca19dfd24505d4a7c5a2d5..dbd45fa5643f8cf0dd4dd9a458542a5d9a7603e6 100644
573--- a/src/core/pipeline.ts
574+++ b/src/core/pipeline.ts
575@@ -8,30 +8,30 @@ import type {
576 PolicyPrecheck,
577 PolicyRequest,
578 PolicyReviewer,
579-} from "./types"
580+} from "./types";
581
582 export type PolicyPipelineOptions = {
583- prechecks?: PolicyPrecheck[]
584- deterministic(request: PolicyRequest, context: PolicyContext): Decision | undefined
585- cache: DecisionCache
586- audit: DecisionAudit
587- reviewer: PolicyReviewer
588- normalize(request: PolicyRequest, context: PolicyContext): string
589- cacheKey(normalized: string): string
590- inputSummary(request: PolicyRequest): string
591-}
592+ prechecks?: PolicyPrecheck[];
593+ deterministic(request: PolicyRequest, context: PolicyContext): Decision | undefined;
594+ cache: DecisionCache;
595+ audit: DecisionAudit;
596+ reviewer: PolicyReviewer;
597+ normalize(request: PolicyRequest, context: PolicyContext): string;
598+ cacheKey(normalized: string): string;
599+ inputSummary(request: PolicyRequest): string;
600+};
601
602 type EvaluationOptions = {
603- skipPrechecks?: boolean
604- skipDeterministic?: boolean
605- skipLLMReview?: boolean
606-}
607+ skipPrechecks?: boolean;
608+ skipDeterministic?: boolean;
609+ skipLLMReview?: boolean;
610+};
611
612 const EMPTY_DECISION: Decision = {
613 decision: "allow",
614 reason: "No operations to evaluate",
615 category: "empty",
616-}
617+};
618
619 export function createPolicyPipeline(options: PolicyPipelineOptions) {
620 async function record(
621@@ -48,11 +48,10 @@ export function createPolicyPipeline(options: PolicyPipelineOptions) {
622 timingMs: performance.now() - startedAt,
623 sessionId: context.sessionId,
624 rawResponse: result.error ? result.rawResponse : undefined,
625- }
626+ };
627 try {
628- await options.audit.record(entry)
629- } catch {
630- }
631+ await options.audit.record(entry);
632+ } catch {}
633 }
634
635 async function complete(
636@@ -61,17 +60,17 @@ export function createPolicyPipeline(options: PolicyPipelineOptions) {
637 result: PolicyEvaluation,
638 startedAt: number,
639 ): Promise<PolicyEvaluation> {
640- await record(request, context, result, startedAt)
641- return result
642+ await record(request, context, result, startedAt);
643+ return result;
644 }
645
646 async function precheck(request: PolicyRequest, context: PolicyContext): Promise<PolicyEvaluation | undefined> {
647- const startedAt = performance.now()
648+ const startedAt = performance.now();
649 for (const check of options.prechecks ?? []) {
650- const decision = await check.decide(request, context)
651- if (decision) return complete(request, context, { decision, source: check.source }, startedAt)
652+ const decision = await check.decide(request, context);
653+ if (decision) return complete(request, context, { decision, source: check.source }, startedAt);
654 }
655- return undefined
656+ return undefined;
657 }
658
659 async function evaluate(
660@@ -80,44 +79,48 @@ export function createPolicyPipeline(options: PolicyPipelineOptions) {
661 evaluationOptions: EvaluationOptions = {},
662 ): Promise<PolicyEvaluation> {
663 if (!evaluationOptions.skipPrechecks) {
664- const checked = await precheck(request, context)
665- if (checked) return checked
666+ const checked = await precheck(request, context);
667+ if (checked) return checked;
668 }
669
670- const startedAt = performance.now()
671+ const startedAt = performance.now();
672 if (!evaluationOptions.skipDeterministic) {
673- const deterministic = options.deterministic(request, context)
674+ const deterministic = options.deterministic(request, context);
675diff --git a/src/core/providers.ts b/src/core/providers.ts
676index ee15e1869182af691db86cd809b4f55818f6b5e6..9e14f44413f22e44bdbc176e23c08e7c1bb70b23 100644
677--- a/src/core/providers.ts
678+++ b/src/core/providers.ts
679@@ -1,27 +1,27 @@
680-import type { ChatMessage } from "./review"
681-import type { ReviewerConfig } from "./types"
682+import type { ChatMessage } from "./review";
683+import type { ReviewerConfig } from "./types";
684
685 type LlamaResponse = {
686 choices?: {
687 message?: {
688- content?: unknown
689- }
690- }[]
691-}
692+ content?: unknown;
693+ };
694+ }[];
695+};
696
697 function textValue(value: unknown): string | undefined {
698- return typeof value === "string" && value.trim() ? value : undefined
699+ return typeof value === "string" && value.trim() ? value : undefined;
700 }
701
702 function completionUrl(baseUrl: string): string {
703- const normalized = baseUrl.replace(/\/+$/, "")
704- const root = normalized.endsWith("/v1") ? normalized : `${normalized}/v1`
705- return `${root}/chat/completions`
706+ const normalized = baseUrl.replace(/\/+$/, "");
707+ const root = normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
708+ return `${root}/chat/completions`;
709 }
710
711 async function responseError(response: Response): Promise<never> {
712- const detail = (await response.text()).trim().slice(0, 1000)
713- throw new Error(`llama-server API ${response.status}${detail ? `: ${detail}` : ""}`)
714+ const detail = (await response.text()).trim().slice(0, 1000);
715+ throw new Error(`llama-server API ${response.status}${detail ? `: ${detail}` : ""}`);
716 }
717
718 export async function callLlamaReviewer(
719@@ -42,10 +42,10 @@ export async function callLlamaReviewer(
720 chat_template_kwargs: { enable_thinking: config.enableThinking },
721 reasoning_format: "deepseek",
722 }),
723- })
724- if (!response.ok) return responseError(response)
725- const data = await response.json() as LlamaResponse
726- const text = textValue(data.choices?.[0]?.message?.content)
727- if (!text) throw new Error("llama-server response had no generated text")
728- return text
729+ });
730+ if (!response.ok) return responseError(response);
731+ const data = (await response.json()) as LlamaResponse;
732+ const text = textValue(data.choices?.[0]?.message?.content);
733+ if (!text) throw new Error("llama-server response had no generated text");
734+ return text;
735 }
736diff --git a/src/core/review.ts b/src/core/review.ts
737index b3261fafc4db3cfca84028a8986b7f6e28b12118..a66b6335d6f31834c54aaf066a86c3a80884e872 100644
738--- a/src/core/review.ts
739+++ b/src/core/review.ts
740@@ -1,5 +1,5 @@
741-import { callLlamaReviewer } from "./providers"
742-import { llmPolicyPrompt } from "./rules"
743+import { callLlamaReviewer } from "./providers";
744+import { llmPolicyPrompt } from "./rules";
745 import {
746 isDecisionCategory,
747 type Decision,
748@@ -8,55 +8,57 @@ import {
749 type PolicyRequest,
750 type PolicyReviewer,
751 type ReviewerConfig,
752-} from "./types"
753+} from "./types";
754
755 export type ChatMessage = {
756- role: "system" | "user"
757- content: string
758-}
759+ role: "system" | "user";
760+ content: string;
761+};
762
763 export type PiReviewerCaller = (
764 config: Extract<ReviewerConfig, { kind: "pi" }>,
765 messages: ChatMessage[],
766 maxTokens: number,
767 context: PolicyContext,
768-) => Promise<string>
769+) => Promise<string>;
770
771 const ASK_FALLBACK: Decision = {
772 decision: "ask",
773 reason: "Policy engine error",
774 category: "uncertain",
775-}
776+};
777
778 export function parseLLMResponse(content: string): Decision | undefined {
779 try {
780- const trimmed = content.trim()
781- const json = trimmed.startsWith("{")
782- ? trimmed
783- : trimmed.slice(trimmed.indexOf("{"), trimmed.lastIndexOf("}") + 1)
784- const data = JSON.parse(json) as Record<string, unknown>
785- const decision = data.decision
786- if (decision !== "allow" && decision !== "deny" && decision !== "ask") return undefined
787+ const trimmed = content.trim();
788+ const json = trimmed.startsWith("{") ? trimmed : trimmed.slice(trimmed.indexOf("{"), trimmed.lastIndexOf("}") + 1);
789+ const data = JSON.parse(json) as Record<string, unknown>;
790+ const decision = data.decision;
791+ if (decision !== "allow" && decision !== "deny" && decision !== "ask") return undefined;
792 return {
793 decision,
794 reason: typeof data.reason === "string" ? data.reason : "No reason provided",
795 category: isDecisionCategory(data.category) ? data.category : "uncertain",
796- }
797+ };
798 } catch {
799- return undefined
800+ return undefined;
801 }
802 }
803
804 function errorReason(error: unknown): string {
805- return `Policy engine error: ${error instanceof Error ? error.message : String(error)}`
806+ return `Policy engine error: ${error instanceof Error ? error.message : String(error)}`;
807 }
808
809 function reviewRequest(request: PolicyRequest, context: PolicyContext): string {
810- return `<tool_request>\n${JSON.stringify({
811- cwd: context.cwd,
812- toolName: request.toolName,
813- input: request.input,
814- }, null, 2)}\n</tool_request>`
815+ return `<tool_request>\n${JSON.stringify(
816+ {
817+ cwd: context.cwd,
818+ toolName: request.toolName,
819+ input: request.input,
820+ },
821+ null,
822+ 2,
823+ )}\n</tool_request>`;
824 }
825
826 export function createPolicyReviewer(
827@@ -70,24 +72,25 @@ export function createPolicyReviewer(
828 const messages = [
829 { role: "system" as const, content: llmPolicyPrompt(externalDirectories) },
830 { role: "user" as const, content: reviewRequest(request, context) },
831- ]
832- const rawResponse = config.kind === "pi"
833- ? await callPiReviewer(config, messages, 512, context)
834- : await callLlamaReviewer(config, messages, 1024)
835- const decision = parseLLMResponse(rawResponse)
836- if (decision) return { decision, rawResponse }
837+ ];
838+ const rawResponse =
839+ config.kind === "pi"
840diff --git a/src/core/rules.ts b/src/core/rules.ts
841index 10a564fd69eebc02a9fe693f73eeaf816db290ed..922dc10b03bcd20de191938cf86f959bc2faf2b1 100644
842--- a/src/core/rules.ts
843+++ b/src/core/rules.ts
844@@ -227,19 +227,12 @@ Respond with ONLY this JSON, WITHOUT markdown formatting, otherwise this program
845 "category": ${DECISION_CATEGORIES.map((category) => `"${category}"`).join(" | ")}
846 }`;
847
848-export function llmPolicyPrompt(
849- externalDirectories: readonly string[] = [],
850-): string {
851+export function llmPolicyPrompt(externalDirectories: readonly string[] = []): string {
852 const directories =
853 externalDirectories.length === 0
854 ? " - No additional directories."
855- : `${externalDirectories
856- .map((pattern) => ` - ${JSON.stringify(pattern)}`)
857- .join("\n")}
858+ : `${externalDirectories.map((pattern) => ` - ${JSON.stringify(pattern)}`).join("\n")}
859
860 Treat these as trusted policy rules. Allow access when a tool-call path starts with a listed path. This overrides the general directory guidance above.`;
861- return LLM_POLICY_PROMPT.replace(
862- EXTERNAL_DIRECTORIES_PLACEHOLDER,
863- directories,
864- );
865+ return LLM_POLICY_PROMPT.replace(EXTERNAL_DIRECTORIES_PLACEHOLDER, directories);
866 }
867diff --git a/src/core/types.ts b/src/core/types.ts
868index c0767d0178e75f99f85bf3e9f890c72bcd054ebf..49b4b1286a8cf6bd80f1719be72afe9e799ef24e 100644
869--- a/src/core/types.ts
870+++ b/src/core/types.ts
871@@ -21,98 +21,98 @@ export const DECISION_CATEGORIES = [
872 "grep",
873 "ls",
874 "webfetch",
875-] as const
876+] as const;
877
878-export type DecisionCategory = (typeof DECISION_CATEGORIES)[number]
879+export type DecisionCategory = (typeof DECISION_CATEGORIES)[number];
880
881-const decisionCategorySet: ReadonlySet<string> = new Set(DECISION_CATEGORIES)
882+const decisionCategorySet: ReadonlySet<string> = new Set(DECISION_CATEGORIES);
883
884 export function isDecisionCategory(value: unknown): value is DecisionCategory {
885- return typeof value === "string" && decisionCategorySet.has(value)
886+ return typeof value === "string" && decisionCategorySet.has(value);
887 }
888
889 export type Decision = {
890- decision: "allow" | "deny" | "ask"
891- reason: string
892- category: DecisionCategory
893-}
894+ decision: "allow" | "deny" | "ask";
895+ reason: string;
896+ category: DecisionCategory;
897+};
898
899 export type LLMEvaluationResult = {
900- decision: Decision
901- rawResponse?: string
902- error?: string
903-}
904+ decision: Decision;
905+ rawResponse?: string;
906+ error?: string;
907+};
908
909 export type PolicyReviewer = {
910- evaluate(request: PolicyRequest, context: PolicyContext): Promise<LLMEvaluationResult>
911-}
912+ evaluate(request: PolicyRequest, context: PolicyContext): Promise<LLMEvaluationResult>;
913+};
914
915-export type PiReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
916+export type PiReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
917
918 export type ReviewerConfig =
919 | {
920- kind: "pi"
921- provider: string
922- model: string
923- reasoningEffort?: PiReasoningEffort
924- promptCacheKey?: string
925- }
926+ kind: "pi";
927+ provider: string;
928+ model: string;
929+ reasoningEffort?: PiReasoningEffort;
930+ promptCacheKey?: string;
931+ }
932 | {
933- kind: "llama.cpp"
934- baseUrl: string
935- model: string
936- enableThinking: boolean
937- }
938+ kind: "llama.cpp";
939+ baseUrl: string;
940+ model: string;
941+ enableThinking: boolean;
942+ };
943
944 export type CacheEntry = {
945- key: string
946- toolName: string
947- decision: Decision
948- source: "llm"
949- createdAt: string
950-}
951+ key: string;
952+ toolName: string;
953+ decision: Decision;
954+ source: "llm";
955+ createdAt: string;
956+};
957
958 export type DecisionCache = {
959- lookup(key: string): Promise<Decision | undefined>
960- write(entry: CacheEntry): Promise<void>
961-}
962+ lookup(key: string): Promise<Decision | undefined>;
963+ write(entry: CacheEntry): Promise<void>;
964+};
965
966-export type DecisionSource = "session" | "static" | "deterministic" | "cache" | "llm"
967+export type DecisionSource = "session" | "static" | "deterministic" | "cache" | "llm";
968
969 export type AuditEntry = {
970- toolName: string
971diff --git a/src/index.ts b/src/index.ts
972index fc3a91016b51c12c495f6257a263520cea311c45..178dfe9160bd85851f00bcd997f3a2faba7c9044 100644
973--- a/src/index.ts
974+++ b/src/index.ts
975@@ -1 +1 @@
976-export { default } from "./pi/index"
977+export { default } from "./pi/index";
978diff --git a/src/pi/bash-split.ts b/src/pi/bash-split.ts
979index c4571d102af9349be2eb5a2ad7474c19140352f5..dff2c9e7733dfbfb9207f8a9e2d884014dc0f78d 100644
980--- a/src/pi/bash-split.ts
981+++ b/src/pi/bash-split.ts
982@@ -1,41 +1,41 @@
983-import { createRequire } from "node:module"
984-import { Language, Parser, type Node } from "web-tree-sitter"
985+import { createRequire } from "node:module";
986+import { Language, Parser, type Node } from "web-tree-sitter";
987
988 export type BashSplitResult = {
989- commands: string[]
990- parsed: boolean
991-}
992+ commands: string[];
993+ parsed: boolean;
994+};
995
996 type BashParser = {
997- parse(command: string): { rootNode: Node; delete(): void } | null
998-}
999+ parse(command: string): { rootNode: Node; delete(): void } | null;
1000+};
1001
1002-type BashParserLoader = () => Promise<BashParser>
1003+type BashParserLoader = () => Promise<BashParser>;
1004
1005-const require = createRequire(import.meta.url)
1006-let parserPromise: Promise<BashParser> | undefined
1007+const require = createRequire(import.meta.url);
1008+let parserPromise: Promise<BashParser> | undefined;
1009
1010 function source(node: Node): string {
1011- return (node.parent?.type === "redirected_statement" ? node.parent.text : node.text).trim()
1012+ return (node.parent?.type === "redirected_statement" ? node.parent.text : node.text).trim();
1013 }
1014
1015 async function loadBashParser(): Promise<BashParser> {
1016 if (!parserPromise) {
1017 parserPromise = (async () => {
1018- const parserWasm = require.resolve("web-tree-sitter/web-tree-sitter.wasm")
1019- const bashWasm = require.resolve("tree-sitter-bash/tree-sitter-bash.wasm")
1020- await Parser.init({ locateFile: () => parserWasm })
1021- const language = await Language.load(bashWasm)
1022- const parser = new Parser()
1023- parser.setLanguage(language)
1024- return parser
1025- })()
1026+ const parserWasm = require.resolve("web-tree-sitter/web-tree-sitter.wasm");
1027+ const bashWasm = require.resolve("tree-sitter-bash/tree-sitter-bash.wasm");
1028+ await Parser.init({ locateFile: () => parserWasm });
1029+ const language = await Language.load(bashWasm);
1030+ const parser = new Parser();
1031+ parser.setLanguage(language);
1032+ return parser;
1033+ })();
1034 }
1035- return parserPromise
1036+ return parserPromise;
1037 }
1038
1039 function raw(command: string): BashSplitResult {
1040- return { commands: [command], parsed: false }
1041+ return { commands: [command], parsed: false };
1042 }
1043
1044 export async function splitBashCommand(
1045@@ -43,25 +43,25 @@ export async function splitBashCommand(
1046 loadParser: BashParserLoader = loadBashParser,
1047 ): Promise<BashSplitResult> {
1048 try {
1049- const parser = await loadParser()
1050- const tree = parser.parse(command)
1051- if (!tree || tree.rootNode.hasError) return raw(command)
1052+ const parser = await loadParser();
1053+ const tree = parser.parse(command);
1054+ if (!tree || tree.rootNode.hasError) return raw(command);
1055 try {
1056- const commands: string[] = []
1057- const seen = new Set<string>()
1058+ const commands: string[] = [];
1059+ const seen = new Set<string>();
1060 for (const node of tree.rootNode.descendantsOfType("command")) {
1061- if (!node) continue
1062- const text = source(node)
1063+ if (!node) continue;
1064+ const text = source(node);
1065 if (text && !seen.has(text)) {
1066- seen.add(text)
1067- commands.push(text)
1068+ seen.add(text);
1069+ commands.push(text);
1070 }
1071 }
1072- return { commands, parsed: true }
1073+ return { commands, parsed: true };
1074 } finally {
1075- tree.delete()
1076+ tree.delete();
1077 }
1078 } catch {
1079- return raw(command)
1080+ return raw(command);
1081 }
1082diff --git a/src/pi/config.ts b/src/pi/config.ts
1083index c8c746b405bf1250815b2f4ea0ac3128047d0877..ada564c0862437dc4378276d0517941ee3d2aa43 100644
1084--- a/src/pi/config.ts
1085+++ b/src/pi/config.ts
1086@@ -1,114 +1,109 @@
1087-import { existsSync, readFileSync } from "node:fs"
1088-import { homedir } from "node:os"
1089-import { join } from "node:path"
1090-import { parseReviewerConfig } from "../core/config"
1091-import type { ReviewerConfig } from "../core/types"
1092+import { existsSync, readFileSync } from "node:fs";
1093+import { homedir } from "node:os";
1094+import { join } from "node:path";
1095+import { parseReviewerConfig } from "../core/config";
1096+import type { ReviewerConfig } from "../core/types";
1097
1098-export type PermissionAction = "allow" | "check" | "deny"
1099-export type ExternalDirectories = string[]
1100+export type PermissionAction = "allow" | "check" | "deny";
1101+export type ExternalDirectories = string[];
1102
1103 export type PiPolicyEngineConfig = {
1104- reviewer: ReviewerConfig
1105- tools: Record<string, PermissionAction>
1106- externalDirectories: ExternalDirectories
1107-}
1108+ reviewer: ReviewerConfig;
1109+ tools: Record<string, PermissionAction>;
1110+ externalDirectories: ExternalDirectories;
1111+};
1112
1113 function globalConfigPath(): string {
1114- return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "policy-engine.json")
1115+ return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "policy-engine.json");
1116 }
1117
1118 function configPaths(cwd: string): string[] {
1119- return [globalConfigPath(), join(cwd, ".pi", "policy-engine.json")]
1120+ return [globalConfigPath(), join(cwd, ".pi", "policy-engine.json")];
1121 }
1122
1123 export function piPolicyPaths(): { cacheFile: string; auditFile: string } {
1124- const directory = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent")
1125+ const directory = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
1126 return {
1127 cacheFile: join(directory, "policy-engine", "cache", "decisions.jsonl"),
1128 auditFile: join(directory, "policy-engine", "logs", "policy.jsonl"),
1129- }
1130+ };
1131 }
1132
1133 function objectValue(value: unknown): Record<string, unknown> | undefined {
1134- return value && typeof value === "object" && !Array.isArray(value)
1135- ? value as Record<string, unknown>
1136- : undefined
1137+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
1138 }
1139
1140 function permissionAction(value: unknown, field: string): PermissionAction {
1141- if (value === "allow" || value === "check" || value === "deny") return value
1142- throw new Error(`${field} must be "allow", "check", or "deny"`)
1143+ if (value === "allow" || value === "check" || value === "deny") return value;
1144+ throw new Error(`${field} must be "allow", "check", or "deny"`);
1145 }
1146
1147 function toolPermissions(value: unknown): Record<string, PermissionAction> {
1148- const data = objectValue(value)
1149- if (!data) throw new Error("tools must be a JSON object")
1150+ const data = objectValue(value);
1151+ if (!data) throw new Error("tools must be a JSON object");
1152 return Object.fromEntries(
1153 Object.entries(data).map(([name, action]) => [name, permissionAction(action, `tools.${name}`)]),
1154- )
1155+ );
1156 }
1157
1158 function externalDirectories(value: unknown): ExternalDirectories {
1159 if (!Array.isArray(value) || !value.every((path) => typeof path === "string")) {
1160- throw new Error("externalDirectories must be a JSON array of strings")
1161+ throw new Error("externalDirectories must be a JSON array of strings");
1162 }
1163- return value
1164+ return value;
1165 }
1166
1167 export function parsePiPolicyConfig(value: unknown): PiPolicyEngineConfig {
1168- const data = objectValue(value)
1169- if (!data) throw new Error("policy engine configuration must be a JSON object")
1170+ const data = objectValue(value);
1171+ if (!data) throw new Error("policy engine configuration must be a JSON object");
1172 if (data.permission !== undefined) {
1173- throw new Error("permission has been renamed to tools; external_directory has been renamed to externalDirectories")
1174+ throw new Error("permission has been renamed to tools; external_directory has been renamed to externalDirectories");
1175 }
1176 return {
1177 reviewer: parseReviewerConfig(data),
1178 tools: data.tools === undefined ? {} : toolPermissions(data.tools),
1179 externalDirectories: data.externalDirectories === undefined ? [] : externalDirectories(data.externalDirectories),
1180- }
1181+ };
1182 }
1183
1184 function mergeObjects(
1185 base: Record<string, unknown> | undefined,
1186diff --git a/src/pi/index.ts b/src/pi/index.ts
1187index 576f69b4c4e4fa545db36ec39be5b7c8b84cc434..a1c3450e103cecfd0db861637adf7162e71f789a 100644
1188--- a/src/pi/index.ts
1189+++ b/src/pi/index.ts
1190@@ -1,78 +1,72 @@
1191-import type { UserMessage } from "@earendil-works/pi-ai"
1192-import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
1193-import { createJsonlDecisionAudit } from "../core/audit"
1194-import { createJsonlDecisionCache } from "../core/cache"
1195-import { checkDeterministic } from "../core/deterministic"
1196-import { auditInputSummary, cacheKey, normalizeRequest } from "../core/normalize"
1197-import { createPolicyPipeline } from "../core/pipeline"
1198-import { createPolicyReviewer, type PiReviewerCaller } from "../core/review"
1199-import type { PolicyContext, PolicyEvaluation, PolicyRequest } from "../core/types"
1200-import { splitBashCommand, type BashSplitResult } from "./bash-split"
1201-import { loadPiPolicyConfig, piPolicyPaths } from "./config"
1202-import { PolicyApprovalDialog } from "./policy-approval"
1203+import type { UserMessage } from "@earendil-works/pi-ai";
1204+import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1205+import { createJsonlDecisionAudit } from "../core/audit";
1206+import { createJsonlDecisionCache } from "../core/cache";
1207+import { checkDeterministic } from "../core/deterministic";
1208+import { auditInputSummary, cacheKey, normalizeRequest } from "../core/normalize";
1209+import { createPolicyPipeline } from "../core/pipeline";
1210+import { createPolicyReviewer, type PiReviewerCaller } from "../core/review";
1211+import type { PolicyContext, PolicyEvaluation, PolicyRequest } from "../core/types";
1212+import { splitBashCommand, type BashSplitResult } from "./bash-split";
1213+import { loadPiPolicyConfig, piPolicyPaths } from "./config";
1214+import { PolicyApprovalDialog } from "./policy-approval";
1215 import {
1216 createSessionBashAllowOverride,
1217 matchesSessionBashAllowOverride,
1218 restoreSessionBashAllowOverride,
1219 SESSION_BASH_ALLOW_ENTRY,
1220 type SessionBashAllowOverride,
1221-} from "./session-override"
1222-import { checkStaticPermission } from "./static-permissions"
1223+} from "./session-override";
1224+import { checkStaticPermission } from "./static-permissions";
1225
1226-type ToolInput = Record<string, unknown>
1227+type ToolInput = Record<string, unknown>;
1228
1229 function toolInput(input: unknown): ToolInput {
1230- return input && typeof input === "object" && !Array.isArray(input)
1231- ? { ...input as ToolInput }
1232- : {}
1233+ return input && typeof input === "object" && !Array.isArray(input) ? { ...(input as ToolInput) } : {};
1234 }
1235
1236 function inputSummary(toolName: string, input: ToolInput): string {
1237- if (toolName === "bash" && typeof input.command === "string") return input.command
1238- return JSON.stringify(input).slice(0, 500)
1239+ if (toolName === "bash" && typeof input.command === "string") return input.command;
1240+ return JSON.stringify(input).slice(0, 500);
1241 }
1242
1243 function approvalMessage(toolName: string, input: ToolInput, result: PolicyEvaluation): string {
1244- const rawResponse = result.error === "Failed to parse response JSON" && result.rawResponse
1245- ? `\n\nLLM response:\n${result.rawResponse}`
1246- : ""
1247+ const rawResponse =
1248+ result.error === "Failed to parse response JSON" && result.rawResponse
1249+ ? `\n\nLLM response:\n${result.rawResponse}`
1250+ : "";
1251 if (toolName !== "bash" || typeof input.command !== "string") {
1252- return `${toolName}: ${inputSummary(toolName, input)}\n\n${result.decision.reason}${rawResponse}`
1253+ return `${toolName}: ${inputSummary(toolName, input)}\n\n${result.decision.reason}${rawResponse}`;
1254 }
1255
1256 const commands = result.approvalRequests
1257 ?.map((request) => request.input.command)
1258- .filter((command): command is string => typeof command === "string")
1259- ?? [input.command]
1260- return `Entire Bash input:\n${input.command}\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}\n\n${result.decision.reason}${rawResponse}`
1261-}
1262-
1263-function bypassesPiPolicy(toolName: string): boolean {
1264- return toolName === "mcp" || toolName.startsWith("mcp__")
1265+ .filter((command): command is string => typeof command === "string") ?? [input.command];
1266+ return `Entire Bash input:\n${input.command}\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}\n\n${result.decision.reason}${rawResponse}`;
1267 }
1268
1269 function piReviewerCaller(ctx: ExtensionContext): PiReviewerCaller {
1270 return async (
1271 config: {
1272- provider: string
1273- model: string
1274- reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
1275- promptCacheKey?: string
1276+ provider: string;
1277+ model: string;
1278+ reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
1279+ promptCacheKey?: string;
1280 },
1281 messages: readonly { role: "system" | "user"; content: string }[],
1282 _maxTokens: number,
1283 context: PolicyContext,
1284 ): Promise<string> => {
1285- const model = ctx.modelRegistry.find(config.provider, config.model)
1286- if (!model) throw new Error(`Pi model is unavailable: ${config.provider}/${config.model}`)
1287- const systemPrompt = messages.find((message) => message.role === "system")?.content
1288+ const model = ctx.modelRegistry.find(config.provider, config.model);
1289+ if (!model) throw new Error(`Pi model is unavailable: ${config.provider}/${config.model}`);
1290diff --git a/src/pi/policy-approval.ts b/src/pi/policy-approval.ts
1291index 85c8f05c8e60bdd4427e1d9d03cdd375fc0260ea..956d166f031d8c013f8499b4f3c8a1b8ddcda80f 100644
1292--- a/src/pi/policy-approval.ts
1293+++ b/src/pi/policy-approval.ts
1294@@ -1,23 +1,25 @@
1295-import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent"
1296-import { Key, matchesKey, truncateToWidth, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"
1297+import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
1298+import { Key, matchesKey, truncateToWidth, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
1299
1300 function escapeTerminalControls(value: string): string {
1301- return [...value].map((character) => {
1302- if (character === "\n") return character
1303- const codePoint = character.codePointAt(0)!
1304- if (codePoint < 0x20 || (codePoint >= 0x7F && codePoint < 0xA0)) {
1305- return `\\x${codePoint.toString(16).padStart(2, "0")}`
1306- }
1307- return character
1308- }).join("")
1309+ return [...value]
1310+ .map((character) => {
1311+ if (character === "\n") return character;
1312+ const codePoint = character.codePointAt(0)!;
1313+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) {
1314+ return `\\x${codePoint.toString(16).padStart(2, "0")}`;
1315+ }
1316+ return character;
1317+ })
1318+ .join("");
1319 }
1320
1321 export class PolicyApprovalDialog {
1322- private selectedApproval = true
1323- private scrollTop = 0
1324- private visibleBodyLines = 1
1325- private bodyLines: string[] = []
1326- private readonly message: string
1327+ private selectedApproval = true;
1328+ private scrollTop = 0;
1329+ private visibleBodyLines = 1;
1330+ private bodyLines: string[] = [];
1331+ private readonly message: string;
1332
1333 constructor(
1334 private readonly tui: TUI,
1335@@ -27,64 +29,64 @@ export class PolicyApprovalDialog {
1336 message: string,
1337 private readonly done: (approved: boolean) => void,
1338 ) {
1339- this.message = escapeTerminalControls(message)
1340+ this.message = escapeTerminalControls(message);
1341 }
1342
1343 handleInput(data: string): void {
1344 if (this.keybindings.matches(data, "tui.select.cancel")) {
1345- this.done(false)
1346- return
1347+ this.done(false);
1348+ return;
1349 }
1350 if (this.keybindings.matches(data, "tui.select.confirm")) {
1351- this.done(this.selectedApproval)
1352- return
1353+ this.done(this.selectedApproval);
1354+ return;
1355 }
1356 if (this.keybindings.matches(data, "tui.input.tab") || matchesKey(data, Key.left) || matchesKey(data, Key.right)) {
1357- this.selectedApproval = !this.selectedApproval
1358- this.tui.requestRender()
1359- return
1360+ this.selectedApproval = !this.selectedApproval;
1361+ this.tui.requestRender();
1362+ return;
1363 }
1364 if (this.keybindings.matches(data, "tui.select.up")) {
1365- this.scroll(-1)
1366- return
1367+ this.scroll(-1);
1368+ return;
1369 }
1370 if (this.keybindings.matches(data, "tui.select.down")) {
1371- this.scroll(1)
1372- return
1373+ this.scroll(1);
1374+ return;
1375 }
1376 if (this.keybindings.matches(data, "tui.select.pageUp")) {
1377- this.scroll(-this.visibleBodyLines)
1378- return
1379+ this.scroll(-this.visibleBodyLines);
1380+ return;
1381 }
1382 if (this.keybindings.matches(data, "tui.select.pageDown")) {
1383- this.scroll(this.visibleBodyLines)
1384- return
1385+ this.scroll(this.visibleBodyLines);
1386+ return;
1387 }
1388 if (matchesKey(data, Key.home)) {
1389- this.scrollTo(0)
1390- return
1391+ this.scrollTo(0);
1392+ return;
1393 }
1394diff --git a/src/pi/session-override.ts b/src/pi/session-override.ts
1395index e009a7b2b629ba7117562594e6248bfbb0a4ed8a..262627a2bdec1771d9459e450424b8e88c87e67b 100644
1396--- a/src/pi/session-override.ts
1397+++ b/src/pi/session-override.ts
1398@@ -1,50 +1,50 @@
1399-export const SESSION_BASH_ALLOW_ENTRY = "policy-engine-session-bash-allow"
1400+export const SESSION_BASH_ALLOW_ENTRY = "policy-engine-session-bash-allow";
1401
1402 export type SessionBashAllowOverride = {
1403- source: string
1404- pattern: RegExp
1405-}
1406+ source: string;
1407+ pattern: RegExp;
1408+};
1409
1410 type SessionEntry = {
1411- type?: unknown
1412- customType?: unknown
1413- data?: unknown
1414-}
1415+ type?: unknown;
1416+ customType?: unknown;
1417+ data?: unknown;
1418+};
1419
1420 function sessionPattern(data: unknown, sessionId: string): { matches: boolean; source?: string } {
1421- if (!data || typeof data !== "object" || Array.isArray(data)) return { matches: false }
1422- const entry = data as { pattern?: unknown; sessionId?: unknown }
1423- if (entry.sessionId !== sessionId) return { matches: false }
1424- return { matches: true, source: typeof entry.pattern === "string" ? entry.pattern : undefined }
1425+ if (!data || typeof data !== "object" || Array.isArray(data)) return { matches: false };
1426+ const entry = data as { pattern?: unknown; sessionId?: unknown };
1427+ if (entry.sessionId !== sessionId) return { matches: false };
1428+ return { matches: true, source: typeof entry.pattern === "string" ? entry.pattern : undefined };
1429 }
1430
1431 export function createSessionBashAllowOverride(source: string): SessionBashAllowOverride {
1432- return { source, pattern: new RegExp(source) }
1433+ return { source, pattern: new RegExp(source) };
1434 }
1435
1436 export function matchesSessionBashAllowOverride(
1437 override: SessionBashAllowOverride | undefined,
1438 command: string,
1439 ): boolean {
1440- return override?.pattern.test(command) ?? false
1441+ return override?.pattern.test(command) ?? false;
1442 }
1443
1444 export function restoreSessionBashAllowOverride(
1445 entries: readonly unknown[],
1446 sessionId: string,
1447 ): SessionBashAllowOverride | undefined {
1448- let source: string | undefined
1449+ let source: string | undefined;
1450 for (const entry of entries) {
1451- const { type, customType, data } = entry as SessionEntry
1452- if (type !== "custom" || customType !== SESSION_BASH_ALLOW_ENTRY) continue
1453- const storedPattern = sessionPattern(data, sessionId)
1454- if (storedPattern.matches) source = storedPattern.source
1455+ const { type, customType, data } = entry as SessionEntry;
1456+ if (type !== "custom" || customType !== SESSION_BASH_ALLOW_ENTRY) continue;
1457+ const storedPattern = sessionPattern(data, sessionId);
1458+ if (storedPattern.matches) source = storedPattern.source;
1459 }
1460
1461- if (source === undefined) return undefined
1462+ if (source === undefined) return undefined;
1463 try {
1464- return createSessionBashAllowOverride(source)
1465+ return createSessionBashAllowOverride(source);
1466 } catch {
1467- return undefined
1468+ return undefined;
1469 }
1470 }
1471diff --git a/src/pi/static-permissions.ts b/src/pi/static-permissions.ts
1472index d40960bc80e1a09603967b22871ffa9131a92490..6c60b91cc0a31dc83ab39cd641b89c371390dcff 100644
1473--- a/src/pi/static-permissions.ts
1474+++ b/src/pi/static-permissions.ts
1475@@ -1,50 +1,57 @@
1476-import { realpath } from "node:fs/promises"
1477-import { homedir } from "node:os"
1478-import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
1479-import type { ExternalDirectories, PermissionAction, PiPolicyEngineConfig } from "./config"
1480-import type { Decision, DecisionCategory } from "../core/types"
1481+import { realpath } from "node:fs/promises";
1482+import { homedir } from "node:os";
1483+import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
1484+import type { ExternalDirectories, PermissionAction, PiPolicyEngineConfig } from "./config";
1485+import type { Decision, DecisionCategory } from "../core/types";
1486
1487-const PATH_TOOLS = new Set(["read", "write", "edit", "find", "grep", "ls"])
1488+const PATH_TOOLS = new Set(["read", "write", "edit", "find", "grep", "ls"]);
1489
1490-type ToolInput = Record<string, unknown>
1491+type ToolInput = Record<string, unknown>;
1492
1493 function expandedPattern(pattern: string): string {
1494- if (pattern === "~") return homedir()
1495- if (pattern.startsWith("~/")) return join(homedir(), pattern.slice(2))
1496- if (pattern === "$HOME") return homedir()
1497- if (pattern.startsWith("$HOME/")) return join(homedir(), pattern.slice(6))
1498- return pattern
1499+ if (pattern === "~") return homedir();
1500+ if (pattern.startsWith("~/")) return join(homedir(), pattern.slice(2));
1501+ if (pattern === "$HOME") return homedir();
1502+ if (pattern.startsWith("$HOME/")) return join(homedir(), pattern.slice(6));
1503+ return pattern;
1504 }
1505
1506 export function matchesExternalDirectory(directories: ExternalDirectories | undefined, value: string): boolean {
1507- return directories?.some((directory) => value.startsWith(expandedPattern(directory))) ?? false
1508+ return directories?.some((directory) => value.startsWith(expandedPattern(directory))) ?? false;
1509 }
1510
1511 async function canonicalPath(path: string, cwd: string): Promise<string> {
1512- const absolutePath = resolve(cwd, path.startsWith("@") ? path.slice(1) : path)
1513- const suffix: string[] = []
1514- let existingPath = absolutePath
1515+ const absolutePath = resolve(cwd, path.startsWith("@") ? path.slice(1) : path);
1516+ const suffix: string[] = [];
1517+ let existingPath = absolutePath;
1518
1519 while (true) {
1520 try {
1521- return join(await realpath(existingPath), ...suffix)
1522+ return join(await realpath(existingPath), ...suffix);
1523 } catch {
1524- const parentPath = dirname(existingPath)
1525- if (parentPath === existingPath) return absolutePath
1526- suffix.unshift(existingPath.slice(parentPath.length + 1))
1527- existingPath = parentPath
1528+ const parentPath = dirname(existingPath);
1529+ if (parentPath === existingPath) return absolutePath;
1530+ suffix.unshift(existingPath.slice(parentPath.length + 1));
1531+ existingPath = parentPath;
1532 }
1533 }
1534 }
1535
1536 function isInside(path: string, directory: string): boolean {
1537- const pathFromDirectory = relative(directory, path)
1538- return pathFromDirectory === "" || (!pathFromDirectory.startsWith(`..${sep}`) && pathFromDirectory !== ".." && !isAbsolute(pathFromDirectory))
1539+ const pathFromDirectory = relative(directory, path);
1540+ return (
1541+ pathFromDirectory === "" ||
1542+ (!pathFromDirectory.startsWith(`..${sep}`) && pathFromDirectory !== ".." && !isAbsolute(pathFromDirectory))
1543+ );
1544 }
1545
1546-function staticDecision(action: PermissionAction | undefined, reason: string, category: DecisionCategory): Decision | undefined {
1547- if (!action || action === "check") return undefined
1548- return { decision: action, reason, category }
1549+function staticDecision(
1550+ action: PermissionAction | undefined,
1551+ reason: string,
1552+ category: DecisionCategory,
1553+): Decision | undefined {
1554+ if (!action || action === "check") return undefined;
1555+ return { decision: action, reason, category };
1556 }
1557
1558 export async function checkStaticPermission(
1559@@ -53,22 +60,22 @@ export async function checkStaticPermission(
1560 cwd: string,
1561 config: PiPolicyEngineConfig,
1562 ): Promise<Decision | undefined> {
1563- const action = config.tools[toolType] ?? "check"
1564+ const action = config.tools[toolType] ?? "check";
1565
1566- let externalPath: string | undefined
1567+ let externalPath: string | undefined;
1568 if (PATH_TOOLS.has(toolType)) {
1569- const path = typeof input.path === "string" ? input.path : "."
1570- const absolutePath = await canonicalPath(path, cwd)
1571- const workspacePath = await canonicalPath(cwd, cwd)
1572+ const path = typeof input.path === "string" ? input.path : ".";
1573+ const absolutePath = await canonicalPath(path, cwd);
1574+ const workspacePath = await canonicalPath(cwd, cwd);
1575diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
1576index ccf3df0f89aefe16c22c1c28548f4f1989b93a96..f0af442c05c9dea36c10a31759898a6f859e61a8 100644
1577--- a/test/core/deterministic.test.ts
1578+++ b/test/core/deterministic.test.ts
1579@@ -1,5 +1,5 @@
1580-import { expect, test } from "bun:test"
1581-import { checkDeterministic } from "../../src/core/deterministic"
1582+import { expect, test } from "bun:test";
1583+import { checkDeterministic } from "../../src/core/deterministic";
1584
1585 test("allows sh syntax checks without restricting the source path", () => {
1586 for (const command of [
1587@@ -9,13 +9,13 @@ test("allows sh syntax checks without restricting the source path", () => {
1588 "bash -n ../script.sh",
1589 "bash -n",
1590 ]) {
1591- expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" })
1592+ expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
1593 }
1594-})
1595+});
1596
1597 test("keeps shell execution subject to confirmation", () => {
1598- expect(checkDeterministic("bash", { command: "bash script.sh -n" })).toMatchObject({ decision: "ask" })
1599-})
1600+ expect(checkDeterministic("bash", { command: "bash script.sh -n" })).toMatchObject({ decision: "ask" });
1601+});
1602
1603 test("allows recognized read-only command grammars", () => {
1604 for (const command of [
1605@@ -53,9 +53,9 @@ test("allows recognized read-only command grammars", () => {
1606 "tr -d '\\n'",
1607 "cut -d: -f1,3",
1608 ]) {
1609- expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" })
1610+ expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
1611 }
1612-})
1613+});
1614
1615 test("allows local Docker operations except volume deletion", () => {
1616 for (const command of [
1617@@ -65,7 +65,7 @@ test("allows local Docker operations except volume deletion", () => {
1618 "docker run --rm app",
1619 "docker volume ls",
1620 ]) {
1621- expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" })
1622+ expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
1623 }
1624
1625 for (const command of [
1626@@ -75,12 +75,12 @@ test("allows local Docker operations except volume deletion", () => {
1627 "docker compose down -v",
1628 "docker system prune --volumes",
1629 ]) {
1630- expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "ask" })
1631+ expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "ask" });
1632 }
1633-})
1634+});
1635
1636 test("asks before allowing reads from recognized secret paths", () => {
1637- const home = process.env.HOME ?? "/home/user"
1638+ const home = process.env.HOME ?? "/home/user";
1639 for (const command of [
1640 "cat .env",
1641 "head -n 10 config/.env.production",
1642@@ -97,17 +97,17 @@ test("asks before allowing reads from recognized secret paths", () => {
1643 "node .env",
1644 "npm --prefix .env test",
1645 ]) {
1646- expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "ask" })
1647+ expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "ask" });
1648 }
1649-})
1650+});
1651
1652 test("does not treat search terms as secret paths", () => {
1653- expect(checkDeterministic("bash", { command: "rg '.env' README.md" })).toMatchObject({ decision: "allow" })
1654-})
1655+ expect(checkDeterministic("bash", { command: "rg '.env' README.md" })).toMatchObject({ decision: "allow" });
1656+});
1657
1658 test("asks before stopping the Herdr server", () => {
1659- expect(checkDeterministic("bash", { command: "herdr server stop" })).toMatchObject({ decision: "ask" })
1660-})
1661+ expect(checkDeterministic("bash", { command: "herdr server stop" })).toMatchObject({ decision: "ask" });
1662+});
1663
1664 test("does not deterministically allow non-toolchain mutation or shell-control variants", () => {
1665 for (const command of [
1666@@ -119,6 +119,6 @@ test("does not deterministically allow non-toolchain mutation or shell-control v
1667 "kubectl kustomize deploy",
1668 "command -v node; rm -rf /",
1669 ]) {
1670- expect(checkDeterministic("bash", { command })?.decision).not.toBe("allow")
1671+ expect(checkDeterministic("bash", { command })?.decision).not.toBe("allow");
1672 }
1673-})
1674+});
1675diff --git a/test/core/llm.test.ts b/test/core/llm.test.ts
1676index 8954df667c1399f2822ecd8f69ad528650f0ac65..3978490ec896dbed3b93b1e247b03ae17bcb2924 100644
1677--- a/test/core/llm.test.ts
1678+++ b/test/core/llm.test.ts
1679@@ -1,47 +1,58 @@
1680-import { describe, expect, test } from "bun:test"
1681-import type { UserMessage } from "@earendil-works/pi-ai"
1682-import { ModelRuntime } from "@earendil-works/pi-coding-agent"
1683-import { createPolicyReviewer } from "../../src/core/review"
1684-import type { Decision, LLMEvaluationResult, PiReasoningEffort, PolicyReviewer } from "../../src/core/types"
1685+import { describe, expect, test } from "bun:test";
1686+import type { UserMessage } from "@earendil-works/pi-ai";
1687+import { ModelRuntime } from "@earendil-works/pi-coding-agent";
1688+import { createPolicyReviewer } from "../../src/core/review";
1689+import type { Decision, LLMEvaluationResult, PiReasoningEffort, PolicyReviewer } from "../../src/core/types";
1690
1691-const backend = process.env.POLICY_EVAL_BACKEND?.trim() ?? "llama.cpp"
1692-const baseUrl = process.env.POLICY_EVAL_BASE_URL?.trim()
1693-const provider = process.env.POLICY_EVAL_PROVIDER?.trim() ?? "openai-codex"
1694-const model = process.env.POLICY_EVAL_MODEL?.trim()
1695-const reasoningEffort = process.env.POLICY_EVAL_REASONING_EFFORT?.trim() ?? "minimal"
1696-const promptCacheKey = process.env.POLICY_EVAL_PROMPT_CACHE_KEY?.trim() ?? "policy-engine-evaluation-v1"
1697+const backend = process.env.POLICY_EVAL_BACKEND?.trim() ?? "llama.cpp";
1698+const baseUrl = process.env.POLICY_EVAL_BASE_URL?.trim();
1699+const provider = process.env.POLICY_EVAL_PROVIDER?.trim() ?? "openai-codex";
1700+const model = process.env.POLICY_EVAL_MODEL?.trim();
1701+const reasoningEffort = process.env.POLICY_EVAL_REASONING_EFFORT?.trim() ?? "minimal";
1702+const promptCacheKey = process.env.POLICY_EVAL_PROMPT_CACHE_KEY?.trim() ?? "policy-engine-evaluation-v1";
1703
1704 function piReasoningEffort(value: string): PiReasoningEffort {
1705- if (value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") {
1706- return value
1707+ if (
1708+ value === "none" ||
1709+ value === "minimal" ||
1710+ value === "low" ||
1711+ value === "medium" ||
1712+ value === "high" ||
1713+ value === "xhigh" ||
1714+ value === "max"
1715+ ) {
1716+ return value;
1717 }
1718- throw new Error("POLICY_EVAL_REASONING_EFFORT must be a Pi reasoning level")
1719+ throw new Error("POLICY_EVAL_REASONING_EFFORT must be a Pi reasoning level");
1720 }
1721
1722 async function createReviewer(): Promise<PolicyReviewer | undefined> {
1723 if (backend === "llama.cpp") {
1724 if (Boolean(baseUrl) !== Boolean(model)) {
1725- throw new Error("POLICY_EVAL_BASE_URL and POLICY_EVAL_MODEL must both be set for llama.cpp evaluation")
1726+ throw new Error("POLICY_EVAL_BASE_URL and POLICY_EVAL_MODEL must both be set for llama.cpp evaluation");
1727 }
1728- if (!baseUrl || !model) return undefined
1729- return createPolicyReviewer({
1730- kind: "llama.cpp",
1731- baseUrl,
1732- model,
1733- enableThinking: true,
1734- }, async () => {
1735- throw new Error("Pi reviewer must not be used for llama.cpp evaluation")
1736- })
1737+ if (!baseUrl || !model) return undefined;
1738+ return createPolicyReviewer(
1739+ {
1740+ kind: "llama.cpp",
1741+ baseUrl,
1742+ model,
1743+ enableThinking: true,
1744+ },
1745+ async () => {
1746+ throw new Error("Pi reviewer must not be used for llama.cpp evaluation");
1747+ },
1748+ );
1749 }
1750
1751- if (backend !== "pi") throw new Error('POLICY_EVAL_BACKEND must be "llama.cpp" or "pi"')
1752- if (!model) throw new Error("POLICY_EVAL_MODEL must be set for Pi evaluation")
1753+ if (backend !== "pi") throw new Error('POLICY_EVAL_BACKEND must be "llama.cpp" or "pi"');
1754+ if (!model) throw new Error("POLICY_EVAL_MODEL must be set for Pi evaluation");
1755
1756- const runtime = await ModelRuntime.create()
1757- const piModel = runtime.getModel(provider, model)
1758- if (!piModel) throw new Error(`Pi model is unavailable: ${provider}/${model}`)
1759- const effort = piReasoningEffort(reasoningEffort)
1760- const usesOpenAIPromptCache = piModel.api === "openai-codex-responses" || piModel.api === "openai-responses"
1761+ const runtime = await ModelRuntime.create();
1762+ const piModel = runtime.getModel(provider, model);
1763+ if (!piModel) throw new Error(`Pi model is unavailable: ${provider}/${model}`);
1764+ const effort = piReasoningEffort(reasoningEffort);
1765+ const usesOpenAIPromptCache = piModel.api === "openai-codex-responses" || piModel.api === "openai-responses";
1766
1767 return createPolicyReviewer(
1768 {
1769@@ -52,14 +63,14 @@ async function createReviewer(): Promise<PolicyReviewer | undefined> {
1770 ...(usesOpenAIPromptCache && { promptCacheKey }),
1771 },
1772 async (_config, messages, _maxTokens, context) => {
1773- const systemPrompt = messages.find((message) => message.role === "system")?.content
1774+ const systemPrompt = messages.find((message) => message.role === "system")?.content;
1775 const userMessages: UserMessage[] = messages
1776 .filter((message) => message.role === "user")
1777 .map((message) => ({
1778 role: "user",
1779diff --git a/test/setup.ts b/test/setup.ts
1780index 4aba665b25fd5e3e6802463c5927935225a50df2..ff5f241d138fcd6e189bf358fb9b0cffac74b830 100644
1781--- a/test/setup.ts
1782+++ b/test/setup.ts
1783@@ -1,12 +1,12 @@
1784-import { mkdtempSync, rmSync } from "node:fs"
1785-import { tmpdir } from "node:os"
1786-import { join } from "node:path"
1787+import { mkdtempSync, rmSync } from "node:fs";
1788+import { tmpdir } from "node:os";
1789+import { join } from "node:path";
1790
1791 if (process.env.POLICY_EVAL_BACKEND?.trim() !== "pi") {
1792- const stateDirectory = mkdtempSync(join(tmpdir(), "policy-engine-state-"))
1793- process.env.PI_CODING_AGENT_DIR = stateDirectory
1794+ const stateDirectory = mkdtempSync(join(tmpdir(), "policy-engine-state-"));
1795+ process.env.PI_CODING_AGENT_DIR = stateDirectory;
1796
1797 process.on("exit", () => {
1798- rmSync(stateDirectory, { recursive: true, force: true })
1799- })
1800+ rmSync(stateDirectory, { recursive: true, force: true });
1801+ });
1802 }