@hank-warren/pi-auto-permissions 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE.md CHANGED
@@ -4,4 +4,4 @@ This package is a fork of [`@ogulcancelik/pi-auto-permissions`](https://github.c
4
4
 
5
5
  The original work is Copyright (c) 2026 Can Celik and is used under the MIT License included in [`LICENSE`](LICENSE).
6
6
 
7
- Fork-specific changes (the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25)) are Copyright (c) 2026 Hank Warren and are released under the same MIT License.
7
+ Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License. They include the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25), and the prompted-review evaluation log with explicit user feedback.
package/README.md CHANGED
@@ -183,6 +183,33 @@ A successful result from an allowlisted tool qualifies when its `details` are `{
183
183
 
184
184
  The allowlist matches what you wrote: a bare name such as `ask_user_question` matches that tool in any namespace (`functions.ask_user_question` included), while a dotted name matches exactly. The default is an empty list.
185
185
 
186
+ ## Prompted-review evaluation log
187
+
188
+ Auto Permissions can append a private JSONL regression record whenever the guardian asks for confirmation and the user gives explicit feedback:
189
+
190
+ ```json
191
+ {
192
+ "evaluationLog": {
193
+ "enabled": true,
194
+ "path": "./review-evals.jsonl"
195
+ }
196
+ }
197
+ ```
198
+
199
+ The path defaults to `review-evals.jsonl` beside the Auto Permissions config and resolves relative to that config. The file is created with mode `0600`.
200
+
201
+ When logging is enabled, prompted reviews offer three choices:
202
+
203
+ - **Allow — asking was unnecessary** executes the command and records `userChoice: "allow_unnecessary"` with `expectedDecision: "approve"`.
204
+ - **Block** remains the second choice, blocks the command, and records `userChoice: "block"` with `expectedDecision: "ask_user"`.
205
+ - **Allow — asking was appropriate** executes the command and records `userChoice: "allow_appropriate"` with `expectedDecision: "ask_user"`.
206
+
207
+ When logging is disabled, the prompt retains the normal **Allow** and **Block** choices. Prompts from Pi or other extensions are unchanged.
208
+
209
+ Each version 2 record contains the collected user request, exact command, compact reviewer evidence, guardian reason, gate and session metadata, raw user choice, and both labels used for evaluation. The guardian's `actualDecision` is `ask_user`. Automatic-review failures are identified separately with `decisionSource: "review_failure"`. Existing version 1 records can remain in the same JSONL file.
210
+
211
+ Logging is disabled by default. Records can contain sensitive conversation text and shell commands, so keep the file private and out of repositories. Cancelled or interrupted prompts are not labeled or logged.
212
+
186
213
  ## Review display
187
214
 
188
215
  The default UI shows guardian progress in a temporary widget below the editor. Configure it with:
package/config.ts CHANGED
@@ -23,6 +23,10 @@ export interface AutoPermissionsConfig {
23
23
  projectInstructions: boolean;
24
24
  userAnswerTools: string[];
25
25
  };
26
+ evaluationLog: {
27
+ enabled: boolean;
28
+ path: string;
29
+ };
26
30
  rules: Gate[];
27
31
  ui: {
28
32
  enabled: boolean;
@@ -121,6 +125,28 @@ function resolveReviewEvidence(raw: Record<string, unknown>): AutoPermissionsCon
121
125
  };
122
126
  }
123
127
 
128
+ function resolveEvaluationLog(
129
+ raw: Record<string, unknown>,
130
+ configFilePath: string,
131
+ ): AutoPermissionsConfig["evaluationLog"] {
132
+ const defaultPath = resolve(dirname(configFilePath), "review-evals.jsonl");
133
+ if (raw.evaluationLog === undefined) return { enabled: false, path: defaultPath };
134
+ if (!raw.evaluationLog || typeof raw.evaluationLog !== "object" || Array.isArray(raw.evaluationLog)) {
135
+ throw new Error("evaluationLog must be an object");
136
+ }
137
+ const evaluationLog = raw.evaluationLog as Record<string, unknown>;
138
+ if (evaluationLog.enabled !== undefined && typeof evaluationLog.enabled !== "boolean") {
139
+ throw new Error("evaluationLog.enabled must be boolean");
140
+ }
141
+ const configuredPath = optionalString(evaluationLog.path, "evaluationLog.path");
142
+ if (!configuredPath) return { enabled: evaluationLog.enabled === true, path: defaultPath };
143
+ const expanded = configuredPath.startsWith("~/") ? join(homedir(), configuredPath.slice(2)) : configuredPath;
144
+ return {
145
+ enabled: evaluationLog.enabled === true,
146
+ path: isAbsolute(expanded) ? expanded : resolve(dirname(configFilePath), expanded),
147
+ };
148
+ }
149
+
124
150
  function resolveUi(raw: Record<string, unknown>): AutoPermissionsConfig["ui"] {
125
151
  if (raw.ui === undefined) return { enabled: true, resultDisplayMs: 2500, placement: "widget" };
126
152
  if (!raw.ui || typeof raw.ui !== "object" || Array.isArray(raw.ui)) {
@@ -172,6 +198,7 @@ export function loadAutoPermissionsConfig(path = configPath()): AutoPermissionsC
172
198
  reviewer: resolveReviewer(raw),
173
199
  systemPrompt: resolvePrompt(raw, path),
174
200
  reviewEvidence: resolveReviewEvidence(raw),
201
+ evaluationLog: resolveEvaluationLog(raw, path),
175
202
  rules: customRules,
176
203
  ui: resolveUi(raw),
177
204
  };
@@ -0,0 +1,72 @@
1
+ import { chmodSync, mkdirSync, appendFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import type { PermissionDecision, ReviewEvidenceRecord } from "./review.js";
4
+
5
+ export const PROMPT_FEEDBACK_OPTIONS = {
6
+ allowUnnecessary: "Allow — asking was unnecessary",
7
+ allowAppropriate: "Allow — asking was appropriate",
8
+ block: "Block",
9
+ } as const;
10
+
11
+ export type PromptEvaluationUserChoice = "allow_unnecessary" | "allow_appropriate" | "block";
12
+
13
+ export interface PromptChoiceClassification {
14
+ allowsExecution: boolean;
15
+ userChoice?: PromptEvaluationUserChoice;
16
+ }
17
+
18
+ export function permissionPromptOptions(evaluationLoggingEnabled: boolean): string[] {
19
+ return evaluationLoggingEnabled
20
+ ? [
21
+ PROMPT_FEEDBACK_OPTIONS.allowUnnecessary,
22
+ PROMPT_FEEDBACK_OPTIONS.block,
23
+ PROMPT_FEEDBACK_OPTIONS.allowAppropriate,
24
+ ]
25
+ : ["Allow", "Block"];
26
+ }
27
+
28
+ export interface PromptEvaluationRecord {
29
+ version: 2;
30
+ timestamp: string;
31
+ sessionId: string;
32
+ cwd: string;
33
+ tool: string;
34
+ gate: {
35
+ label: string;
36
+ group: string;
37
+ };
38
+ userRequest: string;
39
+ command: string;
40
+ relevantContext: ReviewEvidenceRecord[];
41
+ actualDecision: "ask_user";
42
+ actualReason: string;
43
+ decisionSource: "guardian" | "review_failure";
44
+ userChoice: PromptEvaluationUserChoice;
45
+ expectedDecision: Extract<PermissionDecision, "approve" | "ask_user">;
46
+ }
47
+
48
+ export function classifyPromptChoice(choice: string | undefined): PromptChoiceClassification | undefined {
49
+ if (choice === "Allow") return { allowsExecution: true };
50
+ if (choice === PROMPT_FEEDBACK_OPTIONS.allowUnnecessary) {
51
+ return { allowsExecution: true, userChoice: "allow_unnecessary" };
52
+ }
53
+ if (choice === PROMPT_FEEDBACK_OPTIONS.allowAppropriate) {
54
+ return { allowsExecution: true, userChoice: "allow_appropriate" };
55
+ }
56
+ if (choice === PROMPT_FEEDBACK_OPTIONS.block) {
57
+ return { allowsExecution: false, userChoice: "block" };
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ export function expectedDecisionForChoice(
63
+ choice: PromptEvaluationRecord["userChoice"],
64
+ ): PromptEvaluationRecord["expectedDecision"] {
65
+ return choice === "allow_unnecessary" ? "approve" : "ask_user";
66
+ }
67
+
68
+ export function appendPromptEvaluation(path: string, record: PromptEvaluationRecord): void {
69
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
70
+ appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
71
+ chmodSync(path, 0o600);
72
+ }
package/index.ts CHANGED
@@ -8,6 +8,13 @@ import { randomBytes, randomUUID } from "node:crypto";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { loadAutoPermissionsConfig, type AutoPermissionsConfig } from "./config.js";
11
+ import {
12
+ appendPromptEvaluation,
13
+ classifyPromptChoice,
14
+ expectedDecisionForChoice,
15
+ permissionPromptOptions,
16
+ type PromptEvaluationUserChoice,
17
+ } from "./evaluation-log.js";
11
18
  import { findGates, type Gate } from "./gates.js";
12
19
  import {
13
20
  buildReviewEnvelope,
@@ -191,6 +198,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
191
198
  const allowedConventionCommands = new Set<string>();
192
199
  let trustedGroups = new Set<string>();
193
200
  let lastConfigError: string | undefined;
201
+ let lastEvaluationLogError: string | undefined;
194
202
  let sessionActive = true;
195
203
  let bashRendererRegistered = false;
196
204
  let bashRendererSourcePath: string | undefined;
@@ -519,6 +527,50 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
519
527
  }
520
528
  }
521
529
 
530
+ function logPromptEvaluation(
531
+ gate: Gate,
532
+ command: string,
533
+ detail: string,
534
+ config: AutoPermissionsConfig,
535
+ ctx: ExtensionContext,
536
+ target: ReviewTarget,
537
+ relevantContext: ReviewEvidenceRecord[],
538
+ decisionSource: "guardian" | "review_failure",
539
+ userChoice: PromptEvaluationUserChoice,
540
+ ): void {
541
+ if (!config.evaluationLog.enabled) return;
542
+ try {
543
+ const userRequest = relevantContext
544
+ .filter((record) => record.source === "user")
545
+ .map((record) => record.text)
546
+ .join("\n");
547
+ appendPromptEvaluation(config.evaluationLog.path, {
548
+ version: 2,
549
+ timestamp: new Date().toISOString(),
550
+ sessionId: ctx.sessionManager.getSessionId(),
551
+ cwd: ctx.cwd,
552
+ tool: target.toolName,
553
+ gate: { label: gate.label, group: gate.group },
554
+ userRequest,
555
+ command,
556
+ relevantContext,
557
+ actualDecision: "ask_user",
558
+ actualReason: detail,
559
+ decisionSource,
560
+ userChoice,
561
+ expectedDecision: expectedDecisionForChoice(userChoice),
562
+ });
563
+ lastEvaluationLogError = undefined;
564
+ } catch (error) {
565
+ const message = error instanceof Error ? error.message : String(error);
566
+ if (message !== lastEvaluationLogError) {
567
+ lastEvaluationLogError = message;
568
+ console.error(`[pi-auto-permissions] could not append evaluation log: ${message}`);
569
+ ctx.ui.notify("Auto Permissions could not append its evaluation log.", "warning");
570
+ }
571
+ }
572
+ }
573
+
522
574
  async function askUser(
523
575
  gate: Gate,
524
576
  command: string,
@@ -528,6 +580,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
528
580
  ctx: ExtensionContext,
529
581
  target: ReviewTarget,
530
582
  lifecycleSignal: AbortSignal,
583
+ decisionSource: "guardian" | "review_failure",
531
584
  ): Promise<BlockResult | undefined> {
532
585
  const lifecycleStale = () => lifecycleSignal.aborted || reviewerLifecycleController.signal !== lifecycleSignal;
533
586
  const promptSignal = signal ? AbortSignal.any([signal, lifecycleSignal]) : lifecycleSignal;
@@ -542,13 +595,20 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
542
595
  return { block: true, reason: `${gate.label} requires user approval: ${detail}` };
543
596
  }
544
597
 
598
+ const evaluationContext = config.evaluationLog.enabled
599
+ ? collectReviewEvidence(
600
+ ctx.sessionManager.buildContextEntries(),
601
+ target.toolCallId,
602
+ config.reviewEvidence.userAnswerTools,
603
+ )
604
+ : [];
545
605
  setHerdrBlocked(true, gate.label);
546
606
  try {
547
607
  let choice: string | undefined;
548
608
  try {
549
609
  choice = await ctx.ui.select(
550
610
  `${gate.label} — Auto Permissions needs approval\n\n${detail}\n\n${command}`,
551
- ["Allow", "Block"],
611
+ permissionPromptOptions(config.evaluationLog.enabled),
552
612
  { signal: promptSignal },
553
613
  );
554
614
  } catch (error) {
@@ -562,7 +622,21 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
562
622
  if (sessionActive) clearReviewDisplay(ctx, target);
563
623
  return cancelled();
564
624
  }
565
- if (choice === "Allow") {
625
+ const classification = classifyPromptChoice(choice);
626
+ if (config.evaluationLog.enabled && classification?.userChoice) {
627
+ logPromptEvaluation(
628
+ gate,
629
+ command,
630
+ detail,
631
+ config,
632
+ ctx,
633
+ target,
634
+ evaluationContext,
635
+ decisionSource,
636
+ classification.userChoice,
637
+ );
638
+ }
639
+ if (classification?.allowsExecution) {
566
640
  showReviewDisplay(ctx, config, gate, command, target, "approved", "approved by user", true);
567
641
  return undefined;
568
642
  }
@@ -628,14 +702,24 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
628
702
  reason: `Auto Permissions requested revision: ${verdict.reason}\nRevise the command and try again.`,
629
703
  };
630
704
  }
631
- return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal);
705
+ return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal, "guardian");
632
706
  } catch (error) {
633
707
  if (lifecycleStale() || reviewCancelled(signal)) {
634
708
  if (!lifecycleStale() && sessionActive) clearReviewDisplay(ctx, target);
635
709
  return { block: true, reason: "Auto Permissions review cancelled" };
636
710
  }
637
711
  const reason = error instanceof Error ? error.message : String(error);
638
- return askUser(gate, command, `Automatic review failed: ${reason}`, config, signal, ctx, target, lifecycleSignal);
712
+ return askUser(
713
+ gate,
714
+ command,
715
+ `Automatic review failed: ${reason}`,
716
+ config,
717
+ signal,
718
+ ctx,
719
+ target,
720
+ lifecycleSignal,
721
+ "review_failure",
722
+ );
639
723
  }
640
724
  });
641
725
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-auto-permissions",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Fork of @ogulcancelik/pi-auto-permissions with dialog-answer user evidence: context-aware Bash permissions for Pi with automated guardian review.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -40,6 +40,7 @@
40
40
  "NOTICE.md",
41
41
  "index.ts",
42
42
  "config.ts",
43
+ "evaluation-log.ts",
43
44
  "gates.ts",
44
45
  "review.ts",
45
46
  "README.md",