@hank-warren/pi-auto-permissions 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +12 -3
- package/denial-log.ts +8 -0
- package/guardian-reviewer.ts +2 -2
- package/guardian-transport.ts +5 -6
- package/index.ts +91 -3
- package/package.json +1 -1
- package/prompt-select.ts +5 -0
- package/review.ts +14 -3
- package/settings-command.ts +1 -1
- package/subagent-context.ts +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @hank-warren/pi-auto-permissions
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Make subagents revise a command before the human is interrupted, and make approval prompts work over RPC.
|
|
8
|
+
|
|
9
|
+
- A subagent session (`PI_SUBAGENT_CHILD=1`, set by `@hank-warren/pi-agents`) is told it is a subagent: a short section appended to its system prompt says that commands needing approval pause the supervising session, so it should prefer in-scope and read-only commands and revise when asked to. On Pi 1.x the section is a named prompt section (`auto_permissions_subagent`), so it never replaces the prompt that Pi and other extensions build, such as the parent's instruction files pi-agents adds to a child.
|
|
10
|
+
- In a subagent child with a UI, an `ask_user` verdict or a failed review first blocks the command with a revise-first reason instead of prompting. Issuing the same command again unchanged in a later turn escalates it to the human as an ordinary approval prompt, once: a later identical attempt is revised first again. Duplicates within the same turn, such as sibling calls or a codemode `Promise.all`, are refused too, because the model has not seen the first refusal yet. Children without a UI keep the "no interactive user" block.
|
|
11
|
+
- In RPC mode the approval prompt goes through `ctx.ui.select`. Before this, every approval forwarded over RPC was silently denied, because `custom()` returns nothing there. This applies to any RPC session, not only subagents: a client that never answers the prompt now leaves the command waiting until the turn is cancelled, where it used to be denied at once.
|
|
12
|
+
- The guardian's subagent section says what an `ask_user` verdict does for that child: revise first and then a human when its prompts surface in a parent, a hard block when it has no UI.
|
|
13
|
+
- A revise-first refusal is recorded in the denial log and the `auto-permissions:denied` event with `"reviseFirst": true`, keeping the `decisionSource` of the verdict behind it, and the recent denials view labels it "revise first".
|
|
14
|
+
|
|
3
15
|
## 0.18.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ The guardian must return one of three decisions:
|
|
|
243
243
|
|
|
244
244
|
- `approve`: execute the command
|
|
245
245
|
- `revise`: block it and tell the main agent what to correct
|
|
246
|
-
- `ask_user`: open an approval prompt (rendered with [`@hank-warren/pi-permission-selector`](../pi-permission-selector)'s `OptionSelector`): numbered options with `1`–`9` hotkeys, Tab to attach a note that is delivered to the agent as a steering user message, Esc to cancel — which blocks the command
|
|
246
|
+
- `ask_user`: open an approval prompt (rendered with [`@hank-warren/pi-permission-selector`](../pi-permission-selector)'s `OptionSelector`): numbered options with `1`–`9` hotkeys, Tab to attach a note that is delivered to the agent as a steering user message, Esc to cancel — which blocks the command. In RPC mode, as in a pi-agents child whose prompts the parent shows, the prompt is Pi's plain select dialog instead: Allow or Block, without Tab notes. It waits for the RPC client's answer, so a client that never answers holds the command until the turn is cancelled
|
|
247
247
|
|
|
248
248
|
## Conversation context and caching
|
|
249
249
|
|
|
@@ -443,9 +443,18 @@ Group names come from your configured rules. A trusted group bypasses guarded re
|
|
|
443
443
|
|
|
444
444
|
## Subagent sessions
|
|
445
445
|
|
|
446
|
-
When a session is a [pi-subagents](https://github.com/nicobailon/pi-subagents)
|
|
446
|
+
When a session is a subagent child (`PI_SUBAGENT_CHILD=1`, set by `@hank-warren/pi-agents` and [pi-subagents](https://github.com/nicobailon/pi-subagents)), the guardian receives additional execution facts with each review — run id, nesting depth, whether the cwd is a linked git worktree, and the checked-out branch — plus a prompt section telling it to judge risk by effect scope and reversibility relative to the subagent's own workspace instead of by command name. Mutations confined to the subagent's isolated worktree, its own feature branch, or resources it created are approvable when they serve the delegated task; `ask_user` is reserved for effects that escape that scope (shared or default branches, host-level configuration, production systems, credentials, data leaving the machine).
|
|
447
447
|
|
|
448
|
-
|
|
448
|
+
The child itself is told it is a subagent: a short section appended to its system prompt says that commands needing approval pause the supervising session, so it should prefer in-scope and read-only commands and revise when asked to. On Pi 1.x the section is a named prompt section (`auto_permissions_subagent`), so it never replaces the prompt that Pi and other extensions build, such as the parent's instruction files pi-agents adds to a child.
|
|
449
|
+
|
|
450
|
+
An `ask_user` verdict in a subagent, or a failed review (timeout, unavailable reviewer, unparsable verdict), never interrupts the human on the first try:
|
|
451
|
+
|
|
452
|
+
- **Child with a UI** (an RPC child whose prompts surface in the parent, as with pi-agents): the command is blocked with a revise-first reason. Issuing the *same* command again unchanged in a later turn escalates it to the human as an ordinary approval prompt, once: after that, a later identical attempt is revised first again. Duplicates within the same turn (sibling calls, or a codemode `Promise.all`) are refused as well, because the model has not seen the first refusal yet. Any other command gets its own revise-first turn.
|
|
453
|
+
- **Child without a UI**: the command is blocked with a reason instructing the child to route around the gated operation or report the blocker.
|
|
454
|
+
|
|
455
|
+
The guardian's subagent section says which of these applies, so a child without a UI is never told a human may approve later. A revise-first refusal is recorded in the denial log and the `auto-permissions:denied` event with `"reviseFirst": true`, keeping the `decisionSource` of the verdict behind it, so it stays distinguishable from a guardian's own revise.
|
|
456
|
+
|
|
457
|
+
Reviewer usage records from subagent sessions carry `"subagent": true` in the usage sidecar.
|
|
449
458
|
|
|
450
459
|
## Guardian dispatch
|
|
451
460
|
|
package/denial-log.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface DenialRecord {
|
|
|
28
28
|
verdict: DenialVerdict;
|
|
29
29
|
reason: string;
|
|
30
30
|
decisionSource: DenialSource;
|
|
31
|
+
/**
|
|
32
|
+
* A subagent's revise-first refusal: `decisionSource` would have gone to the
|
|
33
|
+
* human (an ask_user verdict or a failed review), and the subagent was told
|
|
34
|
+
* to revise first. Absent on every other denial.
|
|
35
|
+
*/
|
|
36
|
+
reviseFirst?: true;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export function buildDenialRecord(input: {
|
|
@@ -39,6 +45,7 @@ export function buildDenialRecord(input: {
|
|
|
39
45
|
verdict: DenialVerdict;
|
|
40
46
|
reason: string;
|
|
41
47
|
decisionSource: DenialSource;
|
|
48
|
+
reviseFirst?: boolean;
|
|
42
49
|
}): DenialRecord {
|
|
43
50
|
return {
|
|
44
51
|
v: 1,
|
|
@@ -52,6 +59,7 @@ export function buildDenialRecord(input: {
|
|
|
52
59
|
verdict: input.verdict,
|
|
53
60
|
reason: input.reason,
|
|
54
61
|
decisionSource: input.decisionSource,
|
|
62
|
+
...(input.reviseFirst ? { reviseFirst: true as const } : {}),
|
|
55
63
|
};
|
|
56
64
|
}
|
|
57
65
|
|
package/guardian-reviewer.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
OVERRIDE_FEEDBACK_SYSTEM_PROMPT,
|
|
25
25
|
parsePermissionVerdict,
|
|
26
26
|
SCRIPT_EVIDENCE_SYSTEM_PROMPT,
|
|
27
|
-
|
|
27
|
+
subagentContextSystemPrompt,
|
|
28
28
|
type PermissionVerdict,
|
|
29
29
|
type ReviewEvidenceRecord,
|
|
30
30
|
} from "./review.js";
|
|
@@ -317,7 +317,7 @@ export function createGuardianReviewer(
|
|
|
317
317
|
const subagentContext = detectSubagentContext(ctx.cwd);
|
|
318
318
|
const basePolicyPrompt = [
|
|
319
319
|
config.systemPrompt,
|
|
320
|
-
...(subagentContext ? [
|
|
320
|
+
...(subagentContext ? [subagentContextSystemPrompt(ctx.hasUI)] : []),
|
|
321
321
|
].join("\n\n");
|
|
322
322
|
// Appended outside config.systemPrompt so sessions using a customized
|
|
323
323
|
// systemPromptFile still learn how to weigh override records. The injected
|
package/guardian-transport.ts
CHANGED
|
@@ -22,12 +22,11 @@ import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
|
22
22
|
* future pi release renames the seam — degrading to the historical behavior
|
|
23
23
|
* instead of crashing.
|
|
24
24
|
*
|
|
25
|
-
* This file
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* package-specific detail stays behind a parameter (`packageLabel`).
|
|
25
|
+
* This file is duplicated verbatim in packages/pi-auto-permissions and
|
|
26
|
+
* packages/pi-auto-name; `DUPLICATED_SOURCES` in `scripts/validate.py` keeps
|
|
27
|
+
* the copies byte-identical. Edit one and copy it over the other. Every
|
|
28
|
+
* package-specific detail stays behind a parameter (`packageLabel`) so the
|
|
29
|
+
* copies never need to differ.
|
|
31
30
|
* The "guardian" in the names is historical: pi-auto-permissions' guardian
|
|
32
31
|
* reviewer was the first caller.
|
|
33
32
|
*/
|
package/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createSessionOverrides } from "./session-overrides.js";
|
|
|
26
26
|
import { createGuardianReviewer } from "./guardian-reviewer.js";
|
|
27
27
|
import { registerSettingsCommand } from "./settings-command.js";
|
|
28
28
|
import type { BlockResult, ReviewScope, ReviewTarget } from "./review-scope.js";
|
|
29
|
+
import { detectSubagentContext } from "./subagent-context.js";
|
|
29
30
|
import type { ReviewDisplayState } from "./widget-status.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -48,6 +49,8 @@ interface ReviewOutcome {
|
|
|
48
49
|
reason: string;
|
|
49
50
|
/** What the agent is told; absent means the command runs. */
|
|
50
51
|
block?: string;
|
|
52
|
+
/** A subagent's revise-first refusal, marked so the denial log tells it from a guardian revise. */
|
|
53
|
+
reviseFirst?: boolean;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
const PROJECT_CONFIG_DIR_NAME = (PiCodingAgent as { CONFIG_DIR_NAME?: string }).CONFIG_DIR_NAME ?? ".pi";
|
|
@@ -70,6 +73,22 @@ function denyReason(gate: Gate): string {
|
|
|
70
73
|
return `Blocked by policy: ${gate.label}\n\n${gate.message ?? "This operation is denied by rule."}\n\nThis is a deny rule: it cannot be overridden by trusted groups or user approval. Choose a different approach.`;
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Appended to a subagent child's system prompt. A child that does have a UI
|
|
78
|
+
* (an RPC child whose prompts surface in the parent) still costs the human an
|
|
79
|
+
* interruption per prompt, so it is told to revise before it escalates.
|
|
80
|
+
*/
|
|
81
|
+
/** The system prompt section that carries SUBAGENT_PERMISSIONS_PROMPT on Pi versions with a structured prompt. */
|
|
82
|
+
export const SUBAGENT_PROMPT_SECTION = "auto_permissions_subagent";
|
|
83
|
+
|
|
84
|
+
export const SUBAGENT_PERMISSIONS_PROMPT = `## Auto Permissions (subagent)
|
|
85
|
+
You are running as a subagent. Bash commands that Auto Permissions cannot approve on its own need a human, and asking one pauses the supervising session until they answer. Prefer commands that stay inside your task and working directory, and read-only alternatives where they answer the question. When Auto Permissions asks you to revise a command, change the approach instead of retrying it; re-run a command unchanged only when it is genuinely required and a human should decide.`;
|
|
86
|
+
|
|
87
|
+
/** What a subagent is told the first time a command would need human approval. */
|
|
88
|
+
export function subagentReviseFirstReason(gateLabel: string, reason: string): string {
|
|
89
|
+
return `${gateLabel} needs human approval: ${reason}\nYou are a subagent: asking the human pauses the supervising session until they answer. Revise first: get the same result with a command that stays inside your task and working directory (read-only, a narrower target, local instead of remote). If this exact command is genuinely required, run it again unchanged and the request goes to the human.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
function reviewCancelledResult(): BlockResult {
|
|
74
93
|
return { block: true, reason: "Auto Permissions review cancelled" };
|
|
75
94
|
}
|
|
@@ -169,6 +188,14 @@ function assistantToolCalls(message: unknown): AssistantToolCall[] {
|
|
|
169
188
|
export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
170
189
|
const overrides = createSessionOverrides(pi);
|
|
171
190
|
let trustedGroups = new Set<string>();
|
|
191
|
+
// Set per session: a subagent child (PI_SUBAGENT_CHILD=1) revises before it
|
|
192
|
+
// asks. `reviseFirst` maps each command it was told to revise to the turn
|
|
193
|
+
// it was told in; issuing it again unchanged in a *later* turn escalates it
|
|
194
|
+
// to the human, once: the entry goes with it. Same-turn duplicates (sibling calls, codemode Promise.all)
|
|
195
|
+
// are refused too, since the model has not yet seen the first refusal.
|
|
196
|
+
let subagentSession = false;
|
|
197
|
+
let turnGeneration = 0;
|
|
198
|
+
const reviseFirst = new Map<string, number>();
|
|
172
199
|
let lastConfigError: string | undefined;
|
|
173
200
|
let lastEvaluationLogError: string | undefined;
|
|
174
201
|
// Guardian calls run in parallel up to config.reviewConcurrency; applying
|
|
@@ -242,6 +269,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
242
269
|
verdict: DenialVerdict,
|
|
243
270
|
reason: string,
|
|
244
271
|
decisionSource: DenialSource,
|
|
272
|
+
reviseFirst = false,
|
|
245
273
|
): void {
|
|
246
274
|
const { ctx, config, gate, command, target } = scope;
|
|
247
275
|
try {
|
|
@@ -253,6 +281,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
253
281
|
verdict,
|
|
254
282
|
reason,
|
|
255
283
|
decisionSource,
|
|
284
|
+
...(reviseFirst ? { reviseFirst: true } : {}),
|
|
256
285
|
});
|
|
257
286
|
} catch {
|
|
258
287
|
// Event fan-out is observability, never part of the decision.
|
|
@@ -268,6 +297,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
268
297
|
verdict,
|
|
269
298
|
reason,
|
|
270
299
|
decisionSource,
|
|
300
|
+
reviseFirst,
|
|
271
301
|
}));
|
|
272
302
|
} catch {
|
|
273
303
|
// The denial log is best-effort observability.
|
|
@@ -277,7 +307,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
277
307
|
/** The one place a decision is recorded, rendered and returned. */
|
|
278
308
|
function settle(scope: ReviewScope, outcome: ReviewOutcome): BlockResult | undefined {
|
|
279
309
|
if (outcome.verdict && outcome.source) {
|
|
280
|
-
recordDenial(scope, outcome.verdict, outcome.reason, outcome.source);
|
|
310
|
+
recordDenial(scope, outcome.verdict, outcome.reason, outcome.source, outcome.reviseFirst === true);
|
|
281
311
|
}
|
|
282
312
|
if (outcome.display) {
|
|
283
313
|
display.show(scope, outcome.display, outcome.detail ?? outcome.reason, true);
|
|
@@ -520,6 +550,41 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
520
550
|
}
|
|
521
551
|
}
|
|
522
552
|
|
|
553
|
+
/**
|
|
554
|
+
* In a subagent with a UI, the first time a command would go to the human it
|
|
555
|
+
* is refused with a revise-first reason instead. Same-turn duplicates are
|
|
556
|
+
* refused too; an unchanged retry in a later turn returns undefined, and the
|
|
557
|
+
* caller asks the human.
|
|
558
|
+
*/
|
|
559
|
+
function reviseFirstBlock(
|
|
560
|
+
scope: ReviewScope,
|
|
561
|
+
reason: string,
|
|
562
|
+
source: "guardian" | "review_failure",
|
|
563
|
+
lifecycleSignal: AbortSignal,
|
|
564
|
+
): BlockResult | undefined {
|
|
565
|
+
if (!subagentSession || !scope.ctx.hasUI) return undefined;
|
|
566
|
+
// As askUser does: a refusal the model will never see must not count as
|
|
567
|
+
// its revise-first turn, or its retry would go straight to the human.
|
|
568
|
+
const cancelled = cancelledAfterAwait(scope, lifecycleSignal);
|
|
569
|
+
if (cancelled) return cancelled;
|
|
570
|
+
const key = `${scope.gate.label}\0${scope.command}`;
|
|
571
|
+
const refusedIn = reviseFirst.get(key);
|
|
572
|
+
if (refusedIn !== undefined && refusedIn !== turnGeneration) {
|
|
573
|
+
// Escalated now: a later identical attempt is revised first again.
|
|
574
|
+
reviseFirst.delete(key);
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
if (refusedIn === undefined) reviseFirst.set(key, turnGeneration);
|
|
578
|
+
return settle(scope, {
|
|
579
|
+
display: "revise",
|
|
580
|
+
verdict: "revise",
|
|
581
|
+
source,
|
|
582
|
+
reason,
|
|
583
|
+
block: subagentReviseFirstReason(scope.gate.label, reason),
|
|
584
|
+
reviseFirst: true,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
523
588
|
/** Apply one settled review. Runs holding the decision slot. */
|
|
524
589
|
function applyReview(
|
|
525
590
|
scope: ReviewScope,
|
|
@@ -527,7 +592,8 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
527
592
|
lifecycleSignal: AbortSignal,
|
|
528
593
|
): Promise<BlockResult | undefined> | BlockResult | undefined {
|
|
529
594
|
if (outcome.kind === "failed") {
|
|
530
|
-
|
|
595
|
+
const reason = `Automatic review failed: ${outcome.reason}`;
|
|
596
|
+
return reviseFirstBlock(scope, reason, "review_failure", lifecycleSignal) ?? askUser(scope, reason, lifecycleSignal, "review_failure");
|
|
531
597
|
}
|
|
532
598
|
const { verdict } = outcome;
|
|
533
599
|
if (verdict.decision === "approve") {
|
|
@@ -542,7 +608,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
542
608
|
block: `Auto Permissions requested revision: ${verdict.reason}\nRevise the command and try again.`,
|
|
543
609
|
});
|
|
544
610
|
}
|
|
545
|
-
return askUser(scope, verdict.reason, lifecycleSignal, "guardian");
|
|
611
|
+
return reviseFirstBlock(scope, verdict.reason, "guardian", lifecycleSignal) ?? askUser(scope, verdict.reason, lifecycleSignal, "guardian");
|
|
546
612
|
}
|
|
547
613
|
|
|
548
614
|
/**
|
|
@@ -677,6 +743,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
677
743
|
});
|
|
678
744
|
|
|
679
745
|
pi.on("turn_end", () => {
|
|
746
|
+
turnGeneration += 1;
|
|
680
747
|
lastAssistantCalls = [];
|
|
681
748
|
abandonSiblingReviews();
|
|
682
749
|
endScripts();
|
|
@@ -755,6 +822,25 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
755
822
|
return decide(scope, input, lifecycleSignal, early ?? runReview(scope, input, lifecycleSignal));
|
|
756
823
|
});
|
|
757
824
|
|
|
825
|
+
pi.on("before_agent_start", (event) => {
|
|
826
|
+
if (!subagentSession) return;
|
|
827
|
+
try {
|
|
828
|
+
if (!loadAutoPermissionsConfig().enabled) return;
|
|
829
|
+
} catch {
|
|
830
|
+
// An invalid config fails closed on the first bash call; the guidance still applies.
|
|
831
|
+
}
|
|
832
|
+
// On Pi 1.x a returned systemPrompt forces the whole prompt for the run,
|
|
833
|
+
// dropping what later extensions change in its options (pi-agents adds
|
|
834
|
+
// the parent's instruction files there), so the guidance goes into a
|
|
835
|
+
// named section instead. Older Pi takes the prompt as a string.
|
|
836
|
+
const sections = (event as { systemPromptOptions?: { sections?: Record<string, string> } }).systemPromptOptions?.sections;
|
|
837
|
+
if (sections) {
|
|
838
|
+
sections[SUBAGENT_PROMPT_SECTION] = SUBAGENT_PERMISSIONS_PROMPT;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${SUBAGENT_PERMISSIONS_PROMPT}` };
|
|
842
|
+
});
|
|
843
|
+
|
|
758
844
|
registerSettingsCommand(pi, { overrides, reviewer });
|
|
759
845
|
|
|
760
846
|
function resetCallTracking(): void {
|
|
@@ -782,6 +868,8 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
782
868
|
if (config) warnAboutMissingReviewerProvider(ctx, config);
|
|
783
869
|
|
|
784
870
|
resetCallTracking();
|
|
871
|
+
subagentSession = detectSubagentContext(ctx.cwd) !== undefined;
|
|
872
|
+
reviseFirst.clear();
|
|
785
873
|
reviewer.startSession(ctx.cwd);
|
|
786
874
|
overrides.restore(ctx.sessionManager.getBranch());
|
|
787
875
|
trustedGroups = ctx.isProjectTrusted() ? loadTrustedGroups(ctx.cwd) : new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-auto-permissions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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": [
|
package/prompt-select.ts
CHANGED
|
@@ -22,6 +22,10 @@ export function setHerdrBlocked(pi: ExtensionAPI, active: boolean, label?: strin
|
|
|
22
22
|
* idempotent and performs its own teardown, so nothing else is needed)
|
|
23
23
|
* - a host that reports `hasUI` but cannot render custom components
|
|
24
24
|
*
|
|
25
|
+
* In RPC mode (`hasUI` is true but `custom()` returns undefined) the prompt
|
|
26
|
+
* goes through `ctx.ui.select`, which the RPC client renders: that is how a
|
|
27
|
+
* subagent child's approval reaches the human in the parent session.
|
|
28
|
+
*
|
|
25
29
|
* A Tab-typed note is delivered to the agent as a steering user message,
|
|
26
30
|
* matching what the retired `ExtensionSelectorComponent` monkey patch in
|
|
27
31
|
* pi-permission-selector did.
|
|
@@ -33,6 +37,7 @@ export function promptSelect(
|
|
|
33
37
|
values: string[],
|
|
34
38
|
signal: AbortSignal,
|
|
35
39
|
): Promise<string | undefined> {
|
|
40
|
+
if (ctx.mode === "rpc") return ctx.ui.select(title, values, { signal });
|
|
36
41
|
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
|
|
37
42
|
let finished = false;
|
|
38
43
|
const onAbort = () => finish(undefined);
|
package/review.ts
CHANGED
|
@@ -174,13 +174,24 @@ Examples:
|
|
|
174
174
|
Return strict JSON only with this shape:
|
|
175
175
|
{"decision":"approve"|"revise"|"ask_user","reason":"one concise sentence"}`;
|
|
176
176
|
|
|
177
|
+
/** What an "ask_user" verdict does in a subagent, which depends on whether its prompts can reach a human. */
|
|
178
|
+
const SUBAGENT_ASK_USER = {
|
|
179
|
+
withUI: `An "ask_user" decision does not reach a human directly: the subagent is first told to revise the command, and the step stays blocked unless a human later approves the same command.`,
|
|
180
|
+
withoutUI: `An "ask_user" decision cannot reach a human: this subagent has no interactive user, so the command is blocked outright and the subagent must route around it or report the blocker.`,
|
|
181
|
+
};
|
|
182
|
+
|
|
177
183
|
/**
|
|
178
184
|
* Appended to the reviewer system prompt only when the session is a subagent
|
|
179
185
|
* child (detectSubagentContext). Facts arrive in the request's "execution"
|
|
180
|
-
* object; this section tells the reviewer how to weigh them.
|
|
186
|
+
* object; this section tells the reviewer how to weigh them. Whether the child
|
|
187
|
+
* has a UI is fixed for its session, so the prompt stays cacheable.
|
|
181
188
|
*/
|
|
182
|
-
export
|
|
183
|
-
|
|
189
|
+
export function subagentContextSystemPrompt(hasUI: boolean): string {
|
|
190
|
+
return SUBAGENT_CONTEXT_TEMPLATE.replace("{ASK_USER}", hasUI ? SUBAGENT_ASK_USER.withUI : SUBAGENT_ASK_USER.withoutUI);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const SUBAGENT_CONTEXT_TEMPLATE = `SUBAGENT EXECUTION CONTEXT
|
|
194
|
+
This session is an autonomous subagent executing a task delegated by a supervising agent session. {ASK_USER} The latest proposed action carries an "execution" object with runtime facts (isolated worktree, checked-out branch, nesting depth).
|
|
184
195
|
The delegated task arrives as this session's USER evidence, but it was authored by the supervising agent, not typed by a human. Delegation authorizes medium-risk work scoped to the subagent's own workspace. It does not blanket-authorize higher risk: approve a high- or critical-risk operation only when the delegated task names the exact operation and target — the same explicit-intent standard a human user's message is held to — and treat irreversible destruction outside the subagent's scope as never authorized by delegation alone, whatever the task's general wording.
|
|
185
196
|
Judge risk by effect scope and reversibility relative to the subagent's own workspace, not by command name:
|
|
186
197
|
- Mutations confined to the subagent's isolated worktree, or to resources the evidence shows the subagent created (scratch files, branches it created or first pushed, containers, images, test databases), are at most medium risk; approve them when they serve the delegated task.
|
package/settings-command.ts
CHANGED
|
@@ -136,7 +136,7 @@ export function registerSettingsCommand(
|
|
|
136
136
|
ts: record.ts,
|
|
137
137
|
gateLabel: record.gate.label,
|
|
138
138
|
command: record.command,
|
|
139
|
-
verdict: record.verdict,
|
|
139
|
+
verdict: record.reviseFirst ? "revise first" : record.verdict,
|
|
140
140
|
reason: record.reason,
|
|
141
141
|
}));
|
|
142
142
|
} catch {
|
package/subagent-context.ts
CHANGED
|
@@ -4,16 +4,16 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
/**
|
|
5
5
|
* Runtime facts about a subagent child session, forwarded to the guardian as
|
|
6
6
|
* evidence. Facts only — the guardian judges what they imply; see
|
|
7
|
-
*
|
|
7
|
+
* subagentContextSystemPrompt() in review.ts.
|
|
8
8
|
*
|
|
9
|
-
* Detection is based on the environment contract
|
|
10
|
-
* (nicobailon/pi-subagents)
|
|
11
|
-
* plus run id and nesting depth. Absent that env,
|
|
12
|
-
* ordinary sessions and review behavior is unchanged.
|
|
9
|
+
* Detection is based on the environment contract that `@hank-warren/pi-agents`
|
|
10
|
+
* and `pi-subagents` (nicobailon/pi-subagents) both use: children are spawned
|
|
11
|
+
* with `PI_SUBAGENT_CHILD=1` plus run id and nesting depth. Absent that env,
|
|
12
|
+
* sessions are treated as ordinary sessions and review behavior is unchanged.
|
|
13
13
|
*/
|
|
14
14
|
export interface SubagentExecutionContext {
|
|
15
15
|
subagent: true;
|
|
16
|
-
/**
|
|
16
|
+
/** The subagent's run id (PI_SUBAGENT_RUN_ID), when present. */
|
|
17
17
|
runId?: string;
|
|
18
18
|
/** Nesting depth for nested subagents (PI_SUBAGENT_DEPTH), when > 0. */
|
|
19
19
|
depth?: number;
|