pi-proactive 0.2.0-beta.3 → 0.2.0-beta.5

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.
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L1 内核 · 宿主无关(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 不得 import 宿主包、不得出现宿主类型/字段;对外只经宿主无关接口(HostPort 一族)。
1
3
  /**
2
4
  * `src/mailbox.ts` —— 收件侧文件邮箱(M4c:扫描 / 原子认领 / 过期归档 / 去重闭环 / 清理)。
3
5
  *
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L2 适配器 · 宿主相关(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 宿主概念只许出现在这一层;这里的耦合是**适配**,不是内核耦合。
1
3
  /**
2
4
  * 设备级通知(形态 A · N1)与「会话内联系」**共用同一份载荷**。
3
5
  *
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L3 宿主集成(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 直接面对宿主/操作系统;内核面(机制/协议/记录/决策/目标/接口)不在本文件。
1
3
  /**
2
4
  * `src/peer-spawn.ts` —— **创建**一个对端会话(peer 生命周期的「创建」这条腿)。
3
5
  *
package/build/src/peer.js CHANGED
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L1 内核 · 宿主无关(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 不得 import 宿主包、不得出现宿主类型/字段;对外只经宿主无关接口(HostPort 一族)。
1
3
  /**
2
4
  * `src/peer.ts` —— M5a:**逐条** reply 归因(问 → 答 → 回信 一一对应)。
3
5
  *
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L1 内核 · 宿主无关(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 不得 import 宿主包、不得出现宿主类型/字段;对外只经宿主无关接口(HostPort 一族)。
1
3
  import fs from "node:fs";
2
4
  import os from "node:os";
3
5
  import path from "node:path";
@@ -1,3 +1,5 @@
1
+ // 【S0 归属】L1 内核 · 宿主无关(契约:docs/project/kernel-contract-s0.md §6.1)
2
+ // → 不得 import 宿主包、不得出现宿主类型/字段;对外只经宿主无关接口(HostPort 一族)。
1
3
  /**
2
4
  * `src/socket.ts` —— 跨进程实时传输通道(M4b:unix domain socket)。
3
5
  *
@@ -0,0 +1,355 @@
1
+ /**
2
+ * 监督面的**状态聚合**(双面同源的单一真源)。
3
+ *
4
+ * 为什么要单独一个模块:Web 看板与 pi-agent 的 TUI 面板是**两个渲染层**,
5
+ * 若各算各的 → 两处数字必然漂移(这是本仓库反复踩到的坑)。
6
+ * ⇒ 本模块**只读**既有事实面,算出**一次**,两个渲染层各自呈现、**不重算**。
7
+ *
8
+ * 只读的事实面(**不新造数据结构、不新增协议**):
9
+ * - 事件流(血缘)`<agentDir>/proactive/lineage/*.jsonl` → 联系时间线 + 四段计数;
10
+ * - 心跳 `<agentDir>/proactive/daemon/heartbeat.json` → 在不在 / 宿主 / 进程 / 心跳龄期;
11
+ * - 值班状态 `<agentDir>/proactive/daemon/state.json` → 预算已用 + 人工确认队列;
12
+ * - 配置 `<agentDir>/proactive/daemon.json` → 预算硬上限;
13
+ * - 托付登记册 `<agentDir>/proactive/trust/registry.json` → 托付 / 目标。
14
+ *
15
+ * 口径说明:
16
+ * - 四段计数 = **按事件类型计数**(triggered / delivered / answered / outcome),
17
+ * 与 `scripts/team/progress-snapshot.sh` 的守恒等式**同一口径**(第三方可独立复算);
18
+ * - 托付四段(登记/到达/知会/结账)直接复用 `src/trust-metrics.ts` 的守恒判定,**不另写一套**;
19
+ * - **脱敏默认**:会话正文只给长度与摘要(sha256 前 12 位);提示词原文、队列原始提示一律不进结果。
20
+ *
21
+ * **只读**:本模块不写任何文件(坏文件只跳过、不修复、不备份)。
22
+ */
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import { proactiveDir, resolveAgentDir } from "./config.js";
26
+ import { daemonConfigPath, daemonStatePath, heartbeatPath, loadDaemonConfig } from "./daemon-config.js";
27
+ import { formatAgo, readAssistantStatus } from "./assistant.js";
28
+ import { computeTrustMetrics, readLineageEvents } from "./trust-metrics.js";
29
+ import { TrustRegistry } from "./trust-registry.js";
30
+ /** 读 JSON(坏文件 / 不存在 / 不是对象 → undefined,绝不抛、绝不写)。 */
31
+ function readJsonSafe(file) {
32
+ try {
33
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
34
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
35
+ return undefined;
36
+ return parsed;
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ function num(v, fallback) {
43
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
44
+ }
45
+ function str(v) {
46
+ return typeof v === "string" && v !== "" ? v : undefined;
47
+ }
48
+ /** 一条事件的人话摘要(**只描述事件事实,不含正文**)。 */
49
+ function describeEvent(e) {
50
+ switch (e.type) {
51
+ case "triggered":
52
+ return `触发(${e.cause})`;
53
+ case "delivered":
54
+ return `投递 → ${e.result ?? "(无结果)"}`;
55
+ case "answered":
56
+ return `回应(${e.responder ?? "未知"})`;
57
+ case "outcome":
58
+ return `结账 → ${e.result ?? "(无结果)"}`;
59
+ case "dropped":
60
+ return `拦下(${e.reason ?? "未注明"})`;
61
+ case "decision":
62
+ return `决策 → ${e.decision?.decision ?? "(无)"}(${e.decision?.reason ?? "未注明"})`;
63
+ case "trust":
64
+ return `托付 · ${e.trust?.stage ?? "(无阶段)"}`;
65
+ default:
66
+ return e.type;
67
+ }
68
+ }
69
+ /** 时间线一条(脱敏)。 */
70
+ function toTimelineEntry(e) {
71
+ const entry = {
72
+ ts: e.ts,
73
+ type: e.type,
74
+ cause: e.cause,
75
+ summary: describeEvent(e),
76
+ };
77
+ if (e.bodyLen !== undefined)
78
+ entry.bodyLen = e.bodyLen;
79
+ if (e.bodyDigest !== undefined)
80
+ entry.bodyDigest = e.bodyDigest;
81
+ if (e.peer !== undefined)
82
+ entry.peer = e.peer;
83
+ const detail = e.result ?? e.reason;
84
+ if (detail !== undefined)
85
+ entry.detail = detail;
86
+ return entry;
87
+ }
88
+ /**
89
+ * 采集监督面状态(**只读**)。
90
+ *
91
+ * @param opts 见 {@link SupervisorOptions}。
92
+ */
93
+ export function collectSupervisorState(opts = {}) {
94
+ const agentDir = opts.agentDir ?? resolveAgentDir();
95
+ const now = opts.now ?? Date.now();
96
+ const agentId = opts.agentId ?? "supervisor";
97
+ const limit = opts.timelineLimit ?? 50;
98
+ // ---- 事件流(血缘):时间线 + 四段计数 + 托付四段 ----
99
+ const lineageDir = path.join(proactiveDir(agentDir), "lineage");
100
+ const events = readLineageEvents(lineageDir);
101
+ const sorted = [...events].sort((a, b) => a.ts - b.ts);
102
+ let triggered = 0;
103
+ let delivered = 0;
104
+ let answered = 0;
105
+ let outcome = 0;
106
+ for (const e of sorted) {
107
+ if (e.type === "triggered")
108
+ triggered += 1;
109
+ else if (e.type === "delivered")
110
+ delivered += 1;
111
+ else if (e.type === "answered")
112
+ answered += 1;
113
+ else if (e.type === "outcome")
114
+ outcome += 1;
115
+ }
116
+ const metrics = computeTrustMetrics(sorted);
117
+ const timeline = sorted.slice(Math.max(0, sorted.length - limit)).map(toTimelineEntry);
118
+ // ---- 状态卡(心跳 / 上次联系):复用既有读法,不另写一套 ----
119
+ const assistant = readAssistantStatus(agentDir, now);
120
+ const hb = readJsonSafe(heartbeatPath(agentDir));
121
+ const status = {
122
+ present: assistant.present,
123
+ running: assistant.installed,
124
+ pidAlive: assistant.pidAlive,
125
+ };
126
+ if (assistant.notRunningReason !== undefined)
127
+ status.notRunningReason = assistant.notRunningReason;
128
+ if (assistant.heartbeatTs !== undefined)
129
+ status.heartbeatTs = assistant.heartbeatTs;
130
+ if (assistant.heartbeatAgeMs !== undefined)
131
+ status.heartbeatAgeMs = assistant.heartbeatAgeMs;
132
+ if (assistant.lastContactTs !== undefined) {
133
+ status.lastContactTs = assistant.lastContactTs;
134
+ status.lastContactAgeMs = Math.max(0, now - assistant.lastContactTs);
135
+ }
136
+ if (hb !== undefined) {
137
+ const host = str(hb.host);
138
+ if (host !== undefined)
139
+ status.host = host;
140
+ const pid = hb.pid;
141
+ if (typeof pid === "number" && Number.isFinite(pid))
142
+ status.pid = pid;
143
+ const hbAgent = str(hb.agentId);
144
+ if (hbAgent !== undefined)
145
+ status.agentId = hbAgent;
146
+ const runMode = str(hb.runMode);
147
+ if (runMode !== undefined)
148
+ status.runMode = runMode;
149
+ if (typeof hb.enabled === "boolean")
150
+ status.enabled = hb.enabled;
151
+ if (typeof hb.contactEnabled === "boolean")
152
+ status.contactEnabled = hb.contactEnabled;
153
+ }
154
+ // ---- 预算(上限来自配置,已用来自值班状态)----
155
+ const configured = fs.existsSync(daemonConfigPath(agentDir));
156
+ const daemonConfig = loadDaemonConfig(agentDir, agentId).config;
157
+ const limits = daemonConfig.budget;
158
+ const rawState = readJsonSafe(daemonStatePath(agentDir));
159
+ const used = rawState?.budget ?? {};
160
+ const hourCount = num(used.hourCount, 0);
161
+ const dayCount = num(used.dayCount, 0);
162
+ const limitedDayCount = num(used.limitedDayCount, 0);
163
+ const tokenUnits = num(used.tokenUnits, 0);
164
+ const budget = {
165
+ configured,
166
+ hourCount,
167
+ maxActionsPerHour: limits.maxActionsPerHour,
168
+ hourRemaining: Math.max(0, limits.maxActionsPerHour - hourCount),
169
+ dayCount,
170
+ maxActionsPerDay: limits.maxActionsPerDay,
171
+ dayRemaining: Math.max(0, limits.maxActionsPerDay - dayCount),
172
+ limitedDayCount,
173
+ maxLimitedActionsPerDay: limits.maxLimitedActionsPerDay,
174
+ limitedDayRemaining: Math.max(0, limits.maxLimitedActionsPerDay - limitedDayCount),
175
+ tokenUnits,
176
+ maxTokenUnitsPerDay: limits.maxTokenUnitsPerDay,
177
+ tokenRemaining: Math.max(0, limits.maxTokenUnitsPerDay - tokenUnits),
178
+ exhausted: false,
179
+ degradeToObserve: limits.degradeToObserveWhenExhausted,
180
+ };
181
+ if (hourCount >= limits.maxActionsPerHour) {
182
+ budget.exhausted = true;
183
+ budget.reason = `每小时动作上限已满(${hourCount}/${limits.maxActionsPerHour})`;
184
+ }
185
+ else if (dayCount >= limits.maxActionsPerDay) {
186
+ budget.exhausted = true;
187
+ budget.reason = `每日动作上限已满(${dayCount}/${limits.maxActionsPerDay})`;
188
+ }
189
+ else if (tokenUnits >= limits.maxTokenUnitsPerDay) {
190
+ budget.exhausted = true;
191
+ budget.reason = `每日 token 估算上限已满(${tokenUnits}/${limits.maxTokenUnitsPerDay},估算口径非实测)`;
192
+ }
193
+ // ---- 托付登记册(坏文件 → 空册,绝不抛)----
194
+ let goals = [];
195
+ let entrustments = [];
196
+ try {
197
+ const registry = new TrustRegistry({ agentDir, agentId });
198
+ goals = registry.listGoals();
199
+ entrustments = registry.listEntrustments();
200
+ }
201
+ catch {
202
+ // 坏登记册 → 当作空册(只读面不做任何修复写)
203
+ }
204
+ const trusts = entrustments.map((e) => {
205
+ const entry = { id: e.id, target: e.target, status: e.status, createdAt: e.createdAt };
206
+ if (e.goalId !== undefined)
207
+ entry.goalId = e.goalId;
208
+ if (e.arrivedAt !== undefined)
209
+ entry.arrivedAt = e.arrivedAt;
210
+ if (e.notifiedAt !== undefined)
211
+ entry.notifiedAt = e.notifiedAt;
212
+ if (e.settledAt !== undefined)
213
+ entry.settledAt = e.settledAt;
214
+ if (e.revokedAt !== undefined)
215
+ entry.revokedAt = e.revokedAt;
216
+ if (e.needsHuman === true)
217
+ entry.needsHuman = true;
218
+ if (e.humanReason !== undefined)
219
+ entry.humanReason = e.humanReason;
220
+ return entry;
221
+ });
222
+ const goalEntries = goals.map((g) => {
223
+ const entry = {
224
+ id: g.id,
225
+ title: g.title,
226
+ status: g.status,
227
+ progressSteps: g.progressSteps,
228
+ maxActions: g.budget?.maxActions ?? 0,
229
+ usedActions: g.budget?.usedActions ?? 0,
230
+ permission: g.permission,
231
+ updatedAt: g.updatedAt,
232
+ };
233
+ if (g.humanReason !== undefined)
234
+ entry.humanReason = g.humanReason;
235
+ return entry;
236
+ });
237
+ // ---- 阻塞(等人决策 / 预算闸门 / 动作失败)----
238
+ const blockers = [];
239
+ for (const g of goalEntries) {
240
+ if (g.status === "needs-human") {
241
+ blockers.push({
242
+ id: `goal:${g.id}`,
243
+ category: "human-decision",
244
+ title: `目标等人工决策:${g.title}`,
245
+ ...(g.humanReason !== undefined ? { detail: g.humanReason } : {}),
246
+ since: g.updatedAt,
247
+ });
248
+ }
249
+ }
250
+ for (const t of trusts) {
251
+ if (t.needsHuman === true) {
252
+ blockers.push({
253
+ id: `entrustment:${t.id}`,
254
+ category: "human-decision",
255
+ title: `托付等人工决策:${t.target}`,
256
+ ...(t.humanReason !== undefined ? { detail: t.humanReason } : {}),
257
+ since: t.createdAt,
258
+ });
259
+ }
260
+ }
261
+ for (const q of rawState?.queue ?? []) {
262
+ const title = str(q.title) ?? "(未命名动作)";
263
+ const reason = str(q.reason);
264
+ blockers.push({
265
+ id: `queue:${str(q.id) ?? String(q.ts ?? 0)}`,
266
+ category: "human-decision",
267
+ title: `待人工确认:${title}`,
268
+ ...(reason !== undefined ? { detail: reason } : {}),
269
+ ...(typeof q.ts === "number" ? { since: q.ts } : {}),
270
+ });
271
+ }
272
+ if (budget.exhausted) {
273
+ blockers.push({
274
+ id: "budget:exhausted",
275
+ category: "budget",
276
+ title: budget.reason ?? "预算已耗尽",
277
+ });
278
+ }
279
+ for (const a of rawState?.actions ?? []) {
280
+ if (a.outcome !== "failed")
281
+ continue;
282
+ const title = str(a.title) ?? "(未命名动作)";
283
+ const reason = str(a.reason);
284
+ blockers.push({
285
+ id: `action:${String(a.ts ?? 0)}`,
286
+ category: "action-failed",
287
+ title: `动作失败:${title}`,
288
+ ...(reason !== undefined ? { detail: reason } : {}),
289
+ ...(typeof a.ts === "number" ? { since: a.ts } : {}),
290
+ });
291
+ }
292
+ const conservation = {
293
+ triggered,
294
+ delivered,
295
+ answered,
296
+ outcome,
297
+ equal: triggered === delivered && delivered === answered && answered === outcome,
298
+ entrustmentRegistered: metrics.conservation.registered,
299
+ entrustmentArrived: metrics.conservation.arrived,
300
+ entrustmentNotified: metrics.conservation.notified,
301
+ entrustmentSettled: metrics.conservation.settled,
302
+ entrustmentRevoked: metrics.conservation.revoked,
303
+ entrustmentInFlight: metrics.conservation.inFlight,
304
+ entrustmentBalanced: metrics.conservation.balanced,
305
+ entrustmentViolations: metrics.conservation.violations,
306
+ responseRate: metrics.responseRate.value,
307
+ closureRate: metrics.entrustmentClosureRate.value,
308
+ watchDurationMs: metrics.watchDuration.averageMs,
309
+ };
310
+ return {
311
+ generatedAt: now,
312
+ agentDir,
313
+ status,
314
+ timeline,
315
+ trusts,
316
+ goals: goalEntries,
317
+ budget,
318
+ blockers,
319
+ conservation,
320
+ };
321
+ }
322
+ // ---------------------------------------------------------------- 共享口径文本
323
+ //
324
+ // 为什么文本也放在这里:两个渲染面(Web 看板 / TUI 面板)显示的**同一组数字**
325
+ // 必须**逐字一致**(验收 A8)→ 文本构造与聚合放在同一处,两边只是摆放。
326
+ // (若各写各的字符串,迟早漂移 —— 这正是本仓库反复踩到的坑。)
327
+ /** 「助理在不在」的一行摘要(**状态栏一行**:在不在 · 上次联系 · 预算余量)。 */
328
+ export function formatSupervisorSummary(state) {
329
+ const where = state.status.running ? "在" : state.status.present ? "不在(装过但没在跑)" : "不在";
330
+ const contact = state.status.lastContactAgeMs === undefined ? "从未联系" : `${formatAgo(state.status.lastContactAgeMs)}`;
331
+ return `助理 ${where} · 上次联系 ${contact} · 预算余量 每日 ${state.budget.dayRemaining}/${state.budget.maxActionsPerDay}(小时 ${state.budget.hourRemaining}/${state.budget.maxActionsPerHour})`;
332
+ }
333
+ /** 四段守恒等式的一行(`triggered=… delivered=… answered=… outcome=…`)。 */
334
+ export function formatConservationLine(state) {
335
+ const c = state.conservation;
336
+ return `triggered=${c.triggered} delivered=${c.delivered} answered=${c.answered} outcome=${c.outcome} → ${c.equal ? "相等(无悬空)" : "有悬空"}`;
337
+ }
338
+ /** 托付四段守恒等式的一行。 */
339
+ export function formatEntrustmentConservationLine(state) {
340
+ const c = state.conservation;
341
+ return `登记=${c.entrustmentRegistered} 到达=${c.entrustmentArrived} 知会=${c.entrustmentNotified} 结账=${c.entrustmentSettled} 撤销=${c.entrustmentRevoked} 在途=${c.entrustmentInFlight} → ${c.entrustmentBalanced ? "守恒" : "有悬空"}`;
342
+ }
343
+ /** 预算的一行(三档余量)。 */
344
+ export function formatBudgetLine(state) {
345
+ const b = state.budget;
346
+ return `小时 ${b.hourCount}/${b.maxActionsPerHour}(余 ${b.hourRemaining})· 每日 ${b.dayCount}/${b.maxActionsPerDay}(余 ${b.dayRemaining})· token ${b.tokenUnits}/${b.maxTokenUnitsPerDay}(余 ${b.tokenRemaining})`;
347
+ }
348
+ /** 状态卡的一行(宿主 / 进程 / 心跳龄期)。 */
349
+ export function formatStatusLine(state) {
350
+ const s = state.status;
351
+ const host = s.host ?? "未知";
352
+ const pid = s.pid === undefined ? "无" : String(s.pid);
353
+ const age = s.heartbeatAgeMs === undefined ? "无心跳" : formatAgo(s.heartbeatAgeMs);
354
+ return `宿主 ${host} · 进程 ${pid}(${s.pidAlive ? "活" : "不在"})· 心跳 ${age}`;
355
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * TUI 监督面(**只读渲染层**):状态栏摘要 + 可展开面板。
3
+ *
4
+ * 与 Web 面**同源**:两个渲染层都消费 `src/supervisor-state.ts` 的**同一份聚合**,
5
+ * 且关键数字的**文本构造**也在那个模块里(`formatConservationLine` 等)→
6
+ * 两处的数字**逐字一致**(验收 A8),不是"各算各的、碰巧一样"。
7
+ *
8
+ * 本模块**纯文本**、不依赖宿主 API(无 ANSI、无终端宽度库):
9
+ * 着色由扩展层用宿主的 theme 加,宽度裁剪由扩展层做 → 本模块可被纯 Node 测试直接断言。
10
+ */
11
+ import { formatBudgetLine, formatConservationLine, formatEntrustmentConservationLine, formatStatusLine, formatSupervisorSummary, } from "./supervisor-state.js";
12
+ /** 毫秒 → `MM-DD HH:mm:ss`(本地时区)。 */
13
+ function fmtTime(ts) {
14
+ const d = new Date(ts);
15
+ const p = (n) => String(n).padStart(2, "0");
16
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
17
+ }
18
+ /**
19
+ * 渲染监督面板的行(**只读**:没有任何可点的操作)。
20
+ *
21
+ * @param state 同一份聚合状态(Web 面用的是同一个对象来源)。
22
+ * @param opts 显示条数上限。
23
+ */
24
+ export function formatSupervisorPanel(state, opts = {}) {
25
+ const timelineLimit = opts.timelineLimit ?? 10;
26
+ const trustLimit = opts.trustLimit ?? 10;
27
+ const blockerLimit = opts.blockerLimit ?? 10;
28
+ const lines = [];
29
+ lines.push("监督面(只读 · 改配置请回 Pi 会话)");
30
+ lines.push(`状态 ${formatSupervisorSummary(state)}`);
31
+ lines.push(` ${formatStatusLine(state)}`);
32
+ const timeline = state.timeline.slice(Math.max(0, state.timeline.length - timelineLimit)).reverse();
33
+ lines.push(`时间线(最近 ${timeline.length} 条 · 会话正文只给长度/摘要)`);
34
+ if (timeline.length === 0) {
35
+ lines.push(" 暂无记录");
36
+ }
37
+ else {
38
+ for (const t of timeline) {
39
+ const len = t.bodyLen === undefined ? "" : ` · 正文 ${t.bodyLen} 字`;
40
+ const digest = t.bodyDigest === undefined ? "" : ` · 摘要 ${t.bodyDigest}`;
41
+ const peer = t.peer === undefined ? "" : ` · 对端 ${t.peer}`;
42
+ lines.push(` ${fmtTime(t.ts)} ${t.summary}${len}${digest}${peer}`);
43
+ }
44
+ }
45
+ lines.push(`托付(${state.trusts.length})`);
46
+ if (state.trusts.length === 0) {
47
+ lines.push(" 暂无托付");
48
+ }
49
+ else {
50
+ for (const t of state.trusts.slice(0, trustLimit)) {
51
+ const human = t.needsHuman ? " · 需人决策" : "";
52
+ const reason = t.humanReason === undefined ? "" : ` · ${t.humanReason}`;
53
+ lines.push(` ${t.target} · ${t.status}${human}${reason}`);
54
+ }
55
+ }
56
+ if (state.goals.length > 0) {
57
+ lines.push(`目标(${state.goals.length})`);
58
+ for (const g of state.goals.slice(0, trustLimit)) {
59
+ const reason = g.humanReason === undefined ? "" : ` · ${g.humanReason}`;
60
+ lines.push(` ${g.title} · ${g.status} · 推进 ${g.progressSteps} 步 · 行动 ${g.usedActions}/${g.maxActions}${reason}`);
61
+ }
62
+ }
63
+ lines.push(`阻塞(${state.blockers.length})`);
64
+ if (state.blockers.length === 0) {
65
+ lines.push(" 无阻塞");
66
+ }
67
+ else {
68
+ for (const b of state.blockers.slice(0, blockerLimit)) {
69
+ const detail = b.detail === undefined ? "" : ` · ${b.detail}`;
70
+ lines.push(` ${b.title}${detail}`);
71
+ }
72
+ }
73
+ lines.push(`预算 ${formatBudgetLine(state)}`);
74
+ lines.push(`守恒 ${formatConservationLine(state)}`);
75
+ lines.push(` ${formatEntrustmentConservationLine(state)}`);
76
+ return lines;
77
+ }
78
+ /**
79
+ * 非交互模式(json / print / rpc 无 TUI)下的**纯文本**呈现。
80
+ *
81
+ * 与面板**逐字同源**(同一份聚合 + 同一批文本构造)→ 监督信息不依赖 TUI 能力(验收 A10)。
82
+ */
83
+ export function formatSupervisorText(state, opts = {}) {
84
+ return formatSupervisorPanel(state, opts).join("\n");
85
+ }