pi-pier 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/schemas/role-manifest.schema.json +1 -2
  3. package/src/bootstrap.ts +18 -22
  4. package/src/core/grid-shape.ts +13 -14
  5. package/src/core/subagent.ts +325 -857
  6. package/src/core/terminal.ts +25 -28
  7. package/src/core/todo.ts +47 -53
  8. package/src/gc-core.ts +15 -15
  9. package/src/git-adapter.ts +104 -0
  10. package/src/herdr-client.ts +76 -68
  11. package/src/history-store.ts +70 -36
  12. package/src/index-locks.ts +105 -0
  13. package/src/index-master.ts +113 -0
  14. package/src/index-notices.ts +43 -0
  15. package/src/index-pipe.ts +73 -0
  16. package/src/index-runtime.ts +23 -0
  17. package/src/index-worker.ts +39 -0
  18. package/src/index.ts +134 -386
  19. package/src/ledger.ts +15 -17
  20. package/src/lock-core.ts +15 -29
  21. package/src/manifest-compose.ts +9 -23
  22. package/src/notice-buffer.ts +9 -14
  23. package/src/pane-title.ts +24 -24
  24. package/src/pi-surface.ts +22 -29
  25. package/src/pipe-channel.ts +67 -34
  26. package/src/platform-paths.ts +72 -0
  27. package/src/progress-core.ts +26 -24
  28. package/src/reconcile-core.ts +25 -24
  29. package/src/role-loader.ts +32 -35
  30. package/src/role-manifest.ts +24 -28
  31. package/src/roles/master.json +2 -2
  32. package/src/roles/worker-default.json +3 -3
  33. package/src/runtime-policy.ts +66 -0
  34. package/src/session-tail.ts +46 -45
  35. package/src/settle-wake-core.ts +19 -18
  36. package/src/slim-frame.ts +45 -44
  37. package/src/stale-core.ts +23 -21
  38. package/src/storage-layout.ts +80 -0
  39. package/src/subagent-core.ts +151 -94
  40. package/src/subagent-gc.ts +237 -0
  41. package/src/subagent-git-io.ts +80 -0
  42. package/src/subagent-launch.ts +110 -0
  43. package/src/subagent-poll-loop.ts +285 -0
  44. package/src/subagent-poller.ts +90 -0
  45. package/src/subagent-port.ts +27 -0
  46. package/src/subagent-scope.ts +4 -4
  47. package/src/subagent-session-io.ts +143 -0
  48. package/src/subagent-spawn.ts +156 -0
  49. package/src/terminal-core.ts +56 -54
  50. package/src/todo-core.ts +27 -64
  51. package/src/todo-read-hook.ts +28 -24
  52. package/src/todo-reminder-core.ts +89 -0
  53. package/src/todos-service.ts +36 -25
  54. package/src/tool-gate.ts +7 -28
  55. package/src/vocab.ts +18 -19
package/src/index.ts CHANGED
@@ -1,39 +1,36 @@
1
1
  /**
2
- * pi-herdr 扩展入口(v1.1:todo 闭环 + 交互式子代理,DESIGN.md §12 方案 C)。
2
+ * pi-herdr extension entry point (v1.1: closed-loop todos + interactive subagents,
3
+ * DESIGN.md §12 option C).
3
4
  *
4
- * 安装:pi package(package.json 的 pi.extensions)或 ~/.pi/agent/extensions/。
5
+ * Installation: `pi package` (package.json's pi.extensions) or ~/.pi/agent/extensions/.
5
6
  *
6
- * 能力:
7
- * - 工具 `todo_write`:全量替换式 todo 列表(权威 = 本 pi 会话 JSONL,
8
- * 分支自动回滚;语义对齐 DSH)。
9
- * - 工具 `subagent`:前台/后台委派。子 pane = 交互式 pi TUI(独立会话、
10
- * 独立上下文),人类可随时进入直接对话;主控经 herdr 通道派活
11
- * (状态门:仅 idle 注入)、等待(agent.wait)、取结果(子会话 JSONL)。
12
- * - 工具 `list_agents` / `send_message`(followUp 队列语义)/ `interrupt_agent`(esc)。
13
- * - 命令 `/todos`、TUI widget、herdr 标题投影(无 herdr 环境优雅降级)。
7
+ * Capabilities:
8
+ * - `todo_write`: replace the complete todo list; the current pi session JSONL is authoritative,
9
+ * and branches automatically roll back in DSH-aligned semantics.
10
+ * - `subagent`: foreground/background delegation. Each child pane is an interactive pi TUI
11
+ * with an independent session/context that humans can enter directly; the controller uses
12
+ * herdr for state-gated injection (idle only), waiting (agent.wait), and JSONL result reads.
13
+ * - `list_agents` / `send_message` (followUp queue semantics) / `interrupt_agent` (esc).
14
+ * - `/todos`, TUI widget, and herdr title projection with graceful degradation without herdr.
14
15
  *
15
- * ⚠️ pi 0.84.2 契约(实测):
16
- * - onUpdate 必须是 AgentToolResult 形状(字符串会让 TUI 崩溃退出 pi);
17
- * - 工具结果 details 随会话 JSONL 持久化、getBranch() 重放实现分支回滚。
16
+ * pi 0.84.2 contract (validated):
17
+ * - onUpdate must have AgentToolResult shape; a string causes the TUI to crash and exit pi;
18
+ * - tool-result details persist in session JSONL, and getBranch() replay implements branch rollback.
18
19
  */
19
20
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
20
21
  import { Type } from 'typebox';
21
- import {
22
- formatSettlementNotice,
23
- } from './vocab.ts';
24
- import { collapseNotices } from './notice-buffer.ts';
22
+ import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
25
23
  import {
26
24
  TODO_EDIT_CUSTOM_TYPE,
27
25
  currentActivity,
28
26
  } from './todo-core.ts';
29
- import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
30
27
 
31
- /** 档2:worker 会话内记录运行所依据的合成 manifest(D38 同构,分支可回放)。 */
28
+ /** Tier 2: record the synthesized manifest that governed a worker session (D38-compatible and branch-replayable). */
32
29
  const ROLE_MANIFEST_CUSTOM_TYPE = 'pi-herdr.role-manifest';
33
30
 
34
- import { createHerdrClient, type HerdrClientLike, type HerdrAgentState } from './herdr-client.ts';
35
- // subagent 族已迁 core/subagent.ts(loader entry,D78/D81);subagent-core/history-store/
36
- // session-tail/gc-core 导入随迁(index 仅留 common 面用到的 lastAssistantText/readSessionFile)。
31
+ import { createHerdrClient } from './herdr-client.ts';
32
+ // The subagent family moved to core/subagent.ts (loader entry, D78/D81); its history-store,
33
+ // session-tail, and gc-core imports moved with it, leaving index.ts only the common readers.
37
34
  import { lastAssistantText, readSessionFile } from './session-tail.ts';
38
35
  import { fileURLToPath } from 'node:url';
39
36
  import { TodosService } from './todos-service.ts';
@@ -44,32 +41,29 @@ import {
44
41
  planToolBadge,
45
42
  progressOf,
46
43
  } from './progress-core.ts';
47
- import {
48
- WRITE_LOCK_ENV,
49
- acquireTokensFor,
50
- isLockTokenKey,
51
- parseLockTokenValue,
52
- planWriteGuard,
53
- releaseTokensFor,
54
- type LockAgentView,
55
- } from './lock-core.ts';
44
+ import { WRITE_LOCK_ENV } from './lock-core.ts';
56
45
  import { ABORT_STOP_REASON, planSettleWake } from './settle-wake-core.ts';
57
46
  import { composeForRole } from './manifest-compose.ts';
58
47
  import { parseRuntimeManifest, planActiveTools, planToolGate, type RuntimeRoleManifest } from './tool-gate.ts';
59
- import { detectHerdrEnv } from './herdr-client.ts';
48
+
60
49
  import { formatPaneTitle } from './pane-title.ts';
61
50
  import { registerSlimFrame, updateSlimFrame } from './slim-frame.ts';
51
+ import { planIndexMode } from './index-runtime.ts';
52
+ import { emptySubagentPortBox } from './subagent-port.ts';
53
+ import { createNoticeBuffer } from './index-notices.ts';
54
+ import { handlePipeRequest } from './index-pipe.ts';
55
+ import { installWriteLocks } from './index-locks.ts';
62
56
 
63
57
  /**
64
- * WS-D7:master pane 自应用 manifest——与 subagent 同一条强制链
65
- * (闸门 + 可见层 + 徽标 + 会话记录),档案基线 = 自研核心 + 检视/执行工具。
66
- * 判别:herdr pane(HERDR_ENV=1)且非 subagent 且未被显式下发 manifest。
67
- * 独立 pi 会话(无 herdr env)不命中 → 保持全量(向后兼容)。
68
- * master.json 畸形 → fail-open 到无角色态(与 env 畸形同语义)。
58
+ * WS-D7: apply the master-pane manifest to itself through the same mandatory chain as subagents
59
+ * (gate, visible layer, badge, and session record); the baseline includes our core plus inspect/execute tools.
60
+ * Match only a herdr pane (HERDR_ENV=1) that is not a subagent and has no explicitly supplied manifest.
61
+ * Standalone pi sessions without herdr remain full-featured for backward compatibility.
62
+ * A malformed master.json fails open to no-role state, matching malformed env semantics.
69
63
  */
70
64
  function composeMasterRuntime(): RuntimeRoleManifest | null {
71
65
  try {
72
- // v1.1:内置名直读——workspace 放 master.json 诱饵不影响自应用(保留名检查只对 spawn 响亮报错)
66
+ // v1.1: read the built-in name directly so a workspace master.json decoy cannot affect self-application; the reserved-name check remains for loud spawn errors.
73
67
  const { role, manifest } = composeForRole('master', [], { loadRoleOpts: { builtinDirect: true } });
74
68
  return {
75
69
  role: role.role,
@@ -84,24 +78,24 @@ function composeMasterRuntime(): RuntimeRoleManifest | null {
84
78
  return null;
85
79
  }
86
80
  }
87
- // C3:cordis 只进 master 进程——subagent-scope(内含 @deepseek-ai/cordis)
88
- // 在 master 分支内动态 import,worker 进程永不加载该模块。
89
- // subagent 族常量/助手已随迁 core/subagent.ts。
81
+ // C3: cordis belongs only in the master process; subagent-scope includes @deepseek-ai/cordis.
82
+ // Dynamically import it on the master branch so worker processes never load the module.
83
+ // Subagent constants/helpers moved with the family to core/subagent.ts.
90
84
 
91
85
  export default async function (pi: ExtensionAPI) {
92
- // 档2(Week4-5):worker 运行时 manifest(env 下发;显式 > master 自应用 > 无)
93
- // WS-D7:herdr 内非 subagent pane = master → 自应用 master 档案(同一条强制链)
86
+ const mode = planIndexMode();
87
+ const isSubagent = mode.isSubagent;
94
88
  const runtimeManifest =
95
89
  parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
96
- (detectHerdrEnv() && process.env.PI_HERDR_SUBAGENT !== '1' ? composeMasterRuntime() : null);
97
- const todos = new TodosService(TodosService.configFromRuntime(runtimeManifest, process.env.PI_HERDR_SUBAGENT === '1')); // D75 阶段 3
90
+ (mode.composeMaster ? composeMasterRuntime() : null);
91
+ const todos = new TodosService(TodosService.configFromRuntime(runtimeManifest, isSubagent));
98
92
  const { client, env } = createHerdrClient();
99
93
 
100
94
  let sessionId: string = process.env.PI_SESSION_FILE ?? process.env.PI_SESSION_ID ?? '';
101
- /** M16:todo 完成时间戳序列(速率估算原料;随 todo.completed 事件追加)。 */
95
+ /** M16: completion timestamps used as rate-estimation input, appended with todo.completed events. */
102
96
  const completedStamps: number[] = [];
103
97
 
104
- /* ── 状态重建(分支正确性:取分支路径上最后一次 todo_write 快照)── */
98
+ /* ── State reconstruction (branch correctness comes from the last todo_write snapshot on the branch) ── */
105
99
 
106
100
  function rebuildFromBranch(ctx: unknown): void {
107
101
  try {
@@ -109,13 +103,13 @@ export default async function (pi: ExtensionAPI) {
109
103
  ?.sessionManager?.getBranch?.() ?? [];
110
104
  todos.rebuild(entries);
111
105
  } catch {
112
- // 重建失败不影响主流程;下次 todo_write 会重新锚定。
106
+ // A reconstruction failure must not disrupt the main flow; the next todo_write re-anchors state.
113
107
  }
114
108
  }
115
109
 
116
- /* ── herdr 上报(Noop 时零成本;失败静默,绝不影响 pi 主流程) ── */
110
+ /* ── herdr reporting (zero cost for Noop; silent failures never affect pi's main flow) ── */
117
111
 
118
- /** 档2:worker 的 role 徽标(env 下发 manifest 解析出;idle 态粘性显示)。 */
112
+ /** Tier 2: worker role badge parsed from the env manifest and kept visible while idle. */
119
113
  let roleBadge: string | null = null;
120
114
 
121
115
  function reportAgent(state: 'working' | 'idle' | 'blocked', activity: string | null): void {
@@ -129,7 +123,7 @@ export default async function (pi: ExtensionAPI) {
129
123
  client.reportAgentSession(path).catch(() => {});
130
124
  }
131
125
 
132
- /** session 标识:sessionManager 权威(print/rpc 模式下 env 可能未设),env 兜底。 */
126
+ /** Session identity: sessionManager is authoritative (env may be unset in print/RPC mode), with env as fallback. */
133
127
  function resolveSessionId(ctx: unknown): string {
134
128
  try {
135
129
  const sm = (ctx as {
@@ -153,16 +147,16 @@ export default async function (pi: ExtensionAPI) {
153
147
  function mirrorTodos(): void {
154
148
  if (!client.available) return;
155
149
  const label = sessionId || (env ? `pane:${env.paneId}` : '');
156
- // M16:进度徽标(保守 N/M + 置信 ETA;估算不可信回退纯计数)
150
+ // M16: progress badge (conservative N/M plus confidence ETA; fall back to a plain count when estimates are unreliable).
157
151
  const p = progressOf(todos.items);
158
152
  const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
159
153
  const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
160
- // 反冻结:lastWriteAt 随行 → 标题侧 archived 判定(pane-title)
154
+ // Unfreeze by carrying lastWriteAt into the title-side archived check (pane-title).
161
155
  const title = formatPaneTitle(todos.items, null, {
162
156
  progressSuffix: suffix,
163
157
  lastWriteAt: todos.lastWriteAt,
164
158
  });
165
- // D97:窄格静帧与 pane 标题同参同源(thinking token 碰不到 → 静帧即冻结)
159
+ // D97: narrow-frame static state shares inputs with the pane title; thinking tokens cannot reach it, so a static frame would freeze.
166
160
  updateSlimFrame(title);
167
161
  client.reportMetadata({
168
162
  session: label,
@@ -172,7 +166,7 @@ export default async function (pi: ExtensionAPI) {
172
166
  }).catch(() => {});
173
167
  }
174
168
 
175
- /* ── M17:结算自动对账(纯规划器 + D38 权威路径;幂等,双路径双跑无害) ── */
169
+ /* ── M17: automatic settlement reconciliation (pure planner + D38 authority; idempotent and safe to run through both paths) ── */
176
170
 
177
171
  function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
178
172
  if (!description) return [];
@@ -185,7 +179,7 @@ export default async function (pi: ExtensionAPI) {
185
179
  { version: 1, edits: plan.edits, ts: Date.now() },
186
180
  );
187
181
  } catch {
188
- /* 持久化尽力而为(内存态仍推进) */
182
+ /* Best effort persistence; in-memory state still advances. */
189
183
  }
190
184
  todos.applyEdits(plan.edits);
191
185
  mirrorTodos();
@@ -196,24 +190,24 @@ export default async function (pi: ExtensionAPI) {
196
190
  }
197
191
  }
198
192
 
199
- /** M17:对账提示行拼进结算通知(空数组 = 原文返回)。 */
193
+ /** M17: append reconciliation note lines to settlement notices; an empty list returns the original text. */
200
194
  function withReconcileNotes(base: string, notes: readonly string[]): string {
201
195
  return notes.length ? `${base}\n${notes.join('\n')}` : base;
202
196
  }
203
197
 
204
- /* ── M16:进度徽标 + 工具徽标(title 后缀 + report_agent.message;无新协议) ── */
198
+ /* ── M16: progress and tool badges (title suffix + report_agent.message; no new protocol) ── */
205
199
 
206
200
  todos.on('todo.completed', (e: { count: number; at: number }) => {
207
201
  for (let i = 0; i < e.count; i++) completedStamps.push(e.at);
208
202
  mirrorTodos();
209
203
  });
210
204
 
211
- // 工具徽标:并行模式 start 源序 / end 完成序(pi docs 实测契约)→ Map 跟踪在途
205
+ // Tool badge: parallel mode starts in source order and ends in completion order (pi docs contract); track in-flight calls in a Map.
212
206
  const runningTools = new Map<string, string>(); // toolCallId → toolName
213
207
  let lastToolBadge: string | null = null;
214
208
  function reportToolBadge(): void {
215
209
  const badge = planToolBadge([...new Set(runningTools.values())]);
216
- if (badge === lastToolBadge) return; // 幂等:变化才报(M13 渲染纪律)
210
+ if (badge === lastToolBadge) return; // Idempotent: report only changes (M13 rendering discipline).
217
211
  lastToolBadge = badge;
218
212
  if (badge) reportAgent('working', badge);
219
213
  else reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
@@ -232,125 +226,40 @@ export default async function (pi: ExtensionAPI) {
232
226
  }
233
227
  });
234
228
 
235
- /* ── M18:文件级写锁(S2:默认软 veto;PI_HERDR_WRITE_LOCK=1 硬启) ──
236
- * 登记 = report_metadata tokens(键 lock-<归一路径哈希>,值 paneId|path——schema 键模式
237
- * ^[A-Za-z0-9_-]{1,32}$ 装不下路径);检查 = agent.list 读全 pane tokens;
238
- * 释放 = settled 置 null(pane 关闭 = agent 条目消失,锁自灭)。
239
- * v1 范围:write/edit(bash 文件目标提取不可靠,声明为 seam)。 */
240
-
241
- const writeLockHard = process.env[WRITE_LOCK_ENV] === '1';
242
- /** 本 pane 当前持有的锁(归一路径;settled 时统一释放)。 */
243
- const heldLocks = new Set<string>();
244
- /** 软 veto 待附警告的 toolCall → 警告文本。 */
245
- const lockWarnByToolCall = new Map<string, string>();
246
-
247
- async function acquireLocks(paths: readonly string[]): Promise<void> {
248
- for (const p of paths) heldLocks.add(p);
249
- await client.reportLockTokens(acquireTokensFor(paths, env?.paneId ?? ''));
250
- }
251
-
252
- pi.on('tool_call', async (event: { toolName?: string; toolCallId?: string; input?: unknown }, ctx: { cwd?: string }) => {
253
- if (!client.available || !env) return;
254
- const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
255
- let agents: LockAgentView[];
256
- try {
257
- agents = (await client.listAgents()).map((a) => ({ paneId: a.paneId, tokens: a.tokens }));
258
- } catch {
259
- return; // 查询失败放行(fail-open:锁是协作性保护,不作单点故障)
260
- }
261
- const plan = planWriteGuard({
262
- toolName: event.toolName ?? '',
263
- input: event.input,
264
- agents,
265
- ownPaneId: env.paneId,
266
- cwd,
267
- hard: writeLockHard,
268
- });
269
- if (plan.kind === 'skip') return;
270
- if (plan.kind === 'block') {
271
- return { block: true, reason: plan.reason };
272
- }
273
- if (plan.kind === 'warn' && typeof event.toolCallId === 'string') {
274
- lockWarnByToolCall.set(event.toolCallId, plan.warning);
275
- // 软模式:写入照常进行,但本 pane 也成为编辑者 → 接管锁(对称可见)
276
- }
277
- await acquireLocks(plan.paths);
278
- });
279
-
280
- pi.on('tool_result', async (event: { toolCallId?: string; content?: Array<{ type: string; text: string }> }) => {
281
- if (typeof event?.toolCallId !== 'string') return;
282
- const warning = lockWarnByToolCall.get(event.toolCallId);
283
- if (!warning) return;
284
- lockWarnByToolCall.delete(event.toolCallId);
285
- const content = Array.isArray(event.content) ? event.content : [];
286
- return { content: [...content, { type: 'text', text: warning }] };
287
- });
288
-
289
- // 锁释放:settled(一轮结束)置 null;pane 关闭由 agent 条目消失兜底
290
- pi.on('agent_settled', async () => {
291
- if (heldLocks.size === 0) return;
292
- const paths = [...heldLocks];
293
- heldLocks.clear();
294
- await client.reportLockTokens(releaseTokensFor(paths));
295
- });
296
-
297
- // 人类可读:/locks 查看本 pane 持有 + 全局锁视图
298
- pi.registerCommand('locks', {
299
- description: 'Show write locks held by this pane and all live panes (M18)',
300
- handler: async (_args, ctx) => {
301
- const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
302
- const mine = [...heldLocks];
303
- const lines = [`held by this pane (${mine.length}):`];
304
- lines.push(...(mine.length ? mine.map((p) => ` ${p}`) : [' (none)']));
305
- lines.push('all live locks:');
306
- let any = false;
307
- try {
308
- for (const a of await client.listAgents()) {
309
- for (const [k, v] of Object.entries(a.tokens)) {
310
- if (!isLockTokenKey(k) || typeof v !== 'string' || !v) continue;
311
- const parsed = parseLockTokenValue(v);
312
- if (!parsed) continue;
313
- any = true;
314
- lines.push(` ${parsed.path} → pane ${parsed.holderPaneId}`);
315
- }
316
- }
317
- } catch {
318
- lines.push(' (agent.list failed)');
319
- }
320
- if (!any && lines[lines.length - 1] !== ' (agent.list failed)') lines.push(' (none)');
321
- ui?.notify?.(lines.join('\n'), 'info');
322
- },
229
+ installWriteLocks(pi, {
230
+ client,
231
+ env,
232
+ hard: process.env[WRITE_LOCK_ENV] === '1',
323
233
  });
324
234
 
325
- /* ── todo 族槽(core/todo.ts 插件回填;widget 渲染已随族迁移) ── */
235
+ /* ── todo family slot (core/todo.ts fills the plugin hook; widget rendering moved with the family) ── */
326
236
  const todoUi: { renderWidget: (ctx: unknown) => void } = {
327
- renderWidget: () => { /* 插件挂载前 no-op(挂载后回填) */ },
237
+ renderWidget: () => { /* No-op before plugin mounting; filled after mounting. */ },
328
238
  };
329
239
 
330
- /* ── 生命周期 ────────────────────────────────────────────────────── */
240
+ /* ── Lifecycle ────────────────────────────────────────────────────── */
331
241
 
332
242
  pi.on('session_start', async (event, ctx) => {
333
243
  sessionId = resolveSessionId(ctx);
334
244
  rebuildFromBranch(ctx);
335
- // v1.3 M9 修复(实测):resume 时 pi 会从会话恢复 widget 状态;
336
- // 此时再 setWidget 会破坏 TUI 的 '/' 命令面板输入路由('/' 被当消息文本送给模型)。
245
+ // v1.3 M9 fix (observed): on resume pi restores widget state from the session;
246
+ // calling setWidget again breaks the TUI '/' command-panel route ('/' would be sent to the model as message text).
337
247
  const reason = (event as { reason?: string } | undefined)?.reason;
338
248
  if (reason !== 'resume') todoUi.renderWidget(ctx);
339
- // D97:窄格静帧 overlay(幂等注册;herdr pane 内才有意义——普通终端窄窗
340
- // 盖住交互面是事故,且热力放大才提供退出路径。resume 也重注册:pi 会话
341
- // 切换会 resetExtensionUI 拆掉 overlay)。
249
+ // D97: narrow-frame overlay is meaningful only inside herdr; covering an interactive narrow terminal is unsafe,
250
+ // and the heatmap amplification provides the exit path. Re-register on resume because session switching resets the overlay.
342
251
  if (env) registerSlimFrame(ctx);
343
252
  mirrorTodos();
344
253
  reportSession(sessionId);
345
- // D93:侧边栏身份 = role 名(display_agent 优先于 agent 检测值,0.8.2 actions.rs:563)。
346
- // master → 'master';worker → manifest.role(worker-default 美化为 worker)。
347
- // 无 manifest 的裸 pi(独立会话)不上报——不打扰普通 pi 使用。
254
+ // D93: sidebar identity is the role name (display_agent takes precedence over detected agent, actions.rs:563 in 0.8.2).
255
+ // master → 'master'; worker → manifest.role (prettify worker-default as worker).
256
+ // A bare pi without a manifest does not report, so ordinary pi sessions remain undisturbed.
348
257
  if (runtimeManifest) {
349
258
  const roleDisplay = runtimeManifest.role === 'worker-default' ? 'worker' : runtimeManifest.role;
350
259
  void client.reportDisplayAgent(roleDisplay);
351
260
  }
352
- // 档2:worker role manifest(env,进程启动时已解析)→ 徽标 + 会话权威记录
353
- // (custom 条目与 D38 todo-edit 同构:执行期强化的回放锚点)
261
+ // Tier 2: worker role manifest parsed at process start supplies the badge and authoritative session record.
262
+ // The custom entry mirrors D38 todo-edit and anchors execution-time replay.
354
263
  if (runtimeManifest) {
355
264
  roleBadge = `role ${runtimeManifest.role} v${runtimeManifest.version ?? '?'} (${runtimeManifest.tools.length} tools)`;
356
265
  try {
@@ -359,10 +268,10 @@ export default async function (pi: ExtensionAPI) {
359
268
  { version: 1, role: runtimeManifest.role, manifestVersion: runtimeManifest.version, tools: runtimeManifest.tools, permissions: runtimeManifest.permissions, unknownTools: runtimeManifest.unknownTools ?? 'deny', ts: Date.now() },
360
269
  );
361
270
  } catch {
362
- /* 记录尽力而为 */
271
+ /* Best effort recording. */
363
272
  }
364
- // D77 可见层:manifest 外的工具从模型视野移除(session_start = 所有插件已加载完)。
365
- // 交集语义防清空;master 无 manifest 不动(全量)。底层 API 缺失则静默跳过(旧 pi 兼容)。
273
+ // D77 visible layer: remove tools outside the manifest from the model's view after all plugins load at session_start.
274
+ // Intersection semantics prevent clearing everything; master without a manifest stays full. Missing APIs are skipped for old pi compatibility.
366
275
  const piTools = pi as { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
367
276
  if (typeof piTools.getActiveTools === 'function' && typeof piTools.setActiveTools === 'function') {
368
277
  try {
@@ -376,17 +285,17 @@ export default async function (pi: ExtensionAPI) {
376
285
  console.error(`[pi-herdr] D77 visible-layer: role ${runtimeManifest.role} tools ${active.length} → ${vis.next.length}`);
377
286
  }
378
287
  } catch {
379
- /* 可见层尽力而为(强制层仍在) */
288
+ /* Best effort visible layer; the mandatory layer remains active. */
380
289
  }
381
290
  }
382
291
  }
383
292
  reportAgent('idle', null);
384
293
  });
385
294
 
386
- /* ── 档2 Week4-5:worker 执行期强制(manifest 最后一道闸) ──
387
- * deny / 不在 manifest → block(reason 给模型);ask → v1 退化放行 + stderr 日志
388
- * (V56 验收锚点)。master / 纯标签 worker 无 manifest = open。
389
- * 限速已移除(WS-D6):权限边界归我们,资源配额归插件引入者。 */
295
+ /* ── Tier 2 Weeks 4–5: worker execution enforcement (the manifest's final gate) ──
296
+ * deny / outside manifest → block (reason is given to the model); ask → v1 allows with stderr logging
297
+ * (V56 acceptance anchor). Master and label-only workers without a manifest remain open.
298
+ * Rate limiting was removed (WS-D6): we own the permission boundary; plugin integrators own resource quotas. */
390
299
  pi.on('tool_call', async (event: { toolName?: string }) => {
391
300
  if (!runtimeManifest) return;
392
301
  const tool = typeof event?.toolName === 'string' ? event.toolName : '';
@@ -396,20 +305,20 @@ export default async function (pi: ExtensionAPI) {
396
305
  }
397
306
  if (gate.kind === 'ask') {
398
307
  console.error(`${gate.notice} (v1 soft-approval: allowed, hard gate lands in v2)`);
399
- // 持久留痕(V56 锚点):stderr 会被 TUI 重绘刷掉,会话 custom 条目才是权威
308
+ // Durable trace (V56 anchor): TUI redraw erases stderr, so the session custom entry is authoritative.
400
309
  try {
401
310
  (pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
402
311
  'pi-herdr.approval-needed',
403
312
  { role: runtimeManifest.role, tool, ts: Date.now() },
404
313
  );
405
314
  } catch {
406
- /* 尽力而为 */
315
+ /* Best effort. */
407
316
  }
408
317
  }
409
318
  });
410
319
 
411
320
 
412
- // 会话树跳转(/tree、/fork 后):分支正确性靠重建。
321
+ // Session-tree navigation (/tree, /fork): reconstruction preserves branch correctness.
413
322
  pi.on('session_tree', async (_event, ctx) => {
414
323
  sessionId = resolveSessionId(ctx);
415
324
  rebuildFromBranch(ctx);
@@ -418,11 +327,12 @@ export default async function (pi: ExtensionAPI) {
418
327
  });
419
328
 
420
329
  let agentActive = false;
421
- // D96 状态(settle-wake-core 去重/冷却的锚点)。
330
+ const subagentPort = emptySubagentPortBox();
331
+ // D96 state, anchoring settle-wake-core deduplication and cooldown.
422
332
  let d96NoticeKey: string | null = null;
423
333
  let d96NoticeAt = 0;
424
- // 反唤醒风暴(settle-wake-core):追踪最后一次 assistant turn 的 stopReason,
425
- // 'aborted' = 用户 ESC 显式叫停 → settled 时不得注入任何唤醒型消息。
334
+ // Prevent wake-up storms by tracking the last assistant turn's stopReason;
335
+ // 'aborted' means the user explicitly pressed ESC, so settlement must inject no wake-up message.
426
336
  let lastStopReason: string | null = null;
427
337
  pi.on('turn_start', async () => {
428
338
  agentActive = true;
@@ -430,7 +340,7 @@ export default async function (pi: ExtensionAPI) {
430
340
  });
431
341
  pi.on('turn_end', async (event: unknown) => {
432
342
  if (event === null || typeof event !== 'object' || !('message' in event)) return;
433
- const msg = (event as { message: unknown }).message; // 'message' in 已守卫
343
+ const msg = (event as { message: unknown }).message; // 'message' in has been guarded.
434
344
  if (msg === null || typeof msg !== 'object') return;
435
345
  const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
436
346
  if (role === 'assistant' && typeof stopReason === 'string') {
@@ -443,7 +353,7 @@ export default async function (pi: ExtensionAPI) {
443
353
  reportAgent('idle', null);
444
354
  const plan = planSettleWake({
445
355
  lastStopReason,
446
- running: subagentSlots.listRunningSubs?.() ?? [],
356
+ running: subagentPort.current?.listRunningSubs() ?? [],
447
357
  lastNoticeKey: d96NoticeKey,
448
358
  lastNoticeAt: d96NoticeAt,
449
359
  now: Date.now(),
@@ -451,35 +361,34 @@ export default async function (pi: ExtensionAPI) {
451
361
  d96NoticeKey = plan.noticeKey;
452
362
  d96NoticeAt = plan.noticeAt;
453
363
  if (!plan.wake) {
454
- // 用户 abort 后静默:结算缓冲保留(pendingSettleNotices 不清空),
455
- // 待下次自然 run 的 turn_end steer / 自然 settled 再投。
364
+ // Stay silent after a user abort while retaining the settlement buffer (do not clear pendingSettleNotices);
365
+ // deliver it on the next natural run's turn_end steer or natural settlement.
456
366
  return;
457
367
  }
458
- // D96:master settled(run 结束/想总结)时仍有后台 subagent running → 注入提醒。
459
- // worker 侧 subagentSlots.listRunningSubs 为 null(不注入)。
368
+ // D96: master settled while background subagents still run → remind (worker port is unbound).
460
369
  if (plan.notice && !isSubagent) {
461
- const running = subagentSlots.listRunningSubs?.() ?? [];
370
+ const running = subagentPort.current?.listRunningSubs() ?? [];
462
371
  const brief = running.map((s) => `${s.paneId} (${s.description})`).join('、');
463
372
  const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(list_agents 查看状态);若不等待,请说明放弃原因。`;
464
373
  void sendUserMessageIn(notice);
465
374
  }
466
375
  });
467
376
 
468
- // v1.3 M8:blocked 自上报(D29 豁免依赖 blocked 可见)。
469
- // 实测:pi 0.84.2 核心不发 "herdr:blocked"(官方集成的监听是死代码)→
470
- // 本扩展自行管理:等待人类回答(ask_user_question)期间 enterBlocked/exitBlocked。
377
+ // v1.3 M8: report blocked state ourselves (D29's exemption relies on blocked visibility).
378
+ // Observed: pi 0.84.2 core emits no "herdr:blocked" (the official integration listener is dead code),
379
+ // so this extension manages enterBlocked/exitBlocked while ask_user_question waits for human input.
471
380
  let blockedDepth = 0;
472
381
  function enterBlocked(label: string | null): void {
473
382
  blockedDepth += 1;
474
383
  reportAgent('blocked', label);
475
- // D95:人类闸门标志(workbench 热力分级区分 ask vs block)
384
+ // D95: human-gate marker lets the workbench heatmap distinguish ask from block.
476
385
  if (label) void client.reportAskFlag(label).catch(() => {});
477
386
  }
478
387
  function exitBlocked(): void {
479
388
  blockedDepth = Math.max(0, blockedDepth - 1);
480
389
  if (blockedDepth === 0) {
481
390
  reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
482
- // D95:闸门解除 → 清标志
391
+ // D95: gate released → clear marker.
483
392
  void client.reportAskFlag(null).catch(() => {});
484
393
  }
485
394
  }
@@ -492,7 +401,7 @@ export default async function (pi: ExtensionAPI) {
492
401
  },
493
402
  );
494
403
 
495
- /* ── 工具:ask_user_question(v1.3 M8 人类闸门,主控与子代理都有) ── */
404
+ /* ── Tool: ask_user_question (v1.3 M8 human gate, available to master and subagents) ── */
496
405
 
497
406
  pi.registerTool({
498
407
  name: 'ask_user_question',
@@ -515,8 +424,8 @@ export default async function (pi: ExtensionAPI) {
515
424
  ui?: { input?: (title: string, placeholder?: string) => Promise<string | undefined> };
516
425
  }).ui;
517
426
  enterBlocked(question);
518
- // v1.3 M8 实测:等待人类期间 herdr 检测持续显示 working,会覆盖单次 blocked 上报
519
- // → 5s 心跳重报,保证 pane 在 herdr 里呈 blocked(人类可见 + GC 豁免)。
427
+ // v1.3 M8 observed: herdr keeps detecting working while waiting for a human, overriding a one-shot blocked report;
428
+ // repeat it every 5s so the pane stays blocked and remains visible/GC-exempt.
520
429
  const hb = setInterval(() => { reportAgent('blocked', question); }, 5000);
521
430
  try {
522
431
  const answer = await ui?.input?.(question, 'your answer');
@@ -535,31 +444,20 @@ export default async function (pi: ExtensionAPI) {
535
444
  client.close();
536
445
  });
537
446
 
538
- /* ── M12:双向消息通道(D49/D50/D48;每个 pane 都在自己名字上监听,无主从) ── */
447
+ /* ── M12: bidirectional message channel (D49/D50/D48; every pane listens on its own name, with no controller/child assumption) ── */
539
448
 
540
449
  const settleNoticeLatch = new Set<string>();
541
- /** 结算通知去重(push 快路径与 pollLoop 兜底二选一;按 paneId+请求id 记轮次,每轮各一条)。 */
450
+ /** Deduplicate settlement notices: choose push fast path or pollLoop fallback; one notice per paneId+request ID per round. */
542
451
  function claimSettleNotice(key: string): boolean {
543
452
  if (settleNoticeLatch.has(key)) return false;
544
453
  settleNoticeLatch.add(key);
545
454
  return true;
546
455
  }
547
- /** O6:master 收到 worker reply 时回写 sessionFile(core/subagent 插件挂载时回填槽)。 */
548
- const subagentSlots: {
549
- applyReplySession: ((paneId: string, sessionFile: string | null) => void) | null;
550
- reconcileOnReply: ((paneId: string) => string[]) | null;
551
- /** D96:处于 running 状态的后台 subagent 描述列表(master settled 时检查用)。 */
552
- listRunningSubs: (() => Array<{ paneId: string; description: string }>) | null;
553
- } = { applyReplySession: null, reconcileOnReply: null, listRunningSubs: null };
554
- /** pipe server 盒(common 持有创建/关闭;core/subagent 的 root effect 也经盒关)。 */
555
456
  const pipeServerBox: { current: ReturnType<typeof startPipeServer> | null } = { current: null };
556
- /** 最近一次机器请求(结算时按它决定是否 push、push 给谁)。 */
557
457
  let pendingMachineRequest: { id: string; from: string | null; push: boolean; sinceTs: number } | null = null;
558
- /** 缓存的扩展上下文(interrupt 用 ctx.abort(),D48)。 */
559
458
  let latestCtx: { abort?: () => void } | null = null;
560
459
 
561
- // D96:triggerTurn:true——followUp 在 idle 时立即触发新 turn(否则通知只排队不唤醒,
562
- // 用户实机:subagent 结算后 idle master 不醒来处理,任务丢失;extensions.md:1410)。
460
+ // triggerTurn:true — idle followUp must start a new turn or settlement is lost.
563
461
  const sendUserMessageIn = (content: string): Promise<void> =>
564
462
  (pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
565
463
  .sendUserMessage?.(content, { deliverAs: 'followUp', triggerTurn: true }) ?? Promise.resolve();
@@ -567,43 +465,17 @@ export default async function (pi: ExtensionAPI) {
567
465
  (pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
568
466
  .sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true }) ?? Promise.resolve();
569
467
 
570
- /* ── D92:结算通知缓冲(followup 堆积修复)─────────────────────────────
571
- * 旧路径 pi.sendUserMessage(followUp) 的 pi 语义 =「agent 没有更多 tool call
572
- * 时才投递」= 整个 run 结束才回填——master 长任务执行期 N 条结算全攒队列,
573
- * 结束瞬间洪水灌入(用户实机红框实证)。
574
- * 新路径:忙时(agentActive)入扩展缓冲;
575
- * - turn_end(LLM 迭代边界)折叠成一条以 steer 注入——pi 文档:steer =
576
- * 「当前 assistant 的 tool call 执行完、下一次 LLM 调用前投递」,即 turn 间隙;
577
- * - agent_settled 兜底:run 真正结束后残余缓冲直投(触发新 run)。
578
- * 折叠规则(notice-buffer.ts):≤3 条原文逐条,>3 条前 3 + 尾行计数/全量指引。 */
579
- const pendingSettleNotices: string[] = [];
580
- /** B2:缓冲中尚未送达的结算通知所属 pane(GC 豁免判据——01a03c0d 实证:
581
- * pD 结算 3s 后 pane 被 GC 关掉,通知 100s 后才经 turn 间隙送达,master 只好 revive)。 */
582
- const pendingNoticePaneIds = new Set<string>();
583
- const deliverNotice = async (content: string, paneId?: string): Promise<void> => {
584
- // 反唤醒风暴:abort 后不直投(会触发新 run)——入缓冲,待下次自然 turn 投递。
585
- // 结算内容本身持久在台账(list_agents / resume_subagent 可查),推送让位于用户意图。
586
- if (content !== '' && paneId !== undefined) pendingNoticePaneIds.add(paneId);
587
- if (agentActive || lastStopReason === ABORT_STOP_REASON) {
588
- pendingSettleNotices.push(content);
589
- return;
590
- }
591
- await sendUserMessageIn(content);
592
- if (content !== '' && paneId !== undefined) pendingNoticePaneIds.delete(paneId);
593
- };
594
- const flushSettleNotices = async (mode: 'steer' | 'followUp'): Promise<void> => {
595
- if (pendingSettleNotices.length === 0) return;
596
- const collapsed = collapseNotices(pendingSettleNotices.splice(0));
597
- pendingNoticePaneIds.clear(); // B2:折叠批次视为已送达(GC 恢复正常回收)
598
- if (collapsed) await sendUserMessageAs(collapsed, mode);
599
- };
468
+ const notices = createNoticeBuffer({
469
+ isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON,
470
+ send: sendUserMessageAs,
471
+ });
472
+ const deliverNotice = notices.deliverNotice;
600
473
  pi.on('turn_end', () => {
601
- void flushSettleNotices('steer');
474
+ void notices.flush('steer');
602
475
  });
603
476
  pi.on('agent_settled', () => {
604
- // abort 后不投缓冲(会触发新 run;缓冲保留待下次自然 turn)——见 settle-wake-core。
605
477
  if (lastStopReason === ABORT_STOP_REASON) return;
606
- void flushSettleNotices('followUp');
478
+ void notices.flush('followUp');
607
479
  });
608
480
 
609
481
  pi.on('session_start', async (_event, ctx) => {
@@ -613,61 +485,32 @@ export default async function (pi: ExtensionAPI) {
613
485
  if (!paneId) return;
614
486
  const name = pipeNameFor(cwd, paneId);
615
487
  if (pipeServerBox.current) {
616
- try { pipeServerBox.current.close(); } catch { /* 旧实例 */ }
488
+ try { pipeServerBox.current.close(); } catch { /* Previous instance. */ }
617
489
  pipeServerBox.current = null;
618
490
  }
619
491
  try {
620
- pipeServerBox.current = startPipeServer(name, async (req) => {
621
- if (req.type === 'ping') return { type: 'ok', id: req.id, detail: paneId };
622
- if (req.type === 'prompt' || req.type === 'follow_up') {
623
- pendingMachineRequest = {
624
- id: req.id,
625
- from: req.from ?? null,
626
- push: req.push === true,
627
- sinceTs: Date.now(),
628
- };
629
- // B3:follow_up 带 steer → 以 steer 投递(pi 语义:当前 tool call 执行完、
630
- // 下次 LLM 调用前到达)——长 run 中途的补充契约秒级生效,不再排队整个 run
631
- // (01a03c0d:契约 20min 后才到,worker 按旧契约实现被迫返工)。
632
- // 初始 prompt 不 steer:空闲 worker 上的 followUp 才触发新 run(D96 语义)。
633
- if (req.steer === true) await sendUserMessageAs(req.text, 'steer');
634
- else await sendUserMessageIn(req.text);
635
- return { type: 'ok', id: req.id };
636
- }
637
- if (req.type === 'interrupt') {
638
- // D48:进程内中止(ctx.abort(),pi 0.84.2 实测存在);排队机器消息保持排队
639
- latestCtx?.abort?.();
640
- pendingMachineRequest = null; // 被中断的轮次不 push(请求方已知情)
641
- return { type: 'ok', id: req.id };
642
- }
643
- if (req.type === 'reply') {
644
- // D50 / O6:回写 sessionFile 与通知去重解耦——账本必须先纠正
645
- subagentSlots.applyReplySession?.(req.paneId, req.sessionFile);
646
- if (claimSettleNotice(`${req.paneId}:${req.id}`)) {
647
- // M17:快路径结算 → 自动对账(幂等;与 pollLoop 兜底双跑无害)
648
- const notes = subagentSlots.reconcileOnReply?.(req.paneId) ?? [];
649
- const head = formatSettlementNotice(`${req.paneId}`, req.text);
650
- const body = (req.sessionFile ? `${head}\nSession: ${req.sessionFile}` : head)
651
- + (notes.length ? `\n${notes.join('\n')}` : '');
652
- // D92:经缓冲注入(忙时 turn_end 折叠 steer,闲时直投)
653
- await deliverNotice(body, req.paneId);
654
- }
655
- return { type: 'ok', id: req.id };
656
- }
657
- return { type: 'error', id: req.id, message: `unknown type ${req.type}` };
658
- });
492
+ pipeServerBox.current = startPipeServer(name, async (req) => handlePipeRequest(req, {
493
+ paneId,
494
+ port: subagentPort,
495
+ claimSettleNotice,
496
+ deliverNotice,
497
+ sendUserMessageIn,
498
+ sendUserMessageAs,
499
+ abort: () => { latestCtx?.abort?.(); },
500
+ setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
501
+ }));
659
502
  } catch {
660
- /* 管道名占用(罕见):本次会话不提供通道,调用方 ping 超时后报错 */
503
+ /* Pipe name collision (rare): this session has no channel; callers report an error after ping times out. */
661
504
  }
662
505
  });
663
506
  pi.on('session_shutdown', () => {
664
507
  if (pipeServerBox.current) {
665
- try { pipeServerBox.current.close(); } catch { /* 已关 */ }
508
+ try { pipeServerBox.current.close(); } catch { /* Already closed. */ }
666
509
  pipeServerBox.current = null;
667
510
  }
668
511
  });
669
512
 
670
- // D50:本 pane 自己的结算 → 若有待回信的机器请求,push 摘要+会话路径给请求方
513
+ // D50: when this pane settles, push a summary and session path to any machine request awaiting a reply.
671
514
  pi.on('agent_settled', async () => {
672
515
  const req = pendingMachineRequest;
673
516
  if (!req || !req.push || !req.from) return;
@@ -686,128 +529,33 @@ export default async function (pi: ExtensionAPI) {
686
529
  sessionFile: sessionId || null,
687
530
  }, 5000);
688
531
  } catch {
689
- /* push 失败静默:请求方的 pollLoop 兜底 */
532
+ /* Push failed silently; the requester's pollLoop is the fallback. */
690
533
  }
691
534
  });
692
535
 
693
- /* ── 工具:subagent / list_agents / send_message / interrupt_agent(v1.1 交互式子 pane;子代理侧不注册,深度限 1) ── */
694
-
695
- const isSubagent = process.env.PI_HERDR_SUBAGENT === '1';
536
+ /* subagent tools are master-only (depth 1). C3: worker never loads bootstrap. */
696
537
  if (!isSubagent) {
697
- // 档1:cordis 树根升级为 bootstrap(Loader + group builtin + 条件 hmr,D78/D80/D81);
698
- // subagent scope 挂载/拆除仍走 subagent-scope(worker 进程两模块都不加载,C3)。
699
- const { disposeSessionRoot } = await import('./subagent-scope.ts');
700
- const { createCordisApp } = await import('./bootstrap.ts');
701
- const { PiSurface } = await import('./pi-surface.ts');
702
- const cordisApp = await createCordisApp();
703
- const sessionRoot = cordisApp.root;
704
-
705
- // pipe server 关闭挂在树根(common 段创建、树根 LIFO 拆除)——不进任何 core
706
- // 插件:hmr 重载插件不能误杀 index 拥有的跨属资源(d87 修)。
707
- sessionRoot.effect(() => () => {
708
- if (pipeServerBox.current) {
709
- try { pipeServerBox.current.close(); } catch { /* 已关 */ }
710
- pipeServerBox.current = null;
711
- }
712
- }, 'pipe-server');
713
-
714
- // D79 pi 注册面:core 模块经 surface 注册(tombstone + ledger 咬合);
715
- // index 自身的注册仍走 pi 原面(逐模块迁移,本批 = terminal 族)。
716
- const surface = new PiSurface(pi as unknown as object, cordisApp.ledger);
717
- sessionRoot.provide('pi-herdr.surface', surface);
718
- // terminal 依赖槽:client/env 直传 + state 槽回填(GC 豁免查询)
719
- const terminalDeps = {
538
+ const { mountMasterPlugins } = await import('./index-master.ts');
539
+ await mountMasterPlugins({
540
+ pi,
720
541
  client,
721
542
  env,
722
- state: { activePaneIds: (): Set<string> => new Set() },
723
- };
724
- sessionRoot.provide('pi-herdr.terminal-deps', terminalDeps);
725
-
726
- // M14 terminal 族迁 loader entry(热换面 = core/terminal.ts;loader 降级 → 直接 root.plugin)
727
- const terminalMod = await import('./core/terminal.ts');
728
- if (cordisApp.loaderReady) {
729
- const withLoader = sessionRoot as typeof sessionRoot & {
730
- loader?: { create: (o: { name: string }) => Promise<unknown> };
731
- };
732
- await withLoader.loader?.create({ name: './core/terminal.ts' });
733
- } else {
734
- await sessionRoot.plugin(terminalMod.default);
735
- }
736
-
737
- // todo 族迁 loader entry(协调工具:master/worker 都挂;本分支 = master 走 loader)
738
- sessionRoot.provide('pi-herdr.todo-deps', {
739
543
  todos,
740
- allowParallelInProgress: todos.config.allowParallelInProgress,
741
- maxItems: 15,
544
+ todoUi,
742
545
  mirrorTodos,
743
- appendEntry: (customType: string, data: unknown) => {
744
- (pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
745
- },
746
- state: todoUi,
747
- });
748
- const todoMod = await import('./core/todo.ts');
749
- if (cordisApp.loaderReady) {
750
- const withLoader = sessionRoot as typeof sessionRoot & {
751
- loader?: { create: (o: { name: string }) => Promise<unknown> };
752
- };
753
- await withLoader.loader?.create({ name: './core/todo.ts' });
754
- } else {
755
- await sessionRoot.plugin(todoMod.default);
756
- }
757
-
758
- // subagent 族迁 loader entry(master-only:五工具 + 注册表 + poller + 历史 + GC)
759
- sessionRoot.provide('pi-herdr.subagent-deps', {
760
- client,
761
- env,
762
546
  extPath: fileURLToPath(import.meta.url),
763
- sessionRoot,
764
- slots: subagentSlots,
547
+ port: subagentPort,
548
+ pipeServerBox,
765
549
  deliverNotice,
766
- noticePending: () => pendingNoticePaneIds,
550
+ noticePending: notices.noticePending,
767
551
  getSessionId: () => sessionId,
768
552
  getBlockedDepth: () => blockedDepth,
769
553
  reconcileOnSettlement,
770
554
  withReconcileNotes,
771
555
  claimSettleNotice,
772
- terminalState: terminalDeps.state,
773
- todos,
774
- });
775
- const subagentMod = await import('./core/subagent.ts');
776
- if (cordisApp.loaderReady) {
777
- const withLoader = sessionRoot as typeof sessionRoot & {
778
- loader?: { create: (o: { name: string }) => Promise<unknown> };
779
- };
780
- await withLoader.loader?.create({ name: './core/subagent.ts' });
781
- } else {
782
- await sessionRoot.plugin(subagentMod.default);
783
- }
784
-
785
- // 档0 起的拆除链:session_shutdown → dispose 整树(scope/effect LIFO)
786
- pi.on('session_shutdown', () => {
787
- void disposeSessionRoot(sessionRoot);
788
556
  });
789
557
  } else {
790
- // C3/D81:worker = 裸根 + 手动 mount(短命进程,无 loader/hmr/timer)。
791
- // todo 族是协调工具(任何角色档案必有)→ worker 也挂同一 core/todo.ts 插件。
792
- const { Context } = await import('@deepseek-ai/cordis');
793
- const { PiSurface } = await import('./pi-surface.ts');
794
- const workerRoot = new Context();
795
- const workerSurface = new PiSurface(pi as unknown as object);
796
- workerRoot.provide('pi-herdr.surface', workerSurface);
797
- workerRoot.provide('pi-herdr.todo-deps', {
798
- todos,
799
- allowParallelInProgress: todos.config.allowParallelInProgress,
800
- maxItems: 15,
801
- mirrorTodos,
802
- appendEntry: (customType: string, data: unknown) => {
803
- (pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
804
- },
805
- state: todoUi,
806
- });
807
- const todoMod = await import('./core/todo.ts');
808
- await workerRoot.plugin(todoMod.default);
809
- pi.on('session_shutdown', () => {
810
- void workerRoot.fiber.dispose();
811
- });
558
+ const { mountWorkerTodo } = await import('./index-worker.ts');
559
+ await mountWorkerTodo({ pi, todos, todoUi, mirrorTodos });
812
560
  }
813
561
  }