pi-extended-teams 2.2.6 → 2.2.7

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/README.md CHANGED
@@ -55,6 +55,8 @@ This works well for multi-angle code review, root-cause investigation, parallel
55
55
 
56
56
  With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `i` to interrupt its currently running tool command, `x` to stop the whole agent, and Escape to return.
57
57
 
58
+ Inside Herdr, press `h` in an ordinary direct agent's preview to move it into a focused sibling Pi pane in the same workspace. Its conversation and team communication continue there. Nested helpers and delegation-enabled or workflow agents are excluded.
59
+
58
60
  Scroll the transcript with Page Up/Page Down or the mouse wheel, including under Herdr. Press End or scroll back to the bottom to follow new output.
59
61
 
60
62
  The lead can invoke the same command-only behavior with `interrupt_teammate({ agent_name: "agent" })`. It keeps the agent's session, task context, and file claims intact so you can send follow-up work. In-process cancellation is cooperative and may report that it is still pending; for tmux-backed agents, success means Pi's Escape key was delivered, not that command settlement was independently confirmed.
package/docs/access.md CHANGED
@@ -8,13 +8,13 @@
8
8
  - Write teammates start a separate Pi process in a tmux pane. The extension builds and runs the local Pi launch command with the teammate's working directory, model and thinking configuration, selected extensions, and `PI_TEAM_NAME`, `PI_AGENT_NAME`, and `PI_LIFECYCLE_RUN_ID` environment variables.
9
9
  - Before launching a write teammate, the extension runs a bounded local `pi models --all` preflight through `sh -c` to determine whether the configured model is available.
10
10
  - On macOS, while at least one write teammate is active, the extension may run `/usr/bin/caffeinate -i -w <lead-pid>` to prevent idle system sleep. It terminates that helper when no write teammates remain or the extension is disposed.
11
- - In a Herdr session, the extension can invoke the configured `herdr` binary to split, start, and close panes when moving an agent. The binary path defaults to `herdr` and can be set with `HERDR_BIN_PATH`.
11
+ - Inside Herdr, pressing `h` can move an eligible in-process agent into a sibling Pi pane. The extension calls the `herdr` executable to split, run, focus, or close that pane.
12
12
 
13
13
  ## Files and local state
14
14
 
15
15
  The extension reads settings from `~/.pi/agent/pi-extended-teams/settings.json` and `<project>/.pi/pi-extended-teams.json`. Model-provider compatibility also reads the legacy `~/.pi/pi-extended-teams.json` path. Predefined agents can be read from `~/.pi/agent/agents/` and `<project>/.pi/agents/`; team templates can be read or written at `~/.pi/teams.yaml`, `~/.pi/agent/teams.yaml`, and `<project>/.pi/teams.yaml`.
16
16
 
17
- For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
17
+ For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts and handoff prompt snapshots under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
18
18
 
19
19
  The extension reads project extension sources only when Pi marks the same working directory trusted. Teammates receive the working directory supplied at spawn time. A write teammate can use any filesystem access granted to its child Pi process and enabled tools. A read teammate's tools are restricted by the extension, but it still receives prompt and project context supplied by the lead.
20
20
 
@@ -1,4 +1,9 @@
1
1
  import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { buildPiCommand, getPiLaunchCommand, shellQuote } from "../internal/pi-command";
6
+ import { herdrCommand } from "../runtime/herdr";
2
7
  import * as runtime from "../../src/utils/runtime";
3
8
  import * as teams from "../../src/utils/teams";
4
9
  import * as messaging from "../../src/utils/messaging";
@@ -12,7 +17,7 @@ import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../
12
17
  import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
13
18
  import { parseQualifiedModel } from "../../src/utils/model-resolution";
14
19
  import { closePersistedRecipient } from "../team/recipient-closure";
15
- import { generateExtensionInstanceId, generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
20
+ import { generateExtensionInstanceId, generateLifecycleRunId, withLifecycleTombstoneLock } from "../../src/utils/lifecycle-tombstone";
16
21
  import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
17
22
  import {
18
23
  createSpawnResourcePlan,
@@ -634,6 +639,7 @@ export async function runReadAgentInProcess(
634
639
  getTeamName: () => readTeamName,
635
640
  }).shutdownTeammate;
636
641
 
642
+ let handoffRequested = false;
637
643
  let submittedFinalReport: SubmittedAgentReport | undefined;
638
644
  let finalReportSubmissionInProgress = false;
639
645
  let childSessionManager: any;
@@ -660,6 +666,7 @@ export async function runReadAgentInProcess(
660
666
  const closeRecipient = async (): Promise<ReadAgentDeliveryCloseResult> => {
661
667
  if (pendingChildParent) pendingChildController?.cancelParent(pendingChildParent);
662
668
  const deliveryClose = closeReadAgentMessageDelivery(state);
669
+ if (handoffRequested) return deliveryClose;
663
670
  if (!state.recipientClosurePromise) {
664
671
  state.recipientClosurePromise = closePersistedRecipient(
665
672
  readTeamName,
@@ -959,7 +966,7 @@ export async function runReadAgentInProcess(
959
966
  } as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
960
967
 
961
968
  state.session = session;
962
- installReadAgentSessionLifecycle(session);
969
+ const sessionLifecycle = installReadAgentSessionLifecycle(session);
963
970
  try {
964
971
  if (typeof session.bindExtensions === "function") {
965
972
  await (session.bindExtensions as (bindings: { mode: "print" }) => Promise<void>)({ mode: "print" });
@@ -973,6 +980,96 @@ export async function runReadAgentInProcess(
973
980
  if (state.teardownState !== "persistence_failed") await state.teardownPromise;
974
981
  return;
975
982
  }
983
+ if (process.env.HERDR_ENV === "1" && (member.delegationDepth ?? 0) === 0
984
+ && !member.requestedBy && !member.parentAgentName && !member.allowNestedReadAgents
985
+ && !shouldSuppressLeadReportInjection(member)) {
986
+ let moving: Promise<void> | undefined;
987
+ let paneId: string | undefined;
988
+ let command = "";
989
+ let queued: string[] = [];
990
+ let released = false;
991
+ const release = Promise.all([finished, sessionLifecycle.finalized]).then(() => {
992
+ if (handoffRequested) { state.session = undefined; released = true; }
993
+ });
994
+ void release.catch(() => {});
995
+ const recordPane = (id: string | undefined) => withLifecycleTombstoneLock(readTeamName, member.name, async lock => {
996
+ const current = (await teams.readConfig(readTeamName)).members.find(item => item.name === member.name);
997
+ if (!current || current.lifecycleRunId !== state.runId || current.isActive === false || lock.read().status !== "absent") {
998
+ throw new Error("The agent is no longer available to move.");
999
+ }
1000
+ await teams.updateMember(readTeamName, member.name, { herdrPaneId: id, tmuxPaneId: "" });
1001
+ });
1002
+ state.moveToHerdr = () => moving ??= (async () => {
1003
+ if (!options.isCurrentReadAgentRun(key, state) || (!handoffRequested && !state.acceptingMessages)) {
1004
+ throw new Error("The agent is already finishing.");
1005
+ }
1006
+ if (handoffRequested && !released) throw new Error("The current operation is still stopping. Try h after it settles.");
1007
+ if (paneId) { herdrCommand("pane", "close", paneId); await recordPane(undefined); paneId = undefined; }
1008
+ const sessionFile = childSessionManager.getSessionFile();
1009
+ if (!sessionFile || !fs.existsSync(sessionFile)) throw new Error("The agent has no saved session yet.");
1010
+ if (!handoffRequested) {
1011
+ const promptDir = privateSessionDirectory ?? preparePrivateAgentSessionDirectory(readTeamName, member.name, state.runId);
1012
+ const promptFile = path.join(promptDir, "herdr-system-prompt.txt");
1013
+ fs.writeFileSync(promptFile, session.agent.state.systemPrompt.replace("running in-process so the lead can follow and control you from Pi", "running in a Herdr pane"), { mode: 0o600 });
1014
+ const tools = session.agent.state.tools.map(tool => tool.name).join(",");
1015
+ const identity = {
1016
+ HOME: os.homedir(), PI_CODING_AGENT_DIR: agentDir,
1017
+ PI_AGENT_NAME: member.name, PI_TEAM_NAME: readTeamName, PI_LIFECYCLE_RUN_ID: state.runId,
1018
+ PI_EXTENDED_TEAMS_HERDR_RESUME: "1",
1019
+ };
1020
+ const currentModel = session.model ?? model;
1021
+ const launch = buildPiCommand(getPiLaunchCommand(), `${currentModel.provider}/${currentModel.id}`,
1022
+ session.thinkingLevel ?? member.thinking, resourcePlan.extensionPaths,
1023
+ resourcePlan.trust.projectTrusted, resourcePlan.selfExtensionPath);
1024
+ command = [
1025
+ "env", ...Object.entries(identity).map(([name, value]) => `${name}=${shellQuote(value)}`), launch,
1026
+ "--session", shellQuote(sessionFile), "--tools", shellQuote(tools), "--system-prompt", shellQuote(promptFile),
1027
+ ].join(" ");
1028
+ // Keep fresh-shell input below the PTY line limit without shortening the launch arguments.
1029
+ const launchFile = path.join(promptDir, "herdr-launch.sh");
1030
+ fs.writeFileSync(launchFile, `exec ${command}\n`, { mode: 0o600 });
1031
+ command = `/bin/sh ${shellQuote(launchFile)}`;
1032
+ }
1033
+ paneId = JSON.parse(herdrCommand("pane", "split", "--current", "--direction", "right", "--cwd", member.cwd, "--focus")).result?.pane?.pane_id;
1034
+ if (typeof paneId !== "string" || !paneId) throw new Error("Herdr did not return a pane ID.");
1035
+ try {
1036
+ if (!handoffRequested) {
1037
+ handoffRequested = true;
1038
+ state.stopRequested = true;
1039
+ const delivery = closeReadAgentMessageDelivery(state);
1040
+ const pending = session.clearQueue();
1041
+ queued = [...pending.steering, ...pending.followUp];
1042
+ signalReadAgentWake(state);
1043
+ const shutdown = await sessionLifecycle.requestShutdown("resume", delivery.rawDeliverySettlement);
1044
+ if (shutdown.status !== "settled" || shutdown.abort !== "settled" || shutdown.dispose !== "settled") {
1045
+ throw new Error("The current operation has not stopped. Try h after it settles.");
1046
+ }
1047
+ await release;
1048
+ }
1049
+ if (queued.length) {
1050
+ SessionManager.open(sessionFile).appendMessage({ role: "user", content: queued.join("\n\n"), timestamp: Date.now() });
1051
+ queued = [];
1052
+ }
1053
+ const previousPid = (await runtime.readRuntimeStatus(readTeamName, member.name))?.pid;
1054
+ await recordPane(paneId);
1055
+ herdrCommand("pane", "run", paneId, command);
1056
+ const deadline = Date.now() + 10000;
1057
+ for (;;) {
1058
+ const status = await runtime.readRuntimeStatus(readTeamName, member.name);
1059
+ if (status?.lifecycleRunId === state.runId && status.pid && status.pid !== previousPid && status.pid !== process.pid) break;
1060
+ if (Date.now() >= deadline) throw new Error("Pi did not resume. The saved session is retained; h can retry.");
1061
+ await new Promise(resolve => setTimeout(resolve, 50));
1062
+ }
1063
+ if (options.isCurrentReadAgentRun(key, state)) options.runningReadAgents.delete(key);
1064
+ options.renderReadAgentStatus();
1065
+ } catch (error) {
1066
+ herdrCommand("pane", "close", paneId);
1067
+ await recordPane(undefined);
1068
+ paneId = undefined;
1069
+ throw error;
1070
+ }
1071
+ })().catch(error => { moving = undefined; throw error; });
1072
+ }
976
1073
  markReadAgentActivity(state, "started", "thinking");
977
1074
  options.renderReadAgentStatus();
978
1075
 
@@ -1262,6 +1359,7 @@ export async function runReadAgentInProcess(
1262
1359
  if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
1263
1360
  state.heartbeatTimer = undefined;
1264
1361
  closeReadAgentMessageDelivery(state);
1362
+ if (handoffRequested) { state.resolveFinished?.(); return; }
1265
1363
  const teardown = await shutdownTeammate(readTeamName, member, { reason: "quit" });
1266
1364
  if (pendingChildController && pendingChildParent) {
1267
1365
  pendingChildController.forgetParent(pendingChildParent);
@@ -220,10 +220,14 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
220
220
  }
221
221
 
222
222
  scheduleTeammateOneShot(() => {
223
- options.quietTrigger("read_inbox to get your instructions, then begin your work.");
223
+ options.quietTrigger(process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1"
224
+ ? "Continue your existing assignment. Read any new inbox messages, then finish and report_and_exit from this pane."
225
+ : "read_inbox to get your instructions, then begin your work.");
224
226
  }, 1000);
225
227
 
226
228
  if (teamName) {
229
+ const herdrResume = process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1";
230
+ let notifiedInbox: string | undefined;
227
231
  let wakeInFlight = false;
228
232
  const wakeIfUnread = async () => {
229
233
  if (teammateInboxDisposed) return;
@@ -232,7 +236,7 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
232
236
  scheduleTeammateInboxWake(250);
233
237
  return;
234
238
  }
235
- if (!ctx.isIdle()) {
239
+ if (!ctx.isIdle() && !herdrResume) {
236
240
  teammatePendingInboxWake = true;
237
241
  scheduleTeammateInboxWake(250);
238
242
  return;
@@ -246,7 +250,15 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
246
250
  lastHeartbeatAt: Date.now(),
247
251
  });
248
252
  if (unread.length > 0) {
249
- options.quietTrigger(`You have ${unread.length} new inbox message(s). Read them with read_inbox and act.`);
253
+ const content = `You have ${unread.length} new inbox message(s). Read them with read_inbox and act.`;
254
+ if (herdrResume) {
255
+ const inboxVersion = JSON.stringify(unread);
256
+ if (inboxVersion !== notifiedInbox) {
257
+ pi.sendMessage({ customType: "pi-extended-teams-wake", content, display: false },
258
+ { triggerTurn: true, deliverAs: "steer" });
259
+ notifiedInbox = inboxVersion;
260
+ }
261
+ } else options.quietTrigger(content);
250
262
  }
251
263
  } catch (e) {
252
264
  if (!teammateInboxDisposed) {
@@ -0,0 +1,13 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export function herdrCommand(...args: string[]): string {
4
+ if (process.env.HERDR_ENV !== "1") throw new Error("This action requires Herdr.");
5
+ const result = spawnSync("herdr", args, { encoding: "utf8", timeout: 5000 });
6
+ if (result.status !== 0) {
7
+ let code: string | undefined;
8
+ try { code = JSON.parse(result.stderr).error?.code; } catch { /* Non-JSON CLI failure. */ }
9
+ if (args[0] === "pane" && args[1] === "close" && code === "pane_not_found") return "";
10
+ throw new Error(result.stderr || result.error?.message || "Herdr command failed.");
11
+ }
12
+ return result.stdout;
13
+ }
@@ -47,6 +47,7 @@ export interface RunningReadAgent extends ManagedReadAgentLifecycleState {
47
47
  /** Fail finalization closed so runtime, member, fence, and transcript remain recoverable. */
48
48
  finalizationBlockedReason?: string;
49
49
  session?: AgentSession;
50
+ moveToHerdr?(): Promise<void>;
50
51
  finished?: Promise<void>;
51
52
  }
52
53
 
@@ -20,7 +20,8 @@ import {
20
20
  type ReadAgentTeardownResult,
21
21
  } from "../agents/read-agent-session-lifecycle";
22
22
  import { releaseAllClaimsForAgent } from "./roster";
23
- import { cleanupPidFileProcess } from "../internal/session-files";
23
+ import { cleanupPidFileProcess, unlinkPidFile } from "../internal/session-files";
24
+ import { herdrCommand } from "../runtime/herdr";
24
25
  import { closePersistedRecipient } from "./recipient-closure";
25
26
  import { cleanupPrivateAgentSessionDirectory } from "../internal/agent-session-files";
26
27
 
@@ -65,7 +66,10 @@ export function createLifecycleRuntime(options: LifecycleRuntimeOptions) {
65
66
  async function finalizeTeammateRuntime(teamName: string, member: Member, expectedRunId: string): Promise<void> {
66
67
  const pidFile = path.join(paths.teamDir(teamName), `${member.name}.pid`);
67
68
  const pidFileExisted = fs.existsSync(pidFile);
68
- const pidCleanup = cleanupPidFileProcess(pidFile, { skipPid: process.pid });
69
+ if (member.herdrPaneId) herdrCommand("pane", "close", member.herdrPaneId);
70
+ const pidCleanup = member.herdrPaneId
71
+ ? { pid: undefined, killError: undefined, unlinked: !pidFileExisted || unlinkPidFile(pidFile) }
72
+ : cleanupPidFileProcess(pidFile, { skipPid: process.pid });
69
73
  if (pidFileExisted && !pidCleanup.unlinked) {
70
74
  throw new Error(`Could not remove pid file for ${member.name}.`);
71
75
  }
@@ -147,6 +147,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
147
147
  async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
148
148
  const targetTeamName = requireCurrentSession(options);
149
149
  if (!options.isTeammate) throw new Error("report_and_exit is only available to spawned agents.");
150
+ if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && !params.content.trim()) throw new Error("Final report content must not be empty.");
150
151
 
151
152
  const config = await teams.readConfig(targetTeamName);
152
153
  const member = config.members.find(m => m.name === options.agentName);
@@ -271,6 +272,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
271
272
  async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, _ctx: any) {
272
273
  const targetTeamName = requireCurrentSession(options);
273
274
  const targetAgent = params.agent_name || options.agentName;
275
+ if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && targetAgent !== options.agentName) throw new Error("A resumed subagent can only read its own inbox.");
274
276
  const markAsRead = params.mark_as_read !== false;
275
277
  const unreadOnly = params.unread_only !== false;
276
278
  const msgs = await messaging.readInbox(targetTeamName, targetAgent, unreadOnly, markAsRead);
@@ -605,11 +605,12 @@ export function createAgentFollowComponent(
605
605
  const logAction = expandLargeToolResults ? "l collapse logs" : "l expand logs";
606
606
  const messageAction = options.sendMessage ? " · m message" : "";
607
607
  const interruptAction = options.interruptAgent ? " · i interrupt" : "";
608
- const help = composingMessage
608
+ const herdrAction = process.env.HERDR_ENV === "1" && agent.moveToHerdr ? " · h Herdr" : "";
609
+ const help = messageStatus && !options.sendMessage ? messageStatus : composingMessage
609
610
  ? `message ${agent.name} · enter send · esc cancel`
610
611
  : agents.length > 1
611
- ? `↑ previous/main · ↓ next agent · ←/→ agent · ${logAction}${messageAction}${interruptAction} · x stop · pgup/pgdn scroll · esc main`
612
- : `↑/esc main · ${logAction}${messageAction}${interruptAction} · x stop · pgup/pgdn scroll · end follow`;
612
+ ? `↑ previous/main · ↓ next agent · ←/→ agent · ${logAction}${messageAction}${interruptAction}${herdrAction} · x stop · pgup/pgdn scroll · esc main`
613
+ : `↑/esc main · ${logAction}${messageAction}${interruptAction}${herdrAction} · x stop · pgup/pgdn scroll · end follow`;
613
614
 
614
615
  const currentTranscriptWidth = Math.max(20, innerWidth);
615
616
  if (transcriptAgent !== agent
@@ -766,6 +767,14 @@ export function createAgentFollowComponent(
766
767
  tui.requestRender();
767
768
  return;
768
769
  }
770
+ if (data.toLowerCase() === "h" && process.env.HERDR_ENV === "1") {
771
+ const agent = currentAgent(sortedAgents(), selectedName);
772
+ if (agent?.moveToHerdr) void agent.moveToHerdr().then(done).catch(error => {
773
+ messageStatus = sanitizePlainTuiLine(error instanceof Error ? error.message : String(error));
774
+ tui.requestRender();
775
+ });
776
+ return;
777
+ }
769
778
  if (data.toLowerCase() === "i" && options.interruptAgent) {
770
779
  const agent = currentAgent(sortedAgents(), selectedName);
771
780
  if (!agent || interruptingAgents.has(agent.name)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extended-teams",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
4
  "description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "scripts": {
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "vitest run",
31
- "test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts",
31
+ "test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts extensions/ui/agent-follow-view.test.ts extensions/tools/coordination-tools.test.ts",
32
32
  "build": "tsc --noEmit"
33
33
  },
34
34
  "main": "extensions/index.ts",
@@ -10,6 +10,7 @@ export interface Member {
10
10
  model?: string;
11
11
  joinedAt: number;
12
12
  tmuxPaneId: string;
13
+ herdrPaneId?: string;
13
14
  windowId?: string;
14
15
  cwd: string;
15
16
  subscriptions: any[];
@@ -33,7 +34,7 @@ export interface Member {
33
34
  parentAgentName?: string;
34
35
  /** Runtime-owned parent lifecycle identity for a restricted nested read child. */
35
36
  parentLifecycleRunId?: string;
36
- /** "read" agents run in-process (no pane); "write" agents spawn in tmux. */
37
+ /** Tool role; execution may be in-process or in a terminal pane. */
37
38
  role?: "read" | "write";
38
39
  /** Optional category preset name from settings.json. */
39
40
  category?: string;