pi-herdr-agents 2.0.3 → 2.0.5
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/CHANGELOG.md +31 -1
- package/README.md +5 -5
- package/docs/adr/0011-explicit-worktree-cleanup.md +1 -1
- package/docs/worktree-subagents.md +6 -4
- package/package.json +1 -1
- package/pi-extension/subagents/index.ts +56 -42
- package/pi-extension/subagents/launch.ts +47 -2
- package/pi-extension/subagents/worktree-cleanup.ts +10 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
9
9
|
|
|
10
|
-
## [v2.0.
|
|
10
|
+
## [v2.0.5](https://github.com/giuseppecrj/pi-herdr-agents/compare/v2.0.4...v2.0.5)
|
|
11
|
+
|
|
12
|
+
### Merged
|
|
13
|
+
|
|
14
|
+
- test(worktrees): fix path assertions and clarify cleanup scope [`#65`](https://github.com/giuseppecrj/pi-herdr-agents/pull/65)
|
|
15
|
+
- fix(worktrees): support launches from linked checkouts [`#64`](https://github.com/giuseppecrj/pi-herdr-agents/pull/64)
|
|
16
|
+
- fix(subagents): accept null worktree for strict tool clients [`#57`](https://github.com/giuseppecrj/pi-herdr-agents/pull/57)
|
|
17
|
+
- fix(subagents): interactive mode time [`#62`](https://github.com/giuseppecrj/pi-herdr-agents/pull/62)
|
|
18
|
+
|
|
19
|
+
### Commits
|
|
20
|
+
|
|
21
|
+
- test(worktrees): make quoting expectations independent [`a526824`](https://github.com/giuseppecrj/pi-herdr-agents/commit/a526824dcffd97fa09b158fba5cae05ff75fc100)
|
|
22
|
+
- Merge commit '5fc2ea04b52a03df35b246cef6cb524516d34350' into maint/pr57-null-render-type [`e6fe747`](https://github.com/giuseppecrj/pi-herdr-agents/commit/e6fe747d5df44789df1e8fd42b585d4dbc36a144)
|
|
23
|
+
- perf(startup): skip worktree inventory during session init [`8de2447`](https://github.com/giuseppecrj/pi-herdr-agents/commit/8de24470cb53fd345979cfe501a10645202403e0)
|
|
24
|
+
- fix(worktrees): keep in-scope inventory rows visible [`41639a7`](https://github.com/giuseppecrj/pi-herdr-agents/commit/41639a7673e7a48c28e34e3f3f079c02f6454264)
|
|
25
|
+
- refactor(cleanup): remove unused inventory notice [`330cc96`](https://github.com/giuseppecrj/pi-herdr-agents/commit/330cc9669eff14358e9eea8999f024e0ebfe4187)
|
|
26
|
+
- docs: make worktree inventory on-demand [`e946dba`](https://github.com/giuseppecrj/pi-herdr-agents/commit/e946dba9ca246c15e581f35c098ccf88f003c4f9)
|
|
27
|
+
- fix nullable worktree render args [`afc50bc`](https://github.com/giuseppecrj/pi-herdr-agents/commit/afc50bc6e96005f3a36c3e29a17be36f99d81845)
|
|
28
|
+
|
|
29
|
+
## [v2.0.4](https://github.com/giuseppecrj/pi-herdr-agents/compare/v2.0.3...v2.0.4) - 2026-09-22
|
|
30
|
+
|
|
31
|
+
### Merged
|
|
32
|
+
|
|
33
|
+
- perf: batch persistent delivery ledger reads [`#58`](https://github.com/giuseppecrj/pi-herdr-agents/pull/58)
|
|
34
|
+
|
|
35
|
+
### Commits
|
|
36
|
+
|
|
37
|
+
- chore: release v2.0.4 [`4cacc1a`](https://github.com/giuseppecrj/pi-herdr-agents/commit/4cacc1a65c3cb8627f05264b61684a8089cb7512)
|
|
38
|
+
|
|
39
|
+
## [v2.0.3](https://github.com/giuseppecrj/pi-herdr-agents/compare/v2.0.2...v2.0.3) - 2026-09-21
|
|
11
40
|
|
|
12
41
|
### Commits
|
|
13
42
|
|
|
14
43
|
- fix: skip startup worktree inventory outside Herdr [`34800d7`](https://github.com/giuseppecrj/pi-herdr-agents/commit/34800d79464575063a61ad6f240cd49cc8acd730)
|
|
44
|
+
- chore: release v2.0.3 [`60888f2`](https://github.com/giuseppecrj/pi-herdr-agents/commit/60888f2dea48317257909abbcd0dd374ed861d77)
|
|
15
45
|
|
|
16
46
|
## [v2.0.2](https://github.com/giuseppecrj/pi-herdr-agents/compare/v2.0.1...v2.0.2) - 2026-09-19
|
|
17
47
|
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Delegate investigation, implementation, and review without blocking the parent s
|
|
|
23
23
|
- [Herdr](https://herdr.dev) and its CLI
|
|
24
24
|
- `HERDR_ENV=1` — start Pi from inside Herdr
|
|
25
25
|
|
|
26
|
-
Other terminal multiplexers are not supported.
|
|
26
|
+
Other terminal multiplexers are not supported. Session startup skips worktree inventory to avoid blocking Pi initialization; use `/worktree list` or `worktree_list` to inspect managed worktrees. Outside Herdr, explicit inventory tools still report unavailable inspection as unknown. Worktrees isolate Git checkouts, not processes or permissions; child agents and installed Pi packages run with your user account's access.
|
|
27
27
|
|
|
28
28
|
## Install
|
|
29
29
|
|
|
@@ -576,7 +576,7 @@ subagent({
|
|
|
576
576
|
| `skills` | string | — | Comma-separated skill names |
|
|
577
577
|
| `tools` | string | — | Comma-separated tool names |
|
|
578
578
|
| `cwd` | string | — | Working directory, or source repository when `worktree` is set (see [Role Folders](#role-folders)) |
|
|
579
|
-
| `worktree` | object
|
|
579
|
+
| `worktree` | object \| null | — | Isolated Herdr-managed Git worktree; requires `branch`, with optional `base` (committed `HEAD` by default). Omit or pass `null` to use an ordinary pane in `cwd` when a client requires the property. |
|
|
580
580
|
|
|
581
581
|
### Naming coordinated children
|
|
582
582
|
|
|
@@ -589,7 +589,7 @@ prompts, handoffs, and results.
|
|
|
589
589
|
|
|
590
590
|
### Isolated worktree runs
|
|
591
591
|
|
|
592
|
-
Use one worktree per parallel independent writing task; a single or sequential writer can work in the parent checkout, and read-only agents use ordinary panes. `cwd` selects the source Git repository, `branch` must be unique, and `base` is resolved to an exact commit before creation. If `base` is omitted, the source checkout's committed `HEAD` is used. Parent-checkout changes that have not been committed are not copied.
|
|
592
|
+
Use one worktree per parallel independent writing task; a single or sequential writer can work in the parent checkout, and read-only agents use ordinary panes. Omit `worktree` for an ordinary pane; clients whose generated tool schema requires every property may send `worktree: null` with the same effect. `cwd` selects the source Git repository, `branch` must be unique, and `base` is resolved to an exact commit before creation. If `cwd` is a linked checkout, Herdr provisioning uses the principal checkout while the requested checkout supplies the base SHA and manifest provenance. A successful launch from that linked checkout does not itself authorize cleanup there: cleanup checks the canonical principal/source repository under the invoking parent session's cwd, not `manifest.sourceCwd` or shared Git identity. If cleanup is needed, start the parent Pi session rooted at the principal checkout or an ancestor containing it, then use the normal explicit cleanup flow; changing directories inside an existing Pi session does not change its session cwd. If `base` is omitted, the source checkout's committed `HEAD` is used. Parent-checkout changes that have not been committed are not copied.
|
|
593
593
|
|
|
594
594
|
A launch with `worktree` and an effective bundled `scout`, `reviewer`, or `adversarial-reviewer` returns a non-blocking warning. Scouts and reviewers normally need an ordinary pane; the adversarial reviewer is a coordinator that uses an ordinary pane for its child reviewers. To inspect or review an existing worker result, start an ordinary child in that retained worktree path. Project or global role overrides do not receive these bundled-role warnings. A `read,bash` allowlist is not an enforced read-only boundary because shell commands can mutate files; report-only roles must restrict Bash to safe inspection and avoid artifact-generating verification in the reviewed checkout.
|
|
595
595
|
|
|
@@ -746,9 +746,9 @@ BTW shares the current working directory. It treats inherited work as reference
|
|
|
746
746
|
|
|
747
747
|
Parent sessions can call `worktree_remove({ target: "<path|branch|workspace-id>", preserve: true })` or `/worktree remove <target> [--preserve]`. Preservation is optional and never implied: dirty work is blocked unless explicitly committed first or preserved as a WIP commit. The result reports its SHA even if removal later fails or is refused. A failed preservation commit restores the pre-preservation index and never proceeds to removal. Inventory and removal reports disclose exact ignored-file counts: enumeration is streamed rather than buffered as one listing. Ignored files do not block cleanup and are not captured by preservation; failed counting still blocks removal.
|
|
748
748
|
|
|
749
|
-
Eligibility is rechecked at removal time: canonical source-repository cwd containment, registered linked checkout, no detected process holder, known live child, or persistent lease, and clean Git state with no untracked files or conflicts. Unknown inspection, identity disagreements, detached HEAD, locked checkouts, and initialized submodules block removal. Out-of-scope repositories are never eligible. Open workspaces use Herdr removal; orphans use Git removal and registration pruning after checkout absence is verified. Owned reachable manifests are marked `removed`; manifests from other sessions are not required or rewritten. Stale removed manifests never govern a recreated checkout. Missing or dangling manifest paths do not affect unrelated checkouts; undecidable or conflicting manifest identity still blocks removal. A failed manifest update after removal is reported as a warning. Symlinked ancestors are supported; checkout symlinks escaping the canonical managed root are blocked. Process inspection covers observable same-user processes across sessions, regardless of runtime name, exempting only Herdr-confirmed idle retained shells, never runtimes at the same PID. Unreadable individual process details produce non-blocking warnings without an override flag; scanning continues so another observable holder still blocks removal. Human and structured inventory/removal results disclose incomplete coverage, including warnings seen before a later recheck or failure. Same-user inspection is permission-limited, and other-user processes are not inspected: a protected process could hold the checkout undetected. Warnings aggregate counts and bounded PID samples, not commands or environments. Linux uses `/proc`; macOS uses same-user `lsof` cwd records and warns for unreadable individual records. Unsupported platforms, failed global enumeration (including a failed `lsof` with partial output), and other unverifiable eligibility evidence still block removal. Cleanup Git and Herdr calls have 30-second timeouts.
|
|
749
|
+
Eligibility is rechecked at removal time: canonical source-repository cwd containment, registered linked checkout, no detected process holder, known live child, or persistent lease, and clean Git state with no untracked files or conflicts. A successful launch from a linked checkout does not itself authorize its removal; cleanup uses the canonical principal/source repository under the invoking parent session's cwd, not `manifest.sourceCwd` or shared Git identity. Start the parent Pi session rooted at that principal checkout or an ancestor containing it, then use normal explicit cleanup; `cd` inside an existing Pi session does not change the session cwd. Unknown inspection, identity disagreements, detached HEAD, locked checkouts, and initialized submodules block removal. Out-of-scope repositories are never eligible. Open workspaces use Herdr removal; orphans use Git removal and registration pruning after checkout absence is verified. Owned reachable manifests are marked `removed`; manifests from other sessions are not required or rewritten. Stale removed manifests never govern a recreated checkout. Missing or dangling manifest paths do not affect unrelated checkouts; undecidable or conflicting manifest identity still blocks removal. A failed manifest update after removal is reported as a warning. Symlinked ancestors are supported; checkout symlinks escaping the canonical managed root are blocked. Process inspection covers observable same-user processes across sessions, regardless of runtime name, exempting only Herdr-confirmed idle retained shells, never runtimes at the same PID. Unreadable individual process details produce non-blocking warnings without an override flag; scanning continues so another observable holder still blocks removal. Human and structured inventory/removal results disclose incomplete coverage, including warnings seen before a later recheck or failure. Same-user inspection is permission-limited, and other-user processes are not inspected: a protected process could hold the checkout undetected. Warnings aggregate counts and bounded PID samples, not commands or environments. Linux uses `/proc`; macOS uses same-user `lsof` cwd records and warns for unreadable individual records. Unsupported platforms, failed global enumeration (including a failed `lsof` with partial output), and other unverifiable eligibility evidence still block removal. Cleanup Git and Herdr calls have 30-second timeouts.
|
|
750
750
|
|
|
751
|
-
Cleanup never deletes or rewrites branches, uses force flags, or runs automatically. Session
|
|
751
|
+
Cleanup never deletes or rewrites branches, uses force flags, or runs automatically. Session startup does not scan worktree inventory, avoiding blocking Pi initialization; use `/worktree list` or `worktree_list` for an explicit inventory. Child sessions retain `/worktree list` and `/worktree <name>`, but receive neither cleanup tools nor the remove subcommand. Their repository-local listing labels detached entries `(detached HEAD)`; a detached sibling does not prevent inspection of named branches. See [cleanup and recovery](docs/worktree-subagents.md#cleanup) for details.
|
|
752
752
|
|
|
753
753
|
## Custom Agents
|
|
754
754
|
|
|
@@ -47,7 +47,7 @@ only the cleanup tools and removal subcommand are parent-only.
|
|
|
47
47
|
|
|
48
48
|
Open workspaces are removed through Herdr. Git-only orphans use Git removal,
|
|
49
49
|
verify checkout absence, and then prune stale registrations. Branches and their
|
|
50
|
-
commits are retained. Session
|
|
50
|
+
commits are retained. Session startup does not scan worktree inventory, avoiding blocking Pi initialization; use `/worktree list` or `worktree_list` for an explicit inventory.
|
|
51
51
|
|
|
52
52
|
## Rejected alternatives
|
|
53
53
|
|
|
@@ -36,17 +36,19 @@ subagent({
|
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Omit `worktree` for an ordinary pane. If a generated tool client requires every declared property, pass `worktree: null`; the extension treats it exactly like omission and starts the child in `cwd` without calling `herdr worktree create`.
|
|
40
|
+
|
|
39
41
|
Worktrees isolate checkouts, indexes, and `HEAD`. They are **not security sandboxes**: worktrees still share the repository's object database and most refs, and the child process has the same host permissions as Pi.
|
|
40
42
|
|
|
41
43
|
## Launch contract
|
|
42
44
|
|
|
43
45
|
For a worktree launch:
|
|
44
46
|
|
|
45
|
-
- `cwd` selects the source Git repository. A relative tool argument is resolved from the parent Pi process's current directory.
|
|
47
|
+
- `cwd` selects the source Git repository. A relative tool argument is resolved from the parent Pi process's current directory. When `cwd` is a linked checkout, Herdr provisioning uses its principal checkout while the requested checkout supplies the base SHA and manifest provenance.
|
|
46
48
|
- `worktree.branch` is a new, unique branch name. Git/Herdr rejects a branch that cannot be created or is already checked out elsewhere.
|
|
47
49
|
- `worktree.base` may be any revision that resolves to a commit in the source repository. It defaults to committed `HEAD`.
|
|
48
50
|
- The extension resolves `base` to an exact SHA, writes an ownership manifest, then calls `herdr worktree create --no-focus`.
|
|
49
|
-
- The child starts at the root of the returned worktree, in that workspace's retained root pane and newly owned `Agents` tab. Finishing Pi returns to the interactive checkout shell; it does not close the root pane.
|
|
51
|
+
- The child starts at the root of the returned worktree, in that workspace's retained root pane and newly owned `Agents` tab. Finishing Pi returns to the interactive checkout shell; it does not close the root pane. A successful launch from a linked checkout does not itself authorize cleanup there: cleanup checks the canonical principal/source repository under the invoking parent session's cwd, not `manifest.sourceCwd` or shared Git identity. Start the parent Pi session rooted at the principal checkout or an ancestor containing it, then use normal explicit cleanup; changing directories inside an existing Pi session does not change its session cwd.
|
|
50
52
|
- Uncommitted and untracked files from the parent checkout are not copied. Commit anything the child must see before spawning it, or pass the needed context in the task.
|
|
51
53
|
- Worktree creation does not steal terminal focus.
|
|
52
54
|
|
|
@@ -217,7 +219,7 @@ Parent sessions have an inspect-only inventory and an explicit removal surface:
|
|
|
217
219
|
|
|
218
220
|
The equivalent tools are `worktree_list({})` and `worktree_remove({ target, preserve?: true })`. Use the exact path when a branch name is ambiguous. Children are not offered these tools or `/worktree remove`. Their existing `/worktree list` (repository-local Herdr listing) and `/worktree <name>` handoff remain available. Detached entries are labeled `(detached HEAD)` in the child listing and do not prevent cleanup inspection of named-branch siblings; the detached checkout itself remains blocked.
|
|
219
221
|
|
|
220
|
-
Authorization uses **cwd containment**: the canonical source repository root must equal or descend from the canonical session cwd. It does not use the managed checkout's location or require a current-session manifest. Discovery scans `~/.herdr/worktrees/*/*/`, joins Git registration and Herdr workspace state, and includes orphans from ended sessions. Out-of-scope entries are never removable; unregistered residue and failed probes are unknown, not clean. Inventory includes path, branch, source, workspace, Git state, manifest presence, concrete blockers, and separate process-inspection warnings. The managed root, source, and cwd are canonicalized: symlinked ancestors work normally, but a checkout symlink escaping the managed root is blocked.
|
|
222
|
+
Authorization uses **cwd containment**: the canonical source repository root must equal or descend from the canonical session cwd. For linked checkouts, this means the canonical principal/source repository under the invoking parent session's cwd; a successful launch does not itself authorize cleanup, and authorization does not use `manifest.sourceCwd` or shared Git identity. Start the parent Pi session rooted at the principal checkout or an ancestor containing it, then use normal explicit cleanup; changing directories inside an existing Pi session does not change its session cwd. It does not use the managed checkout's location or require a current-session manifest. Discovery scans `~/.herdr/worktrees/*/*/`, joins Git registration and Herdr workspace state, and includes orphans from ended sessions. Out-of-scope entries are never removable; unregistered residue and failed probes are unknown, not clean. Inventory includes path, branch, source, workspace, Git state, manifest presence, concrete blockers, and separate process-inspection warnings. The managed root, source, and cwd are canonicalized: symlinked ancestors work normally, but a checkout symlink escaping the managed root is blocked.
|
|
221
223
|
|
|
222
224
|
Removal rechecks containment, Git registration, no detected process holder, known live child, or persistent-specialist lease, and no uncommitted, untracked, or conflicted files. Liveness checks observable same-user processes from any session with a cwd inside the checkout, regardless of runtime name (including thread-suffixed Node names). Only Herdr-confirmed idle retained shell PIDs are exempt; an active runtime at the same PID is not. Detached HEAD, locked checkouts, initialized submodules, and conflicting identity evidence block removal. For initialized submodules, deinitialize them deliberately or use operator removal. No force option is provided.
|
|
223
225
|
|
|
@@ -231,7 +233,7 @@ Ignored files do not block cleanup. Inventory shows their exact file count, and
|
|
|
231
233
|
|
|
232
234
|
Open workspaces are removed through Herdr. Git-only orphans use `git worktree remove`, verify checkout absence, and then prune stale Git registrations. Failed removal is reported, never forced. A reachable owned manifest is merge-updated to `removed` with `workspaceRemovedAt` only after success; absent cross-session manifests are reported and are not rewritten. A manifest update failure after removal is a removed-with-warning result, not a failed removal. Removed manifests provide already-removed no-op evidence only; a new checkout at the same path is classified independently. Missing and dangling manifest paths are non-matches. Permission errors, symlink loops, and other undecidable manifest identities remain blockers, as do genuine identity disagreements. Cleanup Git and Herdr calls have 30-second timeouts; a timed-out operation fails closed rather than proceeding with removal.
|
|
233
235
|
|
|
234
|
-
Branches and their commits are always retained: cleanup never deletes, force-updates, or rewrites a branch. It never runs on completion, shutdown, or a timer.
|
|
236
|
+
Branches and their commits are always retained: cleanup never deletes, force-updates, or rewrites a branch. It never runs on completion, shutdown, or a timer. Session startup skips worktree inventory to avoid blocking Pi initialization; use `/worktree list` or `worktree_list` for an explicit inventory. Outside Herdr, explicit inventory tools remain available and report unavailable inspection as unknown. Unsupported platforms or failed global process enumeration block removal; individual visibility gaps are disclosed warnings, never proof that an orphan is idle.
|
|
235
237
|
|
|
236
238
|
## Current limits
|
|
237
239
|
|
package/package.json
CHANGED
|
@@ -129,7 +129,6 @@ import {
|
|
|
129
129
|
listContainedWorktrees,
|
|
130
130
|
removeContainedWorktree,
|
|
131
131
|
formatWorktreeInventory,
|
|
132
|
-
worktreeInventoryNotice,
|
|
133
132
|
type WorktreeCleanupOperations,
|
|
134
133
|
} from "./worktree-cleanup.ts";
|
|
135
134
|
import {
|
|
@@ -282,19 +281,28 @@ const SubagentParams = Type.Object({
|
|
|
282
281
|
}),
|
|
283
282
|
),
|
|
284
283
|
worktree: Type.Optional(
|
|
285
|
-
Type.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
284
|
+
Type.Union(
|
|
285
|
+
[
|
|
286
|
+
Type.Object({
|
|
287
|
+
branch: Type.String({
|
|
288
|
+
minLength: 1,
|
|
289
|
+
description:
|
|
290
|
+
"New branch name for an isolated Herdr-managed Git worktree",
|
|
291
|
+
}),
|
|
292
|
+
base: Type.Optional(
|
|
293
|
+
Type.String({
|
|
294
|
+
description:
|
|
295
|
+
"Git revision to branch from. Defaults to the source checkout's committed HEAD.",
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
295
298
|
}),
|
|
296
|
-
|
|
297
|
-
|
|
299
|
+
Type.Null(),
|
|
300
|
+
],
|
|
301
|
+
{
|
|
302
|
+
description:
|
|
303
|
+
"Optional isolated Herdr-managed Git worktree. Omit or pass null to use an ordinary pane in cwd.",
|
|
304
|
+
},
|
|
305
|
+
),
|
|
298
306
|
),
|
|
299
307
|
fork: Type.Optional(
|
|
300
308
|
Type.Boolean({
|
|
@@ -1218,7 +1226,7 @@ interface PartialSubagentArgs {
|
|
|
1218
1226
|
task?: unknown;
|
|
1219
1227
|
agent?: unknown;
|
|
1220
1228
|
cwd?: unknown;
|
|
1221
|
-
worktree?: PartialWorktreeArgs;
|
|
1229
|
+
worktree?: PartialWorktreeArgs | null;
|
|
1222
1230
|
}
|
|
1223
1231
|
|
|
1224
1232
|
function sendSubagentResult(
|
|
@@ -2422,6 +2430,7 @@ export const __test__ = {
|
|
|
2422
2430
|
resolveUnexpectedErrorPresentation,
|
|
2423
2431
|
shouldAdvanceToFallback,
|
|
2424
2432
|
deliverPersistentTaskEvent,
|
|
2433
|
+
drainPersistentTaskEvents,
|
|
2425
2434
|
notifyPersistentCrash,
|
|
2426
2435
|
sendSubagentResult,
|
|
2427
2436
|
shouldRetainSubagentSurface,
|
|
@@ -2657,11 +2666,13 @@ function deliverPersistentTaskEvent(
|
|
|
2657
2666
|
running: RunningSubagent,
|
|
2658
2667
|
event: ReturnType<typeof readPersistentTaskEvents>[number],
|
|
2659
2668
|
api: Pick<ExtensionAPI, "sendMessage">,
|
|
2669
|
+
ledgerSnapshot?: ReturnType<typeof readPersistentDeliveryLedger>,
|
|
2660
2670
|
): void {
|
|
2661
2671
|
if (!running.persistent || event.generation !== running.generationId) return;
|
|
2662
2672
|
const deliveryKey = `${running.id}:${event.type}:${event.task}`;
|
|
2663
2673
|
if (inFlightPersistentTaskDeliveries.has(deliveryKey)) return;
|
|
2664
|
-
const ledger =
|
|
2674
|
+
const ledger =
|
|
2675
|
+
ledgerSnapshot ?? readPersistentDeliveryLedger(running.sessionFile);
|
|
2665
2676
|
if (event.type === "help-request") {
|
|
2666
2677
|
if (
|
|
2667
2678
|
ledger.some(
|
|
@@ -2685,13 +2696,15 @@ function deliverPersistentTaskEvent(
|
|
|
2685
2696
|
},
|
|
2686
2697
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
2687
2698
|
);
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2699
|
+
ledger.push(
|
|
2700
|
+
appendPersistentDeliveryLedger(running.sessionFile, {
|
|
2701
|
+
task: event.task,
|
|
2702
|
+
outcome: "help-requested",
|
|
2703
|
+
generation: running.generationId!,
|
|
2704
|
+
logicalId: running.logicalId!,
|
|
2705
|
+
policyHash: running.policyHash!,
|
|
2706
|
+
}),
|
|
2707
|
+
);
|
|
2695
2708
|
if (running.taskId === event.task) running.taskId = undefined;
|
|
2696
2709
|
} finally {
|
|
2697
2710
|
inFlightPersistentTaskDeliveries.delete(deliveryKey);
|
|
@@ -2724,13 +2737,15 @@ function deliverPersistentTaskEvent(
|
|
|
2724
2737
|
policyHash: running.policyHash!,
|
|
2725
2738
|
},
|
|
2726
2739
|
);
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2740
|
+
ledger.push(
|
|
2741
|
+
appendPersistentDeliveryLedger(running.sessionFile, {
|
|
2742
|
+
task: event.task,
|
|
2743
|
+
outcome: "delivered",
|
|
2744
|
+
generation: running.generationId!,
|
|
2745
|
+
logicalId: running.logicalId!,
|
|
2746
|
+
policyHash: running.policyHash!,
|
|
2747
|
+
}),
|
|
2748
|
+
);
|
|
2734
2749
|
running.tasksCompleted = completed;
|
|
2735
2750
|
if (running.taskId === event.task) running.taskId = undefined;
|
|
2736
2751
|
if (running.stopState === "pending")
|
|
@@ -2743,13 +2758,25 @@ function deliverPersistentTaskEvent(
|
|
|
2743
2758
|
function drainPersistentTaskEvents(
|
|
2744
2759
|
running: RunningSubagent,
|
|
2745
2760
|
api: Pick<ExtensionAPI, "sendMessage">,
|
|
2761
|
+
readLedger = readPersistentDeliveryLedger,
|
|
2746
2762
|
): void {
|
|
2747
2763
|
const events = readPersistentTaskEvents(running.sessionFile);
|
|
2764
|
+
let ledger: ReturnType<typeof readPersistentDeliveryLedger> | undefined;
|
|
2748
2765
|
for (const event of events.slice(running.observedTaskEvents ?? 0)) {
|
|
2766
|
+
if (!running.persistent || event.generation !== running.generationId)
|
|
2767
|
+
continue;
|
|
2768
|
+
if (
|
|
2769
|
+
inFlightPersistentTaskDeliveries.has(
|
|
2770
|
+
`${running.id}:${event.type}:${event.task}`,
|
|
2771
|
+
)
|
|
2772
|
+
)
|
|
2773
|
+
continue;
|
|
2774
|
+
ledger ??= readLedger(running.sessionFile);
|
|
2749
2775
|
deliverPersistentTaskEvent(
|
|
2750
2776
|
running,
|
|
2751
2777
|
event,
|
|
2752
2778
|
selectCompletionApi(api, runtime.pi),
|
|
2779
|
+
ledger,
|
|
2753
2780
|
);
|
|
2754
2781
|
}
|
|
2755
2782
|
running.observedTaskEvents = events.length;
|
|
@@ -3131,19 +3158,6 @@ export default function subagentsExtension(
|
|
|
3131
3158
|
startStatusRefresh(pi);
|
|
3132
3159
|
updateWidget();
|
|
3133
3160
|
}
|
|
3134
|
-
if (parentSession && ctx.cwd && ctx.hasUI && isTerminalAvailable()) {
|
|
3135
|
-
try {
|
|
3136
|
-
const notice = worktreeInventoryNotice(
|
|
3137
|
-
await listContainedWorktrees(cleanupInput(ctx)),
|
|
3138
|
-
);
|
|
3139
|
-
if (notice) ctx.ui.notify(notice, "info");
|
|
3140
|
-
} catch (error) {
|
|
3141
|
-
ctx.ui.notify(
|
|
3142
|
-
`Worktree inventory unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
3143
|
-
"warning",
|
|
3144
|
-
);
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
3161
|
});
|
|
3148
3162
|
|
|
3149
3163
|
// Clean up on session shutdown
|
|
@@ -84,7 +84,7 @@ export interface FreshPiLaunchRequest {
|
|
|
84
84
|
task: string;
|
|
85
85
|
agent?: string;
|
|
86
86
|
cwd?: string;
|
|
87
|
-
worktree?: { branch: string; base?: string };
|
|
87
|
+
worktree?: { branch: string; base?: string } | null;
|
|
88
88
|
fork?: boolean;
|
|
89
89
|
handoff?: { leafId: string };
|
|
90
90
|
surface?: string;
|
|
@@ -391,6 +391,7 @@ function prepareLaunchSurface(
|
|
|
391
391
|
|
|
392
392
|
const baseRef = request.worktree.base ?? "HEAD";
|
|
393
393
|
const baseSha = resolveGitCommit(resolved.sourceCwd, baseRef);
|
|
394
|
+
const provisionCwd = resolveWorktreeProvisionCwd(resolved.sourceCwd);
|
|
394
395
|
const manifestFile = join(
|
|
395
396
|
resolved.artifactDir,
|
|
396
397
|
"worktree-runs",
|
|
@@ -414,7 +415,7 @@ function prepareLaunchSurface(
|
|
|
414
415
|
try {
|
|
415
416
|
created = operations.createWorktree(
|
|
416
417
|
request.name,
|
|
417
|
-
|
|
418
|
+
provisionCwd,
|
|
418
419
|
request.worktree.branch,
|
|
419
420
|
baseSha,
|
|
420
421
|
);
|
|
@@ -918,6 +919,50 @@ function resolveGitCommit(cwd: string, ref: string): string {
|
|
|
918
919
|
}).trim();
|
|
919
920
|
}
|
|
920
921
|
|
|
922
|
+
function resolveWorktreeProvisionCwd(sourceCwd: string): string {
|
|
923
|
+
let gitDir: string;
|
|
924
|
+
let commonDir: string;
|
|
925
|
+
try {
|
|
926
|
+
gitDir = resolveGitPath(sourceCwd, "--git-dir");
|
|
927
|
+
commonDir = resolveGitPath(sourceCwd, "--git-common-dir");
|
|
928
|
+
} catch (error) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
`Unable to identify the Git checkout for worktree provisioning from ${sourceCwd}: ${errorMessage(error)}`,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (gitDir === commonDir) return sourceCwd;
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
const output = execFileSync(
|
|
937
|
+
"git",
|
|
938
|
+
["worktree", "list", "--porcelain", "-z"],
|
|
939
|
+
{ cwd: sourceCwd },
|
|
940
|
+
).toString("utf8");
|
|
941
|
+
const principal = output
|
|
942
|
+
.split("\0")
|
|
943
|
+
.find((record) => record.startsWith("worktree "))
|
|
944
|
+
?.slice("worktree ".length);
|
|
945
|
+
if (!principal) throw new Error("Git returned no principal worktree");
|
|
946
|
+
return principal;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Unable to determine the principal Git checkout for linked worktree ${sourceCwd}: ${errorMessage(error)}`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function resolveGitPath(
|
|
955
|
+
cwd: string,
|
|
956
|
+
flag: "--git-dir" | "--git-common-dir",
|
|
957
|
+
): string {
|
|
958
|
+
const output = execFileSync(
|
|
959
|
+
"git",
|
|
960
|
+
["rev-parse", "--path-format=absolute", flag],
|
|
961
|
+
{ cwd, encoding: "utf8" },
|
|
962
|
+
);
|
|
963
|
+
return output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
964
|
+
}
|
|
965
|
+
|
|
921
966
|
export function readWorktreeManifest(path: string): JsonObject | undefined {
|
|
922
967
|
try {
|
|
923
968
|
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -214,7 +214,12 @@ export function formatWorktreeInventory(
|
|
|
214
214
|
rows: WorktreeInventoryEntry[],
|
|
215
215
|
): string {
|
|
216
216
|
return (
|
|
217
|
-
rows
|
|
217
|
+
[...rows]
|
|
218
|
+
.sort(
|
|
219
|
+
(a, b) =>
|
|
220
|
+
Number(a.contained) - Number(b.contained) ||
|
|
221
|
+
a.path.localeCompare(b.path),
|
|
222
|
+
)
|
|
218
223
|
.map(
|
|
219
224
|
(row) =>
|
|
220
225
|
`${row.branch ?? "unknown branch"} — ${row.path}\nSource: ${row.sourceRepo ?? "unknown"} · workspace: ${row.workspaceId ?? "none"} · manifest: ${row.manifest.length ? row.manifest.map(({ value }) => value.state ?? "unknown").join(", ") : "absent"}\n${row.classification} · Git: ${row.git ? `${row.git.dirtyFiles} dirty, ${row.git.untrackedFiles} untracked, ${row.git.ignoredFiles} ignored, ${row.git.conflicts} conflicts` : "unknown"}${row.blockers.length ? ` · ${row.blockers.join("; ")}` : " · clean"}${row.warnings.length ? `\nWarning: ${row.warnings.join("; ")}` : ""}`,
|
|
@@ -223,17 +228,6 @@ export function formatWorktreeInventory(
|
|
|
223
228
|
);
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
export function worktreeInventoryNotice(
|
|
227
|
-
rows: WorktreeInventoryEntry[],
|
|
228
|
-
): string | undefined {
|
|
229
|
-
const present = rows.filter((row) => row.contained);
|
|
230
|
-
if (!present.length) return undefined;
|
|
231
|
-
const eligible = present.filter(
|
|
232
|
-
(row) => row.classification === "eligible",
|
|
233
|
-
).length;
|
|
234
|
-
return `Worktrees: ${present.length} present · ${eligible} eligible · ${present.length - eligible} blocked. /worktree list`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
231
|
export async function removeContainedWorktree(
|
|
238
232
|
input: CleanupInput & { target: string; preserve?: boolean },
|
|
239
233
|
): Promise<WorktreeRemovalResult> {
|
|
@@ -552,6 +546,8 @@ function processHolders(
|
|
|
552
546
|
const blockers: string[] = [];
|
|
553
547
|
const unreadable = new Set<string>();
|
|
554
548
|
const shellNames = new Set(["bash", "zsh", "fish", "sh", "dash"]);
|
|
549
|
+
const checkout =
|
|
550
|
+
platform === "linux" || platform === "darwin" ? realpathSync(path) : path;
|
|
555
551
|
if (!process.getuid) throw new Error("Process user identity unavailable");
|
|
556
552
|
const uid = process.getuid();
|
|
557
553
|
if (platform === "linux") {
|
|
@@ -571,7 +567,7 @@ function processHolders(
|
|
|
571
567
|
// A shell can exec a runtime without changing PID/process group.
|
|
572
568
|
if (idleShellPids.has(Number(pid)) && shellNames.has(command)) continue;
|
|
573
569
|
const cwd = realpathSync(`${procRoot}/${pid}/cwd`);
|
|
574
|
-
if (contained(
|
|
570
|
+
if (contained(checkout, cwd))
|
|
575
571
|
blockers.push(`Live process ${pid} holds the checkout`);
|
|
576
572
|
} catch (error) {
|
|
577
573
|
// SAFETY: filesystem probes throw Node errors with an optional errno code.
|
|
@@ -644,7 +640,7 @@ function processHolders(
|
|
|
644
640
|
try {
|
|
645
641
|
if (!cwd || !isAbsolute(cwd))
|
|
646
642
|
throw new Error("Process cwd unavailable");
|
|
647
|
-
if (contained(
|
|
643
|
+
if (contained(checkout, realpathSync(cwd)))
|
|
648
644
|
blockers.push(`Live process ${pid} holds the checkout`);
|
|
649
645
|
} catch {
|
|
650
646
|
unreadable.add(pid);
|