@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
package/dist/index.js CHANGED
@@ -16,7 +16,14 @@ import { getGlobalSharedContext } from './social/global-shared-context.js';
16
16
  import { createBollharnessIntegration } from './bollharness-integration/index.js';
17
17
  import * as readline from 'readline';
18
18
  import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderMessageBox, renderToolCallListItem, termWidth, ROBOT_HEAD, BOLLOON_BANNER, dispWidth } from './cli/loading-tui.js';
19
- import { startInk, stopInk, inkAppendLine as appendLine, inkReplaceMatchingLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
19
+ import { startInk, stopInk, suspendInk, resumeInk, inkAppendLine as appendLine, inkReplaceMatchingLine, inkSetStatus, inkSetThinking, inkSetTransient } from './cli/ink-app.js';
20
+ // 2026-09-26: 启动期日志闸门 (默认静默加载日志 + 写文件 + verbose 全量回流 + 信号行不吞)
21
+ import { installStartupLogGate, isStartupVerbose, startupLogPath, VERBOSE_ENV, carriesHumanSignal, logStartupLine } from './cli/log-gate.js';
22
+ // 2026-09-27: 回复流卫生 — 内部运行日志 (循环推进/运行登记/收尾计数…) 不进对话回复流, 改落日志文件
23
+ import { isInternalRunLog, appendInternalRunLog } from './cli/reply-hygiene.js';
24
+ // 2026-09-27: 启动**前言 / 就绪度报告**闸门 (默认不上屏 → 面板一行 + 显式查询命令; 失败折成面板提示)
25
+ import { noticeLine as noticePreamble, flushStartupNotices, setStartupReadiness, alertsFromSetup, readinessLine, preambleHintLine, startupPreambleVisible, pushStartupAlert, takeStartupPanelNotes, clearBufferedNotices, } from './cli/startup-notice.js';
26
+ import { clipboardWrite } from './cli/clipboard.js';
20
27
  // 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
21
28
  import { createRequire } from 'module';
22
29
  const _require = createRequire(import.meta.url);
@@ -124,6 +131,53 @@ const s = {
124
131
  // @ts-ignore - noble/ed25519 v3 requires sha512 to be set
125
132
  ed25519.hashes.sha512 = sha512;
126
133
  // ---------------------------------------------------------------------------
134
+ // 启动期(面板之前)输出 —— 「面板优先」与「失败不许吞」两条口径的收口 (2026-09-27)
135
+ // ---------------------------------------------------------------------------
136
+ /**
137
+ * true = 这一程走「面板优先」口径 (交互 CLI 且没开 verbose/前言), 由 `main()` 的交互分支置位。
138
+ * 为什么需要它: 下面那些 `s.info` / `s.warn` (**面板之前**打的那几条进度/降级) 不能只看
139
+ * "有没有 ⚠" 决定上不上屏 —— 默认口径要求**面板第一帧之前一行都没有**, 而 leo 的铁律
140
+ * 又要求错误/降级**必须看得见**。两条同时成立 => 不放 stderr, 折进面板。
141
+ */
142
+ let startupPanelFirst = false;
143
+ /** 面板第一帧已经画出来了 (startCLI 取走面板提示之后): 晚到的降级提示直接追加进面板 */
144
+ let startupPanelReady = false;
145
+ /**
146
+ * 启动期(面板之前)一行进度 / 降级输出 —— 交互默认口径下的**唯一出路**。
147
+ *
148
+ * leo 口径 (2026-09-27, 两条必须同时成立):
149
+ * ① 「打完 `bolloon --cli` 直接就是面板」⇒ 面板第一帧之前**一行都不许上屏** (进度/就绪度/初始化日志都算);
150
+ * ② 「错误 / 降级 / 需人介入不许吞」⇒ 它必须**在屏上真能看到**。
151
+ * 于是这一层把面板之前那些 `s.info` / `s.warn` 分成两种去处:
152
+ * · 带人信号的 (失败/降级/超时/不可用/⚠ …, 判据复用 `log-gate.carriesHumanSignal` —— 只有一处) →
153
+ * **折进面板告警通道**: 面板还没画就登记 (`pushStartupAlert`, 与 `/!\ ⚠ 未就绪 …` 同一出路),
154
+ * 面板已经画了就地 `appendLine` 追加一行 (晚到的失败也不许消失在内存里);
155
+ * · 不带信号的纯进度 → 不上屏, 只落 `~/.bolloon/logs/startup.log` (诊断不丢)。
156
+ *
157
+ * `--verbose` / `BOLLOON_STARTUP_PREAMBLE=1` / 非交互 / web: **逐字照旧** (`s.warn` / `s.info`
158
+ * 原样打屏, 非交互那几条就是正常输出 —— 那里没有面板接着显示, 折进面板等于吞)。
159
+ *
160
+ * @param fallback 老路径的渲染方式 (只有 publishDID 那条 `s.step(…, 'warn')` 与 `s.warn` 不同)
161
+ */
162
+ function bootNotice(kind, line, fallback) {
163
+ if (!startupPanelFirst || startupPreambleVisible()) {
164
+ if (fallback)
165
+ fallback();
166
+ else if (kind === 'warn')
167
+ s.warn(line);
168
+ else
169
+ s.info(line);
170
+ return;
171
+ }
172
+ logStartupLine(line); // 诊断不丢: 无论上不上屏
173
+ if (!carriesHumanSignal(line))
174
+ return; // 纯进度: 默认口径下不上屏
175
+ if (startupPanelReady)
176
+ appendLine(`${C_WARN}/!\\ ${line}${RESET}`);
177
+ else
178
+ pushStartupAlert(line);
179
+ }
180
+ // ---------------------------------------------------------------------------
127
181
  // Harness loop ─ poll-free event-driven (Hyperswarm DHT 自动推送)
128
182
  // ---------------------------------------------------------------------------
129
183
  /** 原始 Hyperswarm stream 缓存; HyperswarmCommunicator.sendToConnection 只打日志不写流 */
@@ -186,8 +240,14 @@ function publishDID(name, kp) {
186
240
  }
187
241
  catch (e) {
188
242
  // 一次失败直接放弃 — 本地模式运行就够了, 不重试
189
- appendLine(` ${YELLOW}⚠ IPFS 发布失败 (${e?.message?.slice(0, 80) || 'unknown'}), 本地模式运行${RESET}`);
190
- s.step(2, 5, '发布 DID → IPFS', 'warn');
243
+ const reason = e?.message?.slice(0, 80) || 'unknown';
244
+ // 失败详情照旧进面板 (appendLine → Ink): "为什么降级"这一条本来就是给面板看的
245
+ appendLine(` ${YELLOW}⚠ IPFS 发布失败 (${reason}), 本地模式运行${RESET}`);
246
+ // 2026-09-27 口径收口: 这一行原来是打在**面板之前**的进度行 (默认口径下不许上屏),
247
+ // 可它是"降级" ⇒ 按铁律不许吞。两条同时成立的做法 = 折进面板告警通道
248
+ // (`verify-cli-panel` 的 A1「面板前 0 行」+ A7「面板里能定位到这条降级原文」成对钉住)。
249
+ // verbose / 非交互: 逐字回到老样子 (`s.step(…, 'warn')`)。
250
+ bootNotice('warn', `⚠ 降级: [2/5] 发布 DID → IPFS 失败 (${reason}) · 本地模式继续`, () => s.step(2, 5, '发布 DID → IPFS', 'warn'));
191
251
  resolve({});
192
252
  }
193
253
  };
@@ -418,6 +478,8 @@ function rpcErr(code, msg) {
418
478
  // 2026-07-28: 改用 readline.createInterface + replReadline 循环
419
479
  let isRunning = false;
420
480
  let queueMode = false;
481
+ // 2026-09-26: 启动期日志闸门句柄 (main() 里装; 诊断/验收用 `gate.stats` 读被静默的行数)
482
+ let _startupLogGate = null;
421
483
  const pendingQueue = [];
422
484
  let cliStartTime = 0;
423
485
  let cliModelName = '…';
@@ -633,33 +695,11 @@ async function startCLI(commReady) {
633
695
  // 内部用法全空安全 (P2P 功能自动降级, 就绪后立即可用)
634
696
  let comm = null;
635
697
  commReady.then((c) => { comm = c; }).catch(() => { });
636
- // CLI 模式下静音所有 console.log/warn/info/debug
637
- // (Ink 用自己的 render 引擎, console 输出会污染终端)
638
- console.log = () => { };
639
- console.warn = () => { };
640
- console.info = () => { };
641
- console.debug = () => { };
642
- // 过滤 process.stdout/stderr.write — 丢弃启动期 SDK/后台日志
643
- // (ISO 时间戳前缀如 `2026-09-08T...Z [info]:` 或被 [info]/[warn]/[error] 标记的行)
644
- const _origStdout = process.stdout.write.bind(process.stdout);
645
- const _origStderr = process.stderr.write.bind(process.stderr);
646
- const isLogLine = (line) => {
647
- const t = line.trimStart();
648
- return t.startsWith('[') || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(t) || /\[\s*(info|warn|error|debug|log)\s*\]/.test(t)
649
- // Kubo/ipfs 启动噪声: "Use 'ipfs init --help'..." / "ipfs daemon is running..." 提示行无时间戳, 一并丢弃
650
- || /ipfs init --help|ipfs daemon is running|please stop it to run this command/i.test(t);
651
- };
652
- const wrap = (orig) => (chunk, ...rest) => {
653
- const s = typeof chunk === 'string' ? chunk : String(chunk);
654
- if (!isLogLine(s))
655
- return orig(chunk, ...rest);
656
- const keep = s.split('\n').filter((l) => !isLogLine(l)).join('\n');
657
- if (keep)
658
- orig(keep, ...rest);
659
- return true;
660
- };
661
- process.stdout.write = wrap(_origStdout);
662
- process.stderr.write = wrap(_origStderr);
698
+ // 2026-09-26: 启动期日志静默**上移到 `src/cli/log-gate.ts`** (闸门在 main() 里装, 覆盖 CLI 交互 +
699
+ // dashboard/web + plain 三个启动面)。这里不再自己 wrap stdout/stderr / 清空 console ——
700
+ // 旧写法只丢「行首 `[`」, 既漏掉依赖库的 ISO 时间戳日志, 又会把 `[supervisor] 初始化未就绪`
701
+ // 这种**需要人介入**的行一起吞掉 (与「错误不许被吞」冲突)。闸门按行分类, 信号行改道 stderr。
702
+ void _startupLogGate;
663
703
  let peerCount = 0;
664
704
  void commReady.then((c) => { try {
665
705
  if (c)
@@ -855,6 +895,24 @@ async function startCLI(commReady) {
855
895
  '⟳ 正在加载技能 / 工具...',
856
896
  ]);
857
897
  appendLine(bootBox);
898
+ // 2026-09-27 (leo 口径: `bolloon --cli` 起来直接就是面板, 启动前不刷前言):
899
+ // 就绪度报告 / 初始化续跑前言**默认不上屏** —— 但它们不是消失, 而是变成**面板里的行**:
900
+ // · 就绪度折成一行 `就绪: basic ✓ · agent ✓ · durable ✗ · network ✓ · 详情: …`
901
+ // · 失败 / 需人介入 (缺 key、连通性实测失败、初始化中断) 折成 `/!\` 开头的提示行 —— **不许吞**
902
+ // 注: 字面量里必须写 `\\ ` (反斜杠要转义), 否则 JS 会把 `\ ` 吃成空格 —— 屏上就变成 `/! ` 了
903
+ // (验收门 `verify-cli-panel` 的 A4 按 `/!\` 判, 真踩过一次)。
904
+ // 全量版仍在 `startup.log`; `--verbose` / BOLLOON_VERBOSE=1 下逐字回到启动前的位置 (走 stderr)。
905
+ const notes = takeStartupPanelNotes();
906
+ const verbosePreamble = startupPreambleVisible();
907
+ if (notes.readiness && !verbosePreamble)
908
+ appendLine(`${C_DIM}${notes.readiness}${RESET}`);
909
+ for (const a of notes.alerts)
910
+ appendLine(`${C_WARN}/!\\ ${a}${RESET}`);
911
+ // 已经到面板了: 缓冲里剩下的纯前言**不上屏**(已在 startup.log), 丢掉引用免得下次启动重放
912
+ clearBufferedNotices();
913
+ // 面板第一帧之后: 晚到的降级提示 (后台 iroh / bootstrap / DID 发布…) 直接追加进面板,
914
+ // 不再等下一次 `takeStartupPanelNotes()` —— 否则它们会静静地烂在告警数组里 (= 吞)
915
+ startupPanelReady = true;
858
916
  void bootPanel({ dir: bootDirShort, model: (cliModelName && cliModelName !== '…') ? cliModelName : undefined, session: bootSessionId })
859
917
  .then((box) => { if (box)
860
918
  inkReplaceMatchingLine(bootBox, box); }).catch(() => { });
@@ -1345,6 +1403,10 @@ async function processInputInner(input, comm) {
1345
1403
  }
1346
1404
  const r = await agent.resumeRun(runId);
1347
1405
  appendLine(r.ok ? `${C_ACCENT}✅ 恢复执行完成${RESET}` : `${C_ERROR}恢复失败: ${r.reason}${RESET}`);
1406
+ if (r.modelDrift?.drifted)
1407
+ appendLine(`${C_ERROR}⚠ 模型配置已漂离快照: ${r.modelDrift.message}${RESET}`);
1408
+ if (r.modelApplied)
1409
+ appendLine(`${C_DIM} 本次恢复用的模型 (按 Run 快照装配): ${r.modelApplied.provider}/${r.modelApplied.model}${RESET}`);
1348
1410
  }
1349
1411
  catch (e) {
1350
1412
  appendLine(`${C_ERROR}/resume 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
@@ -1408,6 +1470,8 @@ async function processInputInner(input, comm) {
1408
1470
  }
1409
1471
  const r = await agent.resumeRun(runId);
1410
1472
  appendLine(r.ok ? `${C_ACCENT}✅ 已批准并继续执行${RESET}` : `${C_ERROR}批准后恢复失败: ${r.reason}${RESET}`);
1473
+ if (r.modelApplied)
1474
+ appendLine(`${C_DIM} 本次恢复用的模型 (按 Run 快照装配): ${r.modelApplied.provider}/${r.modelApplied.model}${RESET}`);
1411
1475
  }
1412
1476
  catch (e) {
1413
1477
  appendLine(`${C_ERROR}/approve 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
@@ -1598,6 +1662,7 @@ async function processInputInner(input, comm) {
1598
1662
  try {
1599
1663
  const { getSupervisor } = await import('./agents/execution-supervisor.js');
1600
1664
  const { wakeReport, listRunnableGoals, formatGoalLine } = await import('./agents/goal-store.js');
1665
+ const { USER_VISIBLE_STATE_LABELS } = await import('./agents/goal-flywheel/types.js');
1601
1666
  const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
1602
1667
  const st = getSupervisor().status();
1603
1668
  appendLine(`${C_DIM}supervisor: owner=${st.owner} running=${st.running ? 'yes' : 'no'} tick=${st.tickIntervalMs}ms lease=${st.leaseTtlMs}ms dryRun=${st.dryRun ? 'yes' : 'no'} ticks=${st.ticks}${RESET}`);
@@ -1637,10 +1702,23 @@ async function processInputInner(input, comm) {
1637
1702
  const sup = new ExecutionSupervisor({ runner: runner, maxPerTick: 1, log: (m) => appendLine(`${C_DIM}${m}${RESET}`) });
1638
1703
  const rep = await sup.tickOnce();
1639
1704
  appendLine(`${C_ACCENT}调度周期 #${rep.tick}${RESET} 认领 ${rep.claimed.length} · 执行 ${rep.executed.length}${rep.skipped.length ? ` · 跳过 ${rep.skipped.length}` : ''}`);
1705
+ // 2026-09-25 (飞轮接线): 把飞轮的**判断**如实打出来 —— 为什么跑/为什么停, 用的是哪一类用户可见态
1640
1706
  for (const s of rep.skipped.slice(0, 6))
1641
1707
  appendLine(` ${C_DIM}跳过 ${s.goalId}: ${s.reason}${RESET}`);
1642
1708
  for (const e of rep.executed)
1643
1709
  appendLine(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
1710
+ for (const f of rep.flywheel) {
1711
+ const zh = USER_VISIBLE_STATE_LABELS[f.visibleState]?.zh || f.visibleState;
1712
+ appendLine(` ${f.runnable ? C_DIM : C_WARN}飞轮 ${f.goalId}: ${f.decision}/${f.state} 无进展连续 ${f.noProgressStreak} 轮 · 用户可见态=${zh} · ${f.reason}${RESET}`);
1713
+ }
1714
+ for (const c of rep.closures) {
1715
+ appendLine(` ${C_DIM}收尾 ${c.goalId}/${c.runId}: ${c.steps} 步 → ${c.decision} (memory ${c.memories} · skill 候选 ${c.candidates})${RESET}`);
1716
+ appendLine(` ${C_DIM}用户汇报: ${c.reportPath}${RESET}`);
1717
+ }
1718
+ for (const b of rep.blocks)
1719
+ appendLine(` ${C_WARN}阻塞 ${b.goalId}/${b.workId}: ${b.kind} → ${b.action} — ${b.note}${RESET}`);
1720
+ for (const w of rep.workContracts)
1721
+ appendLine(` ${C_DIM}工作合同已签发 ${w.goalId}: workId=${w.workId} 能力=${w.capability}${RESET}`);
1644
1722
  return;
1645
1723
  }
1646
1724
  const rows = await wakeReport();
@@ -1648,8 +1726,12 @@ async function processInputInner(input, comm) {
1648
1726
  appendLine(`${C_DIM}还没有目标。${RESET}`);
1649
1727
  return;
1650
1728
  }
1651
- for (const r of rows.slice(0, 12))
1652
- appendLine(` ${r.goalId} [${r.status}] 唤醒: ${r.wake}${r.lease ? ` (lease ${r.lease})` : ''}`);
1729
+ // 界面只上屏**用户可见态** (六类); 内部状态缩到 dim 尾巴里, 只作为排障线索
1730
+ for (const r of rows.slice(0, 12)) {
1731
+ const zh = USER_VISIBLE_STATE_LABELS[r.visible]?.zh || r.visible || r.status;
1732
+ appendLine(` ${r.goalId} [${zh}] 唤醒: ${r.wake}${r.nextAction ? ` 下一步: ${String(r.nextAction).slice(0, 60)}` : ''}${r.lease ? ` (lease ${r.lease})` : ''}`);
1733
+ appendLine(`${C_DIM} (内部: status=${r.status} visible=${r.visible})${RESET}`);
1734
+ }
1653
1735
  if (runnable.length) {
1654
1736
  appendLine(`${C_DIM}现在可推进:${RESET}`);
1655
1737
  for (const g of runnable.slice(0, 5))
@@ -1678,46 +1760,99 @@ async function processInputInner(input, comm) {
1678
1760
  }
1679
1761
  return;
1680
1762
  }
1681
- // /model — 无参: 交互选择器 (ink 渲染, 复用 MentionPopup); 有参: 直接切换/测连通/看状态
1763
+ // /model — 无参: 分步选择器 (供应商 → 凭证 → 模型 → 参数 → 作用域 → 测试 → 确认);
1764
+ // 有参: 直接切换/测连通/看状态 (与 bolloon model 同一套语义)
1682
1765
  if (cmd === '/model' || cmd.startsWith('/model ')) {
1683
1766
  const modelArg = trimmed.slice('/model'.length).trim();
1684
- if (modelArg) {
1685
- try {
1686
- const { runModelCommand } = await import('./cli/setup-wizard.js');
1687
- // 会话内不提供隐藏输入 (避免 API key 留在会话回显/记录里) → 需要 key 时给出系统终端指引
1688
- const out = await runModelCommand(modelArg);
1689
- for (const line of String(out).split('\n'))
1690
- appendLine(`${C_DIM}${line}${RESET}`);
1691
- }
1692
- catch (e) {
1693
- appendLine(`${C_ERROR}/model 失败: ${String(e?.message || e).slice(0, 150)}${RESET}`);
1694
- }
1695
- return;
1767
+ if (!modelArg) {
1768
+ appendLine(`${C_DIM}模型选择 (分步): 供应商 → 凭证 → 模型 → 生成参数 → 作用域 → 测试连接 → 确认切换${RESET}`);
1696
1769
  }
1697
1770
  try {
1698
- const { llmConfigStore, PROVIDER_INFO } = await import('./llm/config-store.js');
1699
- await llmConfigStore.initialize();
1700
- const config = await llmConfigStore.getConfig();
1701
- const items = Object.entries(config.providers).map(([name, p]) => ({
1702
- kind: 'command',
1703
- label: name,
1704
- hint: `${String(PROVIDER_INFO[name]?.name || '').padEnd(14)} ${p.apiKey ? '🔑' : p.requiresApiKey ? '⚠ 无key' : ''} ${p.model || ''}`,
1705
- insert: name,
1706
- }));
1707
- globalThis.__inkOpenPicker?.(items, '选择模型供应商 (↑↓ 选择 · Enter 确认 · Esc 取消)', async (it) => {
1771
+ const { runModelCommand, modelTtyIO } = await import('./cli/setup-wizard.js');
1772
+ /**
1773
+ * 把渲染层的选择器借给分步选择器用: 每次要一个选择时开窗, 拿到结果 (或用户 Esc) 就往下走。
1774
+ * Esc 通过取消回调报回 null —— 没有这条通路时"按 Esc"会让调用方一直等。
1775
+ *
1776
+ * ⚠️ 这条 (`inkChoose`, Ink 画的小弹窗) 现在只是**非终端**的兜底 (管道/脚本)。
1777
+ * 真终端走下面那条: 与 `bolloon model` **同一个** `tui-select` 全屏选择器
1778
+ * (固定高度视窗 + 跟随 + 折叠 + 搜索 + 光标高亮 + "还有 M 家")。
1779
+ */
1780
+ const inkChoose = (items, title) => new Promise((resolve) => {
1781
+ const g = globalThis;
1782
+ if (typeof g.__inkOpenPicker !== 'function') {
1783
+ resolve(null);
1784
+ return;
1785
+ }
1786
+ g.__inkOpenPicker(items.map((c) => ({ kind: 'command', label: c.label, hint: c.hint, insert: c.value })), title, (it) => resolve(it?.insert ?? it?.label ?? null), () => resolve(null));
1787
+ });
1788
+ // 真终端: 让出终端给**同一个**全屏选择器组件 (两个渲染器不能同时抢一个 tty), 用完挂回来。
1789
+ const sharedPicker = await import('./cli/tui-select.js').then((m) => m.tuiCapable()).catch(() => false);
1790
+ let out;
1791
+ if (sharedPicker) {
1792
+ suspendInk();
1708
1793
  try {
1709
- await llmConfigStore.setActiveProvider(it.label);
1710
- const active = await llmConfigStore.getActiveProvider();
1711
- appendLine(`${C_OK}✓ 已切换到 ${it.label} (${String(PROVIDER_INFO[it.label]?.name || '')})${RESET}`);
1712
- appendLine(`${C_DIM} 当前模型: ${config.providers[it.label]?.model || '默认'}${RESET}`);
1794
+ const tio = modelTtyIO();
1795
+ out = await runModelCommand(modelArg, {
1796
+ ...tio,
1797
+ // 会话内**不提供**隐藏输入: API key 不许进会话回显/记录 (需要 key 时选择器给系统终端指引)
1798
+ askHidden: undefined,
1799
+ // `tio.live = true`: 每一步当场打在终端上 (选择器自己就是整屏渲染, 不需要再回放整段)
1800
+ });
1713
1801
  }
1714
- catch (e) {
1715
- appendLine(`${C_ERROR}✗ 切换失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
1802
+ finally {
1803
+ resumeInk();
1716
1804
  }
1717
- });
1805
+ }
1806
+ else {
1807
+ out = await runModelCommand(modelArg, { choose: inkChoose });
1808
+ }
1809
+ for (const line of String(out).split('\n'))
1810
+ if (line.trim())
1811
+ appendLine(`${C_DIM}${line}${RESET}`);
1812
+ }
1813
+ catch (e) {
1814
+ appendLine(`${C_ERROR}/model 失败: ${String(e?.message || e).slice(0, 150)}${RESET}`);
1815
+ }
1816
+ return;
1817
+ }
1818
+ // /copy — 把**最近一条回复** (或整段会话) 真写进系统剪贴板 (2026-09-27)
1819
+ // 为什么要有: 面板是 Ink 画的整屏自绘帧, 终端里用鼠标框选会被"重画一帧"打断;
1820
+ // 与其让用户撞运气, 不如给一条**不靠鼠标**的显式通路 (`/copy` / `/copy all` / `/copy 3`)。
1821
+ if (cmd === '/copy' || cmd.startsWith('/copy ')) {
1822
+ const arg = trimmed.slice('/copy'.length).trim().toLowerCase();
1823
+ try {
1824
+ const store = await import('./cli/stores.js');
1825
+ const msgs = store.transcriptStore.get().filter((m) => String(m ?? '').trim());
1826
+ let text = '';
1827
+ let what = '';
1828
+ if (arg === 'all') {
1829
+ text = store.allMsgsText();
1830
+ what = `整段会话 (${msgs.length} 条)`;
1831
+ }
1832
+ else if (/^\d+$/.test(arg)) {
1833
+ const n = Math.max(1, Math.min(Number(arg), msgs.length));
1834
+ text = msgs.slice(msgs.length - n).join('\n');
1835
+ what = `最近 ${n} 条`;
1836
+ }
1837
+ else {
1838
+ text = store.lastMsg();
1839
+ what = '最近一条';
1840
+ }
1841
+ if (!String(text).trim()) {
1842
+ appendLine(`${C_DIM}面板里还没有可复制的内容${RESET}`);
1843
+ return;
1844
+ }
1845
+ const r = clipboardWrite(text);
1846
+ if (r.ok) {
1847
+ appendLine(`${C_ACCENT}✅ 已复制${what} → 系统剪贴板 (${r.tool} · ${r.chars} 字符)${RESET}`);
1848
+ }
1849
+ else {
1850
+ appendLine(`${C_ERROR}✗ 复制失败 (${what}): ${r.error}${RESET}`);
1851
+ appendLine(`${C_DIM} 兜底: 终端里用鼠标框选不会被自动重画打断 (上滚会自动暂停跟随)${RESET}`);
1852
+ }
1718
1853
  }
1719
1854
  catch (e) {
1720
- appendLine(`${C_ERROR}/model 失败: ${String(e.message || e).slice(0, 150)}${RESET}`);
1855
+ appendLine(`${C_ERROR}/copy 失败: ${String(e?.message || e).slice(0, 150)}${RESET}`);
1721
1856
  }
1722
1857
  return;
1723
1858
  }
@@ -2928,8 +3063,21 @@ async function processInputInner(input, comm) {
2928
3063
  }
2929
3064
  else if (e.type === 'status' && e.content) {
2930
3065
  const content = String(e.content);
3066
+ // 2026-09-27 (回复流卫生): **内部运行日志不进对话回复流** ——
3067
+ // CLI 交互面没有独立的状态区, 这条 onStream 就是"回复流", 于是
3068
+ // `🔄 开始 ReAct 循环...` / `🧷 运行已登记 (...)` / `✅ 处理完成,共 N 次循环` /
3069
+ // `🎯 目标仍在进行 (未判完成): …` 这类**给开发者看的运行过程**会和用户的结论
3070
+ // 混成一列消息 (leo 报的正是这个)。判据不按子串猜, 只认发送点的显式声明
3071
+ // (`StreamEvent.internal`, 见 pi-sdk-types) —— 旧的子串过滤继续保留在下面兜底。
3072
+ // 搬走不是删掉: 同一行原文照落 logs/startup.log (目的地走已有的 startupLogPath()),
3073
+ // `--verbose`/`BOLLOON_VERBOSE=1` 时原样上屏, 诊断能力一点不丢。
3074
+ if (isInternalRunLog(e)) {
3075
+ appendInternalRunLog(content, e.tool);
3076
+ if (isStartupVerbose())
3077
+ appendLine(`${C_DIM}${content}${RESET}`);
3078
+ }
2931
3079
  // Reflection / 反思 / 💡 → 圆角思考框 (和回复一样走 renderMessageBox, 白字+亮边框)
2932
- if (content.includes('Reflection') || content.includes('反思') || content.includes('💡')) {
3080
+ else if (content.includes('Reflection') || content.includes('反思') || content.includes('💡')) {
2933
3081
  const body = content.replace(/^💡\s*/, '').slice(0, 1500);
2934
3082
  if (body.trim())
2935
3083
  appendLine(renderMessageBox({ title: '💡 反思', body, color: C_WARN }));
@@ -3112,7 +3260,53 @@ const AVAILABLE_TOOLS = [
3112
3260
  { name: 'update_check', description: '检查 npm 包更新', example: '--update-check' },
3113
3261
  { name: 'update_now', description: '立即更新到最新版本', example: '--update-now [package]' },
3114
3262
  ];
3115
- async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
3263
+ /**
3264
+ * 给 `--delegate` 找长期目标上下文 (P5 验收修复)。
3265
+ *
3266
+ * `SubAgentManager.delegateTask` 只在拿到 `goalId` 时才签工作合同 (`issueWorkContractFor`:
3267
+ * "没有长期目标就没有合同上下文"), 而合同是"回报必须带逐条证据"这道门的唯一凭据。
3268
+ *
3269
+ * ★ 之前 CLI 是唯一的生产派遣调用方, **不传 goalId** ⇒ 真路径上一条合同都不签:
3270
+ * 运行时复现过"回一句『全部做完了』就把任务标成 completed, 父目标上什么都没留下"。
3271
+ *
3272
+ * 两种入口:
3273
+ * - `--goal <goalId>`: 用既有目标 (目标不存在 → 抛错, 让调用方如实报告"没签合同");
3274
+ * - 没给: 为这次委派**建一个目标** (`createdBy='cli:delegate'`) 并进"等外部回报"态 ——
3275
+ * 委派总是有目标上下文, 合同门不再空转。
3276
+ *
3277
+ * 为什么是 `awaiting_external` + `autoContinue=false`: 这次委派的执行者是**子 Agent**, 不是本节点
3278
+ * harness。`awaiting_external` 会被 Supervisor 明确跳过 (`listRunnableGoals`: "等外部事件, 不重复
3279
+ * 发送"), 因此不会出现"同一件事被 harness 和子 Agent 各跑一遍"; 同时"没拿到带证据的回报前不自动
3280
+ * 继续"。
3281
+ */
3282
+ async function ensureDelegateGoal(explicitGoalId, taskDesc, caps) {
3283
+ const { createGoal, readGoal, setContinuation, updateGoal } = await import('./agents/goal-store.js');
3284
+ if (explicitGoalId) {
3285
+ const g = await readGoal(explicitGoalId);
3286
+ if (!g)
3287
+ throw new Error(`--goal ${explicitGoalId} 不存在 (不签合同)`);
3288
+ return { goalId: g.goalId, note: `合同签在既有目标 ${g.goalId} (状态 ${g.status}) 上` };
3289
+ }
3290
+ const g = await createGoal({
3291
+ objective: `【委派】${taskDesc}`,
3292
+ successCriteria: [taskDesc],
3293
+ createdBy: 'cli:delegate',
3294
+ });
3295
+ await updateGoal(g.goalId, { status: 'awaiting_external' });
3296
+ await setContinuation(g.goalId, {
3297
+ state: 'awaiting_external',
3298
+ wakeReason: 'awaiting_external',
3299
+ autoContinue: false,
3300
+ needsExternal: `等子 Agent 「${caps.join('/')}」交回带逐条证据的回报 (回报核验不过不算完成)`,
3301
+ });
3302
+ return {
3303
+ goalId: g.goalId,
3304
+ note: `已为本次委派建目标 ${g.goalId} (状态=等外部回复: 等子 Agent 的带证据回报)`,
3305
+ };
3306
+ }
3307
+ async function runToolCommand(tool, args, outputJson, comm, model, prompt,
3308
+ /** `--goal <goalId>` (P5 验收修复: 给 `--delegate` 指目标上下文) */
3309
+ goalFlag) {
3116
3310
  const a = await getAgent();
3117
3311
  const startTime = Date.now();
3118
3312
  let response;
@@ -3572,18 +3766,39 @@ async function runToolCommand(tool, args, outputJson, comm, model, prompt) {
3572
3766
  case 'delegate': {
3573
3767
  const [taskDesc, ...requiredCaps] = args;
3574
3768
  if (!taskDesc) {
3575
- response = '用法: --delegate <任务描述> [能力要求1] [能力要求2] ...';
3769
+ response = '用法: --delegate <任务描述> [能力要求1] [能力要求2] ... [--goal <goalId>]';
3576
3770
  error = response;
3577
3771
  break;
3578
3772
  }
3773
+ const caps = requiredCaps.length > 0 ? requiredCaps : ['general'];
3579
3774
  const manager = await createSubAgentManager();
3580
- const a = await getAgent();
3581
- const { task, agent } = await manager.delegateTask('cli-user', taskDesc, requiredCaps.length > 0 ? requiredCaps : ['general']);
3582
- if (agent) {
3583
- response = `✅ 任务已委派:\n 任务ID: ${task.id}\n 执行Agent: ${agent.name} (${agent.id})\n 状态: ${task.status}`;
3775
+ // ★ 2026-09-25 (P5 验收修复): 派遣**必须**带 goalId。
3776
+ // `SubAgentManager` 只在拿到 goalId 时才签工作合同 (没有长期目标就没有合同上下文);
3777
+ // 而在真实运行里复现过: 不签合同 ⇒ 回报核验这道门完全不生效 ——
3778
+ // 一句"全部做完了"就能把任务标成 completed, 父 Goal 上也留不下"等回报"的痕迹。
3779
+ // 显式 `--goal <goalId>` (校验目标存在) 优先; 没给就为这次委派**建一个目标**,
3780
+ // 合同门在生产路径上不再空转 (目标进"等外部回报"态, 见 ensureDelegateGoal)。
3781
+ let goalId = '';
3782
+ let goalNote = '';
3783
+ try {
3784
+ const g = await ensureDelegateGoal(goalFlag || '', taskDesc, caps);
3785
+ goalId = g.goalId;
3786
+ goalNote = g.note;
3787
+ }
3788
+ catch (err) {
3789
+ goalNote = `目标上下文准备失败 → 本次不签工作合同 (合同门不生效): ${String(err?.message || err).slice(0, 160)}`;
3790
+ }
3791
+ const { task, agent, workContract } = await manager.delegateTask('cli-user', taskDesc, caps, 'normal', undefined, goalId ? { goalId, successCriteria: [taskDesc] } : undefined);
3792
+ const head = agent
3793
+ ? `✅ 任务已委派:\n 任务ID: ${task.id}\n 执行Agent: ${agent.name} (${agent.id})\n 状态: ${task.status}`
3794
+ : `⚠️ 未找到合适的Agent,任务已创建:\n 任务ID: ${task.id}\n 状态: ${task.status}`;
3795
+ if (workContract) {
3796
+ response = `${head}\n ${goalNote}\n 工作合同: ${workContract.workId} (目标 ${goalId})\n`
3797
+ + ` 完成判据 ${workContract.successCriteria.length} 条 · 必带证据 ${workContract.requiredEvidence.length} 条\n`
3798
+ + ` ⚠️ 回报必须按判据逐条给证据: 一句"全部做完了"**不会**被接受为完成`;
3584
3799
  }
3585
3800
  else {
3586
- response = `⚠️ 未找到合适的Agent,任务已创建:\n 任务ID: ${task.id}\n 状态: ${task.status}`;
3801
+ response = `${head}\n ⚠️ 未签工作合同: ${goalNote || '没有目标上下文'} —— 无合同的回报不做证据核验`;
3587
3802
  }
3588
3803
  break;
3589
3804
  }
@@ -3985,7 +4200,7 @@ async function runNonInteractive(args, comm) {
3985
4200
  outputBuffer += params.join(' ') + '\n';
3986
4201
  };
3987
4202
  if (tool) {
3988
- await runToolCommand(tool, toolArgs, false, comm, args.model, prompt);
4203
+ await runToolCommand(tool, toolArgs, false, comm, args.model, prompt, args.goal);
3989
4204
  }
3990
4205
  else if (prompt) {
3991
4206
  const a = await getAgent();
@@ -4025,7 +4240,7 @@ async function runNonInteractive(args, comm) {
4025
4240
  return;
4026
4241
  }
4027
4242
  if (tool) {
4028
- await runToolCommand(tool, toolArgs, !!json, comm, args.model, prompt);
4243
+ await runToolCommand(tool, toolArgs, !!json, comm, args.model, prompt, args.goal);
4029
4244
  }
4030
4245
  else if (prompt) {
4031
4246
  const startTime = Date.now();
@@ -4186,6 +4401,10 @@ function parseArgs() {
4186
4401
  }
4187
4402
  result.toolArgs = delArgs;
4188
4403
  break;
4404
+ // ★ 2026-09-25 (P5 验收修复): 给 --delegate 指定长期目标 (有 goalId 才签工作合同)
4405
+ case '--goal':
4406
+ result.goal = args[++i];
4407
+ break;
4189
4408
  case '--engine':
4190
4409
  case '-e':
4191
4410
  result.engine = args[++i];
@@ -4389,7 +4608,9 @@ function printHelp() {
4389
4608
  # SubAgent 管理
4390
4609
  --agents 列出所有 SubAgent
4391
4610
  --register-agent <name> [cap1] [cap2]... 注册新 SubAgent
4392
- --delegate <任务描述> [能力要求...] 委派任务给最佳 Agent
4611
+ --delegate <任务描述> [能力要求...] [--goal <goalId>]
4612
+ 委派任务给最佳 Agent (签工作合同: 回报必须带逐条证据;
4613
+ --goal 不给则为本次委派建一个目标)
4393
4614
 
4394
4615
  # 全局共享上下文
4395
4616
  --context 显示全局共享上下文摘要
@@ -4502,6 +4723,29 @@ async function main() {
4502
4723
  printHelp();
4503
4724
  process.exit(0);
4504
4725
  }
4726
+ // 2026-09-26: 启动期日志闸门 —— 在**任何 bootstrap 之前**装上, 覆盖三个启动面:
4727
+ // `cli-interactive` (CLI 交互启动, stdout 归 Ink, 信号行改道 stderr) / `web` (dashboard) / 其它 `plain`。
4728
+ // 默认: 加载日志不上控制台 + 全部落日志文件; `--verbose` / `BOLLOON_VERBOSE=1`: 一行不改地全量回流;
4729
+ // 带「失败/未就绪/超时/⚠」等信号的行任何模式下都可见 (leo: 错误不许被吞)。
4730
+ // 不装闸门的三类: `--json` (stdout 是机器读的数据本体)、一次性工具调用 (`--prompt`/`--tool`/`--read` …)、
4731
+ // 命令式诊断 (`--setup-*` / `--supervise*`, 它们的输出就是交付物)。
4732
+ const isOneShotTool = !!(args.tool || args.prompt);
4733
+ const isCommandLike = !!(args.supervise || args.setupStatus || args.setupResume || args.setupRepair
4734
+ || args.setupReconfigure || args.setupTest);
4735
+ if (!args.json && !isOneShotTool && !isCommandLike) {
4736
+ _startupLogGate = installStartupLogGate({
4737
+ mode: args.web ? 'web' : 'cli-interactive',
4738
+ args: process.argv.slice(2),
4739
+ });
4740
+ // 闸门自己的失败必须可见 (否则「日志写不进去」会成为新的静默缺陷)
4741
+ if (_startupLogGate.stats.fileError) {
4742
+ process.stderr.write(`${_startupLogGate.stats.fileError}\n`);
4743
+ }
4744
+ // 诊断模式提示: 告诉用户「现在全量输出」+ 静默时的日志落哪 (默认模式下这一行不出现)
4745
+ if (isStartupVerbose(process.argv.slice(2))) {
4746
+ process.stderr.write(`[log-gate] 诊断模式 (--verbose / ${VERBOSE_ENV}=1): 启动日志全量输出, 同时落盘 ${startupLogPath()}\n`);
4747
+ }
4748
+ }
4505
4749
  // 启动时后台检查更新 (不阻塞主流程)。
4506
4750
  // 2026-09-19 行为变更: **默认只通知, 不自动安装** —— 发现新版本会打印一行提示 +
4507
4751
  // "bolloon update plan / now"。要恢复自动安装需显式 config.json `autoInstall: true`
@@ -4593,54 +4837,75 @@ async function main() {
4593
4837
  };
4594
4838
  const isCLIInteractive = mode === 'cli' && !isNonInteractive;
4595
4839
  if (isCLIInteractive) {
4596
- console.log = () => { };
4597
- console.info = () => { };
4598
- // 2026-08-07: 只吞 SDK 结构化日志 (2026-...T 前缀直接写 stdout 的), 其余 (Ink ANSI 渲染) 走原始 write
4599
- // 不能整体 no-op — Ink 渲染依赖 write callback, 且 pty/管道缓冲满时 no-op 会掩盖真实写状态
4600
- process.stdout.write = ((chunk, ...rest) => {
4601
- const s = String(chunk);
4602
- if (/^\d{4}-\d{2}-\d{2}T/.test(s))
4603
- return true; // SDK 结构化日志 → 吞
4604
- return originalStdoutWrite(chunk, ...rest);
4605
- });
4840
+ // 2026-09-26: console.log/info 与 stdout 的静默已由 `src/cli/log-gate.ts` 统一负责
4841
+ // (mode='cli-interactive': 加载日志丢弃并写文件, 信号行改道 stderr, Ink 的 ANSI 原样放行)。
4842
+ // 这里只留「交互模式不打扰用户」的那一件与日志无关的事: 静音 auto-update 通知。
4606
4843
  // 2026-08-07: 交互模式静音 auto-update 后台通知 (stderr), 避免 "🔍 检查更新" 污染 TUI
4607
4844
  void import('./utils/auto-update.js').then(({ setNotifyQuiet }) => setNotifyQuiet(true)).catch(() => { });
4608
4845
  }
4609
4846
  // 2026-09-13: 首次运行引导 — 没有可用模型供应商 / 还没有用户身份时, 先走初始化向导
4610
4847
  // (放在 CLI 启动前: 用户先回答"你是谁 / 用哪个模型", 再进 TUI 面板)
4611
4848
  if (isCLIInteractive) {
4849
+ // 面板优先口径 (2026-09-27): 这一程后面打的所有启动期进度/降级行都走 `bootNotice()`
4850
+ // —— 面板第一帧之前不上屏, 但"失败/降级/需人介入"折进面板告警 (两条口径同时成立)。
4851
+ // `--verbose` / `BOLLOON_STARTUP_PREAMBLE=1` 时这个开关是 false ⇒ 逐字照旧打屏。
4852
+ startupPanelFirst = !startupPreambleVisible();
4612
4853
  // 2026-09-16 (M4 启动硬门禁): 先把初始化事实读出来 —— 未就绪就**先修**, 修不好就非零退出。
4613
4854
  // 旧行为是"向导失败也只 warn, 照常进对话", 结果是半成品配置能进运行态 (看起来启动成功、实际不可执行)。
4855
+ //
4856
+ // 2026-09-27 (leo 口径): **打完 `bolloon --cli` 就该直接看到面板** ——
4857
+ // 下面这一整段 (门禁报告 / 向导续跑前言 / `✅ 初始化完成`) 属于"启动前的日志", 默认**一个字节都不上屏**:
4858
+ // · `describeSetup(ev)` 改走 `noticePreamble()` → 默认进缓冲 (照落启动日志), verbose/`BOLLOON_STARTUP_PREAMBLE=1` 时逐字回来;
4859
+ // · 就绪度折成**面板一行** (`就绪: basic ✓ · … · 详情: bolloon setup status`, 在 startCLI 里 appendLine);
4860
+ // · 失败 / 需人介入折成**面板告警行** (`alertsFromSetup`) —— 不许被吞, 但也不许刷屏。
4861
+ // 兜底: 只要这一程**走不到面板** (非零退出) 或**要人当场回答** (向导提问前), 缓冲先 flush 到 stderr。
4614
4862
  const w = (m) => process.stderr.write(m.endsWith('\n') ? m : m + '\n');
4615
4863
  let gateEv = null;
4616
4864
  try {
4617
4865
  const { refreshSetupState, describeSetup } = await import('./setup/setup-store.js');
4618
4866
  gateEv = await refreshSetupState({ light: true });
4867
+ // 面板一行就绪度 + 告警 (先登记, 面板渲染时取走)
4868
+ setStartupReadiness(readinessLine(gateEv.state?.readiness || {}));
4869
+ for (const a of alertsFromSetup(gateEv)) {
4870
+ pushStartupAlert(a);
4871
+ noticePreamble(a);
4872
+ }
4619
4873
  if (gateEv.gate !== 'ready') {
4620
- w(describeSetup(gateEv));
4874
+ noticePreamble(describeSetup(gateEv));
4621
4875
  if (process.env.BOLLOON_SKIP_SETUP === '1') {
4622
- w('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
4876
+ noticePreamble('⚠ BOLLOON_SKIP_SETUP=1 → 诊断模式: 可以看状态/修配置, 但 agent 执行被门禁拦住 (不能绕过)');
4623
4877
  }
4624
4878
  else {
4625
4879
  const { runSetupWizard } = await import('./cli/setup-wizard.js');
4626
- await runSetupWizard({ interactive: true });
4880
+ // quietStartup: 向导的续跑前言默认不上屏 (它自己会在"要人回答/有失败"时先 flush)
4881
+ await runSetupWizard({ interactive: true, quietStartup: true });
4627
4882
  const after = await refreshSetupState({});
4883
+ setStartupReadiness(readinessLine(after.state?.readiness || {}));
4884
+ for (const a of alertsFromSetup(after)) {
4885
+ pushStartupAlert(a);
4886
+ noticePreamble(a);
4887
+ }
4628
4888
  if (after.gate !== 'ready') {
4629
- w(describeSetup(after));
4889
+ // 走不到面板 → 前言必须先落屏 (否则就是被吞)
4890
+ noticePreamble(describeSetup(after));
4891
+ flushStartupNotices();
4630
4892
  w('⛔ 初始化未完成 → 退出 (不会以"看起来能跑"的状态进入对话)');
4631
4893
  process.exit(1);
4632
4894
  }
4633
- w('✅ 初始化完成, 进入正常模式');
4895
+ noticePreamble('✅ 初始化完成, 进入正常模式');
4634
4896
  }
4635
4897
  }
4636
4898
  }
4637
4899
  catch (e) {
4638
- // 评估/向导自身异常: fail-closed —— 不假装就绪
4639
- w(`⛔ 初始化流程失败 (fail-closed): ${String(e?.message || e).slice(0, 200)}`);
4640
- w(` 当前状态评估: ${gateEv ? gateEv.gate : '未知 (评估都没跑通)'}`);
4641
- w(' 排查: `bolloon setup --status` (或删掉 ~/.bolloon/setup-state.json 后重跑 setup)');
4900
+ // 评估/向导自身异常: fail-closed —— 不假装就绪, 且缓冲**必须**落屏 (这就要退出了, 没有面板接着)
4901
+ noticePreamble(`⛔ 初始化流程失败 (fail-closed): ${String(e?.message || e).slice(0, 200)}`, { visible: true });
4902
+ noticePreamble(` 当前状态评估: ${gateEv ? gateEv.gate : '未知 (评估都没跑通)'}`, { visible: true });
4903
+ noticePreamble(` 排查: \`bolloon setup status\` (或删掉 ~/.bolloon/setup-state.json 后重跑 setup)`, { visible: true });
4904
+ flushStartupNotices();
4642
4905
  process.exit(1);
4643
4906
  }
4907
+ if (startupPreambleVisible())
4908
+ w(preambleHintLine());
4644
4909
  }
4645
4910
  if (isNonInteractive) {
4646
4911
  console.error = () => { };
@@ -4667,8 +4932,20 @@ async function main() {
4667
4932
  hasKimi ? 'Kimi' :
4668
4933
  hasGlm ? 'GLM' :
4669
4934
  hasQwen ? 'Qwen' : null;
4935
+ // 2026-09-26: 启动装配统一从「有效模型配置」来 (Session > Global > 供应商默认 > 环境变量)。
4936
+ // 旧写法按"环境变量里有哪些 key"挑供应商, 与 Web 启动(读配置文件)给出不同结果 ——
4937
+ // 同一个人配了 deepseek 配置文件、环境里又留着 OPENAI_API_KEY, 换个入口就换模型。
4670
4938
  if (llmProvider) {
4671
- initMinimax({ provider: llmProvider.toLowerCase() });
4939
+ try {
4940
+ const { applyEffectiveToRuntime } = await import('./llm/model-selection.js');
4941
+ const eff = await applyEffectiveToRuntime();
4942
+ bootNotice('info', `模型: ${eff.provider}/${eff.model} (来源: ${eff.source})`);
4943
+ }
4944
+ catch (e) {
4945
+ // 装配失败不静默: 退回按环境变量初始化 (至少让用户看到原因)
4946
+ bootNotice('warn', `按有效配置装配模型失败 (${String(e?.message || e).slice(0, 120)}), 退回环境变量猜测`);
4947
+ initMinimax({ provider: llmProvider.toLowerCase() });
4948
+ }
4672
4949
  }
4673
4950
  else {
4674
4951
  if (isNonInteractive) {
@@ -4688,11 +4965,11 @@ async function main() {
4688
4965
  if (typeof checkKuboSetup === 'function') {
4689
4966
  const setup = await checkKuboSetup(true, true);
4690
4967
  kuboReady = !!(setup?.ready && setup?.daemonRunning);
4691
- s.info(kuboReady ? 'IPFS 本地 Kubo 就绪 → IPNS 发布/解析可用' : 'Kubo 不可用, IPFS 降级本地模式');
4968
+ bootNotice('info', kuboReady ? 'IPFS 本地 Kubo 就绪 → IPNS 发布/解析可用' : 'Kubo 不可用, IPFS 降级本地模式');
4692
4969
  }
4693
4970
  }
4694
4971
  catch (e) {
4695
- s.warn(`Kubo 自动安装失败 (非致命): ${String(e?.message || e).slice(0, 120)}`);
4972
+ bootNotice('warn', `Kubo 自动安装失败 (非致命): ${String(e?.message || e).slice(0, 120)}`);
4696
4973
  }
4697
4974
  publishDID(name, keypair).then(({ cid, ipnsName }) => {
4698
4975
  if (cid)
@@ -4714,7 +4991,7 @@ async function main() {
4714
4991
  agentIdentity.p2pChannel = 'bolloon-agent-harness';
4715
4992
  }
4716
4993
  }).catch(err => {
4717
- s.warn(`P2P Web 模式启动失败: ${err.message}`);
4994
+ bootNotice('warn', `P2P Web 模式启动失败: ${err.message}`);
4718
4995
  });
4719
4996
  }
4720
4997
  else if (isCLIInteractive) {
@@ -4722,7 +4999,7 @@ async function main() {
4722
4999
  // startCLI 收 Promise, 内部空安全 (P2P 功能就绪前自动降级, 一般 1-3s 内可用)
4723
5000
  commReady = withTimeout(bootstrapP2P(verifier), 20_000, 'P2P 网络初始化')
4724
5001
  .catch((err) => {
4725
- s.warn(`P2P 初始化超时/失败, 降级无 P2P 模式: ${err.message}`);
5002
+ bootNotice('warn', `P2P 初始化超时/失败, 降级无 P2P 模式: ${err.message}`);
4726
5003
  return null;
4727
5004
  });
4728
5005
  void commReady.then((c) => {
@@ -4739,7 +5016,7 @@ async function main() {
4739
5016
  // 2026-08-07: 弱网下 hyperswarm DHT start/joinTopic 可能无限挂起 → 20s 超时门, 超时降级无 P2P 模式
4740
5017
  comm = await withTimeout(bootstrapP2P(verifier), 20_000, 'P2P 网络初始化')
4741
5018
  .catch((err) => {
4742
- s.warn(`P2P 初始化超时/失败, 降级无 P2P 模式: ${err.message}`);
5019
+ bootNotice('warn', `P2P 初始化超时/失败, 降级无 P2P 模式: ${err.message}`);
4743
5020
  return null;
4744
5021
  });
4745
5022
  if (comm) {
@@ -4752,37 +5029,37 @@ async function main() {
4752
5029
  }
4753
5030
  }
4754
5031
  catch (err) {
4755
- s.warn(`P2P 初始化失败: ${err.message}`);
4756
- s.warn('将使用无 P2P 模式运行');
5032
+ bootNotice('warn', `P2P 初始化失败: ${err.message}`);
5033
+ bootNotice('warn', '将使用无 P2P 模式运行');
4757
5034
  }
4758
5035
  if (isCLIInteractive) {
4759
5036
  // 2026-09-08 加速启动: iroh + Bolloon bootstrap 也全部后台, 不阻塞 UI 首帧
4760
5037
  void withTimeout(bootstrapIroh(keypair, name), 15_000, 'iroh P2P 初始化')
4761
- .catch((err) => s.warn(`iroh 初始化超时, 继续使用 Hyperswarm P2P: ${err.message}`));
5038
+ .catch((err) => bootNotice('warn', `iroh 初始化超时, 继续使用 Hyperswarm P2P: ${err.message}`));
4762
5039
  void (async () => {
4763
5040
  try {
4764
5041
  const { bootstrapBolloon } = await import('./pi-ecosystem-judgment/human-value-pipeline.js');
4765
5042
  const bs = await withTimeout(bootstrapBolloon({ cwd: process.cwd() }), 20_000, 'Bolloon 上下文扫描');
4766
- s.info(`Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
5043
+ bootNotice('info', `Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
4767
5044
  }
4768
5045
  catch (err) {
4769
- s.warn(`Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
5046
+ bootNotice('warn', `Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
4770
5047
  }
4771
5048
  })();
4772
5049
  }
4773
5050
  else {
4774
5051
  await withTimeout(bootstrapIroh(keypair, name), 15_000, 'iroh P2P 初始化')
4775
- .catch((err) => s.warn(`iroh 初始化超时, 继续使用 Hyperswarm P2P: ${err.message}`));
5052
+ .catch((err) => bootNotice('warn', `iroh 初始化超时, 继续使用 Hyperswarm P2P: ${err.message}`));
4776
5053
  // Bolloon Bootstrap: 启动扫描 + Context 收集 + 挂定时任务
4777
5054
  // 失败静默 (主流程不被阻塞)
4778
5055
  try {
4779
5056
  const { bootstrapBolloon } = await import('./pi-ecosystem-judgment/human-value-pipeline.js');
4780
- s.info('正在 bootstrap bolloon 上下文...');
5057
+ bootNotice('info', '正在 bootstrap bolloon 上下文...');
4781
5058
  const bs = await withTimeout(bootstrapBolloon({ cwd: process.cwd() }), 20_000, 'Bolloon 上下文扫描');
4782
- s.info(`Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
5059
+ bootNotice('info', `Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
4783
5060
  }
4784
5061
  catch (err) {
4785
- s.warn(`Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
5062
+ bootNotice('warn', `Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
4786
5063
  }
4787
5064
  }
4788
5065
  if (mode === 'web') {