@bolloon/bolloon-agent 0.4.33 → 0.5.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 (214) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +580 -40
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/model-policy.js +651 -0
  31. package/dist/agents/network-pulse.js +304 -18
  32. package/dist/agents/pi-sdk-tools.js +84 -33
  33. package/dist/agents/pi-sdk.js +157 -42
  34. package/dist/agents/run-store.js +71 -5
  35. package/dist/agents/skill-readiness.js +52 -10
  36. package/dist/agents/subagent-manager.js +209 -3
  37. package/dist/agents/task/task-runner.js +17 -4
  38. package/dist/agents/task-group.js +8 -51
  39. package/dist/agents/task-public-text.js +89 -0
  40. package/dist/agents/workflow-pivot-loop.js +19 -12
  41. package/dist/cli/clipboard.js +65 -0
  42. package/dist/cli/commands/index.js +1 -0
  43. package/dist/cli/commands/tasks.js +350 -2
  44. package/dist/cli/identity-command.js +177 -0
  45. package/dist/cli/ink-app.js +145 -17
  46. package/dist/cli/log-gate.js +386 -0
  47. package/dist/cli/mention-data.js +2 -0
  48. package/dist/cli/model-selector.js +538 -0
  49. package/dist/cli/reply-hygiene.js +54 -0
  50. package/dist/cli/setup-wizard.js +757 -69
  51. package/dist/cli/startup-notice.js +160 -0
  52. package/dist/cli/stores.js +22 -1
  53. package/dist/cli/theme.js +49 -0
  54. package/dist/cli/tui-select.js +970 -0
  55. package/dist/cli/update-commands.js +150 -24
  56. package/dist/cli-entry.js +137 -20
  57. package/dist/electron-build/agents/contacts/chain.js +26 -6
  58. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  59. package/dist/electron-build/agents/execution-supervisor.js +578 -38
  60. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  61. package/dist/electron-build/agents/external-events.js +34 -5
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/goal-criteria.js +10 -3
  64. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  65. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  66. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  67. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  68. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  69. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  70. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  71. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  72. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  73. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  74. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  75. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  76. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  86. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  87. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  88. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  89. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  90. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  91. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  92. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  93. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  94. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  95. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  96. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  97. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  98. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  99. package/dist/electron-build/agents/goal-store.js +35 -1
  100. package/dist/electron-build/agents/goal-store.js.map +1 -1
  101. package/dist/electron-build/agents/model-policy.js +706 -0
  102. package/dist/electron-build/agents/model-policy.js.map +1 -0
  103. package/dist/electron-build/agents/network-pulse.js +307 -19
  104. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  105. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  106. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  107. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  108. package/dist/electron-build/agents/pi-sdk.js +156 -41
  109. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  110. package/dist/electron-build/agents/run-store.js +75 -6
  111. package/dist/electron-build/agents/run-store.js.map +1 -1
  112. package/dist/electron-build/agents/skill-readiness.js +52 -9
  113. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  114. package/dist/electron-build/agents/task/task-runner.js +16 -3
  115. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  116. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  117. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  118. package/dist/electron-build/llm/config-store.js +146 -6
  119. package/dist/electron-build/llm/config-store.js.map +1 -1
  120. package/dist/electron-build/llm/connection-probe.js +764 -0
  121. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  122. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  123. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  124. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  125. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  126. package/dist/electron-build/llm/model-catalog.js +778 -0
  127. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  128. package/dist/electron-build/llm/model-discovery.js +1202 -0
  129. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  130. package/dist/electron-build/llm/model-selection.js +1315 -0
  131. package/dist/electron-build/llm/model-selection.js.map +1 -0
  132. package/dist/electron-build/llm/pi-ai.js +70 -11
  133. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  134. package/dist/electron-build/llm/provider-catalog.js +969 -0
  135. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  136. package/dist/electron-build/llm/provider-registry.js +950 -0
  137. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  138. package/dist/electron-build/llm/tool-name.js +266 -0
  139. package/dist/electron-build/llm/tool-name.js.map +1 -0
  140. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  141. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  142. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  143. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  144. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  145. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  146. package/dist/electron-build/utils/dual-source.js +347 -0
  147. package/dist/electron-build/utils/dual-source.js.map +1 -0
  148. package/dist/electron-build/utils/update-manager.js +328 -104
  149. package/dist/electron-build/utils/update-manager.js.map +1 -1
  150. package/dist/electron-build/utils/update-state.js +9 -11
  151. package/dist/electron-build/utils/update-state.js.map +1 -1
  152. package/dist/electron-build/utils/version-identity.js +193 -0
  153. package/dist/electron-build/utils/version-identity.js.map +1 -0
  154. package/dist/electron-build/utils/version-info.js +57 -31
  155. package/dist/electron-build/utils/version-info.js.map +1 -1
  156. package/dist/index.js +385 -108
  157. package/dist/ios/bolloon-web.json +10 -0
  158. package/dist/ios/chain-explorer.js +617 -0
  159. package/dist/ios/explorer.html +163 -0
  160. package/dist/ios/index.html +96 -0
  161. package/dist/ios/mobile-agent.js +2 -2
  162. package/dist/ios/mobile-contacts.js +362 -0
  163. package/dist/ios/mobile-core.js +3102 -310
  164. package/dist/ios/mobile-privacy.js +185 -0
  165. package/dist/ios/mobile-tasks.js +339 -0
  166. package/dist/ios/mobile-update.js +1442 -0
  167. package/dist/ios/mobile.css +19 -0
  168. package/dist/ios/mobile.html +96 -0
  169. package/dist/ios/mobile.js +1013 -41
  170. package/dist/ios/routes-chain-index.js +108 -0
  171. package/dist/ios/routes-contacts.js +420 -0
  172. package/dist/ios/routes-llm-config.js +344 -52
  173. package/dist/ios/routes-mobile-tasks.js +353 -0
  174. package/dist/ios/routes-onchain-trade.js +99 -0
  175. package/dist/ios/routes-x402-info.js +1 -0
  176. package/dist/ios/server.js +921 -0
  177. package/dist/ios/sw.js +1 -1
  178. package/dist/ios/task-frame-server.js +211 -0
  179. package/dist/llm/config-store.js +146 -6
  180. package/dist/llm/connection-probe.js +744 -0
  181. package/dist/llm/custom-provider-store.js +224 -0
  182. package/dist/llm/data/provider-catalog-baked.js +19 -0
  183. package/dist/llm/model-catalog.js +717 -0
  184. package/dist/llm/model-discovery.js +1137 -0
  185. package/dist/llm/model-selection.js +1241 -0
  186. package/dist/llm/pi-ai.js +70 -11
  187. package/dist/llm/provider-catalog.js +898 -0
  188. package/dist/llm/provider-registry.js +917 -0
  189. package/dist/llm/tool-name.js +220 -0
  190. package/dist/orbitdb/cid-database.js +81 -16
  191. package/dist/orbitdb/ipfs-node.js +31 -2
  192. package/dist/setup/onboard.js +34 -8
  193. package/dist/utils/dual-source-facts.js +223 -0
  194. package/dist/utils/dual-source.js +297 -0
  195. package/dist/utils/update-cli.js +16 -21
  196. package/dist/utils/update-health.js +35 -5
  197. package/dist/utils/update-manager.js +325 -96
  198. package/dist/utils/update-state.js +13 -10
  199. package/dist/utils/version-identity.js +176 -0
  200. package/dist/utils/version-info.js +27 -22
  201. package/dist/web/bolloon-web.json +10 -0
  202. package/dist/web/mobile-contacts.js +44 -11
  203. package/dist/web/mobile-core.js +2901 -580
  204. package/dist/web/mobile-tasks.js +339 -0
  205. package/dist/web/mobile-update.js +1442 -0
  206. package/dist/web/mobile.css +19 -0
  207. package/dist/web/mobile.html +64 -0
  208. package/dist/web/mobile.js +838 -35
  209. package/dist/web/routes-llm-config.js +344 -52
  210. package/dist/web/routes-mobile-tasks.js +353 -0
  211. package/dist/web/server.js +163 -4
  212. package/dist/web/sw.js +1 -1
  213. package/package.json +3 -1
  214. package/scripts/build-web.ts +36 -0
@@ -0,0 +1,406 @@
1
+ /**
2
+ * goal-state-reducer.ts — **改动 Goal 状态的唯一漏斗** (M0 接线冻结, 2026-09-25)
3
+ *
4
+ * ## 为什么必须只有这一个漏斗
5
+ *
6
+ * 接线前, 全仓有 6 个模块各拿 `updateGoal(goalId, { status: ... })` 直接改 Goal 状态
7
+ * (Supervisor / 技能门禁 / 外部事件 / 联系方式链 / 判据提议 / Web 唤醒)。同一个 Goal 的终态
8
+ * 于是有两个以上的写入者 —— 谁最后写谁赢, 且没有任何一处能看到"这个状态是怎么来的"。
9
+ *
10
+ * 这个模块把**所有** Goal 状态变更收敛成一个两步:
11
+ *
12
+ * ① `planGoalStateChange(intent)` —— **纯函数**: 吃一个声明式的**意图** (intent), 吐出计划
13
+ * (`status` / `continuation` 补丁 / 证据 / 未解决项 / 结构化原因)。不碰盘、不读钟、不 import fs。
14
+ * 纯函数的意义: 状态判决可以被单测穷举, 也可以被门断言, 而不必先造一个真 Goal。
15
+ * ② `applyGoalStatePlan(plan)` —— **唯一**的落盘出口 (调 goal-store 的 `updateGoal` / `setContinuation`
16
+ * / `addEvidence`)。源码级门钉住: 除 `goal-store.ts` 本体外, 只有本文件允许出现
17
+ * `updateGoal(..., { status: ... })` (见 `wiring/seams.ts` 的规则 ②)。
18
+ *
19
+ * ## 边界 (刻意不做)
20
+ *
21
+ * - **不新增状态机**: intent 的名字就是既有的调用点语义, 一个不多。
22
+ * - **不新增存储**: 全部落进既有的 Goal / continuation 字段。
23
+ * - **不决定"是否继续"**: 那是 Supervisor + continuation 接缝的事 (规则 ①)。本模块只负责
24
+ * "把已经作出的决定**如实**写成 Goal 的状态", 并对完成门**不放松** (completed 仍必须过
25
+ * `completeGoalIfEligible`; 门拒绝就退回 active, 不许装作完成)。
26
+ * - **不碰 Run**: 关闭 Run 只有 closeRun 一条路 (规则 ③), 与本模块无关。
27
+ */
28
+ import { addEvidence, completeGoalIfEligible, readGoal, setContinuation, setUnresolved, updateGoal, } from './goal-store.js';
29
+ // ============================================================================
30
+ // §1. 意图 (intent) —— 声明式, 一个意图 = 一个既有调用点的语义
31
+ // ============================================================================
32
+ export const GOAL_STATE_INTENTS = [
33
+ 'closure_outcome', // Run 收尾后的合并结论 (Supervisor / Runner 独立收尾)
34
+ 'flywheel_stop', // 飞轮明确判停 (fail / pause / 无进展熔断 / 硬底线)
35
+ 'user_revoked_goal', // 用户明确撤销 → abandoned (规则 1: 用户撤销优先级最高)
36
+ 'skill_gate_block', // 技能门禁拦下 → 交人 (不启动 Run)
37
+ 'skill_gate_record', // 只记技能就绪事实, 不改状态
38
+ 'skill_upgrade_approved', // 人工批准技能升级 → 回到可继续
39
+ 'external_wait_enter', // 进入"等外部回话"
40
+ 'external_wait_expired', // 外部等待超时 → 交人
41
+ 'external_event_arrived', // 外部事件到达 → 拉回 active
42
+ 'contact_revoked_human', // 联系方式授权被撤销 → 交人
43
+ 'criteria_needs_human', // 判据提不出来 → 交人
44
+ 'block_escalated_human', // 子 Agent 被阻塞且已升级 → 交人 (规则 ④ 的一条终止路径)
45
+ 'manual_wake', // 人明确说"外部条件我已满足"
46
+ 'scheduled_wake', // 到点唤醒 (retry_wait 的 wakeAt 到了) → 回到可继续 (2026-09-25 M5 真跑)
47
+ ];
48
+ function planBase(goalId, intent, reason) {
49
+ return {
50
+ goalId,
51
+ intent,
52
+ status: null,
53
+ evidence: [],
54
+ unresolvedItems: null,
55
+ completionGate: false,
56
+ reason,
57
+ };
58
+ }
59
+ /**
60
+ * 意图 → 计划。**纯函数**: 不读盘、不读钟 (`now`/`reason` 全部由调用方注入)。
61
+ *
62
+ * 阴性对照 (门里真跑): 把 `closure_outcome` 的 `completionGate` 从 true 改成 false →
63
+ * 完成门就被绕过了, `goal-state-reducer.test.ts` 的"完成门拒绝 → 退回 active"立刻判红。
64
+ */
65
+ export function planGoalStateChange(input) {
66
+ const { goalId, now } = input;
67
+ const by = input.by ?? 'system';
68
+ const runRef = input.runId ? `run=${input.runId}` : 'run=-';
69
+ switch (input.intent) {
70
+ case 'closure_outcome': {
71
+ const o = input.outcome;
72
+ if (!o) {
73
+ // 没有结论就不许改状态 (不许"顺手"写一个)
74
+ return planBase(goalId, input.intent, '缺 outcome: 收尾结论未给出 → 不改 Goal 状态');
75
+ }
76
+ const p = planBase(goalId, input.intent, o.reason);
77
+ p.status = o.goalStatus;
78
+ p.completionGate = o.goalStatus === 'completed';
79
+ p.continuation = o.continuation;
80
+ if (o.goalStatus === 'failed' || o.goalStatus === 'abandoned') {
81
+ p.resolution = { reason: o.reason, at: now };
82
+ }
83
+ if (input.runEvidence?.length) {
84
+ p.evidence = input.runEvidence.slice(-5).map((s) => `${runRef}: ${String(s).slice(0, 200)}`);
85
+ }
86
+ return p;
87
+ }
88
+ case 'flywheel_stop': {
89
+ const status = input.stopStatus ?? 'needs_human';
90
+ const p = planBase(goalId, input.intent, `飞轮判停 (${status}): ${input.reason ?? ''}`.trim());
91
+ p.status = status;
92
+ if (status === 'failed')
93
+ p.resolution = { reason: `飞轮判不可达: ${input.reason ?? ''}`, at: now };
94
+ p.continuation = {
95
+ state: status,
96
+ autoContinue: false,
97
+ wakeAt: undefined,
98
+ wakeReason: status === 'failed' ? 'failed' : status === 'paused' ? 'paused' : 'needs_human',
99
+ lastDecisionId: input.decisionId ?? undefined,
100
+ unresolvedItems: [...(input.unresolvedItems ?? [])],
101
+ nextAction: input.nextAction || undefined,
102
+ updatedAt: now,
103
+ };
104
+ p.unresolvedItems = [...(input.unresolvedItems ?? [])].slice(0, 30);
105
+ return p;
106
+ }
107
+ case 'user_revoked_goal': {
108
+ const reason = input.reason || '用户明确撤销 → 目标终止 (不许继续跑)';
109
+ const p = planBase(goalId, input.intent, reason);
110
+ p.status = 'abandoned';
111
+ p.resolution = { reason, at: now };
112
+ p.continuation = {
113
+ autoContinue: false,
114
+ wakeAt: undefined,
115
+ wakeReason: 'active',
116
+ state: 'abandoned',
117
+ updatedAt: now,
118
+ };
119
+ p.evidence = [`用户撤销 (by ${by}): ${reason}`];
120
+ return p;
121
+ }
122
+ case 'skill_gate_block': {
123
+ const reason = input.reason || '技能未就绪';
124
+ const p = planBase(goalId, input.intent, `技能门禁拦截: ${reason}`);
125
+ p.status = 'needs_human';
126
+ p.continuation = {
127
+ wakeReason: 'needs_human',
128
+ autoContinue: false,
129
+ needsExternal: undefined,
130
+ skillReadiness: input.skillReadiness ?? { ok: false, at: now, reason },
131
+ updatedAt: now,
132
+ };
133
+ p.clearContinuationFields = ['needsExternal'];
134
+ p.evidence = [`技能门禁拦截 (by ${by}): ${reason}`];
135
+ return p;
136
+ }
137
+ case 'skill_gate_record': {
138
+ const p = planBase(goalId, input.intent, '技能就绪检查结论已记事实 (不改状态)');
139
+ p.continuation = { skillReadiness: input.skillReadiness ?? {}, updatedAt: now };
140
+ return p;
141
+ }
142
+ case 'skill_upgrade_approved': {
143
+ const p = planBase(goalId, input.intent, '人工批准技能升级 → 回到可继续');
144
+ p.status = 'active';
145
+ p.continuation = {
146
+ skillReadiness: input.skillReadiness ?? { ok: true, at: now, reason: '人工批准技能升级' },
147
+ wakeReason: 'active',
148
+ autoContinue: true,
149
+ updatedAt: now,
150
+ };
151
+ p.evidence = [input.reason ?? `技能升级已被人工批准 (by ${by})`];
152
+ return p;
153
+ }
154
+ case 'external_wait_enter': {
155
+ const p = planBase(goalId, input.intent, `进入等外部回话: ${input.externalWait ?? ''}`.trim());
156
+ p.status = 'awaiting_external';
157
+ return p;
158
+ }
159
+ case 'external_wait_expired': {
160
+ const reason = input.reason || '外部等待超时 → 转人工';
161
+ const p = planBase(goalId, input.intent, reason);
162
+ p.status = 'needs_human';
163
+ p.continuation = {
164
+ wakeReason: 'needs_human',
165
+ needsExternal: undefined,
166
+ autoContinue: false,
167
+ lastExternalTimeout: reason,
168
+ updatedAt: now,
169
+ };
170
+ p.clearContinuationFields = ['needsExternal', 'external'];
171
+ p.evidence = [`外部事件超时 (by ${by}): ${reason}`];
172
+ return p;
173
+ }
174
+ case 'external_event_arrived': {
175
+ const p = planBase(goalId, input.intent, '外部事件到达 → 拉回 active (下一 tick 真执行)');
176
+ p.status = 'active';
177
+ p.continuation = {
178
+ wakeReason: 'active',
179
+ needsExternal: undefined,
180
+ autoContinue: true,
181
+ wakeAt: undefined,
182
+ updatedAt: now,
183
+ };
184
+ p.clearContinuationFields = ['needsExternal', 'external'];
185
+ return p;
186
+ }
187
+ case 'contact_revoked_human': {
188
+ const reason = input.revokeNote || '联系方式授权被撤销 → 该联系不可自动重试';
189
+ const p = planBase(goalId, input.intent, `转人工: ${reason}`);
190
+ p.status = 'needs_human';
191
+ p.continuation = {
192
+ wakeReason: 'needs_human',
193
+ needsExternal: undefined,
194
+ autoContinue: false,
195
+ updatedAt: now,
196
+ };
197
+ p.clearContinuationFields = ['needsExternal'];
198
+ p.evidence = [`联系方式授权撤销 (by ${by}): ${reason}`];
199
+ return p;
200
+ }
201
+ case 'criteria_needs_human': {
202
+ const reason = input.reason || '判据提不出来 (目标太模糊) → 交人';
203
+ const p = planBase(goalId, input.intent, reason);
204
+ p.status = 'needs_human';
205
+ p.evidence = [`判据生成失败 (by ${by}): ${reason}`];
206
+ return p;
207
+ }
208
+ /**
209
+ * 规则 ④ 的一条终止路径: 子 Agent 被阻塞且升级到父 → 交人。
210
+ * 旧写法在接线层直接 setContinuation + updateGoal(status) —— 那是绕过 Goal reducer 的状态写入。
211
+ */
212
+ case 'block_escalated_human': {
213
+ const reason = input.reason || '子 Agent 被阻塞且已升级 → 交人';
214
+ const p = planBase(goalId, input.intent, `阻塞升级转人工: ${reason}`);
215
+ p.status = 'needs_human';
216
+ p.continuation = {
217
+ state: 'needs_human',
218
+ autoContinue: false,
219
+ wakeReason: 'needs_human',
220
+ needsExternal: (input.needsExternalText ?? reason).slice(0, 300),
221
+ // 旧写法把未解决项写在 continuation 上 (Supervisor 下一轮读的就是这里) —— 语义不变, 两个落点都写
222
+ unresolvedItems: input.unresolvedItems ?? [],
223
+ updatedAt: now,
224
+ };
225
+ if (input.unresolvedItems)
226
+ p.unresolvedItems = input.unresolvedItems;
227
+ p.evidence = [`阻塞升级转人工 (by ${by}): ${reason}`];
228
+ return p;
229
+ }
230
+ case 'manual_wake': {
231
+ const p = planBase(goalId, input.intent, `人工唤醒: ${input.reason ?? '外部条件已满足'}`);
232
+ p.status = 'active';
233
+ p.continuation = {
234
+ wakeReason: 'active',
235
+ needsExternal: undefined,
236
+ autoContinue: true,
237
+ wakeAt: undefined,
238
+ updatedAt: now,
239
+ };
240
+ p.clearContinuationFields = ['needsExternal'];
241
+ return p;
242
+ }
243
+ /**
244
+ * 到点唤醒 (2026-09-25 M5 真跑补): `retry_wait` 的 `wakeAt` 到了 → 这个目标该跑了。
245
+ *
246
+ * 为什么必须**连状态一起写**: 旧写法 (Supervisor 的 `runGoal`) 只把 continuation 上的
247
+ * `wakeAt/wakeReason` 清掉, `goal.status` 仍停在 `retry_wait`。于是这条 Run 跑完、收尾判节奏时
248
+ * P0 读到的是**stale** 的 `goal.status=retry_wait` → 判 `wait`; 合并规则再把"等"落成
249
+ * `awaiting_external` (此时 wakeAt 已被清空 ⇒ 只能靠外部事件唤醒) —— 一个**刚跑出进展**的目标
250
+ * 就这样被挂起来, 界面显示"等待外部回复", 而没有任何人会再跑它。
251
+ * 真跑证据: P6-③ 时钟用例的阴性对照判红 (`repFast.executed=0`); 临时探针复现:
252
+ * tick 后 `goal.status=awaiting_external / cont.state=awaiting_external / wakeAt=undefined`,
253
+ * 下一 tick 的跳过理由是 `awaiting_external: 等外部事件, 不重复发送`。
254
+ */
255
+ case 'scheduled_wake': {
256
+ const p = planBase(goalId, input.intent, input.reason ?? '到点唤醒 (wakeAt 已到) → 回到可继续');
257
+ p.status = 'active';
258
+ p.continuation = {
259
+ wakeReason: 'active',
260
+ wakeAt: undefined,
261
+ autoContinue: true,
262
+ state: 'active',
263
+ updatedAt: now,
264
+ };
265
+ return p;
266
+ }
267
+ default: {
268
+ // 穷举不完 = 有人加了 intent 忘了写计划 → 宁可拒绝也不静默
269
+ const never = input.intent;
270
+ return planBase(goalId, never, `未登记的意图: ${String(never)} → 拒绝改状态`);
271
+ }
272
+ }
273
+ }
274
+ /** 终态: 已终的 Goal 不许被非终态意图改回来 (历史不可改写) */
275
+ const GOAL_TERMINAL = ['completed', 'failed', 'abandoned'];
276
+ /**
277
+ * 把计划落盘。**全仓唯一的 Goal 状态写入出口**。
278
+ *
279
+ * 三条纪律:
280
+ * ① 终态不可被非终态意图改回 (除了显式的人工意图); 已终的 Goal 不改写历史;
281
+ * ② `completed` 必须过完成门 —— 门拒绝就退回 active 并**如实**记原因 (不许装作完成);
282
+ * ③ 写失败不静默: 返回 ok=false + 原因, 由调用方原样记进 report/errors。
283
+ */
284
+ export async function applyGoalStatePlan(plan) {
285
+ const res = {
286
+ ok: true, goalId: plan.goalId, intent: plan.intent, status: null, applied: [], reason: plan.reason,
287
+ };
288
+ const applied = res.applied;
289
+ const goal = await readGoal(plan.goalId).catch(() => null);
290
+ if (!goal) {
291
+ res.ok = false;
292
+ res.reason = `${plan.reason} (goal 不存在: ${plan.goalId})`;
293
+ return res;
294
+ }
295
+ // ① 终态保护: 已经完成的/失败的/放弃的目标不许被别的意图拉回来
296
+ const HUMAN_INTENTS = ['skill_upgrade_approved', 'manual_wake', 'user_revoked_goal'];
297
+ if (GOAL_TERMINAL.includes(goal.status) && plan.status && plan.status !== goal.status && !HUMAN_INTENTS.includes(plan.intent)) {
298
+ res.ok = false;
299
+ res.status = goal.status;
300
+ res.reason = `已终态 (${goal.status}) 不许被 ${plan.intent} 改写 → 拒绝 (历史不可改写)`;
301
+ return res;
302
+ }
303
+ // ② 完成门: completed 只有一条路
304
+ let effectiveStatus = plan.status;
305
+ if (plan.completionGate && plan.status === 'completed') {
306
+ const gate = await completeGoalIfEligible(plan.goalId).catch((err) => ({
307
+ ok: false, reason: `完成门自身失败: ${String(err?.message || err).slice(0, 160)}`, goal: null,
308
+ }));
309
+ if (!gate.ok) {
310
+ // 门拒绝 → 如实退回 active (保留原因), 不许装作完成
311
+ res.gateRejected = gate.reason;
312
+ effectiveStatus = 'active';
313
+ res.reason = `完成门拒绝 (${gate.reason}) → 退回 active`;
314
+ await setContinuation(plan.goalId, {
315
+ ...(plan.continuation ?? {}),
316
+ wakeReason: 'active',
317
+ autoContinue: true,
318
+ updatedAt: plan.continuation?.updatedAt ?? new Date().toISOString(),
319
+ }).then(() => applied.push('continuation:reject_rollback')).catch(() => null);
320
+ // 未解决项照旧记下来 (完成门说缺什么, 就写什么 —— 这是下一轮继续的依据, 不许丢)
321
+ const gateMissing = 'missing' in gate ? gate.missing : undefined;
322
+ if (gateMissing?.length) {
323
+ await setUnresolved(plan.goalId, gateMissing).then(() => applied.push('unresolved')).catch(() => null);
324
+ }
325
+ await updateGoal(plan.goalId, { status: 'active' }).then(() => applied.push('status:active')).catch(() => null);
326
+ res.status = (await readGoal(plan.goalId).catch(() => null))?.status ?? null;
327
+ return res;
328
+ }
329
+ applied.push('completion_gate:passed');
330
+ res.status = (await readGoal(plan.goalId).catch(() => null))?.status ?? null;
331
+ // ★ M5-⑥ (2026-09-25 真跑验收): 完成这条路**必须**把收尾 continuation 落盘。
332
+ // 旧写法 `if (plan.continuation.wakeReason !== 'completed')` 只在**不是**完成时写 —— 而走到
333
+ // 这里 (completionGate 通过) 的场景**恰好**就是 `wakeReason='completed'`
334
+ // (`decideGoalOutcome` 的完成分支 + `mergeGoalOutcome` 的 complete 分支都写 'completed'),
335
+ // 于是这个条件在唯一需要它的时刻恒假 ⇒ 完成时**一条 continuation 都不写**:
336
+ // 盘上留着上一条 Run 的收尾记录 (`state:'active'` + 旧 `lastDecisionId` + 旧 `nextAction`),
337
+ // 而 `toUserVisibleState` 只读 continuation ⇒ **已完成**的 Goal 在界面/CLI 上显示"正在执行"
338
+ // (界面比系统更乐观)。真跑证据 (`_probe-completion-continuation.ts`, 修前):
339
+ // goal.status=completed 而 applied=['completion_gate:passed'] (无 continuation 写入) →
340
+ // goalVisibleState='executing' (M5-⑨ 正向对照)。
341
+ // 完成门已经在这一步通过 (status 已真写 completed), 所以这里写的是**通过之后**的权威 continuation。
342
+ if (plan.continuation) {
343
+ await setContinuation(plan.goalId, plan.continuation).then(() => applied.push('continuation')).catch(() => null);
344
+ }
345
+ return res;
346
+ }
347
+ // ③ continuation (整片覆盖: 先清需要清的, 再写)
348
+ if (plan.continuation) {
349
+ const patch = { ...plan.continuation };
350
+ for (const f of plan.clearContinuationFields ?? []) {
351
+ if (!(f in patch))
352
+ patch[f] = undefined;
353
+ }
354
+ await setContinuation(plan.goalId, patch).then(() => applied.push('continuation')).catch(() => null);
355
+ }
356
+ // ④ 证据 / 未解决项
357
+ if (plan.evidence.length) {
358
+ await addEvidence(plan.goalId, plan.evidence).then(() => applied.push('evidence')).catch(() => null);
359
+ }
360
+ if (plan.unresolvedItems) {
361
+ await setUnresolved(plan.goalId, plan.unresolvedItems).then(() => applied.push('unresolved')).catch(() => null);
362
+ }
363
+ // ⑤ 状态本身
364
+ if (effectiveStatus && effectiveStatus !== goal.status) {
365
+ const patch = { status: effectiveStatus };
366
+ if (plan.resolution && effectiveStatus !== 'active')
367
+ patch.resolution = plan.resolution;
368
+ const next = await updateGoal(plan.goalId, patch).catch(() => null);
369
+ if (!next) {
370
+ res.ok = false;
371
+ res.reason = `${plan.reason} (状态写入失败: status=${effectiveStatus})`;
372
+ }
373
+ else {
374
+ applied.push(`status:${effectiveStatus}`);
375
+ }
376
+ }
377
+ else if (effectiveStatus) {
378
+ applied.push(`status:${effectiveStatus}(unchanged)`);
379
+ }
380
+ res.status = (await readGoal(plan.goalId).catch(() => null))?.status ?? effectiveStatus;
381
+ return res;
382
+ }
383
+ /** 意图 → 计划 → 落盘 (调用方只用这一个入口) */
384
+ export async function reduceGoalState(input) {
385
+ return applyGoalStatePlan(planGoalStateChange(input));
386
+ }
387
+ // ============================================================================
388
+ // §4. 给上层的只读辅助 (不写盘)
389
+ // ============================================================================
390
+ /**
391
+ * 这个 Goal 现在**允许**被哪个状态吗? (终态/等人态的边界查询)
392
+ *
393
+ * 存在的意义: 接线前的调用点各自手写 `!['completed','failed','abandoned'].includes(status)`,
394
+ * 六处写法不一致 (有的漏 abandoned)。收敛到一处, 由门断言只有这一处。
395
+ */
396
+ export function goalAcceptsStatusChange(current, next, intent) {
397
+ if (current === next)
398
+ return { ok: true, reason: '状态未变' };
399
+ if (GOAL_TERMINAL.includes(current)) {
400
+ const allowedByHuman = ['skill_upgrade_approved', 'manual_wake', 'user_revoked_goal'];
401
+ if (!allowedByHuman.includes(intent)) {
402
+ return { ok: false, reason: `已终态 (${current}): ${intent} 不许改写` };
403
+ }
404
+ }
405
+ return { ok: true, reason: `${current} → ${next} (${intent})` };
406
+ }
@@ -18,6 +18,7 @@ import * as fs from 'fs/promises';
18
18
  import * as path from 'path';
19
19
  import * as os from 'os';
20
20
  import * as crypto from 'crypto';
21
+ import { toUserVisibleState } from './goal-flywheel/work-monitor.js';
21
22
  /** 可被 Supervisor 自动唤醒推进的状态 (其余必须等人或等外部事件) */
22
23
  export const GOAL_RUNNABLE_STATUSES = ['open', 'active', 'recovering', 'retry_wait', 'stalled'];
23
24
  export function goalsDir() {
@@ -131,6 +132,16 @@ export async function listGoals(opts = {}) {
131
132
  out.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1));
132
133
  return typeof opts.limit === 'number' ? out.slice(0, opts.limit) : out;
133
134
  }
135
+ /**
136
+ * 低层写入原语: 把 patch 合并进 goal.json。
137
+ *
138
+ * ★ 2026-09-25 (M0 接线冻结, 规则 ②「只有 Goal reducer 能改 Goal 状态」):
139
+ * 改 `status` 的**唯一**合法调用方是 `goal-state-reducer.ts` 的 `reduceGoalState`。
140
+ * 其他任何模块想改状态, 都必须表达成一个 intent 交给 reducer (它负责: 终态保护 /
141
+ * 完成门 / continuation 的唤醒语义 / 审计痕迹)。直接在这里写 status 会被
142
+ * `src/test/goal-flywheel-m0-freeze.test.ts` 的源码级门判红 (按**文件**粒度扫, 不按行)。
143
+ * 非状态字段 (evidence / criteria / title ...) 仍可直接调本函数。
144
+ */
134
145
  export async function updateGoal(goalId, patch) {
135
146
  return withGoalLock(goalId, async () => {
136
147
  const rec = await readGoal(goalId);
@@ -520,7 +531,30 @@ export async function wakeReport(now = Date.now()) {
520
531
  }
521
532
  else if (live)
522
533
  wake = `已被 ${lease.owner} 认领`;
523
- out.push({ goalId: g.goalId, status: g.status, wake, autoContinue: c?.autoContinue !== false, lease: live ? lease.owner : undefined });
534
+ // 用户视野: 只经 toUserVisibleState 映射; 阻塞明细由飞轮接线层的 collectWorkBlocks 提供
535
+ // (这里传空阻塞表: goal-store 不读子工作目录, 保持单一事实来源)。
536
+ const visible = toUserVisibleState(continuationView(c, g.status), [], null);
537
+ out.push({ goalId: g.goalId, status: g.status, wake, autoContinue: c?.autoContinue !== false, lease: live ? lease.owner : undefined, visible, nextAction: c?.nextAction ?? '' });
524
538
  }
525
539
  return out;
526
540
  }
541
+ /**
542
+ * 把 Goal 上收敛后的 continuation 读成冻结面 `GoalContinuationRecord`。
543
+ * 缺失字段按"没有"补默认值 —— 不许因为少一个字段就崩 (界面是只读视图)。
544
+ */
545
+ function continuationView(c, status) {
546
+ if (!c)
547
+ return null;
548
+ return {
549
+ nextAction: c.nextAction ?? '',
550
+ wakeAt: c.wakeAt ?? null,
551
+ wakeReason: c.wakeReason ?? 'active',
552
+ autoContinue: c.autoContinue !== false,
553
+ requiredAgent: c.requiredAgent ?? null,
554
+ pendingReports: c.pendingReports ?? [],
555
+ unresolvedItems: c.unresolvedItems ?? [],
556
+ lastDecisionId: c.lastDecisionId ?? null,
557
+ state: c.state ?? (status === 'open' ? 'active' : status),
558
+ updatedAt: c.updatedAt ?? '',
559
+ };
560
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * mobile-flywheel-view.ts — 「飞轮进度」的手机视图投影 (2026-09-25)
3
+ *
4
+ * 手机上新加的「飞轮进度」页**只读消费** `src/agents/goal-flywheel/*` 的冻结类型, 不造第二套状态:
5
+ * · 用户可见状态 = `toUserVisibleState(continuation, blocks, decision)` (P3, 闭集五类)
6
+ * · 五类的中英文案 = `USER_VISIBLE_STATE_LABELS` (冻结表)
7
+ * · 阻塞建议动作 = `planBlockHandling` (P3, 不自动绕过 Harness)
8
+ * · 汇报字段面 = `USER_REPORT_FIELDS` (P4b 的九字段) —— 视图里的 `report` 只允许出现这九个
9
+ * · 内部字段 (lease / reducer / internal_status / retry_counter / worker_owner) **一律不出现**
10
+ *
11
+ * 本文件是纯函数 (不读钟、不做 I/O、不 import 任何 P4 之前的实现文件), 所以**桌面路由与手机 UI 可以共用一份**,
12
+ * 也就能被单测直接钉住"五类映射 + 不泄漏内部词"这两条。
13
+ *
14
+ * 刻意不做 (如实): 不读 lease / 不判"谁在跑" / 不显示 goalId·runId·workId·childAgentId (那是内部标识);
15
+ * 风险级别只透传 decision.riskLevel (高/中/低) 供 UI 提示"这一步需要你留意", 不据此自动做什么。
16
+ */
17
+ import { toUserVisibleState, planBlockHandling } from './goal-flywheel/work-monitor.js';
18
+ import { USER_VISIBLE_STATE_LABELS, USER_REPORT_FIELDS, MUST_NOT_EXPOSE_FIELDS, GOAL_TERMINAL_STATES, BLOCK_RESOLUTION_ACTIONS, } from './goal-flywheel/types.js';
19
+ /** 已冻结: 汇报字段面 + 内部字段面 (这里再导出一次, 供视图/测试直接引用) */
20
+ export { USER_REPORT_FIELDS, MUST_NOT_EXPOSE_FIELDS };
21
+ // ── 呈现用文案 (只翻译冻结枚举值, 不新增状态) ────────────────────────────────
22
+ export const BLOCK_OWNER_LABELS = {
23
+ parent: { zh: '由主任务处理', en: 'Handled by the main task' },
24
+ child: { zh: '由子任务处理', en: 'Handled by the sub-task' },
25
+ external: { zh: '等外部处理', en: 'Waiting on an external party' },
26
+ system: { zh: '由系统处理', en: 'Handled by the system' },
27
+ user: { zh: '要你来处理', en: 'Needs you' },
28
+ };
29
+ export const BLOCK_ACTION_LABELS = {
30
+ send_adjustment: { zh: '先发一次调整指令', en: 'Send an adjustment first' },
31
+ replace_child: { zh: '替换执行者', en: 'Replace the runner' },
32
+ takeover: { zh: '主任务接管', en: 'Main task takes over' },
33
+ request_report: { zh: '要求补齐报告', en: 'Request a complete report' },
34
+ escalate_parent: { zh: '上报主任务', en: 'Escalate to the main task' },
35
+ change_plan: { zh: '改计划', en: 'Change the plan' },
36
+ wait_dependency: { zh: '继续等', en: 'Keep waiting' },
37
+ needs_human: { zh: '转人工', en: 'Hand to a human' },
38
+ };
39
+ /** 阻塞类型 → 极短的人话 (只说"卡在哪一类", 不暴露内部字段名) */
40
+ export const BLOCK_KIND_LABELS = {
41
+ no_heartbeat: { zh: '子任务没有心跳了', en: 'Sub-task stopped reporting' },
42
+ waiting_dependency: { zh: '在等依赖', en: 'Waiting on a dependency' },
43
+ repeated_failure: { zh: '反复失败', en: 'Repeated failures' },
44
+ no_progress: { zh: '活着但没进展', en: 'Alive but no progress' },
45
+ tool_blocked: { zh: '工具或资源被阻', en: 'Tool or resource blocked' },
46
+ budget_blocked: { zh: '预算用尽', en: 'Budget exhausted' },
47
+ permission_blocked: { zh: '权限不足', en: 'Permission missing' },
48
+ runner_unavailable: { zh: '没有可用执行器', en: 'No runner available' },
49
+ external_timeout: { zh: '等外部超时', en: 'External wait timed out' },
50
+ report_missing: { zh: '报告不完整或没回', en: 'Report missing or incomplete' },
51
+ };
52
+ function isoOrNull(v) {
53
+ if (typeof v !== 'string' || !v)
54
+ return null;
55
+ return Number.isFinite(Date.parse(v)) ? v : null;
56
+ }
57
+ function msSince(iso, now) {
58
+ const t = iso ? Date.parse(iso) : NaN;
59
+ return Number.isFinite(t) ? Math.max(0, now - t) : 0;
60
+ }
61
+ /**
62
+ * 飞轮进度 → 手机视图。
63
+ * `continuation` 为 null 时 (终态目标没有 continuation) 仍如实给出五类之一 + 结论,
64
+ * **不编造"正在执行"** (那正是 P3 要防的假象)。
65
+ */
66
+ export function buildMobileFlywheelView(input) {
67
+ const now = Number.isFinite(input?.now) ? input.now : 0;
68
+ const continuation = input?.continuation ?? null;
69
+ // 终态**不由调用方口述**: 直接读冻结的 GOAL_TERMINAL_STATES, 与 toUserVisibleState 第 ② 步同一判据
70
+ const terminal = continuation !== null
71
+ && GOAL_TERMINAL_STATES.includes(continuation.state);
72
+ const allBlocks = Array.isArray(input?.blocks) ? input.blocks : [];
73
+ const decision = input?.decision ?? null;
74
+ // 冻结的映射: 用户可见状态只有一个来源
75
+ const visibleState = toUserVisibleState(continuation, allBlocks, decision);
76
+ const unresolved = allBlocks.filter((b) => b?.resolvedAt === null);
77
+ const blocks = unresolved.map((b) => {
78
+ // 冻结表里没有这个 kind / 调用抛错时, 页面也不能打崩 —— 退回线索里的建议动作, 再不行按"转人工"
79
+ let action;
80
+ try {
81
+ action = planBlockHandling(b);
82
+ }
83
+ catch {
84
+ action = BLOCK_RESOLUTION_ACTIONS.includes(String(b.suggestedAction))
85
+ ? b.suggestedAction
86
+ : 'needs_human';
87
+ }
88
+ return {
89
+ kind: b.kind,
90
+ kindLabel: BLOCK_KIND_LABELS[b.kind] || { zh: '未知阻塞', en: 'Unknown block' },
91
+ ownerLabel: BLOCK_OWNER_LABELS[b.owner] || { zh: '未知处理方', en: 'Unknown owner' },
92
+ suggestedAction: action,
93
+ actionLabel: BLOCK_ACTION_LABELS[action] || { zh: '转人工', en: 'Hand to a human' },
94
+ blockedForMs: msSince(b.blockedAt, now),
95
+ dependency: b.dependency ? String(b.dependency) : null,
96
+ note: String(b.note || ''),
97
+ escalationAt: isoOrNull(b.escalationAt),
98
+ lastProgressAt: typeof b.lastProgressAt === 'string' ? b.lastProgressAt : '',
99
+ };
100
+ });
101
+ const wakeAt = isoOrNull(continuation?.wakeAt);
102
+ // 终态目标不承诺"下次醒来" (即使记录里还留着旧的 wakeAt, 也不该让人以为还会醒)
103
+ const resumeInMs = terminal || !wakeAt ? null : Date.parse(wakeAt) - now;
104
+ const completed = (decision?.progressDelta?.newlyCompletedCriteria || []);
105
+ const evidence = (decision?.progressDelta?.newEvidence || []).map((e) => String(e)).slice(0, 10);
106
+ // 口径: continuation = **当前计划** (下一步/还剩什么/何时醒来); decision = **最近一次自述** (为什么/证据/已满足判据)。
107
+ // 所以还剩什么以继续记录为准, 没有才退回最近的决策记录。
108
+ const remaining = (continuation ? continuation.unresolvedItems : decision?.unresolvedItems || []).map((s) => String(s)).slice(0, 10);
109
+ const blockReasons = blocks.map((b) => `${b.kindLabel.zh} (${b.ownerLabel.zh}: ${b.actionLabel.zh})`);
110
+ const nextStep = String(continuation?.nextAction || decision?.nextAction || '').trim()
111
+ || (terminal ? '已经结束了 —— 看看结论, 决定要不要立新目标' : '(还没有写下下一步 —— 这本身就是要人看一眼的信号)');
112
+ const conclusion = decision?.reason
113
+ ? String(decision.reason)
114
+ : (terminal ? '这个目标已经结束' : String(continuation?.wakeReason || ''));
115
+ const exposed = ['conclusion', 'completed', 'evidence', 'remaining', 'blockReasons', 'nextStep', 'willContinue', 'expectedResumeAt', 'visibleState'];
116
+ const report = {
117
+ conclusion,
118
+ completed: completed.map((i) => `判据 #${i}`),
119
+ evidence,
120
+ remaining,
121
+ blockReasons,
122
+ nextStep,
123
+ willContinue: continuation?.autoContinue === true,
124
+ expectedResumeAt: wakeAt,
125
+ visibleState,
126
+ exposedFields: exposed,
127
+ generatedAt: new Date(now).toISOString(),
128
+ };
129
+ return {
130
+ visibleState,
131
+ stateLabel: USER_VISIBLE_STATE_LABELS[visibleState],
132
+ report,
133
+ blocks,
134
+ resumeInMs,
135
+ requiredAgent: continuation?.requiredAgent ? `指定执行者: ${String(continuation.requiredAgent).slice(0, 24)}` : null,
136
+ riskLevel: decision?.riskLevel ?? null,
137
+ generatedAt: new Date(now).toISOString(),
138
+ };
139
+ }
140
+ /** 视图里是否含内部字段名 (单测/门: 必须为空) */
141
+ export function findInternalFieldLeaks(text) {
142
+ const t = String(text ?? '');
143
+ return MUST_NOT_EXPOSE_FIELDS.filter((f) => t.includes(f));
144
+ }