@henryqw/pi-subagent 26.1.4 → 26.2.1

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 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
- - Checkout admission in Main serializes write-capable Pi tool calls per checkout by their model-issued root call: a `codemode` script's nested writes share the script's ownership, independent writers wait, and reads never wait.
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
@@ -6,7 +6,7 @@ Delegate work to configured Pi Roles with [Herdr](https://herdr.dev/). Authorize
6
6
 
7
7
  ## Install
8
8
 
9
- Requires Pi 1.0.0 or later. Development and automated checks use Pi 1.0.2. Older Pi releases are no longer supported.
9
+ Requires Pi 1.0.0 or later. Development and automated checks use Pi 1.0.0. Older Pi releases are no longer supported.
10
10
 
11
11
  ```bash
12
12
  pi install npm:@henryqw/pi-task-models
@@ -44,7 +44,7 @@ Commands are for you. Delegation and state tools and the packaged skill are for
44
44
 
45
45
  ![Main, its coordinator extension, and a Herdr subagent through checked work, worker failure, and coordinator interruption](./docs/worker-lifecycle.svg)
46
46
 
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. In Main, one model-issued call at a time may write to a checkout through Pi tools; a `codemode` script owns the checkout from its first nested write until the script ends, so its own nested calls pass while an independent writer is blocked with a reason and retries after it settles. Reads never wait.
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.
48
48
 
49
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.
50
50
 
@@ -88,7 +88,7 @@ Chain: { mode: "direct", chain: [{ role, name, task, ... }] }
88
88
 
89
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.
90
90
 
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) run their tasks serially and hold checkout admission after the initial handle returns; competing Pi writes fail until all owned workers are proved stopped. 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.
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.
92
92
 
93
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.
94
94
 
@@ -225,7 +225,7 @@ Each call is cancellable and limited to 30 seconds. Results retain at most the l
225
225
 
226
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.
227
227
 
228
- Checkout admission keeps a script's writer ownership until the script and all its admitted nested writes finish, including writes still settling after cancellation. Independent writers stay blocked during that time; nested writes that have not passed admission before the script ends are rejected.
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.
229
229
 
230
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.
231
231
 
@@ -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 !== "message")
37
+ if (entry.type === "session")
38
38
  continue;
39
- const id = field(entry.id, "message 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) {
@@ -26,71 +26,51 @@ async function checkoutKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<str
26
26
  }
27
27
 
28
28
  /**
29
- * Coordinate Pi-owned calls that can mutate one checkout. This is admission,
30
- * not an OS sandbox: external processes and trusted extension lifecycle code
31
- * remain outside interception. A model-issued call owns the checkout for the
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 checkoutByRoot = new Map<string, string>();
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 root = rootByCall.get(toolCallId);
45
- if (root === undefined) return;
46
- rootByCall.delete(toolCallId);
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 root = event.parentToolCallId === undefined
58
- ? event.toolCallId
59
- : rootByCall.get(event.parentToolCallId) ?? event.parentToolCallId;
60
- rootByCall.set(event.toolCallId, root);
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
- // A codemode result can settle while an unawaited nested call is still resolving its checkout.
67
- if (root !== event.toolCallId && !rootByCall.has(root)) {
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
- if (owner && owner !== root) {
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} already has an admitted Pi writer (${owner}); retry after it settles.`,
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
- ownerByCheckout.set(key, root);
78
- checkoutByRoot.set(root, key);
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 call of rootByCall.keys()) release(call);
68
+ for (const toolCallId of calls.keys()) release(toolCallId);
88
69
  });
89
- // A direct handle settles the tool call before the worker settles. Keep its
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 root = rootByCall.get(toolCallId);
93
- if (root === undefined || !writersByRoot.get(root)?.has(toolCallId)) {
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);
@@ -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 true;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "26.1.4",
3
+ "version": "26.2.1",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -66,17 +66,17 @@
66
66
  "image": "https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-subagent/example.png"
67
67
  },
68
68
  "dependencies": {
69
- "@henryqw/pi-config-store": "^2.0.0",
69
+ "@henryqw/pi-config-store": "^3.0.0",
70
70
  "@henryqw/pi-herdr": "^0.4.9",
71
- "@henryqw/pi-multi-codex": "^2.0.0",
71
+ "@henryqw/pi-multi-codex": "^3.0.0",
72
72
  "@henryqw/pi-process": "^0.2.0",
73
73
  "@henryqw/pi-task-models": "^7.0.0",
74
74
  "proper-lockfile": "^4.1.2"
75
75
  },
76
76
  "devDependencies": {
77
- "@earendil-works/pi-ai": "1.0.2",
78
- "@earendil-works/pi-coding-agent": "1.0.2",
79
- "@earendil-works/pi-tui": "1.0.2",
77
+ "@earendil-works/pi-ai": "1.0.0",
78
+ "@earendil-works/pi-coding-agent": "1.0.0",
79
+ "@earendil-works/pi-tui": "1.0.0",
80
80
  "@types/proper-lockfile": "^4.1.4"
81
81
  }
82
82
  }
@@ -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. Requests with potential writers run serially and block competing Pi writes until owned workers are proved stopped. Main may read or do work elsewhere while waiting, not mutate the reserved checkout. 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.
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