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,120 @@
1
+ /**
2
+ * 子进程登记簿(04 §11:全部 spawn 子进程入册;写面容错;孤儿清扫)。
3
+ *
4
+ * 三语义:
5
+ * - **写面容错**:登记/出册的持久化失败降 warn 不抛——「登记丢失不损执行
6
+ * 正确性」,杀树退化为已知 pid 集兜底;持久化走 tmp+rename 原子写(半写
7
+ * 文件不进正身)。
8
+ * - **孤儿清扫**(启动期一次):读残留册,上代宿主已死(entry.hostPid 判
9
+ * 活)→ 验命令行(pid 复用防线——命令行与登记 argv 不符即跳过,杀错无
10
+ * 辜进程比漏杀孤儿更糟)→ 树杀 → 出册。
11
+ * - **stale 判定**:清扫只认「宿主已死」的条目;宿主仍活的条目是并发实例
12
+ * 的活账,不动。
13
+ */
14
+ import { rename, readFile, writeFile } from 'node:fs/promises';
15
+ import { createLogger } from '../context/index.js';
16
+ /**
17
+ * 建子进程登记簿。内存 Map 是唯一真相源,文件是崩溃恢复账——写路径全容错
18
+ * (任何一步失败 warn 后照常返回,调用方无感)。
19
+ */
20
+ export function createProcessRegistry(options = {}) {
21
+ const logger = options.logger ?? createLogger('exec');
22
+ const entries = new Map();
23
+ /** 持久化串行链(写-写竞态防线——见 persist 注记) */
24
+ let persistChain = Promise.resolve();
25
+ // 持久化(tmp+rename 原子写;全链容错——04 §11「写面容错」条款)。
26
+ // 串行化关键:多次 add/remove 各自 fire-and-forget 会并发争用同一 tmp 路径
27
+ // (先到 rename 可吞后到写入——终态倒退成旧快照);承诺链按入队序逐笔落盘,
28
+ // 末笔即最新快照,文件终态收敛
29
+ const persist = (snapshot) => {
30
+ if (options.filePath === undefined)
31
+ return;
32
+ const filePath = options.filePath;
33
+ const tmp = `${filePath}.tmp`;
34
+ persistChain = persistChain.then(async () => {
35
+ try {
36
+ await writeFile(tmp, `${JSON.stringify(snapshot)}\n`, 'utf8');
37
+ await rename(tmp, filePath);
38
+ }
39
+ catch (error) {
40
+ logger.warn('登记簿持久化失败(容错降级——内存账仍准确)', {
41
+ filePath,
42
+ error: error instanceof Error ? error.message : String(error),
43
+ });
44
+ }
45
+ });
46
+ };
47
+ return {
48
+ add(entry) {
49
+ entries.set(entry.pid, entry);
50
+ persist([...entries.values()]);
51
+ },
52
+ remove(pid) {
53
+ // 幂等出册(结算可能后于清扫——缺席即无事)
54
+ if (!entries.delete(pid))
55
+ return;
56
+ persist([...entries.values()]);
57
+ },
58
+ list() {
59
+ return [...entries.values()];
60
+ },
61
+ };
62
+ }
63
+ /**
64
+ * 启动期孤儿清扫(04 §11:扫残留册、hostPid 判活、验命令行、树杀)。
65
+ * 读文件失败(无文件/坏 JSON)= 空册起步 + warn——清扫是恢复面不是启动门槛。
66
+ *
67
+ * 先把残留条目回填进内存账再逐条裁——直接 remove 未回填条目会把内存快照
68
+ * (空)持久化、误抹并发活实例的账本。
69
+ * @param deps 判活/命令行/树杀依赖面(注入桩可测;装配批接真实现)
70
+ */
71
+ export async function sweepOrphans(registry, options, deps) {
72
+ const logger = options.logger ?? createLogger('exec');
73
+ const hostPid = options.hostPid ?? process.pid;
74
+ let persisted = [];
75
+ if (options.filePath !== undefined) {
76
+ try {
77
+ const raw = await readFile(options.filePath, 'utf8');
78
+ const parsed = JSON.parse(raw);
79
+ if (Array.isArray(parsed))
80
+ persisted = parsed;
81
+ }
82
+ catch {
83
+ logger.warn('登记簿文件缺失/坏形——空册起步(清扫恢复面非启动门槛)', {
84
+ filePath: options.filePath,
85
+ });
86
+ return { killed: 0, skipped: 0, removed: 0 };
87
+ }
88
+ }
89
+ // 残留条目回填内存账(stale 条目随裁剪出册——持久化走内存快照单源)
90
+ for (const entry of persisted)
91
+ registry.add(entry);
92
+ let killed = 0;
93
+ let skipped = 0;
94
+ let removed = 0;
95
+ // 裁剪对象 = 回填后的整本账(内存既有 + 文件残留——启动期内存通常为空,
96
+ // 但纯内存腿调用〔无 filePath〕同样可扫)
97
+ for (const entry of registry.list()) {
98
+ // 宿主仍活:并发实例的活账,不动(stale 判定——只清上代遗产)
99
+ if (entry.hostPid === hostPid || deps.isAlive(entry.hostPid))
100
+ continue;
101
+ // 孤儿谱系:进程本身已死则纯出册;活着则验命令行再树杀(pid 复用防线)
102
+ if (deps.isAlive(entry.pid)) {
103
+ const cmdline = deps.readCmdline(entry.pid);
104
+ const expected = entry.argv.join(' ');
105
+ if (cmdline === null || !cmdline.includes(expected)) {
106
+ skipped += 1;
107
+ logger.warn('孤儿命令行不符——pid 复用嫌疑跳过树杀(宁漏杀孤儿不误杀无辜)', {
108
+ pid: entry.pid,
109
+ expected,
110
+ });
111
+ continue;
112
+ }
113
+ deps.killTree(entry.pid);
114
+ killed += 1;
115
+ }
116
+ registry.remove(entry.pid);
117
+ removed += 1;
118
+ }
119
+ return { killed, skipped, removed };
120
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * spawn 管道真身(04 §11 六条款的执行体:失败二分/进程组树杀/超时归因/
3
+ * 输出保尾/env 白名单/子进程登记)。
4
+ *
5
+ * 编排单入口 run():空 argv 与预中止两防御 → spawn(detached 独立进程组)
6
+ * → 登记入册 → 三源竞速(自然退出/超时/打断——先到者落归因即封,其余源
7
+ * 结算即摘监听)→ close 结算保尾产出。spawn 'error'(进程从未存在)走
8
+ * BaseError EXEC_SPAWN_FAILED 抛出面——与 exitCode ≠ 0(执行阶段失败,正常
9
+ * 结算进 result)构成失败二分。
10
+ *
11
+ * 树杀纪律:超时/打断即进程组树杀(SIGKILL 直杀不递进 TERM——预算已燃尽,
12
+ * 可捕获信号的优雅退出窗会拉长超时尾;组杀失败〔组不在〕降杀进程本体)。
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ import { readFileSync } from 'node:fs';
16
+ import { execSync } from 'node:child_process';
17
+ import { BaseError } from '../contracts/index.js';
18
+ import { createLogger } from '../context/index.js';
19
+ import { buildChildEnv } from './env.js';
20
+ import { createProcessRegistry } from './registry.js';
21
+ import { OutputTail } from './tail.js';
22
+ /**
23
+ * 建 spawn 管道(登记簿随建——宿主启动期孤儿清扫消费 pipeline.registry)。
24
+ */
25
+ export function createSpawnPipeline(options = {}) {
26
+ const logger = options.logger ?? createLogger('exec');
27
+ const now = options.now ?? Date.now;
28
+ const hostEnv = options.hostEnv ?? process.env;
29
+ const hostPid = options.hostPid ?? process.pid;
30
+ const registry = createProcessRegistry(options);
31
+ const deps = {
32
+ logger,
33
+ now,
34
+ hostEnv,
35
+ hostPid,
36
+ registry,
37
+ resolveEnvRef: options.resolveEnvRef,
38
+ };
39
+ return {
40
+ registry,
41
+ run: (request) => runSpawn(request, deps),
42
+ spawnInteractive: (request) => spawnInteractiveChild(request, deps),
43
+ };
44
+ }
45
+ /**
46
+ * 长存活双工子进程真身(三桥 stdio 桥——MCP/LSP 的 spawn 面)。
47
+ *
48
+ * 与 run 的分野:run 跑完即结算(三源竞速 + 保尾产出);本面桥侧独占
49
+ * stdin/stdout 双工(协议帧往返)、进程退出经 onExit 一次送达(close 语义
50
+ * ——stdio 全闭;spawn 'error'〔进程从未存在〕折 spawnError 位送达,失败
51
+ * 二分信息不丢)。同律不变项:detached 独立进程组(树杀组锚)、env 白名单
52
+ * deny-by-default、登记簿同册(spawn 入册 / close 出册——幂等闸防 error 与
53
+ * close 双计双删)。
54
+ */
55
+ function spawnInteractiveChild(request, deps) {
56
+ const argv0 = request.argv[0];
57
+ if (argv0 === undefined || argv0 === '') {
58
+ throw new BaseError('EXEC_SPAWN_FAILED', 'argv 为空——无可执行(spawn 阶段失败前置防御)');
59
+ }
60
+ const child = spawn(argv0, request.argv.slice(1), {
61
+ cwd: request.cwd,
62
+ // 独立进程组(posix)——树杀的组锚;win32 语义为 CREATE_NEW_PROCESS_GROUP
63
+ detached: true,
64
+ // 三桥双工面:stdin 必须管道(协议帧写出);run 管道的 'ignore' 缺省不适用
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ // 引用形展开唯一执法点(注入腿 c-4——两入口同点汇入)
67
+ env: buildChildEnv(request.env, deps.hostEnv, deps.resolveEnvRef),
68
+ });
69
+ if (child.pid !== undefined) {
70
+ deps.registry.add({
71
+ pid: child.pid,
72
+ argv: request.argv,
73
+ owner: request.owner,
74
+ hostPid: deps.hostPid,
75
+ startedAt: deps.now(),
76
+ });
77
+ }
78
+ // 退出一次结算(error/close 竞速同封——先到者送达,迟到源 no-op)
79
+ let exited = false;
80
+ let lastInfo;
81
+ const exitCallbacks = [];
82
+ const finish = (info) => {
83
+ if (exited)
84
+ return;
85
+ exited = true;
86
+ lastInfo = info;
87
+ if (child.pid !== undefined)
88
+ deps.registry.remove(child.pid);
89
+ for (const cb of [...exitCallbacks])
90
+ cb(info);
91
+ };
92
+ // 失败二分前者:error 事件 = spawn 阶段失败(进程从未存在——ENOENT/EACCES);
93
+ // 结算后的迟到 error(stdio EPIPE 类)不改写已封事实
94
+ child.on('error', (error) => finish({ code: null, spawnError: error }));
95
+ // close(非 exit)——stdio 全闭才是协议桥的终点事件
96
+ child.on('close', (code) => finish({ code }));
97
+ return {
98
+ pid: child.pid,
99
+ argv: request.argv,
100
+ // stdio 三管道位已定形——此处恒非 null(类型面收窄)
101
+ stdin: child.stdin,
102
+ stdout: child.stdout,
103
+ stderr: child.stderr,
104
+ onExit(callback) {
105
+ // 已退出:即回调(迟到订阅不丢事件);未退:入队
106
+ if (exited)
107
+ callback(lastInfo);
108
+ else
109
+ exitCallbacks.push(callback);
110
+ },
111
+ kill() {
112
+ // 幂等(已退出 no-op);树杀组锚 = pid(子未成组长降杀本体——killProcessTree 内处理)
113
+ if (!exited && child.pid !== undefined)
114
+ killProcessTree(child.pid, deps.logger);
115
+ },
116
+ };
117
+ }
118
+ /** 单次执行真身(run 入口的自由函数形——闭包依赖束显式传) */
119
+ async function runSpawn(request, deps) {
120
+ const argv0 = request.argv[0];
121
+ if (argv0 === undefined || argv0 === '') {
122
+ throw new BaseError('EXEC_SPAWN_FAILED', 'argv 为空——无可执行(spawn 阶段失败前置防御)');
123
+ }
124
+ // 预中止请求:不造进程直接结算(打断先到——竞速的时序前移态)
125
+ if (request.signal?.aborted) {
126
+ return {
127
+ outcome: 'abort',
128
+ exitCode: null,
129
+ stdout: '',
130
+ stderr: '',
131
+ truncated: false,
132
+ bytes: 0,
133
+ durationMs: 0,
134
+ };
135
+ }
136
+ const start = deps.now();
137
+ const child = spawn(argv0, request.argv.slice(1), {
138
+ cwd: request.cwd,
139
+ // 独立进程组(posix)——树杀的组锚;win32 语义为 CREATE_NEW_PROCESS_GROUP
140
+ detached: true,
141
+ // 04 §8 无 stdin:bash 工具面不喂输入;后续桥件自定 stdio 时不走本管道缺省
142
+ stdio: ['ignore', 'pipe', 'pipe'],
143
+ // 引用形展开唯一执法点(注入腿 c-4——两入口同点汇入)
144
+ env: buildChildEnv(request.env, deps.hostEnv, deps.resolveEnvRef),
145
+ });
146
+ if (child.pid !== undefined) {
147
+ deps.registry.add({
148
+ pid: child.pid,
149
+ argv: request.argv,
150
+ owner: request.owner ?? 'exec',
151
+ hostPid: deps.hostPid,
152
+ startedAt: deps.now(),
153
+ });
154
+ }
155
+ return await new Promise((resolve, reject) => {
156
+ const tail = new OutputTail();
157
+ let settled = false;
158
+ let outcome = 'exit';
159
+ let timer;
160
+ let onAbort;
161
+ // 摘源(先到者结算即封后,其余源到达即摘——timer 清除 + abort 摘监听)
162
+ const detachSources = () => {
163
+ if (timer !== undefined) {
164
+ clearTimeout(timer);
165
+ timer = undefined;
166
+ }
167
+ if (onAbort !== undefined && request.signal !== undefined) {
168
+ request.signal.removeEventListener('abort', onAbort);
169
+ onAbort = undefined;
170
+ }
171
+ };
172
+ const settle = (o) => {
173
+ if (settled)
174
+ return;
175
+ settled = true;
176
+ outcome = o;
177
+ detachSources();
178
+ // 超时/打断即树杀(close 随后到达完成结算——归因已封不改写)
179
+ if (o !== 'exit' && child.pid !== undefined)
180
+ killProcessTree(child.pid, deps.logger);
181
+ };
182
+ child.stdout?.on('data', (chunk) => tail.append('stdout', chunk));
183
+ child.stderr?.on('data', (chunk) => tail.append('stderr', chunk));
184
+ // 失败二分前者:error 事件 = spawn 阶段失败(进程从未存在——ENOENT/EACCES);
185
+ // 结算后的迟到 error(stdio EPIPE 类)不改写已封归因
186
+ child.on('error', (error) => {
187
+ if (settled)
188
+ return;
189
+ settled = true;
190
+ detachSources();
191
+ if (child.pid !== undefined)
192
+ deps.registry.remove(child.pid);
193
+ reject(new BaseError('EXEC_SPAWN_FAILED', `spawn 失败(进程从未存在):${request.argv.join(' ')}——${error.message}`, {
194
+ cause: error,
195
+ }));
196
+ });
197
+ if (request.timeoutMs !== undefined && request.timeoutMs > 0) {
198
+ timer = setTimeout(() => settle('timeout'), request.timeoutMs);
199
+ }
200
+ if (request.signal !== undefined) {
201
+ onAbort = () => settle('abort');
202
+ request.signal.addEventListener('abort', onAbort);
203
+ }
204
+ // close(非 exit)——stdio 全闭才是输出终值点;自然退出也经 settle 收编
205
+ // (已封则 no-op,归因保留先到者)
206
+ child.on('close', (code) => {
207
+ settle('exit');
208
+ detachSources();
209
+ if (child.pid !== undefined)
210
+ deps.registry.remove(child.pid);
211
+ const finished = tail.finish();
212
+ resolve({
213
+ outcome,
214
+ exitCode: outcome === 'exit' ? (code ?? child.exitCode ?? null) : null,
215
+ ...finished,
216
+ durationMs: deps.now() - start,
217
+ });
218
+ });
219
+ });
220
+ }
221
+ /* ------------------------------------------------------------------ */
222
+ /* 平台真身(孤儿清扫 SweepDeps 与树杀的平台实现——装配/测试注入桩替身可测) */
223
+ /* ------------------------------------------------------------------ */
224
+ /**
225
+ * 进程组树杀(04 §11):posix 负 pid 组杀(SIGKILL 直杀——预算已燃尽不递进
226
+ * TERM,可捕获信号会拉长超时尾);组不在(子未成组长)降杀本体。win32 走
227
+ * taskkill /T /F(树形递归强杀)。
228
+ */
229
+ export function killProcessTree(pid, logger = createLogger('exec')) {
230
+ if (process.platform === 'win32') {
231
+ const killer = spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
232
+ killer.on('error', (error) => logger.warn('taskkill 树杀失败', { pid, error: error.message }));
233
+ return;
234
+ }
235
+ try {
236
+ process.kill(-pid, 'SIGKILL');
237
+ }
238
+ catch {
239
+ try {
240
+ process.kill(pid, 'SIGKILL');
241
+ }
242
+ catch {
243
+ // 本体也已不在——树杀幂等语义,静默(close 随后到达)
244
+ }
245
+ }
246
+ }
247
+ /** pid 判活(posix 惯例 kill(pid, 0);EPERM = 存在但非本主——仍算活) */
248
+ export function isPidAlive(pid) {
249
+ try {
250
+ process.kill(pid, 0);
251
+ return true;
252
+ }
253
+ catch (error) {
254
+ return error.code === 'EPERM';
255
+ }
256
+ }
257
+ /**
258
+ * 读进程命令行(pid 复用防线比对源):linux 走 /proc/<pid>/cmdline(NUL 分隔
259
+ * argv),其余平台走 ps -o command=(同步一次性——仅宿主启动期清扫用,不进
260
+ * 热路径)。读不到返回 null(清扫面视为不匹配跳过)。
261
+ */
262
+ export function readCmdlineSync(pid) {
263
+ if (process.platform === 'linux') {
264
+ try {
265
+ const raw = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
266
+ return raw.split('\0').filter(Boolean).join(' ').trim();
267
+ }
268
+ catch {
269
+ return null;
270
+ }
271
+ }
272
+ try {
273
+ return execSync(`ps -o command= -p ${pid}`, {
274
+ encoding: 'utf8',
275
+ stdio: ['ignore', 'pipe', 'ignore'],
276
+ }).trim();
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 输出保尾器(04 §11:stdout/stderr 合计截尾保后半 60KiB)。
3
+ *
4
+ * 合计帽语义:两流按到达序共享同一字节预算——超预算时弃头部保尾部(尾部
5
+ * 才有失败现场:栈迹/错误摘要在尾)。实现 = 到达序分块账本(每块记来源流
6
+ * 与字节),超帽先整块弃最旧,仅剩单块仍超帽时劈尾保后半。
7
+ *
8
+ * 解码纪律:UTF-8 增量宽容解码(decoder.decode(part, {stream:true})——分块
9
+ * 边界不劈多字节字符;fatal:false 替换符)。与 fs 读的 strict 拒收分立:
10
+ * fs 读是数据面(mojibake 进上下文污染模型观察),进程输出是诊断面(二进
11
+ * 制命令〔cat file.bin〕的输出须能以可读形态回执,不能整单拒收)。
12
+ */
13
+ import { OUTPUT_TAIL_BYTES } from './types.js';
14
+ /** 合计保尾器(构造帽缺省 OUTPUT_TAIL_BYTES;帽 0 = 全弃仍记账) */
15
+ export class OutputTail {
16
+ cap;
17
+ chunks = [];
18
+ totalBytes = 0;
19
+ droppedBytes = 0;
20
+ constructor(cap = OUTPUT_TAIL_BYTES) {
21
+ this.cap = cap;
22
+ }
23
+ /** 追加一块输出(到达序);随即收缩——账本在任意时刻都满足保尾不变式 */
24
+ append(stream, bytes) {
25
+ if (bytes.byteLength === 0 || this.cap === 0) {
26
+ // 帽 0 形:不记账本只记体量(全弃仍是真实输出量)
27
+ this.droppedBytes += bytes.byteLength;
28
+ return;
29
+ }
30
+ this.chunks.push({ stream, bytes });
31
+ this.totalBytes += bytes.byteLength;
32
+ // 整块弃最旧(块级粒度:read() 边界的块不必劈——留最后一块保证有现场)
33
+ while (this.chunks.length > 1 && this.totalBytes > this.cap) {
34
+ const oldest = this.chunks.shift();
35
+ if (oldest === undefined)
36
+ break;
37
+ this.totalBytes -= oldest.bytes.byteLength;
38
+ this.droppedBytes += oldest.bytes.byteLength;
39
+ }
40
+ // 仅剩单块仍超帽:劈尾保后半(单块 100KiB 帽 60KiB 形)
41
+ const only = this.chunks[0];
42
+ if (only !== undefined && this.totalBytes > this.cap) {
43
+ const drop = this.totalBytes - this.cap;
44
+ this.chunks[0] = { stream: only.stream, bytes: only.bytes.subarray(drop) };
45
+ this.totalBytes = this.cap;
46
+ this.droppedBytes += drop;
47
+ }
48
+ }
49
+ /** 结算:按流增量解码归并(分块边界不劈多字节字符) */
50
+ finish() {
51
+ // 每流一个增量解码器 + 累积面——decode(part, {stream:true}) 的返回值是本批
52
+ // 已解码文本(解码器内部只留存尾部未完序列不存产出),丢弃返回值 = 丢文本
53
+ const decoders = {
54
+ stdout: new TextDecoder('utf-8'),
55
+ stderr: new TextDecoder('utf-8'),
56
+ };
57
+ let stdout = '';
58
+ let stderr = '';
59
+ for (const chunk of this.chunks) {
60
+ const text = decoders[chunk.stream].decode(chunk.bytes, { stream: true });
61
+ if (chunk.stream === 'stdout')
62
+ stdout += text;
63
+ else
64
+ stderr += text;
65
+ }
66
+ // 终了 flush:补出留存在解码器内的尾部未完序列(替换符或空)
67
+ stdout += decoders.stdout.decode();
68
+ stderr += decoders.stderr.decode();
69
+ // 实收总量含已丢弃——披露真实体量(「输出被截了多少」模型可自知)
70
+ return {
71
+ stdout,
72
+ stderr,
73
+ truncated: this.droppedBytes > 0,
74
+ bytes: this.totalBytes + this.droppedBytes,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,5 @@
1
+ /** 输出保尾预算(04 §11:stdout/stderr 合计 60KiB 截尾保后半——尾部才有失败现场) */
2
+ export const OUTPUT_TAIL_BYTES = 60 * 1024;
3
+ /** bash 工具超时缺省与上限(04 §8:缺省 120s、上限 600s) */
4
+ export const BASH_TIMEOUT_DEFAULT_MS = 120_000;
5
+ export const BASH_TIMEOUT_MAX_MS = 600_000;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * goal 域错误码注册(03 §10.5 + 04 §12——GOAL_ 前缀族;02 §5.3 批 15b
3
+ * 明列四码,前两码承 berry 同名码〔03 §10 章头指认〕、后两码本仓新立)。
4
+ *
5
+ * 码语义分层:GOAL_TRANSITION_INVALID 管状态迁转机器否决(完成否决律/
6
+ * 撞席守卫/挂钟注册失败回执同折);GOAL_TODO_SCOPE 管 todo/write 段约束
7
+ * 双向执法(goal 段缺字段/坏词法、非段申报扩字段、gate 申报位 fail-closed);
8
+ * GOAL_NOT_FOUND 管幽灵 goalId;GOAL_GOAL_INVALID 管行载荷词法。
9
+ * 本文件由模块公开面 index.ts 引入(注册纪律:import 发生才注册)。
10
+ */
11
+ import { registerErrorCodes } from '../contracts/index.js';
12
+ registerErrorCodes([
13
+ {
14
+ code: 'GOAL_TRANSITION_INVALID',
15
+ module: 'goal',
16
+ description: 'goal 状态迁转机器否决——goal_update 终态 completed 必附 evidence 且 open 项在/携 gate 项未全绿即拒(响亮回执列 open 项)、同会话既有 active goal 撞席拒、GoalJobsFace register 坏串回执同折、/goal approve 批准无对象〔未申报 needsWrite 或终态行——f-1 批 03 §10.5 定形注〕',
17
+ },
18
+ {
19
+ code: 'GOAL_TODO_SCOPE',
20
+ module: 'goal',
21
+ description: 'todo/write 段约束双向执法——goal 段内 deferred 缺 resume_when、completed 缺后继二择一(follow_up/noFollowUp)、resume_when 词法坏形拒;非 goal 段申报扩字段亦拒;gate 声明申报位 fail-closed(command 未过 needsWrite 申报+人面批准、diagnostics 缺 lsp 面)',
22
+ },
23
+ {
24
+ code: 'GOAL_NOT_FOUND',
25
+ module: 'goal',
26
+ description: '幽灵 goalId 零行守卫——update/complete/abandon/wake/approve 作用行缺席响亮拒',
27
+ },
28
+ {
29
+ code: 'GOAL_GOAL_INVALID',
30
+ module: 'goal',
31
+ description: 'goal 行载荷坏——objective 空/超 16KiB',
32
+ },
33
+ ]);