@henryqw/pi-subagent 25.1.1 → 26.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 +3 -3
- package/README.md +12 -6
- package/dist/direct-herdr.d.ts +2 -2
- package/dist/direct-herdr.js +25 -5
- package/dist/index.js +1 -1
- package/dist/mcp-role.d.ts +2 -1
- package/dist/mcp-role.js +15 -2
- package/docs/orchestration.md +4 -2
- package/examples/roles/implementer.md +1 -1
- package/extensions/admission.ts +15 -2
- package/extensions/subagent.ts +25 -12
- package/extensions/workflow.ts +1 -1
- package/package.json +7 -7
- package/skills/pi-subagent/SKILL.md +3 -3
package/CONTEXT.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
6
6
|
|
|
7
|
-
- **direct mode** runs
|
|
7
|
+
- **direct mode** runs authorized compact shared-checkout work in Herdr tabs in Main's workspace and returns a handle before completion;
|
|
8
8
|
- **isolated mode** runs durable checked graphs with Herdr worktrees, exact Git identities, guarded integration, and recovery; and
|
|
9
9
|
- the package root exposes the Role, executor, worktree, evidence, schema, state, and runtime mechanisms used by the extension.
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
13
13
|
- **Main**: the Pi session and checkout coordinating delegated work.
|
|
14
14
|
- **Role**: package or user Markdown defining responsibility, tools, trusted extension sources, Skills, optional MCP names, instructions, and an optional model-class default. Roles do not select isolation.
|
|
15
15
|
- **Model Class**: `fast`, `balanced`, `frontier`, or `fav`, resolved through `pi-task-models`.
|
|
16
|
-
- **Direct task**: one task using
|
|
16
|
+
- **Direct task**: one bounded task using its Role's declared resources in Main's shared checkout; authorized writes take effect immediately without checked integration.
|
|
17
17
|
- **Isolated request**: one durable ID, goal, and checked task graph.
|
|
18
18
|
- **Candidate**: exact clean committed task-worktree identity produced by an isolated changeset worker.
|
|
19
19
|
- **Readiness**: durable proof that one exact live-worker candidate passed its preliminary checks and is sealed for Main's explicit selection; it is not a separate user approval.
|
|
@@ -41,7 +41,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
41
41
|
|
|
42
42
|
### Direct evidence
|
|
43
43
|
|
|
44
|
-
- Direct mode admits
|
|
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 through exact completion; uncertain outcomes retain admission for manual recovery. Potential-writer answers require an explicit structured `succeeded` outcome; failed, blocked, or malformed completion stops dispatch and retains admission. This is a worker report, not proof of validation, cleanliness, rollback, or checked integration. Direct changesets require isolated mode.
|
|
45
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.
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Delegate work to configured Pi Roles with [Herdr](https://herdr.dev/).
|
|
3
|
+
Delegate work to configured Pi Roles with [Herdr](https://herdr.dev/). Authorized direct tasks run in Herdr tabs in your current workspace; checked implementation runs in isolated Herdr worktrees. Both return a handle so Main can continue while they work.
|
|
4
4
|
|
|
5
5
|

|
|
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.
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
12
|
pi install npm:@henryqw/pi-task-models
|
|
11
13
|
pi install npm:@henryqw/pi-subagent
|
|
@@ -29,7 +31,7 @@ Commands are for you; tools and the packaged skill are for Main. You do not need
|
|
|
29
31
|
| --- | --- | --- |
|
|
30
32
|
| `/subagent` | command | Browse work, launch isolated status inspection in a separate Herdr tab with the `fast` model, or send/edit pending instructions. Requires interactive TUI or RPC; inspection also requires a Herdr-managed pane and a read-only `scout` Role. |
|
|
31
33
|
| `/subagent recover` | command | Classify orphaned isolated work in the current canonical repository and send Main one follow-up with request IDs, blockers, and reported next actions. No UI or Herdr pane required. |
|
|
32
|
-
| `delegate_task` | tool | Start
|
|
34
|
+
| `delegate_task` | tool | Start authorized shared-checkout work or a durable isolated checked graph. |
|
|
33
35
|
| `subagent_status` | tool | Read durable isolated state and the exact allowed continuation without replaying work. Declares an output schema, so `codemode` scripts receive the same bounded public projection as structured content. |
|
|
34
36
|
| `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation; does not replace staging or integration. |
|
|
35
37
|
| `subagent_stage` | tool | Stage or resolve an exact candidate, or reject/revise it; rejection of a staged candidate freezes that generation. |
|
|
@@ -51,7 +53,7 @@ Main chooses the mode from your request; you do not need to select one. Words li
|
|
|
51
53
|
|
|
52
54
|
| Mode | Request that triggers it | Checkout behavior |
|
|
53
55
|
| --- | --- | --- |
|
|
54
|
-
| `direct` |
|
|
56
|
+
| `direct` | Bounded research, review, or explicitly authorized shared-checkout writes and commits | Opens non-focused Herdr tabs in Main's current workspace. No worktree is created. |
|
|
55
57
|
| `isolated` | Implementation or a checked task graph | Runs changesets in owned Herdr worktrees; Main selects, validates, and promotes exact candidates. |
|
|
56
58
|
|
|
57
59
|
Keep trivial mechanically verifiable work in Main. Keep tightly coupled changes under one owner rather than splitting by file count.
|
|
@@ -77,7 +79,11 @@ Parallel: { mode: "direct", tasks: [{ role, name, task, ... }] }
|
|
|
77
79
|
Chain: { mode: "direct", chain: [{ role, name, task, ... }] }
|
|
78
80
|
```
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
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
|
+
|
|
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 Pi writes fail until exact successful completion. 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. On failure, cancellation, or uncertain launch after tab creation, admission stays held: inspect the exact tabs/session files through `/subagent`, stop any remaining owned workers, inspect and preserve their changes, then restart Pi before new writes. Never blindly retry a commit or delete work to recover.
|
|
85
|
+
|
|
86
|
+
Potential-writer tasks must return one JSON completion object, such as `{"outcome":"succeeded","answer":"Scoped commit completed."}`. `outcome` must be `succeeded`, `failed`, or `blocked`, and `answer` must be non-empty. A normal final Pi turn is not enough: failure, blocked work, plain text, or malformed completion stops later dispatch and retains admission. Success is the worker's explicit report, not independent validation of its changes.
|
|
81
87
|
|
|
82
88
|
The tool returns a task ID and first Herdr tab after launch, not the answer. The extension observes each worker and sends one result to Main when the workflow finishes. If Main is busy, Pi queues it after the current turn; if idle, it starts a turn. A blocked, stalled, unknown, or truncated result is not reported as success. Session switch or shutdown stops observation and preserves tab identities for recovery. Open `/subagent` on the session branch to inspect exact tabs, agents, and Pi session files, including tabs launched after the first. A recorded tab may no longer be running; locally observed work is labelled separately.
|
|
83
89
|
|
|
@@ -191,7 +197,7 @@ Checkout admission keeps a script's writer ownership until the script and all it
|
|
|
191
197
|
|
|
192
198
|
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.
|
|
193
199
|
|
|
194
|
-
The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In a Role that activates `codemode`, each selected server keeps its configured `exposure` and `toolExposure` (Pi's default `codemode` exposure makes tools callable from scripts without declaring them, `direct` declares them, `deferred` tools stay reachable from scripts, `hidden` tools stay unreachable), and an invalid exposure value fails the launch. In every other Role, selected servers are forced to `direct` exposure and `toolExposure` is ignored, because that child cannot reach undeclared tools. Declared tools appear to the model as `mcp__<server>__<tool>` once the server connects. Pi replaces `-` in server and tool names with `_` and appends a hash suffix when two tool names collide or a name exceeds 64 characters. Each selected entry is checked against Pi's documented `mcpServers` rules before launch, in Main and again in the child: a disabled, malformed, or unknown-transport entry, an invalid server name, two selected names that differ only in `-` and `_`, `auth.provider` over plain HTTP outside `localhost`, `127.0.0.1`, or `[::1]`, or a non-HTTPS `oauth.authServerMetadataUrl` outside those hosts fails the launch with the file path and field. The file is never modified; fix the entry or choose another server. Valid `auth`, `oauth`, and `description` fields are passed through so Pi's native provider auth and OAuth apply. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
|
|
200
|
+
The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In a Role that activates `codemode`, each selected server keeps its configured `exposure` and `toolExposure` (Pi's default `codemode` exposure makes tools callable from scripts without declaring them, `direct` declares them, `deferred` tools stay reachable from scripts, `hidden` tools stay unreachable), and an invalid exposure value fails the launch. In every other Role, selected servers are forced to `direct` exposure and `toolExposure` is ignored, because that child cannot reach undeclared tools. Declared tools appear to the model as `mcp__<server>__<tool>` once the server connects. Pi replaces `-` in server and tool names with `_` and appends a hash suffix when two tool names collide or a name exceeds 64 characters. Each selected entry is checked against Pi's documented `mcpServers` rules before launch, in Main and again in the child: a disabled, malformed, or unknown-transport entry, an invalid server name, two selected names that differ only in `-` and `_`, `auth.provider` over plain HTTP outside `localhost`, `127.0.0.1`, or `[::1]`, or a non-HTTPS `oauth.authServerMetadataUrl` outside those hosts fails the launch with the file path and field. The file is never modified; fix the entry or choose another server. Valid `auth`, `oauth`, and `description` fields are passed through so Pi's native provider auth and OAuth apply. `oauth.clientRegistration` must be `dcr` or `cimd`; `cimd` requires Pi 1.0.1 or later; Pi 1.0.0 rejects it before child launch. It cannot use `clientId` or `clientName`, and any `callbackUrl` must use localhost or 127.0.0.1 with path `/callback`. Project MCP overrides remain ignored: a project cannot change the Role's global server allowlist or exposure. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
|
|
195
201
|
|
|
196
202
|
## API
|
|
197
203
|
|
|
@@ -218,4 +224,4 @@ Durable state is private under `config/pi-subagent/state/`. Isolated worker Pi s
|
|
|
218
224
|
|
|
219
225
|
## Limits and recovery
|
|
220
226
|
|
|
221
|
-
Role extensions and MCP servers are trusted executable code, not a sandbox. Select the smallest resource set. Direct
|
|
227
|
+
Role extensions and MCP servers are trusted executable code, not a sandbox. Select the smallest resource set. Direct tasks may use declared write-capable resources in Main's checkout only within the authorized scope. Checkout admission coordinates this Main's Pi calls, not other Pi sessions, trusted extension lifecycle code, or external processes. Concurrent changes can make a direct worker's read stale. Retained-work reports identify exact resources for deliberate recovery.
|
package/dist/direct-herdr.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export interface DirectHandle {
|
|
|
12
12
|
cancel(): Promise<void>;
|
|
13
13
|
}
|
|
14
14
|
export declare function exactDirectTerminalTurn(jsonl: string, prompt: string): boolean;
|
|
15
|
-
export declare function exactDirectAnswer(jsonl: string, prompt: string, maxBytes?: number): string;
|
|
15
|
+
export declare function exactDirectAnswer(jsonl: string, prompt: string, maxBytes?: number, requireSuccess?: boolean): string;
|
|
16
16
|
export declare function directSessionTokens(jsonl: string, prompt: string): number | undefined;
|
|
17
17
|
export type DirectTab = Pick<DirectHandle, "name" | "tabId" | "paneId" | "sessionFile">;
|
|
18
18
|
export declare function createDirectHerdr(pi: Pick<ExtensionAPI, "exec">, cwd: string, idleMs: number): {
|
|
19
|
-
start(launch: ResolvedRoleLaunch, name: string, label: string, task: string, signal: AbortSignal, onTab: (tab: DirectTab) => void): Promise<DirectHandle>;
|
|
19
|
+
start(launch: ResolvedRoleLaunch, name: string, label: string, task: string, signal: AbortSignal, onTab: (tab: DirectTab) => void, requireSuccess?: boolean): Promise<DirectHandle>;
|
|
20
20
|
};
|
package/dist/direct-herdr.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createHerdrClient, hasHerdrErrorCode, herdrCommandFailure, startPiAgent
|
|
|
7
7
|
const OPERATION_MS = 30_000;
|
|
8
8
|
const SESSION_LIMIT = 16 * 1024 * 1024;
|
|
9
9
|
const ANSWER_LIMIT = 50 * 1024;
|
|
10
|
+
const DIRECT_OUTCOMES = ["succeeded", "failed", "blocked"];
|
|
10
11
|
const object = (value, label) => {
|
|
11
12
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
13
|
throw new Error(`Malformed Herdr ${label}.`);
|
|
@@ -63,7 +64,7 @@ export function exactDirectTerminalTurn(jsonl, prompt) {
|
|
|
63
64
|
const reason = exactFinalTurn(jsonl, prompt)?.stopReason;
|
|
64
65
|
return reason === "stop" || reason === "error" || reason === "aborted";
|
|
65
66
|
}
|
|
66
|
-
export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT) {
|
|
67
|
+
export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT, requireSuccess = false) {
|
|
67
68
|
const final = exactFinalTurn(jsonl, prompt);
|
|
68
69
|
if (!final || final.stopReason !== "stop" || !Array.isArray(final.content)) {
|
|
69
70
|
throw new Error("Pi did not persist an exact successful final answer for this prompt.");
|
|
@@ -76,7 +77,23 @@ export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT) {
|
|
|
76
77
|
throw new Error("Pi persisted an empty final answer.");
|
|
77
78
|
if (Buffer.byteLength(text, "utf8") > maxBytes)
|
|
78
79
|
throw new Error(`Pi final answer exceeds the ${maxBytes}-byte workflow limit; read the private session file for recovery.`);
|
|
79
|
-
|
|
80
|
+
if (!requireSuccess)
|
|
81
|
+
return text;
|
|
82
|
+
let completion;
|
|
83
|
+
try {
|
|
84
|
+
completion = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new Error("Pi potential-writer final answer must be a JSON completion object.", { cause: error });
|
|
88
|
+
}
|
|
89
|
+
if (!completion || typeof completion !== "object" || Array.isArray(completion)
|
|
90
|
+
|| !DIRECT_OUTCOMES.some((outcome) => outcome === completion.outcome)
|
|
91
|
+
|| typeof completion.answer !== "string" || !completion.answer.trim()) {
|
|
92
|
+
throw new Error("Pi potential-writer completion must have a valid outcome and a non-empty answer.");
|
|
93
|
+
}
|
|
94
|
+
if (completion.outcome !== "succeeded")
|
|
95
|
+
throw new Error(`Direct task reported ${completion.outcome}: ${completion.answer}`);
|
|
96
|
+
return completion.answer;
|
|
80
97
|
}
|
|
81
98
|
// Ignore a partial trailing JSONL entry while the child is writing it.
|
|
82
99
|
export function directSessionTokens(jsonl, prompt) {
|
|
@@ -132,7 +149,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
132
149
|
return field(agent.agent_status, "agent state");
|
|
133
150
|
};
|
|
134
151
|
return {
|
|
135
|
-
async start(launch, name, label, task, signal, onTab) {
|
|
152
|
+
async start(launch, name, label, task, signal, onTab, requireSuccess = false) {
|
|
136
153
|
const caller = result(await herdr.json(["pane", "current", "--current"], options(signal)), "pane_current");
|
|
137
154
|
const pane = object(caller.pane, "calling pane");
|
|
138
155
|
if (pane.pane_id !== callerPane || pane.workspace_id !== workspaceId)
|
|
@@ -170,7 +187,10 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
170
187
|
// A canceled start may have succeeded server-side; retain its tab but
|
|
171
188
|
// never submit a new prompt after cancellation.
|
|
172
189
|
signal.throwIfAborted();
|
|
173
|
-
const
|
|
190
|
+
const completionInstruction = requireSuccess
|
|
191
|
+
? `\n\nPotential-writer completion: Return exactly one JSON object without Markdown, {"outcome":"succeeded","answer":"summary"}. outcome must be ${DIRECT_OUTCOMES.join(", ")}. Use succeeded only when the entire authorized task, including requested checks and commits, completed successfully; a failed mutation, unresolved partial work, or missing authorization must be failed or blocked, even if your Pi turn ends normally. Describe changes, checks, commit identity, remaining risks, and checkout status in answer.`
|
|
192
|
+
: "";
|
|
193
|
+
const prompt = `${task}\n\nDirect shared-checkout boundary: use only your Role's resources for the explicitly authorized task and scope. Writes and commits affect Main's existing checkout immediately; preserve unrelated files and staged changes. Do not create or manage worktrees, change branches, push, or perform external mutations without explicit task authorization. There is no isolated integration, automatic validation, or rollback. If instructions conflict or authorization is missing, stop and report it. Report changes, commit identity if any, checks, remaining risks, and checkout status; never claim isolated guarantees.${completionInstruction}\n\nTurn identity: ${randomBytes(16).toString("hex")}`;
|
|
174
194
|
// Native prompt without --wait acknowledges submission, not completion of the turn.
|
|
175
195
|
const accepted = await herdr.json(["agent", "prompt", name, prompt], options(signal));
|
|
176
196
|
const state = inspect(accepted, "agent_prompted", name, paneId, tabId);
|
|
@@ -195,7 +215,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
195
215
|
const info = await stat(sessionFile);
|
|
196
216
|
if (info.size > SESSION_LIMIT)
|
|
197
217
|
throw new Error(`Pi session in tab ${tabId} exceeds 16 MiB; inspect ${sessionFile} for recovery.`);
|
|
198
|
-
return exactDirectAnswer(await readFile(sessionFile, "utf8"), prompt, maxBytes);
|
|
218
|
+
return exactDirectAnswer(await readFile(sessionFile, "utf8"), prompt, maxBytes, requireSuccess);
|
|
199
219
|
};
|
|
200
220
|
const maybeAnswer = async () => {
|
|
201
221
|
try {
|
package/dist/index.js
CHANGED
|
@@ -97,7 +97,7 @@ const stringList = (value, field, source) => {
|
|
|
97
97
|
}
|
|
98
98
|
return value.map((item) => item.trim());
|
|
99
99
|
};
|
|
100
|
-
// Pi
|
|
100
|
+
// Pi built-in extensions (`builtin:<name>`, see Pi's settings docs); Pi exports no list of them.
|
|
101
101
|
// `builtin:mcp` is excluded: it connects every server in mcp.json and would bypass the Role `mcps` allowlist.
|
|
102
102
|
const BUILTIN_EXTENSION_NAMES = ["codemode", "tool-search", "llama.cpp"];
|
|
103
103
|
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
package/dist/mcp-role.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export declare function parseRoleMcpAllowlist(value: unknown): string[];
|
|
3
3
|
/**
|
|
4
4
|
* Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
|
|
@@ -9,4 +9,5 @@ export declare function parseRoleMcpAllowlist(value: unknown): string[];
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function loadRoleMcpConfig(agentDir: string, allowlist: readonly string[], options?: {
|
|
11
11
|
codemode: boolean;
|
|
12
|
+
piVersion?: string;
|
|
12
13
|
}): LoadedMcpConfig;
|
package/dist/mcp-role.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
export function parseRoleMcpAllowlist(value) {
|
|
4
5
|
if (typeof value !== "string")
|
|
5
6
|
throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
|
|
@@ -62,6 +63,7 @@ const HTTP_CHECKS = {
|
|
|
62
63
|
}, "an http URL on localhost, 127.0.0.1, or [::1] without query or fragment, on the \"oauth.callbackPort\" port"],
|
|
63
64
|
"oauth.scope": [isString, "a string"],
|
|
64
65
|
"oauth.clientName": [(value) => typeof value === "string" && value.trim() !== "", "a non-empty string"],
|
|
66
|
+
"oauth.clientRegistration": [(value) => value === "dcr" || value === "cimd", '"dcr" or "cimd"'],
|
|
65
67
|
"oauth.authServerMetadataUrl": [(value) => isHttpsOrLoopback(parseUrl(value)), "an https URL, or http on localhost, 127.0.0.1, or [::1]"],
|
|
66
68
|
auth: [(value) => isRecord(value) && typeof value.provider === "string" && value.provider !== "", "an object with a non-empty \"provider\" string"],
|
|
67
69
|
};
|
|
@@ -70,7 +72,7 @@ const EXPOSURE_CHECKS = {
|
|
|
70
72
|
toolExposure: [(value) => isRecord(value) && Object.values(value).every(isExposure), "an object mapping tool names to exposures"],
|
|
71
73
|
};
|
|
72
74
|
/** Validate one selected server against Pi's documented `mcpServers` rules; the native loader is bypassed by `loadConfig`. */
|
|
73
|
-
function validateRoleMcpServer(path, name, value, codemode) {
|
|
75
|
+
function validateRoleMcpServer(path, name, value, codemode, piVersion) {
|
|
74
76
|
const fail = (message) => { throw new Error(`${path}: MCP server "${name}" ${message}`); };
|
|
75
77
|
if (!SERVER_NAME.test(name))
|
|
76
78
|
fail("has an invalid name (use letters, digits, \"_\" and \"-\").");
|
|
@@ -87,6 +89,17 @@ function validateRoleMcpServer(path, name, value, codemode) {
|
|
|
87
89
|
if (fieldValue !== undefined && !valid(fieldValue, value))
|
|
88
90
|
fail(`field "${field}" must be ${shape}.`);
|
|
89
91
|
}
|
|
92
|
+
if (http && isRecord(value.oauth) && value.oauth.clientRegistration === "cimd") {
|
|
93
|
+
if (piVersion === "1.0.0")
|
|
94
|
+
fail('field "oauth.clientRegistration" "cimd" requires Pi 1.0.1 or later; current Pi is 1.0.0.');
|
|
95
|
+
if (value.oauth.clientId !== undefined || value.oauth.clientName !== undefined) {
|
|
96
|
+
fail('field "oauth.clientRegistration" "cimd" cannot be combined with "oauth.clientId" or "oauth.clientName".');
|
|
97
|
+
}
|
|
98
|
+
const callback = parseUrl(value.oauth.callbackUrl);
|
|
99
|
+
if (callback && (callback.hostname === "[::1]" || callback.pathname !== "/callback")) {
|
|
100
|
+
fail('field "oauth.clientRegistration" "cimd" requires "oauth.callbackUrl" on localhost or 127.0.0.1 with path /callback.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
90
103
|
// The native transport sends the provider's token to `url`, so Pi only allows it over https or loopback.
|
|
91
104
|
if (value.auth !== undefined && !isHttpsOrLoopback(parseUrl(value.url))) {
|
|
92
105
|
fail("field \"auth\" requires \"url\" to use https, or http on localhost, 127.0.0.1, or [::1].");
|
|
@@ -133,7 +146,7 @@ export function loadRoleMcpConfig(agentDir, allowlist, options = { codemode: fal
|
|
|
133
146
|
if (clash)
|
|
134
147
|
throw new Error(`${path}: Role MCP server "${name}" conflicts with "${clash}": names that differ only in "-" and "_" share one tool namespace.`);
|
|
135
148
|
namespaces.set(mcpNamespace(name), name);
|
|
136
|
-
return { name, config: validateRoleMcpServer(path, name, configured[name], options.codemode), source: path, scope: "global" };
|
|
149
|
+
return { name, config: validateRoleMcpServer(path, name, configured[name], options.codemode, options.piVersion ?? VERSION), source: path, scope: "global" };
|
|
137
150
|
});
|
|
138
151
|
return { servers, autoEnableCodemode: false, errors: [] };
|
|
139
152
|
}
|
package/docs/orchestration.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`pi-subagent` owns both delegation paths behind `delegate_task`:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
|
-
┌─ direct ───
|
|
6
|
+
┌─ direct ─── authorized shared-checkout Herdr tab
|
|
7
7
|
request + Role + route ──┤
|
|
8
8
|
└─ isolated ─ Herdr worktree, checked candidate, durable state
|
|
9
9
|
```
|
|
@@ -62,7 +62,9 @@ Each entry has:
|
|
|
62
62
|
|
|
63
63
|
An explicit model must support the route's thinking level. A launched child is never retried automatically after provider or process failure.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Direct tasks use their Role's declared resources, including write tools, within explicit task authorization. Read-only Role restrictions, resource/trust validation, and recursive-delegation exclusions still apply. A direct `changeset` is rejected; use isolated mode for checked integration. Tasks in any potential-writer request run serially and reserve checkout admission until exact successful completion; pure read-only parallel tasks run concurrently. Results stay in request order. A chain substitutes only the preceding successful output and stops at its first failure.
|
|
66
|
+
|
|
67
|
+
Writes and commits immediately change the shared checkout; there is no automatic validation, rollback, or isolated promotion. A potential-writer final answer must be a JSON object with `outcome` (`succeeded`, `failed`, or `blocked`) and a non-empty `answer`; only explicit worker-reported success permits later dispatch or admission release, not merely a normally settled Pi turn. Preserve unrelated files and index entries, require explicit commit/external-action scope, and report checkout status. Uncertain launch after tab creation, failure, or cancellation retains admission and exact recovery identities. Inspect `/subagent`, stop remaining owned workers, inspect and preserve changes, then restart Pi before new writes. Admission covers this Main's Pi calls, not other sessions or external processes.
|
|
66
68
|
|
|
67
69
|
The tool returns a handle after the first exact Herdr tab is launched. The remaining tabs are launched asynchronously; their identities are recorded on the session branch. A session-owned observer awaits each exact agent's lifecycle and retrieves its final answer from its Pi session file, not its possibly truncated screen. Idle, blocked, unknown, malformed, or oversized answers fail with recoverable tab/session identity; no whole-run deadline is imposed. `/subagent` shows exact recorded tabs on this session branch, distinguishing locally observed work from unobserved records. A session switch stops observation and carries tab identity into the new branch.
|
|
68
70
|
|
|
@@ -20,6 +20,6 @@ Run focused checks required by the task. Before reporting, remove only task-crea
|
|
|
20
20
|
|
|
21
21
|
Access credentials or the network, create extra artifacts, or broaden scope only when the task requires it. Never invoke external LLM APIs, SDKs, agent harnesses, or model CLIs.
|
|
22
22
|
|
|
23
|
-
Follow the task's commit policy. Direct delegation
|
|
23
|
+
Follow the task's commit policy. Direct delegation may write or commit only within Main's explicitly authorized scope; otherwise leave changes uncommitted. Isolated delegation commits completed scoped changes. Do not create or manage another worktree. Leave the assigned checkout and branch intact. Never push or open a pull request without explicit authorization.
|
|
24
24
|
|
|
25
25
|
Report briefly: outcome, commit, checks, and remaining risks.
|
package/extensions/admission.ts
CHANGED
|
@@ -9,7 +9,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
11
|
export function roleCanWrite(role: Role): boolean {
|
|
12
|
-
return role.tools.some((tool) => !READ_ONLY_TOOLS.has(tool));
|
|
12
|
+
return role.tools.some((tool) => !READ_ONLY_TOOLS.has(tool)) || role.extensions.length > 0 || Boolean(role.mcps?.length);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function roleIsReadOnlyScout(role: Role): boolean {
|
|
@@ -32,13 +32,15 @@ async function checkoutKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<str
|
|
|
32
32
|
* calls it makes through `ctx.executeTool()` (codemode scripts), so its nested
|
|
33
33
|
* writes pass while independent writers wait until it settles.
|
|
34
34
|
*/
|
|
35
|
-
export function registerCheckoutAdmission(pi: ExtensionAPI, directCanWrite: (input: unknown) => boolean): void {
|
|
35
|
+
export function registerCheckoutAdmission(pi: ExtensionAPI, directCanWrite: (input: unknown) => boolean): (toolCallId: string) => () => void {
|
|
36
|
+
const heldCalls = new Set<string>();
|
|
36
37
|
const ownerByCheckout = new Map<string, string>();
|
|
37
38
|
const checkoutByRoot = new Map<string, string>();
|
|
38
39
|
const writersByRoot = new Map<string, Set<string>>();
|
|
39
40
|
/** Model-issued ancestor of each in-flight call; Pi names nested calls `<parent id>/<n>`. */
|
|
40
41
|
const rootByCall = new Map<string, string>();
|
|
41
42
|
const release = (toolCallId: string) => {
|
|
43
|
+
if (heldCalls.has(toolCallId)) return;
|
|
42
44
|
const root = rootByCall.get(toolCallId);
|
|
43
45
|
if (root === undefined) return;
|
|
44
46
|
rootByCall.delete(toolCallId);
|
|
@@ -85,5 +87,16 @@ export function registerCheckoutAdmission(pi: ExtensionAPI, directCanWrite: (inp
|
|
|
85
87
|
checkoutByRoot.clear();
|
|
86
88
|
writersByRoot.clear();
|
|
87
89
|
rootByCall.clear();
|
|
90
|
+
heldCalls.clear();
|
|
88
91
|
});
|
|
92
|
+
// A direct handle settles the tool call before the worker settles. Keep its
|
|
93
|
+
// admission until exact completion; uncertain workers require manual recovery.
|
|
94
|
+
return (toolCallId) => {
|
|
95
|
+
const root = rootByCall.get(toolCallId);
|
|
96
|
+
if (root === undefined || !writersByRoot.get(root)?.has(toolCallId)) {
|
|
97
|
+
throw new Error(`Direct call ${toolCallId} was not admitted as a checkout writer; delegate again with the current Role resources.`);
|
|
98
|
+
}
|
|
99
|
+
heldCalls.add(toolCallId);
|
|
100
|
+
return () => { heldCalls.delete(toolCallId); release(toolCallId); };
|
|
101
|
+
};
|
|
89
102
|
}
|
package/extensions/subagent.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { availableTaskModels, loadTaskModelsConfig, modelReference, registerModelTask, resolveAvailableModel, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
|
-
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
|
|
5
|
+
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, DELEGATE_TASK, formatDuration, loadRoles, prepareRoleLaunch, resolveRolePackageResources, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
|
|
6
6
|
import { readSubagentConfig, resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
|
|
7
7
|
import { registerCheckoutAdmission, roleCanWrite, roleIsReadOnlyScout } from "./admission.ts";
|
|
8
8
|
import { registerIsolatedExtension } from "./isolated.ts";
|
|
@@ -181,7 +181,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
181
181
|
return true;
|
|
182
182
|
}
|
|
183
183
|
};
|
|
184
|
-
registerCheckoutAdmission(pi, canWrite);
|
|
184
|
+
const holdAdmission = registerCheckoutAdmission(pi, canWrite);
|
|
185
185
|
const isolatedSurface = registerIsolatedExtension(pi, {
|
|
186
186
|
executor,
|
|
187
187
|
policy: initialPolicy,
|
|
@@ -452,10 +452,10 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
452
452
|
pi.registerTool({
|
|
453
453
|
name: "delegate_task",
|
|
454
454
|
label: "Subagent",
|
|
455
|
-
description: `Delegate
|
|
456
|
-
promptSnippet: "Delegate
|
|
455
|
+
description: `Delegate explicitly authorized direct work in the shared checkout or a durable checked isolated graph to Pi Roles. Roles: ${roleSummary()}.`,
|
|
456
|
+
promptSnippet: "Delegate authorized shared-checkout work or a checked isolated task graph",
|
|
457
457
|
promptGuidelines: [
|
|
458
|
-
"Keep trivial mechanically verifiable work in Main. Use mode direct
|
|
458
|
+
"Keep trivial mechanically verifiable work in Main. Use mode direct for bounded, explicitly authorized work with the Role's declared resources in the shared checkout, including writes or commits. Specify allowed scope and exclusions; preserve unrelated changes. Direct work has no isolated checks, rollback, or integration guarantees. Use mode isolated for checked changesets; explicit mode never falls back.",
|
|
459
459
|
"Direct requests use one compact role/name/task packet, tasks for independent packets, or chain with {previous}. Direct work returns a Herdr handle after launch; results arrive as one follow-up message.",
|
|
460
460
|
"Isolation uses typed tasks and dependencies. Keep tightly coupled changes with one owner; do not split by file count. Failures, ambiguity, limits, and conflicts retain work and never waive checks or identity guards.",
|
|
461
461
|
`For delegate_task, ${MODEL_CLASS_GUIDANCE} A direct model replaces only the selected route's model; its thinking level stays unchanged.`,
|
|
@@ -482,8 +482,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
482
482
|
for (const delegation of workflow.delegations) {
|
|
483
483
|
const role = roles.get(delegation.role);
|
|
484
484
|
if (!role) throw boundedError(new Error(`Unknown Subagent role: ${delegation.role}. Available roles: ${[...roles.keys()].join(", ") || "none"}.`));
|
|
485
|
-
if (roleCanWrite(role)) throw new Error(`Role ${role.name} exposes write-capable or unverified resources. Use mode isolated for this Role; direct is read-only.`);
|
|
486
485
|
}
|
|
486
|
+
const writes = workflow.delegations.some((delegation) => roleCanWrite(roles.get(delegation.role)!));
|
|
487
487
|
const policy = currentPolicy();
|
|
488
488
|
const herdr = createDirectHerdr(pi, ctx.cwd, policy.childIdleMs);
|
|
489
489
|
const entries = identifyWorkflowEntries(toolCallId, workflow);
|
|
@@ -500,17 +500,25 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
500
500
|
const settled = new Promise<void>((resolve) => { resolveSettled = resolve; });
|
|
501
501
|
// Register before Herdr can create a tab: session switches must await this
|
|
502
502
|
// launch, including its identity callback, before copying recovery records.
|
|
503
|
+
const releaseAdmission = writes ? holdAdmission(toolCallId) : () => {};
|
|
503
504
|
directTasks.set(taskId, { controller, settled, handles, tabs });
|
|
504
505
|
const launch = async (entry: WorkflowEntry, activeSignal: AbortSignal) => {
|
|
505
506
|
activeSignal.throwIfAborted();
|
|
506
507
|
const role = loadRoles().find((candidate) => candidate.name === entry.delegation.role);
|
|
507
|
-
if (!role
|
|
508
|
+
if (!role) throw new Error(`Role ${entry.delegation.role} disappeared.`);
|
|
509
|
+
if (!writes && roleCanWrite(role)) throw new Error(`Role ${role.name} became write-capable after admission; delegate again with its current resources.`);
|
|
508
510
|
const context = latestCtx ?? ctx;
|
|
509
|
-
const
|
|
511
|
+
const resources = await resolveRolePackageResources(role, context);
|
|
512
|
+
const effectiveRole = { ...role, extensions: resources.extensions };
|
|
513
|
+
const route = prepareRoleLaunch(pi, context, { role: effectiveRole, task: DELEGATE_TASK,
|
|
510
514
|
...(entry.delegation.modelClass === undefined ? {} : { modelClass: entry.delegation.modelClass }) });
|
|
511
515
|
const prepared = entry.delegation.model === undefined ? route : prepareRoleLaunch(pi, context, {
|
|
512
|
-
role, route: replaceRouteModel(context, entry.delegation.model, route),
|
|
516
|
+
role: effectiveRole, route: replaceRouteModel(context, entry.delegation.model, route),
|
|
513
517
|
});
|
|
518
|
+
prepared.args.push("--no-prompt-templates", "--no-themes",
|
|
519
|
+
...resources.skills.flatMap((path) => ["--skill", path]),
|
|
520
|
+
...resources.prompts.flatMap((path) => ["--prompt-template", path]),
|
|
521
|
+
...resources.themes.flatMap((path) => ["--theme", path]));
|
|
514
522
|
states.set(entry.id, { ...states.get(entry.id)!, model: modelReference(prepared.model), thinkingLevel: prepared.thinkingLevel, status: "running", assistantOutput: "" });
|
|
515
523
|
startWidgetItem(entry.id, taskId, role.name, prepared.model.id, prepared.thinkingLevel, entry.delegation.name, ctx);
|
|
516
524
|
const transient = await materializeTransientLaunch({ launch: prepared, prompt: prepared.systemPrompt,
|
|
@@ -524,7 +532,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
524
532
|
pi.appendEntry(DIRECT_TAB_TYPE, record);
|
|
525
533
|
tabs.push(record);
|
|
526
534
|
tabByEntry.set(entry.id, record);
|
|
527
|
-
});
|
|
535
|
+
}, writes);
|
|
528
536
|
} catch (error) {
|
|
529
537
|
// A failed start can still be in flight. Keep its private prompt for recovery.
|
|
530
538
|
throw new Error(`${error instanceof Error ? error.message : String(error)}. Direct Role prompt retained at ${transient.launch.args[prepared.promptArgIndex + 1]} after uncertain start.`, { cause: error });
|
|
@@ -552,15 +560,17 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
552
560
|
finishWidgetItem(entries[0]!.id, "failure");
|
|
553
561
|
directTasks.delete(taskId);
|
|
554
562
|
resolveSettled();
|
|
563
|
+
if (!tabs.length) releaseAdmission();
|
|
555
564
|
throw boundedError(error);
|
|
556
565
|
} finally { signal?.removeEventListener("abort", abortLaunch); }
|
|
557
566
|
void (async () => {
|
|
558
567
|
try {
|
|
559
568
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
560
569
|
let active = 0;
|
|
570
|
+
let writerFailed = false;
|
|
561
571
|
const queue: Array<() => void> = [];
|
|
562
572
|
const permit = async () => {
|
|
563
|
-
if (active >= policy.maxSubagents) await new Promise<void>((resolve) => queue.push(resolve));
|
|
573
|
+
if (active >= (writes ? 1 : policy.maxSubagents)) await new Promise<void>((resolve) => queue.push(resolve));
|
|
564
574
|
active++;
|
|
565
575
|
return () => { active--; queue.shift()?.(); };
|
|
566
576
|
};
|
|
@@ -568,6 +578,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
568
578
|
const release = await permit();
|
|
569
579
|
try {
|
|
570
580
|
controller.signal.throwIfAborted();
|
|
581
|
+
if (writerFailed) throw new Error("Previous direct task did not complete exactly; inspect recorded tabs before new shared-checkout work.");
|
|
571
582
|
const handle = entry.index === 0 ? first : await launch(entry, controller.signal);
|
|
572
583
|
const answer = await handle.answer(Math.floor(40 * 1024 / entries.length));
|
|
573
584
|
try {
|
|
@@ -578,6 +589,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
578
589
|
finishWidgetItem(entry.id, "success");
|
|
579
590
|
return answer;
|
|
580
591
|
} catch (error) {
|
|
592
|
+
if (writes) writerFailed = true;
|
|
581
593
|
const { assistantOutput: _partial, ...base } = states.get(entry.id)!;
|
|
582
594
|
states.set(entry.id, { ...base, status: "rejected", failure: `${tabByEntry.has(entry.id) ? `recover from Herdr tab ${tabByEntry.get(entry.id)!.tabId}, agent ${tabByEntry.get(entry.id)!.name}, session ${tabByEntry.get(entry.id)!.sessionFile}: ` : ""}${capOutput(error instanceof Error ? error.message : String(error))}` });
|
|
583
595
|
finishWidgetItem(entry.id, "failure");
|
|
@@ -596,13 +608,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
596
608
|
status: "skipped",
|
|
597
609
|
});
|
|
598
610
|
}
|
|
611
|
+
if ([...states.values()].every(({ status }) => status === "succeeded")) releaseAdmission();
|
|
599
612
|
if (!controller.signal.aborted) reportDirect(launchEpoch, taskId, workflow.mode, [...states.values()], tabs);
|
|
600
613
|
directTasks.delete(taskId);
|
|
601
614
|
resolveSettled();
|
|
602
615
|
}
|
|
603
616
|
})();
|
|
604
617
|
return {
|
|
605
|
-
content: [{ type: "text" as const, text: capOutput(`Direct delegation started · ${taskId}\nHerdr tab: ${first.tabId} · pane: ${first.paneId} · agent: ${first.name}\nExact session: ${first.sessionFile}\nAll launched tabs: /subagent (on this session branch).\n${entries.length} task(s); result will arrive in one follow-up message
|
|
618
|
+
content: [{ type: "text" as const, text: capOutput(`Direct delegation started · ${taskId}\nHerdr tab: ${first.tabId} · pane: ${first.paneId} · agent: ${first.name}\nExact session: ${first.sessionFile}\nAll launched tabs: /subagent (on this session branch).\n${entries.length} task(s); result will arrive in one follow-up message. Direct work shares this checkout; completion is an exact answer, not checked integration. ${writes ? "Potential writers run serially; competing Pi writes stay blocked until exact completion. On failure or cancellation, inspect owned tabs before restarting Pi; changes are not rolled back." : "Concurrent changes may make reads stale."}`) }],
|
|
606
619
|
details: { taskId, mode: workflow.mode, tabId: first.tabId, sessionFile: first.sessionFile,
|
|
607
620
|
entries: entries.map(({ id, index, delegation }) => ({ id, index, name: delegation.name, role: delegation.role })) },
|
|
608
621
|
};
|
package/extensions/workflow.ts
CHANGED
|
@@ -107,7 +107,7 @@ export function parseWorkflow(value: unknown): ParsedWorkflow {
|
|
|
107
107
|
const mode = workflowMode(value);
|
|
108
108
|
if (!Check(DirectWorkflowSchema, value)) {
|
|
109
109
|
const issue = Errors(DirectWorkflowSchema, value)[0];
|
|
110
|
-
throw new Error(`direct workflow must match the declared tool schema${issue ? ` at ${issue.instancePath || "/"}: ${issue.message}` : ""}. Direct mode
|
|
110
|
+
throw new Error(`direct workflow must match the declared tool schema${issue ? ` at ${issue.instancePath || "/"}: ${issue.message}` : ""}. Direct mode returns task answers without checked integration; use mode isolated for changesets.`);
|
|
111
111
|
}
|
|
112
112
|
if (!mode) throw new Error("direct workflow must select exactly one of single, tasks, or chain.");
|
|
113
113
|
const input = value as DirectInput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "26.0.0",
|
|
4
4
|
"description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"pack:check": "npm pack --dry-run"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@earendil-works/pi-ai": "
|
|
44
|
-
"@earendil-works/pi-coding-agent": "
|
|
45
|
-
"@earendil-works/pi-tui": "
|
|
43
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
45
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
46
46
|
"typebox": "^1.3.15"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"proper-lockfile": "^4.1.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@earendil-works/pi-ai": "1.0.
|
|
78
|
-
"@earendil-works/pi-coding-agent": "1.0.
|
|
79
|
-
"@earendil-works/pi-tui": "1.0.
|
|
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",
|
|
80
80
|
"@types/proper-lockfile": "^4.1.4"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -11,8 +11,8 @@ Record the requested outcome, allowed scope and exclusions, local delegation/che
|
|
|
11
11
|
|
|
12
12
|
Keep trivial, mechanically verifiable work in Main. Choose the user-requested mode when one is explicit. Otherwise:
|
|
13
13
|
|
|
14
|
-
- Use `mode: direct` for bounded
|
|
15
|
-
- Use `mode: isolated` for
|
|
14
|
+
- Use `mode: direct` for bounded research, analysis, review, or explicitly authorized shared-checkout writes and commits using the selected Role's resources.
|
|
15
|
+
- Use `mode: isolated` for independently checked changes, or an exploratory or discardable candidate. A single isolated task is valid.
|
|
16
16
|
- Keep one implementation owner for tightly coupled work. Size alone does not justify splitting it.
|
|
17
17
|
- Never silently fall back from isolated to direct. Scope growth requires a checkpoint and replan, not migration of dirty work.
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ After delegation acknowledges a handle or durable request, do other work or end
|
|
|
22
22
|
|
|
23
23
|
# Direct requests
|
|
24
24
|
|
|
25
|
-
Use one compact `role`/`name`/`task` packet, `tasks` for independent packets, or `chain` for dependent packets. Only
|
|
25
|
+
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 exact completion. Main may read or do work elsewhere while waiting, not mutate the reserved checkout. On failure/cancellation or an uncertain launched tab, inspect `/subagent` identities, stop remaining owned workers, preserve and inspect changes, then restart Pi 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.
|
|
26
26
|
|
|
27
27
|
# Isolated requests
|
|
28
28
|
|