@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,818 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLOSURE_STEP_ORDER = exports.GOAL_STATE_FOR_DECISION = exports.CLOSURE_RESULT_KEYS_EXACT = exports.RunClosureError = exports.CLOSURE_RESULT_FIELDS = exports.CLOSURE_MIN_PURPOSE_LENGTH = exports.CLOSURE_DEFAULT_NO_PROGRESS_CIRCUIT_BREAKER = exports.CLOSURE_MAX_EVIDENCE = exports.CLOSURE_MAX_FACTS = void 0;
4
+ exports.parseFinalReview = parseFinalReview;
5
+ exports.gateClosureCandidate = gateClosureCandidate;
6
+ exports.closeRun = closeRun;
7
+ exports.auditClosureSteps = auditClosureSteps;
8
+ /**
9
+ * goal-flywheel/run-closure.ts — P1「强制收尾飞轮」(2026-09-25)
10
+ *
11
+ * 职责: 把 **Run 结束** 这件事变成一条**固定顺序的流水线**, 成功 / 失败 / 中断恢复后的 Run
12
+ * 都必须走同一条路 —— 而不是"只在成功时顺手收个尾"。
13
+ * 产物四类 (缺任何一类 = 收尾未完成): 事实 / 教训 / Skill 改进候选 / 下一步建议。
14
+ * 另外两份输出 (§P4b): 用户汇报 `UserReport` + 机器继续记录 `GoalContinuationRecord`。
15
+ *
16
+ * 边界 (与设计文档 `docs/wiki/goal-continuation-flywheel.md` §13/§14 一致):
17
+ * - **只读** `types.ts` (冻结面), 只通过它的类型与别的阶段耦合; 实现文件之间零 import。
18
+ * - 依赖**全部参数注入** (`CloseRunDeps`): Memory 写入 / Skill 候选写入 / 继续决策。
19
+ * P1b 的 `writeMemoryRecords`、P1b 的 `writeCandidate` 包装、P0 的 `decideContinuation`
20
+ * 在接线时被注入进来即可 —— 本文件不 import 它们 (那也是它们此刻不存在的原因)。
21
+ * - 时间一律 `now: IsoTimestamp` 注入; **不读真实钟**, 不 import `fs`, 不做任何 I/O。
22
+ *
23
+ * 副作用时序 (如实说明): `deps.decide` 在**读完步骤与证据后立刻**调用 —— 因为 §5 的第 6/7/8
24
+ * 步 (写 Skill 候选 / 更新 Goal continuation / 用户汇报) 都要用它的返回值。而 `steps`
25
+ * 记录的是「收尾流水线走了哪 9 步」(§5 的冻结清单), 不是副作用的毫秒时序。
26
+ *
27
+ * `finalReview` 的形状 (P1 的输入契约): 一个**字符串**。
28
+ * - 是结构化 JSON (或文本里带 ```json 围栏 / 首个 `{...}` 块) 且带 `reviewedBy` → 被采信;
29
+ * 参照 `bolloon-run-final-review/1`:
30
+ * {
31
+ * "reviewedBy": "leo", // 必填 —— 没有评审人的评审不予采信
32
+ * "verdict": "reusable" | "one_off" | "unknown",
33
+ * "methodEffective": "…", "methodFailed": "…", "nextTimeChange": "…", // lesson 三件套
34
+ * "facts": [{ "claim": "…", "assertion": "confirmed"|"inferred", "refs": ["…"] }],
35
+ * "skills": [{ "name": "…", "purpose": "…", "occurrences": 2, "boundaryClear": true,
36
+ * "inputSchema": "…", "outputSchema": "…", "guarantees": ["…"],
37
+ * "doesNotGuarantee": ["…"], "failureCases": ["…"], "evidenceRefs": ["…"] }]
38
+ * }
39
+ * - 是一段散文 (或空) → **仍然收尾**, 但明确记为"评审非结构化": 不产 lesson, 不产 skill 候选,
40
+ * 并把原因写进 `skipped`。这是刻意的: 收尾不能因为评审写得随意就静默跳过 (§5)。
41
+ *
42
+ * 两处**收尾收紧** (与 P0 的 `applyHardLimits` 同一精神: 只能收紧, 不能放宽):
43
+ * ① 决策为 `complete` 但本轮**没有任何已确认证据** → 不许以"完成"收场, 收紧为 `ask_human`;
44
+ * ② 决策为 `complete` 但 `unresolvedItems` 非空 → 同上 (未解决项非空则不许判完成)。
45
+ * 所有收紧都在 `skipped` 里留下结构化原因, 并写进用户汇报的结论里。
46
+ */
47
+ const types_js_1 = require("./types.js");
48
+ // ============================================================================
49
+ // 上限 / 退化值 (全部显式命名, 便于接线层核对, 不做暗默认)
50
+ // ============================================================================
51
+ /** 一次收尾最多写多少条事实 (超出部分丢弃并记 `facts_capped`) */
52
+ exports.CLOSURE_MAX_FACTS = 50;
53
+ /** 用户汇报里最多列多少条证据引用 */
54
+ exports.CLOSURE_MAX_EVIDENCE = 50;
55
+ /** 调用方没注入 `hardLimits` 时的无进展熔断阈值 (退化值, 安全线仍由真的接线层注入) */
56
+ exports.CLOSURE_DEFAULT_NO_PROGRESS_CIRCUIT_BREAKER = 3;
57
+ /** lesson 的一句话门槛: 短于此长度视为"只是一句经验", 不写 lesson */
58
+ exports.CLOSURE_MIN_PURPOSE_LENGTH = 10;
59
+ /** §14 冻结的返回字段 (运行时键集必须**恰好**是这些) */
60
+ exports.CLOSURE_RESULT_FIELDS = [
61
+ 'steps',
62
+ 'facts',
63
+ 'lessons',
64
+ 'candidates',
65
+ 'nextStep',
66
+ 'decision',
67
+ 'userReport',
68
+ 'continuation',
69
+ 'skipped',
70
+ ];
71
+ /** 收尾自身无法继续的错误 (例如决策依赖失效 —— 宁可显式失败, 不编一个决策出来) */
72
+ class RunClosureError extends Error {
73
+ step;
74
+ constructor(step, message) {
75
+ super(message);
76
+ this.name = 'RunClosureError';
77
+ this.step = step;
78
+ }
79
+ }
80
+ exports.RunClosureError = RunClosureError;
81
+ const RESULT_KEYS_MATCH_SECTION14 = true;
82
+ /** 编译期门: `CloseRunResult` 的键集与 §14 完全一致 (多/少一个 → tsc 判红) */
83
+ exports.CLOSURE_RESULT_KEYS_EXACT = true;
84
+ function asText(v) {
85
+ return typeof v === 'string' ? v.trim() : '';
86
+ }
87
+ function asTextArray(v) {
88
+ if (!Array.isArray(v))
89
+ return [];
90
+ return v.filter((x) => typeof x === 'string').map((s) => s.trim()).filter((s) => s.length > 0);
91
+ }
92
+ function asFiniteNumber(v, dflt) {
93
+ return typeof v === 'number' && Number.isFinite(v) ? v : dflt;
94
+ }
95
+ function asBool(v, dflt) {
96
+ return typeof v === 'boolean' ? v : dflt;
97
+ }
98
+ /** 从文本里捞出评审 JSON: 整体 → ```json 围栏 → 首个 `{...}` 块 */
99
+ function extractReviewJson(text) {
100
+ const t = text.trim();
101
+ if (!t)
102
+ return undefined;
103
+ const tryParse = (s) => {
104
+ try {
105
+ return JSON.parse(s);
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ };
111
+ const whole = tryParse(t);
112
+ if (whole !== undefined)
113
+ return whole;
114
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(t);
115
+ if (fenced) {
116
+ const parsed = tryParse(fenced[1].trim());
117
+ if (parsed !== undefined)
118
+ return parsed;
119
+ }
120
+ const first = t.indexOf('{');
121
+ const last = t.lastIndexOf('}');
122
+ if (first >= 0 && last > first) {
123
+ const parsed = tryParse(t.slice(first, last + 1));
124
+ if (parsed !== undefined)
125
+ return parsed;
126
+ }
127
+ return undefined;
128
+ }
129
+ /**
130
+ * 解析 Final Review。**没有评审人就不采信** (lesson 必须带 `reviewedBy`)。
131
+ * 理由码: empty_final_review · no_json_found · not_a_json_object · missing_reviewed_by · ok
132
+ */
133
+ function parseFinalReview(finalReview) {
134
+ const empty = {
135
+ structured: false,
136
+ reason: 'empty_final_review',
137
+ reviewedBy: null,
138
+ verdict: 'unknown',
139
+ methodEffective: '',
140
+ methodFailed: '',
141
+ nextTimeChange: '',
142
+ facts: [],
143
+ skills: [],
144
+ };
145
+ const raw = typeof finalReview === 'string' ? finalReview : '';
146
+ if (!raw.trim())
147
+ return empty;
148
+ const json = extractReviewJson(raw);
149
+ if (json === undefined)
150
+ return { ...empty, reason: 'no_json_found' };
151
+ if (json === null || typeof json !== 'object' || Array.isArray(json)) {
152
+ return { ...empty, reason: 'not_a_json_object' };
153
+ }
154
+ const obj = json;
155
+ const reviewedBy = asText(obj.reviewedBy);
156
+ if (!reviewedBy)
157
+ return { ...empty, reason: 'missing_reviewed_by' };
158
+ const verdictRaw = asText(obj.verdict);
159
+ const verdict = verdictRaw === 'reusable' || verdictRaw === 'one_off' ? verdictRaw : 'unknown';
160
+ const facts = [];
161
+ if (Array.isArray(obj.facts)) {
162
+ for (const f of obj.facts) {
163
+ if (!f || typeof f !== 'object')
164
+ continue;
165
+ const fo = f;
166
+ const claim = asText(fo.claim);
167
+ if (!claim)
168
+ continue;
169
+ const refs = asTextArray(fo.refs);
170
+ // 声明 confirmed 但没有来源 → 降级为推测 (推测永不当证据)
171
+ const declared = asText(fo.assertion) === 'confirmed' ? 'confirmed' : 'inferred';
172
+ const assertion = declared === 'confirmed' && refs.length > 0 ? 'confirmed' : 'inferred';
173
+ facts.push({ claim, declared, assertion, refs });
174
+ }
175
+ }
176
+ const skills = [];
177
+ if (Array.isArray(obj.skills)) {
178
+ for (const s of obj.skills) {
179
+ if (!s || typeof s !== 'object')
180
+ continue;
181
+ const so = s;
182
+ const name = asText(so.name);
183
+ if (!name)
184
+ continue;
185
+ skills.push({
186
+ name,
187
+ purpose: asText(so.purpose),
188
+ occurrences: asFiniteNumber(so.occurrences, 0),
189
+ boundaryClear: asBool(so.boundaryClear, false),
190
+ inputSchema: asText(so.inputSchema),
191
+ outputSchema: asText(so.outputSchema),
192
+ guarantees: asTextArray(so.guarantees),
193
+ doesNotGuarantee: asTextArray(so.doesNotGuarantee),
194
+ failureCases: asTextArray(so.failureCases),
195
+ evidenceRefs: asTextArray(so.evidenceRefs),
196
+ });
197
+ }
198
+ }
199
+ return {
200
+ structured: true,
201
+ reason: 'ok',
202
+ reviewedBy,
203
+ verdict,
204
+ methodEffective: asText(obj.methodEffective),
205
+ methodFailed: asText(obj.methodFailed),
206
+ nextTimeChange: asText(obj.nextTimeChange),
207
+ facts,
208
+ skills,
209
+ };
210
+ }
211
+ // ============================================================================
212
+ // 候选门 (P1 版的"防 Skill 垃圾"检查, 只认冻结的 SkillJunkReason 取值)
213
+ // ============================================================================
214
+ const TEMP_PATH_HINT = /(^|[\s"'(=:])(\/tmp\/|tmp\/|\/var\/folders\/)/;
215
+ /**
216
+ * 用**冻结的** `SkillJunkReason` 取值给一条评审候选过门。
217
+ *
218
+ * 与 P1b 的 `assessCandidate()` 的关系: 那个还做「与现有 Skill 比版本/重复」, 需要已有 Skill 列表;
219
+ * 本文件在收尾时拿不到那份列表 (也不该 import 别的阶段), 因此**只**做自洽性门:
220
+ * 一次偶然成功 · 无 IO 契约 · 无失败边界 · 依赖临时路径 · 结果不可验证 · 只是一句经验。
221
+ * 命中任意一条 → `status='rejected'`, **绝不**送去 `writeCandidate`。
222
+ */
223
+ function gateClosureCandidate(raw) {
224
+ const junk = [];
225
+ if (raw.purpose.length < exports.CLOSURE_MIN_PURPOSE_LENGTH)
226
+ junk.push('one_line_experience');
227
+ if (raw.occurrences < 2)
228
+ junk.push('single_success');
229
+ if (!raw.boundaryClear)
230
+ junk.push('no_failure_boundary');
231
+ if (raw.failureCases.length === 0)
232
+ junk.push('no_failure_boundary');
233
+ if (!raw.inputSchema || !raw.outputSchema)
234
+ junk.push('no_io_schema');
235
+ if (raw.evidenceRefs.length === 0)
236
+ junk.push('unverifiable_result');
237
+ if (raw.guarantees.length === 0 || raw.doesNotGuarantee.length === 0)
238
+ junk.push('no_failure_boundary');
239
+ const touchesTemp = [raw.inputSchema, raw.outputSchema, ...raw.evidenceRefs, ...raw.failureCases].some((s) => TEMP_PATH_HINT.test(s));
240
+ if (touchesTemp)
241
+ junk.push('temp_path_dependency');
242
+ const unique = [...new Set(junk)];
243
+ const candidate = {
244
+ candidateId: `cand:${raw.name}`,
245
+ name: raw.name,
246
+ purpose: raw.purpose,
247
+ sourceRunIds: [],
248
+ evidenceRefs: [...raw.evidenceRefs],
249
+ failureCases: [...raw.failureCases],
250
+ inputSchema: raw.inputSchema,
251
+ outputSchema: raw.outputSchema,
252
+ guarantees: [...raw.guarantees],
253
+ doesNotGuarantee: [...raw.doesNotGuarantee],
254
+ contentHash: null,
255
+ // 候选永远不带批准 —— 正式变更必须由人批 (冻结语义)
256
+ approval: { state: 'not_requested', approvedBy: null, approvedAt: null, changeReason: null },
257
+ status: unique.length > 0 ? 'rejected' : 'draft',
258
+ occurrences: raw.occurrences,
259
+ boundaryClear: raw.boundaryClear,
260
+ junkReasons: unique,
261
+ proposedAt: '',
262
+ proposedByRunId: '',
263
+ };
264
+ return { candidate, junk: unique };
265
+ }
266
+ // ============================================================================
267
+ // 退化值 (调用方没注入时用; 全部写在注释里, 便于接线层核对)
268
+ // ============================================================================
269
+ /** Run 状态 → Goal 状态 (退化用; 与 goal-store 的语义同域) */
270
+ function goalStatusFromRun(status) {
271
+ switch (status) {
272
+ case 'done':
273
+ return 'active';
274
+ case 'failed':
275
+ case 'aborted':
276
+ return 'failed';
277
+ case 'interrupted':
278
+ return 'recovering';
279
+ case 'stalled':
280
+ return 'stalled';
281
+ case 'needs_human':
282
+ return 'needs_human';
283
+ case 'paused':
284
+ return 'paused';
285
+ case 'awaiting_external':
286
+ return 'awaiting_external';
287
+ default:
288
+ return 'active';
289
+ }
290
+ }
291
+ /**
292
+ * 没注入 `goal` 时的退化 Goal: 只装 run 事实。
293
+ * 关键: `successCriteria` 为空 → 按冻结语义「空判据一律不许自动判完成」,
294
+ * 因此退化形态**不可能**自己判 complete —— 这是安全的默认方向。
295
+ */
296
+ function deriveGoal(input) {
297
+ const run = input.run;
298
+ return {
299
+ goalId: input.goalId,
300
+ objective: run.goal || input.runId,
301
+ successCriteria: [],
302
+ constraints: [],
303
+ status: goalStatusFromRun(run.status),
304
+ createdAt: run.startedAt || input.now,
305
+ updatedAt: input.now,
306
+ currentRunId: input.runId,
307
+ runs: [input.runId],
308
+ completedCriteria: [],
309
+ unresolvedItems: [],
310
+ evidence: [...(run.evidence ?? [])],
311
+ };
312
+ }
313
+ /** 没注入 `hardLimits` 时的退化底线 (maxGoalBudget=null 意味着"未设上限, 必须由人显式确认") */
314
+ function deriveHardLimits(input, goal) {
315
+ const runDeadline = typeof input.run.budget?.deadlineMs === 'number' ? input.run.budget.deadlineMs : 0;
316
+ const goalBudget = typeof goal.budget?.maxRuns === 'number' ? goal.budget.maxRuns : null;
317
+ return {
318
+ maxRunDurationMs: runDeadline,
319
+ maxGoalBudget: goalBudget,
320
+ noProgressCircuitBreaker: exports.CLOSURE_DEFAULT_NO_PROGRESS_CIRCUIT_BREAKER,
321
+ };
322
+ }
323
+ // ============================================================================
324
+ // 事实 / 教训 / 候选 的提取 (纯函数)
325
+ // ============================================================================
326
+ function stepRef(runId, step) {
327
+ return `${runId}#step:${step.n}`;
328
+ }
329
+ function buildFacts(input, review) {
330
+ const now = input.now;
331
+ const runId = input.runId;
332
+ const run = input.run;
333
+ const all = [];
334
+ const push = (f) => {
335
+ all.push({ ...f, layer: 'run_fact', goalId: input.goalId, runId, createdAt: now });
336
+ };
337
+ // ① 逐步事实: 成功与失败都是**已确认**的事实 (失败也是事实, 不是"没发生")
338
+ for (const s of run.steps ?? []) {
339
+ const ref = stepRef(runId, s);
340
+ push({
341
+ memoryId: `mem:${runId}:step:${s.n}`,
342
+ content: s.ok
343
+ ? `步骤 ${s.n} (${s.tool}) 成功${s.summary ? `: ${s.summary}` : ''}`
344
+ : `步骤 ${s.n} (${s.tool}) 失败: ${s.error ?? '(未记录错误)'}`,
345
+ evidenceRefs: [ref],
346
+ assertion: 'confirmed',
347
+ confirmedRefs: [ref],
348
+ });
349
+ }
350
+ // ② Run 级证据 (每条本身就是可核验引用)
351
+ for (const [i, e] of (run.evidence ?? []).entries()) {
352
+ if (!e || !e.trim())
353
+ continue;
354
+ push({
355
+ memoryId: `mem:${runId}:evidence:${i}`,
356
+ content: `Run 级证据: ${e}`,
357
+ evidenceRefs: [e],
358
+ assertion: 'confirmed',
359
+ confirmedRefs: [e],
360
+ });
361
+ }
362
+ // ③ 结束状态事实
363
+ if (run.error) {
364
+ const ref = `${runId}#error`;
365
+ push({
366
+ memoryId: `mem:${runId}:error`,
367
+ content: `Run 以 ${run.status} 结束: ${run.error}`,
368
+ evidenceRefs: [ref],
369
+ assertion: 'confirmed',
370
+ confirmedRefs: [ref],
371
+ });
372
+ }
373
+ if ((run.recovery ?? []).length > 0) {
374
+ const last = run.recovery[run.recovery.length - 1];
375
+ const ref = `${runId}#recovery:${run.recovery.length}`;
376
+ push({
377
+ memoryId: `mem:${runId}:recovery`,
378
+ content: `本 Run 经历 ${run.recovery.length} 次恢复尝试 (最后一次: ${last.errorClass} → ${last.action}${last.recovered ? ', 已恢复' : ''})`,
379
+ evidenceRefs: [ref],
380
+ assertion: 'confirmed',
381
+ confirmedRefs: [ref],
382
+ });
383
+ }
384
+ // ④ 评审声明的事实: 推测**不带**任何证据引用 (推测永不当证据)
385
+ for (const [i, f] of review.facts.entries()) {
386
+ const inferred = f.assertion === 'inferred';
387
+ const downgraded = inferred && f.declared === 'confirmed' && f.refs.length === 0;
388
+ push({
389
+ memoryId: `mem:${runId}:review-fact:${i}`,
390
+ content: downgraded ? `${f.claim} (原报 confirmed 但无来源 → 降级为推测)` : f.claim,
391
+ evidenceRefs: inferred ? [] : [...f.refs],
392
+ assertion: f.assertion,
393
+ confirmedRefs: inferred ? [] : [...f.refs],
394
+ });
395
+ }
396
+ const capped = all.slice(0, exports.CLOSURE_MAX_FACTS);
397
+ const seen = new Set();
398
+ const facts = capped.filter((f) => (seen.has(f.memoryId) ? false : (seen.add(f.memoryId), true)));
399
+ return { facts, overflow: all.length - capped.length };
400
+ }
401
+ function buildLesson(input, review, confirmedRefs) {
402
+ if (!review.structured)
403
+ return { lesson: null, reason: `review_not_structured: ${review.reason}` };
404
+ if (review.verdict === 'one_off')
405
+ return { lesson: null, reason: 'review_verdict_one_off' };
406
+ if (review.verdict !== 'reusable')
407
+ return { lesson: null, reason: 'review_verdict_unknown' };
408
+ const missing = [];
409
+ if (!review.reviewedBy)
410
+ missing.push('reviewedBy');
411
+ if (!review.methodEffective)
412
+ missing.push('methodEffective');
413
+ if (!review.methodFailed)
414
+ missing.push('methodFailed');
415
+ if (!review.nextTimeChange)
416
+ missing.push('nextTimeChange');
417
+ if (missing.length > 0)
418
+ return { lesson: null, reason: `lesson_fields_missing: ${missing.join(',')}` };
419
+ const lesson = {
420
+ memoryId: `mem:${input.runId}:lesson`,
421
+ layer: 'lesson',
422
+ goalId: input.goalId,
423
+ runId: input.runId,
424
+ content: `教训: 有效=${review.methodEffective} / 失败=${review.methodFailed} / 下次=${review.nextTimeChange}`,
425
+ evidenceRefs: confirmedRefs.slice(0, exports.CLOSURE_MAX_EVIDENCE),
426
+ createdAt: input.now,
427
+ reviewedBy: review.reviewedBy ?? '',
428
+ reviewVerdict: 'reusable',
429
+ methodEffective: review.methodEffective,
430
+ methodFailed: review.methodFailed,
431
+ nextTimeChange: review.nextTimeChange,
432
+ };
433
+ return { lesson, reason: null };
434
+ }
435
+ function buildCandidates(input, review, contentHashOf) {
436
+ const reasons = [];
437
+ const candidates = [];
438
+ for (const [i, raw] of review.skills.entries()) {
439
+ const { candidate } = gateClosureCandidate(raw);
440
+ const base = {
441
+ ...candidate,
442
+ candidateId: `cand:${input.runId}:${i}:${raw.name}`,
443
+ sourceRunIds: [input.runId],
444
+ proposedAt: input.now,
445
+ proposedByRunId: input.runId,
446
+ };
447
+ // ★ 2026-09-25 (P5 验收修复): 冻结字段 contentHash 由注入的哈希器算 (缺注入 → 保持 null)。
448
+ // 哈希只覆盖**草案内容** (名字/用途/IO 契约/边界/证据面), 不含 candidateId/时间/Run 来源 ——
449
+ // 同一份内容来自不同 Run 必须得到同一个哈希 (否则"与已有 Skill 重复"永远判不出来)。
450
+ const hash = contentHashOf ? contentHashOf({ ...base, contentHash: null }) : null;
451
+ const withRun = { ...base, contentHash: hash ?? base.contentHash };
452
+ candidates.push(withRun);
453
+ if (withRun.junkReasons.length > 0) {
454
+ reasons.push(`${withRun.candidateId}: ${withRun.junkReasons.join(',')}`);
455
+ }
456
+ }
457
+ return { candidates, reasons };
458
+ }
459
+ // ============================================================================
460
+ // 进展与决策
461
+ // ============================================================================
462
+ function computeProgress(run) {
463
+ const newEvidence = [...new Set((run.evidence ?? []).filter((e) => typeof e === 'string' && e.trim().length > 0))];
464
+ return {
465
+ newEvidence,
466
+ // 判据面的增量要跟 Goal 的上一份 completedCriteria 比, 收尾层拿不到历史 → 交给接线层在 goal 里体现
467
+ newlyCompletedCriteria: [],
468
+ stepsAdvanced: (run.steps ?? []).filter((s) => s.ok).length,
469
+ // 未解决项的变化同理需要上一轮快照; 收尾层不猜
470
+ unresolvedDelta: 0,
471
+ };
472
+ }
473
+ /** 决策与状态必须自洽 (冻结注释原话); 不自洽只记原因, 不替 P0 改判 */
474
+ const STATE_OK_FOR_DECISION = {
475
+ continue: ['progressing'],
476
+ wait: ['waiting_external', 'waiting_agent'],
477
+ delegate: ['waiting_agent', 'progressing'],
478
+ ask_human: ['needs_decision', 'blocked', 'no_progress'],
479
+ complete: ['completed'],
480
+ fail: ['failed'],
481
+ pause: ['needs_decision', 'blocked'],
482
+ };
483
+ /** decision → Goal 生命周期状态 (P1 自己拥有 GoalContinuationRecord)
484
+ *
485
+ * ★ 2026-09-26 (M2): 导出为**单一事实来源** —— 接线接缝 `wiring/closure.ts` 的收尾产物审计
486
+ * 要用它在"决策 ↔ continuation.state"之间对齐, 不在这里重抄一份映射 (两套映射 = 两套事实)。
487
+ */
488
+ exports.GOAL_STATE_FOR_DECISION = {
489
+ continue: 'active',
490
+ wait: 'awaiting_external',
491
+ delegate: 'active',
492
+ ask_human: 'needs_human',
493
+ complete: 'completed',
494
+ fail: 'failed',
495
+ pause: 'paused',
496
+ };
497
+ /**
498
+ * 用户可见态 (2026-09-25 起是**六类**)。终态不再借用 `executing`:
499
+ * 第 6 类 `ended` 已补进冻结面, 收尾汇报按它表达"已结束"。
500
+ */
501
+ function visibleStateFor(decision) {
502
+ if (decision.decision === 'ask_human')
503
+ return 'needs_your_decision';
504
+ if (decision.state === 'completed' || decision.state === 'failed')
505
+ return 'ended';
506
+ if (decision.state === 'waiting_external')
507
+ return 'waiting_external_reply';
508
+ if (decision.state === 'blocked')
509
+ return 'child_blocked';
510
+ if (decision.state === 'no_progress')
511
+ return 'no_progress';
512
+ // progressing / waiting_agent → 'executing' (系统在收尾, 不向用户索要决定)
513
+ return 'executing';
514
+ }
515
+ /** 收尾收紧: 只能让"完成"变得更难, 不会把别的决策变成 complete */
516
+ function tightenDecision(decision, confirmedRefs) {
517
+ const notes = [];
518
+ if (decision.decision !== 'complete')
519
+ return { decision, notes, tightened: false };
520
+ const missing = [];
521
+ if (confirmedRefs.length === 0)
522
+ missing.push('complete_without_confirmed_evidence');
523
+ if (decision.unresolvedItems.length > 0)
524
+ missing.push(`complete_with_unresolved_items: ${decision.unresolvedItems.length}`);
525
+ if (missing.length === 0)
526
+ return { decision, notes, tightened: false };
527
+ const next = {
528
+ ...decision,
529
+ decision: 'ask_human',
530
+ state: 'needs_decision',
531
+ reason: `[收尾收紧] 原决策 complete 被拒 (${missing.join('; ')}); 原理由: ${decision.reason}`,
532
+ nextAction: '补齐可核验证据或由人确认是否接受该结果',
533
+ expectedOutcome: '有一条人能核对的决定: 接受该结果 / 补充证据后重跑',
534
+ confidence: Math.min(decision.confidence, 0.5),
535
+ wakeAt: null,
536
+ stopReason: null,
537
+ };
538
+ notes.push(...missing);
539
+ return { decision: next, notes, tightened: true };
540
+ }
541
+ function nextStepOf(decision) {
542
+ const action = decision.nextAction.trim();
543
+ if (action)
544
+ return { nextStep: action, note: null };
545
+ if (decision.decision === 'complete' || decision.decision === 'fail') {
546
+ return { nextStep: `无 (终态: ${decision.decision})`, note: null };
547
+ }
548
+ const fallback = decision.unresolvedItems.length > 0
549
+ ? `先处理未解决项: ${decision.unresolvedItems[0]}`
550
+ : `按 ${decision.decision} 继续 (决策未给 nextAction, 由收尾兜底)`;
551
+ return { nextStep: fallback, note: 'next_action_empty: 决策未给下一步动作, 收尾已兜底' };
552
+ }
553
+ // ============================================================================
554
+ // 用户汇报 (P4b 第一份输出)
555
+ // ============================================================================
556
+ const REPORT_EXPOSED_FIELDS = [
557
+ 'conclusion',
558
+ 'completed',
559
+ 'evidence',
560
+ 'remaining',
561
+ 'blockReasons',
562
+ 'nextStep',
563
+ 'willContinue',
564
+ 'expectedResumeAt',
565
+ 'visibleState',
566
+ ];
567
+ const CONCLUSION_BY_KIND = {
568
+ continue: '本轮有进展, 继续推进',
569
+ wait: '本轮结束, 等待外部条件',
570
+ delegate: '本轮结束, 需要其他执行者接手',
571
+ ask_human: '本轮结束, 需要你做一个决定',
572
+ complete: '目标判据已满足, 本次执行结束',
573
+ fail: '目标无法再满足, 本次执行结束',
574
+ pause: '本次执行已暂停',
575
+ };
576
+ function buildUserReport(input, decision, nextStep, facts, confirmedRefs, tightened) {
577
+ const okFacts = facts.filter((f) => f.assertion === 'confirmed' && /成功/.test(f.content));
578
+ const failedFacts = facts.filter((f) => f.assertion === 'confirmed' && /失败/.test(f.content));
579
+ const evidenceFactClaims = facts.filter((f) => f.assertion === 'confirmed' && /^Run 级证据: /.test(f.content));
580
+ const completed = [...okFacts.map((f) => f.content), ...evidenceFactClaims.map((f) => f.content)];
581
+ const blockReasons = failedFacts.map((f) => f.content);
582
+ if (decision.state === 'blocked')
583
+ blockReasons.push(decision.reason);
584
+ const tail = tightened.length > 0 ? ` [收尾收紧: ${tightened.join('; ')}]` : '';
585
+ const reason = decision.reason.trim() ? `: ${decision.reason.trim()}` : '';
586
+ return {
587
+ conclusion: `${CONCLUSION_BY_KIND[decision.decision]}${reason}${tail}`,
588
+ completed,
589
+ evidence: confirmedRefs.slice(0, exports.CLOSURE_MAX_EVIDENCE),
590
+ remaining: [...decision.unresolvedItems],
591
+ blockReasons,
592
+ nextStep,
593
+ willContinue: decision.decision !== 'complete' && decision.decision !== 'fail' && decision.decision !== 'pause',
594
+ expectedResumeAt: decision.wakeAt,
595
+ visibleState: visibleStateFor(decision),
596
+ exposedFields: [...REPORT_EXPOSED_FIELDS],
597
+ generatedAt: input.now,
598
+ };
599
+ }
600
+ function buildContinuation(input, decision, nextStep) {
601
+ const goalState = exports.GOAL_STATE_FOR_DECISION[decision.decision];
602
+ return {
603
+ nextAction: nextStep,
604
+ wakeAt: decision.wakeAt,
605
+ wakeReason: decision.reason.trim() || `state=${goalState}`,
606
+ autoContinue: decision.decision === 'continue' || decision.decision === 'wait' || decision.decision === 'delegate',
607
+ requiredAgent: decision.decision === 'delegate' ? decision.requiredCapability : null,
608
+ // 未回报的子任务只有拿到 AgentWorkContract 才知道 (P2); 收尾层不编
609
+ pendingReports: [],
610
+ unresolvedItems: [...decision.unresolvedItems],
611
+ lastDecisionId: decision.decisionId,
612
+ state: goalState,
613
+ updatedAt: input.now,
614
+ };
615
+ }
616
+ // ============================================================================
617
+ // 主流程
618
+ // ============================================================================
619
+ function errText(e) {
620
+ const m = e instanceof Error ? e.message : String(e);
621
+ return m.length > 200 ? `${m.slice(0, 200)}…` : m;
622
+ }
623
+ /**
624
+ * Run 收尾 (§14 冻结签名)。**成功 / 失败 / 中断恢复后的 Run 都必须走这一条**。
625
+ * 依赖失效时:
626
+ * - `writeMemory` / `writeCandidate` 抛错 → 记进 `skipped`, **继续**走完后面几步 (收尾不能中途断掉);
627
+ * - `decide` 抛错 → 抛 `RunClosureError` (结构化的"我回答不了下一步"), 不编一个决策出来。
628
+ */
629
+ async function closeRun(input, deps) {
630
+ const now = input.now;
631
+ const runId = input.runId;
632
+ const steps = [];
633
+ const skipped = [];
634
+ const mark = (s) => {
635
+ steps.push(s);
636
+ };
637
+ // ── 第 1 步: 主执行结束 ────────────────────────────────────────────────
638
+ mark('run_ended');
639
+ if (input.run.status === 'running') {
640
+ skipped.push({ step: 'run_ended', reason: 'run_still_running: 还在跑的 Run 没有"结束"可收尾' });
641
+ }
642
+ // ── 第 2 步: 读完整步骤与证据 ──────────────────────────────────────────
643
+ mark('read_full_steps_and_evidence');
644
+ const progress = computeProgress(input.run);
645
+ const streak = typeof input.noProgressStreak === 'number'
646
+ ? input.noProgressStreak
647
+ : progress.newEvidence.length === 0 && progress.stepsAdvanced === 0
648
+ ? 1
649
+ : 0;
650
+ const goal = input.goal ?? deriveGoal(input);
651
+ const hardLimits = input.hardLimits ?? deriveHardLimits(input, goal);
652
+ let decision;
653
+ try {
654
+ decision = deps.decide({ goal, run: input.run, now, progress, noProgressStreak: streak, hardLimits });
655
+ }
656
+ catch (e) {
657
+ throw new RunClosureError('continuation_decision', `继续决策失效, 收尾无法回答"下一步": ${errText(e)}`);
658
+ }
659
+ // ── 第 3 步: Final Review ────────────────────────────────────────────
660
+ mark('final_review');
661
+ const review = parseFinalReview(input.finalReview);
662
+ if (!review.structured) {
663
+ skipped.push({ step: 'final_review', reason: `final_review_unstructured: ${review.reason}` });
664
+ }
665
+ // ── 第 4 步: 提取事实 / 教训 / Skill 候选 ─────────────────────────────
666
+ mark('extract_facts_lessons_and_candidates');
667
+ const { facts, overflow } = buildFacts(input, review);
668
+ if (overflow > 0) {
669
+ skipped.push({
670
+ step: 'extract_facts_lessons_and_candidates',
671
+ reason: `facts_capped: 保留前 ${exports.CLOSURE_MAX_FACTS} 条, 丢弃 ${overflow} 条`,
672
+ });
673
+ }
674
+ const confirmedRefs = [
675
+ ...new Set(facts.filter((f) => f.assertion === 'confirmed').flatMap((f) => f.confirmedRefs)),
676
+ ];
677
+ const { lesson, reason: lessonReason } = buildLesson(input, review, confirmedRefs);
678
+ if (lessonReason) {
679
+ skipped.push({ step: 'extract_facts_lessons_and_candidates', reason: `lesson_skipped: ${lessonReason}` });
680
+ }
681
+ const { candidates, reasons: candidateReasons } = buildCandidates(input, review, deps.contentHashOf);
682
+ if (facts.length === 0 && !lesson && candidates.length === 0) {
683
+ skipped.push({
684
+ step: 'extract_facts_lessons_and_candidates',
685
+ reason: 'nothing_to_extract: 本轮既无步骤/证据, 也没有被采信的结构化评审',
686
+ });
687
+ }
688
+ // ── 第 5 步: 写 Memory ───────────────────────────────────────────────
689
+ mark('write_memory');
690
+ const signals = candidates.map((c) => ({
691
+ memoryId: `mem:${runId}:signal:${c.candidateId}`,
692
+ layer: 'skill_signal',
693
+ goalId: input.goalId,
694
+ runId,
695
+ content: `Skill 改进候选信号: ${c.name} (${c.status})`,
696
+ evidenceRefs: [...c.evidenceRefs],
697
+ createdAt: now,
698
+ candidateId: c.candidateId,
699
+ promotesDirectly: false,
700
+ }));
701
+ const records = [...facts, ...(lesson ? [lesson] : []), ...signals];
702
+ if (records.length === 0) {
703
+ skipped.push({ step: 'write_memory', reason: 'no_memory_records: 本轮没有可写的事实/教训/信号' });
704
+ }
705
+ else {
706
+ try {
707
+ const res = await deps.writeMemory(records, now);
708
+ const written = Array.isArray(res?.written) ? res.written : [];
709
+ const rejected = Array.isArray(res?.rejected) ? res.rejected : [];
710
+ for (const r of rejected) {
711
+ skipped.push({ step: 'write_memory', reason: `memory_rejected: ${r.memoryId}: ${r.reason}` });
712
+ }
713
+ if (written.length + rejected.length !== records.length) {
714
+ skipped.push({
715
+ step: 'write_memory',
716
+ reason: `memory_written_count_mismatch: 交 ${records.length} 条, 报告 written=${written.length} + rejected=${rejected.length}`,
717
+ });
718
+ }
719
+ }
720
+ catch (e) {
721
+ skipped.push({ step: 'write_memory', reason: `write_memory_failed: ${errText(e)}` });
722
+ }
723
+ }
724
+ // ── 第 6 步: 写 Skill 候选 ───────────────────────────────────────────
725
+ mark('write_skill_candidate');
726
+ const promotable = candidates.filter((c) => c.status !== 'rejected');
727
+ if (candidates.length === 0) {
728
+ skipped.push({ step: 'write_skill_candidate', reason: 'no_candidate_extracted' });
729
+ }
730
+ else if (promotable.length === 0) {
731
+ skipped.push({
732
+ step: 'write_skill_candidate',
733
+ reason: `no_promotable_candidate: ${candidateReasons.join('; ')}`,
734
+ });
735
+ }
736
+ else {
737
+ for (const c of promotable) {
738
+ try {
739
+ const id = await deps.writeCandidate(c);
740
+ if (!id) {
741
+ skipped.push({ step: 'write_skill_candidate', reason: `candidate_not_written: ${c.candidateId}` });
742
+ }
743
+ }
744
+ catch (e) {
745
+ skipped.push({ step: 'write_skill_candidate', reason: `write_candidate_failed: ${c.candidateId}: ${errText(e)}` });
746
+ }
747
+ }
748
+ }
749
+ // ── 第 7 步: 更新 Goal continuation ──────────────────────────────────
750
+ mark('update_goal_continuation');
751
+ if (decision.decision === 'delegate' && !decision.requiredCapability) {
752
+ skipped.push({
753
+ step: 'update_goal_continuation',
754
+ reason: 'delegate_without_capability: requiredAgent 将为空, 接线层无法派遣',
755
+ });
756
+ }
757
+ // ── 第 8 步: 用户汇报 ────────────────────────────────────────────────
758
+ mark('user_report');
759
+ // ── 第 9 步: 决定是否继续 (采用 / 收紧) ───────────────────────────────
760
+ mark('continuation_decision');
761
+ if (!STATE_OK_FOR_DECISION[decision.decision].includes(decision.state)) {
762
+ skipped.push({
763
+ step: 'continuation_decision',
764
+ reason: `decision_state_mismatch: decision=${decision.decision} state=${decision.state}`,
765
+ });
766
+ }
767
+ if (decision.decision === 'fail' && !decision.stopReason) {
768
+ skipped.push({ step: 'continuation_decision', reason: 'fail_without_stop_reason: 判 fail 必须给出停止理由' });
769
+ }
770
+ if (decision.decision !== 'fail' && decision.decision !== 'complete' && decision.stopReason) {
771
+ skipped.push({
772
+ step: 'continuation_decision',
773
+ reason: `stop_reason_outside_terminal: decision=${decision.decision} 带了 stopReason`,
774
+ });
775
+ }
776
+ const guarded = tightenDecision(decision, confirmedRefs);
777
+ decision = guarded.decision;
778
+ for (const note of guarded.notes) {
779
+ skipped.push({ step: 'continuation_decision', reason: note });
780
+ }
781
+ const { nextStep, note: nextNote } = nextStepOf(decision);
782
+ if (nextNote)
783
+ skipped.push({ step: 'continuation_decision', reason: nextNote });
784
+ // ── 收尾流水线自检 (M2): 固定 9 步 / 顺序固定 / 不重不漏 ──────────────────
785
+ // `steps` 是"收尾走了哪几步"的唯一记录; 一旦它缺步或换序, 这条纪律就不再是一句没人验的话。
786
+ const stepAudit = auditClosureSteps(steps);
787
+ if (!stepAudit.ok) {
788
+ skipped.push({ step: 'run_ended', reason: `closure_step_order_violation: ${stepAudit.reason}` });
789
+ }
790
+ const userReport = buildUserReport(input, decision, nextStep, facts, confirmedRefs, guarded.notes);
791
+ const continuation = buildContinuation(input, decision, nextStep);
792
+ return { steps, facts, lessons: lesson ? [lesson] : [], candidates, nextStep, decision, userReport, continuation, skipped };
793
+ }
794
+ /** 收尾步骤的固定顺序 (§5) —— 导出一份, 便于接线层与门核对 */
795
+ exports.CLOSURE_STEP_ORDER = types_js_1.RUN_CLOSURE_STEPS;
796
+ /**
797
+ * 收尾流水线自检 (M2): **步数固定 · 顺序固定 · 不重不漏**。
798
+ *
799
+ * 为什么需要它: "成功 / 失败 / 中断恢复后的 Run 都必须走完同一条流水线" 这句话, 之前只由
800
+ * `steps` 数组的**长度**间接体现 —— 少一步/换序都不会有人发现。把这个判据做成纯函数后,
801
+ * 收尾自己每次都会过它一遍 (违反就进 `skipped`), 测试也能把**人为改坏的步骤序列**喂给它。
802
+ */
803
+ function auditClosureSteps(steps) {
804
+ const missing = types_js_1.RUN_CLOSURE_STEPS.filter((s) => !steps.includes(s));
805
+ if (steps.length !== types_js_1.RUN_CLOSURE_STEPS.length || missing.length > 0) {
806
+ return {
807
+ ok: false,
808
+ reason: `步数不对: 走了 ${steps.length} 步, 冻结面是 ${types_js_1.RUN_CLOSURE_STEPS.length} 步 (缺: ${missing.join(',') || '无'})`,
809
+ };
810
+ }
811
+ for (const [i, expected] of types_js_1.RUN_CLOSURE_STEPS.entries()) {
812
+ if (steps[i] !== expected) {
813
+ return { ok: false, reason: `第 ${i + 1} 步是 ${String(steps[i])}, 冻结顺序要求 ${expected}` };
814
+ }
815
+ }
816
+ return { ok: true, reason: `ok: ${types_js_1.RUN_CLOSURE_STEPS.length} 步按 §5 顺序走完` };
817
+ }
818
+ //# sourceMappingURL=run-closure.js.map