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
|
@@ -66,6 +66,12 @@ import {
|
|
|
66
66
|
type ModelRegistryAdapter,
|
|
67
67
|
} from "../core/routing.ts";
|
|
68
68
|
import { loadPaneConfig, type PaneConfig } from "../core/config/pane-config.ts";
|
|
69
|
+
import {
|
|
70
|
+
DelegatedMarkers,
|
|
71
|
+
herdrMarkerWriter,
|
|
72
|
+
type DelegatedMarker,
|
|
73
|
+
type MarkerWriter,
|
|
74
|
+
} from "./delegated-marker.ts";
|
|
69
75
|
import {
|
|
70
76
|
createRunSession,
|
|
71
77
|
type RunSession,
|
|
@@ -327,6 +333,8 @@ export interface PiRunSessionInfrastructure {
|
|
|
327
333
|
supervision: SupervisionCoordinator;
|
|
328
334
|
/** Kernel process-identity probe for worktree cancel; tests inject it. */
|
|
329
335
|
processProbe?: ProcessIdentityProbe;
|
|
336
|
+
/** Sidebar marker transport; defaults to this pane's Herdr socket. */
|
|
337
|
+
markerWriter?: MarkerWriter;
|
|
330
338
|
}
|
|
331
339
|
// pi-herdr-agents extension
|
|
332
340
|
export interface DefaultRunSessionOptions {
|
|
@@ -340,6 +348,8 @@ export interface DefaultRunSessionOptions {
|
|
|
340
348
|
hooks: PiRunSessionHooks;
|
|
341
349
|
persistent: PiPersistentHostOperations;
|
|
342
350
|
forcePolling: boolean;
|
|
351
|
+
/** Mark delegated children's panes for the Herdr sidebar plugin. */
|
|
352
|
+
sidebarMarkers?: boolean;
|
|
343
353
|
}
|
|
344
354
|
// pi-herdr-agents extension
|
|
345
355
|
export interface PiWorktreeHandoffInput {
|
|
@@ -395,6 +405,7 @@ interface PiEntry {
|
|
|
395
405
|
started: PiStartedMetadata;
|
|
396
406
|
completed?: PiCompletedMetadata;
|
|
397
407
|
readResumeResult?: PiSettlementIO["readResumeResult"];
|
|
408
|
+
marker?: DelegatedMarker;
|
|
398
409
|
}
|
|
399
410
|
// The previous facade carries its live composition explicitly across module replacement.
|
|
400
411
|
// No module-local lookup can accidentally rediscover an owner.
|
|
@@ -413,6 +424,8 @@ interface PiSessionOwner {
|
|
|
413
424
|
"surfaceProvider" | "supervision" | "processProbe"
|
|
414
425
|
>;
|
|
415
426
|
worktreeOperations: WorktreeOperations;
|
|
427
|
+
/** Absent outside Herdr: without a socket nothing can be marked. */
|
|
428
|
+
markers?: DelegatedMarkers;
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
// A detached close can outlive settlement without capturing a child, metadata,
|
|
@@ -426,6 +439,21 @@ function temporarySurfaceRelease(
|
|
|
426
439
|
};
|
|
427
440
|
}
|
|
428
441
|
|
|
442
|
+
function createDelegatedMarkers(
|
|
443
|
+
infrastructure: PiRunSessionInfrastructure | undefined,
|
|
444
|
+
probe: ProcessIdentityProbe | undefined,
|
|
445
|
+
): DelegatedMarkers | undefined {
|
|
446
|
+
// Injected infrastructure never falls back to the ambient Herdr socket.
|
|
447
|
+
if (infrastructure) {
|
|
448
|
+
const write = infrastructure.markerWriter;
|
|
449
|
+
return write ? new DelegatedMarkers({ write, probe }) : undefined;
|
|
450
|
+
}
|
|
451
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
452
|
+
return socketPath
|
|
453
|
+
? new DelegatedMarkers({ write: herdrMarkerWriter(socketPath), probe })
|
|
454
|
+
: undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
429
457
|
// Capture only resume presentation scalars, not the child or its owning registry.
|
|
430
458
|
function resumeResultReader(
|
|
431
459
|
sessionFile: string,
|
|
@@ -682,6 +710,20 @@ export function createDefaultRunSession(
|
|
|
682
710
|
// Replacements must invoke the original kernel's invocation closure, which
|
|
683
711
|
// reads this shared current binding, not the old facade's options.
|
|
684
712
|
if (adopted) resumePi = state.resumePi;
|
|
713
|
+
// Live markers and their sequence survive a reload with their runs.
|
|
714
|
+
state.markers ??= createDelegatedMarkers(
|
|
715
|
+
options.infrastructure,
|
|
716
|
+
state.infrastructure.processProbe,
|
|
717
|
+
);
|
|
718
|
+
// Turning the setting off retires every marker now, pending ones included;
|
|
719
|
+
// a retired marker never writes live again, even if the setting returns.
|
|
720
|
+
if (!options.sidebarMarkers)
|
|
721
|
+
for (const entry of state.entries.values()) entry.marker?.retire();
|
|
722
|
+
|
|
723
|
+
/** Markers for new launches, only while the sidebar setting is on. */
|
|
724
|
+
function enabledMarkers(): DelegatedMarkers | undefined {
|
|
725
|
+
return state.options.sidebarMarkers ? state.markers : undefined;
|
|
726
|
+
}
|
|
685
727
|
|
|
686
728
|
function prune(id: string) {
|
|
687
729
|
if (!state.kernel.getTask(id)) state.entries.delete(id);
|
|
@@ -712,11 +754,11 @@ export function createDefaultRunSession(
|
|
|
712
754
|
};
|
|
713
755
|
}
|
|
714
756
|
function observed(child: PiRunRecord, observation: RunObservation) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
)
|
|
719
|
-
|
|
757
|
+
const entry = [...state.entries.values()].find(
|
|
758
|
+
(candidate) => candidate.record === child,
|
|
759
|
+
);
|
|
760
|
+
if (child.lifecycle.delivery === "suppressed" || !entry) return;
|
|
761
|
+
entry.marker?.renew();
|
|
720
762
|
if (observation.kind === "local-evidence" && child.persistent) {
|
|
721
763
|
try {
|
|
722
764
|
state.options.persistent.drain(child, io);
|
|
@@ -745,6 +787,7 @@ export function createDefaultRunSession(
|
|
|
745
787
|
parentRuntime: snapshot.parentRuntime,
|
|
746
788
|
supervision: state.infrastructure.supervision,
|
|
747
789
|
processProbe: state.infrastructure.processProbe,
|
|
790
|
+
recordProcessIdentity: enabledMarkers() !== undefined,
|
|
748
791
|
operations: launchOperations(snapshot),
|
|
749
792
|
onObservation(child, kind) {
|
|
750
793
|
const at = Date.now();
|
|
@@ -794,7 +837,10 @@ export function createDefaultRunSession(
|
|
|
794
837
|
};
|
|
795
838
|
if (suppressed)
|
|
796
839
|
child.lifecycle = markDelivery(child.lifecycle, "suppressed");
|
|
797
|
-
else
|
|
840
|
+
else {
|
|
841
|
+
entry.marker = enabledMarkers()?.attach(child);
|
|
842
|
+
state.entries.set(task.id, entry);
|
|
843
|
+
}
|
|
798
844
|
return {
|
|
799
845
|
handle,
|
|
800
846
|
adapter: owner,
|
|
@@ -805,6 +851,8 @@ export function createDefaultRunSession(
|
|
|
805
851
|
),
|
|
806
852
|
observe: (at) => hydrate(child, at, "refresh"),
|
|
807
853
|
finalize: async (result) => {
|
|
854
|
+
// Clear before delivery; a retained worktree root keeps its pane.
|
|
855
|
+
entry.marker?.retire();
|
|
808
856
|
const at = Date.now();
|
|
809
857
|
let evidence = result.evidence;
|
|
810
858
|
if (evidence)
|
|
@@ -1153,8 +1201,11 @@ export function createDefaultRunSession(
|
|
|
1153
1201
|
suppress(id) {
|
|
1154
1202
|
for (const acquisition of state.pending)
|
|
1155
1203
|
if (acquisition.taskId === id) acquisition.suppressed = true;
|
|
1156
|
-
const
|
|
1204
|
+
const suppressed = live(id);
|
|
1205
|
+
const r = suppressed?.record;
|
|
1157
1206
|
if (r) r.lifecycle = markDelivery(r.lifecycle, "suppressed");
|
|
1207
|
+
// Nothing renews an unsupervised run's marker; clear it now.
|
|
1208
|
+
suppressed?.marker?.retire();
|
|
1158
1209
|
state.kernel.suppress(id);
|
|
1159
1210
|
state.entries.delete(id);
|
|
1160
1211
|
},
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import {
|
|
3
|
+
isNonEmptyString,
|
|
4
|
+
isRecord,
|
|
5
|
+
isString,
|
|
6
|
+
type JsonObject,
|
|
7
|
+
} from "../../core/config/type-guards.ts";
|
|
8
|
+
|
|
9
|
+
export const HERDR_SOCKET_TIMEOUT_MS = 2_000;
|
|
10
|
+
export const HERDR_SOCKET_MAX_REPLY_BYTES = 64 * 1024;
|
|
11
|
+
const MAX_REPORTED_CHARS = 200;
|
|
12
|
+
|
|
13
|
+
export type HerdrSocketReply =
|
|
14
|
+
| { kind: "result"; result: JsonObject }
|
|
15
|
+
/** Herdr answered with an error object. */
|
|
16
|
+
| { kind: "error"; code: string; message: string }
|
|
17
|
+
/** No usable answer: refused, closed, timed out, oversized, malformed, or another id. */
|
|
18
|
+
| { kind: "failed"; reason: string };
|
|
19
|
+
|
|
20
|
+
function bounded(text: string): string {
|
|
21
|
+
return text.replace(/\p{Cc}+/gu, " ").slice(0, MAX_REPORTED_CHARS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let requestCount = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One request on one connection: Herdr closes ordinary connections after a
|
|
28
|
+
* reply. Settles once with the first newline-terminated reply, or fails on a
|
|
29
|
+
* timeout, an oversized reply, or a connection that ends first. The socket
|
|
30
|
+
* and timer are unreferenced, so a pending request never keeps Pi running.
|
|
31
|
+
*/
|
|
32
|
+
export function requestHerdrSocket(
|
|
33
|
+
socketPath: string,
|
|
34
|
+
method: string,
|
|
35
|
+
params: JsonObject,
|
|
36
|
+
options: { timeoutMs?: number; maxReplyBytes?: number } = {},
|
|
37
|
+
): Promise<HerdrSocketReply> {
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? HERDR_SOCKET_TIMEOUT_MS;
|
|
39
|
+
const maxReplyBytes = options.maxReplyBytes ?? HERDR_SOCKET_MAX_REPLY_BYTES;
|
|
40
|
+
const id = `pi-herdr-agents:${process.pid}:${++requestCount}`;
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let settled = false;
|
|
43
|
+
let received = Buffer.alloc(0);
|
|
44
|
+
const socket = connect(socketPath);
|
|
45
|
+
socket.unref();
|
|
46
|
+
const finish = (reply: HerdrSocketReply) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
socket.destroy();
|
|
51
|
+
resolve(reply);
|
|
52
|
+
};
|
|
53
|
+
const timer = setTimeout(
|
|
54
|
+
() =>
|
|
55
|
+
finish({
|
|
56
|
+
kind: "failed",
|
|
57
|
+
reason: `${method} got no reply in ${timeoutMs}ms`,
|
|
58
|
+
}),
|
|
59
|
+
timeoutMs,
|
|
60
|
+
);
|
|
61
|
+
timer.unref();
|
|
62
|
+
socket.on("connect", () => {
|
|
63
|
+
socket.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
64
|
+
});
|
|
65
|
+
socket.on("error", (error) =>
|
|
66
|
+
finish({ kind: "failed", reason: bounded(error.message) }),
|
|
67
|
+
);
|
|
68
|
+
socket.on("close", () =>
|
|
69
|
+
finish({
|
|
70
|
+
kind: "failed",
|
|
71
|
+
reason: `${method}: connection closed before a reply`,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
socket.on("data", (chunk: Buffer) => {
|
|
75
|
+
received = Buffer.concat([received, chunk]);
|
|
76
|
+
const end = received.indexOf(10);
|
|
77
|
+
if ((end < 0 ? received.length : end) > maxReplyBytes) {
|
|
78
|
+
finish({
|
|
79
|
+
kind: "failed",
|
|
80
|
+
reason: `${method} reply exceeds ${maxReplyBytes} bytes`,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (end >= 0)
|
|
85
|
+
finish(
|
|
86
|
+
parseHerdrSocketReply(received.subarray(0, end).toString("utf8"), id),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Herdr answers `ok` even when it ignores a write whose `seq` is not newer
|
|
94
|
+
* than the last one it applied from this source, so `answered` is not proof
|
|
95
|
+
* that the token changed.
|
|
96
|
+
*/
|
|
97
|
+
export type PaneTokenOutcome = "answered" | "pane-missing" | "failed";
|
|
98
|
+
|
|
99
|
+
export interface PaneTokenWrite {
|
|
100
|
+
paneId: string;
|
|
101
|
+
source: string;
|
|
102
|
+
seq: number;
|
|
103
|
+
token: string;
|
|
104
|
+
/** `null` clears the token. */
|
|
105
|
+
value: string | null;
|
|
106
|
+
ttlMs?: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One sequenced `pane.report_metadata` token patch. */
|
|
110
|
+
export async function reportPaneToken(
|
|
111
|
+
socketPath: string,
|
|
112
|
+
write: PaneTokenWrite,
|
|
113
|
+
options?: { timeoutMs?: number },
|
|
114
|
+
): Promise<PaneTokenOutcome> {
|
|
115
|
+
const params: JsonObject = {
|
|
116
|
+
pane_id: write.paneId,
|
|
117
|
+
source: write.source,
|
|
118
|
+
seq: write.seq,
|
|
119
|
+
tokens: { [write.token]: write.value },
|
|
120
|
+
};
|
|
121
|
+
if (write.ttlMs !== undefined) params.ttl_ms = write.ttlMs;
|
|
122
|
+
const reply = await requestHerdrSocket(
|
|
123
|
+
socketPath,
|
|
124
|
+
"pane.report_metadata",
|
|
125
|
+
params,
|
|
126
|
+
options,
|
|
127
|
+
);
|
|
128
|
+
if (reply.kind === "result") return "answered";
|
|
129
|
+
if (reply.kind === "error" && reply.code === "pane_not_found")
|
|
130
|
+
return "pane-missing";
|
|
131
|
+
return "failed";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Parses one reply line; a reply for another request id is never accepted. */
|
|
135
|
+
export function parseHerdrSocketReply(
|
|
136
|
+
line: string,
|
|
137
|
+
id: string,
|
|
138
|
+
): HerdrSocketReply {
|
|
139
|
+
let reply: JsonObject | undefined;
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = JSON.parse(line);
|
|
142
|
+
if (isRecord(parsed)) reply = parsed;
|
|
143
|
+
} catch {
|
|
144
|
+
// Malformed JSON is the same failure as a non-object reply below.
|
|
145
|
+
}
|
|
146
|
+
if (!reply) return { kind: "failed", reason: "reply is not a JSON object" };
|
|
147
|
+
if (reply.id !== id)
|
|
148
|
+
return {
|
|
149
|
+
kind: "failed",
|
|
150
|
+
reason: `reply id ${bounded(JSON.stringify(reply.id) ?? "missing")} is not ${id}`,
|
|
151
|
+
};
|
|
152
|
+
if (isRecord(reply.error))
|
|
153
|
+
return {
|
|
154
|
+
kind: "error",
|
|
155
|
+
code: isNonEmptyString(reply.error.code)
|
|
156
|
+
? bounded(reply.error.code)
|
|
157
|
+
: "herdr_error",
|
|
158
|
+
message: isString(reply.error.message)
|
|
159
|
+
? bounded(reply.error.message)
|
|
160
|
+
: "",
|
|
161
|
+
};
|
|
162
|
+
if (!isRecord(reply.result))
|
|
163
|
+
return { kind: "failed", reason: "reply has no result" };
|
|
164
|
+
return { kind: "result", result: reply.result };
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-herdr-agents",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Asynchronous Pi subagents in Herdr, with optional isolated Git worktrees",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"type": "module",
|
|
31
31
|
"scripts": {
|
|
32
32
|
"changelog": "auto-changelog",
|
|
33
|
-
"format": "biome format --write pi-extension test maestro",
|
|
34
|
-
"format:check": "biome format pi-extension test maestro",
|
|
35
|
-
"lint": "oxlint pi-extension test maestro",
|
|
36
|
-
"test": "node --experimental-strip-types --test test/test.ts test/worktree-cleanup.test.ts test/launch.test.ts test/runtime-routing.test.ts test/release-workflow.test.ts test/package-manifest.test.js test/maestro/*.test.ts",
|
|
33
|
+
"format": "biome format --write pi-extension test maestro plugins",
|
|
34
|
+
"format:check": "biome format pi-extension test maestro plugins",
|
|
35
|
+
"lint": "oxlint pi-extension test maestro plugins",
|
|
36
|
+
"test": "node --experimental-strip-types --test test/test.ts test/worktree-cleanup.test.ts test/launch.test.ts test/runtime-routing.test.ts test/release-workflow.test.ts test/package-manifest.test.js test/sidebar-plugin.test.js test/maestro/*.test.ts",
|
|
37
37
|
"test:integration": "node --experimental-strip-types --test --test-concurrency=1 test/integration/*.test.ts",
|
|
38
38
|
"test:integration:live": "PI_TEST_LIVE=1 node --experimental-strip-types --test --test-concurrency=1 test/integration/*.test.ts",
|
|
39
39
|
"version": "auto-changelog --package && git add CHANGELOG.md"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "2.5.11",
|
|
63
63
|
"@earendil-works/pi-ai": "^1.0.0",
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
65
65
|
"@earendil-works/pi-tui": "^1.0.0",
|
|
66
66
|
"@oxlint/plugins": "^1.80.0",
|
|
67
67
|
"@sinclair/typebox": "^0.34.52",
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type CancelReport,
|
|
39
39
|
} from "../../maestro/runtime/index.ts";
|
|
40
40
|
import { loadSupervisionConfig } from "../../maestro/core/config/supervision-config.ts";
|
|
41
|
+
import { loadSidebarConfig } from "../../maestro/core/config/sidebar-config.ts";
|
|
41
42
|
import {
|
|
42
43
|
discoverAgentCatalog as discoverCoreAgentCatalog,
|
|
43
44
|
ROLE_PACK_DISCOVERY_EVENT,
|
|
@@ -573,6 +574,10 @@ const supervisionConfig = loadSupervisionConfig(
|
|
|
573
574
|
getSubagentsConfigDir(),
|
|
574
575
|
getSubagentsConfigExamplePath(),
|
|
575
576
|
);
|
|
577
|
+
const sidebarConfig = loadSidebarConfig(
|
|
578
|
+
getSubagentsConfigDir(),
|
|
579
|
+
getSubagentsConfigExamplePath(),
|
|
580
|
+
);
|
|
576
581
|
|
|
577
582
|
const MAX_RESULT_PRESENTATION_CHARS = 16_000;
|
|
578
583
|
const MAX_SESSION_REFERENCE_CHARS = 10_000;
|
|
@@ -2447,6 +2452,7 @@ export default function subagentsExtension(
|
|
|
2447
2452
|
configExamplePath: getSubagentsConfigExamplePath(),
|
|
2448
2453
|
roles: [],
|
|
2449
2454
|
forcePolling: supervisionConfig.forcePolling,
|
|
2455
|
+
sidebarMarkers: sidebarConfig.enabled,
|
|
2450
2456
|
infrastructure: options.infrastructure,
|
|
2451
2457
|
getLaunchSnapshot() {
|
|
2452
2458
|
const ctx = runtime.latestCtx;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# Pi Herdr Agents Sidebar
|
|
2
|
+
|
|
3
|
+

|
|
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"]
|