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 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.3](https://github.com/giuseppecrj/pi-herdr-agents/compare/v2.0.2...v2.0.3)
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. Outside Herdr, the extension skips automatic startup worktree inventory. 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.
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 | — | Isolated Herdr-managed Git worktree; requires `branch`, with optional `base` (committed `HEAD` by default) |
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 start only reports retained-worktree counts. 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.
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 start only reports inventory counts.
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. Inside Herdr with its CLI available, parent session start emits only one compact inventory count when contained worktrees remain. Outside Herdr, startup skips the automatic inventory; 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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-agents",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Asynchronous Pi subagents in Herdr, with optional isolated Git worktrees",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -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.Object({
286
- branch: Type.String({
287
- minLength: 1,
288
- description:
289
- "New branch name for an isolated Herdr-managed Git worktree",
290
- }),
291
- base: Type.Optional(
292
- Type.String({
293
- description:
294
- "Git revision to branch from. Defaults to the source checkout's committed HEAD.",
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 = readPersistentDeliveryLedger(running.sessionFile);
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
- appendPersistentDeliveryLedger(running.sessionFile, {
2689
- task: event.task,
2690
- outcome: "help-requested",
2691
- generation: running.generationId!,
2692
- logicalId: running.logicalId!,
2693
- policyHash: running.policyHash!,
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
- appendPersistentDeliveryLedger(running.sessionFile, {
2728
- task: event.task,
2729
- outcome: "delivered",
2730
- generation: running.generationId!,
2731
- logicalId: running.logicalId!,
2732
- policyHash: running.policyHash!,
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
- resolved.sourceCwd,
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(path, cwd))
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(path, realpathSync(cwd)))
643
+ if (contained(checkout, realpathSync(cwd)))
648
644
  blockers.push(`Live process ${pid} holds the checkout`);
649
645
  } catch {
650
646
  unreadable.add(pid);