pi-pier 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +116 -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
@@ -1,32 +1,32 @@
1
1
  /**
2
- * D69:todo 阅读钩子规划(before_agent_start + display:false)。
3
- * 反冻结(stale-core):全完成列表过期后不再复读——
4
- * - stale(turns 维度)→ 警告 + 旧条目(供改写参照),每 N 轮一次、封顶;
5
- * - archived(墙钟维度)→ 按不存在处理,归档通知与空列表共用守卫节奏。
2
+ * D69: Plan the todo read hook (before_agent_start + display:false).
3
+ * Stale-core prevents a fully completed list from being recited forever:
4
+ * - stale (turn-based) → warn with old entries for rewriting, rate-limited and capped;
5
+ * - archived (wall-clock based) → treat it as absent, sharing the guard cadence with empty lists.
6
6
  */
7
7
  import { boundedView, currentActivity, type TodoItem } from './todo-core.ts';
8
8
  import { countTodos } from './vocab.ts';
9
9
  import { STALE_NOTICE_MAX, evaluateStaleness, formatAge } from './stale-core.ts';
10
10
 
11
- /** 空守卫 / stale 警告 / 归档通知共用的注入节奏(每 N 轮)。 */
11
+ /** Share one injection cadence across empty-list guards, stale warnings, and archive notices. */
12
12
  export const EMPTY_GUARD_EVERY_N = 4;
13
13
  export const TODO_READ_CUSTOM_TYPE = 'pi-herdr.todo-read';
14
14
 
15
15
  export type TodoReadEffect =
16
- | 'recite' // 正常复读(列表新鲜)
17
- | 'empty-guard' // 空列表守卫
18
- | 'stale-notice' // A:turns 维度过期警告
19
- | 'archive-notice' // B:墙钟维度归档通知
20
- | 'none'; // 限频/封顶跳过
16
+ | 'recite' // Recite normally while the list is fresh.
17
+ | 'empty-guard' // Guard against an empty list.
18
+ | 'stale-notice' // A: warn when the list is stale by turns.
19
+ | 'archive-notice' // B: notify when the list is archived by wall-clock age.
20
+ | 'none'; // Skip because of rate limits or caps.
21
21
 
22
22
  export interface TodoReadPlan {
23
23
  inject: boolean;
24
24
  effect: TodoReadEffect;
25
- /** 当前归档态(与 inject 无关;调用方据此刷新 widget/title 投影)。 */
25
+ /** Current archive state, independent of injection, so callers can refresh widget/title projections. */
26
26
  archived: boolean;
27
- /** R1:归档通知本次注入即清空内存列表(调用方执行 todos.replace([]) + rm 全量
28
- * 落 JSONL——死列表挡住空守卫的 before-stopping 驱动,01a03c0d 实证归档后
29
- * 2h 多步工作零跟踪)。 */
27
+ /** R1: Clear the in-memory list when injecting the archive notice; callers persist the empty JSONL
28
+ * so a dead list cannot block the before-stopping empty guard (01a03c0d observed 2h of untracked
29
+ * multi-step work after archiving). */
30
30
  clearArchived: boolean;
31
31
  message: {
32
32
  customType: string;
@@ -47,12 +47,12 @@ export function planTodoReadHook(opts: {
47
47
  items: readonly TodoItem[];
48
48
  turn: number;
49
49
  lastEmptyGuardTurn: number | null;
50
- /** stale-core 时钟锚点(TodosService.lastWriteAt)。 */
50
+ /** stale-core clock anchor (TodosService.lastWriteAt). */
51
51
  lastWriteAt: number | null;
52
- /** 距上次 todo 写入的轮数(本进程内已知时)。 */
52
+ /** Turns since the last todo write, when known in this process. */
53
53
  turnsSinceWrite: number | null;
54
54
  now: number;
55
- /** 本停滞期已注入的 stale 警告数。 */
55
+ /** Number of stale warnings injected during the current idle period. */
56
56
  staleNotices: number;
57
57
  lastStaleGuardTurn: number | null;
58
58
  }): TodoReadPlan {
@@ -88,9 +88,12 @@ export function planTodoReadHook(opts: {
88
88
  if (st.kind === 'archived') {
89
89
  const c = countTodos(opts.items as TodoItem[]);
90
90
  const age = st.ageMs == null ? '' : ` ${formatAge(st.ageMs)} ago`;
91
- // R2:重写窗口——本停滞期还没给过任何 stale 警告时,先给一次带旧条目参照的
92
- // 重写机会(idle 期不消耗 turn,墙钟先到 1h 而 turns 警告从未触发,
93
- // 01a03c0d 实证:回来后直接吃终态通知,todo_write 5 次被无视)。
91
+ // R2: Preserve one rewrite window with old entries as a reference before archiving.
92
+ // Idle periods do not consume turns, so the wall clock can reach one hour before a turns-based
93
+ // warning fires; 01a03c0d showed that the final notice then caused five todo_write calls to be ignored.
94
+ // B + R1/R3: The terminal notice no longer injects details and clears the list in the same
95
+ // injection; removing the [] escape hatch ensures multi-step work gets tracked while single-step
96
+ // Q&A remains explicitly allowed.
94
97
  if (opts.staleNotices === 0 && guardDue) {
95
98
  const lines = opts.items.map((it) => ` ${MARKS[it.status]} ${it.content}`);
96
99
  const head = `todos ✓${c.completed} (all completed, last updated${age}) — about to be archived`;
@@ -103,8 +106,9 @@ export function planTodoReadHook(opts: {
103
106
  message: msg([head, ...lines, warn].join('\n')),
104
107
  };
105
108
  }
106
- // B + R1/R3:终态通知——明细不再注入,本次注入即清空;去掉 [] 豁免出口,
107
- // 多步工作必须建清单(单步问答明确放行)。
109
+ // B + R1/R3: The terminal notice no longer injects details and clears the list in the same
110
+ // injection; removing the [] escape hatch ensures multi-step work gets tracked while single-step
111
+ // Q&A remains explicitly allowed.
108
112
  return {
109
113
  inject: guardDue,
110
114
  effect: 'archive-notice',
@@ -119,7 +123,7 @@ export function planTodoReadHook(opts: {
119
123
  }
120
124
 
121
125
  if (st.kind === 'stale') {
122
- // A:复读改警告;保留旧条目行供模型改写参照。封顶 STALE_NOTICE_MAX。
126
+ // A: Turn recitation into a warning while retaining old entries as rewrite references; cap at STALE_NOTICE_MAX.
123
127
  const staleDue = opts.lastStaleGuardTurn == null
124
128
  || opts.turn - opts.lastStaleGuardTurn >= EMPTY_GUARD_EVERY_N;
125
129
  if (!staleDue || opts.staleNotices >= STALE_NOTICE_MAX) {
@@ -139,7 +143,7 @@ export function planTodoReadHook(opts: {
139
143
  };
140
144
  }
141
145
 
142
- // fresh:原有每轮复读。
146
+ // Fresh lists are recited every turn to keep current work visible.
143
147
  const c = countTodos(opts.items);
144
148
  const view = boundedView(opts.items, 6);
145
149
  const lines = view.visible.map((it) => `${MARKS[it.status]} ${it.content}`);
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pure decision core for reminders about unfinished todos when stopping (D41 revision 2;
3
+ * evidenced by session 01a040cc).
4
+ *
5
+ * Evidence: the model's closing prose said that pushing should be left to the user,
6
+ * yet the todo list still contained `[pending] push to repository`. The old D41 injected
7
+ * “Continue working on them before stopping” as a user-role message 116 ms after agent_settled;
8
+ * that channel overrode the model's judgment from 20 seconds earlier, and git push ran within
9
+ * 12 seconds without authorization.
10
+ *
11
+ * Revision 3 keeps the decision and wording here while the timing grace period is wired in
12
+ * core/subagent:
13
+ * - use sendMessage(custom), not the user role, so the reminder source is identifiable rather than impersonating the user;
14
+ * - replace “keep working” with a reconciliation request, making human-blocked items a first-class
15
+ * outcome alongside continuing authorized work and deleting obsolete entries;
16
+ * - preserve the guard matrix: suppress on abort, cap reminders, and skip while a subagent is
17
+ * running, a human gate is blocked, or no open items remain.
18
+ */
19
+ import { ABORT_STOP_REASON } from './settle-wake-core.ts';
20
+
21
+ /** Cap reminders for the lifetime of the process, matching the previous behavior. */
22
+ export const TODO_REMINDERS_MAX = 3;
23
+
24
+ /** Custom message type for reminder injection (registerMessageRenderer can customize its TUI appearance). */
25
+ export const TODO_REMINDER_CUSTOM_TYPE = 'pi-herdr.todo-reminder';
26
+
27
+ /**
28
+ * B3 grace window (milliseconds): delay from settled to injection, giving the user a chance to
29
+ * read the closing answer and intervene; starting any agent during the window cancels it. This
30
+ * follows the MACHINE_INJECT_GRACE_MS pattern; PI_HERDR_TODO_GRACE_MS is read per call so tests
31
+ * can use a small value rather than freezing configuration at module load.
32
+ */
33
+ export function todoReminderGraceMs(): number {
34
+ return Number(process.env.PI_HERDR_TODO_GRACE_MS ?? 30_000) || 30_000;
35
+ }
36
+
37
+ export interface TodoReminderInput {
38
+ /** Stop reason of the last assistant turn before this settlement (null means treat it as natural completion). */
39
+ lastStopReason: string | null;
40
+ /** Number of reminders already injected. */
41
+ reminders: number;
42
+ /** Number of running background subagents; a positive value means the master is already waiting. */
43
+ runningSubs: number;
44
+ /** ask_user_question wait depth; a positive value means the master is waiting for a human response. */
45
+ blockedDepth: number;
46
+ /** Current todo list. */
47
+ items: ReadonlyArray<{ content: string; status: string }>;
48
+ }
49
+
50
+ export interface TodoReminderPlan {
51
+ /** Whether a reminder should be injected. */
52
+ due: boolean;
53
+ /** Complete injected content, or null when due is false. */
54
+ content: string | null;
55
+ /** New count after successful injection, or the original count when due is false. */
56
+ nextReminders: number;
57
+ }
58
+
59
+ function noInject(reminders: number): TodoReminderPlan {
60
+ return { due: false, content: null, nextReminders: reminders };
61
+ }
62
+
63
+ /**
64
+ * A stop reminder is due only when every guard passes and at least one open (pending/in_progress)
65
+ * item exists. blocked, abandoned, and completed items are not unfinished: once human-waiting work
66
+ * is modeled correctly, reminding again would only pressure the model to act without authorization
67
+ * (the missing guard in the 01a040cc incident).
68
+ */
69
+ export function planStopTodoReminder(input: TodoReminderInput): TodoReminderPlan {
70
+ if (input.lastStopReason === ABORT_STOP_REASON) return noInject(input.reminders);
71
+ if (input.reminders >= TODO_REMINDERS_MAX) return noInject(input.reminders);
72
+ if (input.runningSubs > 0) return noInject(input.reminders);
73
+ if (input.blockedDepth > 0) return noInject(input.reminders);
74
+ const open = input.items.filter((it) => it.status === 'pending' || it.status === 'in_progress');
75
+ if (open.length === 0) return noInject(input.reminders);
76
+ const list = open
77
+ .map((it) => (it.status === 'in_progress' ? `▶ ${it.content}` : `· ${it.content}`))
78
+ .join('\n');
79
+ const content = [
80
+ `<system-reminder>You stopped with unfinished todos (Reminder ${input.reminders + 1}/${TODO_REMINDERS_MAX}):`,
81
+ list,
82
+ 'Reconcile the list instead of blindly continuing:',
83
+ '- work that is yours AND already authorized by the user → continue it;',
84
+ '- work waiting on a human (decision / approval / ops action) → mark it blocked with a blocker note, and ask the user via ask_user_question when you need their input — never execute it yourself just to clear the list;',
85
+ '- entries that no longer apply → remove them.',
86
+ 'Update the list with todo_write in any case.</system-reminder>',
87
+ ].join('\n');
88
+ return { due: true, content, nextReminders: input.reminders + 1 };
89
+ }
@@ -1,12 +1,9 @@
1
- /**
2
- * 档 0:todos 升为进程内 Service(D65 / D75 阶段 2)。
3
- * 持久化仍是 pi 会话分支重放;D75 阶段 3:config 从 role manifest services 注入
4
- * (无 manifest 的 master / 纯标签 worker 走默认——worker serial、master parallel)。
5
- */
1
+ /** Why: Preserve the established compatibility and safety behavior (D65, D75). */
6
2
  import { EventEmitter } from 'node:events';
7
3
  import {
8
4
  applyTodoEdits,
9
5
  foldLatestTodosMeta,
6
+ listsEqual,
10
7
  type TodoEdit,
11
8
  type TodoItem,
12
9
  } from './todo-core.ts';
@@ -19,11 +16,14 @@ export interface TodosConfig {
19
16
  }
20
17
 
21
18
  export class TodosService extends EventEmitter {
22
- items: TodoItem[] = [];
19
+ private _items: TodoItem[] = [];
23
20
  /**
24
- * 列表最后一次真实变更时间(epoch ms);分支重建取自条目时间戳,无则 null。
25
- * 陈旧度判定(stale-core)的时钟锚点;null → 保守永不判 stale。
21
+ * Live list (read-only). Mutations must go through replace/applyEdits/rebuild.
26
22
  */
23
+ get items(): readonly TodoItem[] {
24
+ return this._items;
25
+ }
26
+ /** Why: Preserve the established compatibility and safety behavior. */
27
27
  lastWriteAt: number | null = null;
28
28
  readonly config: TodosConfig;
29
29
 
@@ -32,7 +32,7 @@ export class TodosService extends EventEmitter {
32
32
  this.config = config;
33
33
  }
34
34
 
35
- /** D75 阶段 3:role manifest services.todos(mode/reminderLimit)驱动;strict = 进程身份(worker)。 */
35
+ /** Why: Preserve the established compatibility and safety behavior (D75). */
36
36
  static configFromRuntime(manifest: RuntimeRoleManifest | null, isSubagent: boolean): TodosConfig {
37
37
  const strict = isSubagent;
38
38
  const mode = manifest?.services?.todos?.mode;
@@ -40,37 +40,48 @@ export class TodosService extends EventEmitter {
40
40
  return { strict, allowParallelInProgress };
41
41
  }
42
42
 
43
- replace(items: readonly TodoItem[]): void {
44
- const before = this.items;
45
- this.items = items.map((it) => ({ ...it }));
43
+ /** Defensive copy for callers that must not mutate service state. */
44
+ getSnapshot(): TodoItem[] {
45
+ return this._items.map((it) => ({ ...it }));
46
+ }
47
+
48
+ replace(items: readonly TodoItem[]): { changed: boolean } {
49
+ const next = items.map((it) => ({ ...it }));
50
+ if (listsEqual(this._items, next)) return { changed: false };
51
+ const before = this._items;
52
+ this._items = next;
46
53
  this.lastWriteAt = Date.now();
47
- this.emitCompletedTransitions(before, this.items);
48
- this.emit('todo.updated', { items: this.items });
54
+ this.emitCompletedTransitions(before, this._items);
55
+ this.emit('todo.updated', { items: this._items });
56
+ return { changed: true };
49
57
  }
50
58
 
51
- applyEdits(edits: readonly TodoEdit[]): void {
52
- const before = this.items;
53
- this.items = applyTodoEdits(this.items, edits);
59
+ applyEdits(edits: readonly TodoEdit[]): { changed: boolean } {
60
+ const before = this._items;
61
+ const next = applyTodoEdits(this._items, edits);
62
+ if (listsEqual(before, next)) return { changed: false };
63
+ this._items = next;
54
64
  this.lastWriteAt = Date.now();
55
- this.emitCompletedTransitions(before, this.items);
56
- this.emit('todo.edited', { edits, items: this.items });
57
- this.emit('todo.updated', { items: this.items });
65
+ this.emitCompletedTransitions(before, this._items);
66
+ this.emit('todo.edited', { edits, items: this._items });
67
+ this.emit('todo.updated', { items: this._items });
68
+ return { changed: true };
58
69
  }
59
70
 
60
- /** M16:completed 转换数(所有编辑路径统一 diff;速率估算的原料)。 */
71
+ /** Why: Preserve the established compatibility and safety behavior (M16). */
61
72
  private emitCompletedTransitions(before: readonly TodoItem[], after: readonly TodoItem[]): void {
62
73
  const count = countCompletedTransitions(before, after);
63
74
  if (count > 0) this.emit('todo.completed', { count, at: Date.now() });
64
75
  }
65
76
 
66
77
  rebuild(entries: readonly unknown[]): void {
67
- // 与 v1.x rebuildFromBranch 同语义:分支上折叠不到快照时保留现值
68
- //(重建失败不影响主流程;下次 todo_write 会重新锚定)。
78
+ // Why: Preserve the established compatibility and safety behavior.
79
+ // Why: Preserve the established compatibility and safety behavior.
69
80
  const folded = foldLatestTodosMeta(entries as Parameters<typeof foldLatestTodosMeta>[0]);
70
81
  if (folded) {
71
- this.items = folded.items;
82
+ this._items = folded.items;
72
83
  this.lastWriteAt = folded.writtenAt;
73
84
  }
74
- this.emit('todo.updated', { items: this.items, reason: 'rebuild' });
85
+ this.emit('todo.updated', { items: this._items, reason: 'rebuild' });
75
86
  }
76
87
  }
package/src/tool-gate.ts CHANGED
@@ -1,40 +1,28 @@
1
- /**
2
- * 档2 Week4-5:worker 执行期强制(2026-08-18;C7 v2 / D76)。
3
- *
4
- * master 合成的 manifest 经 env 下发后,这里是 worker 侧的最后一道闸:
5
- * - `planToolGate`:工具不在 manifest / 权限 deny → deny(安全护栏);
6
- * ask → 标记(v1 退化:放行 + APPROVAL_NEEDED 日志,硬审批留 v2 拍板);
7
- * - `planActiveTools`(D77 可见层):manifest 外的工具从模型视野移除——
8
- * 不进 system prompt、不诱导尝试(上下文卫生,不止权限)。
9
- * 限速机制已整体移除(WS-D6):权限边界是我们的职责,资源配额是
10
- * 插件引入者/provider 的职责(D79 声明式引入,用户自负责)。
11
- * 纯逻辑零依赖;接缝在 index.ts 的 tool_call 钩子与 session_start。
12
- */
1
+ /** Why: Preserve the established compatibility and safety behavior (C7, D76, D77, D79, WS-D6). */
13
2
  import type { PermissionAction, UnknownToolStance } from './role-manifest.ts';
14
3
 
15
- /** env 下发的运行时形态(PI_HERDR_ROLE_MANIFEST JSON)。 */
4
+ /** Why: Preserve the established compatibility and safety behavior. */
16
5
  export interface RuntimeRoleManifest {
17
6
  role: string;
18
7
  version?: string;
19
8
  tools: string[];
20
9
  permissions: Record<string, PermissionAction>;
21
- /** D82:未知工具姿态(缺省 deny = 安全默认)。 */
10
+ /** Why: Preserve the established compatibility and safety behavior (D82). */
22
11
  unknownTools?: UnknownToolStance;
23
12
  services?: {
24
13
  todos?: {
25
14
  mode?: 'serial' | 'parallel';
26
- reminderLimit?: number;
27
15
  };
28
16
  };
29
17
  }
30
18
 
31
- /** 安全解析 PI_HERDR_ROLE_MANIFEST env(畸形 → null = 不强制,fail-open 到无角色态)。 */
19
+ /** Why: Preserve the established compatibility and safety behavior. */
32
20
  export function parseRuntimeManifest(envValue: string | undefined): RuntimeRoleManifest | null {
33
21
  if (!envValue) return null;
34
22
  try {
35
23
  const m = JSON.parse(envValue) as RuntimeRoleManifest;
36
24
  if (!m || typeof m.role !== 'string' || !Array.isArray(m.tools)) return null;
37
- // D82 硬化:姿态非枚举值 → 收敛为 deny(安全默认),不整个丢弃 manifest
25
+ // Why: Preserve the established compatibility and safety behavior (D82).
38
26
  if (m.unknownTools !== undefined && m.unknownTools !== 'allow' && m.unknownTools !== 'deny') {
39
27
  m.unknownTools = 'deny';
40
28
  }
@@ -55,7 +43,7 @@ export function planToolGate(toolName: string, manifest: RuntimeRoleManifest | n
55
43
  const perm = manifest.permissions[toolName] ?? manifest.permissions['*'] ?? 'allow';
56
44
  const known = manifest.tools.includes(toolName);
57
45
  const stance = manifest.unknownTools ?? 'deny';
58
- // D82:deny 规则最高优先;未知工具按姿态(deny=拒,allow=按规则走,* 兜底)
46
+ // Why: Preserve the established compatibility and safety behavior (D82).
59
47
  if (perm === 'deny' || (!known && stance === 'deny')) {
60
48
  return {
61
49
  kind: 'deny',
@@ -71,16 +59,7 @@ export function planToolGate(toolName: string, manifest: RuntimeRoleManifest | n
71
59
  return { kind: 'allow' };
72
60
  }
73
61
 
74
- /**
75
- * D77 可见层:计算 setActiveTools 的下一状态。
76
- * 交集语义(而非替换)——空交集返回 null(fail-open 保留现状),
77
- * 防"manifest 列了但插件没装"把工具集清空(如 web_search 装之前的 websearch 档案)。
78
- * 保持 currentActive 原顺序;与现状相同 → changed:false(调用方可跳过 set)。
79
- *
80
- * D82 姿态分支:unknownTools=allow → 不按 manifest 交集裁剪,只隐藏
81
- * permissions 中显式 deny 的工具(worker 排除族在继承制下仍不可见);
82
- * 未知工具(用户装的扩展)保持可见。deny(缺省)→ 原交集语义。
83
- */
62
+ /** Why: Preserve the established compatibility and safety behavior (D77, D82). */
84
63
  export function planActiveTools(
85
64
  manifestTools: string[],
86
65
  currentActive: string[],
package/src/vocab.ts CHANGED
@@ -1,55 +1,54 @@
1
1
  /**
2
- * pi-herdr 桥接词汇(M22:标题即看板)。
2
+ * pi-herdr bridge vocabulary (M22: title is the board).
3
3
  *
4
- * 这是 DESIGN.md §4.2 定义的"词汇约定":不发明新传输协议。
5
- * 传输层 = herdr socket API(NDJSON);todo 权威 = pi 会话 JSONL。
6
- * 显示投影 = pane.report_metadata 的 title / state_labels(见 pane-title.ts)。
4
+ * DESIGN.md §4.2: no new transport. Wire = herdr NDJSON; todo authority = pi
5
+ * session JSONL; display = pane.report_metadata title / state_labels.
7
6
  */
8
7
 
9
- /** todo 条目五态(D34:三态 + blocked/abandoned,对齐 OMP)。 */
8
+ /** Todo five-state (D34: three-state + blocked/abandoned, aligned with OMP). */
10
9
  export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'blocked' | 'abandoned';
11
10
 
12
11
  export const TODO_STATUSES: readonly TodoStatus[] = ['pending', 'in_progress', 'completed', 'blocked', 'abandoned'];
13
12
 
14
- /** todo 条目形状:content + 五态 status;可选 blocker(仅 blocked)、可选 phase(D43 分组)。无稳定 id(全量替换语义)。 */
13
+ /** Todo item: content + status; optional blocker (blocked only) and phase (D43). No stable id (last-write-wins). */
15
14
  export interface TodoItem {
16
15
  content: string;
17
16
  status: TodoStatus;
18
- /** 仅 status==='blocked' 时允许非空字符串(D34)。 */
17
+ /** Non-empty string only when status==='blocked' (D34). */
19
18
  blocker?: string;
20
- /** 可选分组名(D43);非空、≤30 字符。 */
19
+ /** Optional group name (D43); non-empty, ≤30 chars. */
21
20
  phase?: string;
22
21
  }
23
22
 
24
- /** 完整 todo 快照(每次 todo_write 提交的就是它,last-wins)。 */
23
+ /** Full todo snapshot (each todo_write submits this; last-wins). */
25
24
  export interface TodoSnapshot {
26
- /** 语义版本,形状变化时递增;消费方据此丢弃陈旧数据。 */
25
+ /** Shape version; consumers drop stale snapshots. */
27
26
  version: 1;
28
27
  items: TodoItem[];
29
28
  }
30
29
 
31
- /** 快照计数(反馈文案与标题投影都用它)。 */
30
+ /** Snapshot counts (confirmation copy and title projection). */
32
31
  export interface TodoCounts {
33
32
  pending: number;
34
33
  inProgress: number;
35
34
  completed: number;
36
- /** D91:blocked 单列(title 四件套 ▶○■✓)。 */
35
+ /** D91: blocked as its own column (title quartet ▶○■✓). */
37
36
  blocked: number;
38
37
  }
39
38
 
40
- /** pi 扩展经 `pane.report-agent` 上报语义状态时使用的 source 名。 */
39
+ /** Source name for pane.report-agent. */
41
40
  export const REPORT_AGENT_SOURCE = 'pi-herdr';
42
41
 
43
- /** 升级后首次上报用来清掉旧 16-key 分块的头键(M13b 残留)。 */
42
+ /** First-upgrade key used to clear the old 16-key chunked tokens (M13b leftover). */
44
43
  export const PI_HERDR_META_KEY = 'pi-herdr';
45
44
 
46
- /** todo 工具面向模型的名称(与 DSH 一致)。 */
45
+ /** Model-facing todo tool name (DSH-compatible). */
47
46
  export const TODO_TOOL_NAME = 'todo_write';
48
47
 
49
- /** 工具结果 details 里存放快照的键(随 pi 会话 JSONL 持久化,分支正确)。 */
48
+ /** Snapshot key in tool-result details (persisted in session JSONL). */
50
49
  export const TODO_DETAILS_KEY = 'pi-herdr.todo';
51
50
 
52
- /** 反馈文案(与 DSH 逐字一致,模型词汇可互迁移)。 */
51
+ /** Confirmation copy (byte-aligned with DSH so model vocab transfers). */
53
52
  export function formatTodoConfirmation(items: TodoItem[]): string {
54
53
  const c = countTodos(items);
55
54
  return `Updated todo list: ${c.pending} pending, ${c.inProgress} in progress, ${c.completed} completed.`;
@@ -65,12 +64,12 @@ export function countTodos(items: TodoItem[]): TodoCounts {
65
64
  else if (it.status === 'in_progress') inProgress++;
66
65
  else if (it.status === 'completed') completed++;
67
66
  else if (it.status === 'blocked') blocked++;
68
- // abandoned 不计入计数(D34);blocked 单列(D91 图标四件套进 title)
67
+ // abandoned is omitted (D34); blocked is its own column (D91)
69
68
  }
70
69
  return { pending, inProgress, completed, blocked };
71
70
  }
72
71
 
73
- /** 子代理结算通知文案(对齐 DSH 的 settlement notice)。 */
72
+ /** Subagent settlement notice (aligned with DSH). */
74
73
  export function formatSettlementNotice(
75
74
  agentId: string,
76
75
  closingMessage: string | null,