@henryqw/pi-subagent 26.5.1 → 27.1.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 +10 -6
- package/README.md +48 -18
- package/dist/direct-herdr.d.ts +18 -5
- package/dist/direct-herdr.js +90 -13
- package/dist/herdr-runtime.js +3 -1
- package/dist/index.d.ts +27 -4
- package/dist/index.js +122 -25
- package/dist/integration-git.js +3 -3
- package/dist/prune.d.ts +50 -0
- package/dist/prune.js +192 -0
- package/dist/runner.d.ts +13 -0
- package/dist/runner.js +104 -10
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -5
- package/dist/store.d.ts +2 -0
- package/dist/store.js +14 -2
- package/dist/worktree.d.ts +3 -0
- package/dist/worktree.js +3 -1
- package/docs/migration.md +163 -0
- package/docs/orchestration.md +19 -10
- package/extensions/admission.ts +6 -11
- package/extensions/config.ts +9 -2
- package/extensions/isolated.ts +115 -37
- package/extensions/result-transport.ts +38 -5
- package/extensions/subagent-command.ts +113 -7
- package/extensions/subagent.ts +177 -60
- package/extensions/workflow.ts +36 -7
- package/package.json +1 -1
- package/skills/pi-subagent/SKILL.md +6 -4
package/CONTEXT.md
CHANGED
|
@@ -13,7 +13,9 @@ 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 bounded task using its Role's declared resources in Main's shared checkout
|
|
16
|
+
- **Direct task**: one bounded task using its Role's declared resources in Main's shared checkout, with a declared `access` of `read` or `write`; authorized writes take effect immediately without checked integration.
|
|
17
|
+
- **Direct reference**: `D<workflow>.<worker>` display reference for one recorded direct worker on the session branch, such as `D7.1`. It identifies a worker for display and `continueFrom`; it never grants ownership.
|
|
18
|
+
- **Continued worker**: a new direct worker, with a new reference and tab, that resumes a finished worker's saved Pi session and private lease through `continueFrom`.
|
|
17
19
|
- **Isolated request**: one durable ID, goal, and checked task graph.
|
|
18
20
|
- **Candidate**: exact clean committed task-worktree identity produced by an isolated changeset worker.
|
|
19
21
|
- **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,10 +43,12 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
41
43
|
|
|
42
44
|
### Direct evidence
|
|
43
45
|
|
|
44
|
-
- Direct mode
|
|
45
|
-
- Only direct requests
|
|
46
|
+
- Direct mode uses each Role's declared resources for explicitly authorized scope. Each task declares `access`; a request with any `write` task is a write request. `read` is Main's unenforced classification: read requests skip admission and return plain text; their parallel tasks run concurrently, chains stay ordered. Write requests serialize tasks and retain Pi checkout admission until exact owned worker termination is proved by pane absence and two empty private process-lease scans; uncertain termination retains admission and live recovery ownership. Write-request answers require an explicit structured `succeeded` outcome; failed, blocked, or malformed completion stops dispatch but does not prevent release after proved termination. `/subagent` Close/cancel-and-release retries termination under session, branch, epoch and exact ownership guards without replaying work or removing checkout/session evidence. This is a worker report, not proof of validation, cleanliness, rollback, or checked integration. Direct changesets require isolated mode.
|
|
47
|
+
- Only direct write requests reserve checkout admission. They cannot start while another direct writer holds it or a Main potentially-writing call is in flight. Main calls, including Bash and nested `codemode` writes, do not block each other. A direct writer blocks Main's writes until verified worker termination; use isolated mode when Main must continue writing. Reads never wait.
|
|
46
48
|
- 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
|
-
-
|
|
49
|
+
- `continueFrom` resolves to exactly one recorded tab with the same Role and a saved turn identity. It refuses before launch while a local task owns the session, when the session is missing, above 16 MiB, or its exact recorded turn is not terminal, or when a still-listed old agent is not the exact idle or done agent. Old-pane closure needs pane absence and two empty lease scans. The relaunch reuses the session file and private lease in a new tab and record.
|
|
50
|
+
- `/subagent` **Stop this worker** exists only for running workers of locally owned all-read workflows. It aborts one entry, proves its termination, and records `Stopped by user from /subagent`; siblings continue and Main receives one result.
|
|
51
|
+
- An exact settled worker with a bounded final Pi answer produces a follow-up to Main. Worker text reaching Main carries a no-authority header with imitated harness framing escaped; a failed task keeps a bounded partial excerpt, never shown as success. Blocked, unknown, idle-stalled, truncated, or ambiguous outcomes retain actionable recovery identity. Session replacement never delivers to the wrong Main session.
|
|
48
52
|
|
|
49
53
|
### Isolated lifecycle
|
|
50
54
|
|
|
@@ -57,13 +61,13 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
57
61
|
- Promotion requires exact unchanged clean Main and a passing combined generation. Durable promotion evidence precedes selected-worker termination and cleanup; recovery never rolls back a proven promotion.
|
|
58
62
|
- Rejected workers and superseded generations stay retained until Main explicitly releases their exact clean, proved-owned resources. At most two unreleased integration checkouts coexist.
|
|
59
63
|
- Failures, ambiguity, interruption, review findings, conflicts, drift, unproved termination, or cleanup failure enter `needs_attention` and preserve evidence.
|
|
60
|
-
- `subagent_status` is read-only, callable from `codemode` scripts, and returns the bounded public projection as structured content, never raw run state. `delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only. `subagent_resume` accepts only strict `retry`, `verify`, or `finalize` continuations. `subagent_abort` terminates only exact owned workers.
|
|
64
|
+
- `subagent_status` is read-only, callable from `codemode` scripts, and returns the bounded public projection as structured content, never raw run state. `delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only. `subagent_resume` accepts only strict `retry`, `verify`, or `finalize` continuations; only a correcting `retry` takes Main's optional instruction. `subagent_abort` terminates only exact owned workers.
|
|
61
65
|
- The extension never pushes, opens a pull request, publishes, deploys, stashes, resets, force-cleans, or deletes unproved recoverable work.
|
|
62
66
|
|
|
63
67
|
### Resource and launch policy
|
|
64
68
|
|
|
65
69
|
- Ambient child extensions and Skills stay disabled. Only Role/caller resources plus required internal adapters load.
|
|
66
|
-
- Every Role requires `tools`, `extensions`, and `skills` arrays. `extensions` may name Pi built-in extensions as `builtin:<name>`. A Role activates `codemode` only by naming it in `tools` with `builtin:codemode` loaded; MCP configuration never activates it, and Pi's codemode settings apply unchanged. `mcps` names servers from the global `~/.pi/agent/mcp.json`; their tools keep their configured exposure in a codemode Role and are forced to direct exposure otherwise. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
70
|
+
- Every Role requires `tools`, `extensions`, and `skills` arrays; unknown frontmatter keys are rejected per file, and an invalid file blocks only the name it claims. `roleDefaults` in config merge into every Role first, deduplicated. `extensions` may name Pi built-in extensions as `builtin:<name>`. A Role activates `codemode` only by naming it in `tools` with `builtin:codemode` loaded; MCP configuration never activates it, and Pi's codemode settings apply unchanged. `mcps` names servers from the global `~/.pi/agent/mcp.json`; their tools keep their configured exposure in a codemode Role and are forced to direct exposure otherwise. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
67
71
|
- The Role tool policy declares the Role's `tools` and the extension tools Pi would declare on registration; extension tools with `codemode` or `deferred` exposure stay callable from scripts without being declared.
|
|
68
72
|
- Selected extensions are trusted executable bundles, not a sandbox. All tools and lifecycle behavior they register load together.
|
|
69
73
|
- Role Skill names resolve through Main's effective Pi registry. Missing Roles, Skills, tools, MCP servers, routes, models, or thinking levels fail before the first productive turn.
|
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ pi install npm:@henryqw/pi-subagent
|
|
|
15
15
|
|
|
16
16
|
Run `/task-models` and configure the `fast`, `balanced`, `frontier`, and `fav` routes you use.
|
|
17
17
|
|
|
18
|
+
### Upgrade from 26.x
|
|
19
|
+
|
|
20
|
+
Version 27.0.0 has breaking changes: each direct task must declare `access`, Role files with unknown frontmatter keys are rejected, and the library replaces `loadRoles` with `loadRoleCatalog` and `findRole`. Read [Migrate from 26.x to 27.0.0](./docs/migration.md) before you upgrade.
|
|
21
|
+
|
|
18
22
|
## Works with
|
|
19
23
|
|
|
20
24
|
| Package | Relationship | Purpose |
|
|
@@ -29,11 +33,13 @@ Commands are for you. Delegation and state tools and the packaged skill are for
|
|
|
29
33
|
|
|
30
34
|
| Surface | Type | Purpose |
|
|
31
35
|
| --- | --- | --- |
|
|
32
|
-
| `/subagent` | command | Browse work, Close/cancel-and-release locally owned direct workers, 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
|
|
36
|
+
| `/subagent` | command | Browse work, Close/cancel-and-release locally owned direct workers, stop one running worker of a read workflow, 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 configured `scout` Role. |
|
|
37
|
+
| `/subagent roles` | command | List each Role with its source (built-in, user override, or user), resolved route model and thinking level or route error, tools, extensions, Skills, MCP servers, and per-file load errors. Read-only; no Herdr pane required. |
|
|
38
|
+
| `/subagent prune` | command | Review leftover worker and integration worktrees and branches in the current canonical repository, then remove the ones already merged after your confirmation. Without interactive UI it only reports. Requires a running Herdr server to prove that no pane uses a checkout. |
|
|
33
39
|
| `/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. |
|
|
34
40
|
| `delegate_task` | tool | Start authorized shared-checkout work or a durable isolated checked graph. |
|
|
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. |
|
|
36
|
-
| `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation; does not replace staging or integration. |
|
|
41
|
+
| `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. Failed review verdicts from tasks (including tasks whose siblings still run), the final gate, and integration generations appear in its text within one shared 32 KiB budget; omitted verdicts are counted. The structured projection carries each verdict up to 32 KiB, with `verdictTruncated` set when it is cut. |
|
|
42
|
+
| `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation; does not replace staging or integration. A `retry` that sends a same-worker correction accepts an optional `instruction` (at most 32000 characters), added to that prompt as Main's instruction after the saved failure. Other actions reject it. |
|
|
37
43
|
| `subagent_stage` | tool | Stage or resolve an exact candidate, or reject/revise it; rejection of a staged candidate freezes that generation. |
|
|
38
44
|
| `subagent_integrate` | tool | Advance staged dependents, refresh after clean Main drift or evidence-less validation failure, validate in the background, record one correction, promote, reconcile interrupted promotion, clean up proven promotion, or explicitly release rejected/superseded resources. |
|
|
39
45
|
| `subagent_abort` | tool | Abort an isolated request only when no retained candidates or integration worktrees remain; cannot discard them. |
|
|
@@ -44,7 +50,7 @@ Commands are for you. Delegation and state tools and the packaged skill are for
|
|
|
44
50
|
|
|
45
51
|

|
|
46
52
|
|
|
47
|
-
`delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only: Pi's `codemode` scripts cannot call them, because delegation and guarded state transitions need Main's visible reasoning. `subagent_status` stays callable from scripts. Main's potentially-writing calls can run together without writer ownership, including all Bash calls and nested `codemode` writes. Only
|
|
53
|
+
`delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only: Pi's `codemode` scripts cannot call them, because delegation and guarded state transitions need Main's visible reasoning. `subagent_status` stays callable from scripts. Main's potentially-writing calls can run together without writer ownership, including all Bash calls and nested `codemode` writes. Only direct requests with a task declaring `access: "write"` reserve the checkout. They can start only when no direct writer owns it and no Main potentially-writing call is in flight there. Each Main call is tracked separately until its result or execution end; a script's result does not release its active nested writes. Reads never wait. Use `mode: isolated` to keep Main free to write; this adds worktree checks and explicit integration.
|
|
48
54
|
|
|
49
55
|
The coordinator is extension code running in Main's Pi process, not another agent. It persists the request, launches the Herdr subagent, checks its committed work, and reports saved results; Main chooses what to stage and promote. If a worker fails, the coordinator reports attention after the wave settles, not necessarily immediately while siblings work. If Main's Pi process stops, no follow-up is guaranteed: restart and run `/subagent recover` in the canonical repository to classify orphaned interruptions and send Main one recovery report. Main then uses `subagent_status` for exact evidence and deliberate next actions. A stale `running` state does not prove a worker is still active; interrupted or ambiguous actions are never replayed automatically.
|
|
50
56
|
|
|
@@ -82,27 +88,38 @@ A read-only task:
|
|
|
82
88
|
"mode": "direct",
|
|
83
89
|
"role": "scout",
|
|
84
90
|
"name": "Map sign-in flow",
|
|
85
|
-
"task": "Trace sign-in through session creation. Report files and risks. Do not edit files."
|
|
91
|
+
"task": "Trace sign-in through session creation. Report files and risks. Do not edit files.",
|
|
92
|
+
"access": "read"
|
|
86
93
|
}
|
|
87
94
|
```
|
|
88
95
|
|
|
89
96
|
Direct mode selects exactly one shape:
|
|
90
97
|
|
|
91
98
|
```text
|
|
92
|
-
Single: { mode: "direct", role, name, task, ... }
|
|
93
|
-
Parallel: { mode: "direct", tasks: [{ role, name, task, ... }] }
|
|
94
|
-
Chain: { mode: "direct", chain: [{ role, name, task, ... }] }
|
|
99
|
+
Single: { mode: "direct", role, name, task, access, ... }
|
|
100
|
+
Parallel: { mode: "direct", tasks: [{ role, name, task, access, ... }] }
|
|
101
|
+
Chain: { mode: "direct", chain: [{ role, name, task, access, ... }] }
|
|
95
102
|
```
|
|
96
103
|
|
|
97
|
-
|
|
104
|
+
Every direct task declares `access: "read"` or `access: "write"`; a missing or other value fails before launch. Main declares `write` when the task may change files, Git state, or external resources. `read` is not enforced: there is no sandbox, and a read task keeps all of its Role's tools. A request is a write request when any task declares `write`. Read requests skip checkout admission and return plain-text answers; their parallel `tasks` run concurrently up to `maxSubagents`, while a `chain` still runs in order. Use isolated mode for checked writes.
|
|
105
|
+
|
|
106
|
+
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. Use isolated mode for a checked changeset.
|
|
98
107
|
|
|
99
|
-
Direct writes affect Main's checkout immediately. No clean-checkout prerequisite, automatic checks, rollback, or isolated integration guarantee applies.
|
|
108
|
+
Direct writes affect Main's checkout immediately. No clean-checkout prerequisite, automatic checks, rollback, or isolated integration guarantee applies. Write requests can start only when no direct writer owns the checkout and no Main potentially-writing call is in flight there. They run their tasks serially and hold checkout admission after the initial handle returns; competing direct writers and Main writes fail until all owned workers are proved stopped. Use `mode: isolated` to keep Main free to write, with worktree checks and explicit integration instead of immediate shared-checkout changes. Parallel read requests still run concurrently. A failure in a write request stops further dispatch; an uncertain worker may still be running. Chains replace each literal `{previous}` with the preceding successful answer and stop on failure. Task outcome and worker termination are separate: after settlement the coordinator closes exact owned writer panes and releases admission only after pane absence and two empty private process-lease scans. Native `lsof` is required for this proof; no Git files, commits, or saved sessions are removed. Failure remains failure even when admission is released. If closing fails, identity changes, a leased child survives, or inspection is uncertain, admission stays held. Open `/subagent`, select the exact direct workflow, and choose **Close/cancel-and-release ALL owned workers in this workflow** to cancel dispatch and retry termination. It stops every owned worker of that workflow, not one selected worker. The action checks session/branch/epoch and live ownership; recorded-only tasks cannot unlock anything. Resolve the reported blocker (including any surviving owned process), then repeat the same action. Restarting Pi is not termination proof. Inspect partial changes before new writes; never blindly retry a commit or delete work to recover.
|
|
100
109
|
|
|
101
110
|
If tab creation times out or returns incomplete or mismatched identity, `/subagent` retains the allocation intent: workspace, checkout, label, private lease/session paths, and any returned tab/pane IDs. No worker is started until creation identity is verified. These records are evidence, not ownership: Close is unavailable for unverified allocation, and admission remains held pending manual reconciliation against Herdr. Do not recreate a tab, guess missing IDs, or restart to bypass this blocker. A Close begun while a worker is active keeps its branch/session/epoch guards through cancellation settlement; stale dialogs cannot close panes or release admission.
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
Tasks in a write request 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, but admission depends on proved termination, not the reported outcome. Success is the worker's explicit report, not independent validation of its changes.
|
|
113
|
+
|
|
114
|
+
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. Each launched worker's line in the result shows its model, thinking level, measured session tokens (`tokens unknown` when Pi recorded no usage), and elapsed time, such as `✓ [1/2] Check · worker · provider/model · thinking high · 1.5k tok · 42s`. Failed and stopped workers show the usage measured until they ended. A worker skipped before launch shows no usage. The result details carry the same values in each entry as `model`, `thinkingLevel`, `tokens`, and `elapsedMs`; `tokens` is absent when unknown. When a task fails after its worker produced text, the result keeps a bounded excerpt marked as partial output; it is never success. Every worker result delivered to Main starts with a header stating that the text is the worker's output and carries no user authority. Text that imitates harness framing, such as `<system-reminder>`-style tags or lines starting with `Human:` or `Assistant:`, gets an inserted backslash; nothing else changes. Isolated status and follow-up answer previews use the same header. 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. The menu has one row per workflow: a single worker shows its reference, Role, and name (`D7.1 · worker · Search audit`); a multi-worker workflow shows its reference and worker count (`D8 · 3 workers`), and selecting it lists each recorded worker by reference, Role, and name with its exact IDs. References and names help you match rows; they are never used to decide ownership. Older records without saved references show their saved allocation label or exact task ID. A `recorded only` row is not proof that its workers are running or finished; locally observed work is labelled separately.
|
|
115
|
+
|
|
116
|
+
### Continue or stop one direct worker
|
|
117
|
+
|
|
118
|
+
Main can continue a finished direct worker instead of starting a fresh one. It adds `continueFrom` with the worker's reference, such as `"continueFrom": "D7.1"`, to any single, parallel, or chain task, and uses the same `role` that the worker ran with. The worker resumes its saved Pi session in a new non-focused Herdr tab, so it keeps its earlier context. The new tab gets a new agent name and reference, and its record points to the same session and private lease file. The launch handle and the result say `continued from D7.1`. Access, admission, write rules, and model routes are the same as for a new task. A request can continue each worker only once, and only from the checkout and Herdr workspace where the worker ran. After a continuation, continue the newest reference; an older reference to the same session is refused because a later turn follows its own.
|
|
104
119
|
|
|
105
|
-
|
|
120
|
+
Before launch, the request fails if the reference is unknown or matches more than one record on this session branch, the Role is different, a locally active task still owns the worker's session, the session file is missing or larger than 16 MiB, or the session's last turn for the recorded prompt is not finished. If Herdr still lists the old agent, it must match the recorded name, pane, tab, workspace, and checkout, and be idle or done; a working or blocked agent is never closed. The old pane is closed, then pane absence and two empty lease scans are proved. Records saved before 27.0.0 have no turn identity and cannot be continued; start a fresh task.
|
|
121
|
+
|
|
122
|
+
In a running workflow that has only `read` tasks, `/subagent` shows **Stop this worker** for each running worker. It aborts only that worker, sends Ctrl-C, closes its exact pane, and proves pane absence and two empty lease scans. The worker's result becomes a failure, `Stopped by user from /subagent`, and keeps any partial output. Other workers continue; a chain stops its later steps. If termination of the stopped worker is unproved, the workflow keeps local ownership so **Close/cancel-and-release** stays available. Main receives one result when the workflow finishes. A workflow with a `write` task offers only **Close/cancel-and-release ALL owned workers in this workflow**. Both actions check session, branch, and epoch, so a stale dialog cannot act.
|
|
106
123
|
|
|
107
124
|
### Isolated checked graphs
|
|
108
125
|
|
|
@@ -151,13 +168,13 @@ An isolated request has one durable ID, one goal, and 1–8 typed tasks:
|
|
|
151
168
|
|
|
152
169
|
`dependsOn` controls scheduling. `contextFrom` also schedules dependencies and carries completed text outputs in the declared order. Do not repeat the same task in both fields. Changesets require focused task checks; a graph with changesets requires final checks. A canonical root `package.json` test script is required: the runner adds `pnpm test` to final checks if missing, and Main runs it on the combined integration checkout before promotion. Checks and judgments bind to exact Git identities; candidate drift, Main drift, mutation during validation, ambiguity, or conflicts stop promotion and retain evidence.
|
|
153
170
|
|
|
154
|
-
`delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after the wave is saved; productive work continues while Main is free. Main can do other work or end its turn instead of sleeping or polling. Pi sends a follow-up in the launching session whenever a checked worker becomes ready or a worker needs attention, and again when the wave finishes. A worker whose exact turn ends without a changed clean commit—including a failed or aborted turn—needs attention, not an inferred candidate; use `subagent_status` for its saved evidence. Session replacement or shutdown suppresses stale delivery and logs the request ID and reason, without prompts or credentials. Synchronous delivery failures are reported without automatic replay; Pi reports asynchronous `send_message` failures through its native runtime. If delivery is missed, inspect `subagent_status`.
|
|
171
|
+
`delegate_task`, `subagent_resume`, and `subagent_integrate advance` return a durable request ID after the wave is saved; productive work continues while Main is free. Main can do other work or end its turn instead of sleeping or polling. Pi sends a follow-up in the launching session whenever a checked worker becomes ready or a worker needs attention, and again when the wave finishes. A worker whose exact turn ends without a changed clean commit—including a failed or aborted turn—needs attention, not an inferred candidate; use `subagent_status` for its saved evidence. If the turn ends with tracked or untracked changes left in the worktree, the coordinator first sends that same worker one saved reminder per attempt to commit them or explain why not. The reminder does not use a correction or a dispatch attempt, and the coordinator never commits, stages, stashes, or discards the changes itself. If the worktree is still dirty after the reminder, the task needs attention and the changes stay in place. Follow-ups show only a short preview of a failed review verdict and tell Main to run `subagent_status` for the full text. Session replacement or shutdown suppresses stale delivery and logs the request ID and reason, without prompts or credentials. Synchronous delivery failures are reported without automatic replay; Pi reports asynchronous `send_message` failures through its native runtime. If delivery is missed, inspect `subagent_status`.
|
|
155
172
|
|
|
156
173
|
Completed text answers are saved independently of cleanup. Status and terminal follow-ups include a bounded accepted-answer preview and cleanup evidence. Ignored automatic artifacts can retain a checkout without violating the read-only contract; no ignored files are deleted to make cleanup pass. Tracked or untracked changes, changed checkout identity, or unproved inspection still require attention: the completed answer remains saved but is not accepted as dependency context or retried automatically. Retained text checkouts require deliberate manual inspection and cleanup; a retained checkout alone is not evidence that the Role edited source.
|
|
157
174
|
|
|
158
175
|
Isolated workers can start while Main has staged, unstaged, or untracked changes: they use only Main's pinned committed branch, HEAD, and tree. Those local edits are neither copied into worker worktrees nor stashed, staged, reset, or merged automatically. Main must remain at that committed identity while workers run; branch or HEAD drift blocks new waves. The worker produces a committed candidate without tracked or untracked changes, then runs focused preliminary checks and optional judgment. Ignored generated files do not block candidacy, but prevent automatic worktree cleanup; inspect them before deleting retained work. The runner records each ready candidate immediately but **does not integrate it automatically**. Main can inspect `subagent_status` and use `subagent_stage` to select the exact candidate in an owned integration worktree while siblings are still working; conflicts are resolved there. Stage and resolve are available during an active wave, but rejection and revision wait for the wave to settle. `subagent_integrate` can advance dependent tasks from a staged snapshot, validate the combined tip with full root checks and optional final judgment after the wave settles, and promote it to Main only after exact validation. A definitive failed combined check/review allows one committed correction in the integration checkout followed by another validation. Main must explicitly promote; promotion and cleanup never silently discard retained work.
|
|
159
176
|
|
|
160
|
-
Open `/subagent` without arguments to pick work by label. The menu shows direct records on this session branch and isolated requests in the current canonical Git checkout (including requests launched in other sessions). Pick an isolated request and **Inspect status and recovery** to launch a non-focused Herdr tab with
|
|
177
|
+
Open `/subagent` without arguments to pick work by label. The menu shows direct records on this session branch and isolated requests in the current canonical Git checkout (including requests launched in other sessions). Pick an isolated request and **Inspect status and recovery** to launch a non-focused Herdr tab with the configured `scout` Role on the configured `fast` route, as a read-access task with the Role's own resources. Its prompt contains bounded saved status (including retained paths, but not raw launch environment or session transcripts); the analysis stays in that tab and does not queue a Main turn. The tab appears in the direct list with its own reference and name, such as `D9.1 · scout · Inspect <request ID>`. The tab and session file are recorded for recovery even if launch becomes uncertain. The read-only `subagent_status` tool remains Main's authority for exact identities before any stage or recovery action. If Herdr, the `scout` Role, or the fast route is unavailable, inspection fails without changing the request; Main can use `subagent_status`. Refresh or choose completed/history for older work. Invalid state IDs are warned about and preserved. Outside Git or when isolated inventory is unavailable, direct recovery still works. Browsing does not resume work or change resources; direct **Close/cancel-and-release** is an explicit termination action.
|
|
161
178
|
|
|
162
179
|
For a locally active unsealed isolated changeset, choose **Send follow-up instructions** to open a native multiline editor; submission queues the instruction, not a completed revision. Choose **Edit queued instructions** to withdraw *all still-pending* instructions for that selected task before the editor opens. Their full text appears in FIFO order separated by blank lines. Explicit submission queues **one replacement** at the tail. Cancel leaves the withdrawn instructions removed; already-claimed instructions, initial prompts, and automatic corrections cannot be recalled. A worker may seal during editing: failed submission reopens the full submitted text for deliberate editing or copying, never automatically requeues it. Combined text above the single-instruction limit is preserved in the editor but rejected until shortened. Cancelling before withdrawal makes no change. Neither action touches Main's editor or Pi's global Option+Up queue shortcut. No-UI sessions can run `/subagent recover` or use agent tools for individual inspection and recovery.
|
|
163
180
|
|
|
@@ -171,6 +188,18 @@ If validation was interrupted, first prove the old coordinator/check processes h
|
|
|
171
188
|
|
|
172
189
|
After a coordinator interruption, run `/subagent recover` once. It acquires the repository productive lease before changing any orphaned `pending` or `running` state, preserves unreadable state files and retained candidates, and reports blockers and existing continuations to Main without replaying prompts, merges, promotion, or cleanup. If another productive run holds the lease, it only reads status and reports the live owner; wait rather than taking over. Repeating the command is safe. Main uses `subagent_status` for exact identities and the recovery tools in the interface table above for deliberate decisions; the command never stages or promotes. Direct Herdr tab recovery remains in `/subagent`.
|
|
173
190
|
|
|
191
|
+
### Prune leftover worktrees
|
|
192
|
+
|
|
193
|
+
Run `/subagent prune` to clean up leftover worker and integration checkouts under `.worktrees/` (branches named `pi-subagent/subagent-` plus 24 hex characters) and such branches without a checkout. The review lists each item with its path, branch, tip, and either why it can be removed or every exact blocker. An item can be removed only when all of these are true:
|
|
194
|
+
|
|
195
|
+
- Its branch tip is already in Main's HEAD or in the local `origin` default-branch ref. Nothing is fetched.
|
|
196
|
+
- Its checkout has no tracked or untracked changes and no merge, rebase, cherry-pick, revert, or bisect in progress.
|
|
197
|
+
- No unfinished isolated request, in any checkout of this repository, records it. An unreadable state file blocks every item it mentions.
|
|
198
|
+
- No productive run holds a repository lease.
|
|
199
|
+
- No Herdr pane works inside the checkout. If Herdr cannot be inspected, every checkout is refused.
|
|
200
|
+
|
|
201
|
+
One confirmation removes every listed removable item. Removing a checkout also deletes its ignored files, such as `node_modules` or `dist`, so checkouts with ignored files are included only after a second confirmation that names each ignored entry. That approval covers everything inside each named entry; a new or changed top-level ignored entry skips the checkout. If a checkout's full list of ignored entries does not fit in that dialog, the checkout is left out of the run. Checkouts with initialized submodules, and owned checkouts that are detached or on another branch, are blocked; remove them manually. Removable items are listed first. A long list is never cut silently: it ends with `+N more not shown`, and items that were not shown are not removed in that run. Run `/subagent prune` again to review them. Each item is checked again just before removal, and any change skips it. If the session or branch changes during removal, the remaining steps are skipped. The branch is deleted only if it still points at the reviewed tip. Afterwards, each aborted request that recorded a removed resource is reconciled through the same guarded path as repeating `subagent_abort`, so its cleanup no longer shows as pending. The report lists removed, skipped, and failed items and any request that could not be reconciled. Without interactive UI, the command only reports.
|
|
202
|
+
|
|
174
203
|
If an isolated worker produced no candidate, `subagent_abort` terminates it and removes only proven unchanged, clean owned resources. Cleanup includes ignored-file checks and never discards unique commits. If productive work is still settling, termination is uncertain, or cleanup fails, the request stays aborted with retained-resource evidence. Inspect `subagent_status`, resolve the reported blocker, then repeat `subagent_abort` to reconcile cleanup; it does not replay worker prompts. Dirty work, unique commits, and uncertain allocations remain for deliberate manual recovery.
|
|
175
204
|
|
|
176
205
|
The following details describe exact identity and retained-work requirements.
|
|
@@ -191,8 +220,9 @@ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. A missing file si
|
|
|
191
220
|
| `maxCorrections` | Same-worker automatic corrections per isolated request | Safe integer ≥ 0 | `1` |
|
|
192
221
|
| `timeout.idleMinutes` | Direct worker and ephemeral child idle timeout | Positive and within Node's timer range | `10` |
|
|
193
222
|
| `timeout.maxMinutes` | Ephemeral child hard runtime | Greater than idle and within Node's timer range | `30` |
|
|
223
|
+
| `roleDefaults` | Optional `extensions`, `tools`, and `skills` arrays merged into every built-in and user Role, with the same validation as Role fields | Object; unknown keys rejected | None |
|
|
194
224
|
|
|
195
|
-
Limits come only from this global file. Request fields cannot override or replenish them. Existing durable requests keep their recorded policy, while a lower current correction limit can tighten recovery. There is intentionally no whole-run timeout setting.
|
|
225
|
+
`roleDefaults` entries come first in each merged list, followed by the Role's own entries, with duplicates removed; merging happens before resource resolution. Use it for the provider extension that every child needs. Limits come only from this global file. Request fields cannot override or replenish them. Existing durable requests keep their recorded policy, while a lower current correction limit can tighten recovery. There is intentionally no whole-run timeout setting.
|
|
196
226
|
|
|
197
227
|
Malformed or unreadable JSON, unknown keys, and invalid values block delegation with one actionable warning. The file is preserved and never rewritten automatically.
|
|
198
228
|
|
|
@@ -210,11 +240,11 @@ Role Markdown lives in `~/.pi/agent/config/pi-subagent/` and requires frontmatte
|
|
|
210
240
|
| `mcps` | Optional exact server names from `~/.pi/agent/mcp.json` in Pi's native `mcpServers` format; omitted or `[]` denies MCP access |
|
|
211
241
|
| body | Required system instructions |
|
|
212
242
|
|
|
213
|
-
Roles describe responsibility and capabilities
|
|
243
|
+
Unknown frontmatter keys are rejected with the file and key named. Each invalid file is reported on its own: valid Roles still load, delegating to the Role an invalid file claims fails with that file's error, and duplicate names make that name unavailable. `/subagent roles` lists the errors. Roles describe responsibility and capabilities, not write access or isolation; each task declares access and each request chooses its mode. A same-named user Role overrides a built-in Role. The package ships `implementer`, `reviewer`, and `scout`. The built-in reviewer includes `git_read`; add it to an existing user reviewer's `tools` array to opt in. No additional extension or Bash access is needed.
|
|
214
244
|
|
|
215
245
|
### Reviewer Git inspection
|
|
216
246
|
|
|
217
|
-
`git_read` is an internal child tool, loaded and registered only for Roles that declare it.
|
|
247
|
+
`git_read` is an internal child tool, loaded and registered only for Roles that declare it. It accepts structured parameters, not shell commands or arbitrary Git options:
|
|
218
248
|
|
|
219
249
|
| `operation` | Parameters and evidence |
|
|
220
250
|
| --- | --- |
|
|
@@ -243,7 +273,7 @@ The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In
|
|
|
243
273
|
|
|
244
274
|
| Surface | Type | Purpose |
|
|
245
275
|
| --- | --- | --- |
|
|
246
|
-
| `
|
|
276
|
+
| `loadRoleCatalog`, `findRole`, `parseRoleName`, `parseRoleDefaults` | functions | Load built-in and user Roles with `roleDefaults` and per-file errors, then select one by name. |
|
|
247
277
|
| `resolveRoleSkills`, `resolveRoleLaunch`, `resolveConfiguredRoleLaunch` | functions | Resolve effective Pi resources and model routes. |
|
|
248
278
|
| `prepareRoleLaunch`, `finalizeRoleLaunch` | functions | Prepare Role prompts, arguments, and tool policy. |
|
|
249
279
|
| `createEphemeralSubagentExecutor` | function | Run bounded no-session Pi children through a FIFO pool. |
|
package/dist/direct-herdr.d.ts
CHANGED
|
@@ -9,11 +9,15 @@ export interface DirectHandle {
|
|
|
9
9
|
prompt: string;
|
|
10
10
|
answer(maxBytes: number): Promise<string>;
|
|
11
11
|
usageTokens(): Promise<number | undefined>;
|
|
12
|
+
/** Bounded assistant text from this turn so far; failure evidence only, never completion. */
|
|
13
|
+
partialText(maxChars: number): Promise<string | undefined>;
|
|
12
14
|
/** Display-only activity from the last usageTokens() sample. */
|
|
13
15
|
readonly activity?: DirectToolActivity;
|
|
14
16
|
cancel(): Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
export declare function exactDirectTerminalTurn(jsonl: string, prompt: string): boolean;
|
|
19
|
+
/** Refuse continuation unless the saved session's exact recorded turn is terminal and no later user turn exists. */
|
|
20
|
+
export declare function assertSettledDirectSession(sessionFile: string, turn: string): Promise<void>;
|
|
17
21
|
export declare function exactDirectAnswer(jsonl: string, prompt: string, maxBytes?: number, requireSuccess?: boolean): string;
|
|
18
22
|
export interface DirectToolActivity {
|
|
19
23
|
pending: string[];
|
|
@@ -26,13 +30,18 @@ export declare function directSessionUsage(jsonl: string, prompt: string): {
|
|
|
26
30
|
tokens?: number;
|
|
27
31
|
activity?: DirectToolActivity;
|
|
28
32
|
};
|
|
33
|
+
/** The last `maxChars` of assistant text in the exact prompt's turn; ignores a partial trailing entry. */
|
|
34
|
+
export declare function directPartialText(jsonl: string, prompt: string, maxChars: number): string | undefined;
|
|
35
|
+
/** `turn` is the prompt's random turn identity; it proves which session turn a record owns.
|
|
36
|
+
* `workspaceId` and `cwd` bind the record to its launching Herdr workspace and checkout. */
|
|
29
37
|
export type DirectTab = Pick<DirectHandle, "name" | "tabId" | "paneId" | "sessionFile"> & {
|
|
30
38
|
leasePath: string;
|
|
39
|
+
turn: string;
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
cwd: string;
|
|
31
42
|
};
|
|
32
43
|
/** Allocation evidence is not ownership and must never authorize pane mutation. */
|
|
33
44
|
export type DirectAllocation = Omit<DirectTab, "tabId" | "paneId"> & {
|
|
34
|
-
workspaceId: string;
|
|
35
|
-
cwd: string;
|
|
36
45
|
label: string;
|
|
37
46
|
tabId?: string;
|
|
38
47
|
paneId?: string;
|
|
@@ -42,8 +51,12 @@ export declare class DirectAllocationError extends Error {
|
|
|
42
51
|
constructor(allocation: DirectAllocation, cause: unknown);
|
|
43
52
|
}
|
|
44
53
|
export declare function createDirectHerdr(pi: Pick<ExtensionAPI, "exec">, cwd: string, idleMs: number): {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
cwd: string;
|
|
45
56
|
/** Close only an exact owned pane, then prove it and its leased children stopped.
|
|
46
|
-
* A task result or Ctrl-C acknowledgement is not termination evidence.
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
* A task result or Ctrl-C acknowledgement is not termination evidence.
|
|
58
|
+
* `settledOnly` refuses to close a listed agent that is not idle or done. */
|
|
59
|
+
stop(tab: Pick<DirectTab, "name" | "tabId" | "paneId" | "leasePath">, current?: () => boolean, settledOnly?: boolean): Promise<void>;
|
|
60
|
+
/** `resume` reuses a stopped worker's saved session and private lease instead of a new temp directory. */
|
|
61
|
+
start(launch: ResolvedRoleLaunch, name: string, label: string, task: string, signal: AbortSignal, onTab: (tab: DirectTab) => void, requireSuccess?: boolean, resume?: Pick<DirectTab, "sessionFile" | "leasePath">): Promise<DirectHandle>;
|
|
49
62
|
};
|
package/dist/direct-herdr.js
CHANGED
|
@@ -69,6 +69,28 @@ export function exactDirectTerminalTurn(jsonl, prompt) {
|
|
|
69
69
|
const reason = exactFinalTurn(jsonl, prompt)?.stopReason;
|
|
70
70
|
return reason === "stop" || reason === "error" || reason === "aborted";
|
|
71
71
|
}
|
|
72
|
+
const turnSuffix = (turn) => `\n\nTurn identity: ${turn}`;
|
|
73
|
+
/** Refuse continuation unless the saved session's exact recorded turn is terminal and no later user turn exists. */
|
|
74
|
+
export async function assertSettledDirectSession(sessionFile, turn) {
|
|
75
|
+
const info = await stat(sessionFile).catch((error) => {
|
|
76
|
+
throw new Error(`Saved Pi session ${sessionFile} is missing or unreadable`, { cause: error });
|
|
77
|
+
});
|
|
78
|
+
if (info.size > SESSION_LIMIT)
|
|
79
|
+
throw new Error(`Saved Pi session ${sessionFile} exceeds 16 MiB`);
|
|
80
|
+
const jsonl = await readFile(sessionFile, "utf8");
|
|
81
|
+
let prompt;
|
|
82
|
+
for (const line of jsonl.trimEnd().split("\n")) {
|
|
83
|
+
const entry = JSON.parse(line);
|
|
84
|
+
const message = entry?.type === "message" ? entry.message : undefined;
|
|
85
|
+
const text = message?.role === "user" && Array.isArray(message.content) && message.content.length === 1
|
|
86
|
+
? message.content[0]?.text : undefined;
|
|
87
|
+
if (typeof text === "string" && text.endsWith(turnSuffix(turn)))
|
|
88
|
+
prompt = text;
|
|
89
|
+
}
|
|
90
|
+
if (prompt === undefined || !exactDirectTerminalTurn(jsonl, prompt)) {
|
|
91
|
+
throw new Error(`Saved Pi session ${sessionFile} has no terminal final turn for the recorded prompt; the worker may still be running`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
72
94
|
export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT, requireSuccess = false) {
|
|
73
95
|
const final = exactFinalTurn(jsonl, prompt);
|
|
74
96
|
if (!final || final.stopReason !== "stop" || !Array.isArray(final.content)) {
|
|
@@ -89,12 +111,12 @@ export function exactDirectAnswer(jsonl, prompt, maxBytes = ANSWER_LIMIT, requir
|
|
|
89
111
|
completion = JSON.parse(text);
|
|
90
112
|
}
|
|
91
113
|
catch (error) {
|
|
92
|
-
throw new Error("Pi
|
|
114
|
+
throw new Error("Pi write-access final answer must be a JSON completion object.", { cause: error });
|
|
93
115
|
}
|
|
94
116
|
if (!completion || typeof completion !== "object" || Array.isArray(completion)
|
|
95
117
|
|| !DIRECT_OUTCOMES.some((outcome) => outcome === completion.outcome)
|
|
96
118
|
|| typeof completion.answer !== "string" || !completion.answer.trim()) {
|
|
97
|
-
throw new Error("Pi
|
|
119
|
+
throw new Error("Pi write-access completion must have a valid outcome and a non-empty answer.");
|
|
98
120
|
}
|
|
99
121
|
if (completion.outcome !== "succeeded")
|
|
100
122
|
throw new Error(`Direct task reported ${completion.outcome}: ${completion.answer}`);
|
|
@@ -160,6 +182,40 @@ export function directSessionUsage(jsonl, prompt) {
|
|
|
160
182
|
return { tokens: observed ? total : undefined,
|
|
161
183
|
activity: pending.size || latest ? { pending: [...pending.values()], latest } : undefined };
|
|
162
184
|
}
|
|
185
|
+
/** The last `maxChars` of assistant text in the exact prompt's turn; ignores a partial trailing entry. */
|
|
186
|
+
export function directPartialText(jsonl, prompt, maxChars) {
|
|
187
|
+
let active = false;
|
|
188
|
+
let texts = [];
|
|
189
|
+
for (const line of jsonl.split("\n")) {
|
|
190
|
+
if (!line.trim())
|
|
191
|
+
continue;
|
|
192
|
+
let entry;
|
|
193
|
+
try {
|
|
194
|
+
entry = JSON.parse(line);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (!entry || entry.type !== "message" || !entry.message || typeof entry.message !== "object")
|
|
200
|
+
continue;
|
|
201
|
+
const message = entry.message;
|
|
202
|
+
if (message.role === "user") {
|
|
203
|
+
active = Array.isArray(message.content) && message.content.length === 1
|
|
204
|
+
&& message.content[0]?.text === prompt;
|
|
205
|
+
if (active)
|
|
206
|
+
texts = [];
|
|
207
|
+
}
|
|
208
|
+
else if (active && message.role === "assistant" && Array.isArray(message.content)) {
|
|
209
|
+
for (const part of message.content)
|
|
210
|
+
if (part?.type === "text" && typeof part.text === "string" && part.text.trim())
|
|
211
|
+
texts.push(part.text);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const chars = Array.from(texts.join("\n").trim());
|
|
215
|
+
if (!chars.length)
|
|
216
|
+
return undefined;
|
|
217
|
+
return chars.length <= maxChars ? chars.join("") : `…${chars.slice(-maxChars).join("")}`;
|
|
218
|
+
}
|
|
163
219
|
export class DirectAllocationError extends Error {
|
|
164
220
|
allocation;
|
|
165
221
|
constructor(allocation, cause) {
|
|
@@ -184,9 +240,12 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
184
240
|
return field(agent.agent_status, "agent state");
|
|
185
241
|
};
|
|
186
242
|
return {
|
|
243
|
+
workspaceId,
|
|
244
|
+
cwd: workingDir,
|
|
187
245
|
/** Close only an exact owned pane, then prove it and its leased children stopped.
|
|
188
|
-
* A task result or Ctrl-C acknowledgement is not termination evidence.
|
|
189
|
-
|
|
246
|
+
* A task result or Ctrl-C acknowledgement is not termination evidence.
|
|
247
|
+
* `settledOnly` refuses to close a listed agent that is not idle or done. */
|
|
248
|
+
async stop(tab, current = () => true, settledOnly = false) {
|
|
190
249
|
const requireCurrent = () => { if (!current())
|
|
191
250
|
throw new Error("Direct task scope changed; reopen /subagent."); };
|
|
192
251
|
requireCurrent();
|
|
@@ -219,6 +278,9 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
219
278
|
|| agent.tab_id !== tab.tabId || agent.workspace_id !== workspaceId || agent.cwd !== workingDir
|
|
220
279
|
|| tab.paneId === callerPane)
|
|
221
280
|
throw new Error("Worker termination requires one exact owned agent, workspace, checkout, tab and pane match.");
|
|
281
|
+
if (settledOnly && agent.agent_status !== "idle" && agent.agent_status !== "done") {
|
|
282
|
+
throw new Error(`Herdr agent ${tab.name} is ${String(agent.agent_status)}, not idle or done; it was not closed`);
|
|
283
|
+
}
|
|
222
284
|
requireCurrent();
|
|
223
285
|
result(await herdr.json(["pane", "close", tab.paneId], options()), "ok");
|
|
224
286
|
}
|
|
@@ -234,17 +296,27 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
234
296
|
}
|
|
235
297
|
requireCurrent();
|
|
236
298
|
},
|
|
237
|
-
|
|
299
|
+
/** `resume` reuses a stopped worker's saved session and private lease instead of a new temp directory. */
|
|
300
|
+
async start(launch, name, label, task, signal, onTab, requireSuccess = false, resume) {
|
|
238
301
|
const caller = result(await herdr.json(["pane", "current", "--current"], options(signal)), "pane_current");
|
|
239
302
|
const pane = object(caller.pane, "calling pane");
|
|
240
303
|
if (pane.pane_id !== callerPane || pane.workspace_id !== workspaceId)
|
|
241
304
|
throw new Error("Herdr caller pane no longer matches the launching workspace.");
|
|
242
305
|
if (Object.keys(launch.env).length)
|
|
243
306
|
throw new Error("Direct Herdr launch cannot transfer Role environment overrides.");
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
307
|
+
let sessionFile;
|
|
308
|
+
let leasePath;
|
|
309
|
+
if (resume) {
|
|
310
|
+
({ sessionFile, leasePath } = resume);
|
|
311
|
+
await assertPrivateLease(leasePath, false);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
const sessionDir = await mkdtemp(join(tmpdir(), "pi-subagent-direct-"));
|
|
315
|
+
sessionFile = join(sessionDir, "session.jsonl");
|
|
316
|
+
leasePath = join(sessionDir, "process.lease");
|
|
317
|
+
await writeFile(leasePath, "", { flag: "wx", mode: 0o600 });
|
|
318
|
+
}
|
|
319
|
+
const turn = randomBytes(16).toString("hex");
|
|
248
320
|
// Pi's native session is the exact answer channel. Herdr screen output is diagnostic only.
|
|
249
321
|
const sessionArgs = launch.args.filter((arg) => arg !== "--no-session");
|
|
250
322
|
if (sessionArgs.length !== launch.args.length - 1)
|
|
@@ -252,7 +324,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
252
324
|
signal.throwIfAborted();
|
|
253
325
|
// Tab creation is bounded but not session-cancellable: Herdr may create it
|
|
254
326
|
// before the CLI responds, and aborting the CLI loses the exact tab ID.
|
|
255
|
-
const allocation = { name, workspaceId, cwd: workingDir, label, sessionFile, leasePath };
|
|
327
|
+
const allocation = { name, workspaceId, cwd: workingDir, label, sessionFile, leasePath, turn };
|
|
256
328
|
let tabId;
|
|
257
329
|
let paneId;
|
|
258
330
|
try {
|
|
@@ -279,7 +351,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
279
351
|
}
|
|
280
352
|
try {
|
|
281
353
|
// Record the tab before the agent start can outlive an aborted launch.
|
|
282
|
-
onTab({ name, tabId, paneId, sessionFile, leasePath });
|
|
354
|
+
onTab({ name, tabId, paneId, sessionFile, leasePath, turn, workspaceId, cwd: workingDir });
|
|
283
355
|
signal.throwIfAborted();
|
|
284
356
|
const started = await startPiAgent(herdr, { name, pane: paneId,
|
|
285
357
|
args: [...sessionArgs, "--session", sessionFile], options: options(signal), shouldRetry: () => false });
|
|
@@ -291,9 +363,9 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
291
363
|
// never submit a new prompt after cancellation.
|
|
292
364
|
signal.throwIfAborted();
|
|
293
365
|
const completionInstruction = requireSuccess
|
|
294
|
-
? `\n\
|
|
366
|
+
? `\n\nWrite-access 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.`
|
|
295
367
|
: "";
|
|
296
|
-
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}
|
|
368
|
+
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}${turnSuffix(turn)}`;
|
|
297
369
|
// Native prompt without --wait acknowledges submission, not completion of the turn.
|
|
298
370
|
const accepted = await herdr.json(["agent", "prompt", name, prompt], options(signal));
|
|
299
371
|
const state = inspect(accepted, "agent_prompted", name, paneId, tabId);
|
|
@@ -316,6 +388,11 @@ export function createDirectHerdr(pi, cwd, idleMs) {
|
|
|
316
388
|
}
|
|
317
389
|
return lastUsage.tokens;
|
|
318
390
|
},
|
|
391
|
+
async partialText(maxChars) {
|
|
392
|
+
if ((await stat(sessionFile)).size > SESSION_LIMIT)
|
|
393
|
+
return undefined;
|
|
394
|
+
return directPartialText(await readFile(sessionFile, "utf8"), prompt, maxChars);
|
|
395
|
+
},
|
|
319
396
|
async answer(maxBytes) {
|
|
320
397
|
const readAnswer = async () => {
|
|
321
398
|
const info = await stat(sessionFile);
|
package/dist/herdr-runtime.js
CHANGED
|
@@ -922,7 +922,9 @@ export class HerdrHostRuntime {
|
|
|
922
922
|
if (SETTLED_AGENT_STATES.has(lifecycle.status)) {
|
|
923
923
|
try {
|
|
924
924
|
if (await this.workerTurnFinished(allocation, prompt)) {
|
|
925
|
-
return
|
|
925
|
+
return inspection.valid && !inspection.clean
|
|
926
|
+
? { outcome: "uncommitted", diagnostic: "Worker finished with uncommitted changes in its owned worktree." }
|
|
927
|
+
: { outcome: "blocked", diagnostic: "Worker finished without an exact changed clean committed candidate." };
|
|
926
928
|
}
|
|
927
929
|
}
|
|
928
930
|
catch (error) {
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,24 @@ export interface Role {
|
|
|
37
37
|
mcps?: string[];
|
|
38
38
|
systemPrompt: string;
|
|
39
39
|
}
|
|
40
|
+
/** Optional `config.json` resources merged into every Role, defaults first. */
|
|
41
|
+
export interface RoleDefaults {
|
|
42
|
+
extensions?: string[];
|
|
43
|
+
tools?: string[];
|
|
44
|
+
skills?: string[];
|
|
45
|
+
}
|
|
46
|
+
export type RoleSource = "built-in" | "user override" | "user";
|
|
47
|
+
/** One invalid user Role file. `name` is the claimed Role name, or the file stem when unreadable. */
|
|
48
|
+
export interface RoleFileError {
|
|
49
|
+
file: string;
|
|
50
|
+
name: RoleName;
|
|
51
|
+
message: string;
|
|
52
|
+
}
|
|
53
|
+
export interface RoleCatalog {
|
|
54
|
+
roles: Role[];
|
|
55
|
+
sources: ReadonlyMap<RoleName, RoleSource>;
|
|
56
|
+
errors: RoleFileError[];
|
|
57
|
+
}
|
|
40
58
|
export interface PiLaunch {
|
|
41
59
|
env: Record<string, string>;
|
|
42
60
|
args: string[];
|
|
@@ -74,15 +92,20 @@ export interface ResolvedRoleSkills {
|
|
|
74
92
|
}
|
|
75
93
|
/** Normalize one arbitrary Role name using the Role configuration contract. */
|
|
76
94
|
export declare function parseRoleName(value: unknown, source?: string): RoleName;
|
|
95
|
+
/** Validate `config.json` `roleDefaults` with the same rules as Role fields; `label` prefixes errors. */
|
|
96
|
+
export declare function parseRoleDefaults(value: unknown, label?: string): RoleDefaults;
|
|
77
97
|
declare const BUILTIN_ROLE_NAMES: readonly ["implementer", "reviewer", "scout"];
|
|
78
98
|
export type BuiltinRoleName = (typeof BUILTIN_ROLE_NAMES)[number];
|
|
79
99
|
export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
|
|
80
100
|
/**
|
|
81
|
-
*
|
|
82
|
-
* A user
|
|
83
|
-
*
|
|
101
|
+
* Package-shipped Roles plus user Roles from `config/pi-subagent`, with
|
|
102
|
+
* `roleDefaults` merged in. A user Role overrides a same-named built-in. Each
|
|
103
|
+
* invalid or duplicate user file is reported and makes its claimed name
|
|
104
|
+
* unavailable without hiding valid Roles. Malformed `roleDefaults` throws.
|
|
84
105
|
*/
|
|
85
|
-
export declare function
|
|
106
|
+
export declare function loadRoleCatalog(agentDir?: string): RoleCatalog;
|
|
107
|
+
/** Select one catalog Role, naming invalid files that claim it. */
|
|
108
|
+
export declare function findRole(catalog: RoleCatalog, name: RoleName): Role;
|
|
86
109
|
type TaskRouteContext = Parameters<typeof resolveTaskModelRoute>[0];
|
|
87
110
|
type RoleLaunchContext = TaskRouteContext & Pick<ExtensionContext, "isProjectTrusted">;
|
|
88
111
|
export declare function resolveTaskRoute(ctx: TaskRouteContext, profileName: ProfileName, agentDir?: string): ResolvedTaskRoute;
|