@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
@@ -81,15 +81,8 @@ export function createProviderSelectionPolicy(input: ProviderSelectionPolicyInpu
81
81
  }
82
82
  }
83
83
 
84
- const orderedProviders = [...explicitProviders];
85
- const seen = new Set(explicitSet);
86
- for (const provider of input.catalogProviders) {
87
- if (seen.has(provider)) {
88
- continue;
89
- }
90
- seen.add(provider);
91
- orderedProviders.push(provider);
92
- }
84
+ // One shared implementation with the standalone accessor; see projectProviderOrder.
85
+ const orderedProviders = projectProviderOrder(explicitProviders, input.catalogProviders);
93
86
 
94
87
  return {
95
88
  rank(provider: string): number {
@@ -113,6 +106,35 @@ export function createProviderSelectionPolicy(input: ProviderSelectionPolicyInpu
113
106
  };
114
107
  }
115
108
 
109
+ /**
110
+ * Project the deterministic provider order: normalized explicit order first, then
111
+ * first-wins catalog order for everything else.
112
+ *
113
+ * This is the single implementation of that ordering. It reads no credentials and
114
+ * takes no auth input at all, so any consumer that only needs "which providers, in
115
+ * what priority" cannot accidentally acquire auth sensitivity. Auth-aware banding
116
+ * lives exclusively in {@link ProviderSelectionPolicy.rank}.
117
+ */
118
+ export function projectProviderOrder(
119
+ explicitProviderOrder: readonly string[],
120
+ catalogProviders: readonly string[],
121
+ ): string[] {
122
+ const ordered: string[] = [];
123
+ const seen = new Set<string>();
124
+ for (const raw of explicitProviderOrder) {
125
+ const normalized = raw.trim().toLowerCase();
126
+ if (!normalized || seen.has(normalized)) continue;
127
+ seen.add(normalized);
128
+ ordered.push(normalized);
129
+ }
130
+ for (const provider of catalogProviders) {
131
+ if (!provider || seen.has(provider)) continue;
132
+ seen.add(provider);
133
+ ordered.push(provider);
134
+ }
135
+ return ordered;
136
+ }
137
+
116
138
  export interface ProviderSelectionCatalog {
117
139
  /** Lowercased provider ids in first-wins registry catalog order. */
118
140
  readonly catalogProviders: readonly string[];
@@ -144,3 +166,32 @@ export function buildProviderSelectionCatalog(models: readonly Model<Api>[]): Pr
144
166
  }
145
167
  return { catalogProviders, catalogModels };
146
168
  }
169
+ /**
170
+ * Deterministic provider priority for a catalog, returned in the catalog's own
171
+ * spelling.
172
+ *
173
+ * Ordering and de-duplication run on normalized ids, but the result restores each
174
+ * provider's first-seen catalog spelling because the autorouting generator matches
175
+ * provider prefixes with case-sensitive exact strings — a lowercased id would
176
+ * silently empty that provider's tiers. Providers absent from the catalog are
177
+ * dropped so a dead declaration cannot pollute a generated declarationFingerprint.
178
+ *
179
+ * Reads no credentials: it takes a catalog and an explicit order, nothing else.
180
+ */
181
+ export function projectCatalogProviderOrder(
182
+ explicitProviderOrder: readonly string[],
183
+ models: readonly Model<Api>[],
184
+ ): string[] {
185
+ const { catalogProviders } = buildProviderSelectionCatalog(models);
186
+ const spelling = new Map<string, string>();
187
+ for (const model of models) {
188
+ const normalized = model.provider.trim().toLowerCase();
189
+ if (normalized && !spelling.has(normalized)) spelling.set(normalized, model.provider);
190
+ }
191
+ const restored: string[] = [];
192
+ for (const provider of projectProviderOrder(explicitProviderOrder, catalogProviders)) {
193
+ const spelled = spelling.get(provider);
194
+ if (spelled !== undefined) restored.push(spelled);
195
+ }
196
+ return restored;
197
+ }
@@ -4,6 +4,18 @@ import { TASK_SIMPLE_MODES } from "../task/simple-mode";
4
4
  import { getThinkingLevelMetadata } from "../thinking-metadata";
5
5
  import { DEFAULT_EDIT_MODE_SETTING, EDIT_MODE_SETTINGS, EDIT_MODES, type EditMode } from "../utils/edit-mode";
6
6
  import { CONFIGURABLE_SEARCH_PROVIDER_IDS } from "../web/search/types";
7
+ import {
8
+ AUTOROUTING_SELECTOR_DESCRIPTION,
9
+ AUTOROUTING_SELECTOR_PATTERN,
10
+ AUTOROUTING_TIERS,
11
+ type AutoroutingLocalIssue,
12
+ type AutoroutingProvenance,
13
+ type AutoroutingSetup,
14
+ type AutoroutingTierMapInput,
15
+ validateAutoroutingLocal,
16
+ validateAutoroutingProvenance,
17
+ validateAutoroutingSetup,
18
+ } from "./autorouting-contract";
7
19
  import type { ModelSelectorValue } from "./model-selector-value";
8
20
  import { UPDATE_CHANNELS } from "./update-channel";
9
21
 
@@ -148,6 +160,22 @@ export type AnyUiMetadata = UiBase & {
148
160
  options?: ReadonlyArray<SubmenuOption> | "runtime";
149
161
  };
150
162
 
163
+ /** JSON Schema fragment carried by settings definitions that own nested validation. */
164
+ export type JsonSchemaObject = {
165
+ [key: string]: unknown;
166
+ type?: string;
167
+ properties?: Record<string, JsonSchemaObject>;
168
+ additionalProperties?: boolean | JsonSchemaObject;
169
+ items?: JsonSchemaObject;
170
+ required?: readonly string[];
171
+ pattern?: string;
172
+ minItems?: number;
173
+ minLength?: number;
174
+ uniqueItems?: boolean;
175
+ minimum?: number;
176
+ const?: unknown;
177
+ };
178
+
151
179
  interface BooleanDef {
152
180
  type: "boolean";
153
181
  default?: boolean;
@@ -186,6 +214,21 @@ type RecordValueDef =
186
214
  | { type: "string-enum"; values: readonly string[] }
187
215
  | { type: "credential-selector" };
188
216
 
217
+ interface ConstrainedRecordValueDef {
218
+ type: "autorouting-selector-value";
219
+ pattern: string;
220
+ description: string;
221
+ }
222
+
223
+ interface ConstrainedRecordDef<T> {
224
+ type: "constrained-record";
225
+ default: T;
226
+ keys: readonly string[];
227
+ valueSchema: ConstrainedRecordValueDef;
228
+ description?: string;
229
+ ui?: UiBase;
230
+ }
231
+
189
232
  interface RecordDef<T> {
190
233
  type: "record";
191
234
  default: Record<string, T>;
@@ -193,13 +236,23 @@ interface RecordDef<T> {
193
236
  ui?: UiBase;
194
237
  }
195
238
 
196
- type SettingDef =
239
+ export interface OptionalObjectDef<T> {
240
+ type: "optional-object";
241
+ default: undefined;
242
+ jsonSchema: JsonSchemaObject;
243
+ validate: (value: unknown) => AutoroutingLocalIssue[];
244
+ _value?: T;
245
+ }
246
+
247
+ export type SettingDef =
197
248
  | BooleanDef
198
249
  | StringDef
199
250
  | NumberDef
200
251
  | EnumDef<readonly string[]>
201
252
  | ArrayDef<unknown>
202
- | RecordDef<unknown>;
253
+ | RecordDef<unknown>
254
+ | ConstrainedRecordDef<unknown>
255
+ | OptionalObjectDef<unknown>;
203
256
 
204
257
  // ═══════════════════════════════════════════════════════════════════════════
205
258
  // Schema Definition
@@ -261,6 +314,56 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
261
314
  },
262
315
  ];
263
316
 
317
+ const AUTOROUTING_SETUP_JSON_SCHEMA: JsonSchemaObject = {
318
+ type: "object",
319
+ properties: {
320
+ schema: { type: "integer", const: 1 },
321
+ providers: {
322
+ type: "array",
323
+ minItems: 1,
324
+ uniqueItems: true,
325
+ items: {
326
+ type: "string",
327
+ minLength: 1,
328
+ pattern: "^[^\\s](?:.*[^\\s])?$",
329
+ },
330
+ },
331
+ models: {
332
+ type: "array",
333
+ items: {
334
+ type: "string",
335
+ minLength: 1,
336
+ maxLength: 256,
337
+ pattern: AUTOROUTING_SELECTOR_PATTERN,
338
+ not: { pattern: "^\\s*[pP][iI]/" },
339
+ },
340
+ },
341
+ },
342
+ additionalProperties: false,
343
+ required: ["schema", "providers"],
344
+ };
345
+
346
+ const AUTOROUTING_PROVENANCE_JSON_SCHEMA: JsonSchemaObject = {
347
+ type: "object",
348
+ properties: {
349
+ schema: { type: "integer", const: 1 },
350
+ source: {
351
+ type: "object",
352
+ properties: {
353
+ catalogFingerprint: { type: "string", pattern: "^[0-9a-f]{64}$" },
354
+ mapFingerprint: { type: "string", pattern: "^[0-9a-f]{64}$" },
355
+ generatorVersion: { type: "integer", minimum: 1 },
356
+ },
357
+ additionalProperties: false,
358
+ required: ["catalogFingerprint", "mapFingerprint", "generatorVersion"],
359
+ },
360
+ declarationFingerprint: { type: "string", pattern: "^[0-9a-f]{64}$" },
361
+ tiersFingerprint: { type: "string", pattern: "^[0-9a-f]{64}$" },
362
+ },
363
+ additionalProperties: false,
364
+ required: ["schema", "source", "declarationFingerprint", "tiersFingerprint"],
365
+ };
366
+
264
367
  export const SETTINGS_SCHEMA = {
265
368
  // ────────────────────────────────────────────────────────────────────────
266
369
  // General settings (no UI)
@@ -618,7 +721,7 @@ export const SETTINGS_SCHEMA = {
618
721
  },
619
722
  "gjc.ralplan.autoHandoff": {
620
723
  type: "enum",
621
- values: ["off", "ultragoal", "team"],
724
+ values: ["off", "ultragoal", "autoresearch"],
622
725
  default: "off",
623
726
  },
624
727
  "gjc.ralplan.maxIterations": {
@@ -3510,6 +3613,39 @@ export const SETTINGS_SCHEMA = {
3510
3613
  valueSchema: MODEL_SELECTOR_VALUE_SCHEMA,
3511
3614
  },
3512
3615
 
3616
+ "task.autorouting.enabled": {
3617
+ type: "boolean",
3618
+ default: false,
3619
+ },
3620
+
3621
+ "task.autorouting.tiers": {
3622
+ type: "constrained-record",
3623
+ default: {} as AutoroutingTierMapInput,
3624
+ keys: AUTOROUTING_TIERS,
3625
+ valueSchema: {
3626
+ type: "autorouting-selector-value",
3627
+ minLength: 1,
3628
+ maxLength: 256,
3629
+ pattern: AUTOROUTING_SELECTOR_PATTERN,
3630
+ description: AUTOROUTING_SELECTOR_DESCRIPTION,
3631
+ },
3632
+ description: AUTOROUTING_SELECTOR_DESCRIPTION,
3633
+ },
3634
+
3635
+ "task.autorouting.setup": {
3636
+ type: "optional-object",
3637
+ default: undefined,
3638
+ jsonSchema: AUTOROUTING_SETUP_JSON_SCHEMA,
3639
+ validate: validateAutoroutingSetup,
3640
+ } as OptionalObjectDef<AutoroutingSetup>,
3641
+
3642
+ "task.autorouting.provenance": {
3643
+ type: "optional-object",
3644
+ default: undefined,
3645
+ jsonSchema: AUTOROUTING_PROVENANCE_JSON_SCHEMA,
3646
+ validate: validateAutoroutingProvenance,
3647
+ } as OptionalObjectDef<AutoroutingProvenance>,
3648
+
3513
3649
  "tasks.todoClearDelay": {
3514
3650
  type: "number",
3515
3651
  default: 60,
@@ -3856,7 +3992,11 @@ export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boo
3856
3992
  ? D
3857
3993
  : Schema[P] extends { type: "record"; default: infer D }
3858
3994
  ? D
3859
- : never;
3995
+ : Schema[P] extends { type: "constrained-record"; default: infer D }
3996
+ ? D
3997
+ : Schema[P] extends OptionalObjectDef<infer D>
3998
+ ? D | undefined
3999
+ : never;
3860
4000
 
3861
4001
  /** Get the default value for a setting path */
3862
4002
  export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
@@ -3930,7 +4070,11 @@ function schemaPaths(value: Record<string, unknown>, prefix = ""): string[] {
3930
4070
  const path = prefix ? `${prefix}.${key}` : key;
3931
4071
  const definition = SETTINGS_SCHEMA[path as SettingPath];
3932
4072
  // Records intentionally accept user-defined keys; validate their entries below.
3933
- if (definition?.type === "record") {
4073
+ if (
4074
+ definition?.type === "record" ||
4075
+ definition?.type === "constrained-record" ||
4076
+ definition?.type === "optional-object"
4077
+ ) {
3934
4078
  paths.push(path);
3935
4079
  } else if (child && typeof child === "object" && !Array.isArray(child)) {
3936
4080
  paths.push(...schemaPaths(child as Record<string, unknown>, path));
@@ -3974,7 +4118,15 @@ function validSettingValue(definition: (typeof SETTINGS_SCHEMA)[SettingPath], va
3974
4118
  (definition.values as readonly string[]).includes(value)) ||
3975
4119
  (definition.type === "array" &&
3976
4120
  validArraySettingValue(value, "items" in definition ? definition.items?.enum : undefined)) ||
3977
- (definition.type === "record" && !!value && typeof value === "object" && !Array.isArray(value))
4121
+ ((definition.type === "record" || definition.type === "constrained-record") &&
4122
+ !!value &&
4123
+ typeof value === "object" &&
4124
+ !Array.isArray(value)) ||
4125
+ (definition.type === "optional-object" &&
4126
+ !!value &&
4127
+ typeof value === "object" &&
4128
+ !Array.isArray(value) &&
4129
+ definition.validate(value).length === 0)
3978
4130
  );
3979
4131
  }
3980
4132
 
@@ -4022,6 +4174,16 @@ export function validateSettingPatch(patch: Record<string, unknown>): Array<{ pa
4022
4174
  issues.push({ path, detail });
4023
4175
  continue;
4024
4176
  }
4177
+ if (definition.type === "constrained-record" && path === "task.autorouting.tiers") {
4178
+ for (const issue of validateAutoroutingLocal({ tiers: value })) {
4179
+ const nestedPath = issue.path.replace(/^tiers\./u, "");
4180
+ issues.push({
4181
+ path: nestedPath ? `task.autorouting.tiers.${nestedPath}` : "task.autorouting.tiers",
4182
+ detail: issue.detail,
4183
+ });
4184
+ }
4185
+ continue;
4186
+ }
4025
4187
  if (definition.type === "record" && "valueSchema" in definition && definition.valueSchema) {
4026
4188
  for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
4027
4189
  if (!validRecordValue(definition.valueSchema, entry)) {
@@ -4065,6 +4227,33 @@ export function reconcileSettingsSchema(raw: Record<string, unknown>): {
4065
4227
  schemaSetAtPath(settings, path, next);
4066
4228
  issues.push({ path, kind: "coerced", detail: `Coerced ${typeof value} to ${definition.type}.` });
4067
4229
  }
4230
+ if (definition.type === "optional-object") {
4231
+ for (const localIssue of definition.validate(next)) {
4232
+ issues.push({
4233
+ path: localIssue.path ? `${path}.${localIssue.path}` : path,
4234
+ kind: "invalid",
4235
+ detail: localIssue.detail,
4236
+ });
4237
+ }
4238
+ continue;
4239
+ }
4240
+ if (definition.type === "constrained-record") {
4241
+ const autorouting = schemaValueAtPath(settings, "task.autorouting");
4242
+ const tiersFragment =
4243
+ autorouting && typeof autorouting === "object" && !Array.isArray(autorouting)
4244
+ ? Object.fromEntries(
4245
+ Object.entries(autorouting).filter(([key]) => key !== "setup" && key !== "provenance"),
4246
+ )
4247
+ : autorouting;
4248
+ for (const localIssue of validateAutoroutingLocal(tiersFragment)) {
4249
+ issues.push({
4250
+ path: localIssue.path ? `task.autorouting.${localIssue.path}` : "task.autorouting",
4251
+ kind: "invalid",
4252
+ detail: localIssue.detail,
4253
+ });
4254
+ }
4255
+ continue;
4256
+ }
4068
4257
  if (!validSettingValue(definition, next)) {
4069
4258
  const arrayItemEnum =
4070
4259
  definition.type === "array" && Array.isArray(next) && "items" in definition
@@ -57,6 +57,11 @@ import {
57
57
  setByPath,
58
58
  withAtomicYamlConfigTransaction,
59
59
  } from "./atomic-yaml-patch";
60
+ import {
61
+ type AutoroutingEffective,
62
+ validateAutoroutingEffective,
63
+ validateAutoroutingLocal,
64
+ } from "./autorouting-contract";
60
65
  import { isModelSelectorValue, type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
61
66
 
62
67
  import {
@@ -68,6 +73,7 @@ import {
68
73
  reconcileSettingsSchema,
69
74
  SETTINGS_SCHEMA,
70
75
  type SettingPath,
76
+ type SettingsSchemaIssue,
71
77
  type SettingsSchemaReport,
72
78
  type SettingValue,
73
79
  } from "./settings-schema";
@@ -524,6 +530,9 @@ export class Settings implements NotificationSettingsReader {
524
530
  #legacyCustomImageProviderDiagnosticLogged = false;
525
531
 
526
532
  #schemaReport: SettingsSchemaReport = { issues: [], valid: true };
533
+
534
+ #autoroutingEffective: AutoroutingEffective = { active: false };
535
+ #autoroutingLocalIssues: SettingsSchemaIssue[] = [];
527
536
  #schemaMigrationPending = false;
528
537
  /** A newer config schema must never be rewritten by legacy migrations. */
529
538
  #futureSchemaVersion = false;
@@ -692,7 +701,34 @@ export class Settings implements NotificationSettingsReader {
692
701
 
693
702
  /** Diagnostics from schema reconciliation during the most recent load. */
694
703
  getSchemaReport(): SettingsSchemaReport {
695
- return structuredClone(this.#schemaReport);
704
+ const issues = [
705
+ ...this.#schemaReport.issues.filter(
706
+ issue =>
707
+ !(
708
+ (issue.kind === "invalid" || issue.kind === "unknown") &&
709
+ (issue.path === "task.autorouting" || issue.path.startsWith("task.autorouting."))
710
+ ),
711
+ ),
712
+ ...this.#autoroutingLocalIssues,
713
+ ...(this.#autoroutingEffective.active || !this.#autoroutingEffective.issue
714
+ ? []
715
+ : [
716
+ {
717
+ path: "task.autorouting",
718
+ kind: "invalid" as const,
719
+ detail: this.#autoroutingEffective.issue.detail,
720
+ },
721
+ ]),
722
+ ];
723
+ return {
724
+ issues: structuredClone(issues),
725
+ valid: !issues.some(issue => issue.kind === "invalid"),
726
+ };
727
+ }
728
+
729
+ /** Effective merged autorouting state shared by settings diagnostics and routing policy. */
730
+ getEffectiveAutorouting(): AutoroutingEffective {
731
+ return structuredClone(this.#autoroutingEffective);
696
732
  }
697
733
 
698
734
  onChanged(listener: (path: SettingPath) => void): () => void {
@@ -1066,6 +1102,9 @@ export class Settings implements NotificationSettingsReader {
1066
1102
  });
1067
1103
  cloned.#storage = this.#storage;
1068
1104
  cloned.#schemaReport = structuredClone(this.#schemaReport);
1105
+
1106
+ cloned.#autoroutingEffective = structuredClone(this.#autoroutingEffective);
1107
+ cloned.#autoroutingLocalIssues = structuredClone(this.#autoroutingLocalIssues);
1069
1108
  cloned.#schemaMigrationPending = this.#schemaMigrationPending;
1070
1109
  cloned.#futureSchemaVersion = this.#futureSchemaVersion;
1071
1110
  cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
@@ -1601,6 +1640,15 @@ export class Settings implements NotificationSettingsReader {
1601
1640
  }
1602
1641
  setByPath(source, pathSegments, sanitized);
1603
1642
  }
1643
+
1644
+ const tiersPath = ["task", "autorouting", "tiers"];
1645
+ const tiers = getByPath(source, tiersPath);
1646
+ if (tiers === undefined) continue;
1647
+ if (!tiers || typeof tiers !== "object" || Array.isArray(tiers)) {
1648
+ logger.warn("Settings: retained malformed autorouting tier record for schema diagnostics", {
1649
+ path: tiersPath.join("."),
1650
+ });
1651
+ }
1604
1652
  }
1605
1653
  }
1606
1654
 
@@ -5860,6 +5908,18 @@ export class Settings implements NotificationSettingsReader {
5860
5908
  throw error;
5861
5909
  }
5862
5910
  }
5911
+ #recomputeAutoroutingDiagnostic(): void {
5912
+ this.#autoroutingLocalIssues = [this.#global, this.#project, this.#overrides].flatMap(source => {
5913
+ const fragment = getByPath(source, ["task", "autorouting"]);
5914
+ return validateAutoroutingLocal(fragment).map(localIssue => ({
5915
+ path: localIssue.path ? `task.autorouting.${localIssue.path}` : "task.autorouting",
5916
+ kind: "invalid" as const,
5917
+ detail: localIssue.detail,
5918
+ }));
5919
+ });
5920
+ this.#autoroutingEffective = validateAutoroutingEffective(getByPath(this.#merged, ["task", "autorouting"]));
5921
+ }
5922
+
5863
5923
  #rebuildMerged(): void {
5864
5924
  const project = structuredClone(this.#project);
5865
5925
  const overrides = structuredClone(this.#overrides);
@@ -5870,6 +5930,7 @@ export class Settings implements NotificationSettingsReader {
5870
5930
  }
5871
5931
  this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), project);
5872
5932
  this.#merged = this.#deepMerge(this.#merged, overrides);
5933
+ this.#recomputeAutoroutingDiagnostic();
5873
5934
  }
5874
5935
 
5875
5936
  #fireAllHooks(): void {
package/src/config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
- import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir } from "@gajae-code/utils";
3
+ import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir, getTrustedHomeDir } from "@gajae-code/utils";
5
4
  import { expandTilde } from "./tools/path-utils";
6
5
 
7
6
  export * from "./config/config-file";
@@ -51,7 +50,7 @@ export function getChangelogPath(): string {
51
50
  * Project-level: .gjc, .gemini
52
51
  */
53
52
  const USER_CONFIG_BASES = USER_CONFIG_PRIORITY.map(({ dir, globalAgentDir }) => ({
54
- base: () => path.join(os.homedir(), globalAgentDir ? globalAgentDir() : dir),
53
+ base: () => path.join(getTrustedHomeDir(), globalAgentDir ? globalAgentDir() : dir),
55
54
  name: dir,
56
55
  }));
57
56
 
@@ -24,7 +24,6 @@ export const COORDINATOR_MCP_TOOL_NAMES = [
24
24
  "gjc_coordinator_ack_codex_handoff",
25
25
  "gjc_delegate_plan",
26
26
  "gjc_delegate_execute",
27
- "gjc_delegate_team",
28
27
  ] as const;
29
28
 
30
29
  export type CoordinatorToolName = (typeof COORDINATOR_MCP_TOOL_NAMES)[number];
@@ -3,6 +3,12 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { withFileLock } from "../config/file-lock";
5
5
  import { assertSafeCodexEndpoint } from "./codex-wake-publisher";
6
+ import {
7
+ ensureCoordinatorDirectory,
8
+ syncCoordinatorDirectory,
9
+ syncCoordinatorFile,
10
+ writeCoordinatorAtomic,
11
+ } from "./durability";
6
12
 
7
13
  export const CODEX_WAKE_EVENT_KINDS = [
8
14
  "question.opened",
@@ -108,52 +114,71 @@ function wakeEventPath(namespaceDir: string, workUnit: string, eventSeq: number)
108
114
  return path.join(namespaceDir, "codex-wake-events", `${assertWorkUnit(workUnit)}__${assertEventSeq(eventSeq)}.json`);
109
115
  }
110
116
 
111
- async function fsyncDirectory(directory: string): Promise<void> {
112
- const handle = await fs.open(directory, "r");
113
- try {
114
- await handle.sync();
115
- } finally {
116
- await handle.close();
117
- }
118
- }
119
-
120
117
  async function writeAtomic(file: string, value: unknown): Promise<void> {
121
- await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
122
- const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
123
- const handle = await fs.open(temp, "wx", 0o600);
124
- try {
125
- await handle.writeFile(JSON.stringify(value));
126
- await handle.sync();
127
- } finally {
128
- await handle.close();
129
- }
130
- await fs.rename(temp, file);
131
- await fsyncDirectory(path.dirname(file));
118
+ await writeCoordinatorAtomic(file, JSON.stringify(value));
132
119
  }
133
120
 
134
121
  async function writeExclusive(file: string, value: unknown): Promise<boolean> {
135
- await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
122
+ await ensureCoordinatorDirectory(path.dirname(file));
136
123
  const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
137
- const handle = await fs.open(temp, "wx", 0o600);
138
- try {
139
- await handle.writeFile(JSON.stringify(value));
140
- await handle.sync();
141
- } finally {
142
- await handle.close();
143
- }
124
+ let linked = false;
125
+ let result = false;
126
+ let primaryError: unknown;
127
+ let tempCreated = false;
144
128
  try {
145
- await fs.link(temp, file);
129
+ const handle = await fs.open(temp, "wx", 0o600);
130
+ tempCreated = true;
131
+ let writeError: unknown;
132
+ try {
133
+ await handle.writeFile(JSON.stringify(value));
134
+ await syncCoordinatorFile(handle);
135
+ } catch (error) {
136
+ writeError = error;
137
+ }
138
+ try {
139
+ await handle.close();
140
+ } catch (closeError) {
141
+ if (writeError) throw new AggregateError([writeError, closeError], "exclusive write and close failed");
142
+ throw closeError;
143
+ }
144
+ if (writeError) throw writeError;
145
+ try {
146
+ await fs.link(temp, file);
147
+ linked = true;
148
+ } catch (error) {
149
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
150
+ await syncCoordinatorDirectory(path.dirname(file));
151
+ result = false;
152
+ }
153
+ if (linked) {
154
+ await syncCoordinatorDirectory(path.dirname(file));
155
+ result = true;
156
+ }
146
157
  } catch (error) {
147
- await fs.unlink(temp).catch(() => {});
148
- if ((error as NodeJS.ErrnoException).code === "EEXIST") {
149
- await fsyncDirectory(path.dirname(file));
150
- return false;
158
+ primaryError = error;
159
+ }
160
+ let cleanupError: unknown;
161
+ if (tempCreated) {
162
+ try {
163
+ await fs.unlink(temp);
164
+ } catch (error) {
165
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") cleanupError = error;
166
+ }
167
+ }
168
+ let cleanupBarrierError: unknown;
169
+ if (tempCreated && !cleanupError) {
170
+ try {
171
+ await syncCoordinatorDirectory(path.dirname(file));
172
+ } catch (error) {
173
+ cleanupBarrierError = error;
151
174
  }
152
- throw error;
153
175
  }
154
- await fs.unlink(temp);
155
- await fsyncDirectory(path.dirname(file));
156
- return true;
176
+ const failures = [primaryError, cleanupError, cleanupBarrierError].filter(
177
+ (error): error is unknown => error !== undefined,
178
+ );
179
+ if (failures.length > 1) throw new AggregateError(failures, "exclusive publication and cleanup failed");
180
+ if (failures.length === 1) throw failures[0];
181
+ return result;
157
182
  }
158
183
 
159
184
  async function readJson<T>(file: string): Promise<T | null> {
@@ -161,7 +186,8 @@ async function readJson<T>(file: string): Promise<T | null> {
161
186
  return JSON.parse(await fs.readFile(file, "utf8")) as T;
162
187
  } catch (error) {
163
188
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
164
- throw new Error("state_corrupt");
189
+ if (error instanceof SyntaxError) throw new Error("state_corrupt");
190
+ throw error;
165
191
  }
166
192
  }
167
193
 
@@ -196,7 +222,7 @@ function assertCodexHandoffOrigin(value: unknown): asserts value is CodexHandoff
196
222
  (typeof origin.codex_host_session_id === "string" && SAFE_ID.test(origin.codex_host_session_id))
197
223
  ) ||
198
224
  !(typeof origin.delegation_id === "string" && SAFE_ID.test(origin.delegation_id)) ||
199
- !["plan", "execute", "team"].includes(origin.workflow as string) ||
225
+ !["plan", "execute"].includes(origin.workflow as string) ||
200
226
  !(typeof origin.bound_at === "string" && Number.isFinite(Date.parse(origin.bound_at)))
201
227
  )
202
228
  throw new Error("state_corrupt");
@@ -343,7 +369,7 @@ export async function listCodexHandoffs(namespaceDir: string): Promise<CodexHand
343
369
  names = await fs.readdir(directory);
344
370
  } catch (error) {
345
371
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
346
- throw new Error("state_corrupt");
372
+ throw error;
347
373
  }
348
374
  const handoffs: CodexHandoffRegistrationV1[] = [];
349
375
  for (const name of names) {
@@ -418,7 +444,7 @@ export async function updateCodexWakeEvent(
418
444
  const file = eventPathForKey(namespaceDir, key);
419
445
  if (patch.status !== undefined && !["pending", "published", "acked", "failed"].includes(patch.status))
420
446
  throw new Error("invalid_wake_event_status");
421
- if (patch.attempts_delta !== undefined && !Number.isInteger(patch.attempts_delta))
447
+ if (patch.attempts_delta !== undefined && (!Number.isSafeInteger(patch.attempts_delta) || patch.attempts_delta < 0))
422
448
  throw new Error("invalid_attempts_delta");
423
449
  return await withFileLock(file, async () => {
424
450
  const event = await readJson<CodexWakeEventV1>(file);
@@ -457,7 +483,7 @@ export async function listCodexWakeEvents(namespaceDir: string, workUnit?: strin
457
483
  names = await fs.readdir(directory);
458
484
  } catch (error) {
459
485
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
460
- throw new Error("state_corrupt");
486
+ throw error;
461
487
  }
462
488
  const events: CodexWakeEventV1[] = [];
463
489
  for (const name of names) {