pi-herdr-agents 3.0.1 → 3.0.3

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/AGENTS.md CHANGED
@@ -96,6 +96,8 @@ npm pack --dry-run
96
96
  git diff --check
97
97
  ```
98
98
 
99
+ `npm run lint` needs Node.js 22.18+ (or 22.6–22.17 with `NODE_OPTIONS=--experimental-strip-types`) because oxlint imports `oxlint.config.ts` and its `tools/oxlint/anti-slop/index.ts` plugin as TypeScript.
100
+
99
101
  Run LSP diagnostics on every changed TypeScript file; lint and tests do not catch every TypeScript error.
100
102
 
101
103
  For Herdr or lifecycle changes, run the deterministic suite from inside Herdr. Run only one integration suite at a time on a Herdr instance; concurrent suites compete for terminal focus and process capacity and can cause false timeouts or leaked test resources.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,45 @@ 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
- ## [v3.0.1](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.0...v3.0.1)
10
+ ## [v3.0.3](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.2...v3.0.3)
11
+
12
+ ### Merged
13
+
14
+ - test(integration): re-read worktree row before asserting its open workspace [`#81`](https://github.com/giuseppecrj/pi-herdr-agents/pull/81)
15
+ - Fix Pi readiness check when Pi rewrites argv [`#80`](https://github.com/giuseppecrj/pi-herdr-agents/pull/80)
16
+ - Fix tsc errors and git-version-sensitive index test; document lint Node floor [`#79`](https://github.com/giuseppecrj/pi-herdr-agents/pull/79)
17
+ - fix(worktrees): report the primary workspace creation opened [`#78`](https://github.com/giuseppecrj/pi-herdr-agents/pull/78)
18
+ - fix: keep parked supervision wakes alive until they settle [`#77`](https://github.com/giuseppecrj/pi-herdr-agents/pull/77)
19
+
20
+ ### Commits
21
+
22
+ - remove openspec [`5e31900`](https://github.com/giuseppecrj/pi-herdr-agents/commit/5e31900ea96fdaab6fcf26fc9a335497dcbc528d)
23
+ - fix(worktrees): bound the primary workspace report and hedge its note [`44a18a8`](https://github.com/giuseppecrj/pi-herdr-agents/commit/44a18a8145549801bd17007f05beb8a8cddf41b9)
24
+ - Confirm Pi startup by launch environment, not rewritten argv [`bbfe7e7`](https://github.com/giuseppecrj/pi-herdr-agents/commit/bbfe7e7093bd76d8803562ddb6263c27cfe4a982)
25
+ - fix: settle stranded supervision waits and test wake retain accounting [`d18df1c`](https://github.com/giuseppecrj/pi-herdr-agents/commit/d18df1cb83bedf86951d2f9702a4a6185b44a8e8)
26
+ - test: bound the identity-capture loop anchor and close supervision test gaps [`4a0e71a`](https://github.com/giuseppecrj/pi-herdr-agents/commit/4a0e71a97ebc9ba3f21a5377504f24c3d837d9a2)
27
+ - Treat an unreadable Pi environment as unproven, not fatal [`b69151a`](https://github.com/giuseppecrj/pi-herdr-agents/commit/b69151ae85bd13c45bdf3e980c0b36e315c7c863)
28
+ - test(worktree-cleanup): make the index-restore test hermetic and deterministic [`16840f6`](https://github.com/giuseppecrj/pi-herdr-agents/commit/16840f64b9481983cf44fc928061fd4e6822434d)
29
+ - test(worktree-cleanup): clear inherited git repository variables in the fixture [`323e1fb`](https://github.com/giuseppecrj/pi-herdr-agents/commit/323e1fbf6e6240eb5e7000d63fc84e23b391259a)
30
+ - test(worktrees): expect launch snapshots in scoped inventory [`f733040`](https://github.com/giuseppecrj/pi-herdr-agents/commit/f73304088c8a781b1ed7333a5a09175ec7c896e9)
31
+ - fix(types): resolve strict tsc errors in activity scope guard and recorder [`26bc9ef`](https://github.com/giuseppecrj/pi-herdr-agents/commit/26bc9ef2e8a2afc23901641efdd19661fcd3f756)
32
+ - docs: limit the lint type-stripping fallback to Node 22.6-22.17 [`ccab597`](https://github.com/giuseppecrj/pi-herdr-agents/commit/ccab597cfaf2bcf6e37889ae3103dd6e9c78c6ef)
33
+ - docs: note that npm run lint needs Node.js 22.18 or newer [`b8ffa8d`](https://github.com/giuseppecrj/pi-herdr-agents/commit/b8ffa8d1d83419588db8d4aed506d5d236360673)
34
+ - test(integration): check cancel manifest workspace id; clarify Herdr Node note [`aac18d9`](https://github.com/giuseppecrj/pi-herdr-agents/commit/aac18d926385582ccc36783fd8a9475db7df7e3d)
35
+ - test(worktree-cleanup): snapshot the index after git status [`aa869b2`](https://github.com/giuseppecrj/pi-herdr-agents/commit/aa869b2a62c8868d377b134d3f05b029692f2eb0)
36
+ - docs: integration tests need the Herdr server on Node.js 22.19+ [`bdd791b`](https://github.com/giuseppecrj/pi-herdr-agents/commit/bdd791bd934ea3174db32e1fcf1353353506224b)
37
+
38
+ ## [v3.0.2](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.1...v3.0.2) - 2026-10-06
39
+
40
+ ### Merged
41
+
42
+ - fix(subagents): resolve submodule sources and refresh fallback context [`#76`](https://github.com/giuseppecrj/pi-herdr-agents/pull/76)
43
+
44
+ ### Commits
45
+
46
+ - chore: release v3.0.2 [`245e54e`](https://github.com/giuseppecrj/pi-herdr-agents/commit/245e54e7ccc937f69bee6093906d26b03b8a92c9)
47
+
48
+ ## [v3.0.1](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.0...v3.0.1) - 2026-10-06
11
49
 
12
50
  ### Merged
13
51
 
@@ -16,6 +54,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
16
54
  ### Commits
17
55
 
18
56
  - docs: README for 3.0.0: pstack ships poteto-mode not a role, safety and uninstall, contents, agent pointer [`7ed9969`](https://github.com/giuseppecrj/pi-herdr-agents/commit/7ed99693b1a2d25137d4ba2430f23255923ce3ea)
57
+ - chore: release v3.0.1 [`ba349db`](https://github.com/giuseppecrj/pi-herdr-agents/commit/ba349db4a4280a39b70a067fb62e7816f9a2061f)
19
58
  - docs: hero image uses generic agent labels instead of removed role names [`70ad6f0`](https://github.com/giuseppecrj/pi-herdr-agents/commit/70ad6f04d492f58e04062e01c1a529ddd1708431)
20
59
  - docs: address review: full write footprint, worktree cleanup before uninstall, skill table delimiter [`ababee3`](https://github.com/giuseppecrj/pi-herdr-agents/commit/ababee3a3f18d2112ec8959303986e1a08e3dc2a)
21
60
 
package/CONTEXT.md CHANGED
@@ -71,6 +71,13 @@ verified by the parent against the Herdr pane. A worktree cancel signals and
71
71
  judges exit only by it.
72
72
  _Avoid_: argv match, foreground process, shell visibility
73
73
 
74
+ **Pi startup confirmation**:
75
+ The bounded check that a `/worktree` handoff's Pi is running before its
76
+ workspace is focused: a Pi process in the root pane with the worktree cwd whose
77
+ launch-time environment names the launched session in
78
+ `PI_HERDR_AGENTS_SESSION`. Nothing inside Pi reads that marker.
79
+ _Avoid_: argv match, `--session` visibility, child-context hint
80
+
74
81
  **No-progress advisory**:
75
82
  An internal warning that an active child shows no durable progress in its session
76
83
  JSONL or activity snapshot. It is advisory only and never changes the child's
@@ -182,9 +189,13 @@ global enumeration and unsupported platforms remain blockers.
182
189
  _Avoid_: Proven unrelated, machine-wide inactivity, bypass permission
183
190
 
184
191
  **Explicit worktree removal**:
185
- A parent-requested removal of one named managed checkout and its open workspace,
186
- with absence verification and retained branch history. Never automatic reaping.
187
- _Avoid_: Branch deletion, completion cleanup
192
+ A parent-requested removal of one named managed checkout and its open worktree
193
+ workspace, with absence verification and retained branch history. Never automatic
194
+ reaping. It never closes the source repository's primary workspace. When this
195
+ process recorded that worktree creation appeared to open that workspace and a
196
+ read taken just before the report still shows it untouched, removal suggests
197
+ the `herdr workspace close` command, to use only if the workspace is unused.
198
+ _Avoid_: Branch deletion, completion cleanup, automatic close of a workspace the user may have used
188
199
 
189
200
  **Dirty-state preservation**:
190
201
  Explicit opt-in staging and WIP commitment of a worktree's uncommitted and
package/README.md CHANGED
@@ -553,9 +553,14 @@ project definitions keep their precedence over them.
553
553
 
554
554
  On supported local filesystems, supervision uses file wake-ups plus one shared
555
555
  4.8-second pane reconciliation. A wake-up only prompts fresh evidence
556
- collection; it never establishes a result by itself. If the watcher or shared
557
- pane inspection becomes unavailable, supervision quietly returns to the legacy
558
- one-second polling cadence. No caller action is required.
556
+ collection; it never establishes a result by itself. While any child's wait for
557
+ that check is parked, the coordinator's file watches stay referenced, so the
558
+ sidecar is still observed when the parent has no other event-loop work. The
559
+ reference is released when the wait settles, is aborted, or its child is
560
+ unregistered. With no parked wait, the watches stay unreferenced and do not
561
+ keep the process running. If the watcher
562
+ or shared pane inspection becomes unavailable, supervision quietly returns to
563
+ the legacy one-second polling cadence. No caller action is required.
559
564
 
560
565
  Set `supervision.forcePolling` to `true` in the durable user `config.json` to
561
566
  disable wake-ups and use that legacy cadence deliberately. The setting is read
@@ -625,10 +630,17 @@ example `provider/preferred, provider/fallback`. The tool argument also accepts
625
630
  candidate before launch, then launches later candidates only after the selected
626
631
  child settles with a provider/agent error. Pi owns any automatic transient
627
632
  retrying inside that child; the extension does not infer retry counts or
628
- permanence from the error text. A completed child result, including a negative
629
- task result, never switches models. Completion metadata reports the requested
630
- candidate, every attempted candidate, the model actually used, and each raw
631
- model failure in attempt order when fallbacks are tried.
633
+ permanence from the error text. A later candidate that launches after a parent
634
+ `/reload`, `/new`, `/resume`, or `/fork` uses the live parent session for its
635
+ artifacts and lineage, as completion delivery does. Its candidate list and
636
+ thinking level stay those of the original call. The original parent directory
637
+ and process directory remain the bases for directory resolution. Role files are
638
+ read again for each attempt, so a changed role `cwd` can redirect a fallback
639
+ when the tool call did not specify `cwd`. If no live parent context is available,
640
+ that candidate fails without launching. A completed child result, including a
641
+ negative task result, never switches models. Completion metadata reports the
642
+ requested candidate, every attempted candidate, the model actually used, and
643
+ each raw model failure in attempt order when fallbacks are tried.
632
644
 
633
645
  A catalog-listed model and configured authentication do not prove that the
634
646
  active provider account can use that model. Providers may reject an account /
@@ -859,15 +871,19 @@ is no other parent/child protocol.
859
871
 
860
872
  `/worktree <worktree> [task]` creates a Herdr-managed worktree from the current committed branch and launches a new interactive Pi session there with the active conversation branch. The original session remains available. Use `/worktree list` or `worktree_list({})` to inspect managed worktrees, including cross-session orphans, whose canonical source repositories are inside the session's cwd subtree. This is a new-process handoff, not an in-place move of the existing shell or Pi process.
861
873
 
874
+ The destination workspace is focused only after Pi startup is confirmed. Every fresh launch, including the handoff, sets `PI_HERDR_AGENTS_SESSION` to its session file; confirmation requires a Pi process in the root pane with the worktree cwd whose launch-time environment carries that value. Pi rewrites its process title, so command-line text is never evidence. Nothing inside Pi reads the variable, and it is not a child-context hint. Confirmation reads Linux `/proc`; see [Worktree subagents](docs/worktree-subagents.md).
875
+
862
876
  ---
863
877
 
864
878
  ### Explicit worktree cleanup
865
879
 
866
880
  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.
867
881
 
868
- 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.
882
+ 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. A source repository that is a Git submodule is located through the `core.worktree` setting of its shared Git directory under the superproject's `.git/modules`, not through that directory's parent. Unknown inspection, identity disagreements, detached HEAD, locked checkouts, and initialized submodules block removal. Out-of-scope repositories are never eligible. Open worktree workspaces use Herdr removal. The source repository's primary workspace is never closed automatically. If this process recorded that worktree creation appeared to open it and a read taken just before the report still shows it untouched, removal adds `wX appears to have been opened by worktree creation; if you haven't used it, close it with herdr workspace close wX`; a missing or wrong-typed field, another open workspace for the repository, another session, or a restarted process reports nothing. 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.
883
+
884
+ A worktree launch may add three synchronous Herdr snapshots (`worktree list` before create, `worktree list` after create, and `pane list` for the opened primary), each bounded at 3 seconds, 9 seconds if all three run to their timeouts. A failed snapshot claims nothing and is returned as a launch diagnostic, shown in the `subagent` acknowledgement and the `/worktree` notice; it is not written to the console. Claims are kept only in this process and are not written to the manifest. Ordinary non-worktree launches add no Herdr calls. Explicit removal adds a primary-workspace report only when this process holds a claim for the removed source repository: `worktree list`, `workspace get`, and `pane list`, each asynchronous and bounded at 3 seconds, taken after the checkout is removed. That is at most three calls and 9 seconds of wall time, without blocking the event loop. These report calls sit on top of the existing cleanup Git and Herdr calls, which stay at 30 seconds each.
869
885
 
870
- 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.
886
+ Cleanup never deletes or rewrites branches, uses force flags, or runs automatically. It never closes the source repository's primary workspace. 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.
871
887
 
872
888
  ## Custom Agents
873
889
 
@@ -1251,12 +1267,16 @@ npm run lint
1251
1267
  npm pack --dry-run
1252
1268
  ```
1253
1269
 
1270
+ `npm run lint` needs Node.js 22.18+ (or 22.6–22.17 with `NODE_OPTIONS=--experimental-strip-types`) because oxlint imports TypeScript directly: its configuration, `oxlint.config.ts`, and the JavaScript plugin it loads, `tools/oxlint/anti-slop/index.ts`. Without type stripping, oxlint fails before linting with `Unknown file extension ".ts"`. This is a contributor tooling requirement, not a runtime requirement for package users.
1271
+
1254
1272
  Run the required end-to-end suite from inside Herdr:
1255
1273
 
1256
1274
  ```bash
1257
1275
  npm run test:integration
1258
1276
  ```
1259
1277
 
1278
+ Start the Herdr server with Node.js 22.19+ on its `PATH`, because test panes inherit the server's environment, not the test runner's: Pi 1.0.0 needs Node.js 22.19+ and crashes at startup on versions older than 22.15.
1279
+
1260
1280
  The deterministic suite launches real Pi sessions, Herdr panes, and worktrees without provider credentials. The optional live-provider smoke test is not a merge gate:
1261
1281
 
1262
1282
  ```bash
package/docs/README.md CHANGED
@@ -60,10 +60,11 @@ real implementations; in-memory fakes are conformance fixtures only.
60
60
  | [`0008`](adr/0008-adopt-pi-only-subagent-execution.md) | Partially superseded by 0012 | Keep Pi-only execution; 0012 supersedes only the adapter-seam restriction. |
61
61
  | [`0009`](adr/0009-remove-workflow-subsystem.md) | Accepted; skill ownership moved by 0013 | Remove the workflow subsystem; use public subagent fan-out and parent synthesis. |
62
62
  | [`0010`](adr/0010-persistent-specialists-as-session-generations.md) | Accepted | Define persistent specialists as logical identities with policy-bound session generations. |
63
- | [`0011`](adr/0011-explicit-worktree-cleanup.md) | Accepted | Authorize explicit worktree cleanup by cwd containment; retain branches and reject automatic reaping. |
63
+ | [`0011`](adr/0011-explicit-worktree-cleanup.md) | Accepted; amended by 0015 | Authorize explicit worktree cleanup by cwd containment; retain branches and reject automatic reaping. |
64
64
  | [`0012`](adr/0012-adopt-maestro-seams-in-repo.md) | Accepted | Adopt in-repo seams and conformance fakes; keep one package, Pi-only execution, and Herdr-only surfaces. |
65
65
  | [`0013`](adr/0013-pack-neutral-execution-host.md) | Accepted | Ship a pack-neutral execution host; roles and workflows move to optional packs, and `/iterate` and `/btw` are removed. |
66
66
  | [`0014`](adr/0014-operator-cancel-terminal-intent.md) | Accepted | Operator cancel records terminal intent before owned termination: no fallback, one confirmed cancelled result, unconfirmed runs stay live. |
67
+ | [`0015`](adr/0015-report-opened-primary-workspace.md) | Accepted | Report, and never automatically close, a primary workspace that worktree creation opened. |
67
68
 
68
69
  ## Historical material
69
70
 
@@ -1,6 +1,6 @@
1
1
  # ADR-0011: Authorize explicit worktree cleanup by cwd containment
2
2
 
3
- - **Status:** Accepted
3
+ - **Status:** Accepted; amended by [ADR-0015](0015-report-opened-primary-workspace.md)
4
4
  - **Date:** 2026-09-17
5
5
  - **Scope:** Managed subagent worktree cleanup (issue #45)
6
6
 
@@ -0,0 +1,121 @@
1
+ # ADR-0015: Report the primary workspace worktree creation opened
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-10-07
5
+ - **Amends:** [ADR-0011](0011-explicit-worktree-cleanup.md)
6
+
7
+ ## Context
8
+
9
+ `herdr worktree create` groups a new worktree with the source repository's
10
+ primary workspace and opens that workspace when none is open. `herdr worktree
11
+ remove` removes only the linked checkout, so the first worktree of an idle
12
+ repository leaves an empty shell workspace behind.
13
+
14
+ Herdr's `WorktreeCreated` result does not include `created_parent`, so this
15
+ extension cannot ask Herdr which workspace the create call opened. The only
16
+ signal is a `herdr worktree list --cwd` snapshot before create and another
17
+ after it. A workspace the user or another session opens during a long checkout
18
+ can be attributed to this create. That race is inherent in the snapshots.
19
+
20
+ ## Decision
21
+
22
+ Explicit worktree removal never closes the source repository's primary
23
+ workspace.
24
+
25
+ On a worktree launch, this process may record one claim: workspace id, the
26
+ source `repo_key`, the only pane's `terminal_id`, and the checkout path the
27
+ workspace was opened at. A workspace that was already open is not claimed. A
28
+ snapshot that fails, times out, or lacks a required field claims nothing. The
29
+ failure is returned as a launch diagnostic, shown in the `subagent`
30
+ acknowledgement and the `/worktree` notice, and never written to the console.
31
+ Ordinary non-worktree launches do not take these snapshots.
32
+
33
+ The claim lives only in the creating process and is not written to the
34
+ manifest. Removal from another session, or after this process restarts,
35
+ reports nothing. Extension reload inside the same process keeps the in-memory
36
+ claim.
37
+
38
+ On explicit removal, core checks whether this process holds a claim whose
39
+ checkout is the removed source repository. If not, removal makes no report
40
+ calls at all. Otherwise the Herdr surface reads the source once more, after
41
+ the checkout is removed and immediately before any sentence. It may tell the
42
+ parent:
43
+
44
+ `wX appears to have been opened by worktree creation; if you haven't used it, close it with herdr workspace close wX`
45
+
46
+ only when that read shows the workspace untouched: the default label equal to
47
+ the repository name (both present), not focused, one tab, not a linked
48
+ worktree, the claimed `repo_key`, one pane, the recorded terminal, and `cwd`
49
+ and `foreground_cwd` at the recorded checkout. No other workspace may be open
50
+ for the repository; a listed row with an open workspace and no boolean
51
+ `is_linked_worktree` blocks the sentence too. Any missing or wrong-typed field
52
+ a check uses means the workspace is not untouched, so removal says nothing
53
+ about it. Shell idleness and child processes are not checked: the sentence is
54
+ advisory and hedged, and the extra reads would double the removal cost
55
+ without making a close safe.
56
+
57
+ The surface method is `SurfaceProvider.reportOpenedPrimaryWorkspace`. Herdr
58
+ implements it. Other surfaces return no report. Core does not import Herdr.
59
+
60
+ A failed report is a warning. The checkout removal still succeeds. Nothing in
61
+ this path polls, retries, or sleeps.
62
+
63
+ ### Blocking time
64
+
65
+ A worktree launch adds at most three synchronous Herdr calls, each bounded at
66
+ 3 seconds:
67
+
68
+ 1. `herdr worktree list --cwd` before create
69
+ 2. `herdr worktree list --cwd` after create, only when the before snapshot
70
+ succeeded and showed no primary workspace
71
+ 3. `herdr pane list --workspace` for that workspace's terminal, only when the
72
+ after snapshot shows a new primary workspace with a repo key and checkout
73
+ path
74
+
75
+ If each runs to its timeout, the extra blocking time is 9 seconds. A failed
76
+ before-snapshot skips the later two. `herdr worktree create` itself is an
77
+ existing call and is not given a new timeout here.
78
+
79
+ Explicit removal adds at most three asynchronous calls, each bounded at
80
+ 3 seconds, and only when this process holds a claim for the removed source
81
+ repository:
82
+
83
+ 1. `herdr worktree list --cwd`
84
+ 2. `herdr workspace get`
85
+ 3. `herdr pane list --workspace`
86
+
87
+ If each runs to its timeout, removal takes up to 9 more seconds of wall time.
88
+ The calls do not block the event loop. A failed or decisive call stops the
89
+ rest. These calls are in addition to the existing cleanup Git and Herdr calls,
90
+ which stay at 30 seconds each.
91
+
92
+ ## Rejected alternatives
93
+
94
+ - **Automatic close from the untouched-workspace heuristic.** One tab, one
95
+ pane, the same terminal id, cwd at the checkout, and the default label, even
96
+ with an idle shell and no children, cannot tell whether the user worked in
97
+ the workspace. `git
98
+ log`, a test run, and `cd` back to the checkout leave that same picture, and
99
+ closing it would destroy the scrollback.
100
+ - **Pane fingerprinting.** Reading `herdr pane read --source recent` at claim
101
+ time and requiring the same text before a close would catch some commands
102
+ the heuristic misses. It is not clearly better than reporting. Prompt
103
+ redraws, hooks, and Herdr's own startup change the text without a user
104
+ command; a cleared screen looks untouched; a read can fail; and the gap
105
+ between the confirming read and a close is still a race. A mistaken close
106
+ is destructive. A mistaken report is a command the user can ignore.
107
+ - **Trusting the manifest after restart.** The creating process is the only
108
+ observer of the before/after snapshots. A later process would be acting on
109
+ a claim it did not make.
110
+
111
+ ## Consequences
112
+
113
+ Cwd containment, branch retention, and the rejection of automatic reaping in
114
+ ADR-0011 are unchanged. A misattributed claim can name a workspace this create
115
+ did not open, so the sentence says the workspace "appears" to have been opened
116
+ and asks the user to close it only if they have not used it. The message does
117
+ not close it. There is no second re-check: the single set of reads is taken
118
+ immediately before the sentence, and a workspace that changes after those
119
+ reads can still be named. The sentence is still only a suggestion.
120
+
121
+ See the [operating guide](../worktree-subagents.md#cleanup).
@@ -48,11 +48,12 @@ For a worktree launch:
48
48
  - `worktree.branch` is a new, unique branch name. Git/Herdr rejects a branch that cannot be created or is already checked out elsewhere.
49
49
  - `worktree.base` may be any revision that resolves to a commit in the source repository. It defaults to committed `HEAD`.
50
50
  - The extension resolves `base` to an exact SHA, writes an ownership manifest, then calls `herdr worktree create --no-focus`.
51
+ - Herdr groups the new worktree with the source repository's primary workspace and opens one when none is open. The extension reads `herdr worktree list --cwd` before and after creation, and `herdr pane list` for the opened workspace's terminal, only to record a claim in this process. Each of those reads has a 3-second timeout. A worktree launch runs at most three of them; if each runs to its timeout, that is 9 seconds on top of create. A workspace you already had open is not claimed. An unreadable, timed-out, or incomplete snapshot claims nothing and is reported as a launch diagnostic in the `subagent` acknowledgement or `/worktree` notice, never on the console. A failed before-snapshot skips the later reads. Ordinary pane launches do not make these calls. The claim is not written to the manifest and is not kept after this process exits.
51
52
  - 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.
52
53
  - 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.
53
54
  - Worktree creation does not steal terminal focus.
54
55
 
55
- For an explicit interactive handoff, use `/worktree <worktree> [task]`. It creates the worktree from the current committed branch, forks the active conversation branch into the target-cwd session, launches a normal long-lived Pi process in the returned root pane, and focuses the destination workspace only after Herdr confirms Pi is running with the expected session and worktree cwd. Use `/worktree list` to inspect managed worktrees whose source repositories are inside the current cwd subtree, including cross-session orphans. The original process and session remain intact; pane movement is not used to change a running shell's cwd.
56
+ For an explicit interactive handoff, use `/worktree <worktree> [task]`. It creates the worktree from the current committed branch, forks the active conversation branch into the target-cwd session, launches a normal long-lived Pi process in the returned root pane, and focuses the destination workspace only after Pi startup is confirmed: a Pi process in the root pane's Herdr foreground list must have the worktree cwd and, in its launch-time environment (`/proc/<pid>/environ`), `PI_HERDR_AGENTS_SESSION` naming the forked session file. An unreadable environment (for example, a process that is exiting) never confirms startup. Command-line text is never evidence, because Pi rewrites its process title and Herdr then reports only `pi`. The confirmation reads Linux `/proc`; on other hosts it fails within its bounded wait and the launch-failure retention below applies. Use `/worktree list` to inspect managed worktrees whose source repositories are inside the current cwd subtree, including cross-session orphans. The original process and session remain intact; pane movement is not used to change a running shell's cwd.
56
57
 
57
58
  `worktree` cannot be set in agent frontmatter and is not exposed by the `/subagent <agent> <task>` shorthand. It is selected per call to the `subagent` tool. Ordered model fallback lists are not supported for worktree subagents: a failed attempt retains its worktree and branch for review, so a retry cannot safely reuse the requested branch. A persistent specialist either holds one worktree lease for its full lifetime or runs read-only in an ordinary pane; it cannot be re-bound.
58
59
 
@@ -223,6 +224,8 @@ The equivalent tools are `worktree_list({})` and `worktree_remove({ target, pres
223
224
 
224
225
  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.
225
226
 
227
+ A source repository can be a Git submodule. Its shared Git directory lives under the superproject's `.git/modules`, so cleanup reads `core.worktree` through `git config`, which also follows included configuration files. Cleanup then requires the same top-level and common-directory proofs as for any other source. If the read or a proof fails, the entry is unknown and cannot be removed. Initialized submodules inside a managed checkout still block removal.
228
+
226
229
  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.
227
230
 
228
231
  Unreadable individual process details are **warnings, not blockers**, with no override flag. Scanning continues after each unreadable entry, so another observable holder still blocks removal. Unknown processes are not classified as unrelated. Inventory rows expose separate `blockers` and `warnings` arrays; removal results expose `warnings` and retain warnings from earlier inspections, including refusals and preservation/removal failures when available. Human-readable output includes the same warnings, with counts and at most ten sampled PIDs per inspection, not process commands or environments. Pi tool refusals and failures use thrown errors, so their warnings travel in the error message rather than structured tool details.
@@ -233,7 +236,7 @@ Dirty work requires an explicit `--preserve` or `preserve: true`. This stages al
233
236
 
234
237
  Ignored files do not block cleanup. Inventory shows their exact file count, and a successful removal reports how many were deleted. Counting streams the NUL-delimited Git listing rather than buffering all paths; errors and the 30-second timeout still block removal, never report a guessed zero. Preservation does not capture ignored files; attempted and successful preservation reports disclose that exclusion when ignored files are present.
235
238
 
236
- 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.
239
+ Open worktree workspaces are removed through Herdr. That removal never closes the source repository's primary workspace. If this process recorded that worktree creation opened it, removal reads the workspace after the checkout is removed and may report `wX appears to have been opened by worktree creation; if you haven't used it, close it with herdr workspace close wX` only when those reads, taken immediately before that sentence, show the workspace untouched: the default label equal to the repository name, not focused, one tab, not a linked worktree, the claimed `repo_key`, one pane, the recorded terminal, and both `cwd` and `foreground_cwd` at the recorded checkout, with no other workspace open for the repository. A listed row with an open workspace but no boolean `is_linked_worktree` blocks the sentence. A missing or wrong-typed `label`, `repo_name`, `focused`, `tab_count`, `foreground_cwd`, `terminal_id`, or any other field a check uses means the workspace is not untouched, so removal says nothing about it. Shell idleness and child processes are not checked; the sentence is a hedged suggestion, not a close. The before/after list can attribute a workspace that you or another session opened during a long checkout, because Herdr does not say which workspace create opened; the message does not close anything, so a wrong id is only a suggestion. Claims live only in the creating process: removal from another session, or after a restart, reports nothing. The report's longest path is three asynchronous calls — `worktree list`, `workspace get`, and `pane list`, 3 seconds each — 9 seconds of wall time if every call runs to its timeout, without blocking the event loop. A failed call stops the rest. With no claim for the removed source repository, removal adds none of these calls. 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.
237
240
 
238
241
  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.
239
242
 
@@ -244,7 +247,8 @@ This first version intentionally does not provide:
244
247
  - automatic push, PR creation, merge, or cherry-pick
245
248
  - automatic worktree or branch removal
246
249
  - worktree-aware `subagent_resume`
247
- - durable restart reconciliation
250
+ - durable restart reconciliation (an opened-primary-workspace claim does not survive process restart)
251
+ - automatic close of the source repository's primary workspace
248
252
  - dependency DAG scheduling or merge queues
249
253
  - stacked-branch management
250
254
  - multi-repository workspaces
@@ -316,8 +316,18 @@ export function createSubagentActivityRecorder(params: {
316
316
  const runningChildId = params.runningChildId?.trim();
317
317
  const activityFile = params.activityFile?.trim();
318
318
  if (!runningChildId || !activityFile) return createNoopRecorder();
319
+ return createFileActivityRecorder(
320
+ runningChildId,
321
+ activityFile,
322
+ params.now ?? (() => Date.now()),
323
+ );
324
+ }
319
325
 
320
- const now = params.now ?? (() => Date.now());
326
+ function createFileActivityRecorder(
327
+ runningChildId: string,
328
+ activityFile: string,
329
+ now: () => number,
330
+ ): SubagentActivityRecorder {
321
331
  const createdAt = now();
322
332
  const activity: SubagentActivityState = {
323
333
  version: 1,
@@ -24,9 +24,11 @@ import type {
24
24
  import {
25
25
  getSubagentProcessIdentityFile,
26
26
  linuxProcessProbe,
27
+ readLinuxProcessEnvironment,
27
28
  readProcessIdentityRecord,
28
29
  verifyProcessIdentityRecord,
29
30
  type PiProcessIdentity,
31
+ type ProcessEnvironmentReader,
30
32
  type ProcessIdentityProbe,
31
33
  } from "./process-identity.ts";
32
34
  import {
@@ -141,11 +143,12 @@ export interface ProcessIdentityExpectation {
141
143
 
142
144
  type MaybePromise<T> = T | Promise<T>;
143
145
 
144
- interface WorktreeSurfaceForLaunch {
146
+ export interface WorktreeSurfaceForLaunch {
145
147
  path: string;
146
148
  branch: string;
147
149
  workspaceId: string;
148
150
  paneId: string;
151
+ diagnostics?: string[];
149
152
  }
150
153
 
151
154
  export interface PiLaunchOperations {
@@ -190,30 +193,38 @@ function placementFromPaneConfig(config: PaneConfig) {
190
193
  function worktreeSurfaceForLaunch(
191
194
  worktree: WorktreeSurface,
192
195
  ): WorktreeSurfaceForLaunch {
193
- return {
196
+ const surface: WorktreeSurfaceForLaunch = {
194
197
  path: worktree.path,
195
198
  branch: worktree.branch,
196
199
  workspaceId: worktree.workspaceId,
197
200
  paneId: worktree.surfaceId,
198
201
  };
202
+ if (worktree.diagnostics?.length) surface.diagnostics = worktree.diagnostics;
203
+ return surface;
199
204
  }
200
205
 
206
+ /**
207
+ * Names the launched session in the Pi process's exec-time environment. Pi
208
+ * rewrites its command line, so `--session` is never readiness evidence.
209
+ * Nothing inside Pi reads it.
210
+ */
211
+ export const PI_LAUNCH_SESSION_ENV = "PI_HERDR_AGENTS_SESSION";
212
+
201
213
  export function isExpectedPiProcess(
202
214
  process: {
215
+ pid: number;
203
216
  name?: string;
204
217
  argv0?: string;
205
- argv?: string[];
206
218
  cwd?: string;
207
219
  },
208
220
  sessionFile: string,
209
221
  cwd: string,
222
+ readEnvironment: ProcessEnvironmentReader = readLinuxProcessEnvironment,
210
223
  ): boolean {
211
- const sessionIndex = process.argv?.indexOf("--session") ?? -1;
212
224
  return (
213
225
  (process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
214
- sessionIndex >= 0 &&
215
- process.argv?.[sessionIndex + 1] === sessionFile &&
216
- process.cwd === cwd
226
+ process.cwd === cwd &&
227
+ readEnvironment(process.pid)?.get(PI_LAUNCH_SESSION_ENV) === sessionFile
217
228
  );
218
229
  }
219
230
 
@@ -232,15 +243,17 @@ async function waitForSurfacePiReady(
232
243
  while (Date.now() <= deadline) {
233
244
  try {
234
245
  const info = await Promise.resolve(provider.getProcessInfo(surface));
235
- if (
236
- info.foregroundProcesses.some((process) =>
237
- isExpectedPiProcess(process, sessionFile, cwd),
238
- )
239
- ) {
240
- return;
246
+ // An unreadable process (one that is exiting) proves nothing, but must
247
+ // not hide a matching sibling.
248
+ for (const process of info.foregroundProcesses) {
249
+ try {
250
+ if (isExpectedPiProcess(process, sessionFile, cwd)) return;
251
+ } catch (error) {
252
+ lastError = errorMessage(error);
253
+ }
241
254
  }
242
255
  } catch (error) {
243
- lastError = error instanceof Error ? error.message : String(error);
256
+ lastError = errorMessage(error);
244
257
  }
245
258
  if (Date.now() >= deadline) break;
246
259
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
@@ -636,6 +649,7 @@ async function prepareLaunchSurface(
636
649
  baseSha,
637
650
  manifestFile,
638
651
  };
652
+ if (created.diagnostics?.length) worktree.diagnostics = created.diagnostics;
639
653
  worktreeOps.writeWorktreeManifest(manifestFile, {
640
654
  state: "provisioned",
641
655
  ...ownership,
@@ -857,7 +871,7 @@ function buildPiCommand(
857
871
  }
858
872
  }
859
873
 
860
- const env: string[] = [];
874
+ const env = [`${PI_LAUNCH_SESSION_ENV}=${shellQuote(artifacts.sessionFile)}`];
861
875
  if (artifacts.localAgentDir) {
862
876
  env.push(`PI_CODING_AGENT_DIR=${shellQuote(artifacts.localAgentDir)}`);
863
877
  } else if (process.env.PI_CODING_AGENT_DIR) {
@@ -123,6 +123,44 @@ export function createLinuxProcessProbe(
123
123
 
124
124
  export const linuxProcessProbe = createLinuxProcessProbe();
125
125
 
126
+ /**
127
+ * A process's environment as it was at exec. Unlike its command line, Pi's
128
+ * process-title rewrite leaves this intact. `undefined` only when no such PID
129
+ * exists; throws on non-Linux hosts and any other read failure.
130
+ */
131
+ export type ProcessEnvironmentReader = (
132
+ pid: number,
133
+ ) => Map<string, string> | undefined;
134
+
135
+ export function createLinuxProcessEnvironmentReader(
136
+ read: (path: string) => string = (path) => readFileSync(path, "utf8"),
137
+ ): ProcessEnvironmentReader {
138
+ return (pid) => {
139
+ if (process.platform !== "linux")
140
+ throw new Error(
141
+ `process environment is unreadable on ${process.platform}; only Linux /proc is read`,
142
+ );
143
+ let text: string;
144
+ try {
145
+ text = read(`/proc/${pid}/environ`);
146
+ } catch (error) {
147
+ const code = error instanceof Error ? errnoCode(error) : undefined;
148
+ if (code === "ENOENT" || code === "ESRCH") return undefined;
149
+ throw error;
150
+ }
151
+ const environment = new Map<string, string>();
152
+ for (const entry of text.split("\0")) {
153
+ const separator = entry.indexOf("=");
154
+ if (separator > 0 && !environment.has(entry.slice(0, separator)))
155
+ environment.set(entry.slice(0, separator), entry.slice(separator + 1));
156
+ }
157
+ return environment;
158
+ };
159
+ }
160
+
161
+ export const readLinuxProcessEnvironment =
162
+ createLinuxProcessEnvironmentReader();
163
+
126
164
  /** The calling process's identity; throws when this host cannot establish it. */
127
165
  export function readOwnProcessIdentity(
128
166
  probe: ProcessIdentityProbe = linuxProcessProbe,
@@ -5,7 +5,7 @@ import type {
5
5
  SubagentActivityState,
6
6
  } from "./types.ts";
7
7
 
8
- const KNOWN_SCOPES = new Set<SubagentActivityScope>([
8
+ const KNOWN_SCOPES: ReadonlySet<string> = new Set<SubagentActivityScope>([
9
9
  "agent",
10
10
  "turn",
11
11
  "provider",