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 +34 -4
- package/package.json +1 -1
- package/src/core/subagent.ts +1 -71
- package/src/core/todo.ts +68 -4
- package/src/index-master.ts +4 -2
- package/src/index-runtime.ts +6 -2
- package/src/index-worker.ts +11 -5
- package/src/index.ts +27 -13
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|
package/src/core/subagent.ts
CHANGED
|
@@ -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
|
|
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,
|
|
7
|
-
* reconciliation, mirroring, and agent reporting stay with the
|
|
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,
|
package/src/index-master.ts
CHANGED
|
@@ -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
|
|
package/src/index-runtime.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
24
|
+
hasHerdr,
|
|
25
|
+
composeMaster: hasHerdr && !isSubagent,
|
|
22
26
|
};
|
|
23
27
|
}
|
package/src/index-worker.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Todo-only plugin mount (bare cordis root, no loader/hmr).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
/*
|
|
564
|
-
|
|
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 {
|
|
586
|
-
await
|
|
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
|
}
|