@henryqw/pi-subagent 15.1.4 → 16.0.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 +7 -10
- package/README.md +20 -53
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/dist/review-evidence.d.ts +1 -1
- package/dist/review-evidence.js +1 -1
- package/dist/worktree.d.ts +1 -1
- package/dist/worktree.js +5 -2
- package/docs/adr/001-composable-ephemeral-execution.md +2 -2
- package/docs/orchestration.md +10 -38
- package/extensions/role-tools.ts +33 -1
- package/extensions/subagent.ts +2 -30
- package/package.json +1 -5
- package/docs/adr/002-package-owned-delegate-flow-orchestration.md +0 -22
- package/docs/delegate-flow.html +0 -202
- package/docs/delegate-flow.svg +0 -181
- package/extensions/delegate-flow.ts +0 -932
- package/skills/pi-subagent-delegated-development/SKILL.md +0 -38
package/CONTEXT.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Provide validated built-in and user Roles, shared task-model Pi launch policy, generic `delegate_task` delegation, and
|
|
5
|
+
Provide validated built-in and user Roles, shared task-model Pi launch policy, generic `delegate_task` delegation, and public executor, worktree, and exact-review-evidence APIs. Main or a consuming package owns any checked implementation protocol.
|
|
6
6
|
|
|
7
7
|
## Domain glossary
|
|
8
8
|
|
|
@@ -14,24 +14,21 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
|
|
|
14
14
|
- **Delegated Task**: one bounded work request sent from Main to one Role.
|
|
15
15
|
- **Workflow**: generic orchestration of one or more Delegated Tasks; `delegate_task` owns its selected mode, while library callers compose executor runs in JavaScript.
|
|
16
16
|
- **Workflow Mode**: `delegate_task` tool policy selected per call for `single`, `parallel`, or `chain` execution; not a Role property or executor API.
|
|
17
|
-
- **
|
|
18
|
-
- **Unit Worktree**: one Flow-owned worktree and branch for one Flow unit; it is reused for rebase, validation, optional exact review, and one repair.
|
|
19
|
-
- **Review Packet**: exact `{base, tip, patchPath}` evidence delivered to the Flow Reviewer only for a unit's explicit `review` criterion.
|
|
17
|
+
- **Exact Review Evidence**: caller-requested private base-to-tip patch plus exact `{base, tip, patchPath}` identity prepared by `prepareExactReviewEvidence`.
|
|
20
18
|
- **Resource Policy**: Role ownership of base tools, extensions, and Skill names, plus explicit caller additions of tools, extensions, and environment through `createRoleLaunch`.
|
|
21
19
|
- **Pi Launch**: reusable `{env,args}` policy for one Role, resolved model route, explicit caller resources, and project trust.
|
|
22
20
|
- **Ephemeral Executor**: mechanism that receives a prepared Pi Launch, runs one bounded Delegated Task in one no-session child process, and returns its result without discovering resources or composing a Workflow.
|
|
23
21
|
|
|
24
22
|
## Invariants
|
|
25
23
|
|
|
26
|
-
- One Delegated Task creates one ephemeral child process and no saved session. Execution ends at the first hard budget: attempted turn 51 by default (`maxTurns` is a safe integer >= 1; default 50), attempted continuation after the optional token handoff, or `deadline = min(last recognized Pi JSON event + idle timeout, child start + maximum runtime)` (recognized Pi events renew; raw bytes do not; max always terminates). A terminal turn 50 succeeds; attempted continuation rejects with `turn_limit`, accumulated usage, and bounded output. Optional `maxTokens` is a safe integer >= 1 with an unlimited default. It is one global executor default applied independently to every `delegate_task` child
|
|
27
|
-
- Every Role launch installs the shared tool policy. On a continuing token crossing, or the continuing penultimate `maxTurns` turn, the policy waits for `turn_end`, disables every tool, and steers one structured final report. This covers `delegate_task` and
|
|
24
|
+
- One Delegated Task creates one ephemeral child process and no saved session. Execution ends at the first hard budget: attempted turn 51 by default (`maxTurns` is a safe integer >= 1; default 50), attempted continuation after the optional token handoff, or `deadline = min(last recognized Pi JSON event + idle timeout, child start + maximum runtime)` (recognized Pi events renew; raw bytes do not; max always terminates). A terminal turn 50 succeeds; attempted continuation rejects with `turn_limit`, accumulated usage, and bounded output. Optional `maxTokens` is a safe integer >= 1 with an unlimited default. It is one global executor default applied independently to every `delegate_task` child; it is neither a shared pool nor a per-call or environment option. Token accounting sums each completed assistant response's `Usage.totalTokens` once, matching the executor's aggregate `Usage`. A terminal response crossing the limit succeeds. A continuing crossing turn and its tools finish, then exactly one extra response turn is permitted. Further continuation rejects with typed `token_limit`, aggregate usage, and bounded last assistant output. This permits a crossing turn plus the final response to overshoot; it is not an exact hard cap. Raw executor launches enforce the extra-turn window but do not guarantee tools are disabled.
|
|
25
|
+
- Every Role launch installs the shared tool policy. On a continuing token crossing, or the continuing penultimate `maxTurns` turn, the policy waits for `turn_end`, disables every tool, and steers one structured final report. This covers `delegate_task` and library Role launches made with the public launch API. With `maxTurns` set to 1, tools are disabled during `session_start`, and the sole provider turn is the response-only handoff. The fixed decision packet is the default, but exact task or Role output takes precedence and is returned alone. Terminal boundary responses get no handoff. Before the final boundary, the policy steers the fixed convergence warning once at each 80% threshold for completed turns, aggregate tokens when configured, and maximum runtime. It combines thresholds first due together and starts no timer or extra warning turn. Timeout, provider, or child-process failures can prevent a handoff. After direct Pi exits, inherited stdout/stderr drain until EOF unless an escaped descendant holds them past short inactivity or a one-second hard deadline. Configurable in `~/.pi/agent/config/pi-subagent/config.json` (`maxTurns` defaults to 50; `maxTokens` defaults to unlimited; `timeout.idleMinutes`/`maxMinutes` default to 10/30).
|
|
28
26
|
- Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/config.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout.
|
|
29
27
|
- Ambient child extensions and Skills stay disabled. Every Role requires `tools`, `extensions`, and `skills` YAML arrays, and every launch installs the Role tool policy. `tools: []` activates no base built-ins but does activate all tools from explicitly selected trusted extension bundles and explicit caller tool additions; `skills: []` selects no separately named Role Skills but trusted selected extension Skills still load; `extensions: []` selects no Role extension bundle. A Role/caller explicitly selected extension is a trusted atomic capability bundle: all tools it registers and all Skills supplied through its Pi package metadata or dynamic `resources_discover` load alongside separately named Role Skills. This intentionally includes the extension's executable lifecycle/prompt behavior; pi-subagent does not infer or externally narrow undocumented dependencies, and loading an extension is not sandboxing. Scope children by selecting fewer trusted extensions; finer granularity requires separate entry points/configuration or an upstream split. Explicit Role/caller tool names still verify against the final filtered registry, while parent-only recursive orchestration tools remain excluded.
|
|
30
28
|
- Role Skill names resolve through Main's effective Pi Skill registry; unavailable names warn and skip without blocking delegation. Explicit Role/caller tool names verify against the final filtered child registry after explicit provider `session_start` handlers, and unavailable names fail before the first turn.
|
|
31
29
|
- Route precedence is explicit call-level `modelClass` > Role `modelClass` > configured Model Task assignment or declared default. pi-subagent's local `pi-subagent/delegateTask` declaration defaults to `fast`. A direct model replaces only the selected route model and must honor its exact thinking level. Library callers select a Role plus their own Model Task declaration.
|
|
32
30
|
- The selected profile resolves primary then fallback only before launch when a route, model, or thinking level is unavailable. A direct model never changes the route level and fails before launch if it cannot honor it. A missing local JSON config uses defaults quietly. Missing shared task-model config warns once per session because delegation needs a route. If neither route is usable, launch rejects with `Run /task-models`; a started child is never retried by this package.
|
|
33
|
-
- User Role Markdown files and Subagent JSON config (`config/pi-subagent/config.json`) live only in the user `config/pi-subagent` directory; model routes live in shared `config/pi-task-models/config.json`. Package-shipped built-in Roles (`implementer`, `reviewer`, `scout`) resolve from the package's own `examples/roles/` Markdown through the same parser and intentionally leave `modelClass` unset; same-named user files override built-ins for `delegate_task
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge validation. It never changes generic `delegate_task` Role resolution, isolation, non-Git fallback, or ordinary direct plan/file review.
|
|
31
|
+
- User Role Markdown files and Subagent JSON config (`config/pi-subagent/config.json`) live only in the user `config/pi-subagent` directory; model routes live in shared `config/pi-task-models/config.json`. Package-shipped built-in Roles (`implementer`, `reviewer`, `scout`) resolve from the package's own `examples/roles/` Markdown through the same parser and intentionally leave `modelClass` unset; same-named user files override built-ins for `delegate_task` and public API callers. The `synthesizer` Markdown remains the only optional inert sample.
|
|
32
|
+
- Low-level worktree APIs allocate optional isolated child worktrees and return explicit `pruned`, `retained`, or `recovery` lifecycle payloads. They never force-delete uncertain work or choose integration policy for callers.
|
|
33
|
+
- `prepareExactReviewEvidence` validates Git identity and produces a bounded private patch with exact base and tip OIDs. The caller owns review criteria, verdict validation, integration, cleanup, and durable state.
|
|
37
34
|
- Numbered Codex routes prefer Main's active account slot and explicitly load the multi-Codex child extension.
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Delegate bounded work from Main to isolated Pi Roles
|
|
3
|
+
Delegate bounded work from Main to isolated Pi Roles. `delegate_task` remains lightweight generic delegation. It does not own durable checked implementation graphs.
|
|
4
|
+
|
|
5
|
+
The public API also supplies Role launch, executor, worktree, and exact-evidence support for packages such as `@henryqw/pi-orchestrator`.
|
|
4
6
|
|
|
5
7
|

|
|
6
|
-

|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -18,6 +19,7 @@ Run `/task-models` and configure the `fast` profile before delegating. Open `/ta
|
|
|
18
19
|
|
|
19
20
|
| Package | Relationship | Purpose |
|
|
20
21
|
| --- | --- | --- |
|
|
22
|
+
| [`@henryqw/pi-orchestrator`](https://pi.henry.wang/extensions/pi-orchestrator) | Consumer | Owns durable checked local implementation graphs. |
|
|
21
23
|
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Supplies `fast`, `balanced`, `frontier`, and `fav` model routes. |
|
|
22
24
|
|
|
23
25
|
Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides. Missing shared model config warns once because delegation needs a route.
|
|
@@ -39,9 +41,6 @@ A separate child returns a bounded report to Main. It creates no saved Pi sessio
|
|
|
39
41
|
| Surface | Type | Purpose |
|
|
40
42
|
| --- | --- | --- |
|
|
41
43
|
| `delegate_task` | tool | Run one bounded task, independent tasks in parallel, or dependent tasks in a chain. |
|
|
42
|
-
| `delegate_flow` | tool | Implement and integrate 1–8 independent Git units. |
|
|
43
|
-
| `delegate_flow_continue` | tool | Repair the one blocked Flow unit once. |
|
|
44
|
-
| `pi-subagent-delegated-development` | skill | Guide Main's planning and orchestration. |
|
|
45
44
|
|
|
46
45
|
Pi's built-in tool block shows each call and result.
|
|
47
46
|
|
|
@@ -68,7 +67,7 @@ Parallel tasks start together, settle together, and report in input order. Chain
|
|
|
68
67
|
|
|
69
68
|
Foreground failures throw after keeping bounded sibling and recovery evidence. One call has one aggregate 50 KiB cap for Main-visible text. Final results show summaries first and full evidence below.
|
|
70
69
|
|
|
71
|
-
The status widget shows each task group name above at most three indented child rows. Each row shows a one-letter Role badge, status, activity, usage, and duration.
|
|
70
|
+
The status widget shows each task group name above at most three indented child rows. Each row shows a one-letter Role badge, status, activity, usage, and duration.
|
|
72
71
|
|
|
73
72
|
Background work belongs to its launching session. Shutdown or reload aborts it and may leave only recoverable-work evidence or no follow-up message.
|
|
74
73
|
|
|
@@ -76,13 +75,13 @@ Each entry resolves its own Role, resources, route, and optional isolation. A Ro
|
|
|
76
75
|
|
|
77
76
|
See the [orchestration guide](./docs/orchestration.md) for full delegation, transport, isolation, and UI behavior.
|
|
78
77
|
|
|
79
|
-
###
|
|
78
|
+
### Delegation guidance
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
`delegate_task` is generic delegation, not checked implementation orchestration. Main or a consuming package owns validation, review, integration, recovery, and durable state.
|
|
82
81
|
|
|
83
82
|
Implementers remove only task-created temporary, generated, or ignored artifacts. Required deliverables and unrelated files stay intact. They never use `git clean` or blanket deletion, and unclear paths block.
|
|
84
83
|
|
|
85
|
-
For known regressions with a runner that supports test-name filtering, use a test-name filter. Keep broad package or workspace checks to one caller-owned final validation after relevant
|
|
84
|
+
For known regressions with a runner that supports test-name filtering, use a test-name filter. Keep broad package or workspace checks to one caller-owned final validation after relevant changes integrate. Generic delegation does not run that check.
|
|
86
85
|
|
|
87
86
|
Before delegating:
|
|
88
87
|
|
|
@@ -98,46 +97,15 @@ Its ordinary review loop is optional. Use it only when the caller or repository
|
|
|
98
97
|
|
|
99
98
|
- Call `delegate_task` with `role: "reviewer"` to select the effective `reviewer` Role.
|
|
100
99
|
- Its task packet must state the read-only scope and exact `PASS` or findings contract. Include exact acceptance criteria and validation evidence.
|
|
101
|
-
- The Reviewer must
|
|
100
|
+
- The Reviewer must receive exact candidate evidence. An ordinary review from Main's unchanged checkout cannot inspect an isolated candidate.
|
|
102
101
|
- Fix initial findings together. Validate repaired inputs once before focused re-review. Include the original findings and acceptance criteria, exact repaired-candidate evidence, and validation evidence.
|
|
103
102
|
- Only `PASS` completes the loop. Surface and block on re-review findings or empty output. Retry empty output only when explicit caller policy requires one. A second empty result blocks. Do not add another round.
|
|
104
103
|
|
|
105
|
-
Flow is separate. It owns exact review evidence, exact `PASS` approval, validation replay, one repair continuation, and no automatic retry.
|
|
106
|
-
|
|
107
|
-
## Flow
|
|
108
|
-
|
|
109
|
-
Flow requires a clean Main worktree on an attached branch with a committed `HEAD`. Use it only for independent Git changes that can merge in any order.
|
|
110
|
-
|
|
111
|
-
Keep work together or run it in order when a split divides an invariant or adds coordination. Do not split units with overlapping mutable ownership. Do not split units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.
|
|
112
|
-
|
|
113
|
-
One Implementer launch must plausibly finish before the configured maximum runtime. Cohesion is not enough when work has several preservable, separately verifiable milestones. Split oversized dependent work into serial one-unit Flows after each milestone integrates. Units in one Flow stay independent and commuting.
|
|
114
|
-
|
|
115
|
-
```text
|
|
116
|
-
delegate_flow({ units: [{ id, name, task, modelClass?, validation: [{ command, args }], review? }] })
|
|
117
|
-
delegate_flow_continue({ guidance, modelClass? })
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
A Flow has 1–8 units with unique non-empty IDs and allows one active Flow. It freezes the effective Implementer at start. It freezes the Reviewer only when a unit requests `review`.
|
|
121
|
-
|
|
122
|
-
- Each unit gets one worktree. Implementers run in parallel. Flow handles units in declared order.
|
|
123
|
-
- Flow runs each declared command with its arguments. That validation is authoritative for objective checks.
|
|
124
|
-
- Without `review`, Flow fast-forwards the exact validated tip.
|
|
125
|
-
- With `review`, the Reviewer receives the exact `{base, tip, patchPath}` packet and must return exactly `PASS` before the same integration path. Use `review` only for stated judgment that validation cannot decide.
|
|
126
|
-
- Trust a successful Flow result. Do not re-read integrated files or repeat its validation merely to confirm it.
|
|
127
|
-
|
|
128
104
|
A Role selects base tools, extensions, named Skills, instructions, and optional worktree isolation. Named Skills resolve from Main's effective Pi registry. Unavailable names warn and skip.
|
|
129
105
|
|
|
130
106
|
Children disable ambient extension and Skill discovery. `tools: []` adds no base tools, but selected extension tools and caller tools still activate. `extensions: []` adds no Role extension bundle. `skills: []` adds no separately named Role Skills, but selected extension Skills still load.
|
|
131
107
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
An explicit unit `modelClass` overrides both frozen Roles. Without one, each Role uses its own `modelClass`, configured `pi-subagent/delegateTask` assignment, or declared default.
|
|
135
|
-
|
|
136
|
-
One `delegate_flow_continue` can repair an Implementer, validation, or review block in the same worktree. Omitting its class keeps the unit's explicit class and frozen Role defaults. Supplying one replaces both Role defaults for that repair and its later Reviewer launch.
|
|
137
|
-
|
|
138
|
-
A second block is terminal. Rebase and infrastructure failures are terminal.
|
|
139
|
-
|
|
140
|
-
Flow has no dependency graph, saved recovery, automatic retry, aggregate review, or post-merge validation.
|
|
108
|
+
Main-only delegation and orchestration tools, plus `ask_question`, are always excluded. Requested Role or caller tool names are checked after provider loading. Unavailable tools fail before the first model turn.
|
|
141
109
|
|
|
142
110
|
## Config
|
|
143
111
|
|
|
@@ -151,7 +119,7 @@ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. It is optional. A
|
|
|
151
119
|
| `timeout.idleMinutes` | Sets the idle timeout for a child. | Positive minutes; minutes × 60,000 ≤ 2,147,483,647 ms | `10` |
|
|
152
120
|
| `timeout.maxMinutes` | Sets the maximum runtime for a child. | Positive minutes greater than `idleMinutes`; minutes × 60,000 ≤ 2,147,483,647 ms | `30` |
|
|
153
121
|
|
|
154
|
-
`maxTokens` applies separately to every child.
|
|
122
|
+
`maxTokens` applies separately to every child. It is not a shared pool or per-call option. Set it only in this file.
|
|
155
123
|
|
|
156
124
|
Pi adds each completed assistant response's `Usage.totalTokens` once. This matches the executor's aggregate `Usage`. At 80%, a Role receives one convergence warning.
|
|
157
125
|
|
|
@@ -161,13 +129,13 @@ Excess children wait FIFO without using a child timeout. A terminal response on
|
|
|
161
129
|
|
|
162
130
|
### Final response handoff
|
|
163
131
|
|
|
164
|
-
Role launches reserve a response-only handoff at a continuing `maxTokens` crossing or the penultimate `maxTurns` turn. This includes `delegate_task` and
|
|
132
|
+
Role launches reserve a response-only handoff at a continuing `maxTokens` crossing or the penultimate `maxTurns` turn. This includes `delegate_task` and Role launches made through the public API.
|
|
165
133
|
|
|
166
134
|
With `maxTurns` set to 1, Pi disables tools at startup. The sole provider turn is the response-only handoff.
|
|
167
135
|
|
|
168
136
|
Pi waits for the current turn's tools. It then disables all tools and requests a final report. A terminal boundary response gets no handoff.
|
|
169
137
|
|
|
170
|
-
The fixed decision packet asks for Status (completed, blocked, or incomplete), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence. The child returns only that output, such as
|
|
138
|
+
The fixed decision packet asks for Status (completed, blocked, or incomplete), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence. The child returns only that caller-required exact output, such as structured JSON or `PASS`. The handoff stays within `maxTurns`, but it is the one allowed turn after a token crossing. Structured executor and retained-worktree facts remain authoritative; the model handoff supplies semantic context and a suggested next action.
|
|
171
139
|
|
|
172
140
|
A raw `createEphemeralSubagentExecutor` launch can enforce the extra-turn window. It cannot guarantee disabled tools or the final handoff. A timeout, provider failure, or child-process failure can also end a Role launch before handoff.
|
|
173
141
|
|
|
@@ -190,19 +158,17 @@ Role Markdown files live beside the config file. They require frontmatter and a
|
|
|
190
158
|
| `skills` | Required YAML array of non-empty Skill names. |
|
|
191
159
|
| body | Required Markdown system prompt after the frontmatter. |
|
|
192
160
|
|
|
193
|
-
A Role's `modelClass` is a default. A call-level
|
|
161
|
+
A Role's `modelClass` is a default. A call-level class wins.
|
|
194
162
|
|
|
195
163
|
An unreadable or invalid Role fails loading fast. Duplicate Role names are rejected. A same-named user file overrides a built-in Role.
|
|
196
164
|
|
|
197
|
-
The package always provides these built-in Roles. Their files leave `modelClass` unset, so they use the configured `pi-subagent/delegateTask` assignment or declared default unless a call
|
|
165
|
+
The package always provides these built-in Roles. Their files leave `modelClass` unset, so they use the configured `pi-subagent/delegateTask` assignment or declared default unless a call overrides it:
|
|
198
166
|
|
|
199
167
|
| Role | Purpose | Isolation/use |
|
|
200
168
|
| --- | --- | --- |
|
|
201
169
|
| `implementer` | Make and validate one focused change. | Requests a worktree; commits scoped work locally. Never pushes or opens a PR without permission. |
|
|
202
|
-
| `reviewer` | Review supplied plans or
|
|
203
|
-
| `scout` | Map code and evidence for one bounded task. | Read-only
|
|
204
|
-
|
|
205
|
-
Flow uses the effective Implementer and, only when requested, Reviewer. The Scout is not part of Flow.
|
|
170
|
+
| `reviewer` | Review supplied plans, files, or caller-prepared exact evidence. | Read-only; never edits or commits. |
|
|
171
|
+
| `scout` | Map code and evidence for one bounded task. | Read-only; never changes files. |
|
|
206
172
|
|
|
207
173
|
## API
|
|
208
174
|
|
|
@@ -216,6 +182,7 @@ The package root includes these main exports:
|
|
|
216
182
|
| `createRoleLaunch` | function | Builds launch arguments from a resolved route. |
|
|
217
183
|
| `createEphemeralSubagentExecutor` | function | Creates the bounded child-process executor. |
|
|
218
184
|
| Worktree helpers | functions | Create, inspect, finalize, and report child worktrees. |
|
|
185
|
+
| `prepareExactReviewEvidence` | function | Create a bounded private base-to-tip patch with exact Git identity for caller-owned review. |
|
|
219
186
|
|
|
220
187
|
The executor works only inside the active Pi process. It does not discover or start a standalone Node.js Pi installation.
|
|
221
188
|
|
|
@@ -227,6 +194,6 @@ See the [public Role and executor API](./docs/orchestration.md#public-role-and-e
|
|
|
227
194
|
|
|
228
195
|
An explicitly selected extension is trusted, not sandboxed. Its tools, Skills, and executable behavior load together. Select fewer trusted extensions to reduce scope. pi-subagent does not guess or remove undocumented dependencies.
|
|
229
196
|
|
|
230
|
-
|
|
197
|
+
Worktree cleanup never force-deletes recoverable work. Retained and recovery payloads report the worktree path and branch.
|
|
231
198
|
|
|
232
|
-
See [
|
|
199
|
+
See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for worktree and exact-review-evidence contracts.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-te
|
|
|
4
4
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutionBudget, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
5
5
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
6
6
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
|
7
|
+
export declare const PI_ORCHESTRATOR_PROCESS_LEASE = "PI_ORCHESTRATOR_PROCESS_LEASE";
|
|
7
8
|
export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
8
|
-
export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "
|
|
9
|
+
export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "ask_question", "orchestrate_execute", "orchestrate_status", "orchestrate_resume", "orchestrate_abort"];
|
|
9
10
|
export declare const CHILD_EXCLUDED_TOOLS: string;
|
|
10
11
|
export declare const DELEGATE_TASK: {
|
|
11
12
|
readonly id: "pi-subagent/delegateTask";
|
package/dist/index.js
CHANGED
|
@@ -12,8 +12,16 @@ export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, }
|
|
|
12
12
|
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
|
13
13
|
const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
|
|
14
14
|
const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
|
|
15
|
+
export const PI_ORCHESTRATOR_PROCESS_LEASE = "PI_ORCHESTRATOR_PROCESS_LEASE";
|
|
15
16
|
export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
16
|
-
export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
17
|
+
export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
18
|
+
"delegate_task",
|
|
19
|
+
"ask_question",
|
|
20
|
+
"orchestrate_execute",
|
|
21
|
+
"orchestrate_status",
|
|
22
|
+
"orchestrate_resume",
|
|
23
|
+
"orchestrate_abort",
|
|
24
|
+
];
|
|
17
25
|
export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
|
|
18
26
|
const CHILD_IDENTITY_POLICY = "You are a delegated Pi Subagent, not Main. Execute the assigned Role and task directly. Main-only delegation rules do not apply. Recursive delegation is unavailable; do not seek or invoke delegation tools.";
|
|
19
27
|
export const DELEGATE_TASK = {
|
|
@@ -13,5 +13,5 @@ export interface PreparedReviewEvidence {
|
|
|
13
13
|
patchPath: string;
|
|
14
14
|
cleanup: () => Promise<void>;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** Prepare one exact private patch for caller-owned review. */
|
|
17
17
|
export declare function prepareExactReviewEvidence(request: PrepareExactReviewEvidenceInput, signal?: AbortSignal): Promise<PreparedReviewEvidence>;
|
package/dist/review-evidence.js
CHANGED
|
@@ -200,7 +200,7 @@ async function makeEvidenceDirectory() {
|
|
|
200
200
|
cleanup: () => cleanup ??= fs.rm(directory, { recursive: true, force: true }),
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
-
/**
|
|
203
|
+
/** Prepare one exact private patch for caller-owned review. */
|
|
204
204
|
export async function prepareExactReviewEvidence(request, signal) {
|
|
205
205
|
const requestedWorktree = requestText(request.worktree, "review worktree");
|
|
206
206
|
const worktree = await fs.realpath(requestedWorktree);
|
package/dist/worktree.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare class WorktreeSetupError extends Error {
|
|
|
40
40
|
* repository (unwritable path, stale branch, git lock) throw so an isolated
|
|
41
41
|
* role never silently loses its isolation.
|
|
42
42
|
*/
|
|
43
|
-
export declare function createChildWorktree(cwd: string, childId: string, run?: GitRunner, signal?: AbortSignal): Promise<WorktreeInfo | undefined>;
|
|
43
|
+
export declare function createChildWorktree(cwd: string, childId: string, run?: GitRunner, signal?: AbortSignal, onPrepared?: (worktree: WorktreeInfo) => Promise<void>): Promise<WorktreeInfo | undefined>;
|
|
44
44
|
export interface WorktreeDirtyInspection {
|
|
45
45
|
dirty: boolean;
|
|
46
46
|
failure?: string;
|
package/dist/worktree.js
CHANGED
|
@@ -69,7 +69,7 @@ async function ensureLocalExclude(gitDir) {
|
|
|
69
69
|
* repository (unwritable path, stale branch, git lock) throw so an isolated
|
|
70
70
|
* role never silently loses its isolation.
|
|
71
71
|
*/
|
|
72
|
-
export async function createChildWorktree(cwd, childId, run = runGit, signal) {
|
|
72
|
+
export async function createChildWorktree(cwd, childId, run = runGit, signal, onPrepared) {
|
|
73
73
|
const root = await run(["rev-parse", "--show-toplevel"], cwd, signal);
|
|
74
74
|
if (root.code !== 0) {
|
|
75
75
|
signal?.throwIfAborted();
|
|
@@ -134,6 +134,10 @@ export async function createChildWorktree(cwd, childId, run = runGit, signal) {
|
|
|
134
134
|
const name = `subagent-${sanitizeShortId(childId)}`;
|
|
135
135
|
const branch = `${BRANCH_NAMESPACE}/${name}`;
|
|
136
136
|
const path = join(worktreesRoot, name);
|
|
137
|
+
const worktree = { path, cwd: join(path, relativeCwd), branch, repoRoot: stableRepoRoot, baseCommit };
|
|
138
|
+
if (onPrepared)
|
|
139
|
+
await onPrepared(worktree);
|
|
140
|
+
signal?.throwIfAborted();
|
|
137
141
|
try {
|
|
138
142
|
await mkdir(worktreesRoot, { recursive: true });
|
|
139
143
|
}
|
|
@@ -142,7 +146,6 @@ export async function createChildWorktree(cwd, childId, run = runGit, signal) {
|
|
|
142
146
|
}
|
|
143
147
|
await ensureLocalExclude(gitDir);
|
|
144
148
|
signal?.throwIfAborted();
|
|
145
|
-
const worktree = { path, cwd: join(path, relativeCwd), branch, repoRoot: stableRepoRoot, baseCommit };
|
|
146
149
|
const added = await run(["worktree", "add", path, "-b", branch, baseCommit], repoRoot, signal);
|
|
147
150
|
if (added.code !== 0) {
|
|
148
151
|
throw new WorktreeSetupError(`git worktree add failed after attempting path=${JSON.stringify(path)} branch=${JSON.stringify(branch)} base=${baseCommit}: ${added.stderr.trim().slice(0, 200)}`, worktree);
|
|
@@ -18,8 +18,8 @@ An explicit call `modelClass` selects a task-model route. Without one, an option
|
|
|
18
18
|
|
|
19
19
|
## Scope boundary
|
|
20
20
|
|
|
21
|
-
`
|
|
21
|
+
`delegate_task` and library callers remain generic. The package does not own checked implementation orchestration.
|
|
22
22
|
|
|
23
23
|
## Consequences
|
|
24
24
|
|
|
25
|
-
The executor remains a stable mechanism while
|
|
25
|
+
The executor remains a stable mechanism while callers own semantic protocols, shared workspace and state, validation, review, retry decisions, integration, cleanup, and bounds.
|
package/docs/orchestration.md
CHANGED
|
@@ -10,9 +10,7 @@ caller-owned Model Task declaration, cwd, signal ────────┤
|
|
|
10
10
|
active-Pi ephemeral executor
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Main plans and orchestrates. `delegate_task`
|
|
14
|
-
|
|
15
|
-
`delegate_flow` is the exception: it is a fixed package-owned Git workflow, not an executor primitive or general workflow language. It uses the effective `implementer` Role and, only for explicit judgment review, the effective `reviewer` Role through the same prepared-child runner; its contract is below.
|
|
13
|
+
Main plans and orchestrates. `delegate_task` remains lightweight generic delegation with flat single, parallel, and chain modes. The public executor runs one prepared delegation. Downstream packages compose their own workflows with ordinary JavaScript and own semantic protocols, shared workspace and state, checks, integration, retry decisions, and bounds. There is no recursive workflow AST.
|
|
16
14
|
|
|
17
15
|
## Frozen `delegate_task` contract
|
|
18
16
|
|
|
@@ -81,33 +79,6 @@ Any foreground entry failure makes the tool call throw. Parallel mode first sett
|
|
|
81
79
|
|
|
82
80
|
All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transport cap, including child output, sibling failures, and worktree/recovery evidence. Parallel execution does not multiply the cap by its entry count. Truncation is explicit; internal bookkeeping is not made visible by bypassing the cap.
|
|
83
81
|
|
|
84
|
-
## `delegate_flow`
|
|
85
|
-
|
|
86
|
-
`delegate_flow({ units })` accepts 1–8 units with unique non-empty `id`, a required Main-supplied short `name` (about five words and fewer than 30 characters, without C0/C1 control characters), a non-empty `task`, one or more direct `{command, args}` validation commands, optional `modelClass`, and optional non-empty `review` text. `delegate_flow_continue({ guidance, modelClass? })` is available only for the one blocked unit of the active Flow.
|
|
87
|
-
|
|
88
|
-
A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. An explicit unit class overrides both frozen Roles. Without one, the Implementer and Reviewer each use their own optional Role default; a Role without one uses pi-subagent's local `pi-subagent/delegateTask` assignment or declared `fast` default. The selected class resolves through its existing `pi-task-models` profile model-and-thinking route. It requires clean committed Git Main and creates every Unit Worktree before launching work; setup failure launches no Implementer. Each unit gets exactly one worktree and one Implementer. Implementers run in parallel and all settle. Flow then processes units in declared order:
|
|
89
|
-
|
|
90
|
-
```text
|
|
91
|
-
Implementers (parallel, one Unit Worktree each)
|
|
92
|
-
│ all settle
|
|
93
|
-
v
|
|
94
|
-
for each declared unit:
|
|
95
|
-
rebase in its Unit Worktree when earlier units advanced Main
|
|
96
|
-
inspect committed state; run declared validation (objective authority)
|
|
97
|
-
├─ no review: git merge --ff-only <exact validated tip>
|
|
98
|
-
└─ review: Reviewer receives exact {base, tip, patchPath}
|
|
99
|
-
exact PASS → git merge --ff-only <full reviewed OID>
|
|
100
|
-
git worktree remove; git branch -d
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Flow derives identity from Git, not child output. Add `review` only for an explicit judgment criterion that automated validation cannot establish; it is not a second generic verification pass. The Reviewer reads the exact patch as authoritative and may use the same worktree only for referenced context. A full-OID fast-forward is the only integration path. Cleanup is non-forced; after a successful integration, cleanup refusal returns `completed` with a retained path/branch warning.
|
|
104
|
-
|
|
105
|
-
If rebase drops all unit commits, `base === tip` is a no-op: Flow validates current state, skips Reviewer and merge, then cleans up ordinarily. Implementer failure, dirty or missing committed work, validation failure, or reviewer findings block the first affected declared unit. `delegate_flow_continue({ guidance, modelClass? })` reruns the Flow's frozen Implementer Role in that same worktree once, then repeats derivation, validation, and conditional review with fresh exact evidence. An omitted continuation class retains a supplied Unit class; when no Unit class was supplied, each frozen Role uses its own default. A supplied continuation class replaces both Role defaults for that repair and any subsequent Reviewer launch. A second block is terminal. A failed rebase is aborted and terminates as an infrastructure failure with Git diagnostics; other infrastructure failures are terminal. A reported fast-forward failure completes with its diagnostic as a warning only when Main is clean at the exact integrated tip; otherwise it is terminal. Terminal outcomes retain worktrees for Main to reslice. Earlier integrated units are never rolled back.
|
|
106
|
-
|
|
107
|
-
Flow has no dependency graph, saved state, automatic retry, aggregate review, or post-merge validation. Use it only for commuting changes; combine or sequence units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.
|
|
108
|
-
|
|
109
|
-
`delegate_task` remains generic: its optional worktree isolation, non-Git behavior, and direct plan/file review are unchanged. The built-in `scout` is available only through generic `delegate_task`; Flow uses package-shipped Implementer and conditional Reviewer Roles as defaults while retaining same-named user Role overrides.
|
|
110
|
-
|
|
111
82
|
## Per-delegation resources and isolation
|
|
112
83
|
|
|
113
84
|
`delegate_task` first preflights every requested Role name, so an initially unknown Role starts no sibling. Then, after receiving an executor permit, every single entry, parallel sibling, and chain step independently:
|
|
@@ -119,7 +90,7 @@ Flow has no dependency graph, saved state, automatic retry, aggregate review, or
|
|
|
119
90
|
|
|
120
91
|
Separate deterministic identities produce separate worktree paths and branches. Parallel siblings cannot collide, and a chain does not base one step's worktree on the preceding step's branch. `{previous}` passes text only. There is no implicit shared worktree or hidden workflow state.
|
|
121
92
|
|
|
122
|
-
A worktree starts from Main's current `HEAD`. Clean worktrees with no child commits are pruned; committed, dirty, switched, unmeasurable, or otherwise recoverable work is preserved and reported. For `delegate_task`, non-git directories and repositories with an unborn `HEAD` use Main's working directory. Git submodules reject worktree isolation, and setup failure in a real repository throws rather than silently sharing Main's checkout.
|
|
93
|
+
A worktree starts from Main's current `HEAD`. Clean worktrees with no child commits are pruned; committed, dirty, switched, unmeasurable, or otherwise recoverable work is preserved and reported. For `delegate_task`, non-git directories and repositories with an unborn `HEAD` use Main's working directory. Git submodules reject worktree isolation, and setup failure in a real repository throws rather than silently sharing Main's checkout.
|
|
123
94
|
|
|
124
95
|
If steps must share files, make that an explicit caller decision: use an intentionally shared workspace, merge preserved child commits, or pass state through a caller-owned store. Do not rely on chain order to imply filesystem sharing.
|
|
125
96
|
|
|
@@ -138,19 +109,19 @@ A Role file requires:
|
|
|
138
109
|
|
|
139
110
|
Every launch installs the Role tool policy. At launch, a package caller may add `tools`, `extensions`, and `env`; caller tools are unioned into the Role base list and loaded extension tools activate in every case. Caller `env` adds to or overrides the active Pi process environment for the child.
|
|
140
111
|
|
|
141
|
-
Children start with ambient extension and Skill discovery disabled. Only explicit Role
|
|
112
|
+
Children start with ambient extension and Skill discovery disabled. Only explicit Role or caller extensions, resolved Skill paths, extension package resources, and required internal tool-policy or Codex adapters load. Loaded extension tools activate even when the Role base list is empty. Child-inappropriate parent tools are always excluded: `delegate_task`, `orchestrate_execute`, `orchestrate_status`, `orchestrate_resume`, `orchestrate_abort`, and `ask_question`. Explicit Role or caller tool names are verified against the final filtered registry after each provider extension completes `session_start`. Unavailable names fail before the first model turn and identify the missing names with provider guidance.
|
|
142
113
|
|
|
143
114
|
Role Skill names resolve through Main's effective Pi Skill registry at launch. Missing names are returned in `ResolvedRoleLaunch.missingSkills`; `delegate_task` warns and skips them. Library callers must surface that warning themselves. Missing Skills do not block launch.
|
|
144
115
|
|
|
145
116
|
### Role final-turn handoff
|
|
146
117
|
|
|
147
|
-
Role launches made by `createRoleLaunch` reserve a response-only handoff at either boundary. They use the penultimate `maxTurns` turn or a continuing `maxTokens` crossing. This includes `delegate_task` and
|
|
118
|
+
Role launches made by `createRoleLaunch` reserve a response-only handoff at either boundary. They use the penultimate `maxTurns` turn or a continuing `maxTokens` crossing. This includes `delegate_task` and library Role launches made with the public launch API.
|
|
148
119
|
|
|
149
120
|
With `maxTurns` set to 1, Pi disables tools during `session_start`. The sole provider turn is the response-only handoff.
|
|
150
121
|
|
|
151
122
|
After a continuing boundary turn, Pi completes its tools. It then disables every active tool and queues one structured final handoff. A terminal boundary response succeeds without a handoff. If the child continues after the token handoff, the executor rejects with `token_limit`.
|
|
152
123
|
|
|
153
|
-
The handoff requests a fixed Markdown decision packet with Status (`completed`, `blocked`, or `incomplete`), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence, and the child replies only with it. This preserves
|
|
124
|
+
The handoff requests a fixed Markdown decision packet with Status (`completed`, `blocked`, or `incomplete`), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence, and the child replies only with it. This preserves caller-required exact structured output. The handoff remains inside `maxTurns`, but it is the one permitted response after crossing `maxTokens`. A timeout, provider failure, or child-process failure can end a Role launch before handoff. Structured executor and retained-worktree facts remain authoritative; the model handoff supplies semantic context and a suggested next action.
|
|
154
125
|
|
|
155
126
|
A raw `createEphemeralSubagentExecutor` launch enforces the token extra-turn window. It does not guarantee disabled tools or the handoff message. Only Role launches install that policy.
|
|
156
127
|
|
|
@@ -165,7 +136,8 @@ The package root exports the following mechanism-level APIs:
|
|
|
165
136
|
| `resolveRoleLaunch(pi, ctx, input)` | Resolve a caller-owned Model Task route, applying call-level then Role `modelClass` precedence, and produce `ResolvedRoleLaunch`. |
|
|
166
137
|
| `createRoleLaunch(pi, ctx, input)` | Produce the same launch from a caller-supplied resolved route. |
|
|
167
138
|
| `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
|
|
168
|
-
| `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle. |
|
|
139
|
+
| `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle; `createChildWorktree` can prepare exact metadata before allocation. |
|
|
140
|
+
| `prepareExactReviewEvidence` | Validate Git identity and create a bounded private base-to-tip patch with exact `{base, tip, patchPath}` evidence. |
|
|
169
141
|
|
|
170
142
|
`finalizeChildWorktree` returns the breaking `WorktreePayload` lifecycle union:
|
|
171
143
|
|
|
@@ -425,11 +397,11 @@ The package ships three working built-in Roles, validated by the same parser as
|
|
|
425
397
|
| Built-in | Behavior |
|
|
426
398
|
| --- | --- |
|
|
427
399
|
| `implementer` | Focused implementation requesting `isolation: worktree`; commits scoped changes locally, never pushes or opens PRs without authorization. Non-Git or unborn-`HEAD` contexts may use Main's cwd. |
|
|
428
|
-
| `reviewer` | Read-only correctness review of supplied plans
|
|
400
|
+
| `reviewer` | Read-only correctness review of supplied plans, files, or caller-prepared exact evidence; never edits or commits. |
|
|
429
401
|
| `scout` | Read-only code and evidence mapping for one bounded task; never changes files. |
|
|
430
402
|
|
|
431
403
|
A same-named Markdown file in `config/pi-subagent/` explicitly overrides the built-in default.
|
|
432
404
|
|
|
433
|
-
|
|
405
|
+
`delegate_task` remains the generic flat single, parallel, and chain mechanism. Library callers own any richer protocol.
|
|
434
406
|
|
|
435
|
-
See [ADR 001](./adr/001-composable-ephemeral-execution.md) for the executor boundary
|
|
407
|
+
See [ADR 001](./adr/001-composable-ephemeral-execution.md) for the executor boundary.
|
package/extensions/role-tools.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { isToolCallEventType, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import {
|
|
3
5
|
CHILD_EXCLUDED_TOOL_NAMES,
|
|
4
6
|
EXECUTION_BUDGET_ENV,
|
|
7
|
+
PI_ORCHESTRATOR_PROCESS_LEASE,
|
|
5
8
|
ROLE_TOOL_POLICY_FLAG,
|
|
6
9
|
type EphemeralSubagentExecutionBudget,
|
|
7
10
|
} from "@henryqw/pi-subagent";
|
|
@@ -9,12 +12,27 @@ import {
|
|
|
9
12
|
const childExcludedTools: ReadonlySet<string> = new Set(CHILD_EXCLUDED_TOOL_NAMES);
|
|
10
13
|
const WARNING_RATIO = 0.8;
|
|
11
14
|
const WARNING_MESSAGE_TYPE = "pi-subagent-execution-budget";
|
|
15
|
+
const PROCESS_LEASE_ERROR = `${PI_ORCHESTRATOR_PROCESS_LEASE} must be a nonempty NUL/newline-free absolute path naming a regular non-symlink file owned by the current uid with mode 0600.`;
|
|
12
16
|
const FINAL_HANDOFF_MESSAGE = {
|
|
13
17
|
customType: "pi-subagent-final-handoff",
|
|
14
18
|
content: "**Final handoff required.** Tools are disabled. If your assigned task or Role requires exact output, reply only with that output instead; it takes precedence over this decision packet. Otherwise, reply only with this decision packet:\n\n**Status:** completed | blocked | incomplete\n**Outcome:** one sentence describing what is now true\n**Evidence:** up to three concrete findings, changes, or checks; include an attempted approach only when it prevents Main from repeating failed work\n**Blocker:** none or the exact blocker\n**Risk:** none or one material risk\n**Suggested next:** none or one concrete action",
|
|
15
19
|
display: true,
|
|
16
20
|
};
|
|
17
21
|
|
|
22
|
+
function validateProcessLease(path: string): void {
|
|
23
|
+
if (!path || /[\0\r\n]/.test(path) || !isAbsolute(path)) throw new Error(PROCESS_LEASE_ERROR);
|
|
24
|
+
let stats;
|
|
25
|
+
try {
|
|
26
|
+
stats = lstatSync(path);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(PROCESS_LEASE_ERROR, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
31
|
+
if (uid === undefined || !stats.isFile() || stats.isSymbolicLink() || stats.uid !== uid || (stats.mode & 0o7777) !== 0o600) {
|
|
32
|
+
throw new Error(PROCESS_LEASE_ERROR);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
function configuredTools(value: unknown): string[] {
|
|
19
37
|
if (typeof value !== "string") throw new Error(`${ROLE_TOOL_POLICY_FLAG} must be JSON tool names.`);
|
|
20
38
|
let parsed: unknown;
|
|
@@ -80,6 +98,8 @@ function joinBudgetParts(parts: string[]): string {
|
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
export default function roleTools(pi: ExtensionAPI): void {
|
|
101
|
+
const processLease = process.env[PI_ORCHESTRATOR_PROCESS_LEASE];
|
|
102
|
+
if (processLease !== undefined) validateProcessLease(processLease);
|
|
83
103
|
pi.registerFlag(ROLE_TOOL_POLICY_FLAG, {
|
|
84
104
|
description: "Internal Pi Subagent Role tool policy",
|
|
85
105
|
type: "string",
|
|
@@ -106,6 +126,18 @@ export default function roleTools(pi: ExtensionAPI): void {
|
|
|
106
126
|
}
|
|
107
127
|
});
|
|
108
128
|
|
|
129
|
+
if (processLease !== undefined) {
|
|
130
|
+
pi.on("tool_call", (event) => {
|
|
131
|
+
if (!isToolCallEventType("bash", event)) return;
|
|
132
|
+
event.input.command = `__pi_subagent_process_lease_fd=10
|
|
133
|
+
while : 2>/dev/null <&"$__pi_subagent_process_lease_fd" || : 2>/dev/null >&"$__pi_subagent_process_lease_fd"; do
|
|
134
|
+
__pi_subagent_process_lease_fd=$((__pi_subagent_process_lease_fd + 1))
|
|
135
|
+
done
|
|
136
|
+
eval 'exec '"$__pi_subagent_process_lease_fd"'>>"$${PI_ORCHESTRATOR_PROCESS_LEASE}"' || exit $?
|
|
137
|
+
unset __pi_subagent_process_lease_fd
|
|
138
|
+
${event.input.command}`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
109
141
|
if (!budget) return;
|
|
110
142
|
const warningTurn = Math.ceil(budget.maxTurns * WARNING_RATIO);
|
|
111
143
|
const warningTokens = budget.maxTokens === undefined ? undefined : Math.ceil(budget.maxTokens * WARNING_RATIO);
|