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-"));