@henryqw/pi-subagent 23.0.1 → 24.0.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/CONTEXT.md +1 -1
- package/README.md +6 -4
- package/dist/herdr-runtime.d.ts +1 -0
- package/dist/herdr-runtime.js +50 -4
- package/dist/index.d.ts +5 -6
- package/dist/index.js +37 -59
- package/dist/launch-runtime.d.ts +0 -2
- package/dist/launch-runtime.js +2 -9
- package/dist/mcp-role.d.ts +0 -1
- package/dist/mcp-role.js +0 -11
- package/dist/runner.d.ts +1 -0
- package/dist/runner.js +152 -53
- package/dist/schema.d.ts +9 -8
- package/dist/schema.js +6 -6
- package/dist/worktree.d.ts +0 -2
- package/dist/worktree.js +0 -4
- package/docs/orchestration.md +4 -2
- package/extensions/admission.ts +23 -31
- package/extensions/config.ts +2 -8
- package/extensions/isolated.ts +1 -2
- package/extensions/result-transport.ts +15 -32
- package/extensions/role-mcp.ts +3 -7
- package/extensions/role-tools.ts +2 -8
- package/extensions/subagent-command.ts +5 -5
- package/extensions/subagent.ts +14 -25
- package/extensions/workflow.ts +0 -1
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +2 -2
- package/dist/working-evidence.d.ts +0 -25
- package/dist/working-evidence.js +0 -261
package/CONTEXT.md
CHANGED
|
@@ -76,7 +76,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
76
76
|
- Recognized Pi JSON events renew idle timeout; raw bytes do not. Child maximum runtime always terminates ephemeral children.
|
|
77
77
|
- Output, stderr, protocol events, callback draining, and inherited descendant streams are bounded.
|
|
78
78
|
- Low-level worktree finalization returns explicit `pruned`, `retained`, or `recovery` evidence and never force-deletes uncertain work.
|
|
79
|
-
- Exact review
|
|
79
|
+
- Exact review helpers produce bounded private evidence; callers must preserve identity guards and verdict validation.
|
|
80
80
|
|
|
81
81
|
## Owned storage
|
|
82
82
|
|
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ An isolated request has one durable ID, one goal, and 1–8 typed tasks:
|
|
|
113
113
|
"modelClass": "balanced",
|
|
114
114
|
"requirements": "Implement the smallest correct repair.",
|
|
115
115
|
"deliverable": "A committed candidate in the owned worktree.",
|
|
116
|
-
"dependsOn": [
|
|
116
|
+
"dependsOn": [],
|
|
117
117
|
"contextFrom": ["inspect"],
|
|
118
118
|
"checks": [
|
|
119
119
|
{ "command": "pnpm", "args": ["test", "--", "refresh"] }
|
|
@@ -131,7 +131,7 @@ An isolated request has one durable ID, one goal, and 1–8 typed tasks:
|
|
|
131
131
|
}
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
`dependsOn` controls scheduling. `contextFrom`
|
|
134
|
+
`dependsOn` controls scheduling. `contextFrom` also schedules dependencies and carries completed text outputs in the declared order. Do not repeat the same task in both fields. Changesets require focused task checks; a graph with changesets requires final checks. A canonical root `package.json` test script is required: the runner adds `pnpm test` to final checks if missing, and Main runs it on the combined integration checkout before promotion. Checks and judgments bind to exact Git identities; candidate drift, Main drift, mutation during validation, ambiguity, or conflicts stop promotion and retain evidence.
|
|
135
135
|
|
|
136
136
|
`delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after the wave is saved; productive work continues while Main is free. Main can do other work or end its turn instead of sleeping or polling. Pi sends a follow-up in the launching session whenever a checked worker becomes ready or a worker needs attention, and again when the wave finishes. A worker whose exact turn ends without a changed clean commit—including a failed or aborted turn—needs attention, not an inferred candidate; use `subagent_status` for its saved evidence. If delivery is missed, inspect `subagent_status`.
|
|
137
137
|
|
|
@@ -147,9 +147,11 @@ A queued follow-up runs after the current turn settles and must produce a new cl
|
|
|
147
147
|
|
|
148
148
|
After a coordinator interruption, run `/subagent recover` once. It acquires the repository productive lease before changing any orphaned `pending` or `running` state, preserves unreadable state files and retained candidates, and reports blockers and existing continuations to Main without replaying prompts, merges, promotion, or cleanup. If another productive run holds the lease, it only reads status and reports the live owner; wait rather than taking over. Repeating the command is safe. Main uses `subagent_status` for exact identities and the recovery tools in the interface table above for deliberate decisions; the command never stages or promotes. Direct Herdr tab recovery remains in `/subagent`.
|
|
149
149
|
|
|
150
|
+
If an isolated worker produced no candidate, `subagent_abort` terminates it and removes only proven unchanged, clean owned resources. Cleanup includes ignored-file checks and never discards unique commits. If productive work is still settling, termination is uncertain, or cleanup fails, the request stays aborted with retained-resource evidence. Inspect `subagent_status`, resolve the reported blocker, then repeat `subagent_abort` to reconcile cleanup; it does not replay worker prompts. Dirty work, unique commits, and uncertain allocations remain for deliberate manual recovery.
|
|
151
|
+
|
|
150
152
|
The following details describe exact identity and retained-work requirements.
|
|
151
153
|
|
|
152
|
-
A checked candidate stays retained if Main is dirty. Staging, combined validation, and promotion require Main to be clean at the pinned committed identity; clean your local edits deliberately before using those actions. No dirty work is replayed or merged automatically. Pass the exact `generation`, candidate (`taskId`, `attempt`, `candidate`) and `expectedTip` identities reported by status to stage; pass the generation and combined `expectedTip` to integrate. If Main advances cleanly before promotion, call `subagent_integrate refresh` with the recorded `expectedMain`, old `expectedTip`, and new clean `newMain`. Main must restage selected immutable candidates into the new generation and rerun combined checks. Dirty or divergent Main blocks refresh. Stale identities fail. If integration allocation was uncertain, `subagent_stage resolve` first proves the owned checkout is still at its base before starting the selected merge; otherwise it retains the intent. At most two integration worktrees may remain unreleased per request; a superseded generation is read-only. To release a superseded *clean* integration checkout, call `subagent_integrate` with `{ id, action: "release", generation, expectedTip }`, using that generation's last staged tip (or integration base when no stage completed). To release a rejected candidate after its worker has terminated and all generations using it are released, pass `{ id, action: "release", generation: <latest generation number, or 1>, taskId, attempt, expectedTip: <candidate tip> }`. Release persists each host/checkout/branch step, verifies ownership and cleanliness (including ignored files), and deletes the branch only if its ref still names the exact tip. Dirty, conflicted, changed, or uncertain resources remain for manual inspection; retry the same release after resolving the obstacle. Released checkouts free a retained slot; abort is available after every retained resource is released. Rejection can invalidate dependent work; if fresh dependent execution is impossible, start a new request instead of forcing promotion. Productive requests have no whole-run wall-clock deadline. Resume does not reset the recorded policy or correction count. Child limits, abort signals, process I/O, status inspection, exact termination, and cleanup retain finite safety bounds.
|
|
154
|
+
A checked candidate stays retained if Main is dirty. Staging, combined validation, and promotion require Main to be clean at the pinned committed identity; clean your local edits deliberately before using those actions. No dirty work is replayed or merged automatically. Pass the exact `generation`, candidate (`taskId`, `attempt`, `candidate`) and `expectedTip` identities reported by status to stage; pass the generation and combined `expectedTip` to integrate. If Main advances cleanly before promotion, call `subagent_integrate refresh` with the recorded `expectedMain`, old `expectedTip`, and new clean `newMain`. Main must restage selected immutable candidates into the new generation and rerun combined checks. Dirty or divergent Main blocks refresh. Stale identities fail. If integration allocation was uncertain, `subagent_stage resolve` first proves the owned checkout is still at its base before starting the selected merge; otherwise it retains the intent. At most two integration worktrees may remain unreleased per request; a superseded generation is read-only. To release a superseded *clean* integration checkout, call `subagent_integrate` with `{ id, action: "release", generation, expectedTip }`, using that generation's last staged tip (or integration base when no stage completed). To release a rejected candidate after its worker has terminated and all generations using it are released, pass `{ id, action: "release", generation: <latest generation number, or 1>, taskId, attempt, expectedTip: <candidate tip> }`. Release persists each host/checkout/branch step, verifies ownership and cleanliness (including ignored files), and deletes the branch only if its ref still names the exact tip. Dirty, conflicted, changed, or uncertain resources remain for manual inspection; retry the same release after resolving the obstacle. Released checkouts free a retained slot; abort is available after every retained resource is released. Rejection can invalidate dependent work; if fresh dependent execution is impossible, start a new request instead of forcing promotion. Productive requests have no whole-run wall-clock deadline. Resume does not reset the recorded policy or correction count. Text tasks have two dispatch attempts; inspection failures before dispatch do not consume one. Verification requires passing preliminary checks and any required judgment; it cannot override failed review. Child limits, abort signals, process I/O, status inspection, exact termination, and cleanup retain finite safety bounds.
|
|
153
155
|
|
|
154
156
|
The extension never pushes, opens a pull request, publishes, deploys, force-cleans recoverable work, or silently falls back to Main.
|
|
155
157
|
|
|
@@ -197,7 +199,7 @@ Children disable ambient extension and Skill discovery. Only declared resources
|
|
|
197
199
|
| `prepareRoleLaunch`, `finalizeRoleLaunch` | functions | Prepare Role prompts, arguments, and tool policy. |
|
|
198
200
|
| `createEphemeralSubagentExecutor` | function | Run bounded no-session Pi children through a FIFO pool. |
|
|
199
201
|
| `createChildWorktree`, `finalizeChildWorktree` | functions | Create and conservatively finalize child worktrees. |
|
|
200
|
-
| `prepareExactReviewEvidence
|
|
202
|
+
| `prepareExactReviewEvidence` | function | Prepare private exact base-to-tip review evidence. |
|
|
201
203
|
| `ExecuteRequestSchema`, `StageRequestSchema`, `IntegrationActionSchema` | schemas | Validate checked request and integration inputs. |
|
|
202
204
|
| `IsolatedRunner`, `FileRunStore` | classes | Run checked work and persist durable request state. |
|
|
203
205
|
| `RoleLaunchRuntime`, `HerdrHostRuntime`, `CheckedGitRuntime` | classes | Supply launch, host, and Git behavior to the runner. |
|
package/dist/herdr-runtime.d.ts
CHANGED
package/dist/herdr-runtime.js
CHANGED
|
@@ -643,9 +643,41 @@ export class HerdrHostRuntime {
|
|
|
643
643
|
return { outcome: "unknown", failure: safeText(error) };
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
|
+
async proveAbsentAgentForCleanup(allocation, attempt, context) {
|
|
647
|
+
requireIntentIdentity(allocation, attempt);
|
|
648
|
+
assertAgentIntent(allocation, attempt);
|
|
649
|
+
this.assertLeasePath(allocation.leasePath, attempt.correlationToken);
|
|
650
|
+
await this.assertPrivateLeaseDirectories(allocation.leasePath, true);
|
|
651
|
+
await this.assertPrivateLease(allocation.leasePath, true);
|
|
652
|
+
const agents = (await this.listAgents(allocation.worktreeCwd, context)).map((agent) => ({
|
|
653
|
+
name: typeof agent.name === "string" ? agent.name : undefined,
|
|
654
|
+
paneId: exactString(agent.pane_id, "Herdr listed agent pane ID"),
|
|
655
|
+
tabId: exactString(agent.tab_id, "Herdr listed agent tab ID"),
|
|
656
|
+
}));
|
|
657
|
+
if (agents.some((agent) => agent.name === allocation.agentName
|
|
658
|
+
|| agent.paneId === allocation.paneId || agent.tabId === allocation.tabId)) {
|
|
659
|
+
throw new Error("The exact absent-agent allocation now has a matching Herdr agent.");
|
|
660
|
+
}
|
|
661
|
+
const paneIsAbsent = await this.paneAbsent(allocation.paneId, allocation.worktreeCwd, context);
|
|
662
|
+
if (!paneIsAbsent)
|
|
663
|
+
await this.assertStartableAgentPane(allocation, context, { requireExclusiveTty: true });
|
|
664
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, undefined, true)).length) {
|
|
665
|
+
throw new Error("The absent-agent process lease still has holders.");
|
|
666
|
+
}
|
|
667
|
+
await this.delay(50, context.signal);
|
|
668
|
+
if ((await this.scanLease(allocation.leasePath, allocation.worktreeCwd, context, undefined, true)).length) {
|
|
669
|
+
throw new Error("The absent-agent process lease did not remain empty for two consecutive scans.");
|
|
670
|
+
}
|
|
671
|
+
return paneIsAbsent;
|
|
672
|
+
}
|
|
646
673
|
async cleanupHost(input, context) {
|
|
647
|
-
|
|
648
|
-
|
|
674
|
+
const terminationProven = input.attempt.termination?.status === "terminated";
|
|
675
|
+
const agentIntents = input.attempt.allocations.filter((intent) => intent.kind === "agent");
|
|
676
|
+
const latestAgent = agentIntents.at(-1);
|
|
677
|
+
const neverOwned = !input.attempt.termination && latestAgent?.status === "absent"
|
|
678
|
+
&& agentIntents.every((intent) => intent.status === "absent");
|
|
679
|
+
if (!terminationProven && !neverOwned) {
|
|
680
|
+
return { outcome: "blocked", failure: "Host cleanup requires exact worker termination or a definitive absent-agent allocation." };
|
|
649
681
|
}
|
|
650
682
|
try {
|
|
651
683
|
if (input.kind === "worker_tab") {
|
|
@@ -663,8 +695,19 @@ export class HerdrHostRuntime {
|
|
|
663
695
|
if (tabId === allocation.workspaceRootTabId)
|
|
664
696
|
throw new Error("Saved worker tab aliases the workspace root tab.");
|
|
665
697
|
const paneId = exactString(allocation.paneId, "saved worker pane ID");
|
|
666
|
-
|
|
667
|
-
|
|
698
|
+
const paneIsAbsent = neverOwned
|
|
699
|
+
? await this.proveAbsentAgentForCleanup(latestAgent, input.attempt, context)
|
|
700
|
+
: await this.paneAbsent(paneId, allocation.worktreeCwd, context);
|
|
701
|
+
if (!paneIsAbsent) {
|
|
702
|
+
if (!neverOwned)
|
|
703
|
+
return { outcome: "blocked", failure: "The exact saved worker pane still exists after termination." };
|
|
704
|
+
const closed = await this.herdr.exec(["pane", "close", paneId], this.processOptions(allocation.worktreeCwd, context, HERDR_OPERATION_CAP_MS));
|
|
705
|
+
if (closed.code !== 0 || closed.killed)
|
|
706
|
+
return { outcome: "blocked", failure: safeText(herdrCommandFailure(["pane", "close"], closed)) };
|
|
707
|
+
requireOkResponse(closed.stdout, "Herdr pane close response");
|
|
708
|
+
if (!await this.paneAbsent(paneId, allocation.worktreeCwd, context)) {
|
|
709
|
+
return { outcome: "blocked", failure: "The exact unstarted worker pane still exists after close." };
|
|
710
|
+
}
|
|
668
711
|
}
|
|
669
712
|
const workspaceIsAbsent = await this.workspaceAbsent(workspaceId, workspace, context);
|
|
670
713
|
const tab = await this.getTab(tabId, allocation.worktreeCwd, context);
|
|
@@ -686,6 +729,9 @@ export class HerdrHostRuntime {
|
|
|
686
729
|
const workerCleanup = input.attempt.cleanup.find((step) => step.kind === "worker_tab");
|
|
687
730
|
if (workerCleanup?.status !== "completed")
|
|
688
731
|
return { outcome: "blocked", failure: "Workspace cleanup must follow worker-tab reconciliation." };
|
|
732
|
+
if (neverOwned && !await this.proveAbsentAgentForCleanup(latestAgent, input.attempt, context)) {
|
|
733
|
+
return { outcome: "blocked", failure: "The exact unstarted worker pane remains after worker-tab cleanup." };
|
|
734
|
+
}
|
|
689
735
|
const allocation = ownedIntent(input.attempt, "workspace");
|
|
690
736
|
requireIntentIdentity(allocation, input.attempt);
|
|
691
737
|
assertWorkspaceIntent(allocation, input.attempt);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
3
3
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
|
|
4
|
-
export { parseRoleMcpAllowlist,
|
|
4
|
+
export { parseRoleMcpAllowlist, selectRoleMcpConfig, type RoleMcpConfig } from "./mcp-role.ts";
|
|
5
5
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutionBudget, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
6
|
-
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError,
|
|
6
|
+
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
7
7
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
|
8
|
-
export { captureWorkingCheckoutBaseline, captureWorkingCheckoutState, prepareWorkingChangeEvidence, sameWorkingCheckoutState, sameWorkingSnapshot, type PreparedWorkingChangeEvidence, type WorkingCheckoutBaseline, type WorkingCheckoutState, type WorkingSnapshotIdentity, } from "./working-evidence.ts";
|
|
9
8
|
export declare const PI_SUBAGENT_PROCESS_LEASE = "PI_SUBAGENT_PROCESS_LEASE";
|
|
10
9
|
export declare const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
|
|
10
|
+
/** Read child MCP policy before Pi binds registered extension flag values. */
|
|
11
|
+
export declare function roleMcpAllowlistFromArgv(args: readonly string[]): string[];
|
|
11
12
|
export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
12
|
-
export declare const EXECUTION_BUDGET_FLAG = "pi-subagent-execution-budget";
|
|
13
13
|
export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "ask_question", "subagent_status", "subagent_resume", "subagent_stage", "subagent_abort"];
|
|
14
|
-
export declare const CHILD_EXCLUDED_TOOLS: string;
|
|
15
14
|
export declare const DELEGATE_TASK: {
|
|
16
15
|
readonly id: "pi-subagent/delegateTask";
|
|
17
16
|
readonly label: "Subagent delegation";
|
|
@@ -91,7 +90,7 @@ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">,
|
|
|
91
90
|
export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: CreateRoleLaunchInput): PreparedRoleLaunch;
|
|
92
91
|
/** Resolve and prepare a configured Role with its package-owned resources. */
|
|
93
92
|
export declare function resolveConfiguredRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveConfiguredRoleLaunchInput): Promise<PreparedRoleLaunch>;
|
|
94
|
-
/**
|
|
93
|
+
/** Insert the system prompt pair after a caller has prepared its launch argv. */
|
|
95
94
|
export declare function finalizeRoleLaunch(prepared: PreparedRoleLaunch): ResolvedRoleLaunch;
|
|
96
95
|
export * from "./composition.ts";
|
|
97
96
|
export * from "./git-runtime.ts";
|
package/dist/index.js
CHANGED
|
@@ -4,22 +4,28 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { DefaultPackageManager, getAgentDir, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
6
6
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
7
|
-
import { selectRoleMcpConfig } from "./mcp-role.js";
|
|
7
|
+
import { parseRoleMcpAllowlist, selectRoleMcpConfig } from "./mcp-role.js";
|
|
8
8
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
9
9
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
10
|
-
export { parseRoleMcpAllowlist,
|
|
10
|
+
export { parseRoleMcpAllowlist, selectRoleMcpConfig } from "./mcp-role.js";
|
|
11
11
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
12
|
-
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError,
|
|
12
|
+
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, } from "./worktree.js";
|
|
13
13
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
14
|
-
export { captureWorkingCheckoutBaseline, captureWorkingCheckoutState, prepareWorkingChangeEvidence, sameWorkingCheckoutState, sameWorkingSnapshot, } from "./working-evidence.js";
|
|
15
14
|
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
|
16
15
|
const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
|
|
17
16
|
const ROLE_MCP_EXTENSION = fileURLToPath(new URL("../extensions/role-mcp.ts", import.meta.url));
|
|
18
17
|
const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
|
|
19
18
|
export const PI_SUBAGENT_PROCESS_LEASE = "PI_SUBAGENT_PROCESS_LEASE";
|
|
20
19
|
export const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
|
|
20
|
+
/** Read child MCP policy before Pi binds registered extension flag values. */
|
|
21
|
+
export function roleMcpAllowlistFromArgv(args) {
|
|
22
|
+
const flag = `--${ROLE_MCP_POLICY_FLAG}`;
|
|
23
|
+
const indexes = args.flatMap((arg, index) => arg === flag ? [index] : []);
|
|
24
|
+
if (indexes.length !== 1)
|
|
25
|
+
throw new Error(`${flag} must appear exactly once.`);
|
|
26
|
+
return parseRoleMcpAllowlist(args[indexes[0] + 1]);
|
|
27
|
+
}
|
|
21
28
|
export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
22
|
-
export const EXECUTION_BUDGET_FLAG = "pi-subagent-execution-budget";
|
|
23
29
|
export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
24
30
|
"delegate_task",
|
|
25
31
|
"ask_question",
|
|
@@ -28,7 +34,6 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
|
28
34
|
"subagent_stage",
|
|
29
35
|
"subagent_abort",
|
|
30
36
|
];
|
|
31
|
-
export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
|
|
32
37
|
const SYSTEM_PROMPT_FLAG = "--append-system-prompt";
|
|
33
38
|
const CHILD_IDENTITY_POLICY = "You are a delegated Pi Subagent, not Main. Execute the assigned Role and task directly. Main-only delegation rules do not apply. Recursive delegation is unavailable; do not seek or invoke delegation tools.";
|
|
34
39
|
export const DELEGATE_TASK = {
|
|
@@ -238,9 +243,10 @@ export async function resolveRolePackageResources(role, ctx) {
|
|
|
238
243
|
};
|
|
239
244
|
}
|
|
240
245
|
export function createRoleLaunch(pi, ctx, input) {
|
|
241
|
-
return
|
|
246
|
+
return finalizeRoleLaunch(prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role)));
|
|
242
247
|
}
|
|
243
|
-
|
|
248
|
+
/** Build the prompt-free argv and tool policy; the Role prompt stays separate until `finalizeRoleLaunch`. */
|
|
249
|
+
function prepareRoleLaunchFromSkills(ctx, input, skills) {
|
|
244
250
|
const role = input.role;
|
|
245
251
|
const mcps = mcpList(role.mcps, `Role ${role.name}`);
|
|
246
252
|
const tools = roleToolPolicy(role, input.tools);
|
|
@@ -262,7 +268,7 @@ function createRoleLaunchFromSkills(ctx, input, skills) {
|
|
|
262
268
|
throw new Error(`Invalid launch environment value: ${key}`);
|
|
263
269
|
return [key, value];
|
|
264
270
|
}));
|
|
265
|
-
const args = ["--no-session", "--no-extensions", "--no-skills", "--exclude-tools",
|
|
271
|
+
const args = ["--no-session", "--no-extensions", "--no-skills", "--exclude-tools", CHILD_EXCLUDED_TOOL_NAMES.join(",")];
|
|
266
272
|
if (mcps.length)
|
|
267
273
|
args.push(`--${ROLE_MCP_POLICY_FLAG}`, JSON.stringify(mcps));
|
|
268
274
|
for (const extension of new Set(extensions))
|
|
@@ -274,46 +280,30 @@ function createRoleLaunchFromSkills(ctx, input, skills) {
|
|
|
274
280
|
if (input.route.thinkingLevel)
|
|
275
281
|
args.push("--thinking", input.route.thinkingLevel);
|
|
276
282
|
args.push(ctx.isProjectTrusted() ? "--approve" : "--no-approve");
|
|
277
|
-
args.push(SYSTEM_PROMPT_FLAG, `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`);
|
|
278
283
|
return {
|
|
279
284
|
env,
|
|
280
285
|
args,
|
|
281
286
|
model: input.route.model,
|
|
282
287
|
thinkingLevel: input.route.thinkingLevel,
|
|
283
288
|
missingSkills: skills.missing,
|
|
289
|
+
role: role.name,
|
|
290
|
+
tools: Object.freeze(tools),
|
|
291
|
+
systemPrompt: `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`,
|
|
292
|
+
promptArgIndex: args.length,
|
|
284
293
|
};
|
|
285
294
|
}
|
|
286
|
-
|
|
295
|
+
function prepareResolvedRoleLaunch(pi, ctx, input) {
|
|
287
296
|
const { task, modelClass, agentDir, ...launchInput } = input;
|
|
288
297
|
const selectedClass = modelClass ?? input.role.modelClass;
|
|
289
|
-
return
|
|
298
|
+
return prepareRoleLaunchFromSkills(ctx, {
|
|
290
299
|
...launchInput,
|
|
291
300
|
route: selectedClass === undefined
|
|
292
301
|
? resolveConfiguredTaskRoute(ctx, task, agentDir)
|
|
293
302
|
: resolveTaskRoute(ctx, selectedClass, agentDir),
|
|
294
|
-
});
|
|
303
|
+
}, resolveRoleSkills(pi, input.role));
|
|
295
304
|
}
|
|
296
|
-
function
|
|
297
|
-
|
|
298
|
-
if (indexes.length !== 1)
|
|
299
|
-
throw new Error(`Role launch must contain exactly one ${SYSTEM_PROMPT_FLAG} pair.`);
|
|
300
|
-
const promptArgIndex = indexes[0];
|
|
301
|
-
const systemPrompt = rawArgs[promptArgIndex + 1];
|
|
302
|
-
if (typeof systemPrompt !== "string" || !systemPrompt.includes("\n") || !systemPrompt.trim() || systemPrompt.includes("\0")) {
|
|
303
|
-
throw new Error(`Role ${SYSTEM_PROMPT_FLAG} value must be the exact multiline Role prompt.`);
|
|
304
|
-
}
|
|
305
|
-
const args = [...rawArgs.slice(0, promptArgIndex), ...rawArgs.slice(promptArgIndex + 2)];
|
|
306
|
-
if (args.includes(SYSTEM_PROMPT_FLAG) || args.includes(systemPrompt)) {
|
|
307
|
-
throw new Error("Sanitized Role argv must contain no prompt flag or raw Role prompt.");
|
|
308
|
-
}
|
|
309
|
-
return { args, systemPrompt, promptArgIndex };
|
|
310
|
-
}
|
|
311
|
-
function prepareResolvedRoleLaunch(roleDefinition, launch, additionalTools = []) {
|
|
312
|
-
const role = parseRoleName(roleDefinition.name);
|
|
313
|
-
rejectRetiredRoleIsolation(roleDefinition.isolation, `Role ${role}`);
|
|
314
|
-
const tools = Object.freeze(roleToolPolicy(roleDefinition, additionalTools));
|
|
315
|
-
const { args, systemPrompt, promptArgIndex } = stripRoleSystemPrompt(launch.args);
|
|
316
|
-
return { ...launch, args, role, tools, systemPrompt, promptArgIndex };
|
|
305
|
+
export function resolveRoleLaunch(pi, ctx, input) {
|
|
306
|
+
return finalizeRoleLaunch(prepareResolvedRoleLaunch(pi, ctx, input));
|
|
317
307
|
}
|
|
318
308
|
function assertNoMissingRoleSkills(role, launch) {
|
|
319
309
|
if (!launch.missingSkills.length)
|
|
@@ -321,11 +311,13 @@ function assertNoMissingRoleSkills(role, launch) {
|
|
|
321
311
|
throw new Error(`Role ${parseRoleName(role.name)} requires missing Skills: ${launch.missingSkills.join(", ")}.`);
|
|
322
312
|
}
|
|
323
313
|
export function prepareRoleLaunch(pi, ctx, input) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
314
|
+
const role = parseRoleName(input.role.name);
|
|
315
|
+
rejectRetiredRoleIsolation(input.role.isolation, `Role ${role}`);
|
|
316
|
+
const prepared = "route" in input
|
|
317
|
+
? prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role))
|
|
318
|
+
: prepareResolvedRoleLaunch(pi, ctx, input);
|
|
319
|
+
assertNoMissingRoleSkills(input.role, prepared);
|
|
320
|
+
return { ...prepared, role };
|
|
329
321
|
}
|
|
330
322
|
/** Resolve and prepare a configured Role with its package-owned resources. */
|
|
331
323
|
export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
|
|
@@ -347,36 +339,22 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
|
|
|
347
339
|
...namedSkills,
|
|
348
340
|
paths: [...new Set([...namedSkills.paths, ...resources.skills])],
|
|
349
341
|
};
|
|
350
|
-
const
|
|
342
|
+
const prepared = prepareRoleLaunchFromSkills(ctx, {
|
|
351
343
|
role: effectiveRole,
|
|
352
344
|
route: resolveTaskRoute(ctx, input.modelClass),
|
|
353
345
|
}, skills);
|
|
354
|
-
assertNoMissingRoleSkills(effectiveRole,
|
|
355
|
-
const
|
|
346
|
+
assertNoMissingRoleSkills(effectiveRole, prepared);
|
|
347
|
+
const args = [
|
|
348
|
+
...prepared.args,
|
|
356
349
|
"--no-prompt-templates",
|
|
357
350
|
"--no-themes",
|
|
358
351
|
...resources.prompts.flatMap((path) => ["--prompt-template", path]),
|
|
359
352
|
...resources.themes.flatMap((path) => ["--theme", path]),
|
|
360
353
|
];
|
|
361
|
-
|
|
362
|
-
if (promptArgIndex < 0)
|
|
363
|
-
throw new Error(`Resolved Role launch has no ${SYSTEM_PROMPT_FLAG}.`);
|
|
364
|
-
const args = [...launch.args];
|
|
365
|
-
args.splice(promptArgIndex, 0, ...additions);
|
|
366
|
-
return prepareResolvedRoleLaunch(effectiveRole, { ...launch, args });
|
|
354
|
+
return { ...prepared, args, promptArgIndex: args.length };
|
|
367
355
|
}
|
|
368
|
-
/**
|
|
356
|
+
/** Insert the system prompt pair after a caller has prepared its launch argv. */
|
|
369
357
|
export function finalizeRoleLaunch(prepared) {
|
|
370
|
-
if (prepared.args.includes(SYSTEM_PROMPT_FLAG)) {
|
|
371
|
-
throw new Error(`Prepared Role launch already contains ${SYSTEM_PROMPT_FLAG}.`);
|
|
372
|
-
}
|
|
373
|
-
if (!Number.isSafeInteger(prepared.promptArgIndex)
|
|
374
|
-
|| prepared.promptArgIndex < 0 || prepared.promptArgIndex > prepared.args.length) {
|
|
375
|
-
throw new Error("Prepared Role launch has an invalid system prompt insertion index.");
|
|
376
|
-
}
|
|
377
|
-
if (typeof prepared.systemPrompt !== "string" || !prepared.systemPrompt.trim() || prepared.systemPrompt.includes("\0")) {
|
|
378
|
-
throw new Error("Prepared Role launch has an invalid system prompt.");
|
|
379
|
-
}
|
|
380
358
|
const args = [...prepared.args];
|
|
381
359
|
args.splice(prepared.promptArgIndex, 0, SYSTEM_PROMPT_FLAG, prepared.systemPrompt);
|
|
382
360
|
return {
|
package/dist/launch-runtime.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { EphemeralSubagentExecutionBudget } from "./ephemeral.ts";
|
|
3
2
|
import type { CoordinatorRuntime, OperationContext, TransientLaunchHandle, VerifiedLaunch } from "./runner.ts";
|
|
4
3
|
import { type ExecuteRequest, type ModelClass, type WorkspaceIdentity } from "./schema.ts";
|
|
5
4
|
export declare const ISOLATED_MODEL_TASK: {
|
|
@@ -21,7 +20,6 @@ export interface LaunchRuntimeOptions {
|
|
|
21
20
|
inspectMainBase(input: {
|
|
22
21
|
root: string;
|
|
23
22
|
}, context: OperationContext): Promise<WorkspaceIdentity>;
|
|
24
|
-
executionBudget?: () => Omit<EphemeralSubagentExecutionBudget, "startedAt">;
|
|
25
23
|
now?: () => number;
|
|
26
24
|
randomToken?: () => string;
|
|
27
25
|
}
|
package/dist/launch-runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { chmod, lstat, mkdtemp, open, realpath, rmdir, unlink } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, normalize, relative, sep } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveConfiguredRoleLaunch } from "./index.js";
|
|
7
7
|
import { registerModelTask } from "@henryqw/pi-task-models";
|
|
8
8
|
const PROMPT_FLAG = "--append-system-prompt";
|
|
9
9
|
const DIRECTORY_MODE = 0o700;
|
|
@@ -130,20 +130,13 @@ export class RoleLaunchRuntime {
|
|
|
130
130
|
if (prepared.missingSkills.length) {
|
|
131
131
|
throw new Error(`Role ${role} requires missing Skills: ${prepared.missingSkills.join(", ")}.`);
|
|
132
132
|
}
|
|
133
|
-
const executionBudget = this.options.executionBudget?.();
|
|
134
133
|
return Object.freeze({
|
|
135
134
|
launch: Object.freeze({
|
|
136
135
|
role: prepared.role,
|
|
137
136
|
modelClass,
|
|
138
137
|
model: `${prepared.model.provider}/${prepared.model.id}`,
|
|
139
138
|
thinkingLevel: prepared.thinkingLevel,
|
|
140
|
-
args: Object.freeze([
|
|
141
|
-
...prepared.args,
|
|
142
|
-
...(executionBudget === undefined ? [] : [
|
|
143
|
-
`--${EXECUTION_BUDGET_FLAG}`,
|
|
144
|
-
JSON.stringify({ ...executionBudget, startedAt: this.now() }),
|
|
145
|
-
]),
|
|
146
|
-
]),
|
|
139
|
+
args: Object.freeze([...prepared.args]),
|
|
147
140
|
env: Object.freeze({ ...prepared.env }),
|
|
148
141
|
tools: Object.freeze([...prepared.tools]),
|
|
149
142
|
}),
|
package/dist/mcp-role.d.ts
CHANGED
|
@@ -3,5 +3,4 @@ export interface RoleMcpConfig {
|
|
|
3
3
|
settings?: Record<string, unknown>;
|
|
4
4
|
}
|
|
5
5
|
export declare function parseRoleMcpAllowlist(value: unknown): string[];
|
|
6
|
-
export declare function roleMcpFlagValue(args: readonly string[], flag: string): string | undefined;
|
|
7
6
|
export declare function selectRoleMcpConfig(config: RoleMcpConfig, allowlist: readonly string[]): RoleMcpConfig;
|
package/dist/mcp-role.js
CHANGED
|
@@ -17,17 +17,6 @@ export function parseRoleMcpAllowlist(value) {
|
|
|
17
17
|
throw new Error("The Role MCP policy flag contains duplicate MCP server names.");
|
|
18
18
|
return names;
|
|
19
19
|
}
|
|
20
|
-
export function roleMcpFlagValue(args, flag) {
|
|
21
|
-
const indexes = args.flatMap((arg, index) => arg === flag ? [index] : []);
|
|
22
|
-
if (indexes.length > 1)
|
|
23
|
-
throw new Error(`${flag} must appear at most once.`);
|
|
24
|
-
if (!indexes.length)
|
|
25
|
-
return;
|
|
26
|
-
const value = args[indexes[0] + 1];
|
|
27
|
-
if (value === undefined)
|
|
28
|
-
throw new Error(`${flag} requires a value.`);
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
20
|
export function selectRoleMcpConfig(config, allowlist) {
|
|
32
21
|
const missing = allowlist.filter((name) => !Object.hasOwn(config.mcpServers, name));
|
|
33
22
|
if (missing.length)
|
package/dist/runner.d.ts
CHANGED
|
@@ -334,6 +334,7 @@ export declare class IsolatedRunner {
|
|
|
334
334
|
private cleanupPromoted;
|
|
335
335
|
private requireExactIntegration;
|
|
336
336
|
abort(id: string, root: string, outerSignal?: AbortSignal): Promise<RunResponse>;
|
|
337
|
+
private cleanupAborted;
|
|
337
338
|
status(id: string, root: string, outerSignal?: AbortSignal): Promise<RunResponse>;
|
|
338
339
|
private run;
|
|
339
340
|
private dispatchTask;
|