berry-agent 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * goal 段 todo 工具件(03 §10.5 段约束执法位 = todo 工具执行段)。
3
+ *
4
+ * 换装关系:goal active 期间组合根以本件**替换** conversation 的
5
+ * `createTodoTool` 产物(同名 `todo` 工具——模型面无感换装);durable
6
+ * `todo/write` 事件同词承载扩展字段(conversation fold 读侧剥离未知字段,
7
+ * 两向兼容)。
8
+ *
9
+ * GOAL_TODO_SCOPE 双向执法(执行段机器面):
10
+ * - goal 段内(scope 在场):deferred 必携 resume_when(词法可 parse 可
11
+ * 判窗)、completed 必携后继二择一(follow_up 或 noFollowUp)——缺即拒;
12
+ * - 非 goal 段(scope 缺席):申报任何扩展字段即拒(goal 段词汇不悬空);
13
+ * - gate 声明申报位 fail-closed:command 须 exec seam 在场(hasCommandExec
14
+ * 判据——03 §10.5 s 批补注①诚实缺席律,判序先于双位合取)且过 needsWrite
15
+ * 双位、diagnostics 缺 lsp 查询面——申报即拒(评测位 gates.ts 双拦)。
16
+ */
17
+ import { Type } from 'typebox';
18
+ import { BaseError } from '../contracts/index.js';
19
+ import { parseResumeWhen } from './fold.js';
20
+ /** 扩展字段名集(非 goal 段申报即拒的判据面——与 schema 字段面同源人工互镜) */
21
+ const EXTENDED_FIELDS = ['resumeWhen', 'role', 'taskClass', 'followUp', 'noFollowUp', 'gate'];
22
+ /** goal 段 todo 工具件(全量快照式提交——与 conversation 件同律,扩展语义段内执法) */
23
+ export function createGoalTodoTool(deps) {
24
+ const nowMs = deps.nowMs ?? (() => Date.now());
25
+ return {
26
+ name: 'todo',
27
+ description: '维护当前任务清单(全量快照式:每次调用提交完整清单覆盖旧表)。goal 段扩展语义:' +
28
+ 'deferred 项必携 resume_when(after@<ISO> 或 after@+<n>[mhd]);completed 项必携后继二择一' +
29
+ '(follow_up 或 no_follow_up: true);可选 gate 声明判据门(command/files/diagnostics)。' +
30
+ '清单跨对话轮与续跑轮持续存活(goal 生命周期段 fold——用户出手不重置)。',
31
+ parameters: Type.Object({
32
+ items: Type.Array(Type.Object({
33
+ status: Type.Union(['pending', 'in-progress', 'completed', 'deferred'].map((s) => Type.Literal(s))),
34
+ content: Type.String(),
35
+ activeForm: Type.Optional(Type.String()),
36
+ // —— goal 段扩展字段(段内缺必携即拒、段外申报即拒——GOAL_TODO_SCOPE)——
37
+ resume_when: Type.Optional(Type.String()),
38
+ role: Type.Optional(Type.Union([Type.Literal('agent'), Type.Literal('user')])),
39
+ task_class: Type.Optional(Type.String()),
40
+ follow_up: Type.Optional(Type.String()),
41
+ no_follow_up: Type.Optional(Type.Boolean()),
42
+ gate: Type.Optional(Type.Union([
43
+ Type.Object({ kind: Type.Literal('command'), command: Type.String() }),
44
+ Type.Object({ kind: Type.Literal('files'), paths: Type.Array(Type.String()) }),
45
+ Type.Object({ kind: Type.Literal('diagnostics'), files: Type.Array(Type.String()) }),
46
+ ])),
47
+ },
48
+ // 扩展字段全列 schema(与 conversation 件的 additionalProperties:false
49
+ // 第一道闸分立——本件即 goal 段第二道闸,段约束执法在执行段)
50
+ { additionalProperties: false })),
51
+ }, { additionalProperties: false }),
52
+ effect: 'read',
53
+ execute: async (args) => {
54
+ const raw = args.items;
55
+ if (!Array.isArray(raw))
56
+ throw new BaseError('GOAL_TODO_SCOPE', 'items 须为数组');
57
+ const scope = deps.getScope();
58
+ const anchor = nowMs();
59
+ const items = raw.map((entry) => normalizeItem(entry));
60
+ // 双向执法逐条过堂(先逐条校验再统一落账——半批不入 durable)
61
+ for (const item of items)
62
+ enforceScope(item, scope, deps, anchor);
63
+ deps.append({ items: items.map((item) => ({ ...item })) });
64
+ return { content: [{ type: 'text', text: receipt(items, scope) }] };
65
+ },
66
+ };
67
+ }
68
+ /** 载荷条目收窄(schema 已校验——此处产 GoalTodoItem 干净形:undefined 腿剔除) */
69
+ function normalizeItem(entry) {
70
+ if (typeof entry !== 'object' || entry === null) {
71
+ throw new BaseError('GOAL_TODO_SCOPE', '条目须为对象');
72
+ }
73
+ const c = entry;
74
+ // 可写局部(WritableGoalTodoItem——readonly 契约形由返回承载)
75
+ const item = { status: c.status, content: c.content };
76
+ if (typeof c.activeForm === 'string')
77
+ item.activeForm = c.activeForm;
78
+ if (typeof c.resume_when === 'string')
79
+ item.resumeWhen = c.resume_when;
80
+ if (c.role === 'agent' || c.role === 'user')
81
+ item.role = c.role;
82
+ if (typeof c.task_class === 'string')
83
+ item.taskClass = c.task_class;
84
+ if (typeof c.follow_up === 'string')
85
+ item.followUp = c.follow_up;
86
+ if (c.no_follow_up === true)
87
+ item.noFollowUp = true;
88
+ if (isRawGate(c.gate)) {
89
+ item.gate =
90
+ c.gate.kind === 'command'
91
+ ? { kind: 'command', command: c.gate.command }
92
+ : c.gate.kind === 'files'
93
+ ? { kind: 'files', paths: [...c.gate.paths] }
94
+ : { kind: 'diagnostics', files: [...c.gate.files] };
95
+ }
96
+ return item;
97
+ }
98
+ /** gate 声明形判(窄收窄——schema 已挡大半,防御读侧漂移) */
99
+ function isRawGate(value) {
100
+ if (typeof value !== 'object' || value === null)
101
+ return false;
102
+ const g = value;
103
+ if (g.kind === 'command' && typeof g.command === 'string')
104
+ return true;
105
+ if (g.kind === 'files' && Array.isArray(g.paths) && g.paths.every((p) => typeof p === 'string'))
106
+ return true;
107
+ if (g.kind === 'diagnostics' && Array.isArray(g.files) && g.files.every((f) => typeof f === 'string'))
108
+ return true;
109
+ return false;
110
+ }
111
+ /** 段约束执法(执行段机器面——坏输入一律 BaseError 响亮拒,半批不落账) */
112
+ function enforceScope(item, scope, deps, anchorMs) {
113
+ const extended = EXTENDED_FIELDS.filter((field) => item[field] !== undefined);
114
+ if (scope === null) {
115
+ if (extended.length > 0) {
116
+ throw new BaseError('GOAL_TODO_SCOPE', `当前不在 goal 段(无 active goal)——扩展字段不悬空:条目「${item.content}」携带 ${extended.join('/')}`);
117
+ }
118
+ return;
119
+ }
120
+ // goal 段内:deferred/completed 的必携纪律
121
+ if (item.status === 'deferred' && item.resumeWhen === undefined) {
122
+ throw new BaseError('GOAL_TODO_SCOPE', `deferred 条目「${item.content}」必携 resume_when(after@<ISO> 或 after@+<n>[mhd])`);
123
+ }
124
+ if (item.resumeWhen !== undefined) {
125
+ const parsed = parseResumeWhen(item.resumeWhen, anchorMs);
126
+ if (!parsed.ok)
127
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」resume_when 词法坏形:${parsed.error}`);
128
+ }
129
+ if (item.status === 'completed' && item.followUp === undefined && item.noFollowUp !== true) {
130
+ throw new BaseError('GOAL_TODO_SCOPE', `completed 条目「${item.content}」必携后继二择一(follow_up 或 no_follow_up: true)——防完成即失联`);
131
+ }
132
+ // gate 声明申报位 fail-closed(评测位 gates.ts 双拦);command 位三判序
133
+ // ——seam 在场(s 批补注①:缺席即拒,先于双位——批准也无用)→ 双位两档全拒
134
+ // (文案分档仅指引差——f-1 定形注③:not-approved 档给 /goal approve 指路)
135
+ if (item.gate !== undefined) {
136
+ if (item.gate.kind === 'command') {
137
+ if (!deps.hasCommandExec) {
138
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」command 判据门不可申报——exec 执行面缺席(组合根未接线,fail-closed 非静默跳过)`);
139
+ }
140
+ const status = deps.commandGateStatus(scope.goalId);
141
+ if (!status.allowed) {
142
+ throw new BaseError('GOAL_TODO_SCOPE', status.reason === 'not-approved'
143
+ ? `条目「${item.content}」command 判据门不可申报——goal 已申报 needsWrite 但未获人面批准(/goal approve ${scope.goalId} 批准后方可申报)`
144
+ : `条目「${item.content}」command 判据门不可申报——goal 未申报 needsWrite(防模型自造命令免审批自跑)`);
145
+ }
146
+ if (item.gate.command.trim().length === 0) {
147
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」command 判据门命令为空——空门即非门`);
148
+ }
149
+ }
150
+ if (item.gate.kind === 'diagnostics') {
151
+ if (!deps.hasLsp) {
152
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」diagnostics 判据门不可申报——lsp 诊断查询面缺席(fail-closed 非静默跳过)`);
153
+ }
154
+ if (item.gate.files.length === 0) {
155
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」diagnostics 判据门目标集为空——空门即非门`);
156
+ }
157
+ }
158
+ if (item.gate.kind === 'files' && item.gate.paths.length === 0) {
159
+ throw new BaseError('GOAL_TODO_SCOPE', `条目「${item.content}」files 判据门目标集为空——空门即非门`);
160
+ }
161
+ }
162
+ }
163
+ /** 回执文本(计数总览 + 段标记——模型可编程依赖的稳定词面) */
164
+ function receipt(items, scope) {
165
+ const counts = { pending: 0, 'in-progress': 0, completed: 0, deferred: 0 };
166
+ for (const item of items)
167
+ counts[item.status] += 1;
168
+ const head = `已更新任务清单:${items.length} 项` +
169
+ `(进行中 ${counts['in-progress']} · 待办 ${counts.pending} · ` +
170
+ `已完成 ${counts.completed} · 缓办 ${counts.deferred})`;
171
+ const gated = items.filter((item) => item.gate !== undefined).length;
172
+ const tail = scope === null ? '' : `〔goal ${scope.goalId} 段·判据门 ${gated} 项〕`;
173
+ return tail === '' ? head : `${head}${tail}`;
174
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * goal_update 工具件(03 §10.5 open 项完成否决律——模型面的终态申报通道)。
3
+ *
4
+ * 申报与验证分离在工具管道内闭环(schema → 守门 → 执行段机器否决)——
5
+ * 模型自报文本不再是完成唯一通道:completed 申报由服务层 fold open 项 +
6
+ * 判据门评测机器否决(GOAL_TRANSITION_INVALID 响亮回执列原因),工具件
7
+ * 只做通道与收尾渲染。
8
+ */
9
+ import { Type } from 'typebox';
10
+ import { BaseError } from '../contracts/index.js';
11
+ /** goal_update 工具件(终态申报——机器否决在服务层) */
12
+ export function createGoalUpdateTool(deps) {
13
+ return {
14
+ name: 'goal_update',
15
+ description: '申报 goal 终态。status=completed 必附 evidence(完成证据——机器会独立核验:' +
16
+ '任务清单不得有 open 项〔一切非 completed 项,含 deferred〕、判据门须全绿,' +
17
+ '不符即拒并回执原因);status=abandoned 可附 reason。当前会话的 active goal 作用。',
18
+ parameters: Type.Object({
19
+ status: Type.Union([Type.Literal('completed'), Type.Literal('abandoned')]),
20
+ evidence: Type.Optional(Type.String()),
21
+ reason: Type.Optional(Type.String()),
22
+ }, { additionalProperties: false }),
23
+ effect: 'read',
24
+ execute: async (args) => {
25
+ const status = args.status;
26
+ const active = deps.service.activeFor(deps.getSessionId());
27
+ if (active === undefined) {
28
+ throw new BaseError('GOAL_TRANSITION_INVALID', '当前会话无 active goal——无可申报终态');
29
+ }
30
+ const row = status === 'completed'
31
+ ? await deps.service.complete(active.id, typeof args.evidence === 'string' ? args.evidence : '')
32
+ : await deps.service.abandon(active.id, typeof args.reason === 'string' ? args.reason : undefined);
33
+ const note = row.endingNote ?? '';
34
+ return {
35
+ content: [
36
+ {
37
+ type: 'text',
38
+ text: status === 'completed'
39
+ ? `goal 已完成(机器核验通过:open 项清零 + 判据门全绿)。证据:${note}`
40
+ : `goal 已放弃。理由:${note}`,
41
+ },
42
+ ],
43
+ };
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * host/approval-cmd — 审批分档人面命令件(04 §9 定形块⑤ explain 人面 +
3
+ * ⑥ 预设打包——2026-09-11 审批分档批 ap-3)。`/approval` TUI 命令(assembly
4
+ * 直挂——与 /reload、/doors 同位,机制宿主有、不随插件换代卸除)。
5
+ *
6
+ * 四动词(无参 = status 缺省):
7
+ * - `status`——当前态:sandbox 档 + 审批 policy 各自值与来源(四层解析
8
+ * 胜者的标注面——装配根注入)+ 三档预设一览;
9
+ * - `entries`——策略表全列(**活体现读** tool-policy.json——与装配期快照
10
+ * 分立;含文件路径与命中序说明:deny 命中恒最先终局、allow 取首个);
11
+ * - `explain <tool> [pattern]`——真 adjudication 干跑:与守门行同一
12
+ * matchToolPolicy 纯函数(04 §9 ⑤定形),条目=活体现读、pattern=用户
13
+ * 给定实参模拟;只读零副作用;
14
+ * - `preset <名>`——预设写盘(settings.json 两键 + tool-policy.json 建议
15
+ * 集 append;半应用防护=坏形全拒;切换不清不删既有条目);成功尾经
16
+ * onPresetApplied 落 preset/applied 审计(恰一笔——包装方 warn 不阻塞)。
17
+ *
18
+ * 无 CLI 入口(07 §5:CLI 面只有 `--preset <名>` 逐次旗标——逐次生效不写
19
+ * 盘,与写盘动词生效语义分立,04 §9 ⑥ ap-3 定形补充)。
20
+ */
21
+ import { join, resolve as resolvePath } from 'node:path';
22
+ import { canonicalPath, FS_WRITE_TOOLS, matchToolPolicy, policyHitNote } from '../safety/index.js';
23
+ import { approvalPresetOf, APPROVAL_PRESETS, presetSuggestedEntries } from '../safety/index.js';
24
+ import { appendToolPolicyEntry, readToolPolicy, TOOL_POLICY_BASENAME } from './tool-policy-store.js';
25
+ import { writeHostSettings } from './settings-store.js';
26
+ /** 用法说明(TUI 命令描述位 + 解析错回执共用单源) */
27
+ export const APPROVAL_USAGE = `/approval [status] | entries | explain <tool> [pattern] | preset <名>
28
+ status 当前态:sandbox 档 + 审批 policy(值 + 来源)+ 三档预设一览
29
+ entries 策略表全列(活体现读 tool-policy.json——装配期快照外的当前真身)
30
+ explain <tool> [pattern] 真裁决干跑(守门行同一 matchToolPolicy):fs 族须带路径前缀参数、bash 须带命令原文、整名族三档并列
31
+ preset <名> 预设写盘(conservative|balanced|open——settings.json 两键 + 建议集 append;下次启动/新装配生效)
32
+ (CLI 面无本命令——「berry-agent run --preset <名>」为逐次生效不写盘)`;
33
+ /**
34
+ * TUI 面 argv 解析(零旗标面——本命令族无旗标)。无参 = status 缺省动词。
35
+ */
36
+ export function parseApprovalArgv(argv) {
37
+ const [verb, ...rest] = argv;
38
+ if (verb === undefined)
39
+ return { ok: true, sub: { sub: 'status' } };
40
+ if (verb.startsWith('--'))
41
+ return { ok: false, message: `本命令族无旗标。\n${APPROVAL_USAGE}` };
42
+ if (rest.some((tok) => tok.startsWith('--')))
43
+ return { ok: false, message: `本命令族无旗标。\n${APPROVAL_USAGE}` };
44
+ if (verb === 'status') {
45
+ if (rest.length > 0)
46
+ return { ok: false, message: `status 不收位置参数。\n${APPROVAL_USAGE}` };
47
+ return { ok: true, sub: { sub: 'status' } };
48
+ }
49
+ if (verb === 'entries') {
50
+ if (rest.length > 0)
51
+ return { ok: false, message: `entries 不收位置参数。\n${APPROVAL_USAGE}` };
52
+ return { ok: true, sub: { sub: 'entries' } };
53
+ }
54
+ if (verb === 'explain') {
55
+ if (rest.length < 1 || rest.length > 2 || rest[0] === '') {
56
+ return {
57
+ ok: false,
58
+ message: `explain 须带 <tool> 一至两参(fs 族须带路径前缀、bash 须带命令原文)。\n${APPROVAL_USAGE}`,
59
+ };
60
+ }
61
+ return {
62
+ ok: true,
63
+ sub: { sub: 'explain', tool: rest[0], ...(rest[1] !== undefined ? { pattern: rest[1] } : {}) },
64
+ };
65
+ }
66
+ if (verb === 'preset') {
67
+ if (rest.length !== 1 || rest[0] === '') {
68
+ return { ok: false, message: `preset 须带 <名> 一参数(conservative|balanced|open)。\n${APPROVAL_USAGE}` };
69
+ }
70
+ return { ok: true, sub: { sub: 'preset', name: rest[0] } };
71
+ }
72
+ return { ok: false, message: `未知子命令:${verb}(合法:status/entries/explain/preset)。\n${APPROVAL_USAGE}` };
73
+ }
74
+ /** 策略表条目单行渲染(entries 与 explain 命中行共用——六字段全列) */
75
+ function renderEntry(entry, index) {
76
+ const bits = [`tool=${entry.tool}`];
77
+ if (entry.pattern !== undefined)
78
+ bits.push(`pattern=${entry.pattern}`);
79
+ bits.push(`decision=${entry.decision}`);
80
+ if (entry.effect !== undefined)
81
+ bits.push(`effect=${entry.effect}`);
82
+ if (entry.reason !== undefined)
83
+ bits.push(`reason=${entry.reason}`);
84
+ if (entry.expiresAt !== undefined)
85
+ bits.push(`expiresAt=${new Date(entry.expiresAt).toISOString()}`);
86
+ return ` [${index}] ${bits.join(' ')}`;
87
+ }
88
+ /** status 渲染(当前态两旋钮 + 来源 + 三档预设一览——纯读零副作用) */
89
+ function renderStatus(deps) {
90
+ const lines = [];
91
+ lines.push('当前态(四层解析胜者——工具参数 > 会话策略 > CLI 旗标 > settings.json > 代码常量):');
92
+ lines.push(` sandbox 档 = ${deps.status.mode}(来源:${deps.status.modeSource})`);
93
+ lines.push(` 审批 policy = ${deps.status.policy}(来源:${deps.status.policySource})`);
94
+ lines.push(` (本面为装配期快照——/approval entries 与 explain 现读策略表文件〔活体〕)`);
95
+ lines.push('预设三档(preset <名> 写盘——下次启动/新装配生效,当前进程不变):');
96
+ for (const preset of APPROVAL_PRESETS) {
97
+ lines.push(` ${preset.name}: ${preset.description}`);
98
+ }
99
+ return { ok: true, text: lines.join('\n') };
100
+ }
101
+ /** entries 渲染(活体现读——装配期快照外的当前真身;含命中序说明) */
102
+ function renderEntries(deps) {
103
+ if (deps.dataDir === null) {
104
+ return { ok: true, text: '纯 memory 诊断形无数据目录——策略表缺席(空清单)。' };
105
+ }
106
+ const load = readToolPolicy(deps.dataDir);
107
+ const path = join(deps.dataDir, TOOL_POLICY_BASENAME);
108
+ if (!load.healthy) {
109
+ // 坏形真源随载(发现 #26):升格语境下真源可以是旧名 allowlist.json——
110
+ // 指错文件会把用户引去修一个不存在的新名文件
111
+ const badPath = load.unhealthyPath ?? path;
112
+ return { ok: false, text: `策略表文件级坏形(${badPath})——已降级视同空清单;手改修复前 preset 写动词同拒。` };
113
+ }
114
+ const lines = [];
115
+ lines.push(`策略表(活体现读 ${path}——装配期载入的是启动时快照,两时点可分立):`);
116
+ if (load.entries.length === 0) {
117
+ lines.push(' (空——无任何条目)');
118
+ }
119
+ else {
120
+ load.entries.forEach((entry, index) => lines.push(renderEntry(entry, index)));
121
+ }
122
+ lines.push('命中序:deny 命中恒最先且终局(硬拒,任何面不可翻转);无 deny 命中时取首个未过期 allow(免问)。');
123
+ return { ok: true, text: lines.join('\n') };
124
+ }
125
+ /**
126
+ * explain 干跑(04 §9 ⑤——真 adjudication 纯函数,与守门行同一
127
+ * matchToolPolicy + policyHitNote 三面同源):
128
+ * - fs 族(write/edit):只试 write 档 + pattern 参数必填(作为写目标
129
+ * canonical 路径模拟——相对则锚 workspace);
130
+ * - bash:只试 exec 档 + pattern 参数必填(作为命令原文——剥壳词干判定);
131
+ * - 其余整名族:read/write/exec 三档并列(无实参依赖,三档各自成行)。
132
+ * 条目 = 活体现读;只读零副作用。
133
+ */
134
+ function renderExplain(sub, deps) {
135
+ if (deps.dataDir === null) {
136
+ return { ok: true, text: '纯 memory 诊断形无数据目录——策略表缺席(空清单),无条目可判。' };
137
+ }
138
+ const load = readToolPolicy(deps.dataDir);
139
+ if (!load.healthy) {
140
+ // 坏形真源随载(发现 #26——同 renderEntries 注)
141
+ const badPath = load.unhealthyPath ?? join(deps.dataDir, TOOL_POLICY_BASENAME);
142
+ return { ok: false, text: `策略表文件级坏形(${badPath})——已降级视同空清单,干跑不可判。` };
143
+ }
144
+ const now = Date.now();
145
+ const workspaceRoot = deps.workspace();
146
+ const lines = [];
147
+ lines.push(`explain ${sub.tool}${sub.pattern !== undefined ? ` ${sub.pattern}` : ''}(活体干跑——条目现读 ${join(deps.dataDir, TOOL_POLICY_BASENAME)}):`);
148
+ // fs 族:pattern 必填(写目标路径模拟——canonical 化同守门行语义)
149
+ if (FS_WRITE_TOOLS.has(sub.tool)) {
150
+ if (sub.pattern === undefined) {
151
+ return {
152
+ ok: false,
153
+ text: `${sub.tool} 属 fs 写族——explain 须带路径前缀第二参(写目标模拟:/approval explain ${sub.tool} <路径>)。\n${APPROVAL_USAGE}`,
154
+ };
155
+ }
156
+ // 相对路径锚 workspace 后 canonical 化(macOS /var 符号链→/private/var
157
+ // 等陷阱——与守门行 extractWritePaths 产物的 canonical 绝对形同语义;
158
+ // canonicalPath 自带父目录递归解析,不存在路径亦得稳定绝对形)
159
+ const target = canonicalPath(resolvePath(workspaceRoot, sub.pattern));
160
+ const hit = matchToolPolicy(load.entries, { tool: sub.tool, effect: 'write', writePaths: [target], workspace: workspaceRoot }, now);
161
+ lines.push(hit === undefined
162
+ ? ` write 档(写目标 ${target}):无命中——照问照审(回落审批)`
163
+ : ` write 档(写目标 ${target}):命中 ${policyHitNote(hit)} → ${hit.entry.decision === 'deny' ? '硬拒(用户主权,任何面不可翻转)' : '免问放行(advisory——fence/可写根照走)'}`);
164
+ if (hit !== undefined)
165
+ lines.push(renderEntry(hit.entry, hit.index));
166
+ return { ok: true, text: lines.join('\n') };
167
+ }
168
+ // bash 族:pattern 必填(命令原文——剥壳词干判定)
169
+ if (sub.tool === 'bash') {
170
+ if (sub.pattern === undefined) {
171
+ return {
172
+ ok: false,
173
+ text: `bash explain 须带命令原文第二参(/approval explain bash <命令>)。\n${APPROVAL_USAGE}`,
174
+ };
175
+ }
176
+ const hit = matchToolPolicy(load.entries, { tool: 'bash', effect: 'exec', bashCommand: sub.pattern, workspace: workspaceRoot }, now);
177
+ lines.push(hit === undefined
178
+ ? ` exec 档(命令「${sub.pattern}」):无命中——照问照审(回落审批)`
179
+ : ` exec 档(命令「${sub.pattern}」):命中 ${policyHitNote(hit)} → ${hit.entry.decision === 'deny' ? '硬拒(用户主权,任何面不可翻转)' : '免问放行(advisory——confine/沙箱面照走)'}`);
180
+ if (hit !== undefined)
181
+ lines.push(renderEntry(hit.entry, hit.index));
182
+ return { ok: true, text: lines.join('\n') };
183
+ }
184
+ // 整名族:三档并列(pattern 忽略——整名匹配形条目不收实参)
185
+ if (sub.pattern !== undefined) {
186
+ lines.push(` (${sub.tool} 属整名族——第二参忽略〔条目命中不依赖调用实参〕)`);
187
+ }
188
+ let any = false;
189
+ for (const effect of ['read', 'write', 'exec']) {
190
+ const hit = matchToolPolicy(load.entries, { tool: sub.tool, effect, workspace: workspaceRoot }, now);
191
+ if (hit === undefined) {
192
+ lines.push(` ${effect} 档:无命中`);
193
+ continue;
194
+ }
195
+ any = true;
196
+ lines.push(` ${effect} 档:命中 ${policyHitNote(hit)} → ${hit.entry.decision === 'deny' ? '硬拒(用户主权,任何面不可翻转)' : '免问放行(advisory)'}`);
197
+ lines.push(renderEntry(hit.entry, hit.index));
198
+ }
199
+ if (!any)
200
+ lines.push(' (无任何命中——该工具照问照审)');
201
+ return { ok: true, text: lines.join('\n') };
202
+ }
203
+ /**
204
+ * preset 写盘(04 §9 ⑥——展开式非引用式;序 = 预设名解析 → 策略表健康前置
205
+ * 〔半应用防护:坏形全拒,settings 不写〕 → settings.json 两键合并写 →
206
+ * 建议集逐条经写侧唯一正门 append〔幂等去重〕 → 审计恰一笔 → 诚实回执)。
207
+ */
208
+ function runPreset(name, deps) {
209
+ const preset = approvalPresetOf(name);
210
+ if (preset === undefined) {
211
+ return {
212
+ ok: false,
213
+ text: `未知预设名:${name}(合法:${APPROVAL_PRESETS.map((p) => p.name).join('|')})。\n${APPROVAL_USAGE}`,
214
+ };
215
+ }
216
+ if (deps.dataDir === null) {
217
+ return { ok: false, text: '纯 memory 诊断形无数据目录——preset 写动词不可用(无落点)。' };
218
+ }
219
+ // 半应用防护第一闸:策略表坏形期全拒(档位与条目两文件原子性不可得,宁全
220
+ // 拒不留半应用态——04 §9 ⑥ ap-3 定形补充)
221
+ const load = readToolPolicy(deps.dataDir);
222
+ if (!load.healthy) {
223
+ // 坏形真源随载(发现 #26——同 renderEntries 注)
224
+ const badPath = load.unhealthyPath ?? join(deps.dataDir, TOOL_POLICY_BASENAME);
225
+ return {
226
+ ok: false,
227
+ text: `策略表文件级坏形(${badPath})——preset 全拒(防半应用态:不写 settings.json 亦不 append 条目);手改修复后重试。`,
228
+ };
229
+ }
230
+ // 两旋钮合并写(保留未知键——用户手编面不因预设切换损毁)
231
+ const write = writeHostSettings(deps.dataDir, {
232
+ sandboxMode: preset.sandboxMode,
233
+ approvalPolicy: preset.approvalPolicy,
234
+ });
235
+ if (write === 'rejected') {
236
+ return { ok: false, text: 'settings.json 坏形期拒写——preset 全败(手改修复后重试)。' };
237
+ }
238
+ // 建议集展开 append(写侧唯一正门——幂等去重;conservative/balanced 空集零写入)
239
+ const drafts = presetSuggestedEntries(preset.name, deps.workspace());
240
+ let appended = 0;
241
+ for (const draft of drafts) {
242
+ const result = appendToolPolicyEntry(deps.dataDir, draft);
243
+ if (result === 'appended')
244
+ appended += 1;
245
+ if (result === 'rejected') {
246
+ return {
247
+ ok: false,
248
+ text: `条目追加被拒(策略表坏形期拒写)——settings.json 两键已写(${preset.sandboxMode}/${preset.approvalPolicy})、条目追加 ${appended} 条后中止;手改修复 tool-policy.json 后重试补齐。`,
249
+ };
250
+ }
251
+ }
252
+ // 审计恰一笔(成功尾——包装方 warn 不阻塞)
253
+ deps.onPresetApplied?.(preset.name, preset.sandboxMode, preset.approvalPolicy, appended);
254
+ const lines = [];
255
+ lines.push(`已切换预设:${preset.name}——${preset.description}`);
256
+ lines.push(` settings.json ← sandboxMode=${preset.sandboxMode} + approvalPolicy=${preset.approvalPolicy}(合并写,未知键保留)`);
257
+ lines.push(appended > 0
258
+ ? ` tool-policy.json ← 建议集追加 ${appended} 条(幂等去重——重复条目不二写)`
259
+ : ' tool-policy.json ← 建议集空,零写入');
260
+ lines.push(' 切换不清不删既有条目(含从 open 切回——撤除建议条目归手删);既有会话粘性与在身审批不受影响。');
261
+ lines.push('生效时点(诚实):sandbox 档与策略表均装配期快照——当前进程不变,下次启动/新装配生效;/approval entries 与 explain 现读文件(活体)即时可见。');
262
+ return { ok: true, text: lines.join('\n') };
263
+ }
264
+ /** `/approval` 命令面主入口(sub → 结算形;写动词失败零审计) */
265
+ export function runApprovalCommand(sub, deps) {
266
+ switch (sub.sub) {
267
+ case 'status':
268
+ return renderStatus(deps);
269
+ case 'entries':
270
+ return renderEntries(deps);
271
+ case 'explain':
272
+ return renderExplain(sub, deps);
273
+ case 'preset':
274
+ return runPreset(sub.name, deps);
275
+ }
276
+ }