@henryqw/pi-subagent 26.1.3 → 26.2.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 +12 -5
- package/dist/direct-herdr.js +5 -2
- package/extensions/admission.ts +23 -43
- package/extensions/subagent.ts +1 -1
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +1 -1
package/CONTEXT.md
CHANGED
|
@@ -42,7 +42,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
42
42
|
### Direct evidence
|
|
43
43
|
|
|
44
44
|
- Direct mode admits declared write-capable resources for explicitly authorized scope while preserving read-only Role restrictions. Potential-writer requests serialize tasks and retain Pi checkout admission until exact owned worker termination is proved by pane absence and two empty private process-lease scans; uncertain termination retains admission and live recovery ownership. Potential-writer answers require an explicit structured `succeeded` outcome; failed, blocked, or malformed completion stops dispatch but does not prevent release after proved termination. `/subagent` Close/cancel-and-release retries termination under session, branch, epoch and exact ownership guards without replaying work or removing checkout/session evidence. This is a worker report, not proof of validation, cleanliness, rollback, or checked integration. Direct changesets require isolated mode.
|
|
45
|
-
-
|
|
45
|
+
- Only direct requests with potential writers reserve checkout admission. They cannot start while another direct writer holds it or a Main potentially-writing call is in flight. Main calls, including Bash and nested `codemode` writes, do not block each other. A direct writer blocks Main's writes until verified worker termination; use isolated mode when Main must continue writing. Reads never wait.
|
|
46
46
|
- Each direct worker has a recorded Herdr tab and Pi session identity. The first handle returns before completion; subsequent tab identities remain recoverable on the session branch.
|
|
47
47
|
- An exact settled worker with a bounded final Pi answer produces a follow-up to Main. Blocked, unknown, idle-stalled, truncated, or ambiguous outcomes retain actionable recovery identity. Session replacement never delivers to the wrong Main session.
|
|
48
48
|
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Run `/task-models` and configure the `fast`, `balanced`, `frontier`, and `fav` r
|
|
|
25
25
|
|
|
26
26
|
Ask Main to delegate a bounded task, such as “Have a scout trace sign-in without editing files.” Main uses `delegate_task`; you receive progress and a result or an actionable failure while Main remains available.
|
|
27
27
|
|
|
28
|
-
Commands are for you
|
|
28
|
+
Commands are for you. Delegation and state tools and the packaged skill are for Main. `git_read` is only for children whose Role declares it, not Main. You do not need to call agent tools or manage candidate identities yourself. Delegation and continuation tools use plain-object parameter schemas so providers that omit root-union tools can expose them; each mode and action still requires its exact arguments.
|
|
29
29
|
|
|
30
30
|
| Surface | Type | Purpose |
|
|
31
31
|
| --- | --- | --- |
|
|
@@ -37,15 +37,22 @@ Commands are for you; tools and the packaged skill are for Main. You do not need
|
|
|
37
37
|
| `subagent_stage` | tool | Stage or resolve an exact candidate, or reject/revise it; rejection of a staged candidate freezes that generation. |
|
|
38
38
|
| `subagent_integrate` | tool | Advance staged dependents, refresh after clean Main drift or evidence-less validation failure, validate in the background, record one correction, promote, reconcile interrupted promotion, clean up proven promotion, or explicitly release rejected/superseded resources. |
|
|
39
39
|
| `subagent_abort` | tool | Abort an isolated request only when no retained candidates or integration worktrees remain; cannot discard them. |
|
|
40
|
+
| `git_read` | tool | For Role-declared children (not Main): inspect bounded local Git evidence without Bash, tests, network access, or writes. |
|
|
40
41
|
| `pi-subagent` | skill | Guide Main through delegation, authorization, checks, integration, and recovery. |
|
|
42
|
+
| Direct status widget (`D`) | ui | For you in the TUI: show direct worker status, Role badges, route, elapsed time, and measured session tokens. |
|
|
43
|
+
| Isolated status widget (`I`) | ui | For you in the TUI: show recorded task state, Role/model-class badges, and retained workspaces; readiness does not mean promotion. |
|
|
41
44
|
|
|
42
45
|

|
|
43
46
|
|
|
44
|
-
`delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only: Pi's `codemode` scripts cannot call them, because delegation and guarded state transitions need Main's visible reasoning. `subagent_status` stays callable from scripts.
|
|
47
|
+
`delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only: Pi's `codemode` scripts cannot call them, because delegation and guarded state transitions need Main's visible reasoning. `subagent_status` stays callable from scripts. Main's potentially-writing calls can run together without writer ownership, including all Bash calls and nested `codemode` writes. Only potential-writer direct requests reserve the checkout. They can start only when no direct writer owns it and no Main potentially-writing call is in flight there. Each Main call is tracked separately until its result or execution end; a script's result does not release its active nested writes. Reads never wait. Use `mode: isolated` to keep Main free to write; this adds worktree checks and explicit integration.
|
|
45
48
|
|
|
46
49
|
The coordinator is extension code running in Main's Pi process, not another agent. It persists the request, launches the Herdr subagent, checks its committed work, and reports saved results; Main chooses what to stage and promote. If a worker fails, the coordinator reports attention after the wave settles, not necessarily immediately while siblings work. If Main's Pi process stops, no follow-up is guaranteed: restart and run `/subagent recover` in the canonical repository to classify orphaned interruptions and send Main one recovery report. Main then uses `subagent_status` for exact evidence and deliberate next actions. A stale `running` state does not prove a worker is still active; interrupted or ambiguous actions are never replayed automatically.
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
The TUI has two separate compact status widgets, each with one row per visible worker or task. `D` marks direct work; `I` marks isolated work. Badges such as `[S]` and `[I3]` identify the Role initial. Isolated badges also include the model class: `1` fast, `2` balanced, `3` frontier, `*` fav.
|
|
52
|
+
|
|
53
|
+
The direct widget shows the launched model, thinking level, elapsed time, and measured session tokens (`— tok` until usage is available). Failed rows appear before working and completed rows. When rows overflow, `… N more` includes counts by status. The isolated widget shows the recorded task state and any retained workspace, not reliable live model, thinking, or token readings from Herdr. Attention appears before ordinary work; overflow uses `+N more · /subagent`. An aborted request with visible tasks has its own `request aborted` row and does not change their recorded statuses. A committed worker without a checked candidate can still show attention rather than readiness. Status glyphs use the active TUI theme, while the text still names the status without color.
|
|
54
|
+
|
|
55
|
+
A ready isolated candidate is **not promoted** to Main. Choose **Refresh** in `/subagent` to reload both the menu and isolated widget from saved state; it does not recover or change a request. Use `/subagent` or `subagent_status` for exact identities, failures, and recovery.
|
|
49
56
|
|
|
50
57
|
### How Main routes delegation
|
|
51
58
|
|
|
@@ -81,7 +88,7 @@ Chain: { mode: "direct", chain: [{ role, name, task, ... }] }
|
|
|
81
88
|
|
|
82
89
|
Direct tasks use the selected Role's declared tools, extensions, Skills, and MCP servers, including write-capable resources. Authorization comes from your request and Main's bounded task packet, not the Role's capabilities: name allowed paths/actions and exclusions, preserve unrelated edits and staged changes, and authorize commits or external actions explicitly. For example, a configured Git operator with `bash` can stage named pending files and commit them on the `fast` route. Read-only Roles retain their tool restrictions. Use isolated mode for a checked changeset.
|
|
83
90
|
|
|
84
|
-
Direct writes affect Main's checkout immediately. No clean-checkout prerequisite, automatic checks, rollback, or isolated integration guarantee applies. Requests containing potential writers (including any extensions or MCP servers) run their tasks serially and hold checkout admission after the initial handle returns; competing
|
|
91
|
+
Direct writes affect Main's checkout immediately. No clean-checkout prerequisite, automatic checks, rollback, or isolated integration guarantee applies. Requests containing potential writers (including any extensions or MCP servers) can start only when no direct writer owns the checkout and no Main potentially-writing call is in flight there. They run their tasks serially and hold checkout admission after the initial handle returns; competing direct writers and Main writes fail until all owned workers are proved stopped. Use `mode: isolated` to keep Main free to write, with worktree checks and explicit integration instead of immediate shared-checkout changes. Pure read-only parallel requests still run concurrently. A failure in a potential-writer request stops further dispatch; an uncertain worker may still be running. Chains replace each literal `{previous}` with the preceding successful answer and stop on failure. Task outcome and worker termination are separate: after settlement the coordinator closes exact owned writer panes and releases admission only after pane absence and two empty private process-lease scans. Native `lsof` is required for this proof; no Git files, commits, or saved sessions are removed. Failure remains failure even when admission is released. If closing fails, identity changes, a leased child survives, or inspection is uncertain, admission stays held. Open `/subagent`, select the exact direct task, and choose **Close/cancel-and-release owned workers** to cancel dispatch and retry termination. The action checks session/branch/epoch and live ownership; recorded-only tasks cannot unlock anything. Resolve the reported blocker (including any surviving owned process), then repeat the same action. Restarting Pi is not termination proof. Inspect partial changes before new writes; never blindly retry a commit or delete work to recover.
|
|
85
92
|
|
|
86
93
|
If tab creation times out or returns incomplete or mismatched identity, `/subagent` retains the allocation intent: workspace, checkout, label, private lease/session paths, and any returned tab/pane IDs. No worker is started until creation identity is verified. These records are evidence, not ownership: Close is unavailable for unverified allocation, and admission remains held pending manual reconciliation against Herdr. Do not recreate a tab, guess missing IDs, or restart to bypass this blocker. A Close begun while a worker is active keeps its branch/session/epoch guards through cancellation settlement; stale dialogs cannot close panes or release admission.
|
|
87
94
|
|
|
@@ -218,7 +225,7 @@ Each call is cancellable and limited to 30 seconds. Results retain at most the l
|
|
|
218
225
|
|
|
219
226
|
Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider, and only declared resources plus required internal policy adapters load. `builtin:<name>` entries load Pi built-in extensions (`builtin:codemode`, `builtin:tool-search`, `builtin:llama.cpp`) explicitly; `builtin:mcp` is rejected because it would connect every configured server and bypass `mcps`. Extension tools registered with `codemode` or `deferred` exposure stay callable from scripts but are declared to the model only when `tools` names them.
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
Main's nested calls pass admission by their own tool names, not by script or ancestor ownership. Each potentially-writing call, including Bash, prevents a direct writer from starting until that call returns a result or execution end. Main calls do not block each other. A script's completion does not release its active nested calls; a nested call can still pass admission after its parent ends if no direct writer owns the checkout.
|
|
222
229
|
|
|
223
230
|
To let a Role run `codemode` scripts, list `builtin:codemode` in `extensions` and `codemode` in `tools`; both are required, because the built-in registers the tool inactive and the Role tool policy activates it. `codemode` alone in `tools` fails the launch as an unavailable tool. Pi's `codemode.mode` and `codemode.inlineBudget` settings apply in the child unchanged, so the Role never forces codemode-only operation, and MCP configuration never activates `codemode` in a child. Scripts can call only the Role's active tools plus `codemode`/`deferred` extension and MCP tools; inactive base tools such as `bash` stay unreachable.
|
|
224
231
|
|
package/dist/direct-herdr.js
CHANGED
|
@@ -34,11 +34,14 @@ function exactFinalTurn(jsonl, prompt) {
|
|
|
34
34
|
const parents = new Map();
|
|
35
35
|
for (const line of lines) {
|
|
36
36
|
const entry = object(JSON.parse(line), "session entry");
|
|
37
|
-
if (entry.type
|
|
37
|
+
if (entry.type === "session")
|
|
38
38
|
continue;
|
|
39
|
-
const id = field(entry.id, "
|
|
39
|
+
const id = field(entry.id, "session entry id");
|
|
40
|
+
// Non-message entries also link the exact session branch.
|
|
40
41
|
if (typeof entry.parentId === "string")
|
|
41
42
|
parents.set(id, entry.parentId);
|
|
43
|
+
if (entry.type !== "message")
|
|
44
|
+
continue;
|
|
42
45
|
const message = object(entry.message, "session message");
|
|
43
46
|
if (message.role === "user" && Array.isArray(message.content)
|
|
44
47
|
&& message.content.length === 1 && object(message.content[0], "user content").text === prompt) {
|
package/extensions/admission.ts
CHANGED
|
@@ -26,71 +26,51 @@ async function checkoutKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<str
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* calls it makes through `ctx.executeTool()` (codemode scripts), so its nested
|
|
33
|
-
* writes pass while independent writers wait until it settles.
|
|
29
|
+
* Reserve checkouts only for direct writers. Main writes can run together, but
|
|
30
|
+
* each call must finish before a direct writer can start in that checkout.
|
|
31
|
+
* External processes and trusted extension lifecycle code are not intercepted.
|
|
34
32
|
*/
|
|
35
33
|
export function registerCheckoutAdmission(pi: ExtensionAPI, directCanWrite: (input: unknown) => boolean): (toolCallId: string) => () => void {
|
|
36
34
|
const heldCalls = new Set<string>();
|
|
37
35
|
const ownerByCheckout = new Map<string, string>();
|
|
38
|
-
const
|
|
39
|
-
const writersByRoot = new Map<string, Set<string>>();
|
|
40
|
-
/** Model-issued ancestor of each in-flight call; Pi names nested calls `<parent id>/<n>`. */
|
|
41
|
-
const rootByCall = new Map<string, string>();
|
|
36
|
+
const calls = new Map<string, { direct: boolean; key?: string }>();
|
|
42
37
|
const release = (toolCallId: string) => {
|
|
43
38
|
if (heldCalls.has(toolCallId)) return;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const writers = writersByRoot.get(root);
|
|
48
|
-
writers?.delete(toolCallId);
|
|
49
|
-
if (rootByCall.has(root) || writers?.size) return;
|
|
50
|
-
writersByRoot.delete(root);
|
|
51
|
-
const key = checkoutByRoot.get(root);
|
|
52
|
-
if (!key) return;
|
|
53
|
-
checkoutByRoot.delete(root);
|
|
54
|
-
if (ownerByCheckout.get(key) === root) ownerByCheckout.delete(key);
|
|
39
|
+
const call = calls.get(toolCallId);
|
|
40
|
+
calls.delete(toolCallId);
|
|
41
|
+
if (call?.key && ownerByCheckout.get(call.key) === toolCallId) ownerByCheckout.delete(call.key);
|
|
55
42
|
};
|
|
56
43
|
pi.on("tool_call", async (event, ctx) => {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const potentiallyWriting = event.toolName === "delegate_task"
|
|
62
|
-
? directCanWrite(event.input)
|
|
63
|
-
: !READ_ONLY_TOOLS.has(event.toolName);
|
|
64
|
-
if (!potentiallyWriting) return;
|
|
44
|
+
const direct = event.toolName === "delegate_task";
|
|
45
|
+
if (direct ? !directCanWrite(event.input) : READ_ONLY_TOOLS.has(event.toolName)) return;
|
|
46
|
+
const call: { direct: boolean; key?: string } = { direct };
|
|
47
|
+
calls.set(event.toolCallId, call);
|
|
65
48
|
const key = await checkoutKey(pi, ctx);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return { block: true, reason: `Parent call ${root} already settled; ${event.toolCallId} is not admitted.` };
|
|
49
|
+
if (calls.get(event.toolCallId) !== call) {
|
|
50
|
+
return { block: true, reason: `Call ${event.toolCallId} already settled; use mode: isolated to keep Main free.` };
|
|
69
51
|
}
|
|
70
52
|
const owner = ownerByCheckout.get(key);
|
|
71
|
-
|
|
53
|
+
const main = direct ? [...calls].find(([, active]) => !active.direct && active.key === key)?.[0] : undefined;
|
|
54
|
+
if (owner || main) {
|
|
55
|
+
release(event.toolCallId);
|
|
72
56
|
return {
|
|
73
57
|
block: true,
|
|
74
|
-
reason: `Checkout ${key}
|
|
58
|
+
reason: `Checkout ${key} has ${owner ? `an admitted direct writer (${owner})` : `a Main potentially-writing call in flight (${main})`}; retry after it stops, or use mode: isolated to keep Main free.`,
|
|
75
59
|
};
|
|
76
60
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const writers = writersByRoot.get(root) ?? new Set<string>();
|
|
80
|
-
writers.add(event.toolCallId);
|
|
81
|
-
writersByRoot.set(root, writers);
|
|
61
|
+
call.key = key;
|
|
62
|
+
if (direct) ownerByCheckout.set(key, event.toolCallId);
|
|
82
63
|
});
|
|
83
64
|
pi.on("tool_result", (event) => release(event.toolCallId));
|
|
84
65
|
pi.on("tool_execution_end", (event) => release(event.toolCallId));
|
|
85
66
|
pi.on("session_shutdown", () => {
|
|
86
67
|
// Session replacement is not proof that a held direct worker stopped.
|
|
87
|
-
for (const
|
|
68
|
+
for (const toolCallId of calls.keys()) release(toolCallId);
|
|
88
69
|
});
|
|
89
|
-
//
|
|
90
|
-
// admission until proved termination; uncertain workers require guarded recovery.
|
|
70
|
+
// The tool returns a handle before its worker stops. Release only after verified termination.
|
|
91
71
|
return (toolCallId) => {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
72
|
+
const call = calls.get(toolCallId);
|
|
73
|
+
if (!call?.direct || !call.key) {
|
|
94
74
|
throw new Error(`Direct call ${toolCallId} was not admitted as a checkout writer; delegate again with the current Role resources.`);
|
|
95
75
|
}
|
|
96
76
|
heldCalls.add(toolCallId);
|
package/extensions/subagent.ts
CHANGED
|
@@ -173,7 +173,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
173
173
|
const canWrite = (input: unknown): boolean => {
|
|
174
174
|
try {
|
|
175
175
|
const parsed = parseDelegateTask(input);
|
|
176
|
-
if (parsed.mode === "isolated") return
|
|
176
|
+
if (parsed.mode === "isolated") return false;
|
|
177
177
|
const roles = new Map(loadRoles().map((role) => [role.name, role]));
|
|
178
178
|
return parsed.workflow.delegations.some((delegation) => {
|
|
179
179
|
const role = roles.get(delegation.role);
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ After delegation acknowledges a handle or durable request, do other work or end
|
|
|
24
24
|
|
|
25
25
|
# Direct requests
|
|
26
26
|
|
|
27
|
-
Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or `chain` for dependent packets. Only answer-returning tasks (`kind: text`) are accepted; writes and commits are allowed within explicit task authorization, but `kind: changeset`, structured checks, and judgment require isolated mode. State scope, exclusions, commit policy, and external-action permission; preserve unrelated files and staged changes. Direct writes take effect immediately with no automatic validation, rollback, or isolated integration guarantees.
|
|
27
|
+
Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or `chain` for dependent packets. Only answer-returning tasks (`kind: text`) are accepted; writes and commits are allowed within explicit task authorization, but `kind: changeset`, structured checks, and judgment require isolated mode. State scope, exclusions, commit policy, and external-action permission; preserve unrelated files and staged changes. Direct writes take effect immediately with no automatic validation, rollback, or isolated integration guarantees. Main's potentially-writing calls, including all Bash and nested `codemode` writes, can run together and do not acquire writer ownership. Only potential-writer direct requests reserve the checkout. Start one only when no direct writer owns it and no Main potentially-writing call is in flight there. Each Main call ends separately; a parent's result does not release active nested writes. Direct requests with potential writers run serially and block competing direct writers and Main writes until owned workers are proved stopped. Main may read or do work elsewhere while waiting, not mutate the reserved checkout. Choose `mode: isolated` to keep Main free to write; it adds worktree checks and explicit integration instead of immediate shared-checkout changes. Task failure is not proof a worker stopped. The coordinator closes exact owned writer panes and releases admission only after pane absence and two empty private process-lease scans. If termination remains uncertain, inspect `/subagent` identities and choose **Close/cancel-and-release owned workers** for the exact locally owned task. Resolve the reported blocker, then retry that guarded action; it never replays the prompt or removes dirty files, commits, or sessions. Recorded-only tasks have no live release authority; restarting Pi is not termination proof. An unverified tab allocation retains its workspace, checkout, label, lease/session paths and any returned IDs for manual reconciliation; it cannot authorize Close, guessed identities or replayed creation. Close keeps its branch/session/epoch guards while an active task cancels and settles. Preserve and inspect changes before writing or retrying. A failed commit may already have happened; never replay it blindly. The tool returns a handle after launching Herdr tabs in the current workspace. Main receives the verified result as a follow-up after the workflow finishes, without interrupting an active turn. If observation stops, open `/subagent` to inspect exact recorded tabs and session files on this session branch; recorded does not prove still running.
|
|
28
28
|
|
|
29
29
|
# Isolated requests
|
|
30
30
|
|