@gajae-code/coding-agent 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -0,0 +1,200 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export interface CoordinatorDirectoryBarrierOptions {
5
+ platform?: NodeJS.Platform;
6
+ openDirectory?: (directory: string) => Promise<fs.FileHandle>;
7
+ }
8
+
9
+ export interface CoordinatorFileDurabilityOptions {
10
+ syncFile?: (handle: fs.FileHandle) => Promise<void>;
11
+ }
12
+
13
+ export interface CoordinatorAtomicWriteOptions
14
+ extends CoordinatorDirectoryBarrierOptions,
15
+ CoordinatorFileDurabilityOptions {
16
+ rename?: (source: string, destination: string) => Promise<void>;
17
+ }
18
+
19
+ /** The file was published, but its final directory barrier did not complete. */
20
+ export class CoordinatorPublicationUncertainError extends Error {
21
+ constructor(cause: unknown) {
22
+ super("coordinator publication outcome is uncertain", { cause });
23
+ }
24
+ }
25
+
26
+ export async function ensureCoordinatorDirectory(
27
+ directory: string,
28
+ options: CoordinatorDirectoryBarrierOptions = {},
29
+ ): Promise<void> {
30
+ const missing: string[] = [];
31
+ for (let current = directory; ; current = path.dirname(current)) {
32
+ try {
33
+ const stat = await fs.stat(current);
34
+ if (!stat.isDirectory()) throw new Error(`coordinator directory is not a directory: ${current}`);
35
+ break;
36
+ } catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
38
+ missing.push(current);
39
+ if (current === path.dirname(current)) throw error;
40
+ }
41
+ }
42
+ for (const created of missing.reverse()) {
43
+ try {
44
+ await fs.mkdir(created, { mode: 0o700 });
45
+ } catch (error) {
46
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
47
+ const stat = await fs.stat(created);
48
+ if (!stat.isDirectory()) throw error;
49
+ }
50
+ await syncCoordinatorDirectory(path.dirname(created), options);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Windows does not support fsync on directory handles. File contents must be
56
+ * synced before publication; this barrier only makes the renamed directory
57
+ * entry durable where the platform supports that operation.
58
+ */
59
+ export function isUnsupportedWindowsDirectorySyncError(
60
+ error: unknown,
61
+ platform: NodeJS.Platform = process.platform,
62
+ ): boolean {
63
+ if (platform !== "win32") return false;
64
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
65
+ // Windows directory-handle barriers use the established broker precedent:
66
+ // EPERM/EACCES are tolerated only for the directory open/sync operation.
67
+ return code === "EPERM" || code === "EACCES";
68
+ }
69
+
70
+ export async function syncCoordinatorDirectory(
71
+ directory: string,
72
+ options: CoordinatorDirectoryBarrierOptions = {},
73
+ ): Promise<void> {
74
+ let handle: fs.FileHandle;
75
+ try {
76
+ handle = await (options.openDirectory ?? (path => fs.open(path, "r")))(directory);
77
+ } catch (error) {
78
+ if (!isUnsupportedWindowsDirectorySyncError(error, options.platform)) throw error;
79
+ return;
80
+ }
81
+ let syncError: unknown;
82
+ try {
83
+ await handle.sync();
84
+ } catch (error) {
85
+ if (!isUnsupportedWindowsDirectorySyncError(error, options.platform)) syncError = error;
86
+ }
87
+ try {
88
+ await handle.close();
89
+ } catch (closeError) {
90
+ if (syncError) throw new AggregateError([syncError, closeError], "coordinator directory sync and close failed");
91
+ throw closeError;
92
+ }
93
+ if (syncError) throw syncError;
94
+ }
95
+
96
+ /** File durability is never best-effort: callers must abort on every failure. */
97
+ export async function syncCoordinatorFile(
98
+ handle: fs.FileHandle,
99
+ options: CoordinatorFileDurabilityOptions = {},
100
+ ): Promise<void> {
101
+ await (options.syncFile ?? (file => file.sync()))(handle);
102
+ }
103
+
104
+ /** Append a durable coordinator journal or diagnostic record, then barrier its parent. */
105
+ export async function appendCoordinatorFile(
106
+ file: string,
107
+ contents: string,
108
+ options: CoordinatorDirectoryBarrierOptions & CoordinatorFileDurabilityOptions = {},
109
+ ): Promise<void> {
110
+ await ensureCoordinatorDirectory(path.dirname(file), options);
111
+ let originalSize = 0;
112
+ try {
113
+ originalSize = (await fs.stat(file)).size;
114
+ } catch (error) {
115
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
116
+ }
117
+ const handle = await fs.open(file, "a", 0o600);
118
+ let writeError: unknown;
119
+ try {
120
+ await handle.writeFile(contents);
121
+ await syncCoordinatorFile(handle, options);
122
+ } catch (error) {
123
+ writeError = error;
124
+ }
125
+ if (writeError) {
126
+ try {
127
+ await handle.truncate(originalSize);
128
+ await syncCoordinatorFile(handle, options);
129
+ } catch (rollbackError) {
130
+ writeError = new AggregateError([writeError, rollbackError], "coordinator append rollback failed");
131
+ }
132
+ }
133
+ try {
134
+ await handle.close();
135
+ } catch (closeError) {
136
+ if (writeError) {
137
+ const writeErrors = writeError instanceof AggregateError ? writeError.errors : [writeError];
138
+ throw new AggregateError([...writeErrors, closeError], "coordinator append and close failed");
139
+ }
140
+ throw closeError;
141
+ }
142
+ if (writeError) throw writeError;
143
+ try {
144
+ await syncCoordinatorDirectory(path.dirname(file), options);
145
+ } catch (error) {
146
+ throw new CoordinatorPublicationUncertainError(error);
147
+ }
148
+ }
149
+
150
+ /** Atomically publish a synced coordinator state file, then barrier its parent. */
151
+ export async function writeCoordinatorAtomic(
152
+ file: string,
153
+ contents: string,
154
+ options: CoordinatorAtomicWriteOptions = {},
155
+ ): Promise<void> {
156
+ await ensureCoordinatorDirectory(path.dirname(file), options);
157
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
158
+ let published = false;
159
+ try {
160
+ const handle = await fs.open(temporary, "wx", 0o600);
161
+ let writeError: unknown;
162
+ try {
163
+ await handle.writeFile(contents);
164
+ await syncCoordinatorFile(handle, options);
165
+ } catch (error) {
166
+ writeError = error;
167
+ }
168
+ try {
169
+ await handle.close();
170
+ } catch (closeError) {
171
+ if (writeError) throw new AggregateError([writeError, closeError], "coordinator write and close failed");
172
+ throw closeError;
173
+ }
174
+ if (writeError) throw writeError;
175
+ await (options.rename ?? fs.rename)(temporary, file);
176
+ published = true;
177
+ await syncCoordinatorDirectory(path.dirname(file), options);
178
+ } catch (error) {
179
+ let cleanupError: unknown;
180
+ try {
181
+ await fs.rm(temporary, { force: true });
182
+ } catch (cleanupFailure) {
183
+ cleanupError = cleanupFailure;
184
+ }
185
+ let cleanupBarrierError: unknown;
186
+ if (!cleanupError && !published) {
187
+ try {
188
+ await syncCoordinatorDirectory(path.dirname(file), options);
189
+ } catch (barrierError) {
190
+ cleanupBarrierError = barrierError;
191
+ }
192
+ }
193
+ const failures = [error, cleanupError, cleanupBarrierError].filter(
194
+ (value): value is unknown => value !== undefined,
195
+ );
196
+ if (published) throw new CoordinatorPublicationUncertainError(new AggregateError(failures));
197
+ if (failures.length === 1) throw failures[0];
198
+ throw new AggregateError(failures, "coordinator atomic write and cleanup failed");
199
+ }
200
+ }
@@ -0,0 +1,278 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { $credentialEnv } from "@gajae-code/utils/env";
4
+ import { withFileLock } from "../config/file-lock";
5
+ import { writeCoordinatorAtomic } from "./durability";
6
+
7
+ /**
8
+ * Opt-in webhook delivery of existing coordinator `watch_events` journal rows
9
+ * (issue #4706). Delivery is a durable outbox keyed by the journal row's stable
10
+ * `id`: each delivered row is recorded once, retried with bounded exponential
11
+ * backoff, and never blocks or rewrites the journal append path.
12
+ */
13
+
14
+ export interface EventWebhookConfig {
15
+ url: string;
16
+ tokenFile: string | null;
17
+ sessionIds: Set<string> | null;
18
+ timeoutMs: number;
19
+ maxAttempts: number;
20
+ }
21
+
22
+ interface WebhookDeliveryRecord {
23
+ schema_version: 1;
24
+ event_id: string;
25
+ status: "pending" | "delivered" | "failed";
26
+ attempts: number;
27
+ created_at: string;
28
+ updated_at: string;
29
+ last_error: string | null;
30
+ }
31
+
32
+ function isWebhookDeliveryRecord(value: unknown, eventId: string): value is WebhookDeliveryRecord {
33
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
34
+ const record = value as Record<string, unknown>;
35
+ return (
36
+ record.schema_version === 1 &&
37
+ record.event_id === eventId &&
38
+ (record.status === "pending" || record.status === "delivered" || record.status === "failed") &&
39
+ typeof record.attempts === "number" &&
40
+ Number.isSafeInteger(record.attempts) &&
41
+ record.attempts >= 0 &&
42
+ typeof record.created_at === "string" &&
43
+ typeof record.updated_at === "string" &&
44
+ (record.last_error === null || typeof record.last_error === "string")
45
+ );
46
+ }
47
+
48
+ /** Result of one webhook POST attempt; 2xx is ok, anything else carries the reason. */
49
+ interface WebhookPostOutcome {
50
+ ok: boolean;
51
+ status: number | null;
52
+ error: string | null;
53
+ }
54
+
55
+ export interface WebhookDelivery {
56
+ /** Called with the exact JSON body to POST and per-attempt request options. */
57
+ post(
58
+ body: string,
59
+ options: { url: string; token: string | null; timeoutMs: number; signal: AbortSignal },
60
+ ): Promise<WebhookPostOutcome>;
61
+ /** Test seam: delay between retries; default is `Bun.sleep`. */
62
+ sleep(ms: number): Promise<void>;
63
+ /** Monotonic-ish clock for deadline math; default is `Date.now`. */
64
+ now(): number;
65
+ }
66
+
67
+ /** Default `WebhookDelivery` over `fetch`: POST, 2xx = ok, no redirects, abort on timeout. */
68
+ export function createDefaultEventWebhookDelivery(): WebhookDelivery {
69
+ return {
70
+ async post(body, options) {
71
+ const response = await fetch(options.url, {
72
+ method: "POST",
73
+ redirect: "error",
74
+ signal: options.signal,
75
+ headers: {
76
+ "content-type": "application/json",
77
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
78
+ },
79
+ body,
80
+ });
81
+ // Drain the body so the socket is reusable, then treat non-2xx as failure.
82
+ await response.text().catch(() => "");
83
+ if (!response.ok) return { ok: false, status: response.status, error: `http_${response.status}` };
84
+ return { ok: true, status: response.status, error: null };
85
+ },
86
+ sleep: async ms => {
87
+ await Bun.sleep(ms);
88
+ },
89
+ now: () => Date.now(),
90
+ };
91
+ }
92
+
93
+ export const DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS = 5_000;
94
+ export const MAX_EVENT_WEBHOOK_TIMEOUT_MS = 30_000;
95
+ export const DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS = 5;
96
+ export const MAX_EVENT_WEBHOOK_MAX_ATTEMPTS = 10;
97
+ const WEBHOOK_BACKOFF_BASE_MS = 500;
98
+ const WEBHOOK_BACKOFF_CAP_MS = 15_000;
99
+ const WEBHOOK_ERROR_CAP = 240;
100
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
101
+
102
+ /**
103
+ * Trusted-source resolution for the five webhook variables. Egress destination
104
+ * and the token-file path must never come from the caller's cwd `.env` overlay:
105
+ * Bun loads that overlay into `process.env`, so a raw `process.env` read would
106
+ * let any repository select where coordinator journal rows are POSTed.
107
+ * `$credentialEnv` consults only the inherited environment and GJC/user-owned
108
+ * env files, exactly like the crash relay's DSN. Callers that own a fully
109
+ * controlled environment (tests, rendered hermes blocks) pass `explicitEnv`;
110
+ * production entry points must omit it and resolve through the trusted source.
111
+ */
112
+ function webhookEnvValue(explicitEnv: NodeJS.ProcessEnv | undefined, name: string): string | undefined {
113
+ const explicit = explicitEnv?.[name]?.trim();
114
+ if (explicit) return explicit;
115
+ return $credentialEnv(name)?.trim() || undefined;
116
+ }
117
+
118
+ export function parseEventWebhookConfig(explicitEnv?: NodeJS.ProcessEnv): EventWebhookConfig | null {
119
+ const rawUrl = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_URL");
120
+ if (!rawUrl) return null;
121
+ let parsed: URL;
122
+ try {
123
+ parsed = new URL(rawUrl);
124
+ } catch {
125
+ throw new Error("coordinator_event_webhook_url_invalid");
126
+ }
127
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
128
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && LOOPBACK_HOSTS.has(hostname)))
129
+ throw new Error("coordinator_event_webhook_url_not_allowed");
130
+ const rawSessions = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_SESSION_IDS");
131
+ const sessionIds = rawSessions
132
+ ? new Set(
133
+ rawSessions
134
+ .split(",")
135
+ .map(value => value.trim())
136
+ .filter(value => /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(value)),
137
+ )
138
+ : null;
139
+ const tokenFile = webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TOKEN_FILE") ?? "";
140
+ if (tokenFile !== "" && !path.isAbsolute(tokenFile)) throw new Error("coordinator_event_webhook_token_file_invalid");
141
+ const timeoutRaw = Number.parseInt(
142
+ webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_TIMEOUT_MS") ?? "",
143
+ 10,
144
+ );
145
+ const timeoutMs =
146
+ Number.isFinite(timeoutRaw) && timeoutRaw > 0
147
+ ? Math.min(timeoutRaw, MAX_EVENT_WEBHOOK_TIMEOUT_MS)
148
+ : DEFAULT_EVENT_WEBHOOK_TIMEOUT_MS;
149
+ const attemptsRaw = Number.parseInt(
150
+ webhookEnvValue(explicitEnv, "GJC_COORDINATOR_MCP_EVENT_WEBHOOK_MAX_ATTEMPTS") ?? "",
151
+ 10,
152
+ );
153
+ const maxAttempts =
154
+ Number.isFinite(attemptsRaw) && attemptsRaw > 0
155
+ ? Math.min(attemptsRaw, MAX_EVENT_WEBHOOK_MAX_ATTEMPTS)
156
+ : DEFAULT_EVENT_WEBHOOK_MAX_ATTEMPTS;
157
+ return { url: rawUrl, tokenFile: tokenFile || null, sessionIds, timeoutMs, maxAttempts };
158
+ }
159
+
160
+ function eventWebhookScopeApplies(config: EventWebhookConfig, event: { session_id?: string }): boolean {
161
+ return config.sessionIds === null || (event.session_id !== undefined && config.sessionIds.has(event.session_id));
162
+ }
163
+
164
+ function recordPath(namespaceDir: string, eventId: string): string {
165
+ if (!/^event-\d{12,}$/.test(eventId)) throw new Error("coordinator_event_webhook_event_id_invalid");
166
+ return path.join(namespaceDir, "webhook-outbox", `${eventId}.json`);
167
+ }
168
+
169
+ async function readRecord(file: string, eventId: string): Promise<WebhookDeliveryRecord | null> {
170
+ let source: string;
171
+ try {
172
+ source = await fs.readFile(file, "utf8");
173
+ } catch (error) {
174
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
175
+ throw error;
176
+ }
177
+ try {
178
+ const parsed: unknown = JSON.parse(source);
179
+ return isWebhookDeliveryRecord(parsed, eventId) ? parsed : null;
180
+ } catch (error) {
181
+ if (error instanceof SyntaxError) return null;
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ async function writeRecord(file: string, record: WebhookDeliveryRecord): Promise<void> {
187
+ await writeCoordinatorAtomic(file, JSON.stringify(record));
188
+ }
189
+
190
+ function webhookErrorSummary(error: string | null): string | null {
191
+ if (error === null) return null;
192
+ const normalized = error.replace(/[\r\n]+/g, " ").trim();
193
+ return normalized.length > WEBHOOK_ERROR_CAP ? `${normalized.slice(0, WEBHOOK_ERROR_CAP - 3)}...` : normalized;
194
+ }
195
+
196
+ function backoffDelayMs(attempt: number): number {
197
+ return Math.min(WEBHOOK_BACKOFF_BASE_MS * 2 ** (attempt - 1), WEBHOOK_BACKOFF_CAP_MS);
198
+ }
199
+
200
+ async function readWebhookToken(tokenFile: string | null): Promise<string | null> {
201
+ if (tokenFile === null) return null;
202
+ try {
203
+ return (await fs.readFile(tokenFile, "utf8")).trim();
204
+ } catch {
205
+ throw new Error("coordinator_event_webhook_token_file_unreadable");
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Records the row in the durable outbox and, unless the row is already
211
+ * delivered or exhausted, performs one bounded delivery attempt series. The
212
+ * returned promise settles only after bounded attempts/backoff, so a dead sink
213
+ * can never wedge the caller's persistence; callers run it off the append path.
214
+ */
215
+ export async function deliverEventWebhook(
216
+ namespaceDir: string,
217
+ config: EventWebhookConfig,
218
+ event: { id: string; session_id?: string },
219
+ renderBody: () => string,
220
+ delivery: WebhookDelivery,
221
+ ): Promise<void> {
222
+ if (!eventWebhookScopeApplies(config, event)) return;
223
+ const file = recordPath(namespaceDir, event.id);
224
+ return await withFileLock(file, async () => {
225
+ const now0 = delivery.now();
226
+ let record = await readRecord(file, event.id);
227
+ if (record === null) {
228
+ record = {
229
+ schema_version: 1,
230
+ event_id: event.id,
231
+ status: "pending",
232
+ attempts: 0,
233
+ created_at: new Date(now0).toISOString(),
234
+ updated_at: new Date(now0).toISOString(),
235
+ last_error: null,
236
+ };
237
+ await writeRecord(file, record);
238
+ } else if (record.status !== "pending") {
239
+ return;
240
+ }
241
+ const token = await readWebhookToken(config.tokenFile);
242
+ const body = renderBody();
243
+ while (record.attempts < config.maxAttempts) {
244
+ record.attempts += 1;
245
+ const controller = new AbortController();
246
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs);
247
+ timer.unref?.();
248
+ let outcome: WebhookPostOutcome;
249
+ try {
250
+ outcome = await delivery.post(body, {
251
+ url: config.url,
252
+ token,
253
+ timeoutMs: config.timeoutMs,
254
+ signal: controller.signal,
255
+ });
256
+ } catch (error) {
257
+ outcome = { ok: false, status: null, error: error instanceof Error ? error.message : String(error) };
258
+ } finally {
259
+ clearTimeout(timer);
260
+ }
261
+ const ts = new Date(delivery.now()).toISOString();
262
+ record.updated_at = ts;
263
+ record.last_error = outcome.ok ? null : webhookErrorSummary(outcome.error);
264
+ if (outcome.ok) {
265
+ record.status = "delivered";
266
+ await writeRecord(file, record);
267
+ return;
268
+ }
269
+ if (record.attempts >= config.maxAttempts) {
270
+ record.status = "failed";
271
+ await writeRecord(file, record);
272
+ return;
273
+ }
274
+ await writeRecord(file, record);
275
+ await delivery.sleep(backoffDelayMs(record.attempts));
276
+ }
277
+ });
278
+ }