@bolloon/bolloon-agent 0.4.32 → 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 (157) hide show
  1. package/dist/agents/chain/explorer.js +52 -0
  2. package/dist/agents/chain/index.js +2 -0
  3. package/dist/agents/chain/onchain-trade.js +15 -1
  4. package/dist/agents/contacts/chain.js +27 -7
  5. package/dist/agents/execution-supervisor.js +493 -35
  6. package/dist/agents/external-events.js +35 -6
  7. package/dist/agents/gateway-group.js +100 -12
  8. package/dist/agents/goal-criteria.js +10 -3
  9. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  10. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  11. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  12. package/dist/agents/goal-flywheel/index.js +26 -0
  13. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  14. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  15. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  16. package/dist/agents/goal-flywheel/types.js +329 -0
  17. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  18. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  19. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  20. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  21. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  22. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  23. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  24. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  25. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  26. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  27. package/dist/agents/goal-state-reducer.js +406 -0
  28. package/dist/agents/goal-store.js +35 -1
  29. package/dist/agents/mobile-flywheel-view.js +144 -0
  30. package/dist/agents/mobile-task-actions-verify.js +74 -0
  31. package/dist/agents/mobile-task-actions.js +178 -0
  32. package/dist/agents/mobile-task-views.js +199 -0
  33. package/dist/agents/network-pulse.js +525 -27
  34. package/dist/agents/pi-sdk.js +75 -20
  35. package/dist/agents/run-store.js +18 -5
  36. package/dist/agents/skill-readiness.js +52 -10
  37. package/dist/agents/subagent-manager.js +209 -3
  38. package/dist/agents/task/task-onchain-runner.js +19 -1
  39. package/dist/agents/task/task-runner.js +36 -6
  40. package/dist/agents/task-board.js +702 -0
  41. package/dist/agents/task-group.js +524 -0
  42. package/dist/agents/task-public-text.js +89 -0
  43. package/dist/cli/commands/index.js +3 -0
  44. package/dist/cli/commands/tasks.js +977 -8
  45. package/dist/cli/identity-command.js +177 -0
  46. package/dist/cli/protocol-envelope.js +4 -0
  47. package/dist/cli/setup-wizard.js +71 -0
  48. package/dist/cli/update-commands.js +150 -24
  49. package/dist/cli-entry.js +12 -2
  50. package/dist/electron-build/agents/chain/explorer.js +59 -0
  51. package/dist/electron-build/agents/chain/explorer.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/chain.js +26 -6
  53. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  54. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  55. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  56. package/dist/electron-build/agents/external-events.js +34 -5
  57. package/dist/electron-build/agents/external-events.js.map +1 -1
  58. package/dist/electron-build/agents/goal-criteria.js +10 -3
  59. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  60. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  61. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  63. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  65. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  67. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  69. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  71. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  86. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  87. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  88. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  89. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  90. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  91. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  92. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  93. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  94. package/dist/electron-build/agents/goal-store.js +35 -1
  95. package/dist/electron-build/agents/goal-store.js.map +1 -1
  96. package/dist/electron-build/agents/local-signer.js +101 -0
  97. package/dist/electron-build/agents/local-signer.js.map +1 -0
  98. package/dist/electron-build/agents/network-pulse.js +531 -28
  99. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  100. package/dist/electron-build/agents/pi-sdk.js +74 -19
  101. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  102. package/dist/electron-build/agents/run-store.js +20 -5
  103. package/dist/electron-build/agents/run-store.js.map +1 -1
  104. package/dist/electron-build/agents/skill-readiness.js +52 -9
  105. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  106. package/dist/electron-build/agents/task/task-runner.js +35 -5
  107. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  108. package/dist/electron-build/agents/task-board.js +761 -0
  109. package/dist/electron-build/agents/task-board.js.map +1 -0
  110. package/dist/electron-build/agents/task-contract.js +382 -0
  111. package/dist/electron-build/agents/task-contract.js.map +1 -0
  112. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  113. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  114. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  115. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  116. package/dist/electron-build/utils/dual-source.js +471 -0
  117. package/dist/electron-build/utils/dual-source.js.map +1 -0
  118. package/dist/electron-build/utils/update-manager.js +331 -57
  119. package/dist/electron-build/utils/update-manager.js.map +1 -1
  120. package/dist/electron-build/utils/update-state.js +16 -1
  121. package/dist/electron-build/utils/update-state.js.map +1 -1
  122. package/dist/electron-build/utils/version-info.js +82 -2
  123. package/dist/electron-build/utils/version-info.js.map +1 -1
  124. package/dist/index.js +103 -12
  125. package/dist/ios/chain-explorer.js +617 -0
  126. package/dist/ios/explorer.html +163 -0
  127. package/dist/ios/index.html +96 -0
  128. package/dist/ios/mobile-agent.js +2 -2
  129. package/dist/ios/mobile-contacts.js +329 -0
  130. package/dist/ios/mobile-core.js +1379 -226
  131. package/dist/ios/mobile-privacy.js +185 -0
  132. package/dist/ios/mobile.css +19 -0
  133. package/dist/ios/mobile.html +96 -0
  134. package/dist/ios/mobile.js +706 -7
  135. package/dist/ios/routes-chain-index.js +108 -0
  136. package/dist/ios/routes-contacts.js +420 -0
  137. package/dist/ios/routes-onchain-trade.js +99 -0
  138. package/dist/ios/routes-x402-info.js +1 -0
  139. package/dist/ios/server.js +762 -0
  140. package/dist/ios/task-frame-server.js +211 -0
  141. package/dist/orbitdb/cid-database.js +81 -16
  142. package/dist/orbitdb/ipfs-node.js +31 -2
  143. package/dist/utils/dual-source.js +426 -0
  144. package/dist/utils/update-cli.js +16 -21
  145. package/dist/utils/update-health.js +35 -5
  146. package/dist/utils/update-manager.js +329 -57
  147. package/dist/utils/update-state.js +16 -1
  148. package/dist/utils/version-info.js +75 -1
  149. package/dist/web/mobile-contacts.js +44 -11
  150. package/dist/web/mobile-core.js +1197 -513
  151. package/dist/web/mobile-tasks.js +339 -0
  152. package/dist/web/mobile.css +19 -0
  153. package/dist/web/mobile.html +64 -0
  154. package/dist/web/mobile.js +531 -1
  155. package/dist/web/routes-mobile-tasks.js +353 -0
  156. package/dist/web/server.js +163 -4
  157. package/package.json +3 -1
@@ -0,0 +1,432 @@
1
+ /**
2
+ * wiring/continuation.ts — 接缝 ①「这个 Goal 现在该不该开下一轮, 为什么?」 (**M1 独占**)
3
+ *
4
+ * 归属: `seams.ts` 的 `SEAM_ROSTER` 里 `id: 'continuation'`, `stage: 'M1'`。
5
+ * M1 只准改本文件 + 本接缝声明的 `wiringPoints` (`goal-flywheel/continuation-decision.ts`)。
6
+ *
7
+ * ## 这条接缝钉住的冻结规则
8
+ *
9
+ * **只有 Supervisor 能决定是否继续** (规则 ①)。
10
+ * 钉法不是文档, 是**调用方必须显式声明身份**: 接缝的入口 `preflight()` 要一个
11
+ * `caller` 字段, 非 `supervisor` 一律拒绝。于是"Runner 自己觉得还能再跑一轮"这种
12
+ * 绕过在类型上就写不出来, 在运行时会拿到一条结构化拒绝。
13
+ *
14
+ * ## M1 的实质: 自适应节奏 —— 固定轮次降级为**安全上限**
15
+ *
16
+ * 接缝不再是把注入的结论原样转出去, 它自己把 P0 编起来 (P0 = 本阶段自己的实现模块
17
+ * `../continuation-decision.js`, 见 `seams.ts` 头部的接缝形态第 ③ 条):
18
+ *
19
+ * ```
20
+ * 事实 (goal + run + progress + noProgressStreak)
21
+ * → decideContinuation (节奏: 有没有**新的可核验证据**)
22
+ * → applyHardLimits (三类上限: 只能收紧, 永不放开)
23
+ * → isRunnable (最后一道路闸)
24
+ * → 归因: 是「进展」在说话, 还是「哪一条上限」在说话
25
+ * ```
26
+ *
27
+ * 三类安全上限 (设计稿 §4, 字面登记在 `SAFETY_CAPS`): **单 Run 时间 · 单 Goal 预算 ·
28
+ * 无进展熔断阈值**。它们只回答"什么时候必须停", **不回答"什么时候继续"**。
29
+ * 每一条都交给 P0 去执行 (接缝不自己数任何计数): 时间与熔断阈值 P0 直接吃 `hardLimits`;
30
+ * 「单 Goal 预算」P0 走的是 Goal 自己的预算口径, 所以这里把降级来的上限**投影**成那个口径
31
+ * (`factsWithBudgetCap`), 而不是在同一份事实上再判一遍。
32
+ * 旧配置里的固定轮次 (`maxRounds` / `maxRetries`) 在 `safetyCapsFrom()` 里被**登记成**
33
+ * 这两条上限 (总 Run 数 / 熔断阈值) —— 于是"第几轮"再也不能自己决定停或不停:
34
+ * 只有上限真的触顶它才说话, 有进展就继续。P0 本身根本不读轮次, 本文件也不读。
35
+ *
36
+ * ## 归因怎么做: 反事实, 不抄一遍逻辑
37
+ *
38
+ * "哪条上限在说话"不是本文件用几个 `if` 重新推一遍 (那就成了两套事实), 而是**拿 P0 自己当判据**:
39
+ * 把某条上限放宽后**结论变了**, 说明是它在决定 (`bindingCaps`)。放宽了结论也不变 = 这次停跟它无关
40
+ * (是"没有继续的资格"/终态/等人), 于是不会被错记成"被上限拦住"。
41
+ * 上限**没定义** (例如 `maxGoalBudget === null`) 也算它在说话 —— `applyHardLimits` 会收紧成交人,
42
+ * 而放宽后那条收紧理由会消失, 反事实照样抓得到。
43
+ *
44
+ * ## 事实从哪来 (接缝自己不读盘)
45
+ *
46
+ * · `deps.readFacts` (规范口径): **只读事实, 不判定** —— 判定由本接缝用 P0 做, 全量决策随结果返回;
47
+ * · `deps.decide` (旧口径): 接线层注入的整条判定结论。事实拿不到时的兜底, 只能核验
48
+ * **无进展熔断**这一条上限 (其余上限没有事实就没法核验 → 如实标 `unattributableStop`, 不假装验过;
49
+ * 自称的上限若与阈值对不上 → **真拒**, 见 `judgeInjectedOutcome`)。
50
+ *
51
+ * 本文件**不读盘、不读钟**: `now` 由调用方注入。
52
+ */
53
+ import { applyHardLimits, decideContinuation, isRunnable } from '../continuation-decision.js';
54
+ import { refuse, stripBlockComments } from './seams.js';
55
+ export const CONTINUATION_SEAM_ID = 'continuation';
56
+ // ============================================================================
57
+ // §1. 三类安全上限: 固定轮次在这里被降级
58
+ // ============================================================================
59
+ /** 三类安全上限的机器名 (设计稿 §4 的"保留三类硬底线") */
60
+ export const SAFETY_CAPS = ['max_run_duration', 'goal_budget', 'no_progress_breaker'];
61
+ export const SAFETY_CAP_TEXT = {
62
+ max_run_duration: '单 Run 时间上限',
63
+ goal_budget: '单 Goal 预算上限 (总 Run 数)',
64
+ no_progress_breaker: '无进展熔断阈值',
65
+ };
66
+ /** 接缝自己的上限默认值 (与接线层 M0 同一口径; 接缝不 import 接线层, 所以在这里登记一份) */
67
+ export const SEAM_CAP_DEFAULTS = {
68
+ maxRunDurationMs: 30 * 60_000,
69
+ maxGoalBudget: 50,
70
+ noProgressCircuitBreaker: 3,
71
+ };
72
+ const isCount = (n) => typeof n === 'number' && Number.isInteger(n) && n >= 1;
73
+ const isZeroOrCount = (n) => typeof n === 'number' && Number.isInteger(n) && n >= 0;
74
+ const isPosFinite = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0;
75
+ /**
76
+ * 旧配置 → 三类安全上限。**这是"固定轮次降级"的唯一实现处**。
77
+ *
78
+ * 降级不是"少看一眼", 而是**换一件事管**: 轮次从"继续/停止的依据"变成"总 Run 数的上限"。
79
+ * 于是旧配置的语义被保住 (跑不到第 N 轮以上), 但节奏改由进展决定。
80
+ */
81
+ export function safetyCapsFrom(config = {}) {
82
+ let maxRunDurationMs = SEAM_CAP_DEFAULTS.maxRunDurationMs;
83
+ let maxGoalBudget = SEAM_CAP_DEFAULTS.maxGoalBudget;
84
+ let noProgressCircuitBreaker = SEAM_CAP_DEFAULTS.noProgressCircuitBreaker;
85
+ // 旧的固定轮次 → 单 Goal 预算上限 (Run 数上限)
86
+ if (isCount(config.maxRounds))
87
+ maxGoalBudget = config.maxRounds;
88
+ // 运输层的 maxRetries → 无进展熔断阈值 (与 M0 `hardLimitsFor` 同口径: 阈值 = maxRetries + 1)
89
+ if (isZeroOrCount(config.maxRetries))
90
+ noProgressCircuitBreaker = Math.max(1, config.maxRetries + 1);
91
+ if (isPosFinite(config.maxRunDurationMs))
92
+ maxRunDurationMs = config.maxRunDurationMs;
93
+ const explicit = config.limits;
94
+ if (explicit) {
95
+ // 只有 `undefined` 算"没说"; `null` 是显式语义 (未设上限 → applyHardLimits 会收紧成交人), 不许被默认值盖掉
96
+ if (explicit.maxRunDurationMs !== undefined)
97
+ maxRunDurationMs = explicit.maxRunDurationMs;
98
+ if (explicit.maxGoalBudget !== undefined)
99
+ maxGoalBudget = explicit.maxGoalBudget;
100
+ if (explicit.noProgressCircuitBreaker !== undefined)
101
+ noProgressCircuitBreaker = explicit.noProgressCircuitBreaker;
102
+ }
103
+ return { maxRunDurationMs, maxGoalBudget, noProgressCircuitBreaker };
104
+ }
105
+ /** 放宽后的取值: 大到事实上不可能触顶, 但仍是合法上限 (P0 对上限合法性有判据: 正有限数 / >=1 的整数) */
106
+ const RELAXED_LIMIT = Number.MAX_SAFE_INTEGER;
107
+ /** 上限的**事实层来源**: Goal 自带的 `budget` (maxRuns / deadlineMs) 也是「单 Goal 预算上限」 */
108
+ function relaxFactSource(facts, cap) {
109
+ return cap === 'goal_budget' ? { ...facts, goal: { ...facts.goal, budget: undefined } } : facts;
110
+ }
111
+ /** 放宽这一条上限 (其余保持不变) */
112
+ function capsRelaxingOne(caps, cap) {
113
+ const out = { ...caps };
114
+ if (cap === 'max_run_duration')
115
+ out.maxRunDurationMs = RELAXED_LIMIT;
116
+ else if (cap === 'goal_budget')
117
+ out.maxGoalBudget = RELAXED_LIMIT;
118
+ else
119
+ out.noProgressCircuitBreaker = RELAXED_LIMIT;
120
+ return out;
121
+ }
122
+ /**
123
+ * 把「单 Goal 预算上限」用 **P0 听得懂的口径**表达出来, 而不是在接缝里自己数 Run。
124
+ *
125
+ * 为什么不在接缝里数: 轮次数只有 P0 的预算法则会判 (它读 Goal 自带的 `budget.maxRuns` + Run 列表),
126
+ * 接缝再数一遍就是**第二套事实** —— 而且那正是"用轮次当节奏"的老毛病 (本文件的源码判据也不允许)。
127
+ * 所以这里只做一件事: 把降级来的上限投影成 Goal 的预算口径, 判定仍然只有 P0 那一个出口。
128
+ *
129
+ * 优先级 (与 M0 `hardLimitsFor` 同口径: "Goal 自己声明了预算就用它, 否则退回默认上限"):
130
+ * · Goal **声明**了 `budget.maxRuns` → 用它 (人的意图优先于旧配置; 要让上限更严就改 Goal 的预算,
131
+ * 那是人的取舍, 见 P4 规则 2 "扩/改预算不许 Agent 自动批");
132
+ * · Goal 没声明 → 用上限里的 `maxGoalBudget` (旧 maxRounds 降级来的, 或默认 50) —— 它**真的能停**;
133
+ * · `maxGoalBudget === null` (未设上限) → **不投影**: 那种情况由 `applyHardLimits` 收紧成交人。
134
+ */
135
+ function factsWithBudgetCap(facts, caps) {
136
+ const declared = facts.goal.budget?.maxRuns;
137
+ const cap = caps.maxGoalBudget;
138
+ const usable = typeof cap === 'number' && Number.isFinite(cap) && cap > 0 ? cap : undefined;
139
+ const effective = typeof declared === 'number' ? declared : usable;
140
+ if (effective === undefined || effective === declared)
141
+ return facts; // 事实已经表达了这条上限
142
+ return { ...facts, goal: { ...facts.goal, budget: { ...(facts.goal.budget ?? {}), maxRuns: effective } } };
143
+ }
144
+ /** 本接缝的节奏管线: **唯一的决策出口** (纯函数, 同一批事实重放得到同一条决策) */
145
+ export function rhythmicDecision(facts, caps) {
146
+ return applyHardLimits(decideContinuation({ ...factsWithBudgetCap(facts, caps), hardLimits: caps }), caps);
147
+ }
148
+ /**
149
+ * 决策的**结论投影**: 除 `reason` 之外的一切。
150
+ *
151
+ * 为什么把 `reason` 排除掉: P0 的理由文本里嵌着上限的**真值** (熔断阈值 / 毫秒数 / Run 数),
152
+ * 拿它做比对会把"数字变了"当成"结论变了" —— 于是没触顶的上限也被记成在说话 (实测踩过)。
153
+ * 结论投影只看真正决定行为的字段: decision / state / nextAction / wakeAt / requiredCapability /
154
+ * riskLevel / stopReason / confidence … 上限真的在说话时, 这些里至少有一个会变。
155
+ */
156
+ function verdictOf(d) {
157
+ return JSON.stringify({ ...d, reason: null });
158
+ }
159
+ /**
160
+ * 哪几条安全上限**真的在决定结论** (触顶 + 起决定作用; 触顶但被别条覆盖的不算)。
161
+ *
162
+ * 判据是**反事实**, 不是本文件手算一遍 (手算就是第二套事实):
163
+ * 把这一条上限 (连它的事实层来源) 放宽 → **结论投影变了** ⟹ 是它在说话;
164
+ * 结论没变 ⟹ 这次停跟它无关 (可能压根没触顶, 也可能被更靠前的规则先拦下了)。
165
+ *
166
+ * 于是"上限没定义"也被算成它在说话 (`maxGoalBudget: null` → `applyHardLimits` 收紧成交人;
167
+ * 放宽后那条收紧理由消失, 结论就变了), 而"没有继续的资格"(⑪) 永远不会被错记成"被上限拦住"。
168
+ * 顺序 = P0 的判定顺序 (单 Run 时间 → 单 Goal 预算 → 无进展熔断)。
169
+ */
170
+ export function bindingCaps(facts, caps) {
171
+ const base = verdictOf(rhythmicDecision(facts, caps));
172
+ const out = [];
173
+ for (const cap of SAFETY_CAPS) {
174
+ if (verdictOf(rhythmicDecision(relaxFactSource(facts, cap), capsRelaxingOne(caps, cap))) !== base)
175
+ out.push(cap);
176
+ }
177
+ return out;
178
+ }
179
+ export const RHYTHM_BASIS_TEXT = {
180
+ progress: '有可核验进展',
181
+ delegate: '缺能力该派活',
182
+ wait: '等外部 / 等事件',
183
+ cap: '安全上限触顶',
184
+ human: '没有继续的资格 (交人定夺)',
185
+ terminal: '终态 (完成 / 不可达)',
186
+ };
187
+ function basisOf(d, caps) {
188
+ if (d.decision === 'complete' || d.decision === 'fail')
189
+ return 'terminal';
190
+ if (d.decision === 'wait' || d.decision === 'pause')
191
+ return 'wait';
192
+ if (caps.length > 0)
193
+ return 'cap';
194
+ if (d.decision === 'continue')
195
+ return 'progress';
196
+ if (d.decision === 'delegate')
197
+ return 'delegate';
198
+ return 'human';
199
+ }
200
+ /**
201
+ * 这个 Goal 现在该不该开下一轮 —— **纯函数版** (接缝的 `preflight` 与宿主都可直接用)。
202
+ * 决策只来自 P0; 上限只来自 `safetyCapsFrom` 的降级结果。
203
+ */
204
+ export function planRhythm(facts, config = {}) {
205
+ const capsInEffect = safetyCapsFrom(config);
206
+ const decision = rhythmicDecision(facts, capsInEffect);
207
+ const gate = isRunnable(decision, facts.now);
208
+ const caps = bindingCaps(facts, capsInEffect);
209
+ return {
210
+ goalId: facts.goal.goalId,
211
+ decision,
212
+ runnable: gate.runnable,
213
+ decisionReason: decision.reason,
214
+ runnableReason: gate.reason,
215
+ basis: basisOf(decision, caps),
216
+ caps,
217
+ bindingCap: caps[0] ?? null,
218
+ capsInEffect,
219
+ noProgressStreak: facts.noProgressStreak,
220
+ };
221
+ }
222
+ /**
223
+ * 一个**自称停**的旧结论 (例如运输层的"已到最大轮次 → 交人") 该不该被采纳。
224
+ *
225
+ * 规则 (与设计稿 §4 逐条对应):
226
+ * · 上限触顶 / 终态 / 等外部 → 停得住 (上限是安全线, 该停就停);
227
+ * · 有可核验进展且三类上限一条都没触顶 → **推翻旧结论, 继续** (轮次不再是停止依据);
228
+ * · 其余 (没有继续的资格, basis='human') → 照旧停, 理由是"没证据", 不是"第几轮"。
229
+ */
230
+ export function judgeLegacyStop(claim, plan) {
231
+ if (claim.runnable)
232
+ return { stop: false, overridden: false, reason: claim.reason };
233
+ if (plan.basis === 'terminal' || plan.basis === 'wait') {
234
+ return { stop: true, overridden: false, reason: `停得住 (${RHYTHM_BASIS_TEXT[plan.basis]}): ${claim.reason}` };
235
+ }
236
+ if (plan.caps.length > 0) {
237
+ return {
238
+ stop: true,
239
+ overridden: false,
240
+ reason: `停得住 (安全上限触顶: ${plan.caps.map((c) => SAFETY_CAP_TEXT[c]).join(' + ')}): ${claim.reason}`,
241
+ };
242
+ }
243
+ if (plan.basis === 'progress' && plan.runnable) {
244
+ return {
245
+ stop: false,
246
+ overridden: true,
247
+ reason: `旧结论「${claim.reason}」被推翻: 本轮有可核验进展, 且三类安全上限一条都没触顶 `
248
+ + '→ 继续 (轮次/时长不是停止依据, 只有上限能停)',
249
+ };
250
+ }
251
+ return {
252
+ stop: true,
253
+ overridden: false,
254
+ reason: `停得住 (${RHYTHM_BASIS_TEXT[plan.basis]}): ${claim.reason}`,
255
+ };
256
+ }
257
+ /**
258
+ * 注入的结论 → 可核验的节奏归因。
259
+ *
260
+ * 投影里能核验的只有**无进展熔断** (它只需要 `noProgressStreak`)。
261
+ * 自称别的上限 (时间/预算) 缺事实 → 不认, 但如实标注 (不假装验过);
262
+ * 自称熔断却数与阈值对不上 → **真拒** (用轮次冒充安全上限)。
263
+ */
264
+ export function judgeInjectedOutcome(out, caps) {
265
+ const claimed = (out.caps ?? []).filter((c) => SAFETY_CAPS.includes(c));
266
+ const stop = out.runnable !== true;
267
+ const state = out.decision?.state ?? null;
268
+ const kind = out.decision?.decision ?? null;
269
+ const claimsBreaker = claimed.includes('no_progress_breaker');
270
+ const breakerTripped = out.noProgressStreak >= caps.noProgressCircuitBreaker;
271
+ if (claimsBreaker && !breakerTripped) {
272
+ return refuse('only_supervisor_decides_continuation', `拒绝: 注入的结论自称「${SAFETY_CAP_TEXT.no_progress_breaker}」触顶, 但 noProgressStreak=`
273
+ + `${out.noProgressStreak} < 阈值 ${caps.noProgressCircuitBreaker} —— 用轮次冒充安全上限: `
274
+ + '只有上限真的触顶才能停 (这不算安全线, 是拿"第几轮"当节奏)');
275
+ }
276
+ // 不管自称不自称: 阈值真的到了, 这条上限就是从事实核验得到的
277
+ const verifiedBreaker = stop && (state === 'no_progress') && breakerTripped;
278
+ const unverifiable = claimed.filter((c) => c !== 'no_progress_breaker');
279
+ let basis;
280
+ if (!stop) {
281
+ basis = kind === 'delegate' ? 'delegate' : kind === 'continue' ? 'progress' : 'wait';
282
+ }
283
+ else if (kind === 'complete' || kind === 'fail') {
284
+ basis = 'terminal';
285
+ }
286
+ else if (kind === 'wait' || kind === 'pause') {
287
+ basis = 'wait';
288
+ }
289
+ else if (verifiedBreaker) {
290
+ basis = 'cap';
291
+ }
292
+ else {
293
+ basis = 'human';
294
+ }
295
+ const caps_ = verifiedBreaker ? ['no_progress_breaker'] : [];
296
+ const unattributableStop = stop
297
+ && basis !== 'cap' && basis !== 'terminal' && basis !== 'wait';
298
+ const note = unattributableStop
299
+ ? (unverifiable.length
300
+ ? ` (自称的上限 ${unverifiable.map((c) => SAFETY_CAP_TEXT[c]).join(' + ')} 缺事实, 不可核验 → 不认)`
301
+ : ' (缺事实: 时间/预算两类上限无法核验, 只核验了无进展熔断)')
302
+ : '';
303
+ return {
304
+ ...out,
305
+ source: 'injected',
306
+ basis,
307
+ caps: caps_,
308
+ bindingCap: caps_[0] ?? null,
309
+ capsInEffect: caps,
310
+ fullDecision: null,
311
+ facts: null,
312
+ unattributableStop,
313
+ reason: `${out.reason}${note}`,
314
+ };
315
+ }
316
+ export function createContinuationSeam(deps) {
317
+ return {
318
+ id: CONTINUATION_SEAM_ID,
319
+ stage: 'M1',
320
+ async preflight(input) {
321
+ if (input.caller !== 'supervisor') {
322
+ return refuse('only_supervisor_decides_continuation', `拒绝: caller=${input.caller} 想决定"是否继续 (goal=${input.goalId})" —— `
323
+ + '只有 Supervisor 能决定是否继续 (Runner/子 Agent 只能上报事实与建议)');
324
+ }
325
+ if (!input.goalId) {
326
+ return refuse('only_supervisor_decides_continuation', '拒绝: 没有 goalId 的节奏判定等于给不存在的目标排队');
327
+ }
328
+ const config = {
329
+ maxRounds: input.maxRounds,
330
+ maxRetries: input.maxRetries,
331
+ maxRunDurationMs: input.maxRunDurationMs,
332
+ limits: input.limits,
333
+ };
334
+ const capsInEffect = safetyCapsFrom(config);
335
+ // 规范路径: 事实 → P0 (判定由本接缝做, 全量决策随结果返回)
336
+ if (deps.readFacts) {
337
+ const facts = await deps.readFacts({ goalId: input.goalId, now: input.now });
338
+ if (facts) {
339
+ const plan = planRhythm(facts, config);
340
+ const capNote = plan.caps.length > 0
341
+ ? `, 上限: ${plan.caps.map((c) => SAFETY_CAP_TEXT[c]).join(' + ')}`
342
+ : '';
343
+ return {
344
+ goalId: plan.goalId,
345
+ runnable: plan.runnable,
346
+ reason: `[${RHYTHM_BASIS_TEXT[plan.basis]}${capNote}] ${plan.decisionReason} | 放行: ${plan.runnableReason}`,
347
+ noProgressStreak: plan.noProgressStreak,
348
+ decision: {
349
+ decision: plan.decision.decision,
350
+ state: plan.decision.state,
351
+ nextAction: plan.decision.nextAction,
352
+ requiredCapability: plan.decision.requiredCapability,
353
+ decisionId: plan.decision.decisionId,
354
+ },
355
+ caps: plan.caps,
356
+ stopReason: plan.decision.stopReason,
357
+ progress: plan.decision.progressDelta,
358
+ source: 'facts',
359
+ basis: plan.basis,
360
+ bindingCap: plan.bindingCap,
361
+ capsInEffect,
362
+ fullDecision: plan.decision,
363
+ facts,
364
+ unattributableStop: false,
365
+ };
366
+ }
367
+ // 事实读不到 → 不猜 (下面还有没兜底, 没有就如实说)
368
+ if (!deps.decide)
369
+ return null;
370
+ }
371
+ if (!deps.decide) {
372
+ return refuse('only_supervisor_decides_continuation', '拒绝: 接缝既没有 readFacts 也没有 decide —— 没有事实就没有节奏判定, 不许编一条决策出来');
373
+ }
374
+ const out = await deps.decide({
375
+ goalId: input.goalId,
376
+ now: input.now,
377
+ maxRetries: input.maxRetries,
378
+ writeRecord: input.writeRecord,
379
+ });
380
+ if (!out)
381
+ return null;
382
+ return judgeInjectedOutcome(out, capsInEffect);
383
+ },
384
+ };
385
+ }
386
+ // ============================================================================
387
+ // §5. 源码级判据 (供门与变异验证: 本接缝的"自适应节奏"接线是不是还在)
388
+ // ============================================================================
389
+ /**
390
+ * 必须有: P0 的三个函数 + 上限登记函数 (少一个 = 节奏或上限被摘掉)。
391
+ *
392
+ * 为什么用 `fn.name` 而不是把函数名抄成字面量: 抄成字面量的那一行**自己就含**着这个
393
+ * 名字 —— 于是这道判据会被自己满足 (门空转)。取运行期函数名, 判据只可能在**真的调用**处命中。
394
+ */
395
+ export const SEAM_RHYTHM_REQUIRED = [
396
+ `${decideContinuation.name}(`,
397
+ `${applyHardLimits.name}(`,
398
+ `${isRunnable.name}(`,
399
+ `${safetyCapsFrom.name}(`,
400
+ ];
401
+ /**
402
+ * 不许有: 拿轮次/轮数自己判停 (轮次只准经 `safetyCapsFrom` 变成安全上限)。
403
+ *
404
+ * 词根 + 后缀拼出来, 同样是为了**不自证违规**: 把 `runsUsed` 直接写成本判据里的一行,
405
+ * 那一行就会被自己的模式命中 → 干净树永远判红, 门就废了。
406
+ */
407
+ const RHYTHM_FORBIDDEN_ROOTS = ['runs', 'round', 'attempt'];
408
+ export const SEAM_ROUNDS_AS_RHYTHM = [
409
+ /\bgoal\s*\.\s*runs\b/,
410
+ ...RHYTHM_FORBIDDEN_ROOTS.map((root) => new RegExp(`\\b${root}(?:No|Used|Count)?\\s*>=`)),
411
+ ];
412
+ /**
413
+ * 纯函数判据: 吃源码文本, 吐违规清单。
414
+ * 与 `seams.ts` 同一手法 —— 于是"改坏主不变量 → 必须判红"可以在测试里**每次跑都在验**,
415
+ * 而不是我口头说验过 (见 `src/test/goal-flywheel-wiring-continuation.test.ts` 的变异段)。
416
+ */
417
+ export function scanContinuationSeamRhythm(text) {
418
+ const code = stripBlockComments(String(text)).split(/\r?\n/).map((s) => {
419
+ const i = s.indexOf('//');
420
+ return i >= 0 ? s.slice(0, i) : s;
421
+ }).join('\n');
422
+ const violations = [];
423
+ for (const needle of SEAM_RHYTHM_REQUIRED) {
424
+ if (!code.includes(needle))
425
+ violations.push(`缺少 ${needle} —— 节奏/上限的接线被摘掉了`);
426
+ }
427
+ for (const re of SEAM_ROUNDS_AS_RHYTHM) {
428
+ if (re.test(code))
429
+ violations.push(`出现"按轮次停"的写法 (${String(re)}) —— 轮次只准经 safetyCapsFrom 变成安全上限`);
430
+ }
431
+ return { ok: violations.length === 0, violations };
432
+ }