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 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.0](https://github.com/giuseppecrj/pi-herdr-agents/compare/v3.0.3...v3.1.0)
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 managed worktree child's Pi process named by immutable kernel facts (PID,
69
- start time, boot, and PID namespace), recorded by the child at launch and
70
- verified by the parent against the Herdr pane. A worktree cancel signals and
71
- judges exit only by it.
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
- - 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. 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
+ - 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, and
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, and
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`, and `examples/role-pack/`, 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`.
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
- /** Managed worktree children: where the child records its own process identity. */
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
- /** Verified at launch readiness; a worktree cancel's only signal and exit evidence. */
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 && !resolved.request.handoff
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
- return {
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
+ }