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,171 @@
1
+ /**
2
+ * 任务执行 runner(04 §12:任务执行入口 = `berry-agent run --read-only`
3
+ * ——headless 无应答者、审批天然 fail-closed〔§9 无静默审批单发形〕)。
4
+ *
5
+ * 两层:RunnerFactory 接缝(引擎只认接缝——spawn 编舞装配可换、测试注假件)
6
+ * + 进程实装(node:child_process spawn、argv = `run --read-only --tick <名>`
7
+ * ——子进程按名读行自跑 prompt;07 §5 --tick 旗标 = 触发载体、--read-only =
8
+ * sandboxMode 只读单发)。
9
+ *
10
+ * kill 语义(pi-tick createRunControls 同律):kill(reason) 幂等——首杀
11
+ * SIGTERM、宽限后 SIGKILL 升级;调用方传的 reason 原样进 settled 结局。
12
+ * settled 永不 reject:一切收场(正常/信号/spawn 失败/被杀)都归 RunOutcome。
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ /** 结局预览截断(pi-tick TRANSCRIPT_FINAL_TEXT_PREVIEW 同值) */
16
+ const PREVIEW_CAP = 200;
17
+ /** 输出尾窗(字节——只保尾部供预览,防长输出吃内存) */
18
+ const OUTPUT_TAIL_BYTES = 4096;
19
+ /**
20
+ * 进程 runner 工厂实装。
21
+ *
22
+ * stdout 取尾窗作 finalTextPreview(run --output-format text 缺省只出末条
23
+ * assistant 文本——07 §5;尾窗即足);stderr 尾窗作错误摘要源。
24
+ */
25
+ export function createProcessRunnerFactory(options = {}) {
26
+ const command = options.command ?? 'berry-agent';
27
+ const buildArgv = options.buildArgv ?? ((row) => ['run', '--read-only', '--tick', row.name]);
28
+ const spawnFn = options.spawnFn ?? spawn;
29
+ const killGraceMs = options.killGraceMs ?? 5_000;
30
+ const env = options.env ?? {};
31
+ const now = options.now ?? (() => new Date().toISOString());
32
+ return {
33
+ async spawn(req) {
34
+ const startedAtMs = Date.now();
35
+ let child;
36
+ try {
37
+ child = spawnFn(command, buildArgv(req.row, req.trigger), {
38
+ cwd: req.row.cwd ?? undefined,
39
+ env,
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ // 独立进程组——kill 信号达子进程整树(exec spawn 管道同律)
42
+ detached: true,
43
+ });
44
+ }
45
+ catch (err) {
46
+ // spawn 同步抛(可执行不存在等)——进程从未存在,直接归 spawn 结局
47
+ const outcome = {
48
+ trigger: req.trigger,
49
+ reason: 'spawn',
50
+ error: describeError(err),
51
+ finishedAt: now(),
52
+ };
53
+ return { pid: null, kill: () => { }, settled: Promise.resolve(outcome) };
54
+ }
55
+ let stdoutTail = '';
56
+ let stderrTail = '';
57
+ let settledResolve;
58
+ const settled = new Promise((resolve) => {
59
+ settledResolve = resolve;
60
+ });
61
+ let killedReason = null;
62
+ let escalated = false;
63
+ const appendTail = (current, chunk) => (current + chunk).slice(-OUTPUT_TAIL_BYTES);
64
+ child.stdout?.on('data', (chunk) => {
65
+ stdoutTail = appendTail(stdoutTail, chunk.toString('utf8'));
66
+ });
67
+ child.stderr?.on('data', (chunk) => {
68
+ stderrTail = appendTail(stderrTail, chunk.toString('utf8'));
69
+ });
70
+ const handle = {
71
+ pid: child.pid ?? null,
72
+ kill(reason) {
73
+ if (killedReason !== null || settledResolve === undefined)
74
+ return; // 幂等
75
+ killedReason = reason;
76
+ signalTree(child, 'SIGTERM');
77
+ // 宽限后 KILL 升级(未收场才升——close 事件先到则定时器空放无害)
78
+ setTimeout(() => {
79
+ if (!escalated) {
80
+ escalated = true;
81
+ signalTree(child, 'SIGKILL');
82
+ }
83
+ }, killGraceMs).unref?.();
84
+ },
85
+ settled,
86
+ };
87
+ child.on('error', (err) => {
88
+ if (settledResolve === undefined)
89
+ return;
90
+ const r = settledResolve;
91
+ settledResolve = undefined;
92
+ r({
93
+ trigger: req.trigger,
94
+ reason: killedReason ?? 'spawn',
95
+ error: describeError(err),
96
+ finishedAt: now(),
97
+ });
98
+ });
99
+ child.on('close', (code, signal) => {
100
+ if (settledResolve === undefined)
101
+ return;
102
+ const r = settledResolve;
103
+ settledResolve = undefined;
104
+ if (killedReason !== null) {
105
+ r({
106
+ trigger: req.trigger,
107
+ reason: killedReason,
108
+ error: `被杀(${killedReason === 'timeout' ? '墙钟超时' : '新实例抢占'})`,
109
+ finishedAt: now(),
110
+ });
111
+ return;
112
+ }
113
+ if (signal) {
114
+ r({
115
+ trigger: req.trigger,
116
+ reason: 'killed',
117
+ error: `外部信号 ${signal} 终止`,
118
+ finishedAt: now(),
119
+ });
120
+ return;
121
+ }
122
+ const outcome = {
123
+ trigger: req.trigger,
124
+ reason: 'exit_code',
125
+ exitCode: code ?? 1,
126
+ finishedAt: now(),
127
+ };
128
+ const preview = stdoutTail.trim();
129
+ if (preview)
130
+ outcome.finalTextPreview = preview.slice(0, PREVIEW_CAP);
131
+ if ((code ?? 1) !== 0 && stderrTail.trim()) {
132
+ outcome.error = lastLine(stderrTail).slice(0, PREVIEW_CAP);
133
+ }
134
+ void startedAtMs;
135
+ r(outcome);
136
+ });
137
+ return handle;
138
+ },
139
+ };
140
+ }
141
+ /** 进程组信号(detached 子进程 pid 即组号——先组后单 pid 兜底) */
142
+ function signalTree(child, sig) {
143
+ if (typeof child.pid === 'number') {
144
+ try {
145
+ process.kill(-child.pid, sig);
146
+ }
147
+ catch {
148
+ /* 组不存在——单 pid 兜底 */
149
+ }
150
+ }
151
+ try {
152
+ child.kill(sig);
153
+ }
154
+ catch {
155
+ /* 已死 */
156
+ }
157
+ }
158
+ /** 错误转人读串 */
159
+ function describeError(err) {
160
+ return err instanceof Error ? err.message : String(err);
161
+ }
162
+ /** 尾行提取(错误摘要取末条非空行——pi-tick extractStderrSignature 保守档) */
163
+ function lastLine(text) {
164
+ const lines = text.trimEnd().split('\n');
165
+ for (let i = lines.length - 1; i >= 0; i--) {
166
+ const line = lines[i]?.trim() ?? '';
167
+ if (line)
168
+ return line;
169
+ }
170
+ return '';
171
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * schedule 串的词法、校验与下次到点计算(04 §12 调度条——schedule 形落码面;
3
+ * 词面承 pi-tick 三形 interval/daily/weekly + pilotdeck once/delay 补形)。
4
+ *
5
+ * 串形五写四形(once 有绝对/相对两写):
6
+ * every:<n>[s|m|h] 间隔重复(n 正整数;总秒数下限 5s)
7
+ * once@+<n>[s|m|h] 一次性相对延迟(自建行时刻起)
8
+ * once@<ISO> 一次性绝对时刻
9
+ * daily@HH:MM 每日(本地时区 24h 制)
10
+ * weekly@<days>@HH:MM 每周(days = 逗号分隔 mon/tue/.../sun 全写或三写)
11
+ *
12
+ * 判据全纯函数:解析/校验/推 next-fire 三段零 IO 零时钟——时钟由调用方注入
13
+ * (Date 形),测试确定性。语义钉死三条:
14
+ * ① once 触发后不再到点(next = null——行留表、list 可见、不再 due);
15
+ * ② 错过不重放:next-fire 一律取「严格晚于 now 的下一刻」(宿主停机期的
16
+ * 错过到点重启后直接跳下一刻——pi-tick/launchd/cron 同律);
17
+ * ③ daily/weekly 按本地时区解释(用户面语义),存储/比较一律 ISO UTC。
18
+ */
19
+ import { BaseError } from '../contracts/index.js';
20
+ /** 间隔下限(秒)——进程内挂钟最小粒度(pi-tick macOS 后端同值 5s) */
21
+ export const MIN_INTERVAL_SECONDS = 5;
22
+ /** 周 day 数值(0=周日……6=周六——Date.getDay 同系,跨端无歧义) */
23
+ const DAY_NAMES = {
24
+ sunday: 0,
25
+ sun: 0,
26
+ monday: 1,
27
+ mon: 1,
28
+ tuesday: 2,
29
+ tue: 2,
30
+ wednesday: 3,
31
+ wed: 3,
32
+ thursday: 4,
33
+ thu: 4,
34
+ friday: 5,
35
+ fri: 5,
36
+ saturday: 6,
37
+ sat: 6,
38
+ };
39
+ /** HH:MM 24h 制词法(pi-tick TIME_REGEX 同形) */
40
+ const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
41
+ /** 时长因子:s=1 / m=60 / h=3600 */
42
+ const UNIT_SECONDS = { s: 1, m: 60, h: 3600 };
43
+ /** 相对时长段解析:`<n>[s|m|h]` → 秒数(违例返回 null——不猜不夹取) */
44
+ function parseDuration(text) {
45
+ const m = text.match(/^(\d+)([smh])$/);
46
+ if (!m)
47
+ return null;
48
+ const n = Number(m[1]);
49
+ const unit = UNIT_SECONDS[m[2] ?? ''] ?? 0;
50
+ if (!Number.isSafeInteger(n) || n <= 0)
51
+ return null;
52
+ return n * unit;
53
+ }
54
+ /**
55
+ * schedule 串 → canonical 形(词法/语义全验——坏串抛 SCHEDULER_SCHEDULE_INVALID,
56
+ * message 载原因;GoalJobsFace register 的「响亮拒不炸装配」消费此 message)。
57
+ */
58
+ export function parseSchedule(text) {
59
+ const raw = text.trim();
60
+ const every = raw.match(/^every:(\d+[smh])$/);
61
+ if (every) {
62
+ const seconds = parseDuration(every[1] ?? '');
63
+ if (seconds === null)
64
+ throw badSchedule(raw, 'every 时长段坏形');
65
+ if (seconds < MIN_INTERVAL_SECONDS) {
66
+ throw badSchedule(raw, `间隔下限 ${MIN_INTERVAL_SECONDS}s(得 ${seconds}s)`);
67
+ }
68
+ return { kind: 'every', seconds };
69
+ }
70
+ const onceRel = raw.match(/^once@\+(\d+[smh])$/);
71
+ if (onceRel) {
72
+ // 相对延迟的锚定时刻由建行方落 at(parse 只验词法——纯函数不触时钟)
73
+ const seconds = parseDuration(onceRel[1] ?? '');
74
+ if (seconds === null)
75
+ throw badSchedule(raw, 'once 相对时长段坏形');
76
+ return { kind: 'once', at: `+${seconds}` };
77
+ }
78
+ const onceAbs = raw.match(/^once@(.+)$/);
79
+ if (onceAbs) {
80
+ const at = onceAbs[1] ?? '';
81
+ const t = Date.parse(at);
82
+ if (Number.isNaN(t))
83
+ throw badSchedule(raw, 'once 绝对时刻非合法 ISO');
84
+ return { kind: 'once', at: new Date(t).toISOString() };
85
+ }
86
+ const daily = raw.match(/^daily@(\d{2}:\d{2})$/);
87
+ if (daily && TIME_RE.test(daily[1] ?? '')) {
88
+ return { kind: 'daily', time: daily[1] ?? '' };
89
+ }
90
+ const weekly = raw.match(/^weekly@([a-z,]+)@(\d{2}:\d{2})$/);
91
+ if (weekly && TIME_RE.test(weekly[2] ?? '')) {
92
+ const days = parseDays(weekly[1] ?? '');
93
+ if (!days)
94
+ throw badSchedule(raw, 'weekly 星期段坏形');
95
+ return { kind: 'weekly', days, time: weekly[2] ?? '' };
96
+ }
97
+ throw badSchedule(raw, '须为 every:<n>[smh] / once@+<n>[smh] / once@<ISO> / daily@HH:MM / weekly@<days>@HH:MM 之一');
98
+ }
99
+ /** 星期段解析:逗号分隔名 → 去重升序 day 数组(坏名返回 null) */
100
+ function parseDays(text) {
101
+ if (!/^[a-z,]+$/.test(text))
102
+ return null;
103
+ const days = new Set();
104
+ for (const token of text.split(',')) {
105
+ const d = DAY_NAMES[token ?? ''];
106
+ if (d === undefined)
107
+ return null;
108
+ days.add(d);
109
+ }
110
+ return [...days].sort((a, b) => a - b);
111
+ }
112
+ /** 坏串抛手(message 前缀统一——含原串便于诊断) */
113
+ function badSchedule(raw, reason) {
114
+ return new BaseError('SCHEDULER_SCHEDULE_INVALID', `schedule 串「${raw}」坏形:${reason}`);
115
+ }
116
+ /** canonical 形 → 人读串(/tick list 渲染与日志面;往返 parse(format(x)) 恒等) */
117
+ export function formatSchedule(s) {
118
+ switch (s.kind) {
119
+ case 'every':
120
+ return `every:${formatSeconds(s.seconds)}`;
121
+ case 'once':
122
+ return `once@${s.at}`;
123
+ case 'daily':
124
+ return `daily@${s.time}`;
125
+ case 'weekly':
126
+ return `weekly@${s.days.map((d) => DAY_SHORT[d] ?? String(d)).join(',')}@${s.time}`;
127
+ }
128
+ }
129
+ /** day 数值 → 三写名(formatSchedule weekly 段用) */
130
+ const DAY_SHORT = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
131
+ /** 秒数 → 紧凑时长段(86400 内取最大单位整除形;不合整除回落秒形) */
132
+ function formatSeconds(total) {
133
+ if (total % 3600 === 0)
134
+ return `${total / 3600}h`;
135
+ if (total % 60 === 0)
136
+ return `${total / 60}m`;
137
+ return `${total}s`;
138
+ }
139
+ /** once 相对延迟形(at 以 '+' 开头)的锚定落值:base + 秒数 */
140
+ export function resolveRelativeOnce(s, base) {
141
+ if (s.kind !== 'once' || !s.at.startsWith('+'))
142
+ return s;
143
+ const seconds = Number(s.at.slice(1));
144
+ return { kind: 'once', at: new Date(base.getTime() + seconds * 1000).toISOString() };
145
+ }
146
+ /**
147
+ * 下次到点(严格晚于 now 的下一刻;once 已过或相对未锚定 → null)。
148
+ *
149
+ * `anchor` 仅 every 形消费:上次触发时刻缺省(建行/重启补推进场景)时以
150
+ * now 起算——「错过不重放」语义的落点(past 的 next 直接跳下一刻)。
151
+ */
152
+ export function nextFireAt(s, now, anchor) {
153
+ switch (s.kind) {
154
+ case 'every': {
155
+ const base = anchor && anchor.getTime() < now.getTime() ? now : (anchor ?? now);
156
+ return new Date(base.getTime() + s.seconds * 1000).toISOString();
157
+ }
158
+ case 'once': {
159
+ if (s.at.startsWith('+'))
160
+ return null; // 相对未锚定——建行方先过 resolveRelativeOnce
161
+ const t = Date.parse(s.at);
162
+ return t > now.getTime() ? new Date(t).toISOString() : null;
163
+ }
164
+ case 'daily':
165
+ return nextDailyAt(now, s.time);
166
+ case 'weekly':
167
+ return nextWeeklyAt(now, s.days, s.time);
168
+ }
169
+ }
170
+ /** 每日下一刻(本地时区;今日时刻已过取明日) */
171
+ function nextDailyAt(now, hhmm) {
172
+ const [h, m] = hhmm.split(':').map(Number);
173
+ const cand = new Date(now);
174
+ cand.setHours(h ?? 0, m ?? 0, 0, 0);
175
+ if (cand.getTime() <= now.getTime())
176
+ cand.setDate(cand.getDate() + 1);
177
+ return cand.toISOString();
178
+ }
179
+ /** 每周下一刻(候选日全算取最早严格未来;跨周自动落到下周同日) */
180
+ function nextWeeklyAt(now, days, hhmm) {
181
+ const [h, m] = hhmm.split(':').map(Number);
182
+ let best = null;
183
+ for (const d of days) {
184
+ const cand = new Date(now);
185
+ cand.setHours(h ?? 0, m ?? 0, 0, 0);
186
+ cand.setDate(cand.getDate() + ((d - now.getDay() + 7) % 7));
187
+ if (cand.getTime() <= now.getTime())
188
+ cand.setDate(cand.getDate() + 7);
189
+ if (best === null || cand.getTime() < best.getTime())
190
+ best = cand;
191
+ }
192
+ return best ? best.toISOString() : new Date(now).toISOString();
193
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * scheduler 服务面(04 §12):jobs 表 DAO + /tick 六动词的程序形态 +
3
+ * GoalJobsFace 四法窄面(第五槽——goal 件挂钟行的单漏斗写路径)。
4
+ *
5
+ * 分工线(04 §12「同一 jobs 表两视角」):本面管 **jobs 表行**(挂钟行的注册
6
+ * 管理面);运行中实例的 in-flight 记账归引擎内存表(§10 ctx.jobs 系另册,
7
+ * 语义不在此)。enable/disable **直打 jobs 表行**(幽灵名零行守卫——
8
+ * SCHEDULER_NOT_FOUND 响亮拒,2026-09-04 berry 侧同款守卫);add **名冲突守卫**
9
+ * (SCHEDULER_NAME_EXISTS)。cron 可选后端在场时启停/rm 与 OS 注册面联动
10
+ * (联动细则 = 本件落码面裁决:行翻转与 crontab 同步两段——OS 段失败亮拒
11
+ * 不半态)。
12
+ */
13
+ import { statSync } from 'node:fs';
14
+ import { isAbsolute } from 'node:path';
15
+ import { BaseError } from '../contracts/index.js';
16
+ import { formatSchedule, nextFireAt, parseSchedule, resolveRelativeOnce } from './schedule.js';
17
+ /** 任务名词法(pi-tick id 规则同形——goal-<goalId> 挂钟行名同型合法) */
18
+ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
19
+ /** prompt 硬帽(16KiB——pi-tick 同值) */
20
+ const PROMPT_MAX_BYTES = 16 * 1024;
21
+ /** jobs 表 DAO(service/engine 共用——行映射蛇 ↔ 驼峰单源) */
22
+ export class JobsDao {
23
+ db;
24
+ constructor(db) {
25
+ this.db = db;
26
+ }
27
+ /** 行查(名寻径;缺席 undefined) */
28
+ get(name) {
29
+ const row = this.db
30
+ .prepare(`SELECT name, prompt, cwd, schedule, enabled, builtin, created_at, updated_at,
31
+ next_fire_at, last_fire_at, last_outcome, active_pid, active_started_at
32
+ FROM jobs WHERE name = ?`)
33
+ .get(name);
34
+ return row ? fromDb(row) : undefined;
35
+ }
36
+ /** 全行清单(名序——/tick list 观察面) */
37
+ list() {
38
+ const rows = this.db
39
+ .prepare(`SELECT name, prompt, cwd, schedule, enabled, builtin, created_at, updated_at,
40
+ next_fire_at, last_fire_at, last_outcome, active_pid, active_started_at
41
+ FROM jobs ORDER BY name`)
42
+ .all();
43
+ return rows.map(fromDb);
44
+ }
45
+ /** due 集(启用且到点——挂钟推进轮询面;next_fire_at 升序) */
46
+ due(now) {
47
+ const rows = this.db
48
+ .prepare(`SELECT name, prompt, cwd, schedule, enabled, builtin, created_at, updated_at,
49
+ next_fire_at, last_fire_at, last_outcome, active_pid, active_started_at
50
+ FROM jobs
51
+ WHERE enabled = 1 AND next_fire_at IS NOT NULL AND next_fire_at <= ?
52
+ ORDER BY next_fire_at`)
53
+ .all(now);
54
+ return rows.map(fromDb);
55
+ }
56
+ /** 启用行的下次到点最小值(挂钟下轮调度间隔计算面;无启用行 null) */
57
+ earliestNextFire() {
58
+ const row = this.db
59
+ .prepare(`SELECT MIN(next_fire_at) AS m FROM jobs WHERE enabled = 1 AND next_fire_at IS NOT NULL`)
60
+ .get();
61
+ return row.m;
62
+ }
63
+ /** 建行(名主键冲突由调用方守卫——DAO 不吞) */
64
+ insert(row) {
65
+ this.db
66
+ .prepare(`INSERT INTO jobs (name, prompt, cwd, schedule, enabled, builtin, created_at,
67
+ updated_at, next_fire_at, last_fire_at, last_outcome,
68
+ active_pid, active_started_at)
69
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
70
+ .run(row.name, row.prompt, row.cwd, JSON.stringify(row.schedule), row.enabled ? 1 : 0, row.builtin ? 1 : 0, row.createdAt, row.updatedAt, row.nextFireAt, row.lastFireAt, row.lastOutcome === null ? null : JSON.stringify(row.lastOutcome), row.activePid, row.activeStartedAt);
71
+ }
72
+ /** 更新启停位与时间戳(enable/disable 直打) */
73
+ setEnabled(name, enabled, updatedAt) {
74
+ this.db.prepare(`UPDATE jobs SET enabled = ?, updated_at = ? WHERE name = ?`).run(enabled ? 1 : 0, updatedAt, name);
75
+ }
76
+ /** 删行 */
77
+ remove(name) {
78
+ this.db.prepare(`DELETE FROM jobs WHERE name = ?`).run(name);
79
+ }
80
+ /** 结算回写:last_fire_at + last_outcome + next_fire_at 推进(fire 收场单点) */
81
+ settleFire(name, firedAt, outcome, nextFireAt, updatedAt) {
82
+ this.db
83
+ .prepare(`UPDATE jobs SET last_fire_at = ?, last_outcome = ?, next_fire_at = ?, updated_at = ?
84
+ WHERE name = ?`)
85
+ .run(firedAt, JSON.stringify(outcome), nextFireAt, updatedAt, name);
86
+ }
87
+ /** next_fire_at 单列推进(重启补推进/闸拦跳过——不动 last_fire_at) */
88
+ advanceNextFire(name, nextFireAt, updatedAt) {
89
+ this.db.prepare(`UPDATE jobs SET next_fire_at = ?, updated_at = ? WHERE name = ?`).run(nextFireAt, updatedAt, name);
90
+ }
91
+ /** gated 结算:记拦截结局 + 推进 next(不动 last_fire_at——gated 非真跑,冷却闸判据不被污染) */
92
+ settleGated(name, outcome, nextFireAt, updatedAt) {
93
+ this.db
94
+ .prepare(`UPDATE jobs SET last_outcome = ?, next_fire_at = ?, updated_at = ? WHERE name = ?`)
95
+ .run(JSON.stringify(outcome), nextFireAt, updatedAt, name);
96
+ }
97
+ /** 跨进程在飞占用面写/清(OS cron 后端 --tick 子进程建占用) */
98
+ setActive(name, pid, startedAt, updatedAt) {
99
+ this.db
100
+ .prepare(`UPDATE jobs SET active_pid = ?, active_started_at = ?, updated_at = ? WHERE name = ?`)
101
+ .run(pid, startedAt, updatedAt, name);
102
+ }
103
+ }
104
+ /** DB 行 → 契约行(schedule/last_outcome JSON 解析坏值容错降级——行是自有面,坏值 warn 不炸读) */
105
+ function fromDb(row) {
106
+ let schedule;
107
+ try {
108
+ schedule = JSON.parse(row.schedule);
109
+ }
110
+ catch {
111
+ schedule = { kind: 'once', at: new Date(0).toISOString() };
112
+ }
113
+ let lastOutcome = null;
114
+ if (row.last_outcome) {
115
+ try {
116
+ lastOutcome = JSON.parse(row.last_outcome);
117
+ }
118
+ catch {
119
+ lastOutcome = null;
120
+ }
121
+ }
122
+ return {
123
+ name: row.name,
124
+ prompt: row.prompt,
125
+ cwd: row.cwd,
126
+ schedule,
127
+ enabled: row.enabled === 1,
128
+ builtin: row.builtin === 1,
129
+ createdAt: row.created_at,
130
+ updatedAt: row.updated_at,
131
+ nextFireAt: row.next_fire_at,
132
+ lastFireAt: row.last_fire_at,
133
+ lastOutcome,
134
+ activePid: row.active_pid,
135
+ activeStartedAt: row.active_started_at,
136
+ };
137
+ }
138
+ /** 服务装配(cronRegistrar/pathExists 缺省形态见 deps 注释) */
139
+ export function createSchedulerService(deps) {
140
+ const dao = new JobsDao(deps.db);
141
+ const pathExists = deps.pathExists ?? ((p) => statExists(p));
142
+ const now = deps.now;
143
+ /** 建行共核(add 与 goalJobs.register 单源——守卫/next_fire 初值全同律) */
144
+ function insertRow(req, builtin) {
145
+ if (!NAME_RE.test(req.name)) {
146
+ throw new BaseError('SCHEDULER_NAME_INVALID', `任务名「${req.name}」违例:须匹配 ${NAME_RE.source}(首字符字母数字、总长 ≤64)`);
147
+ }
148
+ if (dao.get(req.name)) {
149
+ throw new BaseError('SCHEDULER_NAME_EXISTS', `任务名「${req.name}」已被占用(jobs 表既有行)`);
150
+ }
151
+ const promptBytes = Buffer.byteLength(req.prompt, 'utf8');
152
+ if (req.prompt.length === 0 || promptBytes > PROMPT_MAX_BYTES) {
153
+ throw new BaseError('SCHEDULER_JOB_INVALID', `prompt 须非空且 ≤16KiB(得 ${promptBytes} 字节)`);
154
+ }
155
+ if (req.cwd !== null && req.cwd !== undefined) {
156
+ if (!isAbsolute(req.cwd)) {
157
+ throw new BaseError('SCHEDULER_JOB_INVALID', `cwd 须绝对路径(得「${req.cwd}」)`);
158
+ }
159
+ if (!pathExists(req.cwd)) {
160
+ throw new BaseError('SCHEDULER_JOB_INVALID', `cwd 不存在(建行时点判——「${req.cwd}」)`);
161
+ }
162
+ }
163
+ const schedule = resolveRelativeOnce(parseSchedule(req.schedule), new Date(now()));
164
+ const enabled = req.enabled === true;
165
+ const ts = now();
166
+ const row = {
167
+ name: req.name,
168
+ prompt: req.prompt,
169
+ cwd: req.cwd ?? null,
170
+ schedule,
171
+ enabled,
172
+ builtin,
173
+ createdAt: ts,
174
+ updatedAt: ts,
175
+ nextFireAt: enabled ? nextFireAt(schedule, new Date(ts)) : null,
176
+ lastFireAt: null,
177
+ lastOutcome: null,
178
+ activePid: null,
179
+ activeStartedAt: null,
180
+ };
181
+ dao.insert(row);
182
+ return row;
183
+ }
184
+ const service = {
185
+ addJob(req) {
186
+ return insertRow(req, false);
187
+ },
188
+ addBuiltinJob(req) {
189
+ return insertRow(req, true);
190
+ },
191
+ listJobs() {
192
+ return dao.list();
193
+ },
194
+ getJob(name) {
195
+ return dao.get(name);
196
+ },
197
+ removeJob(name) {
198
+ const row = dao.get(name);
199
+ if (!row)
200
+ throw new BaseError('SCHEDULER_NOT_FOUND', `任务「${name}」不存在(rm 幽灵名零行守卫)`);
201
+ if (deps.cronRegistrar && row.enabled) {
202
+ deps.cronRegistrar.unregister(name); // 失败抛 SCHEDULER_CRON_*——行不删(不半态)
203
+ }
204
+ dao.remove(name);
205
+ },
206
+ setJobEnabled(name, enabled) {
207
+ const row = dao.get(name);
208
+ if (!row) {
209
+ throw new BaseError('SCHEDULER_NOT_FOUND', `任务「${name}」不存在(${enabled ? 'enable' : 'disable'} 幽灵名零行守卫)`);
210
+ }
211
+ if (deps.cronRegistrar) {
212
+ if (enabled)
213
+ deps.cronRegistrar.register(row); // OS 注册失败抛——行不翻转
214
+ else
215
+ deps.cronRegistrar.unregister(name);
216
+ }
217
+ dao.setEnabled(name, enabled, now());
218
+ // 启用即刻排 next_fire(禁用期未排;once 相对形建行已锚定、every/daily/weekly 从现在取下一刻)
219
+ if (enabled) {
220
+ const fresh = dao.get(name);
221
+ if (fresh) {
222
+ dao.advanceNextFire(name, nextFireAt(fresh.schedule, new Date(now()), fresh.lastFireAt ? new Date(fresh.lastFireAt) : undefined), now());
223
+ }
224
+ }
225
+ else {
226
+ dao.advanceNextFire(name, null, now()); // 停用即摘排(行留史)
227
+ }
228
+ },
229
+ describeSchedule(name) {
230
+ const row = dao.get(name);
231
+ return row ? formatSchedule(row.schedule) : '';
232
+ },
233
+ };
234
+ // ── GoalJobsFace 四法(第五槽窄面——挂钟行一切写经此单漏斗) ─────────────
235
+ const goalJobs = {
236
+ async register(req) {
237
+ const name = `goal-${req.goalId}`;
238
+ try {
239
+ const row = insertRow({ name, prompt: req.promptSnapshot, schedule: req.schedule, enabled: true, builtin: true }, true);
240
+ if (deps.cronRegistrar)
241
+ deps.cronRegistrar.register(row); // 挂钟行启用即挂 OS(乙案形态)
242
+ return { ok: true, message: `挂钟已挂:${name}(${formatSchedule(row.schedule)})` };
243
+ }
244
+ catch (err) {
245
+ // 响亮拒不炸装配——message 载码与原因(goal 件消费方落诊断面)
246
+ const message = err instanceof BaseError ? `${err.code}: ${err.message}` : String(err);
247
+ return { ok: false, message };
248
+ }
249
+ },
250
+ async disable(goalId) {
251
+ const name = `goal-${goalId}`;
252
+ if (!dao.get(name))
253
+ return; // 无行 = 静默 no-op
254
+ service.setJobEnabled(name, false); // 行留史(enable 可复活)
255
+ },
256
+ async enable(goalId) {
257
+ const name = `goal-${goalId}`;
258
+ if (!dao.get(name))
259
+ return; // 无行 = 静默 no-op
260
+ service.setJobEnabled(name, true);
261
+ },
262
+ async remove(goalId) {
263
+ const name = `goal-${goalId}`;
264
+ const row = dao.get(name);
265
+ if (!row)
266
+ return; // 无行 = 静默 no-op
267
+ if (deps.cronRegistrar && row.enabled)
268
+ deps.cronRegistrar.unregister(name); // 同步注销 OS 注册态
269
+ dao.remove(name);
270
+ },
271
+ };
272
+ return { service, dao, goalJobs };
273
+ }
274
+ /** cwd 存在判真身(缺省形态——测试注 pathExists 假件后不触盘) */
275
+ function statExists(p) {
276
+ try {
277
+ statSync(p);
278
+ return true;
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ }