pi-herdr-agents 3.0.3 → 3.2.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.
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentToolResult,
3
3
  ExtensionAPI,
4
+ ExtensionCommandContext,
4
5
  ExtensionContext,
5
6
  SessionShutdownEvent,
6
7
  } from "@earendil-works/pi-coding-agent";
@@ -19,8 +20,9 @@ import { randomUUID } from "node:crypto";
19
20
  import type { RunCancellation, Task } from "../../maestro/core/types.ts";
20
21
  import {
21
22
  createDefaultRunSession,
22
- initializeTaskModels,
23
+ startTaskModelInit,
23
24
  observePiActivity,
25
+ type TaskModelInitOutcome,
24
26
  type PiRunSession,
25
27
  type PiRunSessionInfrastructure,
26
28
  type PiRunRecord,
@@ -36,6 +38,7 @@ import {
36
38
  type CancelReport,
37
39
  } from "../../maestro/runtime/index.ts";
38
40
  import { loadSupervisionConfig } from "../../maestro/core/config/supervision-config.ts";
41
+ import { loadSidebarConfig } from "../../maestro/core/config/sidebar-config.ts";
39
42
  import {
40
43
  discoverAgentCatalog as discoverCoreAgentCatalog,
41
44
  ROLE_PACK_DISCOVERY_EVENT,
@@ -60,9 +63,17 @@ import { wrapPiModelRegistry } from "./model-registry.ts";
60
63
  import {
61
64
  loadModelConfig,
62
65
  MISSING_CONFIG_REVISION,
66
+ parseRankingBasis,
67
+ readModelConfigSnapshot,
63
68
  resolveModelDefault,
64
69
  writeTaskModelConfig,
65
70
  } from "../../maestro/core/config/model-config.ts";
71
+ import {
72
+ type OfferCallback,
73
+ type TaskModelBrief,
74
+ TASK_MODEL_INIT_APPROVAL_EVENT,
75
+ TASK_MODEL_INIT_START_EVENT,
76
+ } from "../../maestro/core/config/task-model-init.ts";
66
77
  import {
67
78
  TASK_CATEGORIES,
68
79
  TASK_CATEGORY_DESCRIPTIONS,
@@ -98,6 +109,7 @@ import { isSubagentActivityScope } from "../../maestro/core/activity.ts";
98
109
  import type { SubagentActivityState } from "../../maestro/core/types.ts";
99
110
  import {
100
111
  isFiniteNumber,
112
+ isFunction,
101
113
  isPlainObject,
102
114
  isString,
103
115
  } from "../../maestro/core/config/type-guards.ts";
@@ -344,6 +356,47 @@ function discoverAgentDefinitions(
344
356
  return discoverAgentCatalog(pi).agents;
345
357
  }
346
358
 
359
+ /**
360
+ * Payload of TASK_MODEL_INIT_APPROVAL_EVENT. `context` is the live context of
361
+ * the command that started init. It passes between trusted in-process
362
+ * extensions and is current only while the request is emitted.
363
+ */
364
+ interface TaskModelApprovalRequest {
365
+ apiVersion: 1;
366
+ brief: TaskModelBrief;
367
+ context: ExtensionCommandContext;
368
+ offer: OfferCallback;
369
+ }
370
+
371
+ function hasMethods(value: any, names: readonly string[]): boolean {
372
+ return value != null && names.every((name) => isFunction(value[name]));
373
+ }
374
+
375
+ /**
376
+ * Read a TASK_MODEL_INIT_START_EVENT v1 payload. A v1 request whose context or
377
+ * preferences cannot run init still gets an offer, so its emitter reports why
378
+ * instead of concluding that no host is loaded.
379
+ */
380
+ function parseInitStartRequest(raw: any):
381
+ | {
382
+ offer: OfferCallback;
383
+ input?: { context: ExtensionCommandContext; preferences: string };
384
+ }
385
+ | undefined {
386
+ if (raw?.apiVersion !== 1 || !isFunction(raw.offer)) return undefined;
387
+ const { offer, context, preferences } = raw;
388
+ const usable =
389
+ isString(preferences) &&
390
+ hasMethods(context, ["isIdle", "hasPendingMessages"]) &&
391
+ hasMethods(context.modelRegistry, ["getAvailable"]);
392
+ return {
393
+ offer: (value) => offer.call(raw, value),
394
+ // The members init reads were checked above; the rest is the emitting
395
+ // command's own live context.
396
+ input: usable ? { context, preferences } : undefined,
397
+ };
398
+ }
399
+
347
400
  function missingRoleMessage(agentName: string): string {
348
401
  return `Agent "${agentName}" was not found. pi-herdr-agents ships no roles: define it in .pi/agents or the global agents directory, install a role pack that provides it, or omit agent for a bare launch.`;
349
402
  }
@@ -521,6 +574,10 @@ const supervisionConfig = loadSupervisionConfig(
521
574
  getSubagentsConfigDir(),
522
575
  getSubagentsConfigExamplePath(),
523
576
  );
577
+ const sidebarConfig = loadSidebarConfig(
578
+ getSubagentsConfigDir(),
579
+ getSubagentsConfigExamplePath(),
580
+ );
524
581
 
525
582
  const MAX_RESULT_PRESENTATION_CHARS = 16_000;
526
583
  const MAX_SESSION_REFERENCE_CHARS = 10_000;
@@ -2395,6 +2452,7 @@ export default function subagentsExtension(
2395
2452
  configExamplePath: getSubagentsConfigExamplePath(),
2396
2453
  roles: [],
2397
2454
  forcePolling: supervisionConfig.forcePolling,
2455
+ sidebarMarkers: sidebarConfig.enabled,
2398
2456
  infrastructure: options.infrastructure,
2399
2457
  getLaunchSnapshot() {
2400
2458
  const ctx = runtime.latestCtx;
@@ -2616,7 +2674,7 @@ export default function subagentsExtension(
2616
2674
  pi.registerTool({
2617
2675
  name: "subagents_write_task_models",
2618
2676
  label: "Write task model preferences",
2619
- description: `Validate and atomically replace models.tasks and models.tasksMeta in the durable Pi agent config, preserving unrelated settings. Supported categories: ${TASK_CATEGORIES.join(", ")}. Partial nonempty categories are accepted; omitted categories are removed. Rejects duplicate exact refs within a category. Review the active authenticated registry and existing preferences first. Optional expectedConfigRevision makes the write conditional: it fails without replacing configuration when the config file no longer matches the revision the proposal was read from. Cooperating writes are serialized and fail on contention rather than waiting. Returns normalized saved preferences, missing categories, and the configRevision of the written file; reload required.`,
2677
+ description: `Validate and atomically replace models.tasks and models.tasksMeta in the durable Pi agent config, preserving unrelated settings. Supported categories: ${TASK_CATEGORIES.join(", ")}. Partial nonempty categories are accepted; omitted categories are removed. Rejects duplicate exact refs within a category. Review the active authenticated registry and existing preferences first. Optional expectedConfigRevision makes the write conditional: it fails without replacing configuration when the config file no longer matches the revision the proposal was read from. Optional basis records what the ranking rests on; its kind must equal tasksMeta.method, and it is returned but not saved. Cooperating writes are serialized and fail on contention rather than waiting. Returns normalized saved preferences, missing categories, the configRevision of the written file, and any basis; reload required.`,
2620
2678
  parameters: Type.Object({
2621
2679
  tasks: Type.Object(
2622
2680
  Object.fromEntries(
@@ -2650,6 +2708,37 @@ export default function subagentsExtension(
2650
2708
  },
2651
2709
  ),
2652
2710
  ),
2711
+ basis: Type.Optional(
2712
+ Type.Union(
2713
+ [
2714
+ Type.Object(
2715
+ { kind: Type.Literal("registry-only") },
2716
+ { additionalProperties: false },
2717
+ ),
2718
+ Type.Object(
2719
+ {
2720
+ kind: Type.Literal("research"),
2721
+ sources: Type.Array(
2722
+ Type.Object(
2723
+ {
2724
+ url: Type.String({ minLength: 1 }),
2725
+ influence: Type.String({ minLength: 1 }),
2726
+ },
2727
+ { additionalProperties: false },
2728
+ ),
2729
+ { minItems: 1 },
2730
+ ),
2731
+ uncertainty: Type.String({ minLength: 1 }),
2732
+ },
2733
+ { additionalProperties: false },
2734
+ ),
2735
+ ],
2736
+ {
2737
+ description:
2738
+ 'What the ranking rests on, as submitted by the proposer and not independently verified: {"kind":"registry-only"}, or {"kind":"research"} with the http(s) sources consulted in this run, how each informed the ranking, and the remaining uncertainty. kind must equal tasksMeta.method. Not saved. Omit for a legacy write.',
2739
+ },
2740
+ ),
2741
+ ),
2653
2742
  }),
2654
2743
  execute: async (_id, params, _signal, _update, ctx) => {
2655
2744
  const registry = wrapPiModelRegistry(ctx.modelRegistry);
@@ -2657,6 +2746,10 @@ export default function subagentsExtension(
2657
2746
  const tasks = params.tasks as TaskPreferences;
2658
2747
  // SAFETY: TypeBox validates the tool payload; the write seam performs stricter schema validation.
2659
2748
  const tasksMeta = params.tasksMeta as TaskPreferencesMeta;
2749
+ // Present-but-invalid values (including null) fail before anything is written.
2750
+ const basis = Object.hasOwn(params, "basis")
2751
+ ? parseRankingBasis(params.basis, tasksMeta.method)
2752
+ : undefined;
2660
2753
  const saved = writeTaskModelConfig(
2661
2754
  getSubagentsConfigPath(),
2662
2755
  getSubagentsConfigExamplePath(),
@@ -2673,14 +2766,15 @@ export default function subagentsExtension(
2673
2766
  ? { expectedConfigRevision: params.expectedConfigRevision }
2674
2767
  : {},
2675
2768
  );
2769
+ const details = basis ? { ...saved, basis } : saved;
2676
2770
  return {
2677
2771
  content: [
2678
2772
  {
2679
2773
  type: "text",
2680
- text: `Wrote task model preferences. Reload required.\n${JSON.stringify(saved, null, 2)}`,
2774
+ text: `Wrote task model preferences. Reload required.\n${JSON.stringify(details, null, 2)}`,
2681
2775
  },
2682
2776
  ],
2683
- details: saved,
2777
+ details,
2684
2778
  };
2685
2779
  },
2686
2780
  });
@@ -3327,21 +3421,73 @@ export default function subagentsExtension(
3327
3421
  },
3328
3422
  });
3329
3423
 
3330
- if (!process.env.PI_SUBAGENT_ID)
3424
+ if (!process.env.PI_SUBAGENT_ID) {
3425
+ const runTaskModelInit = (
3426
+ ctx: ExtensionCommandContext,
3427
+ preferences: string,
3428
+ ): TaskModelInitOutcome => {
3429
+ if (!ctx.isIdle() || ctx.hasPendingMessages())
3430
+ return {
3431
+ kind: "not-started",
3432
+ reason:
3433
+ "a turn is in progress or messages are queued. Nothing was queued; retry when idle",
3434
+ };
3435
+ // Pi refuses a prompt without a model only after submission; refuse first.
3436
+ if (!ctx.model)
3437
+ return {
3438
+ kind: "not-started",
3439
+ reason:
3440
+ "no model is selected, so Pi cannot run the init prompt. Select a model, then retry",
3441
+ };
3442
+ return startTaskModelInit({
3443
+ preferences,
3444
+ readConfig: () => readModelConfigSnapshot(getSubagentsConfigDir()),
3445
+ projectActiveRegistry: (project) => project(ctx.modelRegistry),
3446
+ emitApprovalRequest: (brief, offer) =>
3447
+ pi.events.emit(
3448
+ TASK_MODEL_INIT_APPROVAL_EVENT,
3449
+ Object.freeze({
3450
+ apiVersion: 1,
3451
+ brief,
3452
+ context: ctx,
3453
+ offer,
3454
+ } satisfies TaskModelApprovalRequest),
3455
+ ),
3456
+ isToolActive: (name) => pi.getActiveTools().includes(name),
3457
+ submit: (prompt) => pi.sendUserMessage(prompt),
3458
+ });
3459
+ };
3331
3460
  pi.registerCommand("subagents-init", {
3332
3461
  description:
3333
3462
  "Draft task-category model preferences from the live registry; optional arguments set ranking preferences",
3334
3463
  handler: async (args, ctx) => {
3335
- const registry = ctx.modelRegistry;
3336
- const current = loadModelConfig(getSubagentsConfigDir());
3337
- const prompt = initializeTaskModels({
3338
- projectActiveRegistry: (project) => project(registry),
3339
- current,
3340
- preferences: args,
3341
- });
3342
- pi.sendUserMessage(prompt);
3464
+ const outcome = runTaskModelInit(ctx, args);
3465
+ if (outcome.kind === "not-started")
3466
+ ctx.ui.notify(
3467
+ `Task-model init not started: ${outcome.reason}. Nothing was written.`,
3468
+ "warning",
3469
+ );
3343
3470
  },
3344
3471
  });
3472
+ // Another extension's command may start the same init; it renders the outcome.
3473
+ const stopInitStart = pi.events.on(TASK_MODEL_INIT_START_EVENT, (raw) => {
3474
+ const request = parseInitStartRequest(raw);
3475
+ if (!request) return;
3476
+ const { input } = request;
3477
+ request.offer({
3478
+ owner: "pi-herdr-agents",
3479
+ start: (): TaskModelInitOutcome =>
3480
+ input
3481
+ ? runTaskModelInit(input.context, input.preferences)
3482
+ : {
3483
+ kind: "not-started",
3484
+ reason:
3485
+ "the start request lacks a usable command context or preferences text",
3486
+ },
3487
+ });
3488
+ });
3489
+ pi.on("session_shutdown", () => stopInitStart());
3490
+ }
3345
3491
 
3346
3492
  pi.registerCommand("worktree", {
3347
3493
  description: parentSession
@@ -0,0 +1,242 @@
1
+ # Pi Herdr Agents Sidebar
2
+
3
+ ![Pi Herdr Agents: a parent Pi session delegating to parallel child agents in dedicated Herdr panes, an isolated worktree and a retained session, with a live status widget.](https://raw.githubusercontent.com/giuseppecrj/pi-herdr-agents/main/docs/assets/pi-herdr-agents-gallery.png)
4
+
5
+ > **Compatibility.** The npm release `pi-herdr-agents@3.1.0` predates sidebar support. Use a local checkout that contains this plugin and the `sidebar.enabled` setting, or a later release that includes both.
6
+
7
+ An optional [Herdr](https://herdr.dev) plugin for [Pi Herdr Agents](../../README.md). It keeps your main agents easy to find in Herdr's Agents view while [Pi](https://github.com/earendil-works/pi) runs delegated children.
8
+
9
+ The plugin adds two actions. The extension in the main package marks each delegated child's pane, and Focus hides the marked rows.
10
+
11
+ ## Features
12
+
13
+ - **Focus.** Hides delegated Pi children from the Agents view, except blocked children and children whose status is unknown.
14
+ - **All.** Removes Focus if this plugin set it, then shows every agent again. If another tool replaced the view, All leaves that view in place and reports which tool owns it.
15
+ - **Display only.** Neither action closes, moves, or signals anything. Hidden children keep running, keep their panes, and still raise notifications. `herdr agent list` still lists them.
16
+ - **No automatic Focus.** Installing, linking, or enabling the plugin never turns Focus on. You invoke it.
17
+ - **Opt-in markers.** The extension writes pane markers only when you set `sidebar.enabled` to `true`.
18
+
19
+ ## Requirements
20
+
21
+ - Herdr `0.9.2-preview.2026-09-29-8e78f929d8f0`, the only build the plugin was tested on. Herdr accepts only a plain version in `min_herdr_version` and reports this preview as `0.9.2`, so the manifest declares `0.9.2`. Herdr then also accepts other 0.9.2 builds and newer ones, all untested. A build without the agent view API makes Focus and All fail with Herdr's error.
22
+ - Linux. The extension identifies child processes through `/proc`, so it marks nothing on other systems, and the manifest declares only `linux`.
23
+ - `node` on the `PATH` of the Herdr server. Herdr runs the actions as `node sidebar.mjs focus` and `node sidebar.mjs all`.
24
+ - The `pi-herdr-agents` extension, installed in Pi from a revision that supports `sidebar.enabled`, with that setting turned on. Without markers, Focus hides nothing.
25
+
26
+ ## Install
27
+
28
+ Setup has two parts. Install the Herdr plugin, then enable markers in a compatible Pi extension. The plugin alone hides nothing, and the setting alone adds no actions.
29
+
30
+ ### 1. Install the Herdr plugin
31
+
32
+ Review the plugin source, then link its directory from your local checkout:
33
+
34
+ ```bash
35
+ herdr plugin link /path/to/pi-herdr-agents/plugins/sidebar
36
+ ```
37
+
38
+ Once the GitHub default branch contains `plugins/sidebar`, you can install the subdirectory instead. Herdr shows the manifest and commands for review:
39
+
40
+ ```bash
41
+ herdr plugin install giuseppecrj/pi-herdr-agents/plugins/sidebar
42
+ ```
43
+
44
+ For another published branch, tag, or commit, add `--ref` followed by that revision. The revision must contain the plugin directory.
45
+
46
+ ### 2. Turn on markers in the Pi extension
47
+
48
+ The Pi extension must also contain sidebar support. If you do not already load a compatible copy, install the same local checkout in Pi:
49
+
50
+ ```bash
51
+ pi install /path/to/pi-herdr-agents
52
+ ```
53
+
54
+ Use `pi list` to check your existing package sources. If another copy of Pi Herdr Agents is enabled, disable that copy with `pi config` before loading the checkout. Do not load both copies. Start Pi inside Herdr.
55
+
56
+ 1. Open `$PI_CODING_AGENT_DIR/herdr-agents/config.json`. The default is `~/.pi/agent/herdr-agents/config.json`.
57
+ 2. If the file does not exist, copy `config.json.example` from the extension there first. The extension rejects a `config.json` without its `status` section.
58
+ 3. Merge the `sidebar` section into the file. Keep the existing `status`, `models`, and other sections. Do not replace the file.
59
+
60
+ ```json
61
+ {
62
+ "sidebar": {
63
+ "enabled": true
64
+ }
65
+ }
66
+ ```
67
+
68
+ 4. Run `/reload` in each parent Pi session, or start a new one.
69
+ 5. Launch new children. Children that were already running stay unmarked.
70
+ 6. Invoke Focus yourself. See [Use Focus and All](#use-focus-and-all).
71
+
72
+ ### Check the result
73
+
74
+ After you launch a new child and run Focus, a marked child with status `idle`, `working`, or `done` disappears from the Agents view while your main session stays. Blocked children remain visible. Run All to show the hidden row again. If nothing changes, read the action log:
75
+
76
+ ```bash
77
+ herdr plugin log list --plugin pi-herdr-agents.sidebar
78
+ ```
79
+
80
+ ## Contents
81
+
82
+ - [Requirements](#requirements), [Install](#install), [Use Focus and All](#use-focus-and-all)
83
+ - [Troubleshooting](#troubleshooting), [Safety](#safety), [Uninstall](#uninstall)
84
+ - [Which rows Focus hides](#which-rows-focus-hides), [What a marker proves](#what-a-marker-proves), [Reusing a delegated pane](#reusing-a-delegated-pane), [Limits](#limits)
85
+ - Main package: [Pi Herdr Agents](../../README.md) and its [sidebar markers](../../README.md#sidebar-markers) setting
86
+
87
+ ## Use Focus and All
88
+
89
+ Run an action from the command line:
90
+
91
+ ```bash
92
+ herdr plugin action invoke pi-herdr-agents.sidebar.focus
93
+ herdr plugin action invoke pi-herdr-agents.sidebar.all
94
+ ```
95
+
96
+ To bind a key, add an entry like this to your Herdr config and pick a key you do not already use:
97
+
98
+ ```toml
99
+ [[keys.command]]
100
+ key = "prefix+g"
101
+ type = "plugin_action"
102
+ command = "pi-herdr-agents.sidebar.focus"
103
+ description = "hide delegated Pi children"
104
+ ```
105
+
106
+ Herdr keeps one agent view for the whole server. Focus replaces the view that was active, including a view that another tool set. All sends its clear with this plugin as the source, so Herdr clears the view only if this plugin owns it. If another tool replaced Focus, All leaves that view in place and reports which tool owns it.
107
+
108
+ Each action writes one message to its log. Read it with `herdr plugin log list --plugin pi-herdr-agents.sidebar`. The exit status tells you what happened:
109
+
110
+ | Exit status | Meaning |
111
+ | --- | --- |
112
+ | `0` | Focus is on, or All found no view left. |
113
+ | `1` | The action failed. Herdr refused the request, sent no usable reply within 3 seconds, or still reports this plugin's view after All. The action also fails when it runs outside Herdr or as another plugin. The message names the cause. |
114
+ | `2` | The action name was not `focus` or `all`. |
115
+ | `3` | All left another tool's view in place. |
116
+
117
+ A Herdr server restart drops the view and every marker. The plugin has no startup hook, so run Focus again after a restart if you want it.
118
+
119
+ ## Troubleshooting
120
+
121
+ - **A child you expect to be hidden is still visible.** Check that `sidebar.enabled` is `true`, that you ran `/reload` in the parent, and that the child was launched after the reload. Then run Focus again. A blocked child or a child with unknown status is always shown.
122
+ - **A row you expect is missing.** Run All. Markers change only what the Agents view shows.
123
+ - **Focus or All exits with status `1`.** Read the log with the command above. The message names the cause, such as a Herdr build without the agent view API.
124
+ - **All exits with status `3`.** Another tool owns the view. All leaves it in place.
125
+ - **A new agent in a delegated pane is hidden.** See [Reusing a delegated pane](#reusing-a-delegated-pane).
126
+
127
+ ## Safety
128
+
129
+ - Markers are display only. They are not a security boundary.
130
+ - The plugin sends requests to your local Herdr server. It does not close, move, or signal panes.
131
+ - With `sidebar.enabled` on, the parent records each child's process identity and reports one pane token, `piha_delegated_v1`, through the Herdr socket. With it off, the extension writes no pane metadata. See [Safety and uninstall](../../README.md#safety-and-uninstall) in the main README.
132
+
133
+ ## Uninstall
134
+
135
+ 1. Run All, or skip this step. Herdr also drops this plugin's view when you disable, unlink, or uninstall the plugin.
136
+ 2. Remove the plugin. Use `uninstall` for a GitHub install and `unlink` for a linked directory:
137
+
138
+ ```bash
139
+ herdr plugin uninstall pi-herdr-agents.sidebar
140
+ herdr plugin unlink pi-herdr-agents.sidebar
141
+ ```
142
+
143
+ 3. Set `sidebar.enabled` to `false`, or delete the `sidebar` key, and run `/reload` in each parent session. The reload clears the markers of children that are still running and stops renewing them. A marker whose clear does not reach Herdr expires within 15 seconds.
144
+
145
+ ## Which rows Focus hides
146
+
147
+ The extension writes the pane token `piha_delegated_v1` with the value `live`
148
+ on a delegated child's pane. Focus hides a row only when that token is `live`
149
+ and the agent status is `idle`, `working`, or `done`. Focus sends this
150
+ filter, with no sort:
151
+
152
+ ```json
153
+ {
154
+ "op": "not",
155
+ "filter": {
156
+ "op": "all",
157
+ "filters": [
158
+ { "op": "eq", "field": { "token": "piha_delegated_v1" }, "value": "live" },
159
+ { "op": "in", "field": "status", "values": ["idle", "working", "done"] }
160
+ ]
161
+ }
162
+ }
163
+ ```
164
+
165
+ | Agent | Marker | Focus |
166
+ | --- | --- | --- |
167
+ | Main session or manually started agent | none | shown |
168
+ | `/worktree` handoff session | none | shown |
169
+ | Delegated child: fresh, resumed, persistent, or in a managed worktree | `live` | hidden |
170
+ | Delegated child that Herdr reports as blocked | `live` | shown |
171
+ | Agent whose status is unknown | any | shown |
172
+ | Pane with a missing marker or any other value | missing or other | shown |
173
+ | Retained worktree shell after its child exits | cleared | shown |
174
+
175
+ The `done` status means idle and not yet seen. It does not mean that anyone
176
+ accepted the child's work, so Focus hides it like any idle child.
177
+
178
+ ## What a marker proves
179
+
180
+ A `live` marker means the extension classified the pane's process as a
181
+ delegated child within the last 15 seconds. It does not prove that the same
182
+ process occupies the pane when Herdr draws the row.
183
+
184
+ The parent Pi session writes the marker after its child records its own
185
+ process identity and the parent verifies it. The parent renews the marker every
186
+ 4 to 5 seconds while that exact process (PID and start time) is alive. Renewal
187
+ runs on the parent's existing supervision checks, so if those checks stall,
188
+ the marker expires and the child reappears. The parent clears the marker when
189
+ the child finishes, when it stops supervising the child, or when it finds that
190
+ process gone or unverifiable. The marker belongs to the pane, not to the process.
191
+ Renewal checks only the original process, so a different process in the same
192
+ pane can be hidden for as long as the original process stays alive and
193
+ supervised. See [Reusing a delegated pane](#reusing-a-delegated-pane).
194
+
195
+ Each write carries a 15-second time-to-live and a sequence number that
196
+ increases across all of a parent's writes. The 15-second guarantee counts from
197
+ the last write Herdr accepted. It does not restart when the pane's occupant
198
+ changes or the original process is suspended. Herdr ignores a write whose
199
+ sequence number is not higher than the last one it applied, so a delayed
200
+ renewal cannot undo a clear. The extension does not retry a failed write and
201
+ stops writing after three failures in a row.
202
+
203
+ Some leftover markers still occur:
204
+
205
+ - If the parent process exits or crashes, its last marker stays until its
206
+ time-to-live ends, up to 15 seconds after the last write Herdr applied.
207
+ - A renewal that Herdr processes late starts a new 15-second period, unless a
208
+ clear with a higher sequence number has already arrived.
209
+ - A new agent started in the same pane during that window stays hidden by
210
+ Focus until the marker expires.
211
+ - If the parent restarts while the same Herdr server keeps running, and the
212
+ system clock has moved backwards, the new parent's sequence numbers can fall
213
+ behind the last ones Herdr applied. Herdr then ignores its writes until the
214
+ clock catches up. A Herdr server restart resets the sequence state and drops
215
+ every marker.
216
+
217
+ Run All whenever a row you expect is missing. Markers change only what the
218
+ Agents view shows. They are not a security boundary.
219
+
220
+ ### Reusing a delegated pane
221
+
222
+ The marker follows the pane, and the parent keeps renewing it while the
223
+ original child process is alive and supervised. A child suspended with Ctrl-Z
224
+ is still alive, so if you start another agent in that pane by hand, Focus can
225
+ hide the new agent until the original child is stopped or no longer
226
+ supervised. Run All before you reuse a delegated pane, or start the new agent
227
+ in a fresh pane. To end renewal, stop the child with the host's normal
228
+ controls. The last marker then lasts at most 15 seconds after the last write
229
+ Herdr accepted. The first version of the plugin has this limitation, and no
230
+ fix for pane reuse is planned for it.
231
+
232
+ ## Limits
233
+
234
+ - Tested only on Linux with the Herdr build named above, one local server, and
235
+ one attached client. Remote machines and several attached clients are
236
+ untested.
237
+ - The marker follows the pane, not the process. See
238
+ [Reusing a delegated pane](#reusing-a-delegated-pane).
239
+ - Herdr allows at most 32 sequenced metadata sources per pane. The extension
240
+ uses the single source `pi-herdr-agents` for every marker.
241
+ - The plugin has no popup, settings, or startup hook, and it does not hide
242
+ workspaces or notifications.
@@ -0,0 +1,21 @@
1
+ id = "pi-herdr-agents.sidebar"
2
+ name = "Pi Herdr Agents Sidebar"
3
+ version = "0.1.0"
4
+ # Tested only on 0.9.2-preview.2026-09-29-8e78f929d8f0. Herdr accepts only a
5
+ # plain version here and reports that preview as 0.9.2, so any 0.9.2 build,
6
+ # including a stable one, passes this check untested. See README.md.
7
+ min_herdr_version = "0.9.2"
8
+ description = "Hide delegated Pi children from the Agents view (Focus) or show every agent (All)"
9
+ platforms = ["linux"]
10
+
11
+ [[actions]]
12
+ id = "focus"
13
+ title = "Pi agents: Focus (hide delegated children)"
14
+ contexts = ["global"]
15
+ command = ["node", "sidebar.mjs", "focus"]
16
+
17
+ [[actions]]
18
+ id = "all"
19
+ title = "Pi agents: All (show every agent)"
20
+ contexts = ["global"]
21
+ command = ["node", "sidebar.mjs", "all"]