pi-herdr-agents 3.1.0 → 3.2.1
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 +3 -2
- package/CHANGELOG.md +18 -1
- package/CONTEXT.md +22 -4
- package/README.md +58 -6
- package/RELEASING.md +2 -2
- package/docs/README.md +9 -0
- package/docs/worktree-subagents.md +1 -0
- package/maestro/adapters/pi/launch.ts +26 -4
- package/maestro/adapters/pi/pi-harness-adapter.ts +4 -0
- package/maestro/core/config/sidebar-config.ts +63 -0
- package/maestro/runtime/delegated-marker.ts +206 -0
- package/maestro/runtime/pi-run-session.ts +58 -7
- package/maestro/surfaces/herdr/herdr-socket.ts +165 -0
- package/package.json +6 -6
- package/pi-extension/subagents/index.ts +6 -0
- package/plugins/sidebar/README.md +242 -0
- package/plugins/sidebar/herdr-plugin.toml +21 -0
- package/plugins/sidebar/sidebar.mjs +238 -0
- package/skills/pi-herdr-agents/SKILL.md +40 -1
package/AGENTS.md
CHANGED
|
@@ -27,12 +27,13 @@ Former bundled roles, `/plan`, its plan skill, and `/skill:orchestrate` now belo
|
|
|
27
27
|
- `pi-extension/subagents/index.ts` — Pi composition root: public tools/commands, role-pack event bridge, host policy, widgets, and parent delivery
|
|
28
28
|
- `pi-extension/subagents/model-registry.ts`, `config-path.ts` — permanent host-local SDK capability glue and configuration-path conventions
|
|
29
29
|
- `maestro/core/` — seam interfaces and types; activity/lifecycle/status projection, routing, wake-ups, and supervision
|
|
30
|
+
- `plugins/sidebar/` — optional standalone Node companion Herdr plugin; it imports nothing from the extension
|
|
30
31
|
- `maestro/core/roles/discovery.ts`, `maestro/core/config/` — role parsing/discovery, injected-directory config loaders, and task-model init prompt construction
|
|
31
32
|
- `maestro/core/worktree.ts`, `worktree-cleanup.ts` — manifest schema/state, handoff types, and cleanup eligibility/formatting
|
|
32
33
|
- `maestro/adapters/pi/` — `PiHarnessAdapter`, launch transactions, completion evidence, session I/O, activity files, SDK model glue, and task-model registry projection
|
|
33
34
|
- `maestro/adapters/pi/child/subagent-done.ts` — child protocol: `caller_ping`, `subagent_done`, and activity recording
|
|
34
|
-
- `maestro/surfaces/herdr/` — `HerdrSurfaceProvider`, Herdr CLI driver, and terminal scripts/placement
|
|
35
|
-
- `maestro/runtime/` — `RunSession`, Pi composition, run ownership, controls/retries, observation, delivery-gated cleanup, worktree operations/handoff, and task-model init composition
|
|
35
|
+
- `maestro/surfaces/herdr/` — `HerdrSurfaceProvider`, Herdr CLI driver, and terminal scripts/placement, plus `herdr-socket.ts` for socket pane metadata
|
|
36
|
+
- `maestro/runtime/` — `RunSession`, Pi composition, run ownership, controls/retries, observation, delivery-gated cleanup, worktree operations/handoff, and task-model init composition, plus `delegated-marker.ts` for delegated-child sidebar markers
|
|
36
37
|
- `maestro/adapters/fake/`, `maestro/surfaces/fake/`, `test/maestro/` — conformance fakes, seam tests, and the dependency-rule test
|
|
37
38
|
- `CONTEXT.md` — orchestration-domain glossary
|
|
38
39
|
- `docs/adr/` — hard-to-reverse architectural decisions; [ADR-0012](docs/adr/0012-adopt-maestro-seams-in-repo.md) records the maestro seams decision
|
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,29 @@ 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.1
|
|
10
|
+
## [v3.2.1](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.2.0...v3.2.1)
|
|
11
|
+
|
|
12
|
+
### Commits
|
|
13
|
+
|
|
14
|
+
- add sidebar plan [`675b4af`](https://github.com/giuseppecrj/pi-herdr-agents/commit/675b4af4eadd26c764974ba0257aef861da8a3de)
|
|
15
|
+
- docs: guide agents through sidebar plugin setup [`a65d9ae`](https://github.com/giuseppecrj/pi-herdr-agents/commit/a65d9aec41b694d5c5d72585784e83ddb1922035)
|
|
16
|
+
|
|
17
|
+
## [v3.2.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.1.0...v3.2.0) - 2026-10-09
|
|
18
|
+
|
|
19
|
+
### Commits
|
|
20
|
+
|
|
21
|
+
- feat: add optional Herdr sidebar focus plugin [`9d2fb03`](https://github.com/giuseppecrj/pi-herdr-agents/commit/9d2fb039b006e0d389d19b1105dd54e277f695ba)
|
|
22
|
+
- fix: update dependencies to resolve security advisories [`028ebd6`](https://github.com/giuseppecrj/pi-herdr-agents/commit/028ebd62fa8c8e967c15a9677c0d4a1abc52505a)
|
|
23
|
+
- docs: match sidebar README to main package style [`918db68`](https://github.com/giuseppecrj/pi-herdr-agents/commit/918db68bd5f56782198182aa48eb6586a46a0ef7)
|
|
24
|
+
- chore: release v3.2.0 [`a663716`](https://github.com/giuseppecrj/pi-herdr-agents/commit/a663716d679a3cddc7bc9b7727918409c590b921)
|
|
25
|
+
|
|
26
|
+
## [v3.1.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.3...v3.1.0) - 2026-10-09
|
|
11
27
|
|
|
12
28
|
### Commits
|
|
13
29
|
|
|
14
30
|
- feat: route task-model init through pack approval [`1b6bacd`](https://github.com/giuseppecrj/pi-herdr-agents/commit/1b6bacd0b5eecb6f83ffba1d5a5ee57a8b0fa944)
|
|
15
31
|
- test: dispatch all Pi lifecycle handlers in placement fixtures [`f96e59d`](https://github.com/giuseppecrj/pi-herdr-agents/commit/f96e59de8890d39ce6c79be8de245d287fc2485e)
|
|
32
|
+
- chore: release v3.1.0 [`0bb8bcd`](https://github.com/giuseppecrj/pi-herdr-agents/commit/0bb8bcdc328d152c5cab5eaaf4d261f109055ee4)
|
|
16
33
|
|
|
17
34
|
## [v3.0.3](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.2...v3.0.3) - 2026-10-08
|
|
18
35
|
|
package/CONTEXT.md
CHANGED
|
@@ -65,12 +65,30 @@ taken first stays authoritative.
|
|
|
65
65
|
_Avoid_: Interrupt, suppression, persistent stop, pane-close fallback
|
|
66
66
|
|
|
67
67
|
**Process identity**:
|
|
68
|
-
A
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
A child's Pi process named by immutable kernel facts (PID, start time, boot,
|
|
69
|
+
and PID namespace), recorded by the child at launch and verified by the parent
|
|
70
|
+
against the Herdr pane. Every managed worktree child records it; other children
|
|
71
|
+
record it when sidebar markers are on. A worktree cancel signals and judges
|
|
72
|
+
exit only by it, and a delegated marker renews only while it is alive.
|
|
72
73
|
_Avoid_: argv match, foreground process, shell visibility
|
|
73
74
|
|
|
75
|
+
**Delegated marker**:
|
|
76
|
+
The display-only pane token `piha_delegated_v1=live` that a parent reports on
|
|
77
|
+
a delegated child's pane when `sidebar.enabled` is on. It means the parent
|
|
78
|
+
classified the pane's process as its child within the last 15 seconds, not
|
|
79
|
+
that the process is alive when Herdr draws the row. One stable source, a
|
|
80
|
+
15-second TTL, and increasing sequence numbers bound a leftover marker;
|
|
81
|
+
renewal requires the original process identity. A `/worktree` handoff is
|
|
82
|
+
never marked.
|
|
83
|
+
_Avoid_: liveness proof, ownership record, security boundary, per-run source
|
|
84
|
+
|
|
85
|
+
**Sidebar Focus**:
|
|
86
|
+
The Herdr agent view that the optional `pi-herdr-agents.sidebar` plugin sets
|
|
87
|
+
on request. It hides rows whose delegated marker is `live` while their status
|
|
88
|
+
is idle, working, or done, and keeps blocked, unknown, and unmarked rows. All
|
|
89
|
+
clears only this plugin's view and reports a view that another tool owns.
|
|
90
|
+
_Avoid_: hiding processes, notification filter, automatic view, popup
|
|
91
|
+
|
|
74
92
|
**Pi startup confirmation**:
|
|
75
93
|
The bounded check that a `/worktree` handoff's Pi is running before its
|
|
76
94
|
workspace is focused: a Pi process in the root pane with the worktree cwd whose
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Delegate investigation, implementation, and review without blocking the parent s
|
|
|
17
17
|
- **Conversation handoff** — continue the active Pi conversation in a new worktree with `/worktree` while preserving the parent session.
|
|
18
18
|
- **Pack-neutral roles** — use project or global definitions and installable role packs; this package ships no default roles or workflows.
|
|
19
19
|
- **Persistent specialists** — retain one policy-bound Pi session for sequential, turn-based tasks.
|
|
20
|
+
- **Optional sidebar focus** — the companion [Pi Herdr Agents Sidebar](plugins/sidebar/README.md) Herdr plugin hides marked delegated children from Herdr's Agents view while you run Focus. Markers are display-only and follow the pane, so see its [limits](plugins/sidebar/README.md#reusing-a-delegated-pane).
|
|
20
21
|
|
|
21
22
|
## Requirements
|
|
22
23
|
|
|
@@ -57,7 +58,7 @@ Restart or `/reload` Pi after installation. Review package source before install
|
|
|
57
58
|
- [Requirements](#requirements), [Install](#install), [Safety and uninstall](#safety-and-uninstall)
|
|
58
59
|
- [Quick start](#quick-start), [Release notes](#release-notes), [How it works](#how-it-works)
|
|
59
60
|
- [What's Included](#whats-included): tools, commands, the operating skill, role packs
|
|
60
|
-
- [Async Subagent Flow](#async-subagent-flow): status, configuration, model routing
|
|
61
|
+
- [Async Subagent Flow](#async-subagent-flow): status, configuration, model routing, [sidebar markers](#sidebar-markers)
|
|
61
62
|
- [Spawning Subagents](#spawning-subagents), [Persistent specialists](#persistent-specialists), [Interrupting](#interrupting-a-running-subagent), [Cancelling](#cancelling-a-running-subagent)
|
|
62
63
|
- [The `/worktree` Workflow](#the-worktree-workflow), [Custom Agents](#custom-agents), [Tool Access Control](#tool-access-control)
|
|
63
64
|
- [Development](#development), [License](#license)
|
|
@@ -67,7 +68,8 @@ Restart or `/reload` Pi after installation. Review package source before install
|
|
|
67
68
|
- Child agents are real Pi processes running with your user account's permissions, inside Herdr panes. Worktrees isolate Git checkouts, not processes or permissions.
|
|
68
69
|
- The extension creates Herdr panes, tabs and managed worktrees only when a launch asks for them. It never pushes, merges, opens pull requests, deletes branches or removes worktrees on its own; cleanup is an explicit parent action ([worktree cleanup](#explicit-worktree-cleanup)).
|
|
69
70
|
- What it writes: `$PI_CODING_AGENT_DIR/herdr-agents/config.json` (only through `/subagents-init` or the writer tool, never on startup); per-launch artifacts under the parent session's `artifacts/<session-id>/` directory beside Pi's session store, which hold the child's full task text and any `systemPrompt` as Markdown files, activity snapshots and worktree manifests; and the child's own Pi session file. A managed worktree that carries its own `.pi/agent` directory receives that child's session inside the checkout. Treat task text as potentially sensitive when you share or inspect those files.
|
|
70
|
-
-
|
|
71
|
+
- Managed worktree children record their PID, process start time, boot ID, and PID namespace in `<session>.process.json` beside their session. With [`sidebar.enabled`](#sidebar-markers), every other child records the same facts, and a resumed child records them under `artifacts/<session-id>/process-identity/`. The parent then reports one display-only pane token, `piha_delegated_v1`, on each child's pane through the Herdr socket. With the setting off, the extension writes no pane metadata.
|
|
72
|
+
- To uninstall: first list and remove any retained worktrees while the extension is still loaded (`/worktree list`, then `/worktree remove <target>`), because those commands leave with the package. If you installed the optional sidebar plugin, remove it with `herdr plugin uninstall pi-herdr-agents.sidebar` (or `herdr plugin unlink` for a linked copy). Then run `pi remove npm:pi-herdr-agents`, and delete `$PI_CODING_AGENT_DIR/herdr-agents/` and the `artifacts/` directories above if you no longer want the configuration and launch records.
|
|
71
73
|
|
|
72
74
|
## Quick start
|
|
73
75
|
|
|
@@ -649,6 +651,52 @@ shows a `not-started` reason. The host does not listen in subagent sessions
|
|
|
649
651
|
and unsubscribes at `session_shutdown`. Unsubscribe an approval listener there
|
|
650
652
|
too, as the role-pack bridge does.
|
|
651
653
|
|
|
654
|
+
### Sidebar markers
|
|
655
|
+
|
|
656
|
+
Set `sidebar.enabled` to `true` to mark delegated children for the optional
|
|
657
|
+
[Pi Herdr Agents Sidebar](plugins/sidebar/README.md) Herdr plugin, whose Focus
|
|
658
|
+
action hides marked children from Herdr's Agents view. The setting defaults to
|
|
659
|
+
`false`.
|
|
660
|
+
|
|
661
|
+
```json
|
|
662
|
+
{
|
|
663
|
+
"sidebar": {
|
|
664
|
+
"enabled": true
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Add the key to your existing `config.json`. The extension rejects a
|
|
670
|
+
`config.json` without its `status` section, so copy `config.json.example`
|
|
671
|
+
first if the file does not exist. Run `/reload` after changing the setting.
|
|
672
|
+
Children launched before the change stay unmarked.
|
|
673
|
+
|
|
674
|
+
Setting `sidebar.enabled` to `false` and running `/reload` retires every
|
|
675
|
+
existing marker and clears the markers of running children. A clear can fail to
|
|
676
|
+
reach Herdr, so a token still expires at most 15 seconds after the last write
|
|
677
|
+
Herdr accepted. Setting it back to `true` marks only later launches; a child
|
|
678
|
+
that was already running when you turned it off is never marked again.
|
|
679
|
+
|
|
680
|
+
With markers on, every fresh, resumed, and persistent child records its own
|
|
681
|
+
process identity, as managed worktree children always do. After the parent
|
|
682
|
+
verifies that identity, it reports the pane token `piha_delegated_v1` with the
|
|
683
|
+
value `live`, a 15-second time-to-live, and an increasing sequence number
|
|
684
|
+
through `HERDR_SOCKET_PATH`. It renews the token on supervision checks while
|
|
685
|
+
that exact process is alive. It clears the token when the run finalizes or is
|
|
686
|
+
suppressed, or when the process exits or can no longer be verified. A
|
|
687
|
+
`/worktree` handoff session is never marked.
|
|
688
|
+
|
|
689
|
+
Marker writes never fail a launch or keep Pi running. Each write has a 2-second
|
|
690
|
+
timeout on an unreferenced socket, failed writes are not retried, and three
|
|
691
|
+
failures in a row stop that child's marker. Process identity reads Linux
|
|
692
|
+
`/proc`, so on other systems, and outside Herdr, nothing is marked. After a
|
|
693
|
+
parent crash, a marker can remain for up to 15 seconds after the last write
|
|
694
|
+
Herdr applied. A marker belongs to the pane, so a different process started by
|
|
695
|
+
hand in a delegated pane stays hidden by Focus while the original process is
|
|
696
|
+
alive and supervised, even if it is suspended. Run All first or use a fresh
|
|
697
|
+
pane. See [what a marker proves](plugins/sidebar/README.md#what-a-marker-proves)
|
|
698
|
+
and [reusing a delegated pane](plugins/sidebar/README.md#reusing-a-delegated-pane).
|
|
699
|
+
|
|
652
700
|
### Supervision transport
|
|
653
701
|
|
|
654
702
|
On supported local filesystems, supervision uses file wake-ups plus one shared
|
|
@@ -1347,11 +1395,15 @@ provider; fakes are for conformance tests only.
|
|
|
1347
1395
|
- `maestro/adapters/pi/` — Pi launch, completion, session I/O, activity files,
|
|
1348
1396
|
model SDK glue, and registry projection behind `PiHarnessAdapter`;
|
|
1349
1397
|
`child/subagent-done.ts` implements the child protocol.
|
|
1350
|
-
- `maestro/surfaces/herdr/` — `HerdrSurfaceProvider`, Herdr CLI driver,
|
|
1351
|
-
terminal scripts/placement.
|
|
1398
|
+
- `maestro/surfaces/herdr/` — `HerdrSurfaceProvider`, Herdr CLI driver,
|
|
1399
|
+
terminal scripts/placement, and `herdr-socket.ts`, the bounded socket
|
|
1400
|
+
client for pane metadata.
|
|
1352
1401
|
- `maestro/runtime/` — run ownership, controls/retries, observation,
|
|
1353
|
-
delivery-gated cleanup, Pi composition, worktree operations/handoff,
|
|
1354
|
-
task-model init composition.
|
|
1402
|
+
delivery-gated cleanup, Pi composition, worktree operations/handoff,
|
|
1403
|
+
task-model init composition, and sidebar markers (`delegated-marker.ts`).
|
|
1404
|
+
- `plugins/sidebar/` — the optional Herdr plugin. Its `sidebar.mjs` uses Node
|
|
1405
|
+
built-ins only and imports nothing from the extension, so Herdr can install
|
|
1406
|
+
the directory by itself.
|
|
1355
1407
|
- `maestro/adapters/fake/`, `maestro/surfaces/fake/`, `test/maestro/` —
|
|
1356
1408
|
conformance fakes, seam tests, and the import dependency-rule test.
|
|
1357
1409
|
|
package/RELEASING.md
CHANGED
|
@@ -37,7 +37,7 @@ Manual deterministic Herdr integration (required before you push a release commi
|
|
|
37
37
|
npm run test:integration
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Run that suite from inside Herdr. It uses real Pi and Herdr processes with the local deterministic provider, so it needs no provider credentials or network access.
|
|
40
|
+
Run that suite from inside Herdr. It uses real Pi and Herdr processes with the local deterministic provider, so it needs no provider credentials or network access. `test/integration/sidebar.test.ts` also starts two disposable Herdr servers with their own HOME, configuration, sockets, and plugin registry under the system temporary directory, and removes them afterwards. It needs Linux and skips elsewhere.
|
|
41
41
|
|
|
42
42
|
The optional live-provider smoke test is not a release gate:
|
|
43
43
|
|
|
@@ -45,7 +45,7 @@ The optional live-provider smoke test is not a release gate:
|
|
|
45
45
|
PI_TEST_MODEL="openai-codex/gpt-5.6-luna" PI_TEST_TIMEOUT=180000 npm run test:integration:live
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Do not release from skipped Herdr tests. Confirm the package preview includes `README.md`, `CHANGELOG.md`, `AGENTS.md`, `docs/`, `config.json.example`,
|
|
48
|
+
Do not release from skipped Herdr tests. Confirm the package preview includes `README.md`, `CHANGELOG.md`, `AGENTS.md`, `docs/`, `config.json.example`, `examples/role-pack/`, and exactly `README.md`, `herdr-plugin.toml`, and `sidebar.mjs` under `plugins/sidebar/`, excludes the active `docs/sidebar-plan.md`, contains no `agents/` resources, no `skills/` resources other than the host-owned `skills/pi-herdr-agents/SKILL.md`, and no `pi-extension/subagents/plan-skill.md`, and excludes `pi-extension/subagents/workflow-worker.js`. Confirm it excludes plans, journals, sessions, prototypes, generated evidence, local `config.json` and `openspec/`, and that the worktree integration tests leave no test workspace behind. Durable user configuration is `$PI_CODING_AGENT_DIR/herdr-agents/config.json`; package-root configuration is ignored, so users must move an older file manually or re-run `/subagents-init`.
|
|
49
49
|
|
|
50
50
|
## npm authentication
|
|
51
51
|
|
package/docs/README.md
CHANGED
|
@@ -11,6 +11,9 @@ are evidence, not shipped contracts, when a later ADR supersedes them.
|
|
|
11
11
|
- [`../CONTEXT.md`](../CONTEXT.md) — orchestration glossary.
|
|
12
12
|
- [`worktree-subagents.md`](worktree-subagents.md) — worktree operation,
|
|
13
13
|
review, recovery, and cleanup.
|
|
14
|
+
- [`../plugins/sidebar/README.md`](../plugins/sidebar/README.md) — the
|
|
15
|
+
optional Herdr sidebar plugin: installation, Focus and All, the delegated
|
|
16
|
+
marker contract, and its staleness limits.
|
|
14
17
|
|
|
15
18
|
The package is a pack-neutral execution host: it launches asynchronous Pi
|
|
16
19
|
children in Herdr and supports managed worktrees for writing tasks, but ships no
|
|
@@ -66,6 +69,12 @@ real implementations; in-memory fakes are conformance fixtures only.
|
|
|
66
69
|
| [`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
70
|
| [`0015`](adr/0015-report-opened-primary-workspace.md) | Accepted | Report, and never automatically close, a primary workspace that worktree creation opened. |
|
|
68
71
|
|
|
72
|
+
## Active design
|
|
73
|
+
|
|
74
|
+
- `sidebar-plan.md` — local plan and audit record for the sidebar plugin and
|
|
75
|
+
its marker bridge. It is excluded from the npm package, so it has no link
|
|
76
|
+
here. It is not a shipped contract; the plugin README is.
|
|
77
|
+
|
|
69
78
|
## Historical material
|
|
70
79
|
|
|
71
80
|
- [`orchestrated-review-workflow-plan.md`](orchestrated-review-workflow-plan.md)
|
|
@@ -194,6 +194,7 @@ The extension never pushes, creates a PR, merges, cherry-picks, or changes the p
|
|
|
194
194
|
- **Ordinary launch failure:** if a fresh launch or `subagent_resume` fails after the extension creates its pane, the extension closes that pane and preserves the original launch error. It does not close a caller-supplied surface.
|
|
195
195
|
- **Creation failure:** the manifest is marked failed. If Herdr created the branch but returned an incomplete response, the extension reconciles a unique branch match through `/worktree list` and records any recovered workspace/path.
|
|
196
196
|
- **Launch failure after creation:** the manifest is marked failed and the workspace, forked session, and path are retained. The destination is not focused unless Pi startup is confirmed.
|
|
197
|
+
- **Sidebar marker:** with `sidebar.enabled`, the worktree child's root pane carries the delegated marker only while the child's own Pi process runs. The parent clears it when the run finalizes, so the retained shell, and any agent you later start in it, is not marked. After a parent crash the marker expires within 15 seconds of the last write Herdr applied. See the [sidebar plugin README](../plugins/sidebar/README.md#what-a-marker-proves).
|
|
197
198
|
- **Worker failure:** summary and available Git state are returned; the workspace remains open. Auto-exit waits until Pi is fully settled, so a transient provider error followed by automatic compaction or retry does not end the worker early.
|
|
198
199
|
- **Operator cancel:** `subagent_cancel` never closes the retained root pane or workspace. At launch, the child's protocol extension records its own process identity (PID, `/proc` start time, boot ID, and PID-namespace link) in `<session>.process.json`, and the parent accepts it only while that process is alive in the parent's PID namespace as the Herdr pane shell or a descendant of it. Cancel sends SIGTERM only to that identity, re-verified (PID and start time) immediately before the signal, then confirms that the identity no longer exists, or that the pane is gone while it is not known alive. Command-line text and Herdr's foreground list are never evidence: Pi rewrites its process title, and Herdr lists foreground processes only. A live identity (such as a suspended Pi with SIGTERM pending), or one whose SIGTERM fails while it is still alive, leaves termination `unconfirmed` even if the pane is gone. An identity that was not captured (including on non-Linux hosts), is unreadable, belongs to another boot or PID namespace, or whose PID now has another start time is never signalled and also leaves it `unconfirmed` unless pane absence confirms termination. The PID could still be reused between the final check and the signal, because Node has no pidfd signal. Every Herdr query is bounded by the cancel deadline, and an answer that arrives after the deadline, even before the timer runs, never changes the reported outcome. The run stays live, and retrying `subagent_cancel` re-checks; if the launch capture expired, the retry captures the identity again within its own deadline. On confirmation, the manifest is marked `cancelled` and the normal Git handoff is delivered once. A parent shutdown while termination is unconfirmed records the plain shutdown state, never `cancelled`. No Git cleanup, branch change, or removal happens.
|
|
199
200
|
- **`caller_ping`:** the child exits with `needs_help`; continue worktree-bound follow-up in the retained workspace rather than through `subagent_resume`. Public `subagent_resume` rejects managed-worktree child sessions before creating a pane so it cannot silently lose worktree ownership or policy.
|
|
@@ -54,6 +54,8 @@ export interface FreshPiLaunchRequest {
|
|
|
54
54
|
fork?: boolean;
|
|
55
55
|
handoff?: { leafId: string };
|
|
56
56
|
surface?: string;
|
|
57
|
+
/** Ordinary children record their process identity too (sidebar markers). */
|
|
58
|
+
recordProcessIdentity?: boolean;
|
|
57
59
|
parent: {
|
|
58
60
|
cwd: string;
|
|
59
61
|
invocationCwd?: string;
|
|
@@ -86,6 +88,8 @@ export interface ResumePiLaunchRequest {
|
|
|
86
88
|
name: string;
|
|
87
89
|
sessionFile: string;
|
|
88
90
|
message?: string;
|
|
91
|
+
/** Record the resumed process's identity (sidebar markers). */
|
|
92
|
+
recordProcessIdentity?: boolean;
|
|
89
93
|
parent: {
|
|
90
94
|
sessionId: string;
|
|
91
95
|
sessionDir: string;
|
|
@@ -125,9 +129,15 @@ export interface PiRunningChild {
|
|
|
125
129
|
stopState?: "requested" | "pending" | "failed";
|
|
126
130
|
stopFailure?: string;
|
|
127
131
|
crashNotified?: boolean;
|
|
128
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Where the child records its own process identity: every managed worktree
|
|
134
|
+
* child, and other children when the launch asks for it.
|
|
135
|
+
*/
|
|
129
136
|
processIdentityFile?: string;
|
|
130
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Verified at launch readiness. A worktree cancel's only signal and exit
|
|
139
|
+
* evidence; sidebar markers renew only while it is alive.
|
|
140
|
+
*/
|
|
131
141
|
processIdentity?: PiProcessIdentity;
|
|
132
142
|
/** Settles when launch-readiness capture verifies or gives up; never rejects. */
|
|
133
143
|
processIdentityCapture?: Promise<PiProcessIdentity | undefined>;
|
|
@@ -718,7 +728,8 @@ function prepareChildSession(
|
|
|
718
728
|
activityFile,
|
|
719
729
|
// A handoff Pi loads no child protocol extension, so it records nothing.
|
|
720
730
|
processIdentityFile:
|
|
721
|
-
surface.worktree
|
|
731
|
+
(surface.worktree || resolved.request.recordProcessIdentity) &&
|
|
732
|
+
!resolved.request.handoff
|
|
722
733
|
? getSubagentProcessIdentityFile(sessionFile)
|
|
723
734
|
: undefined,
|
|
724
735
|
};
|
|
@@ -1034,6 +1045,11 @@ async function launchResumedPiSubagent(
|
|
|
1034
1045
|
await operations.waitForShellReady(surface);
|
|
1035
1046
|
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
1036
1047
|
mkdirSync(dirname(activityFile), { recursive: true });
|
|
1048
|
+
// The session file is reused, and its sidecar keeps the first identity
|
|
1049
|
+
// ever published for it, so each resumed run records under its own ID.
|
|
1050
|
+
const processIdentityFile = request.recordProcessIdentity
|
|
1051
|
+
? join(artifactDir, "process-identity", `${id}.json`)
|
|
1052
|
+
: undefined;
|
|
1037
1053
|
|
|
1038
1054
|
let messageFile: string | undefined;
|
|
1039
1055
|
if (request.message) {
|
|
@@ -1058,6 +1074,9 @@ async function launchResumedPiSubagent(
|
|
|
1058
1074
|
`PI_SUBAGENT_ID=${shellQuote(id)}`,
|
|
1059
1075
|
`PI_SUBAGENT_ACTIVITY_FILE=${shellQuote(activityFile)}`,
|
|
1060
1076
|
`PI_SUBAGENT_AUTO_EXIT=${autoExit ? "1" : "0"}`,
|
|
1077
|
+
...(processIdentityFile
|
|
1078
|
+
? [`PI_SUBAGENT_PROCESS_FILE=${shellQuote(processIdentityFile)}`]
|
|
1079
|
+
: []),
|
|
1061
1080
|
];
|
|
1062
1081
|
const toolAllowlist = buildSubagentToolAllowlist(
|
|
1063
1082
|
policy.tools?.join(","),
|
|
@@ -1093,7 +1112,7 @@ async function launchResumedPiSubagent(
|
|
|
1093
1112
|
].join("\n"),
|
|
1094
1113
|
},
|
|
1095
1114
|
);
|
|
1096
|
-
|
|
1115
|
+
const running: PiRunningChild = {
|
|
1097
1116
|
id,
|
|
1098
1117
|
name: request.name,
|
|
1099
1118
|
task: request.message ?? "resumed session",
|
|
@@ -1105,7 +1124,10 @@ async function launchResumedPiSubagent(
|
|
|
1105
1124
|
interactive,
|
|
1106
1125
|
runtimePlan: undefined,
|
|
1107
1126
|
lifecycle: createLifecycle(startTime),
|
|
1127
|
+
processIdentityFile,
|
|
1108
1128
|
};
|
|
1129
|
+
startProcessIdentityCapture(running, operations);
|
|
1130
|
+
return running;
|
|
1109
1131
|
} catch (error) {
|
|
1110
1132
|
try {
|
|
1111
1133
|
await operations.closePane(surface);
|
|
@@ -97,6 +97,8 @@ export interface PiHarnessAdapterOptions {
|
|
|
97
97
|
worktreeOperations?: WorktreeOperations;
|
|
98
98
|
/** Kernel process facts for a retained worktree child's identity; tests inject it. */
|
|
99
99
|
processProbe?: ProcessIdentityProbe;
|
|
100
|
+
/** Ordinary and resumed children also record their identity (sidebar markers). */
|
|
101
|
+
recordProcessIdentity?: boolean;
|
|
100
102
|
/** Bound for confirming a retained worktree child's exit. Default: 5000ms. */
|
|
101
103
|
killTimeoutMs?: number;
|
|
102
104
|
/** Explicit local evidence/lifecycle bridge. The host still owns delivery and deduplication. */
|
|
@@ -209,6 +211,7 @@ export class PiHarnessAdapter implements HarnessAdapter {
|
|
|
209
211
|
: undefined
|
|
210
212
|
: undefined,
|
|
211
213
|
surface: opts.surface?.id,
|
|
214
|
+
recordProcessIdentity: this.options.recordProcessIdentity,
|
|
212
215
|
parent: this.options.parent,
|
|
213
216
|
runtimePlan:
|
|
214
217
|
resolved?.runtimePlan ??
|
|
@@ -294,6 +297,7 @@ export class PiHarnessAdapter implements HarnessAdapter {
|
|
|
294
297
|
name: opts.name,
|
|
295
298
|
sessionFile: opts.sessionId,
|
|
296
299
|
message: opts.message,
|
|
300
|
+
recordProcessIdentity: this.options.recordProcessIdentity,
|
|
297
301
|
parent: this.options.parent,
|
|
298
302
|
behavior: { autoExit: opts.autoExit },
|
|
299
303
|
},
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isBoolean, isRecord } from "./type-guards.ts";
|
|
4
|
+
|
|
5
|
+
export interface SidebarConfig {
|
|
6
|
+
/** Mark delegated children's panes for the Herdr sidebar plugin. */
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function invalid(source: string, message: string): never {
|
|
11
|
+
throw new Error(`Invalid subagent sidebar config in ${source}: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseSidebarConfig(
|
|
15
|
+
rawConfig: any,
|
|
16
|
+
source = "config.json",
|
|
17
|
+
): SidebarConfig {
|
|
18
|
+
if (!isRecord(rawConfig)) invalid(source, "root must be an object");
|
|
19
|
+
if (!Object.hasOwn(rawConfig, "sidebar")) return { enabled: false };
|
|
20
|
+
const sidebar = rawConfig.sidebar;
|
|
21
|
+
if (!isRecord(sidebar)) invalid(source, "sidebar must be an object");
|
|
22
|
+
const unsupported = Object.keys(sidebar).filter((key) => key !== "enabled");
|
|
23
|
+
if (unsupported.length > 0)
|
|
24
|
+
invalid(
|
|
25
|
+
source,
|
|
26
|
+
`sidebar has unsupported key(s): ${unsupported.join(", ")}`,
|
|
27
|
+
);
|
|
28
|
+
const enabled = Object.hasOwn(sidebar, "enabled") ? sidebar.enabled : false;
|
|
29
|
+
if (!isBoolean(enabled)) invalid(source, "sidebar.enabled must be a boolean");
|
|
30
|
+
return { enabled };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadSidebarConfig(
|
|
34
|
+
configDir: string,
|
|
35
|
+
examplePath: string,
|
|
36
|
+
): SidebarConfig {
|
|
37
|
+
let sourcePath = join(configDir, "config.json");
|
|
38
|
+
let rawConfig: string;
|
|
39
|
+
try {
|
|
40
|
+
rawConfig = readFileSync(sourcePath, "utf8");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// SAFETY: readFileSync only throws Node fs errors here, which carry code.
|
|
43
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
44
|
+
sourcePath = examplePath;
|
|
45
|
+
try {
|
|
46
|
+
rawConfig = readFileSync(examplePath, "utf8");
|
|
47
|
+
} catch (exampleError) {
|
|
48
|
+
// SAFETY: readFileSync only throws Node fs errors here, which carry code.
|
|
49
|
+
if ((exampleError as NodeJS.ErrnoException).code === "ENOENT")
|
|
50
|
+
return { enabled: false };
|
|
51
|
+
throw exampleError;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return parseSidebarConfig(JSON.parse(rawConfig), sourcePath);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof SyntaxError)
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid JSON in subagent config ${sourcePath}: ${error.message}`,
|
|
60
|
+
);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
judgeProcessIdentity,
|
|
3
|
+
linuxProcessProbe,
|
|
4
|
+
type PiProcessIdentity,
|
|
5
|
+
type ProcessIdentityProbe,
|
|
6
|
+
type ProcessIdentityState,
|
|
7
|
+
} from "../adapters/pi/process-identity.ts";
|
|
8
|
+
import {
|
|
9
|
+
reportPaneToken,
|
|
10
|
+
type PaneTokenOutcome,
|
|
11
|
+
type PaneTokenWrite,
|
|
12
|
+
} from "../surfaces/herdr/herdr-socket.ts";
|
|
13
|
+
|
|
14
|
+
/** The pane token the Pi Herdr Agents Sidebar plugin reads. */
|
|
15
|
+
export const DELEGATED_MARKER_TOKEN = "piha_delegated_v1";
|
|
16
|
+
/** Recently classified as a delegated child; never proof of a live occupant. */
|
|
17
|
+
export const DELEGATED_MARKER_VALUE = "live";
|
|
18
|
+
/** One stable source: a pane accepts sequenced reports from at most 32. */
|
|
19
|
+
export const DELEGATED_MARKER_SOURCE = "pi-herdr-agents";
|
|
20
|
+
/** Bounds a leftover marker after the last write Herdr applied, not liveness. */
|
|
21
|
+
export const DELEGATED_MARKER_TTL_MS = 15_000;
|
|
22
|
+
/**
|
|
23
|
+
* Renewal rides on supervision observations: the 4.8s reconciliation, or
|
|
24
|
+
* 1s fallback polling. Both leave several renewals inside one TTL.
|
|
25
|
+
*/
|
|
26
|
+
export const DELEGATED_MARKER_RENEW_MS = 4_000;
|
|
27
|
+
/** Consecutive failed writes before a marker stops trying; its TTL ends it. */
|
|
28
|
+
export const DELEGATED_MARKER_MAX_FAILURES = 3;
|
|
29
|
+
|
|
30
|
+
export type MarkerWriter = (write: {
|
|
31
|
+
paneId: string;
|
|
32
|
+
seq: number;
|
|
33
|
+
live: boolean;
|
|
34
|
+
}) => Promise<PaneTokenOutcome>;
|
|
35
|
+
|
|
36
|
+
export function herdrMarkerWriter(socketPath: string): MarkerWriter {
|
|
37
|
+
return ({ paneId, seq, live }) => {
|
|
38
|
+
const write: PaneTokenWrite = {
|
|
39
|
+
paneId,
|
|
40
|
+
source: DELEGATED_MARKER_SOURCE,
|
|
41
|
+
seq,
|
|
42
|
+
token: DELEGATED_MARKER_TOKEN,
|
|
43
|
+
value: null,
|
|
44
|
+
};
|
|
45
|
+
if (live) {
|
|
46
|
+
write.value = DELEGATED_MARKER_VALUE;
|
|
47
|
+
write.ttlMs = DELEGATED_MARKER_TTL_MS;
|
|
48
|
+
}
|
|
49
|
+
return reportPaneToken(socketPath, write);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type MarkerState =
|
|
54
|
+
| { kind: "pending" }
|
|
55
|
+
| { kind: "live"; identity: PiProcessIdentity; lastSentAt: number }
|
|
56
|
+
| { kind: "retired" };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This parent process's marker writes. Every pane shares one sequence that
|
|
60
|
+
* starts at wall-clock microseconds, so a restarted parent still outranks
|
|
61
|
+
* its predecessor's writes unless the clock moved backwards.
|
|
62
|
+
*/
|
|
63
|
+
export class DelegatedMarkers {
|
|
64
|
+
readonly write: MarkerWriter;
|
|
65
|
+
readonly now: () => number;
|
|
66
|
+
private readonly probe: ProcessIdentityProbe;
|
|
67
|
+
private lastSeq = 0;
|
|
68
|
+
|
|
69
|
+
constructor(options: {
|
|
70
|
+
write: MarkerWriter;
|
|
71
|
+
probe?: ProcessIdentityProbe;
|
|
72
|
+
now?: () => number;
|
|
73
|
+
}) {
|
|
74
|
+
this.write = options.write;
|
|
75
|
+
this.probe = options.probe ?? linuxProcessProbe;
|
|
76
|
+
this.now = options.now ?? Date.now;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A marker for one launched child. It publishes only after the child's
|
|
81
|
+
* own process identity is verified; without one it never writes.
|
|
82
|
+
*/
|
|
83
|
+
attach(child: {
|
|
84
|
+
surface: string;
|
|
85
|
+
processIdentityCapture?: Promise<PiProcessIdentity | undefined>;
|
|
86
|
+
}): DelegatedMarker {
|
|
87
|
+
return new DelegatedMarker(
|
|
88
|
+
this,
|
|
89
|
+
child.surface,
|
|
90
|
+
child.processIdentityCapture,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
judge(identity: PiProcessIdentity): ProcessIdentityState {
|
|
95
|
+
return judgeProcessIdentity(identity, this.probe);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
nextSeq(): number {
|
|
99
|
+
this.lastSeq = Math.max(this.lastSeq + 1, this.now() * 1000);
|
|
100
|
+
return this.lastSeq;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One child's marker. Writes are serialized: at most one renewal is queued or
|
|
106
|
+
* in flight, followed by at most one clear. Renewal continues only while the
|
|
107
|
+
* launch-verified process identity is alive, so a later process in the same
|
|
108
|
+
* pane never inherits it.
|
|
109
|
+
*/
|
|
110
|
+
export class DelegatedMarker {
|
|
111
|
+
readonly paneId: string;
|
|
112
|
+
private readonly owner: DelegatedMarkers;
|
|
113
|
+
private state: MarkerState = { kind: "pending" };
|
|
114
|
+
private sentLive = false;
|
|
115
|
+
private liveQueued = false;
|
|
116
|
+
private failures = 0;
|
|
117
|
+
private queue: Promise<void> = Promise.resolve();
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
owner: DelegatedMarkers,
|
|
121
|
+
paneId: string,
|
|
122
|
+
capture: Promise<PiProcessIdentity | undefined> | undefined,
|
|
123
|
+
) {
|
|
124
|
+
this.owner = owner;
|
|
125
|
+
this.paneId = paneId;
|
|
126
|
+
if (!capture) {
|
|
127
|
+
this.state = { kind: "retired" };
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
void capture.then(
|
|
131
|
+
(identity) => this.start(identity),
|
|
132
|
+
() => this.start(undefined),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get kind(): MarkerState["kind"] {
|
|
137
|
+
return this.state.kind;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Resolves when every write queued so far has settled. */
|
|
141
|
+
settled(): Promise<void> {
|
|
142
|
+
return this.queue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Called on each supervision observation; renews at most every 4s. */
|
|
146
|
+
renew(): void {
|
|
147
|
+
const state = this.state;
|
|
148
|
+
const at = this.owner.now();
|
|
149
|
+
if (
|
|
150
|
+
state.kind !== "live" ||
|
|
151
|
+
this.liveQueued ||
|
|
152
|
+
at - state.lastSentAt < DELEGATED_MARKER_RENEW_MS
|
|
153
|
+
)
|
|
154
|
+
return;
|
|
155
|
+
if (this.owner.judge(state.identity).kind !== "alive") {
|
|
156
|
+
this.retire();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
state.lastSentAt = at;
|
|
160
|
+
this.queueWrite(true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Idempotent. Clears the token if any live write may have reached Herdr. */
|
|
164
|
+
retire(): void {
|
|
165
|
+
if (this.state.kind === "retired") return;
|
|
166
|
+
this.state = { kind: "retired" };
|
|
167
|
+
if (this.sentLive) this.queueWrite(false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private start(identity: PiProcessIdentity | undefined): void {
|
|
171
|
+
if (this.state.kind !== "pending") return;
|
|
172
|
+
if (!identity || this.owner.judge(identity).kind !== "alive") {
|
|
173
|
+
this.state = { kind: "retired" };
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.state = { kind: "live", identity, lastSentAt: this.owner.now() };
|
|
177
|
+
this.queueWrite(true);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private queueWrite(live: boolean): void {
|
|
181
|
+
if (live) this.liveQueued = true;
|
|
182
|
+
this.queue = this.queue.then(() => this.send(live));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async send(live: boolean): Promise<void> {
|
|
186
|
+
if (live) this.liveQueued = false;
|
|
187
|
+
// A renewal queued before retirement is dropped, never sent after it.
|
|
188
|
+
if (live && this.state.kind !== "live") return;
|
|
189
|
+
if (live) this.sentLive = true;
|
|
190
|
+
let outcome: PaneTokenOutcome;
|
|
191
|
+
try {
|
|
192
|
+
outcome = await this.owner.write({
|
|
193
|
+
paneId: this.paneId,
|
|
194
|
+
seq: this.owner.nextSeq(),
|
|
195
|
+
live,
|
|
196
|
+
});
|
|
197
|
+
} catch {
|
|
198
|
+
outcome = "failed";
|
|
199
|
+
}
|
|
200
|
+
// A late result never revives or reclassifies a retired marker.
|
|
201
|
+
if (!live || this.state.kind !== "live") return;
|
|
202
|
+
if (outcome === "answered") this.failures = 0;
|
|
203
|
+
else if (outcome === "pane-missing") this.state = { kind: "retired" };
|
|
204
|
+
else if (++this.failures >= DELEGATED_MARKER_MAX_FAILURES) this.retire();
|
|
205
|
+
}
|
|
206
|
+
}
|