703b37d257a0b024a54c1c00d0e585acb45ce83a

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Add rejection feedback prompt

Diff

 1diff --git a/src/pi/index.ts b/src/pi/index.ts
 2index f1c2cea8df1d330af0dab48ed003b2a2027d35e4..29333913397c6c74ad65944e4e826b933fbaeecf 100644
 3--- a/src/pi/index.ts
 4+++ b/src/pi/index.ts
 5@@ -303,9 +303,13 @@ export default function policyEngine(pi: ExtensionAPI) {
 6       pi.events.emit("herdr:blocked", { active: false });
 7     }
 8     if (approved) return undefined;
 9+    const feedback = await ctx.ui.input(
10+      "Why was this rejected?",
11+      "Tell the agent why it was rejected and what to do differently",
12+    );
13     return {
14       block: true,
15-      reason: `Blocked by user: ${result.decision.reason}`,
16+      reason: `Blocked by user: ${feedback?.trim() || result.decision.reason}`,
17     };
18   });
19 }
20diff --git a/test/pi/index.test.ts b/test/pi/index.test.ts
21index 550c218e5604b10191bd191f9d384068f536b893..3bd62e4581985ec74408e03fff59fb5e43563125 100644
22--- a/test/pi/index.test.ts
23+++ b/test/pi/index.test.ts
24@@ -2,8 +2,52 @@ import { expect, test } from "bun:test";
25 import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
26 import { tmpdir } from "node:os";
27 import { join } from "node:path";
28-import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
29-import { evaluatePiToolCall } from "../../src/pi/index";
30+import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
31+import policyEngine, { evaluatePiToolCall } from "../../src/pi/index";
32+
33+test("adds rejection feedback to an interactive approval block", async () => {
34+  const cwd = mkdtempSync(join(tmpdir(), "policy-engine-cwd-"));
35+  try {
36+    mkdirSync(join(cwd, ".pi"));
37+    writeFileSync(
38+      join(cwd, ".pi", "policy-engine.json"),
39+      JSON.stringify({ reviewer: { kind: "none" }, tools: { unknown: "check" } }),
40+    );
41+
42+    type ToolCallHandler = (
43+      event: { toolName: string; input: Record<string, unknown> },
44+      ctx: ExtensionContext,
45+    ) => Promise<unknown>;
46+    let toolCall: ToolCallHandler | undefined;
47+    policyEngine({
48+      on(event: string, handler: unknown) {
49+        if (event === "tool_call") toolCall = handler as ToolCallHandler;
50+      },
51+      registerCommand() {},
52+      appendEntry() {},
53+      events: { emit() {} },
54+    } as unknown as ExtensionAPI);
55+
56+    const confirm = async () => false;
57+    const input = async (title: string, placeholder?: string) => {
58+      expect(title).toBe("Why was this rejected?");
59+      expect(placeholder).toBe("Tell the agent why it was rejected and what to do differently");
60+      return "Use the dry-run command first";
61+    };
62+    const result = await toolCall!({ toolName: "unknown", input: {} }, {
63+      hasUI: true,
64+      mode: "rpc",
65+      cwd,
66+      modelRegistry: {},
67+      sessionManager: { getSessionId: () => "test-session" },
68+      ui: { confirm, input },
69+    } as unknown as ExtensionContext);
70+
71+    expect(result).toEqual({ block: true, reason: "Blocked by user: Use the dry-run command first" });
72+  } finally {
73+    rmSync(cwd, { recursive: true, force: true });
74+  }
75+});
76 
77 test("blocks Bash without consulting the cache or reviewer when the parser is unavailable", async () => {
78   const cwd = mkdtempSync(join(tmpdir(), "policy-engine-cwd-"));