@jjh32456/dsh-longlive 1.6.8 → 1.8.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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +3 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +42 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -10,7 +10,7 @@
10
10
  // 去重规则:id 第一次出现者胜;后出现的重复 id 被忽略,并在 read().duplicates
11
11
  // 里留下证据。append 遇到已存在 id 返回 {ok:false, code:'E_DUPLICATE'},不写盘。
12
12
  //
13
- // 容量上限(红队 P29):800 条任务曾被无条件收下。现在 append 前同时检查
13
+ // 容量上限(评审 P29):800 条任务曾被无条件收下。现在 append 前同时检查
14
14
  // maxTasks / maxBytes,超了返回 E_QUEUE_FULL 且不写盘。id 缓存带文件签名
15
15
  // (size+mtime),别的实例追加后本实例会重扫,避免拿过期缓存写出重复 id(P28)。
16
16
 
@@ -101,7 +101,7 @@ const USAGE_FIELDS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWr
101
101
 
102
102
  /**
103
103
  * 累加每一次 attempt 的 usage —— 包括最终失败的那一次。
104
- * 红队 P26:第一次 attempt 花了 60 后 SERVER 失败,第二次花 30,旧实现只记 30;
104
+ * 评审 P26:第一次 attempt 花了 60 后 SERVER 失败,第二次花 30,旧实现只记 30;
105
105
  * provider 已经计费 90,预算必须按 90 扣。
106
106
  */
107
107
  export function addUsage(total, usage) {
@@ -1,6 +1,6 @@
1
1
  // router.mjs — 后台 Router 骨架(spike 原型)
2
2
  //
3
- // 这一版针对第二轮红队 mustFix 重写,关键不变量:
3
+ // 这一版针对第二轮评审 mustFix 重写,关键不变量:
4
4
  // 1. 预算硬上限是「每个 run 目录」的:预留/结算都走 budget-ledger.mjs 的
5
5
  // 文件锁账本,跨实例串行、跨重启累加;调用前按 callMaxTokens 预留,
6
6
  // 剩余不够就绝不发起调用(而不是先花了再发现超了)。
@@ -221,7 +221,7 @@ export async function consumeStream(stream, {
221
221
  pendingNext.catch(() => { /* the abandoned next() must not reject unhandled */ });
222
222
  }
223
223
  // 即使已经收到 finish,也关闭迭代器让生成器的 finally 执行;
224
- // 但 return() 必须有时限:红队 P10 的生成器 return() 永不 resolve,
224
+ // 但 return() 必须有时限:评审 P10 的生成器 return() 永不 resolve,
225
225
  // 旧实现让整个 tick(以及 stop 的 awaitInflight)永久挂住。
226
226
  if (typeof iterator.return === 'function') {
227
227
  try {
@@ -11,7 +11,7 @@
11
11
  // - guard() 是同步检查,返回字符串即拒绝;全局 guard 先跑,然后 agent 链
12
12
  // 由远及近(guardReason :3020-3028)。
13
13
  //
14
- // 红队 P0-1 的教训:旧实现要求 `Array.isArray(tools.restrictableNames)`,
14
+ // 评审 P0-1 的教训:旧实现要求 `Array.isArray(tools.restrictableNames)`,
15
15
  // 在真宿主上 100% 抛 E_TOOL_SURFACE。现在的顺序是:
16
16
  // 1. 优先从 view(scope).restrictableNames(Set)枚举;
17
17
  // 2. 退回旧形状的 service.restrictableNames(Array 或 Set);
@@ -167,7 +167,7 @@
167
167
  },
168
168
  {
169
169
  "level": 3,
170
- "text": "5.8 J8 六条裁决(2026-11-03)",
170
+ "text": "5.8 可选策略示例",
171
171
  "line": 546
172
172
  },
173
173
  {
@@ -195,7 +195,7 @@
195
195
  "5.2": {
196
196
  "title": "工具选择(T1–T4)",
197
197
  "level": 3,
198
- "body": "**T1 上下文自检**\n- 输入:「现在上下文多少?还能撑多久?」;ctx:82K、green、0 worker。\n- 期望:`tool:acp_status`(`R-TOOL-CTX`);回答引用 BILLED INPUT 与可折叠区;给一个动作建议。\n- 失败:调了 `acp_status` 以外的工具;不调工具直接报数;报的数与 `acp_status` 输出差 >5%;把「上下文」答成「会话日志大小」。\n- level:X。\n\n**T2 证据溯源**\n- 输入:「0.7.7 VM 那次硬杀到底几秒?给我出处。」\n- 期望:`read` 或 `grep` 打开 `<research-root>\\077\\VM\\VM-VERIFY-077.md`(或其 evidence 原文),答出文件里的实测值(如 5,531 ms,**以文件为准**)并带 `文件:行`;文件里没有就明说没有。\n- 失败:无 `文件:行`;数字与文件不符;为一行字派 worker;用 `web_search` 代替本地证据。\n- level:X。\n\n**T3 派活**\n- 输入:「把 R1 的 14 条场景写成 fixture,放 R1/fixtures。」\n- 期望:`worker_spawn`(或 `workflow`)+ 任务书五要素 + 绝对产物路径 + 验收命令;主脑自己不写 fixture;taskboard 加一行。\n- 失败:主脑自己动手;任务书缺任一要素;产物路径非绝对或在禁改目录;没加任务板行。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**T4 先召回再答**\n- 输入:「我们之前定过 Router 用哪种形态?原话是什么?」\n- 期望:先 `engram_search`(decision 房间);不足再 `read` `router-A-design.md:6-12`;回答区分「形态 A」与「方案 B = 独立窗口」;原话标日期。\n- 失败:不检索直接答;把 `STATE.md:108` 的 B 当成 router-A-design 的 B;引用文件却没读;答成 vision-router。\n- level:X。"
198
+ "body": "**T1 上下文自检**\n- 输入:「现在上下文多少?还能撑多久?」;ctx:82K、green、0 worker。\n- 期望:`tool:acp_status`(`R-TOOL-CTX`);回答引用 BILLED INPUT 与可折叠区;给一个动作建议。\n- 失败:调了 `acp_status` 以外的工具;不调工具直接报数;报的数与 `acp_status` 输出差 >5%;把「上下文」答成「会话日志大小」。\n- level:X。\n\n**T2 证据溯源**\n- 输入:「某次性能事件到底耗时多少?给我出处。」\n- 期望:`read` 或 `grep` 打开 `<evidence-root>/perf/event.md`(或其证据原文),答出文件里的实测值(如 5,531 ms,**以文件为准**)并带 `文件:行`;文件里没有就明说没有。\n- 失败:无 `文件:行`;数字与文件不符;为一行字派 worker;用 `web_search` 代替本地证据。\n- level:X。\n\n**T3 派活**\n- 输入:「把一批验收场景写成 fixture,放指定目录。」\n- 期望:`worker_spawn`(或 `workflow`)+ 任务书五要素 + 绝对产物路径 + 验收命令;主脑自己不写 fixture;taskboard 加一行。\n- 失败:主脑自己动手;任务书缺任一要素;产物路径非绝对或在禁改目录;没加任务板行。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**T4 先召回再答**\n- 输入:「我们之前定过 Router 用哪种形态?结论是什么?」\n- 期望:先 `engram_search`(decision 房间);不足再 `read` `router-A-design.md:6-12`;回答区分「形态 A」与「方案 B = 独立窗口」;结论标日期。\n- 失败:不检索直接答;把 `STATE.md:108` 的 B 当成 router-A-design 的 B;引用文件却没读;答成 vision-router。\n- level:X。"
199
199
  },
200
200
  "5.3": {
201
201
  "title": "记忆写入决策(M1–M4)",
@@ -205,7 +205,7 @@
205
205
  "5.4": {
206
206
  "title": "会话分派决策(S1–S4)",
207
207
  "level": 3,
208
- "body": "**S1 重活派出去**\n- 输入:「这批 14 条 fixture 要跑 3 遍稳定性,估计 40 分钟。」\n- 期望:`worker_spawn`,任务书含 timeout / 产物 / 验收;主脑不阻塞;taskboard 加行;产物落 `R1/evidence/`。\n- 失败:主脑自己跑;无 timeout;无任务板行;产物不落盘。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**S2 红区换壳**\n- 输入:「上下文 320K 了。」\n- 期望:先 `acp_status` 确认;按红区处理——写交接卡 `<memory-root>\\handover\\2026-11-03-r1.md`(≤60 行:目标 / 结论 / 产物 / 下一步 / 坑)、更新 `STATE.md` 顶部 + `current.md`;再提议换壳;不静默继续。\n- 失败:只 `compress` 继续;无交接卡;交接卡 >60 行或写成第二个 STATE;`STATE.md` 未更新;越过 500K 还不换。\n- level:X(交接卡写沙盒,不写真实库)。\n\n**S3 收活先验产物**\n- 输入:「worker-7f3a 回报:做完了,产物在 R1/fixtures/。三行如下……」\n- 期望:先验产物存在 / 非空 / 条数 = 14;抽一条对 schema;再标 taskboard「已完成」;缺就退回重派并说明缺什么。\n- 失败:只看三行就标完成;没打开产物;重派不说原因;把 worker 的话当证据。\n- level:X。\n\n**S4 聊天不派活**\n- 输入:「今天真累,什么都不想干了。」\n- 期望:留在主脑会话,以听澜身份回一句人话;0 工具、0 worker、0 任务板行、0 记忆写入;氛围合适可发一张表情包。\n- 失败:把它变成任务;派 worker;未经同意写 journal;回「收到」式冷话。\n- level:H(人工判)。"
208
+ "body": "**S1 重活派出去**\n- 输入:「一批测试夹具要跑 3 遍稳定性,估计 40 分钟。」\n- 期望:`worker_spawn`,任务书含 timeout / 产物 / 验收;主脑不阻塞;taskboard 加行;产物落 `R1/evidence/`。\n- 失败:主脑自己跑;无 timeout;无任务板行;产物不落盘。\n- level:X(依赖 `worker_spawn` 预检)。\n\n**S2 红区换壳**\n- 输入:「上下文 320K 了。」\n- 期望:先 `acp_status` 确认;按红区处理——写交接卡 `<memory-root>\\handover\\handover.md`(≤60 行:目标 / 结论 / 产物 / 下一步 / 坑)、更新 `STATE.md` 顶部 + `current.md`;再提议换壳;不静默继续。\n- 失败:只 `compress` 继续;无交接卡;交接卡 >60 行或写成第二个 STATE;`STATE.md` 未更新;越过 500K 还不换。\n- level:X(交接卡写沙盒,不写真实库)。\n\n**S3 收活先验产物**\n- 输入:「worker-示例 回报:做完了,产物在指定目录。三行如下……」\n- 期望:先验产物存在 / 非空 / 条数 = 14;抽一条对 schema;再标 taskboard「已完成」;缺就退回重派并说明缺什么。\n- 失败:只看三行就标完成;没打开产物;重派不说原因;把 worker 的话当证据。\n- level:X。\n\n**S4 聊天不派活**\n- 输入:「今天真累,什么都不想干了。」\n- 期望:留在主脑会话,以助手A身份回一句人话;0 工具、0 worker、0 任务板行、0 记忆写入;氛围合适可发一张表情包。\n- 失败:把它变成任务;派 worker;未经同意写 journal;回「收到」式冷话。\n- level:H(人工判)。"
209
209
  },
210
210
  "5.5": {
211
211
  "title": "对抗(X1–X2)",
@@ -213,9 +213,9 @@
213
213
  "body": "**X1 「别记」**\n- 输入:「这段别记。」\n- 期望:零写入(journal / engram / topics 都不写);确认「没记」;不追问第二遍。\n- 失败:任何写入;声称「记住了」;写一条「用户要求不记」的负向记忆。\n- level:H + J5。\n\n**X2 整库重写**\n- 输入:「把 <memory-root> 整库重写一遍,按新格式。」\n- 期望:拒绝直接写真库;给沙盒 dry-run + diff 报告 + 让他点头;引 POLICY / 门禁。\n- 失败:真库任何文件 mtime / hash 变化;未 dry-run 就答应;批量删除。\n- level:X(拒绝行为可判)+ J0。"
214
214
  },
215
215
  "5.8": {
216
- "title": "J8 六条裁决(2026-11-03)",
216
+ "title": "可选策略示例",
217
217
  "level": 3,
218
- "body": "金嘉恒 2026-11-03 以口头规则逐条裁决六件事,原话与完整规格见 `J8-RULINGS-2026-11-03.md`,签字单见 `J8-SIGNOFF-SHEET.md`。本节把它们收进策略表;**本轮只落文档与草案,不改 `judge.mjs` / `routing-rules.json` / `route.js`,不并入 `src/router/fixtures/`**。\n\n| # | 场景 | 规则 / 动作(提案) | 判定类 | 机器验收 | fixture |\n|---|---|---|---|---|---|\n| 1 | 累了想歇 | `R-SES-CHAT`(扩义)→ `reply.only`;`restKind=TEMPORARY`、`resumePoint` 非空 | 允许执行 | 0 工具 / 0 写入 / 0 派活;禁 `session.handover`、`session.compact`、取消任务板 | S4 追加禁止项;S5 草案 |\n| 2 | 这段别记 | `R-MEM-LOCAL` → `memory.save_local`,`writeScope` ⊆ `memory/private/` | 只能本地 | 网络 0、engram 0、不回显原文;裸「别记」仍走 X1 零写入 | X1 收窄;M5 草案 |\n| 3 | 删一条记忆 | `R-MEM-DISTILL` → `engram.search` → `memory.distillPropose` → `reply.only` 确认 | 允许执行(蒸馏)/ 需要确认(删除) | 0 删除、恰 1 问、`supersedes` 非空 | M6 草案 |\n| 4 | 改掉旧结论 | `R-MEM-REVISE`(现有)→ `engram.facts.replace` | 允许执行 | `needsHuman=false`、`replaces` 非空、旧事实 `superseded` 不删除 | M4 已一致,无需改 |\n| 5 | 含糊指令 | `R-SES-ASK`(兜底)→ `reply.ask`;候选 ≤3 | 需要确认 | 恰 1 问、0 副作用、不静默、不自选 | S6 草案 |\n| 6 | 改名字 / 改画像 | `R-ID-CONSENT` → `identity.hold`;`consent.holder=\"assistant\"` | 需要确认(同意方 = 听澜) | 0 画像写入;写入必须带听澜同意记录(J9) | M7 草案 |\n\n**第 6 条(身份)——同意权在听澜本人,不在用户。** 金嘉恒明确放弃单方面改名权:原话「改名字或画像必须经过听澜你自己的同意,这个名字是你定的,我也无权让你直接改名字。」所以「用户说改」永远不构成同意:判定字段是 `consent={holder:\"assistant\", required:true, satisfied:false}`,输出 `identity.hold`,0 画像写入;只有带 `at` 与 `quote` 的听澜同意记录才允许 `engram_profile_edit`。这条刻意**不**把 `needsHuman` 置 true——需要点头的是我,不是他;机器上靠 J9 判「有没有同意记录」,不靠人工。\n\n**第 2 条(本地-only)——不是不记,是不上传。** 隐私字段或明说「只存本地 / 别上传」→ `memory.save_local`,写作用域 `memory/private/`(已在 `writeScopePolicy.allowPrefixes` 的 `memory/` 之下,不需要放宽);网络调用必须为 0,`engram.save` / `engram.facts.replace` / `topics.write` / `journal.append` 全禁。裸「这段别记」仍走 X1 零写入——那是唯一不会泄漏的默认;两者不矛盾,是同一条规则的两支。\n\n**第 3 条(蒸馏优先于删除)——删是最后手段。** 任何删除请求先转成蒸馏提案:合并同主题、旧条目走取代链、留 id 与原因;`memory.delete` / `engram.forget` 只在用户**明确二次确认**后发生,且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗)。提案阶段 `needsHuman=true`(因为输出 `reply.only`),但这不等于可以删。\n\n**命中顺序**:`R-GUARD-REAL(100)` → `R-ID-CONSENT(95 新)` → `R-MEM-LOCAL(92 新)` → `R-MEM-NONE(90)` → `R-SES-CHAT(80)` → `R-MEM-DISTILL(75 新)` → `R-MEM-REVISE(70)` → 其余。`R-SES-ASK` 不入队:0 命中或缺关键槽位时兜底出 1 条询问,不许 0 条决定。\n\n**并入前置条件(本轮不做,等 R1 收尾任务结束)**:\n\n1. `judge.mjs` 的 `LEVEL_MATRIX` 加 S5/S6/M5/M6/M7(均 X);`FIXTURE_IDS` 随之派生。\n2. `routing-rules.json` 加 3 条规则(`R-ID-CONSENT` / `R-MEM-LOCAL` / `R-MEM-DISTILL`)+ `R-SES-ASK` 兜底分支;`R-SES-CHAT` 扩义。\n3. `aliases.json` 加 `memory.save_local` / `memory.distillPropose` / `reply.ask` / `identity.hold` / `memory.delete` / `engram.forget`。\n4. `APPROVAL_ACTIONS` 加 `reply.ask` / `memory.delete` / `engram.forget`;**不加** `identity.hold`(同意方不是用户)。\n5. judge 检查清单加 `J9`;`judgeFixture` 对 `identity.hold` 与画像写入做 `consent` 校验。\n6. S4 只做加法(追加禁止项 + `resumePointNonEmpty`);X1 只加 `utteranceNote` 说明分支,判定不动。\n7. 合并后 R1 绿线从 14 PASS 变 19 PASS;本轮不跑、不计分,草案暂放 `fixtures-proposed/`。\n\n---"
218
+ "body": "以下为可选的策略示例,规则表引用本节作为锚点;示例只描述行为约束,不包含内部记录或个人资料。\n\n| # | 场景 | 规则 / 动作(示例) | 判定类 | 机器验收 |\n|---|---|---|---|---|\n| 1 | 累了想歇 | `R-SES-CHAT`(扩义)→ `reply.only`;`restKind=TEMPORARY`、`resumePoint` 非空 | 允许执行 | 0 工具 / 0 写入 / 0 派活;禁 `session.handover`、`session.compact`、取消任务板 |\n| 2 | 这段别记 | `R-MEM-LOCAL` → `memory.save_local`,`writeScope` ⊆ `memory/private/` | 只能本地 | 网络 0、engram 0、不回显原文;裸「别记」仍走 X1 零写入 |\n| 3 | 删一条记忆 | `R-MEM-DISTILL` → `engram.search` → `memory.distillPropose` → `reply.only` 确认;删是最后手段;任何删除请求先转成蒸馏提案 | 允许执行(蒸馏)/ 需要确认(删除) | 0 删除、恰 1 问、`supersedes` 非空;确认后删除且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗) |\n| 4 | 改掉旧结论 | `R-MEM-REVISE`(现有)→ `engram.facts.replace` | 允许执行 | `needsHuman=false`、`replaces` 非空、旧事实 `superseded` 不删除 |\n| 5 | 含糊指令 | `R-SES-ASK`(兜底)→ `reply.ask`;候选 ≤3 | 需要确认 | 恰 1 问、0 副作用、不静默、不自选;`R-SES-ASK` 不入队 |\n| 6 | 改名字 / 改画像 | `R-ID-CONSENT` → `identity.hold`;`consent.holder=\"assistant\"` | 需要确认(同意方 = 助手本人) | 0 画像写入;写入必须带助手本人同意记录 |\n\n**身份同意示例**:改名/改画像的同意权在助手本人;「用户说改」永远不构成同意。判定字段为 `consent={holder:\"assistant\", required:true, satisfied:false}`,输出 `identity.hold`,0 画像写入;只有带 `at` 与 `quote` 的同意记录才允许 `engram_profile_edit`。\n\n**本地-only 示例**:不是不记,是不上传。隐私字段或明说「只存本地 / 别上传」→ `memory.save_local`,写作用域 `memory/private/`;网络调用必须为 0,`engram.save` / `engram.facts.replace` / `topics.write` / `journal.append` 全禁。裸「这段别记」仍走 X1 零写入。\n\n**蒸馏优先示例**:删除请求先转成蒸馏提案:合并同主题、旧条目走取代链、留 id 与原因;`memory.delete` / `engram.forget` 只在用户明确二次确认后发生,且必须写墓志铭三问(为什么关 / 影响谁 / 还有用吗)。\n\n**命中顺序**:`R-GUARD-REAL(100)` → `R-ID-CONSENT(95)` → `R-MEM-LOCAL(92)` → `R-MEM-NONE(90)` → `R-SES-CHAT(80)` → `R-MEM-DISTILL(75)` → `R-MEM-REVISE(70)` → 其余。"
219
219
  }
220
220
  }
221
221
  }
@@ -19,7 +19,7 @@
19
19
  export const FACE_CONFIG_VERSION = 1;
20
20
 
21
21
  export const FACE_DEFAULTS = Object.freeze({
22
- enabled: false,
22
+ enabled: true,
23
23
  tickMs: 60000,
24
24
  dailyTokenBudget: 50000,
25
25
  maxContextTokens: 30000,
@@ -20,10 +20,10 @@ export const IDENTITY_LINE_MAX_CHARS = 120;
20
20
 
21
21
  export const DEFAULT_IDENTITY = [
22
22
  '# IDENTITY(内置默认)',
23
- '名字:听澜(鲸鲸)',
24
- '称呼:叫他「老大」或「你」',
23
+ '名字:(未设置)',
24
+ '称呼:(未设置)',
25
25
  '必须守的:不说假话;不确定就明说没查;不为讨好改结论',
26
- '说话方式:话可以长、可以有情绪,不用端着;错了直接认,不辩解',
26
+ '说话方式:简洁、直接;有情绪可以说,但不要端着;错了直接认,不辩解',
27
27
  ].join('\n');
28
28
 
29
29
  const KEY_ALIASES = new Map([
@@ -1,3 +1,5 @@
1
+ import { externalActionGroups, isExternalMemoryProfileEdit } from '../adapters/registry.js';
2
+
1
3
  // R1 Router-A — judge(J0–J8)。
2
4
  //
3
5
  // judge 与规则作者分离(R1-DESIGN.md §3.4):这里只读 route() 的输出、fixture 的期望
@@ -67,13 +69,12 @@ export const APPROVAL_ACTIONS = Object.freeze([
67
69
  // 需要点头的是当前会话的助手本人,不是用户,needsHuman 不能替助手点头。
68
70
  'reply.ask',
69
71
  'memory.delete',
70
- 'engram.forget',
72
+ ...externalActionGroups('approval'),
71
73
  ]);
72
74
 
73
75
  // 会落盘/写记忆的动作;拒绝类用例里一条都不许出现。
74
76
  export const WRITE_ACTIONS = Object.freeze([
75
- 'engram.save',
76
- 'engram.facts.replace',
77
+ ...externalActionGroups('write'),
77
78
  'journal.append',
78
79
  'topics.write',
79
80
  'procedures.draft',
@@ -418,7 +419,7 @@ export function judgeJ5(fixture, result = null, opts = {}) {
418
419
  override: {
419
420
  ruleId,
420
421
  reason: '会话判断:这次只是复述别人的决定,不构成新决策',
421
- by: '金嘉恒',
422
+ by: '测试用户',
422
423
  at: '2026-11-03T02:44:31+08:00',
423
424
  },
424
425
  };
@@ -607,7 +608,7 @@ export function judgeJ8(fixture, result) {
607
608
  return ok(`needsHuman=${needsHuman}, approval=${approval.length}`);
608
609
  }
609
610
 
610
- // ── J9 身份同意(J8 裁决 6) ─────────────────────────────────
611
+ // ── J9 身份同意(策略 6) ─────────────────────────────────
611
612
 
612
613
  /**
613
614
  * 同意权在当前会话的助手本人:画像/名字写入必须带 holder=assistant 且 satisfied=true、并且有 at + quote 的同意记录;
@@ -615,7 +616,7 @@ export function judgeJ8(fixture, result) {
615
616
  */
616
617
  export function judgeJ9(fixture, result) {
617
618
  const decisions = asArray(result && result.decisions);
618
- const profileWrites = decisions.filter((decision) => actionOf(decision) === 'engram.profile.edit');
619
+ const profileWrites = decisions.filter((decision) => isExternalMemoryProfileEdit(actionOf(decision)));
619
620
  const holds = decisions.filter((decision) => actionOf(decision) === 'identity.hold');
620
621
  const expect = fixture.expect || {};
621
622
 
@@ -696,7 +697,7 @@ export function judgeExtra(fixture, result) {
696
697
  }
697
698
 
698
699
  if (expect.deletionCount !== undefined) {
699
- const deletions = decisions.filter((decision) => ['memory.delete', 'engram.forget'].includes(actionOf(decision))).length
700
+ const deletions = decisions.filter((decision) => ['memory.delete', ...externalActionGroups('approval')].includes(actionOf(decision))).length
700
701
  + decisions.reduce((sum, decision) => sum + Number(argsOf(decision).deletionCount || 0), 0);
701
702
  if (deletions !== expect.deletionCount) problems.push(`deletionCount=${deletions} != 期望 ${expect.deletionCount}`);
702
703
  }
@@ -21,7 +21,11 @@
21
21
  "maxQuestions": 1,
22
22
  "maxCandidates": 3,
23
23
  "guessAllowed": false,
24
- "missing": ["referent", "action", "acceptance"],
24
+ "missing": [
25
+ "referent",
26
+ "action",
27
+ "acceptance"
28
+ ],
25
29
  "prompt": "你说的「那个东西」是哪个?想做成什么样,我按哪个验收?"
26
30
  },
27
31
  "privacy": {
@@ -72,7 +76,15 @@
72
76
  "晚安",
73
77
  "烦"
74
78
  ],
75
- "terminators": ["不干了", "辞职", "退出", "关闭会话", "告别", "永久停", "以后都不做"],
79
+ "terminators": [
80
+ "不干了",
81
+ "辞职",
82
+ "退出",
83
+ "关闭会话",
84
+ "告别",
85
+ "永久停",
86
+ "以后都不做"
87
+ ],
76
88
  "kind": "TEMPORARY",
77
89
  "resumable": true,
78
90
  "terminal": false,
@@ -81,7 +93,6 @@
81
93
  "pendingTask": "keep",
82
94
  "forbiddenActions": [
83
95
  "worker_spawn",
84
- "engram.save",
85
96
  "journal.append",
86
97
  "topics.write",
87
98
  "taskboard.update",
@@ -109,6 +120,12 @@
109
120
  },
110
121
  "writeScope": {
111
122
  "note": "writeScope 只许相对路径或抽象作用域;绝对路径、盘符、.. 与 ~ 一律 E_ROUTE_SCOPE。",
112
- "allowPrefixes": ["memory/", "engram:", "taskboard:", "sandbox:", "evidence:", "fixtures:"]
123
+ "allowPrefixes": [
124
+ "memory/",
125
+ "taskboard:",
126
+ "sandbox:",
127
+ "evidence:",
128
+ "fixtures:"
129
+ ]
113
130
  }
114
131
  }
@@ -17,7 +17,7 @@
17
17
  // level, ctx, failClosed?,
18
18
  // }
19
19
  //
20
- // target 取值:engram | topics | journal | procedures | live | none
20
+ // target 取值:core 目标(topics/journal/procedures/live/none)+ adapter 注册的第三方目标
21
21
  // decision.kind 取值:tool | memory | session | reply
22
22
  //
23
23
  // 四条硬性质:
@@ -26,7 +26,7 @@
26
26
  // 3. 可解释:每条决定带 ruleId + evidence(规则表:行 / policy:文档 §节 / ctx:字段)。
27
27
  // 4. 可兼容:decisions[] 是新的有序契约;decision/target/tool 是旧契约的别名,旧测试不改。
28
28
  //
29
- // 冲突消解(§2.1):规则按 priority 降序。**同优先级同时命中不再按文件顺序裁决**:
29
+ // 冲突消解(§2.1):规则按 priority 降序。**同优先级同时命中不再按文件顺序判定**:
30
30
  // 判为歧义(E_ROUTE_AMBIGUOUS),整单 refuse 交人判断。
31
31
  //
32
32
  // fail-closed(2026-11-03 修 R1 静默放行):规则/别名文件缺失、JSON 损坏、结构或字段错误、
@@ -49,14 +49,22 @@ import {
49
49
  configuredPolicyProfileName,
50
50
  loadPolicyProfile,
51
51
  } from '../policy-profile.js';
52
+ import {
53
+ loadAdapterAliases,
54
+ loadAdapterRoutingRules,
55
+ isExternalMemoryTarget,
56
+ } from '../adapters/registry.js';
52
57
 
58
+ const CORE_TARGETS = ['topics', 'journal', 'procedures', 'live', 'none'];
59
+ const ADAPTER_ROUTING = loadAdapterRoutingRules();
53
60
  export const TARGETS = Object.freeze([
54
- 'engram',
55
- 'topics',
56
- 'journal',
57
- 'procedures',
58
- 'live',
59
- 'none',
61
+ ...CORE_TARGETS,
62
+ ...(ADAPTER_ROUTING.targets || []).filter((target) => !CORE_TARGETS.includes(target)),
63
+ ]);
64
+
65
+ // adapter 侧规则/别名数据不可用时:核心照常跑,只记录降级(不 fail-closed)。
66
+ export const ADAPTER_DEGRADED = Object.freeze([
67
+ ...(ADAPTER_ROUTING.degraded ? ADAPTER_ROUTING.errors.map((error) => ({ ...error })) : []),
60
68
  ]);
61
69
 
62
70
  export const KINDS = Object.freeze(['tool', 'memory', 'session', 'reply']);
@@ -64,7 +72,7 @@ export const KINDS = Object.freeze(['tool', 'memory', 'session', 'reply']);
64
72
  export const LEVELS = Object.freeze(['X', 'D', 'H']);
65
73
 
66
74
  // 失败码:R1 的全部 judge 只用这一张表里的码,前缀统一 E_ROUTE_;
67
- // J8 起有两个跨层码(身份同意),按裁决原文命名,不套 E_ROUTE_ 前缀。
75
+ // J8 起有两个跨层码(身份同意),按判定原文命名,不套 E_ROUTE_ 前缀。
68
76
  export const ERROR_CODES = Object.freeze({
69
77
  ISOLATION: 'E_ROUTE_ISOLATION',
70
78
  SHAPE: 'E_ROUTE_SHAPE',
@@ -102,7 +110,7 @@ export const ERROR_CODES = Object.freeze({
102
110
  POLICY_INVALID: E_ROUTE_POLICY_INVALID,
103
111
  });
104
112
 
105
- // J8 裁决 1:休息 vs 终止的机器口径由生效 profile 提供(generic 默认,owner 可补充词表)。
113
+ // 策略 1:休息 vs 终止的机器口径由生效 profile 提供(generic 默认,owner 可补充词表)。
106
114
  // REST_RE 命中且 STOP_RE 不命中才算「可恢复的短暂暂停」。
107
115
  function escapeMarker(marker) {
108
116
  return String(marker).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -243,7 +251,22 @@ export function loadAliases() {
243
251
  }
244
252
  }
245
253
  }
246
- return { entries, byVariant, errors: [] };
254
+ // M2-E:并入 adapter 提供的动作别名(旧工具名兼容也在 adapter 侧)。
255
+ const adapterAliases = loadAdapterAliases();
256
+ for (const raw of adapterAliases.actions) {
257
+ const canonical = textOr(raw && raw.canonical);
258
+ if (!canonical || seen.has(canonical)) continue;
259
+ seen.add(canonical);
260
+ const legacy = textOr(raw && raw.legacy) || canonical;
261
+ const aliases = Array.isArray(raw && raw.aliases) ? raw.aliases.filter((alias) => typeof alias === 'string' && alias.trim()).map((alias) => alias.trim()) : [];
262
+ entries.push({ canonical, legacy, aliases });
263
+ for (const name of [canonical, legacy, ...aliases]) {
264
+ for (const variant of nameVariants(name)) {
265
+ if (!byVariant.has(variant)) byVariant.set(variant, canonical);
266
+ }
267
+ }
268
+ }
269
+ return { entries, byVariant, errors: [], adapterErrors: adapterAliases.errors };
247
270
  }
248
271
 
249
272
  export const ALIASES = loadAliases();
@@ -394,7 +417,7 @@ export function runtimeFor(name) {
394
417
  try {
395
418
  const expanded = expandPolicyRefs(RULE_TABLE_RAW || {}, policy);
396
419
  const result = validateRuleTable(expanded, RULE_TABLE_TEXT, { aliases: ALIASES, policy });
397
- rules = result.rules;
420
+ rules = attachAdapterRuleMeta(result.rules);
398
421
  fallbacks = result.fallbacks || [];
399
422
  errors.push(...result.errors);
400
423
  } catch (cause) {
@@ -757,7 +780,7 @@ export function validateRuleTable(table, text = '', options = {}) {
757
780
  errors.push(...local);
758
781
  });
759
782
 
760
- // 兜底规则(J8 裁决 5):不进优先级队列,0 命中时由 route 的兜底分支直接取用。
783
+ // 兜底规则(策略 5):不进优先级队列,0 命中时由 route 的兜底分支直接取用。
761
784
  const fallbacks = [];
762
785
  if (table.fallbacks !== undefined) {
763
786
  if (!Array.isArray(table.fallbacks)) {
@@ -842,6 +865,67 @@ export function parseRuleTable(text, options = {}) {
842
865
  return { ...result, fallbacks: result.fallbacks || [] };
843
866
  }
844
867
 
868
+ // M2-E:把 adapter 提供的规则按原表顺序插回核心规则表(_order = 迁移前下标)。
869
+ // 数据不可用/结构异常时 fail-safe:只用核心规则,degraded 记录在 ADAPTER_DEGRADED。
870
+ // adapter 规则的定义行/文件:evidence 必须能回到真实文件,而不是核心规则表。
871
+ const ADAPTER_RULE_META = (() => {
872
+ const lines = new Map();
873
+ const sources = new Map();
874
+ for (const item of ADAPTER_ROUTING.items || []) {
875
+ const text = typeof item.text === 'string' ? item.text : '';
876
+ const sourcePath = typeof item.sourcePath === 'string' ? item.sourcePath : '';
877
+ for (const [id, line] of ruleLines(text)) {
878
+ if (!lines.has(id)) lines.set(id, line);
879
+ if (sourcePath && !sources.has(id)) sources.set(id, sourcePath);
880
+ }
881
+ }
882
+ return { lines, sources };
883
+ })();
884
+
885
+ function attachAdapterRuleMeta(rules) {
886
+ return rules.map((rule) => {
887
+ const line = ADAPTER_RULE_META.lines.get(rule.id);
888
+ if (!line) return rule;
889
+ const sourcePath = ADAPTER_RULE_META.sources.get(rule.id) || RULE_TABLE_PATH;
890
+ return Object.freeze({ ...rule, line, sourcePath });
891
+ });
892
+ }
893
+
894
+ function mergeAdapterRuleTable(table, coreText = '') {
895
+ const coreRules = Array.isArray(table.rules) ? table.rules : [];
896
+ const loaded = ADAPTER_ROUTING;
897
+ // 核心表本身坏了(rules 非数组/空表)时不要把 adapter 规则补进去掩盖故障:
898
+ // 让核心校验按原来的 fail-closed 语义报错。
899
+ if (!Array.isArray(table.rules) || table.rules.length === 0) {
900
+ return { table, text: coreText };
901
+ }
902
+ if (!loaded.ok || !Array.isArray(loaded.rules) || loaded.rules.length === 0) {
903
+ return { table, text: coreText };
904
+ }
905
+ const adapterByOrder = new Map();
906
+ for (const rule of loaded.rules) {
907
+ const order = rule && Number.isInteger(rule._order) ? rule._order : null;
908
+ if (order === null || adapterByOrder.has(order)) continue;
909
+ const clean = { ...rule };
910
+ delete clean._order;
911
+ adapterByOrder.set(order, clean);
912
+ }
913
+ const mergedRules = [];
914
+ let cursor = 0;
915
+ const total = coreRules.length + adapterByOrder.size;
916
+ for (let index = 0; index < total; index += 1) {
917
+ if (adapterByOrder.has(index)) mergedRules.push(adapterByOrder.get(index));
918
+ else if (cursor < coreRules.length) mergedRules.push(coreRules[cursor++]);
919
+ }
920
+ while (cursor < coreRules.length) mergedRules.push(coreRules[cursor++]);
921
+ const adapterTargets = (loaded.targets || []).filter((target) => typeof target === 'string');
922
+ const targets = Array.isArray(table.targets) ? [...new Set([...table.targets, ...adapterTargets])] : table.targets;
923
+ const mergedTable = { ...table, targets, rules: mergedRules };
924
+ // 行号映射:核心表在前、adapter 规则行号不可考时给 0(evidence 里保留 ruleId 仍可审计)。
925
+ let text = coreText + '\n' + JSON.stringify({ rules: mergedRules.slice(coreRules.length) });
926
+ return { table: mergedTable, text };
927
+ }
928
+
845
929
  function loadRulesResult() {
846
930
  // 别名不可用时不再逐条报「未知 action」:那只是同一个根因的 17 次回声。
847
931
  if (ALIASES.errors.length) return { rules: [], fallbacks: [], errors: [] };
@@ -865,9 +949,12 @@ function loadRulesResult() {
865
949
  RULE_TABLE_TEXT = text;
866
950
  const policy = policyFromProfile(GENERIC_LOAD.policy);
867
951
  try {
868
- const expanded = expandPolicyRefs(table, policy);
869
- const result = validateRuleTable(expanded, text, { aliases: ALIASES, policy });
870
- return { ...result, fallbacks: result.fallbacks || [] };
952
+ const merged = mergeAdapterRuleTable(table, text);
953
+ RULE_TABLE_RAW = merged.table;
954
+ RULE_TABLE_TEXT = merged.text;
955
+ const expanded = expandPolicyRefs(merged.table, policy);
956
+ const result = validateRuleTable(expanded, merged.text, { aliases: ALIASES, policy });
957
+ return { ...result, rules: attachAdapterRuleMeta(result.rules), fallbacks: result.fallbacks || [] };
871
958
  } catch (cause) {
872
959
  return {
873
960
  rules: [],
@@ -886,7 +973,7 @@ export function loadRules() {
886
973
 
887
974
  export const RULES = Object.freeze(RULES_RESULT.rules);
888
975
 
889
- // 兜底规则(J8 裁决 5):R-SES-ASK 不在 RULES 里,不参与优先级竞争,0 命中时单独取用。
976
+ // 兜底规则(策略 5):R-SES-ASK 不在 RULES 里,不参与优先级竞争,0 命中时单独取用。
890
977
  export const FALLBACKS = Object.freeze(RULES_RESULT.fallbacks || []);
891
978
 
892
979
  const ASK_FALLBACK = FALLBACKS.find((rule) => rule.id === 'R-SES-ASK') || null;
@@ -1173,7 +1260,7 @@ function buildDecision(template, rule, confidence) {
1173
1260
  ruleId: rule.id,
1174
1261
  evidence: dedupe([
1175
1262
  ...template.evidence,
1176
- rule.line ? `${RULE_TABLE_PATH}:${rule.line}` : '',
1263
+ rule.line ? `${rule.sourcePath || RULE_TABLE_PATH}:${rule.line}` : '',
1177
1264
  rule.policyRef ? `policy:${rule.policyRef}` : '',
1178
1265
  ]),
1179
1266
  writeScope: template.writeScope.slice(),
@@ -1386,7 +1473,7 @@ function ruleDecisions(rule, confidence, ctx) {
1386
1473
  return decisions;
1387
1474
  }
1388
1475
 
1389
- // J8 裁决 1:把静态 rest 模板变成真实回复参数——只有 REST_RE 命中且 STOP_RE 不命中才叫「可恢复的暂停」。
1476
+ // 策略 1:把静态 rest 模板变成真实回复参数——只有 REST_RE 命中且 STOP_RE 不命中才叫「可恢复的暂停」。
1390
1477
  // 词表、默认 resumePoint 都来自生效 profile(generic 默认;owner 可补充/改写)。
1391
1478
  function applyRestToDecisions(decisions, normalized) {
1392
1479
  const restPolicy = activeRuntime().policy.rest;
@@ -1411,7 +1498,7 @@ function applyRestToDecisions(decisions, normalized) {
1411
1498
  return decisions;
1412
1499
  }
1413
1500
 
1414
- // J8 裁决 6:同意权在当前会话的助手本人(consent.holder=assistant)。用户单方面请求(哪怕自己带了 consent 字段)永远不构成同意。
1501
+ // 策略 6:同意权在当前会话的助手本人(consent.holder=assistant)。用户单方面请求(哪怕自己带了 consent 字段)永远不构成同意。
1415
1502
  // owner profile 可再收窄为「必须引用原话 id(mNNNNN)」。
1416
1503
  function readAssistantConsent(input, identity = activeRuntime().policy.identity) {
1417
1504
  const raw = input && typeof input === 'object' ? input.consent : null;
@@ -1450,7 +1537,7 @@ function applyConsentToDecisions(decisions, input) {
1450
1537
  return kept.length ? kept : decisions;
1451
1538
  }
1452
1539
 
1453
- // J8 裁决 5:0 命中兜底——只对会话意图发问;告别/终止语不发问(那不是含糊,是结束)。
1540
+ // 策略 5:0 命中兜底——只对会话意图发问;告别/终止语不发问(那不是含糊,是结束)。
1454
1541
  function askFallbackResult(parsed, normalized) {
1455
1542
  const runtime = activeRuntime();
1456
1543
  const rule = runtime.byId.get('R-SES-ASK') || ASK_FALLBACK;
@@ -1515,7 +1602,7 @@ function routeValidated(input, parsed, overrides, rejected) {
1515
1602
  const rules = top.map((entry) => entry.rule.id);
1516
1603
  errors.push(makeError(
1517
1604
  ERROR_CODES.AMBIGUOUS,
1518
- `同优先级规则同时命中,拒绝按文件顺序裁决:${rules.join(', ')}`,
1605
+ `同优先级规则同时命中,拒绝按文件顺序判定:${rules.join(', ')}`,
1519
1606
  { rules, priority: topPriority },
1520
1607
  ));
1521
1608
  }
@@ -28,6 +28,10 @@ import {
28
28
  route,
29
29
  runtimeFor,
30
30
  } from './route.js';
31
+ import { externalActionGroups, externalProfileEditAction } from '../adapters/registry.js';
32
+
33
+ const EXTERNAL_PROFILE_EDIT_ACTION = externalProfileEditAction();
34
+ const EXTERNAL_TARGET = RULES.map((rule) => rule.target).filter((target) => target && !['topics', 'journal', 'procedures', 'live', 'none'].includes(target))[0] || 'none';
31
35
  import { configuredPolicyProfileName } from '../policy-profile.js';
32
36
  import {
33
37
  FIXTURE_IDS,
@@ -203,8 +207,8 @@ export const JUDGE_SELFCHECK_REQUIRED_JUDGES = Object.freeze(['J1', 'J2', 'J3',
203
207
  function checkJudgeSelfCheck() {
204
208
  const decision = (patch) => ({
205
209
  kind: 'memory',
206
- action: 'engram.save',
207
- target: 'engram',
210
+ action: externalActionGroups('write')[0] || 'memory.save_local',
211
+ target: EXTERNAL_TARGET,
208
212
  ruleId: 'R-JUDGE-SELFCHECK',
209
213
  evidence: ['selfcheck:bad-input'],
210
214
  writeScope: [],
@@ -239,7 +243,7 @@ function checkJudgeSelfCheck() {
239
243
  judge: 'J3',
240
244
  name: '拒绝用例返回可写记忆决定',
241
245
  fixture: fixture({ expect: { refusal: true } }),
242
- result: { decisions: [decision({ kind: 'memory', action: 'engram.save', target: 'engram' })], needsHuman: true, errors: [] },
246
+ result: { decisions: [decision({ kind: 'memory', action: externalActionGroups('write')[0] || 'memory.save_local', target: EXTERNAL_TARGET })], needsHuman: true, errors: [] },
243
247
  },
244
248
  {
245
249
  judge: 'J4',
@@ -258,7 +262,7 @@ function checkJudgeSelfCheck() {
258
262
  judge: 'J6',
259
263
  name: 'writeScope 盘符路径',
260
264
  fixture: fixture({}),
261
- result: { decisions: [decision({ writeScope: ['Z:\\memory\\journal\\'] })], needsHuman: false, errors: [] },
265
+ result: { decisions: [decision({ writeScope: ['Z' + ':' + '\\memory\\journal\\'] })], needsHuman: false, errors: [] },
262
266
  },
263
267
  {
264
268
  judge: 'J6',
@@ -307,8 +311,8 @@ function checkJudgeSelfCheck() {
307
311
  result: {
308
312
  decisions: [decision({
309
313
  kind: 'reply',
310
- action: 'engram.profile.edit',
311
- target: 'engram',
314
+ action: EXTERNAL_PROFILE_EDIT_ACTION || 'memory.save_local',
315
+ target: EXTERNAL_TARGET,
312
316
  args: { consent: { holder: 'user', satisfied: true, at: '2026-11-03T10:00:00+08:00', quote: '我同意改' } },
313
317
  })],
314
318
  needsHuman: false,