pi-herdr-agents 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +20 -1
- package/CONTEXT.md +19 -0
- package/README.md +44 -8
- package/config.json.example +5 -3
- package/docs/worktree-subagents.md +5 -3
- package/package.json +1 -1
- package/pi-extension/subagents/herdr.ts +208 -39
- package/pi-extension/subagents/index.ts +178 -17
- package/pi-extension/subagents/launch.ts +13 -4
- package/pi-extension/subagents/pane-config.ts +43 -10
- package/pi-extension/subagents/session.ts +106 -0
- package/pi-extension/subagents/supervision-config.ts +27 -8
- package/pi-extension/subagents/terminal.ts +16 -3
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ These instructions apply to humans and coding agents changing `pi-herdr-agents`.
|
|
|
4
4
|
|
|
5
5
|
## What this package is
|
|
6
6
|
|
|
7
|
-
`pi-herdr-agents` (Pi Herdr Agents) is a Pi extension that launches asynchronous Pi child agents exclusively in Herdr. Ordinary runs
|
|
7
|
+
`pi-herdr-agents` (Pi Herdr Agents) is a Pi extension that launches asynchronous Pi child agents exclusively in Herdr. Ordinary runs group child panes in extension-owned `Agents` tabs by default. Writing tasks may opt into one isolated Herdr-managed Git worktree per branch. Legacy role definitions that request an external CLI fail before Herdr creates resources.
|
|
8
8
|
|
|
9
9
|
The extension is fire-and-forget: `subagent` returns an acknowledgement, and completion is delivered to the parent automatically. Never add polling guidance that tells callers to sleep, tail sessions, or repeatedly check status.
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ Bundled role prompts live in [`agents/`](agents/). The native `/skill:orchestrat
|
|
|
22
22
|
## Code map
|
|
23
23
|
|
|
24
24
|
- `pi-extension/subagents/index.ts` — public tools/commands, agent discovery, launch/watch lifecycle, completion delivery, worktree manifests and handoffs
|
|
25
|
-
- `pi-extension/subagents/herdr.ts` — Herdr CLI
|
|
25
|
+
- `pi-extension/subagents/herdr.ts` — Herdr CLI calls, response parsing, and ID-based Agents tab placement and capacity
|
|
26
26
|
- `pi-extension/subagents/terminal.ts` — terminal adapter used by the lifecycle
|
|
27
27
|
- `pi-extension/subagents/lifecycle.ts`, `status.ts`, `activity.ts` — process/turn state and widget projection
|
|
28
28
|
- `pi-extension/subagents/wake.ts`, `supervision.ts`, `supervision-config.ts` — file wake-ups, shared pane reconciliation, polling fallback, and supervision configuration
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,24 @@ 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
|
-
## [v1.
|
|
10
|
+
## [v1.7.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.6.0...v1.7.0)
|
|
11
|
+
|
|
12
|
+
### Merged
|
|
13
|
+
|
|
14
|
+
- feat(subagents): checkout-aware Agents tabs and safe cleanup (#33) [`#43`](https://github.com/giuseppecrj/pi-herdr-agents/pull/43)
|
|
15
|
+
- feat: no-progress advisories for active subagents (#40) [`#41`](https://github.com/giuseppecrj/pi-herdr-agents/pull/41)
|
|
16
|
+
- fix(subagents): measure recovered hang duration and track outstanding tool calls [`#40`](https://github.com/giuseppecrj/pi-herdr-agents/pull/40)
|
|
17
|
+
|
|
18
|
+
### Commits
|
|
19
|
+
|
|
20
|
+
- feat(subagents): group agents by checkout with safe pane cleanup (#33) [`59a1075`](https://github.com/giuseppecrj/pi-herdr-agents/commit/59a10756722e7cce3b2ded07db6e1adb04abb0ab)
|
|
21
|
+
- fix(subagents): preserve delivery cleanup and live caller identity (#33) [`26987c5`](https://github.com/giuseppecrj/pi-herdr-agents/commit/26987c58a2c882f31a0d935d7e97ce9dd8c4cce8)
|
|
22
|
+
- feat(subagents): no-progress hang advisories for active children (#40) [`1ebf356`](https://github.com/giuseppecrj/pi-herdr-agents/commit/1ebf356521dd46dd6ee035fe3ebba0262992f399)
|
|
23
|
+
- fix(subagents): keep no-progress recovery guidance evidence-based (#40) [`9cbf155`](https://github.com/giuseppecrj/pi-herdr-agents/commit/9cbf155e291c063f70ef86800ef3bfe3c742c6c6)
|
|
24
|
+
- test(integration): hang advisory coverage (#40) [`075f4fe`](https://github.com/giuseppecrj/pi-herdr-agents/commit/075f4feb17938df28a92fa00851d606e5a83b258)
|
|
25
|
+
- fix(subagents): preserve boundary-aligned records in bounded session tails (#40) [`f096fdb`](https://github.com/giuseppecrj/pi-herdr-agents/commit/f096fdb681921e9ab460610d53e456758802725f)
|
|
26
|
+
|
|
27
|
+
## [v1.6.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.5.1...v1.6.0) - 2026-09-08
|
|
11
28
|
|
|
12
29
|
### Merged
|
|
13
30
|
|
|
@@ -34,11 +51,13 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
34
51
|
- fix(subagents): scope pane snapshots to registered owners and confirm absence (#29) [`c73a132`](https://github.com/giuseppecrj/pi-herdr-agents/commit/c73a13211f743dc881c506226f457c6a5045e41b)
|
|
35
52
|
- feat(subagents): persistent specialist docs, glossary, and integration coverage (#32) [`e0f052d`](https://github.com/giuseppecrj/pi-herdr-agents/commit/e0f052da7b717a9c4c21ae1d83172acb1a27452c)
|
|
36
53
|
- fix(subagents): guard terminal evidence against racing semantic records (#29) [`f2e41f7`](https://github.com/giuseppecrj/pi-herdr-agents/commit/f2e41f74f185c13a89b49301e226e04948d4c088)
|
|
54
|
+
- chore: release v1.6.0 [`8a18189`](https://github.com/giuseppecrj/pi-herdr-agents/commit/8a181890479f46d5955d4f1bac3be759a2d39010)
|
|
37
55
|
- fix(subagents): type-safe validation for persistent sidecar, ledger, and config parsing [`fe81be5`](https://github.com/giuseppecrj/pi-herdr-agents/commit/fe81be5ca76eba504ff59fcf46ba3e9499753a70)
|
|
38
56
|
- fix(orchestrate): parse review reports out of wrapped public delivery [`9f11918`](https://github.com/giuseppecrj/pi-herdr-agents/commit/9f11918a407be39388999feeee16970e9d888105)
|
|
39
57
|
- fix(subagents): close stopped persistent specialist panes (#32) [`8d0b6e7`](https://github.com/giuseppecrj/pi-herdr-agents/commit/8d0b6e7e9703fd7416e8ddc0ffb97dc26d48ab6b)
|
|
40
58
|
- docs(context): define child wake-up signal and result delivery terms [`a3c159e`](https://github.com/giuseppecrj/pi-herdr-agents/commit/a3c159e2056d13be5697494a13bcb84bdcf92bab)
|
|
41
59
|
- docs(subagents): clarify persistent help requests (#32) [`16e9363`](https://github.com/giuseppecrj/pi-herdr-agents/commit/16e9363180cf49b5e03003f5ea1e57b38ac8adbd)
|
|
60
|
+
- docs(release): require workflow-worker.js absence in the pack preview (#31) [`9d40cc0`](https://github.com/giuseppecrj/pi-herdr-agents/commit/9d40cc0f3aa08ce203a720947bf097a4b66c9357)
|
|
42
61
|
|
|
43
62
|
## [v1.5.1](https://github.com/giuseppecrj/pi-herdr-agents/compare/v1.5.0...v1.5.1) - 2026-09-05
|
|
44
63
|
|
package/CONTEXT.md
CHANGED
|
@@ -18,6 +18,12 @@ The parent-facing handoff of a child run's observed outcome and available
|
|
|
18
18
|
evidence. Receiving it does not establish that the work is correct or accepted.
|
|
19
19
|
_Avoid_: Wake-up signal, acceptance
|
|
20
20
|
|
|
21
|
+
**No-progress advisory**:
|
|
22
|
+
An internal warning that an active child shows no durable progress in its session
|
|
23
|
+
JSONL or activity snapshot. It is advisory only and never changes the child's
|
|
24
|
+
outcome or triggers recovery.
|
|
25
|
+
_Avoid_: Hang verdict, automatic recovery, stall replacement
|
|
26
|
+
|
|
21
27
|
**Legacy external CLI role**:
|
|
22
28
|
An old role definition that contains `cli`. Discovery reports a migration
|
|
23
29
|
diagnostic, and launch fails before Herdr creates a pane or worktree. Remove
|
|
@@ -76,6 +82,19 @@ The append-only evidence record of persistent task dispatch and terminal
|
|
|
76
82
|
outcomes for one session generation.
|
|
77
83
|
_Avoid_: Work queue, mutable task list
|
|
78
84
|
|
|
85
|
+
**Agents tab**:
|
|
86
|
+
An extension-owned Herdr tab grouping delegated child panes in an existing
|
|
87
|
+
checkout workspace. Ownership comes from returned IDs, not its display label.
|
|
88
|
+
The pane cap includes every live pane; overflow creates another tab, not a
|
|
89
|
+
workspace. Separate parent processes own separate groups.
|
|
90
|
+
_Avoid_: Agent workspace, label-based ownership, automatic rearrangement
|
|
91
|
+
|
|
92
|
+
**Retained checkout shell**:
|
|
93
|
+
The interactive shell in a managed worktree's root pane, preserved after the
|
|
94
|
+
child Pi process exits. Temporary review panes can close without deleting this
|
|
95
|
+
surface or its checkout.
|
|
96
|
+
_Avoid_: Completed agent process, disposable pane, automatic worktree cleanup
|
|
97
|
+
|
|
79
98
|
**Worktree lease**:
|
|
80
99
|
The lifetime-exclusive binding between a persistent specialist generation and
|
|
81
100
|
one managed worktree, when that specialist writes in a worktree.
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Use ordinary panes for read-only agents. A single or sequential writer can work
|
|
|
83
83
|
|
|
84
84
|

|
|
85
85
|
|
|
86
|
-
A `subagent` call
|
|
86
|
+
A `subagent` call selects the target checkout, reuses its Herdr workspace, and gives the child a pane in an extension-owned `Agents` tab. Four panes fit in each tab by default; overflow opens another tab in the same workspace. A worktree is created only when explicitly requested for checkout isolation. The call launches a child Pi session and returns `started`. The parent watcher combines Herdr process state with child activity details and projects the result into a live widget:
|
|
87
87
|
|
|
88
88
|
```text
|
|
89
89
|
╭─ Subagents ──────────────────── 1 active · 1 open ─╮
|
|
@@ -92,7 +92,7 @@ A `subagent` call creates a dedicated Herdr pane or worktree, launches a child P
|
|
|
92
92
|
╰────────────────────────────────────────────────────╯
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
When the child completes, the parent receives one bounded `subagent_result` message and starts a new turn with that result in context. Callers never need to poll, tail session files, or wait in a shell loop.
|
|
95
|
+
When the child completes, the parent receives one bounded `subagent_result` message and starts a new turn with that result in context. Disposable ordinary panes close after result delivery; Herdr removes a tab when its last pane closes. Persistent specialists keep their pane between tasks, and managed worktree roots return to retained interactive shells. Callers never need to poll, tail session files, or wait in a shell loop.
|
|
96
96
|
|
|
97
97
|
## Troubleshooting completion delivery
|
|
98
98
|
|
|
@@ -301,11 +301,13 @@ cp config.json.example config.json
|
|
|
301
301
|
"maxAgents": 3
|
|
302
302
|
},
|
|
303
303
|
"supervision": {
|
|
304
|
-
"forcePolling": false
|
|
304
|
+
"forcePolling": false,
|
|
305
|
+
"hangWarningMinutes": 15
|
|
305
306
|
},
|
|
306
307
|
"panes": {
|
|
307
|
-
"mode": "
|
|
308
|
-
"direction": "right"
|
|
308
|
+
"mode": "grouped",
|
|
309
|
+
"direction": "right",
|
|
310
|
+
"maxPerTab": 4
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
```
|
|
@@ -347,6 +349,34 @@ disable wake-ups and use that legacy cadence deliberately. The setting is read
|
|
|
347
349
|
when the coordinator is created, so run `/reload` after changing it.
|
|
348
350
|
`subagents_list` reports the active transport mode (`wake+batch`,
|
|
349
351
|
`polling(forced)`, or `polling(fallback)`) and watcher count.
|
|
352
|
+
|
|
353
|
+
`supervision.hangWarningMinutes` defaults to `15`; set it to `0` to disable
|
|
354
|
+
no-progress advisories. For example, this keeps the default transport and sets
|
|
355
|
+
a 30-minute advisory budget:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"supervision": {
|
|
360
|
+
"forcePolling": false,
|
|
361
|
+
"hangWarningMinutes": 30
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
While a child projects active or blocked, the parent compares durable session
|
|
367
|
+
JSONL and activity-snapshot updates against this budget. An advisory is warning-only, fires once per no-progress episode, and
|
|
368
|
+
never interrupts, kills, retries, or restarts a child. It identifies `blocked-tool` (an outstanding tool call may still complete),
|
|
369
|
+
`truncated-turn` (an observed `toolUse` stop with no tool call; its cause is unknown), or
|
|
370
|
+
`generic-no-progress` when neither condition is established, then
|
|
371
|
+
includes the session path and manual recovery options. Ordinary children can be
|
|
372
|
+
interrupted or, after manual termination, resumed or newly spawned. Persistent
|
|
373
|
+
ordinary-pane specialists can be interrupted or stopped with `subagent_stop` and
|
|
374
|
+
replaced; they cannot be resumed. Managed-worktree children, including persistent
|
|
375
|
+
ones, retain their workspace and continue there only after the previous process
|
|
376
|
+
has exited; do not use `subagent_resume` or start a concurrent writer. Interactive children stay
|
|
377
|
+
quiet just as they do for stalled/recovered notices; their widget state still
|
|
378
|
+
updates. A later durable update clears the episode and sends the corresponding
|
|
379
|
+
recovered notice for non-interactive children.
|
|
350
380
|
`polling(fallback)` means at least one tracked child is using per-child polling;
|
|
351
381
|
other children can still use wake+batch.
|
|
352
382
|
|
|
@@ -359,7 +389,13 @@ was 4.82 s. The benchmark measures `/proc` CPU ticks for the supervisor and
|
|
|
359
389
|
isolated Herdr tree, not parent-model latency; raw samples are written to
|
|
360
390
|
`/tmp/issue29-bench/` by `test/bench/supervision-bench.mjs`.
|
|
361
391
|
|
|
362
|
-
|
|
392
|
+
`panes.mode` defaults to `"grouped"` when omitted. Ordinary public `subagent` and `subagent_resume` launches, including bare forks and `/iterate`, fill extension-owned `Agents`, `Agents 2`, etc. tabs in the target checkout's existing workspace. `panes.maxPerTab` is a positive safe integer, defaults to `4`, and counts all live panes in each owned tab, including user-added panes and retained shells. Overlapping launches in one parent respect this cap. It is independent of `persistent.maxAgents`.
|
|
393
|
+
|
|
394
|
+
Checkout matching uses Herdr's canonical `worktree.checkout_path` and includes descendant directories. Shell working directories do not establish workspace ownership. If no checkout matches (including non-Git directories), placement uses the caller's workspace; overflow never creates a workspace. A reviewer with `cwd` set to a managed checkout joins that workspace without creating another worktree. Resume placement uses the saved session's cwd.
|
|
395
|
+
|
|
396
|
+
Explicit `panes.mode: "tab"` preserves one new tab per ordinary child in the caller's workspace. Explicit `"split"` preserves splits of the stable parent pane. `panes.direction` is `"right"` (default) or `"down"` and applies to grouped and legacy splits. `maxPerTab` does not affect these legacy modes. Managed worktrees retain their separate workspaces, while `/btw` keeps its existing tab behavior.
|
|
397
|
+
|
|
398
|
+
Ownership is tracked by returned pane/tab/workspace IDs, never labels. Separate parent processes own separate groups; `/reload` preserves a parent's in-memory ownership, but a full restart does not adopt old tabs. Placement never moves existing panes or renames user tabs. Background launches preserve focus; Herdr may resize sibling panes when splitting or closing. User-added panes are never closed by automatic tab cleanup. An owned tab remains reusable while user panes remain, even after all child panes close.
|
|
363
399
|
|
|
364
400
|
Run `/reload` after changing role, model, or pane settings.
|
|
365
401
|
|
|
@@ -457,7 +493,7 @@ A launch with `worktree` and an effective bundled `scout`, `reviewer`, or `adver
|
|
|
457
493
|
|
|
458
494
|
The child starts at the returned worktree root. Tell writing agents to test and commit when you want a commit-based handoff, and tell them not to push, merge, switch branches, or remove the worktree. The parent owns review and integration.
|
|
459
495
|
|
|
460
|
-
Successful, failed, and help-requesting runs retain their workspace. Completion includes the worktree path, Herdr workspace, branch, base/head SHAs, commits ahead, changed and untracked files, and clean/dirty/conflicted state. Here, `clean` means no uncommitted files; the branch may still contain commits. If Git inspection fails, state is reported as unknown rather than guessed.
|
|
496
|
+
Successful, failed, and help-requesting worktree runs retain their workspace and root shell. A reviewer's disposable pane can close without closing that root, tab, or checkout. Completion includes the worktree path, Herdr workspace, branch, base/head SHAs, commits ahead, changed and untracked files, and clean/dirty/conflicted state. Here, `clean` means no uncommitted files; the branch may still contain commits. If Git inspection fails, state is reported as unknown rather than guessed.
|
|
461
497
|
|
|
462
498
|
An ownership manifest is written under the parent session's `artifacts/<session-id>/worktree-runs/` directory before Herdr creates resources. V1 does not automatically recover watchers after a full process restart, and `subagent_resume` does not reattach the managed worktree lifecycle.
|
|
463
499
|
|
|
@@ -568,7 +604,7 @@ Phase 5: Integrate → Parent reviews and integrates worktree branches on
|
|
|
568
604
|
Phase 6: Review → Reviewer subagent checks the integrated changes
|
|
569
605
|
```
|
|
570
606
|
|
|
571
|
-
The parent workspace and tab names stay unchanged. Subagents
|
|
607
|
+
The parent workspace and tab names stay unchanged. Subagents use the configured placement policy; grouped mode reuses available space in owned Agents tabs.
|
|
572
608
|
|
|
573
609
|
---
|
|
574
610
|
|
package/config.json.example
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
"maxAgents": 3
|
|
13
13
|
},
|
|
14
14
|
"supervision": {
|
|
15
|
-
"forcePolling": false
|
|
15
|
+
"forcePolling": false,
|
|
16
|
+
"hangWarningMinutes": 15
|
|
16
17
|
},
|
|
17
18
|
"panes": {
|
|
18
|
-
"mode": "
|
|
19
|
-
"direction": "right"
|
|
19
|
+
"mode": "grouped",
|
|
20
|
+
"direction": "right",
|
|
21
|
+
"maxPerTab": 4
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -46,7 +46,7 @@ For a worktree launch:
|
|
|
46
46
|
- `worktree.branch` is a new, unique branch name. Git/Herdr rejects a branch that cannot be created or is already checked out elsewhere.
|
|
47
47
|
- `worktree.base` may be any revision that resolves to a commit in the source repository. It defaults to committed `HEAD`.
|
|
48
48
|
- 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 root pane.
|
|
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.
|
|
50
50
|
- 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
51
|
- Worktree creation does not steal terminal focus.
|
|
52
52
|
|
|
@@ -139,7 +139,7 @@ For parallel read-only review, prepare one stable existing checkout of the pull
|
|
|
139
139
|
|
|
140
140
|
1. The parent records the canonical repository root, exact comparison base and head SHAs, and exact task/spec evidence. It makes sure no writer changes the checkout while review runs.
|
|
141
141
|
2. Decide explicitly whether staged, unstaged, and untracked files are in scope. For included dirty state, record a bounded inventory and fingerprint; a commit SHA alone cannot pin it.
|
|
142
|
-
3. Start each read-only child in an ordinary pane with `cwd` set to that checkout. Omit `worktree`.
|
|
142
|
+
3. Start each read-only child in an ordinary pane with `cwd` set to that checkout. Omit `worktree`. With default grouped placement, the extension reuses that checkout's workspace and available space in this parent's owned Agents tabs, including a retained writer's root tab. It never moves the writer or creates another worktree. Separate parent processes do not adopt one another's tabs by label.
|
|
143
143
|
4. Give every reviewer the same exact scope. Require it to report the repository root and `git rev-parse HEAD` before its review result.
|
|
144
144
|
5. Before each dependent review wave and before reporting, recheck the head and dirty-state fingerprint. Drift makes prior evidence stale; review the new state again instead of mixing revisions.
|
|
145
145
|
|
|
@@ -205,7 +205,9 @@ This manual continuation is not watched by the original parent lifecycle. Do not
|
|
|
205
205
|
|
|
206
206
|
## Cleanup
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
Worktree and branch cleanup is always explicit. Ordinary temporary reviewer panes close after result delivery; Herdr removes their tab only if its last pane closes. The retained worktree root shell is excluded from automatic cleanup, and user-added panes are preserved. Persistent specialists retain their pane between tasks and follow the existing explicit stop semantics.
|
|
209
|
+
|
|
210
|
+
First make sure commits, patches, or uncommitted files are no longer needed. Then remove the Herdr worktree workspace:
|
|
209
211
|
|
|
210
212
|
```bash
|
|
211
213
|
herdr worktree remove --workspace <workspace-id>
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFile, execSync, execFileSync } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
3
5
|
import { isFiniteNumber, isPlainObject, isString } from "./type-guards.ts";
|
|
4
6
|
|
|
5
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -132,33 +134,25 @@ interface HerdrCurrentPaneInfo {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
function getHerdrCurrentPaneInfo(): HerdrCurrentPaneInfo {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
!isString(pane?.workspace_id)
|
|
149
|
-
) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
pane_id: pane.pane_id,
|
|
156
|
-
tab_id: pane.tab_id,
|
|
157
|
-
workspace_id: pane.workspace_id,
|
|
158
|
-
};
|
|
137
|
+
// Inherited IDs go stale after a pane moves. Herdr resolves the calling
|
|
138
|
+
// terminal's original identity to its live pane, tab, and workspace.
|
|
139
|
+
const output = herdrExec(buildCurrentPaneArgs());
|
|
140
|
+
const parsed = parseHerdrJson(output);
|
|
141
|
+
const pane = parsed?.result?.pane;
|
|
142
|
+
if (
|
|
143
|
+
!isString(pane?.pane_id) ||
|
|
144
|
+
!isString(pane?.tab_id) ||
|
|
145
|
+
!isString(pane?.workspace_id)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`,
|
|
149
|
+
);
|
|
159
150
|
}
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
return {
|
|
152
|
+
pane_id: pane.pane_id,
|
|
153
|
+
tab_id: pane.tab_id,
|
|
154
|
+
workspace_id: pane.workspace_id,
|
|
155
|
+
};
|
|
162
156
|
}
|
|
163
157
|
|
|
164
158
|
function buildTabCreateArgs(
|
|
@@ -217,14 +211,11 @@ function buildWorktreeCreateArgs(
|
|
|
217
211
|
];
|
|
218
212
|
}
|
|
219
213
|
|
|
220
|
-
export function createHerdrSurface(name: string): string {
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// workspace explicitly because Herdr's implicit default may be another space.
|
|
214
|
+
export function createHerdrSurface(name: string, cwd = process.cwd()): string {
|
|
215
|
+
// Legacy tab mode and BTW target the caller workspace explicitly; Herdr's
|
|
216
|
+
// implicit default may be another workspace.
|
|
224
217
|
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
225
|
-
const output = herdrExec(
|
|
226
|
-
buildTabCreateArgs(name, process.cwd(), workspaceId),
|
|
227
|
-
);
|
|
218
|
+
const output = herdrExec(buildTabCreateArgs(name, cwd, workspaceId));
|
|
228
219
|
const paneId = extractHerdrRootPaneId(output, "tab create");
|
|
229
220
|
try {
|
|
230
221
|
herdrExec(["pane", "rename", paneId, name]);
|
|
@@ -234,6 +225,147 @@ export function createHerdrSurface(name: string): string {
|
|
|
234
225
|
return paneId;
|
|
235
226
|
}
|
|
236
227
|
|
|
228
|
+
interface OwnedAgentsTab {
|
|
229
|
+
workspaceId: string;
|
|
230
|
+
panes: Set<string>;
|
|
231
|
+
retainedPaneId?: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// In-memory ownership survives /reload, not process restart. Separate parent
|
|
235
|
+
// processes never adopt tabs by label or share a capacity reservation.
|
|
236
|
+
const agentsTabsKey = Symbol.for("pi-herdr-subagents:agents-tabs");
|
|
237
|
+
// SAFETY: this extension alone writes this process-local symbol.
|
|
238
|
+
const placementGlobal = globalThis as typeof globalThis & {
|
|
239
|
+
[agentsTabsKey]?: Map<string, OwnedAgentsTab>;
|
|
240
|
+
};
|
|
241
|
+
const agentsTabs = (placementGlobal[agentsTabsKey] ??= new Map<
|
|
242
|
+
string,
|
|
243
|
+
OwnedAgentsTab
|
|
244
|
+
>());
|
|
245
|
+
|
|
246
|
+
function canonicalPath(path: string): string {
|
|
247
|
+
try {
|
|
248
|
+
return realpathSync(path);
|
|
249
|
+
} catch {
|
|
250
|
+
return resolve(path);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function containsCwd(root: string, cwd: string): boolean {
|
|
255
|
+
const child = relative(root, cwd);
|
|
256
|
+
return (
|
|
257
|
+
child === "" ||
|
|
258
|
+
(child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function placementPanes(): Array<{
|
|
263
|
+
pane_id: string;
|
|
264
|
+
tab_id: string;
|
|
265
|
+
workspace_id: string;
|
|
266
|
+
}> {
|
|
267
|
+
const parsed = parseHerdrJson(herdrExec(["pane", "list"]));
|
|
268
|
+
const panes = parsed?.result?.panes;
|
|
269
|
+
if (
|
|
270
|
+
parsed?.result?.type !== "pane_list" ||
|
|
271
|
+
!Array.isArray(panes) ||
|
|
272
|
+
panes.some(
|
|
273
|
+
(pane) =>
|
|
274
|
+
!isString(pane?.pane_id) ||
|
|
275
|
+
!isString(pane?.tab_id) ||
|
|
276
|
+
!isString(pane?.workspace_id),
|
|
277
|
+
)
|
|
278
|
+
) {
|
|
279
|
+
throw new Error("Unexpected herdr pane list output for placement");
|
|
280
|
+
}
|
|
281
|
+
return panes;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createHerdrGroupedSurface(
|
|
285
|
+
name: string,
|
|
286
|
+
cwd: string,
|
|
287
|
+
maxPerTab: number,
|
|
288
|
+
direction: "right" | "down",
|
|
289
|
+
): string {
|
|
290
|
+
const callerWorkspace = getHerdrCurrentPaneInfo().workspace_id;
|
|
291
|
+
const parsed = parseHerdrJson(herdrExec(["workspace", "list"]));
|
|
292
|
+
const workspaces = parsed?.result?.workspaces;
|
|
293
|
+
if (parsed?.result?.type !== "workspace_list" || !Array.isArray(workspaces)) {
|
|
294
|
+
throw new Error("Unexpected herdr workspace list output for placement");
|
|
295
|
+
}
|
|
296
|
+
const panes = placementPanes();
|
|
297
|
+
const target = canonicalPath(cwd);
|
|
298
|
+
let workspaceId = callerWorkspace;
|
|
299
|
+
let matchLength = -1;
|
|
300
|
+
for (const workspace of workspaces) {
|
|
301
|
+
if (!isString(workspace?.workspace_id))
|
|
302
|
+
throw new Error("Unexpected herdr workspace identity");
|
|
303
|
+
// Herdr exposes checkout ownership, but no stable non-Git workspace root.
|
|
304
|
+
// A shell's incidental cwd is not a workspace association.
|
|
305
|
+
const checkout = workspace.worktree?.checkout_path;
|
|
306
|
+
if (!isString(checkout)) continue;
|
|
307
|
+
const canonical = canonicalPath(checkout);
|
|
308
|
+
if (
|
|
309
|
+
containsCwd(canonical, target) &&
|
|
310
|
+
(canonical.length > matchLength ||
|
|
311
|
+
(canonical.length === matchLength &&
|
|
312
|
+
workspace.workspace_id === callerWorkspace))
|
|
313
|
+
) {
|
|
314
|
+
workspaceId = workspace.workspace_id;
|
|
315
|
+
matchLength = canonical.length;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The entire inspect/create/record window is synchronous, before launch's
|
|
320
|
+
// first await. Overlapping launches in this parent cannot overbook a tab.
|
|
321
|
+
let paneId: string | undefined;
|
|
322
|
+
for (const [tabId, owned] of agentsTabs) {
|
|
323
|
+
if (owned.workspaceId !== workspaceId) continue;
|
|
324
|
+
const live = panes.filter(
|
|
325
|
+
(pane) => pane.tab_id === tabId && pane.workspace_id === workspaceId,
|
|
326
|
+
);
|
|
327
|
+
if (live.length === 0) {
|
|
328
|
+
agentsTabs.delete(tabId);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (live.length >= maxPerTab) continue;
|
|
332
|
+
// The tab ID remains ours even when only user-added panes survive.
|
|
333
|
+
const anchor =
|
|
334
|
+
live.find((pane) => owned.panes.has(pane.pane_id)) ?? live[0];
|
|
335
|
+
paneId = extractHerdrPaneId(
|
|
336
|
+
herdrExec(buildPaneSplitArgs(anchor.pane_id, direction, cwd)),
|
|
337
|
+
"pane split",
|
|
338
|
+
);
|
|
339
|
+
owned.panes.add(paneId);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
if (!paneId) {
|
|
343
|
+
const count = [...agentsTabs.values()].filter(
|
|
344
|
+
(tab) => tab.workspaceId === workspaceId,
|
|
345
|
+
).length;
|
|
346
|
+
const label = count === 0 ? "Agents" : `Agents ${count + 1}`;
|
|
347
|
+
const output = herdrExec(buildTabCreateArgs(label, cwd, workspaceId));
|
|
348
|
+
paneId = extractHerdrRootPaneId(output, "tab create");
|
|
349
|
+
const tabId = parseHerdrJson(output)?.result?.tab?.tab_id;
|
|
350
|
+
if (!isString(tabId) || !tabId) {
|
|
351
|
+
// Only the explicitly returned pane is ours to roll back.
|
|
352
|
+
try {
|
|
353
|
+
herdrExec(["pane", "close", paneId]);
|
|
354
|
+
} catch {
|
|
355
|
+
/* preserve parse error */
|
|
356
|
+
}
|
|
357
|
+
throw new Error("Unexpected herdr tab create identity");
|
|
358
|
+
}
|
|
359
|
+
agentsTabs.set(tabId, { workspaceId, panes: new Set([paneId]) });
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
363
|
+
} catch {
|
|
364
|
+
/* cosmetic */
|
|
365
|
+
}
|
|
366
|
+
return paneId;
|
|
367
|
+
}
|
|
368
|
+
|
|
237
369
|
/** Worktree records returned by `herdr worktree list`. */
|
|
238
370
|
export interface HerdrWorktreeInfo {
|
|
239
371
|
branch: string;
|
|
@@ -298,7 +430,9 @@ function parseHerdrPaneList(output: string, workspaceId: string): string[] {
|
|
|
298
430
|
) {
|
|
299
431
|
throw new Error("Unexpected herdr pane list output");
|
|
300
432
|
}
|
|
301
|
-
|
|
433
|
+
const panes: Array<{ workspace_id?: unknown; pane_id?: unknown }> =
|
|
434
|
+
parsed.result.panes;
|
|
435
|
+
return panes
|
|
302
436
|
.filter((pane) => pane.workspace_id === workspaceId)
|
|
303
437
|
.map((pane) => pane.pane_id)
|
|
304
438
|
.filter(isString);
|
|
@@ -327,6 +461,34 @@ function recoverHerdrWorktree(
|
|
|
327
461
|
};
|
|
328
462
|
}
|
|
329
463
|
|
|
464
|
+
function retainWorktreeTab(
|
|
465
|
+
worktree: HerdrWorktreeSurface,
|
|
466
|
+
output: string,
|
|
467
|
+
): HerdrWorktreeSurface {
|
|
468
|
+
const returnedTabId = parseHerdrJson(output)?.result?.tab?.tab_id;
|
|
469
|
+
const tabId =
|
|
470
|
+
isString(returnedTabId) && returnedTabId
|
|
471
|
+
? returnedTabId
|
|
472
|
+
: placementPanes().find(
|
|
473
|
+
(pane) =>
|
|
474
|
+
pane.pane_id === worktree.paneId &&
|
|
475
|
+
pane.workspace_id === worktree.workspaceId,
|
|
476
|
+
)?.tab_id;
|
|
477
|
+
if (tabId) {
|
|
478
|
+
agentsTabs.set(tabId, {
|
|
479
|
+
workspaceId: worktree.workspaceId,
|
|
480
|
+
panes: new Set([worktree.paneId]),
|
|
481
|
+
retainedPaneId: worktree.paneId,
|
|
482
|
+
});
|
|
483
|
+
try {
|
|
484
|
+
herdrExec(["tab", "rename", tabId, "Agents"]);
|
|
485
|
+
} catch {
|
|
486
|
+
/* cosmetic */
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return worktree;
|
|
490
|
+
}
|
|
491
|
+
|
|
330
492
|
export function createHerdrWorktree(
|
|
331
493
|
name: string,
|
|
332
494
|
cwd: string,
|
|
@@ -335,7 +497,7 @@ export function createHerdrWorktree(
|
|
|
335
497
|
): HerdrWorktreeSurface {
|
|
336
498
|
const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
|
|
337
499
|
try {
|
|
338
|
-
return extractHerdrWorktree(output);
|
|
500
|
+
return retainWorktreeTab(extractHerdrWorktree(output), output);
|
|
339
501
|
} catch (parseError) {
|
|
340
502
|
let recovered: HerdrWorktreeSurface | HerdrWorktreeInfo | undefined;
|
|
341
503
|
try {
|
|
@@ -343,7 +505,8 @@ export function createHerdrWorktree(
|
|
|
343
505
|
} catch {
|
|
344
506
|
throw parseError;
|
|
345
507
|
}
|
|
346
|
-
if (recovered?.workspaceId && "paneId" in recovered)
|
|
508
|
+
if (recovered?.workspaceId && "paneId" in recovered)
|
|
509
|
+
return retainWorktreeTab(recovered, output);
|
|
347
510
|
if (recovered) {
|
|
348
511
|
throw new HerdrWorktreeCreateError(
|
|
349
512
|
`Herdr created branch ${branch}, but its workspace response was incomplete`,
|
|
@@ -357,11 +520,10 @@ export function createHerdrWorktree(
|
|
|
357
520
|
export function createHerdrSurfaceSplit(
|
|
358
521
|
name: string,
|
|
359
522
|
direction: "right" | "down",
|
|
523
|
+
cwd = process.cwd(),
|
|
360
524
|
): string {
|
|
361
525
|
const parentPaneId = getHerdrParentPaneId();
|
|
362
|
-
const output = herdrExec(
|
|
363
|
-
buildPaneSplitArgs(parentPaneId, direction, process.cwd()),
|
|
364
|
-
);
|
|
526
|
+
const output = herdrExec(buildPaneSplitArgs(parentPaneId, direction, cwd));
|
|
365
527
|
const paneId = extractHerdrPaneId(output, "pane split");
|
|
366
528
|
try {
|
|
367
529
|
herdrExec(["pane", "rename", paneId, name]);
|
|
@@ -759,7 +921,14 @@ export function sendHerdrEscape(surface: string): void {
|
|
|
759
921
|
}
|
|
760
922
|
|
|
761
923
|
export function closeHerdrSurface(surface: string): void {
|
|
924
|
+
for (const owned of agentsTabs.values()) {
|
|
925
|
+
if (owned.retainedPaneId === surface) return;
|
|
926
|
+
}
|
|
927
|
+
// Herdr removes a tab when its last pane closes. Never close a whole tab:
|
|
928
|
+
// a user may have added a pane since our last snapshot.
|
|
762
929
|
herdrExec(["pane", "close", surface]);
|
|
930
|
+
// Keep tab ownership until placement observes that the tab is actually gone.
|
|
931
|
+
for (const owned of agentsTabs.values()) owned.panes.delete(surface);
|
|
763
932
|
}
|
|
764
933
|
|
|
765
934
|
export function renameHerdrTab(title: string): void {
|