pi-pier 0.2.5 → 0.2.6

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
@@ -1,11 +1,11 @@
1
1
  # pi-pier
2
2
 
3
3
  > [!IMPORTANT]
4
- > **This npm package is only the pi-extension half of pier.** pier is a two-half system — this extension plus a **herdr plugin** (`pier.workbench`). Without the herdr side you still get `todo_write` / `ask_user_question`, but subagents, pane integration, notifications, and the workspace bootstrap all require herdr. See [Install the herdr half](#install-the-herdr-half-companion-plugin) below.
4
+ > **This npm package is only the pi-extension half of pier.** pier is a two-half system — this extension plus a **herdr plugin** (`pier.workbench`). Without the herdr side you still get `todo_write` / `ask_user_question`; `subagent` / `terminal` are not registered. Pane integration, notifications, and workspace bootstrap all require herdr. See [Install the herdr half](#install-the-herdr-half-companion-plugin) below.
5
5
 
6
6
  **pier** = **pi** × h**erdr** — a [pi](https://pi.dev/) extension that fuses coding agent sessions with the [herdr](https://herdr.dev/) pane/tab orchestrator.
7
7
 
8
- `pi-pier` fills two gaps pi deliberately leaves open — the **todo list loop** and **interactive subagents** — using herdr panes/tabs as the visual and interaction substrate. It degrades gracefully outside herdr.
8
+ `pi-pier` fills two gaps pi deliberately leaves open — the **todo list loop** and **interactive subagents** — using herdr panes/tabs as the visual and interaction substrate. Outside herdr it loads only the todo loop.
9
9
 
10
10
  ## Install (this pi half)
11
11
 
@@ -56,13 +56,43 @@ The installer also verifies node / pi / herdr versions, probes local paths, and
56
56
 
57
57
  ## Scope & degradation
58
58
 
59
- Outside a herdr environment the extension auto-degrades: herdr reporting costs nothing, `subagent` / terminal tools return "requires a herdr-managed pane" hints, while `todo_write` / `ask_user_question` keep working (session JSONL as authority, no herdr dependency).
59
+ This extension mutates the pi session even outside herdr. After `pi install npm:pi-pier`:
60
+
61
+ | Surface | Bare `pi` (no `HERDR_ENV`) | Inside herdr |
62
+ |---|---|---|
63
+ | `todo_write`, `/todos`, widget, anti-freeze, stop reminder | live | live |
64
+ | `ask_user_question` | live (TUI input) | live + blocked marker |
65
+ | Hidden inject (`before_agent_start` todo-read; settle reminder) | live | live |
66
+ | `subagent`, `terminal` | **not registered** | live |
67
+ | `/locks`, write-lock on `write`/`edit` | not installed | live |
68
+ | slim-frame overlay, pane title, pipe, isolate worktree | off | live |
69
+ | `setActiveTools` role visible layer | off | on (herdr master) |
70
+
71
+ Session JSONL custom types (`pi-herdr.todo-edit`, `.subs`, `.terminals`, `.todo-read`, `.todo-reminder`, …) persist across `/resume` even without herdr.
72
+
73
+ ## Plugin conflicts
74
+
75
+ pi overwrites tools/commands by name; event listeners stack.
76
+
77
+ **Do not install alongside** (same-name overwrite → wrong handler, split JSONL):
78
+
79
+ - [`@nguyenquangthai/pi-todo`](https://pi.dev/packages/@nguyenquangthai/pi-todo) — `todo_write` + overlay
80
+ - [`@josephyoung/pi-ask-user-question`](https://pi.dev/packages/@josephyoung/pi-ask-user-question) — `ask_user_question`
81
+ - [`pi-herdr-subagents`](https://pi.dev/packages/pi-herdr-subagents) — `subagent` + herdr panes
82
+
83
+ **Soft conflict** (two orchestrators / two injectors — the agent “talks to itself”):
84
+
85
+ - [`@tintinweb/pi-subagents`](https://pi.dev/packages/@tintinweb/pi-subagents) (`Agent`)
86
+ - [`@minhduydev/pi-subagents`](https://pi.dev/packages/@minhduydev/pi-subagents) (`task`)
87
+ - any other extension that calls `setActiveTools`, `ui.custom` overlay, `before_agent_start`, or `sendUserMessage`/`sendMessage` followUp
88
+
89
+ **Designed coexistence:** herdr’s official `herdr:pi` reporter. Keep it. pier emits `herdr:blocked` so that plugin remains lifecycle authority.
60
90
 
61
91
  ## Requirements
62
92
 
63
93
  - Node ≥ 22
64
94
  - pi ≥ 0.84 (`@earendil-works/pi-coding-agent`)
65
- - **herdr ≥ 0.8.0** — required for subagents / pane integration / notifications (see the IMPORTANT note above); without it only `todo_write` / `ask_user_question` work
95
+ - **herdr ≥ 0.8.0** — required for subagents / pane integration / notifications (see the IMPORTANT note above); without it only the todo loop and `ask_user_question` load
66
96
 
67
97
  ## Development
68
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-pier",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "pier: pi extension that fuses pi agent sessions with the herdr pane/tab orchestrator — todo_write loop closure + interactive subagents in isolated panes",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,7 +11,6 @@ import { randomUUID } from 'node:crypto';
11
11
  import { basename, join, dirname } from 'node:path';
12
12
  import type { PiSurface } from '../pi-surface.ts';
13
13
  import type { HerdrClientLike } from '../herdr-client.ts';
14
- import type { TodosService } from '../todos-service.ts';
15
14
  import { Semaphore, SUBS_CUSTOM_TYPE, agoText, buildAliveNotice, buildBlockedGateNotice, buildIsolatePreamble, classifyWorktreeZone, foldSubsRegistry, formatSubagentResult, isAlive, isPathUnder, makeProgressUpdate, makeRegistry, planIsolateWorktree, tabNameForTask, type AliveProbe, type SubEntry } from '../subagent-core.ts';
16
15
  import { applyReportedSessionFile, appendHistory, preferredHistoryFile, inheritOutcome, latestGeneration, readHistory, type HistoryEntry } from '../history-store.ts';
17
16
  import { runtimePolicy } from '../runtime-policy.ts';
@@ -30,7 +29,6 @@ import { promisify } from 'node:util';
30
29
  const statAsync = promisify(statCb);
31
30
  import { pipeNameFor, pipeRequestTo } from '../pipe-channel.ts';
32
31
  import { composeForRole } from '../manifest-compose.ts';
33
- import { TODO_REMINDER_CUSTOM_TYPE, planStopTodoReminder, todoReminderGraceMs } from '../todo-reminder-core.ts';
34
32
  import type { TerminalStateSlot } from './terminal.ts';
35
33
  import { createGitIo } from '../subagent-git-io.ts';
36
34
  import { createSessionIo } from '../subagent-session-io.ts';
@@ -54,7 +52,6 @@ interface SubagentDeps {
54
52
  sessionRoot: Context;
55
53
  port: SubagentPortBox;
56
54
  getSessionId: () => string;
57
- getBlockedDepth: () => number;
58
55
  reconcileOnSettlement: (description: string, outcome: 'settled' | 'failed') => string[];
59
56
  withReconcileNotes: (base: string, notes: readonly string[]) => string;
60
57
  claimSettleNotice: (key: string) => boolean;
@@ -67,7 +64,6 @@ interface SubagentDeps {
67
64
  noticePending?: () => ReadonlySet<string>;
68
65
  /** Terminal-family GC exemption (live terminal panes are not collected). */
69
66
  terminalState: TerminalStateSlot;
70
- todos: TodosService;
71
67
  }
72
68
 
73
69
  const SUBAGENT_DESCRIPTION = [
@@ -103,14 +99,10 @@ function agentRootDir(): string {
103
99
  export default function subagentPlugin(ctx: Context): void {
104
100
  const surface = ctx.get('pi-herdr.surface') as PiSurface<object>;
105
101
  const d = ctx.get('pi-herdr.subagent-deps') as SubagentDeps;
106
- const { client, env, sessionRoot, port, terminalState, todos } = d;
102
+ const { client, env, sessionRoot, port, terminalState } = d;
107
103
  const pi = surface.raw as {
108
104
  appendEntry?: (customType: string, data: unknown) => void;
109
105
  sendUserMessage?: (content: string, opts?: { deliverAs?: string }) => Promise<void>;
110
- sendMessage?: (
111
- message: { customType: string; content: string; display?: boolean; details?: Record<string, unknown> },
112
- opts?: { deliverAs?: string; triggerTurn?: boolean },
113
- ) => Promise<void>;
114
106
  };
115
107
  const scoped = surface.forModule(import.meta.url);
116
108
 
@@ -244,68 +236,6 @@ export default function subagentPlugin(ctx: Context): void {
244
236
  return { content: [{ type: 'text', text: notice }] };
245
237
  });
246
238
 
247
- /* ── D41 safe reminders for unfinished todos ─────────────────────
248
- * 01a040cc showed that a user-role reminder overrode model judgment and executed work reserved
249
- * for the user. Use a distinguishable custom message, request reconciliation rather than action,
250
- * and delay delivery so any new agent start cancels it. Preserve the ESC guard from 01a03bf0 to
251
- * avoid wake-up storms after explicit interruption. */
252
-
253
- let todoReminders = 0;
254
- let lastAssistantStopReason: string | null = null;
255
- let todoReminderTimer: NodeJS.Timeout | null = null;
256
-
257
- function cancelTodoReminder(): void {
258
- if (todoReminderTimer !== null) {
259
- clearTimeout(todoReminderTimer);
260
- todoReminderTimer = null;
261
- }
262
- }
263
-
264
- scoped.on('turn_end', async (event: unknown) => {
265
- if (event === null || typeof event !== 'object' || !('message' in event)) return;
266
- const msg = (event as { message: unknown }).message; // Safe after the property guard above.
267
- if (msg === null || typeof msg !== 'object') return;
268
- const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
269
- if (role === 'assistant' && typeof stopReason === 'string') {
270
- lastAssistantStopReason = stopReason;
271
- }
272
- });
273
-
274
- // B3 cancels the reminder when any source resumes work during the grace window.
275
- scoped.on('agent_start', () => cancelTodoReminder());
276
- scoped.on('session_shutdown', () => cancelTodoReminder());
277
-
278
- scoped.on('agent_settled', async () => {
279
- cancelTodoReminder(); // Cancel any timer left from the previous settlement.
280
- const plan = planStopTodoReminder({
281
- lastStopReason: lastAssistantStopReason,
282
- reminders: todoReminders,
283
- runningSubs: pollers.size,
284
- blockedDepth: d.getBlockedDepth(),
285
- items: todos.items,
286
- });
287
- if (!plan.due || plan.content == null) return;
288
- const content = plan.content;
289
- todoReminderTimer = setTimeout(() => {
290
- todoReminderTimer = null;
291
- void (async () => {
292
- // Without sendMessage, skip the reminder rather than impersonating the user channel.
293
- const send = pi.sendMessage;
294
- if (typeof send !== 'function') return;
295
- try {
296
- await send(
297
- { customType: TODO_REMINDER_CUSTOM_TYPE, content, display: true },
298
- { deliverAs: 'followUp', triggerTurn: true },
299
- );
300
- todoReminders += 1; // Count only delivered reminders so cancellation and failure do not consume the cap.
301
- } catch {
302
- /* Delivery failure is non-fatal. */
303
- }
304
- })();
305
- }, todoReminderGraceMs());
306
- todoReminderTimer.unref?.(); // Do not keep tests or headless processes alive for a reminder.
307
- });
308
-
309
239
  function histFile(cwd: string): string {
310
240
  return preferredHistoryFile(agentRootDir(), cwd);
311
241
  }
package/src/core/todo.ts CHANGED
@@ -3,15 +3,16 @@
3
3
  *
4
4
  * A Cordis plugin keeps the master surface hot-swappable while allowing workers to mount the
5
5
  * one-shot subset directly. Injected services preserve the D79 registration boundary and keep
6
- * session-owned todo state in index. This module owns the tool, command, widget, and read hook;
7
- * reconciliation, mirroring, and agent reporting stay with the session state layer to avoid a
8
- * dependency cycle.
6
+ * session-owned todo state in index. This module owns the tool, command, widget, read hook,
7
+ * and master stop reminder; reconciliation, mirroring, and agent reporting stay with the
8
+ * session state layer to avoid a dependency cycle.
9
9
  */
10
10
  import { Context } from '@deepseek-ai/cordis';
11
11
  import { Type } from 'typebox';
12
12
  import type { PiSurface } from '../pi-surface.ts';
13
13
  import type { TodosService } from '../todos-service.ts';
14
14
  import { planTodoReadHook } from '../todo-read-hook.ts';
15
+ import { TODO_REMINDER_CUSTOM_TYPE, planStopTodoReminder, todoReminderGraceMs } from '../todo-reminder-core.ts';
15
16
  import { makeProgressUpdate } from '../subagent-core.ts';
16
17
  import { TODO_DETAILS_KEY, TODO_TOOL_NAME, formatTodoConfirmation, type TodoItem } from '../vocab.ts';
17
18
  import { formatAge, isArchived } from '../stale-core.ts';
@@ -40,6 +41,11 @@ interface TodoDeps {
40
41
  mirrorTodos: () => void;
41
42
  appendEntry: (customType: string, data: unknown) => void;
42
43
  state: TodoUiSlot;
44
+ /** Master-only unfinished-todo reminder; omitted on worker panes. */
45
+ stopReminder?: {
46
+ getBlockedDepth: () => number;
47
+ getRunningSubs: () => number;
48
+ };
43
49
  }
44
50
 
45
51
  const TOOL_DESCRIPTION = [
@@ -89,7 +95,7 @@ export function widgetLines(items: readonly TodoItem[], opts?: { archivedAgeMs?:
89
95
 
90
96
  export default function todoPlugin(ctx: Context): void {
91
97
  const surface = ctx.get('pi-herdr.surface') as PiSurface<object>;
92
- const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state } =
98
+ const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state, stopReminder } =
93
99
  ctx.get('pi-herdr.todo-deps') as TodoDeps;
94
100
  const scoped = surface.forModule(import.meta.url);
95
101
 
@@ -178,6 +184,64 @@ export default function todoPlugin(ctx: Context): void {
178
184
  };
179
185
  });
180
186
 
187
+ if (stopReminder) {
188
+ const pi = surface.raw as {
189
+ sendMessage?: (
190
+ message: { customType: string; content: string; display?: boolean; details?: Record<string, unknown> },
191
+ opts?: { deliverAs?: string; triggerTurn?: boolean },
192
+ ) => Promise<void>;
193
+ };
194
+ let todoReminders = 0;
195
+ let lastAssistantStopReason: string | null = null;
196
+ let todoReminderTimer: NodeJS.Timeout | null = null;
197
+ function cancelTodoReminder(): void {
198
+ if (todoReminderTimer !== null) {
199
+ clearTimeout(todoReminderTimer);
200
+ todoReminderTimer = null;
201
+ }
202
+ }
203
+ scoped.on('turn_end', async (event: unknown) => {
204
+ if (event === null || typeof event !== 'object' || !('message' in event)) return;
205
+ const msg = (event as { message: unknown }).message;
206
+ if (msg === null || typeof msg !== 'object') return;
207
+ const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
208
+ if (role === 'assistant' && typeof stopReason === 'string') {
209
+ lastAssistantStopReason = stopReason;
210
+ }
211
+ });
212
+ scoped.on('agent_start', () => cancelTodoReminder());
213
+ scoped.on('session_shutdown', () => cancelTodoReminder());
214
+ scoped.on('agent_settled', async () => {
215
+ cancelTodoReminder();
216
+ const plan = planStopTodoReminder({
217
+ lastStopReason: lastAssistantStopReason,
218
+ reminders: todoReminders,
219
+ runningSubs: stopReminder.getRunningSubs(),
220
+ blockedDepth: stopReminder.getBlockedDepth(),
221
+ items: todos.items,
222
+ });
223
+ if (!plan.due || plan.content == null) return;
224
+ const content = plan.content;
225
+ todoReminderTimer = setTimeout(() => {
226
+ todoReminderTimer = null;
227
+ void (async () => {
228
+ const send = pi.sendMessage;
229
+ if (typeof send !== 'function') return;
230
+ try {
231
+ await send(
232
+ { customType: TODO_REMINDER_CUSTOM_TYPE, content, display: true },
233
+ { deliverAs: 'followUp', triggerTurn: true },
234
+ );
235
+ todoReminders += 1;
236
+ } catch {
237
+ /* Delivery failure is non-fatal. */
238
+ }
239
+ })();
240
+ }, todoReminderGraceMs());
241
+ todoReminderTimer.unref?.();
242
+ });
243
+ }
244
+
181
245
  /* ── todo_write tool ───────────────────────────────────────────── */
182
246
  scoped.registerTool({
183
247
  name: TODO_TOOL_NAME,
@@ -89,6 +89,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
89
89
  (m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
90
90
  },
91
91
  state: m.todoUi,
92
+ stopReminder: {
93
+ getBlockedDepth: m.getBlockedDepth,
94
+ getRunningSubs: () => m.port.current?.listRunningSubs().length ?? 0,
95
+ },
92
96
  });
93
97
  await loadEntry(sessionRoot, useLoader, './core/todo.ts', todoPlugin);
94
98
 
@@ -101,12 +105,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
101
105
  deliverNotice: m.deliverNotice,
102
106
  noticePending: m.noticePending,
103
107
  getSessionId: m.getSessionId,
104
- getBlockedDepth: m.getBlockedDepth,
105
108
  reconcileOnSettlement: m.reconcileOnSettlement,
106
109
  withReconcileNotes: m.withReconcileNotes,
107
110
  claimSettleNotice: m.claimSettleNotice,
108
111
  terminalState: terminalDeps.state,
109
- todos: m.todos,
110
112
  });
111
113
  await loadEntry(sessionRoot, useLoader, './core/subagent.ts', subagentPlugin);
112
114
 
@@ -10,14 +10,18 @@ import { detectHerdrEnv } from './herdr-client.ts';
10
10
  export interface IndexMode {
11
11
  /** PI_HERDR_SUBAGENT=1 → worker pane (no subagent tools, no cordis loader). */
12
12
  readonly isSubagent: boolean;
13
- /** herdr pane that is not a worker → apply builtin master manifest. */
13
+ /** HERDR_ENV + socket + pane are present. */
14
+ readonly hasHerdr: boolean;
15
+ /** herdr pane that is not a worker → apply builtin master manifest and mount workbench plugins. */
14
16
  readonly composeMaster: boolean;
15
17
  }
16
18
 
17
19
  export function planIndexMode(env: NodeJS.ProcessEnv = process.env): IndexMode {
18
20
  const isSubagent = env.PI_HERDR_SUBAGENT === '1';
21
+ const hasHerdr = Boolean(detectHerdrEnv(env));
19
22
  return {
20
23
  isSubagent,
21
- composeMaster: Boolean(detectHerdrEnv(env)) && !isSubagent,
24
+ hasHerdr,
25
+ composeMaster: hasHerdr && !isSubagent,
22
26
  };
23
27
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Worker-process todo plugin mount (bare cordis root, no loader/hmr).
2
+ * Todo-only plugin mount (bare cordis root, no loader/hmr).
3
3
  *
4
- * Why: short-lived worker panes only need the todo coordination plugin.
5
- * Dynamically imported from the worker branch so bootstrap/loader stay out.
4
+ * Used by worker panes and by standalone pi outside herdr, so bootstrap/subagent/terminal
5
+ * stay out of those processes.
6
6
  */
7
7
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
8
8
  import { Context } from '@deepseek-ai/cordis';
@@ -11,14 +11,19 @@ import todoPlugin from './core/todo.ts';
11
11
  import type { TodosService } from './todos-service.ts';
12
12
  import type { TodoUiSlot } from './core/todo.ts';
13
13
 
14
- export interface WorkerPluginMount {
14
+ export interface TodoOnlyMount {
15
15
  pi: ExtensionAPI;
16
16
  todos: TodosService;
17
17
  todoUi: TodoUiSlot;
18
18
  mirrorTodos: () => void;
19
+ /** Master-only stop reminder; omit on worker panes. */
20
+ stopReminder?: {
21
+ getBlockedDepth: () => number;
22
+ getRunningSubs: () => number;
23
+ };
19
24
  }
20
25
 
21
- export async function mountWorkerTodo(m: WorkerPluginMount): Promise<void> {
26
+ export async function mountTodoOnly(m: TodoOnlyMount): Promise<void> {
22
27
  const workerRoot = new Context();
23
28
  const workerSurface = new PiSurface(m.pi as unknown as object);
24
29
  workerRoot.provide('pi-herdr.surface', workerSurface);
@@ -31,6 +36,7 @@ export async function mountWorkerTodo(m: WorkerPluginMount): Promise<void> {
31
36
  (m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
32
37
  },
33
38
  state: m.todoUi,
39
+ ...(m.stopReminder ? { stopReminder: m.stopReminder } : {}),
34
40
  });
35
41
  await workerRoot.plugin(todoPlugin);
36
42
  m.pi.on('session_shutdown', () => {
package/src/index.ts CHANGED
@@ -7,10 +7,7 @@
7
7
  * Capabilities:
8
8
  * - `todo_write`: replace the complete todo list; the current pi session JSONL is authoritative,
9
9
  * and branches automatically roll back in DSH-aligned semantics.
10
- * - `subagent`: foreground/background delegation plus resume/list/send/interrupt actions.
11
- * Each child pane is an interactive pi TUI with an independent session/context that humans
12
- * can enter directly; the controller uses herdr for state-gated injection (idle only),
13
- * waiting (agent.wait), and JSONL result reads.
10
+ * - `subagent` / `terminal`: herdr-master only. Bare pi does not register them.
14
11
  * - `/todos`, TUI widget, and herdr title projection with graceful degradation without herdr.
15
12
  *
16
13
  * pi 0.84.2 contract (validated):
@@ -231,11 +228,13 @@ export default async function (pi: ExtensionAPI) {
231
228
  }
232
229
  });
233
230
 
234
- installWriteLocks(pi, {
235
- client,
236
- env,
237
- hard: process.env[WRITE_LOCK_ENV] === '1',
238
- });
231
+ if (env) {
232
+ installWriteLocks(pi, {
233
+ client,
234
+ env,
235
+ hard: process.env[WRITE_LOCK_ENV] === '1',
236
+ });
237
+ }
239
238
 
240
239
  /* ── todo family slot (core/todo.ts fills the plugin hook; widget rendering moved with the family) ── */
241
240
  const todoUi: { renderWidget: (ctx: unknown) => void } = {
@@ -560,8 +559,10 @@ export default async function (pi: ExtensionAPI) {
560
559
  }
561
560
  });
562
561
 
563
- /* subagent tools are master-only (depth 1). C3: worker never loads bootstrap. */
564
- if (!isSubagent) {
562
+ /* Workbench plugins (subagent/terminal + cordis) are herdr-master only.
563
+ * Bare pi and worker panes mount todo only so unused tools never appear.
564
+ * Dynamic import: worker/bare-pi must never load bootstrap/subagent-scope (C3). */
565
+ if (mode.composeMaster) {
565
566
  const { mountMasterPlugins } = await import('./index-master.ts');
566
567
  await mountMasterPlugins({
567
568
  pi,
@@ -582,7 +583,20 @@ export default async function (pi: ExtensionAPI) {
582
583
  claimSettleNotice,
583
584
  });
584
585
  } else {
585
- const { mountWorkerTodo } = await import('./index-worker.ts');
586
- await mountWorkerTodo({ pi, todos, todoUi, mirrorTodos });
586
+ const { mountTodoOnly } = await import('./index-worker.ts');
587
+ await mountTodoOnly({
588
+ pi,
589
+ todos,
590
+ todoUi,
591
+ mirrorTodos,
592
+ ...(isSubagent
593
+ ? {}
594
+ : {
595
+ stopReminder: {
596
+ getBlockedDepth: () => blockedDepth,
597
+ getRunningSubs: () => subagentPort.current?.listRunningSubs().length ?? 0,
598
+ },
599
+ }),
600
+ });
587
601
  }
588
602
  }