@bolloon/bolloon-agent 0.4.33 → 0.5.0

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 (143) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +493 -35
  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/network-pulse.js +304 -18
  31. package/dist/agents/pi-sdk.js +75 -20
  32. package/dist/agents/run-store.js +18 -5
  33. package/dist/agents/skill-readiness.js +52 -10
  34. package/dist/agents/subagent-manager.js +209 -3
  35. package/dist/agents/task/task-runner.js +9 -3
  36. package/dist/agents/task-group.js +8 -51
  37. package/dist/agents/task-public-text.js +89 -0
  38. package/dist/cli/commands/index.js +1 -0
  39. package/dist/cli/commands/tasks.js +350 -2
  40. package/dist/cli/identity-command.js +177 -0
  41. package/dist/cli/setup-wizard.js +71 -0
  42. package/dist/cli/update-commands.js +150 -24
  43. package/dist/cli-entry.js +12 -2
  44. package/dist/electron-build/agents/contacts/chain.js +26 -6
  45. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  46. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  47. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  48. package/dist/electron-build/agents/external-events.js +34 -5
  49. package/dist/electron-build/agents/external-events.js.map +1 -1
  50. package/dist/electron-build/agents/goal-criteria.js +10 -3
  51. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  52. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  53. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  55. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  56. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  57. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  58. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  59. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  60. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  61. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  63. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  65. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  67. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  69. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  71. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  79. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  81. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  83. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  84. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  85. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  86. package/dist/electron-build/agents/goal-store.js +35 -1
  87. package/dist/electron-build/agents/goal-store.js.map +1 -1
  88. package/dist/electron-build/agents/network-pulse.js +307 -19
  89. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  90. package/dist/electron-build/agents/pi-sdk.js +74 -19
  91. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  92. package/dist/electron-build/agents/run-store.js +20 -5
  93. package/dist/electron-build/agents/run-store.js.map +1 -1
  94. package/dist/electron-build/agents/skill-readiness.js +52 -9
  95. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  96. package/dist/electron-build/agents/task/task-runner.js +8 -2
  97. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  98. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  99. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  100. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  101. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  102. package/dist/electron-build/utils/dual-source.js +471 -0
  103. package/dist/electron-build/utils/dual-source.js.map +1 -0
  104. package/dist/electron-build/utils/update-manager.js +331 -57
  105. package/dist/electron-build/utils/update-manager.js.map +1 -1
  106. package/dist/electron-build/utils/update-state.js +16 -1
  107. package/dist/electron-build/utils/update-state.js.map +1 -1
  108. package/dist/electron-build/utils/version-info.js +82 -2
  109. package/dist/electron-build/utils/version-info.js.map +1 -1
  110. package/dist/index.js +103 -12
  111. package/dist/ios/chain-explorer.js +617 -0
  112. package/dist/ios/explorer.html +163 -0
  113. package/dist/ios/index.html +96 -0
  114. package/dist/ios/mobile-agent.js +2 -2
  115. package/dist/ios/mobile-contacts.js +329 -0
  116. package/dist/ios/mobile-core.js +1379 -226
  117. package/dist/ios/mobile-privacy.js +185 -0
  118. package/dist/ios/mobile.css +19 -0
  119. package/dist/ios/mobile.html +96 -0
  120. package/dist/ios/mobile.js +706 -7
  121. package/dist/ios/routes-chain-index.js +108 -0
  122. package/dist/ios/routes-contacts.js +420 -0
  123. package/dist/ios/routes-onchain-trade.js +99 -0
  124. package/dist/ios/routes-x402-info.js +1 -0
  125. package/dist/ios/server.js +762 -0
  126. package/dist/ios/task-frame-server.js +211 -0
  127. package/dist/orbitdb/cid-database.js +81 -16
  128. package/dist/orbitdb/ipfs-node.js +31 -2
  129. package/dist/utils/dual-source.js +426 -0
  130. package/dist/utils/update-cli.js +16 -21
  131. package/dist/utils/update-health.js +35 -5
  132. package/dist/utils/update-manager.js +329 -57
  133. package/dist/utils/update-state.js +16 -1
  134. package/dist/utils/version-info.js +75 -1
  135. package/dist/web/mobile-contacts.js +44 -11
  136. package/dist/web/mobile-core.js +1197 -513
  137. package/dist/web/mobile-tasks.js +339 -0
  138. package/dist/web/mobile.css +19 -0
  139. package/dist/web/mobile.html +64 -0
  140. package/dist/web/mobile.js +531 -1
  141. package/dist/web/routes-mobile-tasks.js +353 -0
  142. package/dist/web/server.js +163 -4
  143. package/package.json +3 -1
@@ -54,6 +54,16 @@ exports.resetSupervisorForTest = resetSupervisorForTest;
54
54
  const os = __importStar(require("os"));
55
55
  const goal_store_js_1 = require("./goal-store.js");
56
56
  const run_store_js_1 = require("./run-store.js");
57
+ // 2026-09-25 (飞轮接线): P0–P4 的七个模块经**唯一**适配层接进真实执行路径
58
+ // (节奏由进展决定 · Run 收尾必过 closeRun · 子 Agent 走工作合同 · 阻塞监控 · 变更注入 · 用户可见态)
59
+ // 2026-09-25 (串行收口): 四条线各自报"只有主线能加"的钩子在这里补齐 ——
60
+ // ① 节奏判定走 **M1 接缝的事实路径** (`preflightGoalStep`, 不再直连 `decideGoalStep`);
61
+ // ② Skill 试用在**候选产出处**开 (`closeGoalRun` 内部) / 在**下一条 Run 成功点**结算;
62
+ // ③ 阻塞巡检走 **M3 的统一巡检** (`flywheelSeams().monitor.sweepAll`, 不再自己拼一条循环);
63
+ // ④ 撤销走 **M4 的 change 接缝** (`ingestRequirement`, 非 web 宿主也能真停住在跑的 Run)。
64
+ const goal_flywheel_wiring_js_1 = require("./goal-flywheel-wiring.js");
65
+ // 2026-09-25 (M0 接线冻结, 规则 ②): Goal 状态只有一个写入出口
66
+ const goal_state_reducer_js_1 = require("./goal-state-reducer.js");
57
67
  /** 自动继续的退避 (attempts → 等待毫秒)。0 次 = 立刻。 */
58
68
  function continuationBackoffMs(attempts) {
59
69
  const steps = [0, 15_000, 60_000, 5 * 60_000, 15 * 60_000];
@@ -237,6 +247,11 @@ class ExecutionSupervisor {
237
247
  this.tickCount++;
238
248
  const report = { ...this.emptyReport(), tick: this.tickCount };
239
249
  try {
250
+ // 0. (2026-09-25, M0 接线冻结) 把"Run 终止 → 唯一责任链"的钩子装上:
251
+ // 崩溃恢复 / 失速这两条终止路径由 run-store 判出, 由接线层收尾 (规则 ④)。
252
+ await (0, goal_flywheel_wiring_js_1.installRunTerminalHook)().catch((err) => {
253
+ report.errors.push(`Run 终止钩子安装失败: ${err?.message || err}`);
254
+ });
240
255
  // 1. 对账孤儿 (进程死了的 run) —— 只在启动后第一次做, 之后靠 tick 的常规巡检
241
256
  if (!this.reconciledOnce) {
242
257
  report.reconciled = await (0, run_store_js_1.reconcileOrphans)();
@@ -289,8 +304,102 @@ class ExecutionSupervisor {
289
304
  // 2. 扫描可执行 Goal
290
305
  const { runnable, skipped } = await (0, goal_store_js_1.listRunnableGoals)({ now: this.now(), owner: this.owner });
291
306
  report.skipped = skipped;
292
- // 3. 逐个推进 (每个 Goal: 抢 lease → 执行 → 决策 → 释放 lease)
307
+ const nowIso = new Date(this.now()).toISOString();
308
+ // 2.5 (2026-09-25, 飞轮接线 P3 + 串行收口): 阻塞巡检 —— **不看进程存活, 看任务是否卡住**
309
+ // 走 M3 的**统一巡检** (`flywheelSeams().monitor.sweepAll`): 一次调用 = 所有待回报 Goal 的
310
+ // 结论 (有界 + 失败留痕 + 每格带用户可见态)。之前 supervisor 自己拼了一条
311
+ // `listGoalsWithPendingWork → collectWorkBlocks → applyBlockHandling` 的循环 —— 那是
312
+ // 第二套巡检实现 (`sweepAll` 才是 M3 的门面), 于是 M3 的"统一巡检"在真路径上从来没有调用方。
313
+ let monitorView = null;
314
+ try {
315
+ const view = await (0, goal_flywheel_wiring_js_1.flywheelSeams)().monitor.sweepAll({
316
+ caller: 'supervisor',
317
+ now: nowIso,
318
+ runnerAvailable: this.canExecute,
319
+ });
320
+ if ((0, goal_flywheel_wiring_js_1.isRefusal)(view)) {
321
+ // 拒绝不是"没有阻塞": 如实记进 errors, 不许静默当成巡检通过
322
+ report.errors.push(`阻塞巡检被拒: ${view.reason}`);
323
+ }
324
+ else {
325
+ monitorView = view;
326
+ report.monitor = view;
327
+ for (const cell of view.goals) {
328
+ for (const a of cell.handling?.actions ?? []) {
329
+ report.blocks.push({ goalId: cell.goalId, workId: a.workId, kind: a.kind, action: a.action, note: a.note });
330
+ }
331
+ if (cell.error)
332
+ report.errors.push(`阻塞巡检 goal=${cell.goalId}: ${cell.error}`);
333
+ }
334
+ const acted = view.goals.filter((c) => (c.handling?.actions.length ?? 0) > 0);
335
+ for (const c of acted) {
336
+ this.log(`[supervisor] goal=${c.goalId} 阻塞巡检: ${(c.handling?.actions ?? []).map((a) => `${a.kind}→${a.action}`).join(', ')}`);
337
+ }
338
+ const escalated = [...new Set(view.escalated)];
339
+ if (escalated.length) {
340
+ // 巡检未完成 (silentRisk) 与"交人"是两回事: 前者是巡检自身没查成, 后者是任务真的卡住
341
+ const why = view.silentRisk
342
+ ? `巡检未完成 (silentRisk=true, 一个 Goal 都没查成): ${escalated.join(', ')}`
343
+ : `子 Agent 工作升级到人: ${escalated.join(', ')}`;
344
+ this.emit({ kind: 'needs_human', goalId: view.goals[0]?.goalId, message: why });
345
+ this.log(`[supervisor] 阻塞巡检 → 交人: ${why}`);
346
+ }
347
+ if (view.takeovers.length)
348
+ this.log(`[supervisor] 父接管子工作 (无心跳 + 执行权空闲 + 合同允许): ${view.takeovers.join(', ')}`);
349
+ if (view.requests.length)
350
+ this.log(`[supervisor] 阻塞巡检要求补齐: ${view.requests.join(', ')}`);
351
+ if (view.errors.length && !escalated.length)
352
+ report.errors.push(`阻塞巡检 (接缝自检): ${view.errors.join(' | ')}`);
353
+ }
354
+ }
355
+ catch (err) {
356
+ report.errors.push(`阻塞巡检: ${err?.message || err}`);
357
+ }
358
+ // 3. 逐个推进 (每个 Goal: 节奏判定 → 抢 lease → 执行 → 收尾 → 决策 → 释放 lease)
293
359
  for (const goal of runnable.slice(0, this.maxPerTick)) {
360
+ // 3.1 (P0 + M1 串行收口) 节奏由进展决定: 走 **M1 接缝的事实路径**
361
+ // (`readFacts` 读 Goal/Run/进展/无进展连击 → 接缝用 P0 自己判 + 上限归因)。
362
+ // 事实拿不到时接缝内部才回落到旧注入口径, 并在 `step.source` 上如实标出来。
363
+ let step = null;
364
+ try {
365
+ const pre = await (0, goal_flywheel_wiring_js_1.preflightGoalStep)({
366
+ goalId: goal.goalId, now: nowIso, maxRetries: this.maxRetries, writeRecord: true,
367
+ });
368
+ if (pre && (0, goal_flywheel_wiring_js_1.isRefusal)(pre)) {
369
+ // 接缝真拒 (caller 不是 supervisor / 没有 goalId) → 如实记, 不静默跳过
370
+ report.errors.push(`${goal.goalId}: 节奏判定被拒: ${pre.reason}`);
371
+ }
372
+ else {
373
+ step = pre;
374
+ }
375
+ }
376
+ catch (err) {
377
+ report.errors.push(`${goal.goalId}: 节奏判定失败 ${err?.message || err}`);
378
+ }
379
+ if (step) {
380
+ // 阻塞结论复用**同一 tick 的统一巡检**结果 (没有那一格才自己读): 一条 tick 里不查第二遍
381
+ const swept = monitorView?.goals.find((c) => c.goalId === goal.goalId)?.blocks ?? null;
382
+ report.flywheel.push(await (0, goal_flywheel_wiring_js_1.flywheelTickNote)({ goalId: goal.goalId, step, now: nowIso, blocks: swept }).catch(() => ({
383
+ goalId: goal.goalId,
384
+ decision: step.decision?.decision ?? 'first_run',
385
+ state: step.decision?.state ?? 'progressing',
386
+ runnable: step.runnable,
387
+ reason: step.reason,
388
+ noProgressStreak: step.noProgressStreak,
389
+ visibleState: 'executing',
390
+ nextAction: step.decision?.nextAction ?? '(首个 Run)',
391
+ source: step.source,
392
+ basis: step.basis,
393
+ caps: step.caps,
394
+ })));
395
+ if (!step.runnable) {
396
+ report.skipped.push({ goalId: goal.goalId, reason: `飞轮: ${step.reason}` });
397
+ await this.applyFlywheelStop(goal.goalId, step, nowIso).catch((err) => {
398
+ report.errors.push(`${goal.goalId}: 飞轮停写失败 ${err?.message || err}`);
399
+ });
400
+ continue;
401
+ }
402
+ }
294
403
  const claimed = await (0, goal_store_js_1.claimGoal)(goal.goalId, { owner: this.owner, ttlMs: this.leaseTtlMs });
295
404
  if (!claimed.ok) {
296
405
  report.skipped.push({ goalId: goal.goalId, reason: claimed.reason || 'claim 失败' });
@@ -299,7 +408,7 @@ class ExecutionSupervisor {
299
408
  report.claimed.push(goal.goalId);
300
409
  const leaseId = claimed.lease.leaseId;
301
410
  try {
302
- const res = await this.runGoal(goal, leaseId, report);
411
+ const res = await this.runGoal(goal, leaseId, report, step);
303
412
  report.executed.push(res);
304
413
  }
305
414
  catch (err) {
@@ -328,10 +437,58 @@ class ExecutionSupervisor {
328
437
  payments: { scanned: 0, reconciled: [], awaitingPayment: [], mustNotRepay: [], closed: [], goalsWoken: [], goalsFlagged: [], errors: [] },
329
438
  supervised: { stalled: [], failed: [] },
330
439
  claimed: [], executed: [], skipped: [], errors: [], dryRun: !this.runner,
440
+ flywheel: [], blocks: [], monitor: null, skillTrials: [], skillTrialOpenings: [], closures: [], workContracts: [],
331
441
  };
332
442
  }
443
+ /**
444
+ * 飞轮说"不该跑"时, 该不该把"停"写进 Goal?
445
+ *
446
+ * 只有**明确要求停**的决策才写 (fail / pause / 无进展熔断 / 硬底线 / 阻塞):
447
+ * 这些是"停就是停"的判断, 不写下去的话下一个 tick 还会来问一遍 (且用户看不到原因)。
448
+ * 其余 `ask_human` (例如"本轮没有新证据, 未到熔断阈值 → 需要人定夺") 只**跳过本轮**、
449
+ * 不动状态: 那属于"没有继续的资格", 由运输层(退避/等外部)与人决定 ——
450
+ * 这样既不会用轮次冒充节奏, 也不会把一次没证据的 Run 直接判死。
451
+ */
452
+ async applyFlywheelStop(goalId, step, now) {
453
+ const d = step.decision;
454
+ if (!d)
455
+ return;
456
+ const hardLine = /硬底线/.test(step.reason);
457
+ const stop = d.decision === 'fail'
458
+ || d.decision === 'pause'
459
+ || (d.decision === 'ask_human' && (d.state === 'no_progress' || d.state === 'blocked'))
460
+ || hardLine;
461
+ if (!stop)
462
+ return;
463
+ const status = d.decision === 'fail' ? 'failed' : d.decision === 'pause' ? 'paused' : 'needs_human';
464
+ // 规则 ②: 状态与 continuation 都经 Goal reducer 写 (终态保护 / 完成门也在这里, 不全仓各写一遍)
465
+ const applied = await (0, goal_state_reducer_js_1.reduceGoalState)({
466
+ goalId,
467
+ intent: 'flywheel_stop',
468
+ now,
469
+ by: this.owner,
470
+ stopStatus: status,
471
+ reason: d.reason,
472
+ decisionId: d.decisionId,
473
+ nextAction: d.nextAction,
474
+ unresolvedItems: d.unresolvedItems,
475
+ });
476
+ if (!applied.ok) {
477
+ this.log(`[supervisor] goal=${goalId} 飞轮判停写不进 Goal (不掩盖): ${applied.reason}`);
478
+ }
479
+ this.emit({ kind: status === 'needs_human' ? 'needs_human' : status, goalId, message: d.reason });
480
+ this.log(`[supervisor] goal=${goalId} 飞轮判停 → ${status}: ${d.reason}`
481
+ + `${applied.applied.length ? ` [${applied.applied.join(', ')}]` : ''}`);
482
+ }
333
483
  /** 认领后执行一个 Goal: 决定 resume 还是开新 Run → 跑 → 决策 Goal 状态 */
334
- async runGoal(goal, leaseId, report) {
484
+ async runGoal(goal, leaseId, report,
485
+ /**
486
+ * ★ 2026-09-25 (串行收口): 主循环**这一刻**已经落定的节奏判定 (3.1 的结果)。
487
+ * 传下来的理由: 「同一条 tick 里事实只读一次」。以前门禁自己又 `preflightGoalStep()` 了一遍,
488
+ * 于是同一次事实读留痕翻倍、而且门禁看到的是**另一个时刻**的事实 —— 与
489
+ * `flywheelTickNote` 复用统一巡检结论 (`blocks`) 是同一条纪律。
490
+ */
491
+ tickStep = null) {
335
492
  // 乐观并发检查: 认领后重新读一次 —— 若这个 Goal 在我扫描之后已被别的 worker 推进
336
493
  // (状态/当前 run/run 列表/continuation 变了), 就让路。否则同一个状态版本会被两个 worker 各跑一次。
337
494
  const freshGoal = await (0, goal_store_js_1.readGoal)(goal.goalId);
@@ -342,11 +499,21 @@ class ExecutionSupervisor {
342
499
  }
343
500
  // 到点唤醒 (2-C.3): 这条 Goal 之前是 retry_wait —— 现在唤醒它, 旧的 wakeAt/wakeReason 必须清掉,
344
501
  // 否则下一轮 tick 还会把它当"等时间"重复跳过 (或留下过期的等待事实)。
502
+ // ★ M5-⑦ (2026-09-25 真跑): **状态也要一起拉回 `active`** —— 旧写法只清 continuation, `goal.status`
503
+ // 仍停在 `retry_wait`; 这条 Run 跑完后的收尾判定读到 stale 的 `retry_wait` → 判 `wait`, 合并规则
504
+ // 再把"等"落成 `awaiting_external` (wakeAt 已清空 ⇒ 只能靠事件唤醒) ⇒ **刚有进展的目标被挂起来,
505
+ // 没人会再跑它** (P6-③ 时钟用例阴性对照判红 + 临时探针复现)。走唯一漏斗 (规则 ②)。
345
506
  if (goal.continuation?.wakeReason === 'retry_wait' || goal.status === 'retry_wait') {
346
- await (0, goal_store_js_1.setContinuation)(goal.goalId, { wakeReason: 'active', wakeAt: undefined });
507
+ await (0, goal_state_reducer_js_1.reduceGoalState)({
508
+ goalId: goal.goalId,
509
+ intent: 'scheduled_wake',
510
+ now: new Date(this.now()).toISOString(),
511
+ by: this.owner,
512
+ reason: `到点唤醒 (wakeAt=${goal.continuation?.wakeAt ?? '-'} 已到) → 回到可继续, 清 wakeAt 并拉回 active`,
513
+ });
347
514
  report.skipped.push({ goalId: goal.goalId, reason: `到点唤醒: 已清 wakeAt (第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续)` });
348
515
  this.emit({ kind: 'retry_woke', goalId: goal.goalId, message: `到点唤醒, 第 ${(goal.continuation?.attempts || 0) + 1} 次自动继续` });
349
- this.log(`[supervisor] goal=${goal.goalId} retry_wait 到点 → 唤醒并清 wakeAt`);
516
+ this.log(`[supervisor] goal=${goal.goalId} retry_wait 到点 → 唤醒 (状态拉回 active) 并清 wakeAt`);
350
517
  }
351
518
  // ★ 2026-09-18 (M2 收口): `bolloon task` 建的目标走**同一个恢复决策函数**
352
519
  // (`decideTaskRecovery`) —— CLI `task --resume` 与 Supervisor 不再各判一次。
@@ -373,9 +540,19 @@ class ExecutionSupervisor {
373
540
  const prevRun = prevRunId ? await (0, run_store_js_1.readRun)(prevRunId) : null;
374
541
  const plan = prevRunId ? await (0, run_store_js_1.buildContinuationPlan)(prevRunId).catch(() => null) : null;
375
542
  const guards = plan?.replayGuards || [];
376
- const instruction = plan
543
+ // 2026-09-25 (飞轮接线 P4 / P2): 下一个 Run 必须真的读到**同一份**事实 ——
544
+ // ① 权威 continuation 的 nextAction (飞轮收尾写下的"下一步是什么");
545
+ // ② 已生效的新要求 (变更注入: 只影响后续 Run, 不改写历史);
546
+ // ③ 还在等回报的子 Agent 工作合同 (规则 5: 子必须拿到同一份合同)。
547
+ const continuationHint = goal.continuation?.nextAction ? `\n飞轮下一步 (权威 continuation): ${goal.continuation.nextAction}` : '';
548
+ const changeHint = await (0, goal_flywheel_wiring_js_1.nextRunChangeDirective)(goal.goalId).catch(() => null);
549
+ const contractHint = await (0, goal_flywheel_wiring_js_1.pendingContractDigest)(goal.goalId).catch(() => null);
550
+ const instruction = (plan
377
551
  ? `继续这个目标 (不要重头开始):\n目标: ${plan.objective || goal.objective}\n已完成 ${plan.completedSteps.length} 步; 下一步: ${plan.nextAction}`
378
- : `开始执行这个目标:\n目标: ${goal.objective}${goal.successCriteria.length ? `\n完成判据: ${goal.successCriteria.join('; ')}` : ''}`;
552
+ : `开始执行这个目标:\n目标: ${goal.objective}${goal.successCriteria.length ? `\n完成判据: ${goal.successCriteria.join('; ')}` : ''}`)
553
+ + continuationHint
554
+ + (changeHint ? `\n${changeHint}` : '')
555
+ + (contractHint ? `\n${contractHint}` : '');
379
556
  const kind = !prevRun ? 'first_run'
380
557
  : run_store_js_1.RESUMABLE_STATUSES.includes(prevRun.status) ? 'resume'
381
558
  : 'continue_new_run';
@@ -407,12 +584,37 @@ class ExecutionSupervisor {
407
584
  return { goalId: goal.goalId, status: 'unresolved' };
408
585
  }
409
586
  // 2026-09-16 (2-G.2): 执行前技能就绪门禁 —— 缺/未启用/损坏/漂移 → 不启动 Run, Goal → needs_human
587
+ //
588
+ // ★ 2026-09-25 (P5 验收修复): 飞轮这一步的裁决是 **delegate** (缺能力 → 派给子 Agent 并签工作合同)
589
+ // 时, **本地技能门禁不适用** —— 这一步本来就不由本节点执行, 能力由子 Agent 凭合同交付。
590
+ // 原来一律拦成 `blocked_by_skills`, 于是"缺能力 → 派活"这条链在真路径上永远走不到,
591
+ // `dispatchIfDelegate` 成了死代码 (P5 ★①②③ 实测: 合同一份都没签发)。
592
+ // 其余裁决 (continue/first_run/...) 照旧: 技能不就绪照样不启动 Run (门禁没有放松)。
593
+ let flywheelDelegates = false;
594
+ let flywheelCapability = '';
595
+ // 与 3.1 **同一份**判定 (而且是**同一次**事实读): 主循环算好的 `tickStep` 直接沿用 ——
596
+ // 旧写法在这里又 `preflightGoalStep()` 了一遍 (判定口径同, 但事实读了两遍、时刻也不同)。
597
+ // `tickStep === null` (主循环没判成 / 门禁被单独调用) → 按"没有裁决"处理 (fail-closed, 照旧拦)。
598
+ {
599
+ flywheelDelegates = tickStep?.decision?.decision === 'delegate';
600
+ flywheelCapability = tickStep?.decision?.requiredCapability || '';
601
+ }
410
602
  try {
411
- const { ensureGoalSkillsReady, blockGoalOnSkills } = await Promise.resolve().then(() => __importStar(require('./skill-readiness.js')));
603
+ const { ensureGoalSkillsReady, blockGoalOnSkills, recordSkillReadiness } = await Promise.resolve().then(() => __importStar(require('./skill-readiness.js')));
412
604
  const ready = await ensureGoalSkillsReady(goal);
413
605
  for (const d of ready.degradations)
414
606
  this.log(`[supervisor] goal=${goal.goalId} 技能降级: ${d}`);
415
- if (!ready.ok) {
607
+ if (!ready.ok && flywheelDelegates) {
608
+ // 本地缺的能力由子 Agent 提供: 记清楚"放过门禁的原因", 不静默。
609
+ // 这里**不进** report.skipped —— 门禁放过 = 本轮**真跑**了 (Run + 之后的合同签发),
610
+ // 记成 skipped 会与事实相反。
611
+ await recordSkillReadiness(goal.goalId, ready).catch(() => { });
612
+ const why = `飞轮裁决=delegate (缺能力「${flywheelCapability || '-'}」) → 本地技能门禁不适用, `
613
+ + `本轮照跑(不拦)并改为签发工作合同交给子 Agent; 本地缺: ${ready.reason || ''}`;
614
+ this.log(`[supervisor] goal=${goal.goalId} 技能门禁对 delegate 放行: ${why}`);
615
+ this.emit({ kind: 'skill_gate_waived_for_delegate', goalId: goal.goalId, message: why });
616
+ }
617
+ else if (!ready.ok) {
416
618
  await blockGoalOnSkills(goal.goalId, ready);
417
619
  report.skipped.push({ goalId: goal.goalId, reason: `技能未就绪: ${ready.reason}` });
418
620
  this.emit({ kind: 'needs_human', goalId: goal.goalId, message: ready.reason || '技能未就绪' });
@@ -474,55 +676,311 @@ class ExecutionSupervisor {
474
676
  }
475
677
  // 重新读一次 Goal: Run 期间判据可能已被满足 (否则会拿旧快照判决)
476
678
  const goalForDecision = (await (0, goal_store_js_1.readGoal)(goal.goalId)) || goal;
477
- const decision = decideGoalOutcome(goalForDecision, finalRun, { now: this.now(), maxAttempts: this.maxRetries, lastRunStatus: finalRun?.status });
478
- await this.applyDecision(goal, decision, finalRun);
479
- this.emit({ kind: 'goal_decision', goalId: goal.goalId, runId: finalRunId, message: `${finalRun?.status || result.status || '?'} → ${decision.goalStatus}: ${decision.reason}` });
480
- this.log(`[supervisor] goal=${goal.goalId} run=${finalRunId || '-'} ${finalRun?.status || result.status || '?'} → goal=${decision.goalStatus} (${decision.reason}) ${Date.now() - t0}ms`);
679
+ const legacy = decideGoalOutcome(goalForDecision, finalRun, { now: this.now(), maxAttempts: this.maxRetries, lastRunStatus: finalRun?.status });
680
+ // ★ 2026-09-25 (飞轮接线 P1): Run 结束**必过收尾飞轮** —— 正常 / 失败 / 中断恢复都走同一条
681
+ // 9 步流水线 (写 Memory · 生成 Skill 候选 · 更新 continuation · 生成用户汇报)。
682
+ // 飞轮决策是权威 (是否继续), 既有 reducer 退化为运输层 (退避/wakeAt/attempts) —— 见 mergeGoalOutcome。
683
+ let closureDecision = null;
684
+ let merged = {
685
+ goalStatus: legacy.goalStatus,
686
+ continuation: legacy.continuation,
687
+ reason: legacy.reason,
688
+ flywheelStop: false,
689
+ roundsOverridden: false,
690
+ };
691
+ /** 本轮收尾要派遣的能力 (定状态之后才签合同 —— 顺序见下方注释) */
692
+ let capabilityWanted = null;
693
+ if (finalRun) {
694
+ try {
695
+ const closureRes = await (0, goal_flywheel_wiring_js_1.closeRunOnce)({
696
+ goalId: goal.goalId,
697
+ runId: finalRun.runId,
698
+ caller: 'supervisor',
699
+ now: new Date(this.now()).toISOString(),
700
+ finalReview: this.finalReviewText(finalRun, result),
701
+ maxRetries: this.maxRetries,
702
+ // ★ 串行收口: 把**申明**的终止原因透传下去 (超时 / 权限·支付·工具失败 / 子阻塞 / 人工停)
703
+ // —— 只由 Run 事实推导的话回执里只剩笼统的 failure。这里给的就是从该 Run 事实推出来的那一个
704
+ // (`claimedTerminalKindForRun` 只在事实足够具体时给更细的类别, 且必须与该状态相容)。
705
+ terminalKind: (0, goal_flywheel_wiring_js_1.claimedTerminalKindForRun)(finalRun),
706
+ });
707
+ if ((0, goal_flywheel_wiring_js_1.isRefusal)(closureRes)) {
708
+ // 说不清是哪条 Run 的收尾 → 如实记, 不当收过 (也不编一份产物)
709
+ report.errors.push(`${goal.goalId}: 收尾被拒: ${closureRes.reason}`);
710
+ this.emit({ kind: 'run_closure_refused', goalId: goal.goalId, runId: finalRun.runId, message: closureRes.reason });
711
+ }
712
+ else if (!closureRes.outcome) {
713
+ report.errors.push(`${goal.goalId}: ${closureRes.reason}`);
714
+ this.emit({ kind: 'run_closure_facts_missing', goalId: goal.goalId, runId: finalRun.runId, message: closureRes.reason });
715
+ }
716
+ else {
717
+ const view = closureRes.outcome;
718
+ closureDecision = view.decision;
719
+ merged = (0, goal_flywheel_wiring_js_1.mergeGoalOutcome)({
720
+ legacy,
721
+ flywheel: { decision: view.decision, continuation: view.continuation },
722
+ run: finalRun,
723
+ pendingReports: goalForDecision.continuation?.pendingReports ?? [],
724
+ now: new Date(this.now()).toISOString(),
725
+ });
726
+ report.closures.push({
727
+ goalId: goal.goalId,
728
+ runId: finalRun.runId,
729
+ steps: view.steps,
730
+ decision: view.decision.decision,
731
+ memories: view.memories,
732
+ candidates: view.candidates,
733
+ // ★ 串行收口 ②(入口): 这次收尾**开出**的 Skill 试用逐条登记 (结构化面, 与
734
+ // `skillTrialOpenings` 同一份事实) —— 候选写出来了但没开试用, 这里就短一截, 一眼可见
735
+ trials: [...(view.trials ?? [])].map((t) => ({ ...t, goalId: goal.goalId, runId: finalRun.runId })),
736
+ reportPath: view.reportPath,
737
+ decisionRecordPath: view.decisionRecordPath,
738
+ });
739
+ // ★ 串行收口 ②(入口): 候选产出处开出的试用 —— 逐个登记 (候选写了但没开试用 = 数组短了, 一眼可见)
740
+ for (const t of view.trials ?? []) {
741
+ report.skillTrialOpenings.push({ ...t, goalId: goal.goalId, runId: finalRun.runId });
742
+ if (t.ok)
743
+ this.log(`[supervisor] goal=${goal.goalId} Skill 试用开出: ${t.skillName} (候选 ${t.candidateId})`);
744
+ else
745
+ this.log(`[supervisor] goal=${goal.goalId} Skill 试用未开 (卡在 ${t.refusal?.stage ?? '?'}): ${t.skillName} — ${t.refusal?.reason ?? ''}`);
746
+ }
747
+ this.emit({
748
+ kind: 'run_closure',
749
+ goalId: goal.goalId,
750
+ runId: finalRun.runId,
751
+ message: `收尾 ${view.steps} 步 → ${view.decision.decision}`
752
+ + `${closureRes.alreadyClosed ? ' [幂等: 这次收尾由 Runner 先做, 事实读回]' : ''}`
753
+ + ` (memory ${view.memories} · skill 候选 ${view.candidates} · 用户汇报 ${view.userReport.visibleState})`,
754
+ });
755
+ }
756
+ }
757
+ catch (err) {
758
+ // 收尾失败**不掩盖**: 如实记进 errors, 但仍按运输层决策收口 (不许因为收尾炸了就不写状态)
759
+ report.errors.push(`${goal.goalId}: Run 收尾失败: ${err?.message || err}`);
760
+ this.emit({ kind: 'run_closure_failed', goalId: goal.goalId, runId: finalRun.runId, message: String(err?.message || err) });
761
+ }
762
+ // ★ 串行收口 ②: **下一条 Run 的成功点**结算 Skill 试用 (通道 ⑥ 的真调用方)。
763
+ // 顺序在这里的理由: 试用的兑现证据 = "另一条 Run 成功 + 它的证据面点名了这个 Skill",
764
+ // 所以必须在这条 Run 的结局**已知**之后 (收尾写下的证据面正好是它的输入)。
765
+ // 不成功 → 回退 (rolled_back) 且**不提升**; 成功但证据面没点名 → 留在试用位 (无证据不算复用)。
766
+ // 只写记录 (校验+快照+可回退), 永不写 skills/ —— 见 settleSkillTrialsForRun 的纪律。
767
+ try {
768
+ const settled = await (0, goal_flywheel_wiring_js_1.settleSkillTrialsForRun)({
769
+ goalId: goal.goalId,
770
+ runId: finalRun.runId,
771
+ runStatus: String(finalRun.status ?? ''),
772
+ now: new Date(this.now()).toISOString(),
773
+ approvedBy: 'supervisor',
774
+ });
775
+ for (const s of settled) {
776
+ report.skillTrials.push({ ...s, runId: finalRun.runId });
777
+ if (s.promoted) {
778
+ this.log(`[supervisor] goal=${goal.goalId} Skill 试用提升: ${s.skillName} → ${s.toVersion} (候选 ${s.candidateId})`);
779
+ this.emit({ kind: 'skill_trial_promoted', goalId: goal.goalId, runId: finalRun.runId, message: s.reason });
780
+ }
781
+ else if (s.status !== 'trialing') {
782
+ this.log(`[supervisor] goal=${goal.goalId} Skill 试用回退 (不提升): ${s.skillName} — ${s.reason}`);
783
+ this.emit({ kind: 'skill_trial_rolled_back', goalId: goal.goalId, runId: finalRun.runId, message: s.reason });
784
+ }
785
+ }
786
+ }
787
+ catch (err) {
788
+ // 结算失败不掩盖 (也不影响 Run 的收口): 如实记进 errors
789
+ report.errors.push(`${goal.goalId}: Skill 试用结算失败: ${err?.message || err}`);
790
+ }
791
+ // P2: 飞轮说"这一步该派活" → **必发工作合同** (不是发一句话)
792
+ // 能力来源 (权威顺序): ① 这次收尾决策点名的能力 ② continuation 上记的 requiredAgent
793
+ // ③ **本轮跑之前**飞轮裁决=delegate 时点名的能力 (且本地**仍然**缺它) ——
794
+ // 收尾决策只拿得到 Run 的结果, 拿不到"目标声明的能力本地有没有"这条输入,
795
+ // 所以"缺能力 → 派活"必须能回落到 preflight 的结论; 否则它永远只在判定层成立、
796
+ // 在动作层消失 (P5 ★① 实测: 技能门禁放行之后, 合同还是一份都签不出来)。
797
+ const stillMissing = (await (0, goal_store_js_1.readGoal)(goal.goalId))?.continuation?.skillReadiness;
798
+ const stillMissingCap = !!flywheelCapability && stillMissing?.ok === false
799
+ && (stillMissing.missing ?? []).includes(flywheelCapability);
800
+ capabilityWanted = closureDecision?.requiredCapability
801
+ || merged.continuation.requiredAgent
802
+ || (stillMissingCap ? flywheelCapability : null);
803
+ // 变更注入的"版本已下发"记账 (新要求只影响后续 Run; 记下它被第几个 Run 读到)
804
+ await (0, goal_flywheel_wiring_js_1.markChangesConsumed)(goal.goalId, goalForDecision.runs.length).catch(() => null);
805
+ }
806
+ await this.applyDecision(goal, merged, finalRun);
807
+ // ★ 合同必须在**状态写完之后**签发 (P5 ★① 实测的顺序缺陷):
808
+ // `applyDecision` 是整份 continuation 的覆盖写 (`setContinuation(decision.continuation)`),
809
+ // 而 `dispatchChildWork` 会把"等这份回报"写进 `continuation.pendingReports` ——
810
+ // 先签发再写状态 ⇒ 刚登记的 pendingReports 立刻被覆盖成空, 父 Goal 上不留痕,
811
+ // 阻塞巡检再次没有输入 (与修复前一样"空转")。顺序: 定状态 → 签合同。
812
+ if (finalRun && capabilityWanted && merged.goalStatus !== 'completed' && !merged.flywheelStop) {
813
+ await this.dispatchIfDelegate(goal.goalId, finalRun, report, capabilityWanted).catch((err) => {
814
+ report.errors.push(`${goal.goalId}: 派遣合同失败: ${err?.message || err}`);
815
+ });
816
+ }
817
+ this.emit({ kind: 'goal_decision', goalId: goal.goalId, runId: finalRunId, message: `${finalRun?.status || result.status || '?'} → ${merged.goalStatus}: ${merged.reason}` });
818
+ this.log(`[supervisor] goal=${goal.goalId} run=${finalRunId || '-'} ${finalRun?.status || result.status || '?'} → goal=${merged.goalStatus} (${merged.reason})`
819
+ + `${merged.roundsOverridden ? ' [飞轮覆盖轮次上限]' : ''} ${Date.now() - t0}ms`);
481
820
  return { goalId: goal.goalId, runId: finalRunId, status: finalRun?.status || result.status, error: result.error };
482
821
  }
822
+ /**
823
+ * Run 收尾用的 Final Review 文本 (P1 的输入契约, 见 run-closure.ts 文件头):
824
+ * ① 宿主/验收可用 `BOLLOON_RUN_FINAL_REVIEW` 注入结构化评审 (评审人 + 事实 + 教训 + 候选);
825
+ * ② 否则用执行器这次给的回执文本 —— 散文**也能收尾** (评论非结构化只影响"是否产出教训/候选",
826
+ * 不会让收尾整条跳过)。
827
+ */
828
+ finalReviewText(run, result) {
829
+ const injected = process.env.BOLLOON_RUN_FINAL_REVIEW;
830
+ if (injected && injected.trim())
831
+ return injected;
832
+ const parts = [
833
+ result.reply,
834
+ run.summary,
835
+ run.error,
836
+ ].filter((s) => typeof s === 'string' && s.trim().length > 0);
837
+ return parts.join('\n');
838
+ }
839
+ /** 飞轮要求派遣 (decision=delegate) → 签发工作合同并把"等回报"写进 continuation */
840
+ async dispatchIfDelegate(goalId, run, report, capability = '') {
841
+ if (!capability)
842
+ return;
843
+ const goal = await (0, goal_store_js_1.readGoal)(goalId);
844
+ if (!goal)
845
+ return;
846
+ const nextAction = goal.continuation?.nextAction || `把需要「${capability}」的子目标派出去`;
847
+ const contract = await (0, goal_flywheel_wiring_js_1.dispatchChildWork)({
848
+ goalId,
849
+ parentRunId: run.runId,
850
+ // 合同只锁"能力", 具体派给谁由执行器解析 (这里如实写能力名, 不假装知道某个 agent 的 id)
851
+ childAgentId: capability,
852
+ capability,
853
+ objective: nextAction,
854
+ inputs: { goalObjective: goal.objective, successCriteria: goal.successCriteria },
855
+ allowedTools: [],
856
+ budget: { maxSteps: null, maxDurationMs: run.budget?.deadlineMs ?? null, maxAmount: null, currency: null },
857
+ deadline: null,
858
+ successCriteria: goal.successCriteria.length ? goal.successCriteria : [nextAction],
859
+ issuedBy: this.owner,
860
+ });
861
+ this.log(`[supervisor] goal=${goalId} 已签发工作合同 workId=${contract.workId} (能力「${capability}」, ${contract.successCriteria.length} 条判据, 必带证据 ${contract.requiredEvidence.length} 条)`);
862
+ this.emit({ kind: 'work_contract_issued', goalId, runId: run.runId, message: `workId=${contract.workId} 能力=${capability}` });
863
+ report.workContracts.push({ goalId, workId: contract.workId, capability });
864
+ }
483
865
  /** 把决策写进 Goal (+ 证据同步 + 完成出口), 并写下一次唤醒信息 */
484
866
  async applyDecision(goal, decision, run) {
485
- const { updateGoal } = await Promise.resolve().then(() => __importStar(require('./goal-store.js')));
486
867
  // 证据同步: Run 的成功步骤 → Goal 证据 (长期执行的判据要有据可依)
487
868
  if (run) {
488
869
  const ev = run.steps.filter((s) => s.ok).slice(-5).map((s) => `${run.runId}/${s.tool}: ${String(s.summary || '(完成)').slice(0, 120)}`);
489
870
  if (ev.length)
490
871
  await (0, goal_store_js_1.addEvidence)(goal.goalId, ev).catch(() => null);
491
872
  }
492
- // 唯一完成出口: 只有经 completeGoalIfEligible 才能把 Goal 判成 completed
493
- if (decision.goalStatus === 'completed') {
494
- // 完成门 (判据存在 + 已确认 + 全满足 + 有证据 + 无未解决项 + 最近 Run 健康)
495
- const r = await (0, goal_store_js_1.completeGoalIfEligible)(goal.goalId);
496
- if (!r.ok) {
497
- // 完成门拒绝 → 如实退回 active, 并保留原因 (不许装作完成)
498
- await (0, goal_store_js_1.setContinuation)(goal.goalId, { ...decision.continuation, wakeReason: 'active', autoContinue: true });
499
- await updateGoal(goal.goalId, { status: 'active' });
500
- this.log(`[supervisor] goal=${goal.goalId} 完成门拒绝: ${r.reason}`);
501
- return;
502
- }
503
- if (decision.continuation.wakeReason !== 'completed')
504
- await (0, goal_store_js_1.setContinuation)(goal.goalId, decision.continuation);
505
- this.emit({ kind: 'goal_completed', goalId: goal.goalId, runId: run?.runId, message: decision.reason });
873
+ // 规则 ②: 唯一落盘出口。完成门 (completed 必须过 completeGoalIfEligible) 也在 reducer 里,
874
+ // 门拒绝 → 退回 active 并保留原因 (不许装作完成)。
875
+ const applied = await (0, goal_state_reducer_js_1.reduceGoalState)({
876
+ goalId: goal.goalId,
877
+ intent: 'closure_outcome',
878
+ now: new Date(this.now()).toISOString(),
879
+ by: this.owner,
880
+ outcome: {
881
+ goalStatus: decision.goalStatus,
882
+ continuation: decision.continuation,
883
+ reason: decision.reason,
884
+ },
885
+ runId: run?.runId ?? null,
886
+ });
887
+ if (applied.gateRejected) {
888
+ this.log(`[supervisor] goal=${goal.goalId} 完成门拒绝: ${applied.gateRejected}`);
889
+ }
890
+ if (!applied.ok) {
891
+ this.log(`[supervisor] goal=${goal.goalId} 决策写不进 Goal (不掩盖): ${applied.reason}`);
892
+ return;
893
+ }
894
+ if (applied.status === 'completed') {
895
+ this.emit({ kind: 'goal_completed', goalId: goal.goalId, runId: run?.runId, message: applied.reason });
506
896
  return;
507
897
  }
508
- if (decision.goalStatus !== goal.status)
509
- await updateGoal(goal.goalId, { status: decision.goalStatus });
510
- await (0, goal_store_js_1.setContinuation)(goal.goalId, decision.continuation);
511
- if (decision.continuation.wakeReason === 'needs_human') {
898
+ if (decision.continuation.wakeReason === 'needs_human' || applied.status === 'needs_human') {
512
899
  this.emit({ kind: 'needs_human', goalId: goal.goalId, message: decision.reason });
513
900
  }
514
901
  }
515
- /** 外部事件到达 → 给对应 Goal 清除等待并加速唤醒 (2-E 第 4 类的入口) */
902
+ /**
903
+ * 外部事件到达 → 给对应 Goal 清除等待并加速唤醒 (2-E 第 4 类的入口)
904
+ * 手动唤醒 (CLI `/wake` / `POST /api/goals/:id/wake`): 人明确说"外部条件我已经满足了"。
905
+ *
906
+ * ★ 2026-09-25 (P5 验收修复): 原来只清等待事实、**不改 goal.status** —— `listRunnableGoals`
907
+ * 又按 `status === 'awaiting_external'` 直接跳过, 于是"唤醒"成了空操作:
908
+ * 接口回话 `woke:true`, 目标却仍然醒不过来 (真 tick executed=0)。
909
+ * 真事件路径 (`deliverExternalEvent`) 本来就把状态拉回 `active` —— 两条唤醒路径必须一致。
910
+ *
911
+ * 只拉**等待类**状态: 终态与人已判定的状态 (completed/failed/abandoned/paused/needs_human)
912
+ * 不动 —— 唤醒是"外部条件到了", 不是绕过人的决定。
913
+ */
516
914
  async notifyExternal(goalId) {
517
915
  const g = await (0, goal_store_js_1.readGoal)(goalId);
518
916
  if (!g)
519
917
  return false;
520
918
  if (g.continuation?.wakeReason !== 'awaiting_external' && !g.continuation?.needsExternal)
521
919
  return false;
522
- await (0, goal_store_js_1.setContinuation)(goalId, { wakeReason: 'active', needsExternal: undefined, autoContinue: true, wakeAt: undefined });
920
+ // 等待类的 continuation.state 也要拉回 `active` (与 goal.status 同步): 界面只读 continuation
921
+ // (`toUserVisibleState`), 留着 `awaiting_external` 的话页面仍显示"等待外部回复" ——
922
+ // 与"已唤醒、下一次 tick 会推进它"自相矛盾 (真 DOM 就是这么读到的)。
923
+ const wasWaiting = ['awaiting_external', 'retry_wait'].includes(String(g.continuation?.state));
924
+ await (0, goal_store_js_1.setContinuation)(goalId, {
925
+ wakeReason: 'active', needsExternal: undefined, autoContinue: true, wakeAt: undefined,
926
+ ...(wasWaiting ? { state: 'active' } : {}),
927
+ });
928
+ if (g.status === 'awaiting_external') {
929
+ // 规则 ②: 唤醒也是一种 Goal 状态变更 → 走 Goal reducer, 不在调用方手写 updateGoal
930
+ await (0, goal_state_reducer_js_1.reduceGoalState)({
931
+ goalId,
932
+ intent: 'manual_wake',
933
+ now: new Date().toISOString(),
934
+ by: 'human',
935
+ reason: '人工唤醒 (外部条件已满足)',
936
+ }).catch(() => null);
937
+ }
523
938
  await (0, goal_store_js_1.bumpContinuationAttempts)(goalId); // 记一次唤醒 (可观测)
524
939
  return true;
525
940
  }
941
+ /**
942
+ * ★ 2026-09-25 (串行收口 ④): **非 web 路径**提一条新要求/撤销 —— CLI 与宿主进程都走它。
943
+ *
944
+ * 为什么要挂在 Supervisor 上: 撤销 (用户说"别继续做了") 的语义是"停住在跑的 Run + 只影响后续 Run",
945
+ * 而这件事**只有一个正确做法** (M4 的 change 接缝: 判定 + 事实注入 + 停止原语)。以前只有
946
+ * `/api/goals/:id/requirement` 那条 HTTP 路由会走到它 —— 于是 CLI / 宿主直接调 `ingestGoalChange`
947
+ * 时, 在跑的 Run **停不下来** (接缝拿到的是 `factsMissing=true`, 只能如实说"不知道")。
948
+ *
949
+ * 这里就是那个入口: 读事实 (`runningRun` / 在跑的子 Agent) → 接缝判定 → 真停 Run (`setRunStatus`)。
950
+ * 返回值原样透出接缝视图 (含 `runBoundary.stopped` 与逐 workId 的下发内容), 拒绝也原样透出。
951
+ */
952
+ async ingestRequirement(goalId, instruction, by = 'human') {
953
+ const now = new Date(this.now()).toISOString();
954
+ const view = await (0, goal_flywheel_wiring_js_1.ingestRequirementViaSeam)({
955
+ goalId,
956
+ instruction,
957
+ source: 'user',
958
+ recordedBy: `cli:${by}`,
959
+ caller: 'human', // 人提的变更: 只有人能提撤销/缩范围
960
+ now,
961
+ });
962
+ if (view === null)
963
+ return { ok: false, reason: `变更无法入档 (goal=${goalId})` };
964
+ if ((0, goal_flywheel_wiring_js_1.isRefusal)(view))
965
+ return { ok: false, reason: view.reason };
966
+ const b = view.runBoundary;
967
+ const note = b.action === 'stop_running_run'
968
+ ? (b.stopped
969
+ ? `在跑的 Run ${b.runId} 已写 ${b.runStatus} (执行器下一轮自行停下)`
970
+ : `判定要停 Run ${b.runId}, 但停止动作没落到记录上: ${b.reason}`)
971
+ : b.reason;
972
+ this.emit({ kind: 'requirement_ingested', goalId, message: `${view.request.kind} (${view.plan.outcome}) — ${note}` });
973
+ this.log(`[supervisor] goal=${goalId} 变更入档: ${view.request.kind} → ${view.plan.outcome}; ${note}`);
974
+ return {
975
+ ok: true,
976
+ kind: view.request.kind,
977
+ outcome: view.plan.outcome,
978
+ stopped: b.stopped,
979
+ runId: b.runId,
980
+ visibleState: view.visibleState,
981
+ note,
982
+ };
983
+ }
526
984
  }
527
985
  exports.ExecutionSupervisor = ExecutionSupervisor;
528
986
  /** 单例 (Web/CLI 共享同一个进程内调度器; 跨进程靠 lease 排他) */