@gajae-code/coding-agent 0.13.3 → 0.14.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 (599) hide show
  1. package/CHANGELOG.md +184 -2
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +1 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -1,7 +1,12 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import * as nodeCrypto from "node:crypto";
2
+ import * as fsSync from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
5
+ import type { NativeExactFileIdentity, NativeExactUnlinkResult, NativeNoReplaceResult } from "@gajae-code/natives";
6
+ import { exactReplacePath, exactUnlink } from "@gajae-code/natives";
7
+ import { isCompiledBinary } from "@gajae-code/utils/env";
4
8
  import { YAML } from "bun";
9
+ import type { AtomicYamlNativeWorkerRequest, AtomicYamlNativeWorkerResponse } from "./atomic-yaml-patch-worker";
5
10
  import { withFileLock } from "./file-lock";
6
11
 
7
12
  export interface AtomicYamlExpectedPrecondition {
@@ -49,7 +54,8 @@ export interface AtomicYamlPatchRevision {
49
54
  export type CasRestoreResult =
50
55
  | { status: "restored"; receipt: CasReceipt }
51
56
  | { status: "conflict"; paths: readonly string[] }
52
- | { status: "discarded" };
57
+ | { status: "discarded" }
58
+ | { status: "not-restorable" };
53
59
 
54
60
  /**
55
61
  * A receipt intentionally exposes only path-level hashes and opaque revisions.
@@ -70,6 +76,15 @@ export interface AtomicYamlUpdate<T> {
70
76
  export interface AtomicYamlPatchOptions {
71
77
  /** Test seam for deterministic pre-rename and Windows sharing-violation failures. */
72
78
  rename?: (from: string, to: string) => Promise<void>;
79
+ /** Test seam for an identity-checked atomic replacement. */
80
+ exactReplace?: (
81
+ sourcePath: string,
82
+ destinationPath: string,
83
+ expectedSource: NativeExactFileIdentity,
84
+ expectedDestination: NativeExactFileIdentity,
85
+ ) => NativeExactUnlinkResult | Promise<NativeExactUnlinkResult>;
86
+ /** Test seam for an atomic no-replace publication. */
87
+ noReplace?: (sourcePath: string, destinationPath: string) => NativeNoReplaceResult | Promise<NativeNoReplaceResult>;
73
88
  /** Test seam for bounded retry timing. */
74
89
  sleep?: (ms: number) => Promise<void>;
75
90
  /** Test seam for Windows rename retry behavior. */
@@ -88,12 +103,36 @@ export class AtomicYamlReplaceError extends Error {
88
103
  readonly configPath: string,
89
104
  readonly attempts: number,
90
105
  readonly cause: unknown,
106
+ readonly preserveTempPath = false,
91
107
  ) {
92
108
  super(`Failed to atomically replace ${configPath} after ${attempts} rename attempts.`);
93
109
  this.name = "AtomicYamlReplaceError";
94
110
  }
95
111
  }
96
112
 
113
+ /**
114
+ * Raised when the LEXICAL config path no longer resolves to the canonical
115
+ * target an operation is about to write: e.g. a `config.yml` symlink was
116
+ * repointed while the operation waited in the per-file queue or ran. The
117
+ * native identity check protects only the previously resolved destination, so
118
+ * without this guard the write would modify an unrelated prior target and
119
+ * report success while the now-active configuration is left unchanged.
120
+ */
121
+ export class AtomicYamlRetargetError extends Error {
122
+ readonly code = "ATOMIC_YAML_RETARGETED";
123
+
124
+ constructor(
125
+ readonly lexicalPath: string,
126
+ readonly expectedCanonicalPath: string,
127
+ readonly actualCanonicalPath: string,
128
+ ) {
129
+ super(
130
+ `Atomic YAML target retargeted: ${lexicalPath} resolves to ${actualCanonicalPath}, expected ${expectedCanonicalPath}.`,
131
+ );
132
+ this.name = "AtomicYamlRetargetError";
133
+ }
134
+ }
135
+
97
136
  type PathState = { exists: boolean; value: unknown };
98
137
  type ReceiptChange = {
99
138
  path: string;
@@ -109,7 +148,109 @@ const WINDOWS_RENAME_BACKOFF_MS = [10, 25, 50, 100, 200] as const;
109
148
  const WINDOWS_SHARING_VIOLATION_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
110
149
 
111
150
  function canonicalConfigPath(configPath: string): string {
112
- return path.normalize(path.resolve(configPath));
151
+ const resolved = path.normalize(path.resolve(configPath));
152
+ try {
153
+ // Follow a symlinked config (e.g. `.gjc/config.yml` -> `../shared/config.yml`)
154
+ // so read, identity capture, and the native exact exchange all operate on the
155
+ // real target consistently: the native replacement opens the destination with
156
+ // no-follow semantics and would reject the symlink entry itself. An absent or
157
+ // dangling path falls back to the normalized path (first-time creation still
158
+ // publishes through the no-replace path).
159
+ return fsSync.realpathSync(resolved);
160
+ } catch {
161
+ return resolveDanglingSymlink(resolved);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Reject a symlink retarget right before publication: the lexical config path
167
+ * (e.g. `.gjc/config.yml`) must still resolve to the canonical target this
168
+ * operation read, or the write would modify an unrelated prior target and
169
+ * report success while the now-active configuration is unchanged. The native
170
+ * identity check protects only the previously resolved destination, so this
171
+ * lexical re-resolution is the retarget guard.
172
+ */
173
+ function assertLexicalTargetUnchanged(lexicalPath: string, canonicalPath: string): void {
174
+ const current = canonicalConfigPath(lexicalPath);
175
+ if (current !== canonicalPath) {
176
+ throw new AtomicYamlRetargetError(lexicalPath, canonicalPath, current);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * realpathSync fails when the final path component is absent or a DANGLING
182
+ * symlink (its target has not been created yet). Treating the symlink pathname
183
+ * itself as the config path would read it as absent while the native no-replace
184
+ * publication rejects the EXISTING symlink directory entry as
185
+ * `destination_exists`, so the first publication must target the link's lexical
186
+ * target instead. An ABSENT final component still resolves its deepest existing
187
+ * ancestor (symlinked parents included), so a parent symlink repointed while
188
+ * the operation waits is caught by the lexical retarget check instead of
189
+ * comparing two unresolved lexical strings. Resolves a chain of dangling links
190
+ * without requiring the target to exist; genuinely absent paths are resolved
191
+ * through their existing ancestors.
192
+ */
193
+ function resolveDanglingSymlink(configPath: string): string {
194
+ const ancestorResolved = resolveAbsentThroughAncestors(configPath);
195
+ if (ancestorResolved !== null) return ancestorResolved;
196
+ // The path itself exists: it may be a dangling final-component symlink
197
+ // (link target absent) or an unreadable entry; walk the link chain.
198
+ let current = configPath;
199
+ for (let depth = 0; depth < 40; depth++) {
200
+ let stat: fsSync.Stats;
201
+ try {
202
+ stat = fsSync.lstatSync(current);
203
+ } catch {
204
+ // The link chain walked into an ABSENT path: resolve the deepest
205
+ // existing ancestor (symlinked parents realpath'd) and re-append
206
+ // the missing suffix, exactly like the absent-top-level case. The
207
+ // unresolved lexical string would not capture a symlinked parent's
208
+ // identity, so repointing that parent while the operation waits
209
+ // must be detectable by the retarget check instead of comparing
210
+ // two identical unresolved strings.
211
+ return resolveAbsentThroughAncestors(current) ?? current;
212
+ }
213
+ if (!stat.isSymbolicLink()) return current;
214
+ let target: string;
215
+ try {
216
+ target = fsSync.readlinkSync(current);
217
+ } catch {
218
+ return current;
219
+ }
220
+ current = path.isAbsolute(target) ? path.normalize(target) : path.resolve(path.dirname(current), target);
221
+ }
222
+ return current;
223
+ }
224
+
225
+ /**
226
+ * Find the deepest EXISTING ancestor of `target`, realpath-resolve it (its
227
+ * symlinks included), and re-append the missing suffix. Returns null when no
228
+ * existing ancestor exists (the filesystem root is reached without an lstat
229
+ * success), leaving the caller free to fall back to the lexical path.
230
+ */
231
+ function resolveAbsentThroughAncestors(target: string): string | null {
232
+ const missing: string[] = [];
233
+ let existing = target;
234
+ for (;;) {
235
+ try {
236
+ fsSync.lstatSync(existing);
237
+ break;
238
+ } catch {
239
+ const parent = path.dirname(existing);
240
+ if (parent === existing) return null; // filesystem root reached
241
+ missing.unshift(path.basename(existing));
242
+ existing = parent;
243
+ }
244
+ }
245
+ // The path itself exists (e.g. a dangling symlink whose chain the caller
246
+ // walks): only a MISSING component is resolved through ancestors, so the
247
+ // caller's link-chain walk owns the existing-path case.
248
+ if (missing.length === 0) return null;
249
+ try {
250
+ return path.join(fsSync.realpathSync(existing), ...missing);
251
+ } catch {
252
+ return path.join(existing, ...missing);
253
+ }
113
254
  }
114
255
 
115
256
  function assertPatch(patch: AtomicYamlPatch): void {
@@ -197,7 +338,8 @@ function stableValue(value: unknown): string {
197
338
  }
198
339
 
199
340
  function stateHash(state: PathState): string {
200
- return createHash("sha256")
341
+ return nodeCrypto
342
+ .createHash("sha256")
201
343
  .update(state.exists ? `present:${stableValue(state.value)}` : "absent")
202
344
  .digest("hex");
203
345
  }
@@ -211,21 +353,31 @@ export function atomicYamlPathHash(value: Record<string, unknown>, path: string)
211
353
  return stateHash(stateAtPath(value, path.split(".")));
212
354
  }
213
355
 
214
- type YamlReadResult = {
356
+ type YamlFileState = {
357
+ exists: boolean;
358
+ raw: string;
359
+ };
360
+
361
+ type YamlReadResult = YamlFileState & {
215
362
  current: Record<string, unknown>;
216
363
  root: unknown;
217
364
  };
218
365
 
219
- async function readYaml(configPath: string): Promise<YamlReadResult> {
366
+ async function readYamlFileState(configPath: string): Promise<YamlFileState> {
220
367
  try {
221
- const root = YAML.parse(await fs.readFile(configPath, "utf8"));
222
- return { current: record(root) ?? {}, root };
368
+ return { exists: true, raw: await Bun.file(configPath).text() };
223
369
  } catch (error) {
224
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return { current: {}, root: undefined };
370
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { exists: false, raw: "" };
225
371
  throw error;
226
372
  }
227
373
  }
228
374
 
375
+ async function readYaml(configPath: string): Promise<YamlReadResult> {
376
+ const file = await readYamlFileState(configPath);
377
+ const root = file.exists ? YAML.parse(file.raw) : undefined;
378
+ return { ...file, current: record(root) ?? {}, root };
379
+ }
380
+
229
381
  async function syncParentDirectory(directory: string): Promise<void> {
230
382
  try {
231
383
  const directoryHandle = await fs.open(directory, "r");
@@ -240,12 +392,247 @@ async function syncParentDirectory(directory: string): Promise<void> {
240
392
  }
241
393
  }
242
394
 
243
- async function replaceWithRetry(tempPath: string, configPath: string, options: AtomicYamlPatchOptions): Promise<void> {
395
+ async function captureExactFileIdentity(file: string): Promise<NativeExactFileIdentity | null> {
396
+ try {
397
+ const [bytes, stat, parent] = await Promise.all([
398
+ Bun.file(file).arrayBuffer(),
399
+ fs.stat(file, { bigint: true }),
400
+ fs.stat(path.dirname(file), { bigint: true }),
401
+ ]);
402
+ return {
403
+ dev: stat.dev,
404
+ ino: stat.ino,
405
+ nlink: stat.nlink,
406
+ parentDev: parent.dev,
407
+ parentIno: parent.ino,
408
+ size: stat.size,
409
+ mtimeNs: stat.mtimeNs,
410
+ sha256: nodeCrypto.createHash("sha256").update(Buffer.from(bytes)).digest("hex"),
411
+ };
412
+ } catch (error) {
413
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
414
+ throw error;
415
+ }
416
+ }
417
+
418
+ async function currentYamlFileState(configPath: string): Promise<YamlFileState> {
419
+ return await readYamlFileState(configPath);
420
+ }
421
+
422
+ async function runNativeAtomicYamlWorker(
423
+ request: AtomicYamlNativeWorkerRequest,
424
+ ): Promise<NativeExactUnlinkResult | NativeNoReplaceResult> {
425
+ const worker = isCompiledBinary()
426
+ ? new Worker("./packages/coding-agent/src/config/atomic-yaml-patch-worker.ts", { type: "module" })
427
+ : new Worker(new URL("./atomic-yaml-patch-worker.ts", import.meta.url).href, { type: "module" });
428
+ const { promise, resolve, reject } = Promise.withResolvers<NativeExactUnlinkResult | NativeNoReplaceResult>();
429
+ let settled = false;
430
+ let cleanedUp = false;
431
+ const cleanup = (): void => {
432
+ if (cleanedUp) return;
433
+ cleanedUp = true;
434
+ worker.removeEventListener("message", onMessage);
435
+ worker.removeEventListener("error", onError);
436
+ worker.removeEventListener("messageerror", onMessageError);
437
+ worker.removeEventListener("close", onClose);
438
+ worker.removeEventListener("exit", onExit);
439
+ };
440
+ const succeed = (result: NativeExactUnlinkResult | NativeNoReplaceResult): void => {
441
+ if (settled) return;
442
+ settled = true;
443
+ cleanup();
444
+ resolve(result);
445
+ };
446
+ const fail = (error: Error): void => {
447
+ if (settled) return;
448
+ settled = true;
449
+ cleanup();
450
+ reject(error);
451
+ };
452
+ const onMessage: EventListener = event => {
453
+ const response = (event as MessageEvent<AtomicYamlNativeWorkerResponse>).data;
454
+ if (response?.type === "result") {
455
+ succeed(response.result);
456
+ return;
457
+ }
458
+ if (response?.type === "error") {
459
+ const failure = new Error(response.message);
460
+ if (response.name) failure.name = response.name;
461
+ if (response.stack) failure.stack = response.stack;
462
+ fail(failure);
463
+ return;
464
+ }
465
+ fail(new Error("Atomic YAML native worker returned an invalid response."));
466
+ };
467
+ const onError: EventListener = () => fail(new Error("Atomic YAML native worker failed."));
468
+ const onMessageError: EventListener = () => fail(new Error("Atomic YAML native worker message failed."));
469
+ // Bun emits `close`; Node-compatible worker hosts may emit `exit` instead.
470
+ const onClose: EventListener = () => fail(new Error("Atomic YAML native worker closed before responding."));
471
+ const onExit: EventListener = () => fail(new Error("Atomic YAML native worker exited before responding."));
472
+ try {
473
+ worker.addEventListener("message", onMessage);
474
+ worker.addEventListener("error", onError);
475
+ worker.addEventListener("messageerror", onMessageError);
476
+ worker.addEventListener("close", onClose);
477
+ worker.addEventListener("exit", onExit);
478
+ worker.postMessage(request);
479
+ return await promise;
480
+ } catch (error) {
481
+ // The native op was already dispatched; a termination without a response
482
+ // leaves the exchange outcome unknown (it may have committed), so the
483
+ // staged path must be retained for recovery instead of unlinked.
484
+ throw new AtomicYamlReplaceError(request.destinationPath, 1, error, true);
485
+ } finally {
486
+ cleanup();
487
+ worker.terminate();
488
+ }
489
+ }
490
+
491
+ async function replaceWithExpectedIdentity(
492
+ tempPath: string,
493
+ configPath: string,
494
+ expectedState: YamlFileState,
495
+ options: AtomicYamlPatchOptions,
496
+ ): Promise<void> {
497
+ const { exists: expectedExists, raw: expectedRaw } = expectedState;
498
+ const expectedHash = nodeCrypto.createHash("sha256").update(expectedRaw).digest("hex");
499
+ const [source, destination] = await Promise.all([
500
+ captureExactFileIdentity(tempPath),
501
+ captureExactFileIdentity(configPath),
502
+ ]);
503
+ if (!source)
504
+ throw new AtomicYamlReplaceError(configPath, 1, new Error("staged YAML disappeared before replacement"));
505
+ if (!destination) {
506
+ if (expectedExists)
507
+ throw new AtomicYamlConflictError(
508
+ configPath,
509
+ expectedHash,
510
+ nodeCrypto.createHash("sha256").update("").digest("hex"),
511
+ );
512
+ const publishOnce = async (): Promise<NativeNoReplaceResult> =>
513
+ options.noReplace
514
+ ? await options.noReplace(tempPath, configPath)
515
+ : ((await runNativeAtomicYamlWorker({
516
+ operation: "no-replace",
517
+ sourcePath: tempPath,
518
+ destinationPath: configPath,
519
+ })) as NativeNoReplaceResult);
520
+ const publishOnceWithLink = async (): Promise<NativeNoReplaceResult> =>
521
+ options.noReplace
522
+ ? await options.noReplace(tempPath, configPath)
523
+ : ((await runNativeAtomicYamlWorker({
524
+ operation: "no-replace-link",
525
+ sourcePath: tempPath,
526
+ destinationPath: configPath,
527
+ })) as NativeNoReplaceResult);
528
+ let published = await publishOnce();
529
+ if (
530
+ !published.ok &&
531
+ (published.reason === "invalid_request" || published.reason === "atomic_unavailable") &&
532
+ published.mutationState === "not_committed" &&
533
+ published.durabilityState === "not_attempted"
534
+ ) {
535
+ // RENAME_NOREPLACE is unsupported (NFS, kernels before 3.15): the native
536
+ // linkat fallback keeps the no-overwrite guarantee (EEXIST on an
537
+ // occupied destination) and is safe because the rename reported a
538
+ // pre-mutation refusal. The staged name survives a link, so the caller
539
+ // still unlinks it after publication.
540
+ published = await publishOnceWithLink();
541
+ }
542
+ if (published.ok) return;
543
+ if (published.reason === "destination_exists") {
544
+ const actual = await currentYamlFileState(configPath);
545
+ throw new AtomicYamlConflictError(
546
+ configPath,
547
+ expectedHash,
548
+ nodeCrypto.createHash("sha256").update(actual.raw).digest("hex"),
549
+ );
550
+ }
551
+ throw new AtomicYamlReplaceError(
552
+ configPath,
553
+ 1,
554
+ new Error(`native no-replace publish failed: ${published.code ?? "unknown"}`),
555
+ published.mutationState === "unknown",
556
+ );
557
+ }
558
+ if (!expectedExists || destination.sha256 !== expectedHash) {
559
+ throw new AtomicYamlConflictError(configPath, expectedHash, destination.sha256 ?? expectedHash);
560
+ }
561
+ // `exactReplacePath` validates both identities inside one native atomic
562
+ // namespace exchange. A save after the snapshots therefore rejects the
563
+ // exchange without replacing the editor's newer destination.
564
+ const replaced: NativeExactUnlinkResult = options.exactReplace
565
+ ? await options.exactReplace(tempPath, configPath, source, destination)
566
+ : ((await runNativeAtomicYamlWorker({
567
+ operation: "exact-replace",
568
+ sourcePath: tempPath,
569
+ destinationPath: configPath,
570
+ expectedSource: source,
571
+ expectedDestination: destination,
572
+ })) as NativeExactUnlinkResult);
573
+ if (replaced.ok) return;
574
+ // RENAME_EXCHANGE is unsupported (NFS, kernels before 3.15): the native
575
+ // exact-replace reports atomic_unavailable. A replacement fallback that
576
+ // briefly detaches the destination would expose an ABSENT config.yml to
577
+ // concurrent readers (workflow resolvers do not take the config lock) that
578
+ // could then fall back to legacy/default settings - so the write FAILS
579
+ // CLOSED instead of trading a transient absence window for availability.
580
+ if (replaced.code === "atomic_unavailable") {
581
+ throw new AtomicYamlReplaceError(
582
+ configPath,
583
+ 1,
584
+ new Error(
585
+ `native exact replacement is unavailable on this filesystem (atomic_unavailable); refusing a fallback that would expose an absent config.yml to concurrent readers`,
586
+ ),
587
+ );
588
+ }
589
+ // A retained successor proves the namespace exchange published the staged
590
+ // document. It may still report a post-exchange verification or durability
591
+ // failure, so this is not a CAS rejection and the detached temp-path object
592
+ // must be retained for native recovery rather than unlinked by the caller.
593
+ if (
594
+ replaced.detachedPath ||
595
+ replaced.retainedSuccessorPath ||
596
+ replaced.retainedPlaceholderPath ||
597
+ replaced.retainedUnknownPath
598
+ ) {
599
+ throw new AtomicYamlReplaceError(
600
+ configPath,
601
+ 1,
602
+ new Error(`native exact replacement completed with retained recovery paths: ${replaced.code ?? "unknown"}`),
603
+ true,
604
+ );
605
+ }
606
+ if (replaced.code === "identity_mismatch") {
607
+ const actual = await currentYamlFileState(configPath);
608
+ throw new AtomicYamlConflictError(
609
+ configPath,
610
+ expectedHash,
611
+ nodeCrypto.createHash("sha256").update(actual.raw).digest("hex"),
612
+ );
613
+ }
614
+ throw new AtomicYamlReplaceError(
615
+ configPath,
616
+ 1,
617
+ new Error(`native exact replacement failed: ${replaced.code ?? "unknown"}`),
618
+ );
619
+ }
620
+
621
+ async function replaceWithRetry(
622
+ tempPath: string,
623
+ configPath: string,
624
+ options: AtomicYamlPatchOptions,
625
+ /** Expected current file state; native exact replacement validates it with the destination identity. */
626
+ expectedState?: YamlFileState,
627
+ ): Promise<void> {
628
+ if (expectedState !== undefined) {
629
+ await replaceWithExpectedIdentity(tempPath, configPath, expectedState, options);
630
+ return;
631
+ }
244
632
  const rename = options.rename ?? fs.rename;
245
633
  const sleep = options.sleep ?? (async (delay: number): Promise<void> => await Bun.sleep(delay));
246
634
  const isWindows = (options.platform ?? process.platform) === "win32";
247
635
  let attempts = 0;
248
-
249
636
  for (;;) {
250
637
  attempts++;
251
638
  try {
@@ -269,22 +656,168 @@ async function writeAtomicYaml(
269
656
  configPath: string,
270
657
  value: Record<string, unknown>,
271
658
  options: AtomicYamlPatchOptions,
272
- ): Promise<void> {
659
+ /** Expected current file state; re-verified immediately before the rename. */
660
+ expectedState?: YamlFileState,
661
+ /** Lexical (unresolved) config path; re-verified against the canonical target before the rename. */
662
+ lexicalConfigPath?: string,
663
+ ): Promise<YamlFileState> {
273
664
  const directory = path.dirname(configPath);
274
- const tempPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
665
+ const tempPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.tmp`);
666
+ const nextRaw = YAML.stringify(value, null, 2);
667
+ let preserveTempPath = false;
275
668
  try {
276
669
  const tempHandle = await fs.open(tempPath, "wx", 0o600);
277
670
  try {
278
- await tempHandle.writeFile(YAML.stringify(value, null, 2), "utf8");
671
+ await tempHandle.writeFile(nextRaw, "utf8");
279
672
  await tempHandle.sync();
280
673
  } finally {
281
674
  await tempHandle.close();
282
675
  }
283
- await replaceWithRetry(tempPath, configPath, options);
676
+ // Reject a lexical retarget (e.g. a repointed config.yml symlink) before
677
+ // the rename, whether it happened while the operation waited in the queue
678
+ // or while it ran; then re-verify the CAS state right before the rename so
679
+ // an external save during the temp write cannot be silently overwritten.
680
+ if (lexicalConfigPath !== undefined) assertLexicalTargetUnchanged(lexicalConfigPath, configPath);
681
+ if (expectedState !== undefined) {
682
+ // The transaction's CAS guard ran before this write; re-verify right
683
+ // before the rename so an external save during the temp write cannot be
684
+ // silently overwritten by the replacement.
685
+ const currentState = await currentYamlFileState(configPath);
686
+ if (currentState.exists !== expectedState.exists || currentState.raw !== expectedState.raw) {
687
+ throw new AtomicYamlConflictError(
688
+ configPath,
689
+ nodeCrypto.createHash("sha256").update(expectedState.raw).digest("hex"),
690
+ nodeCrypto.createHash("sha256").update(currentState.raw).digest("hex"),
691
+ );
692
+ }
693
+ }
694
+ // Capture whether the canonical target existed BEFORE the publication:
695
+ // the retarget rollback must distinguish a first-write (restore absence)
696
+ // from an exchange (restore the pre-publication content). For
697
+ // nontransactional writers expectedState is undefined, so probe the
698
+ // filesystem here - after this point the publication changes the answer.
699
+ const targetExistedBefore =
700
+ expectedState?.exists ??
701
+ (await fs
702
+ .lstat(configPath)
703
+ .then(() => true)
704
+ .catch(() => false));
705
+ let publishedIdentity: NativeExactFileIdentity | null = null;
706
+ publishedIdentity = await captureExactFileIdentity(tempPath);
707
+ try {
708
+ await replaceWithRetry(tempPath, configPath, options, expectedState);
709
+ } catch (error) {
710
+ preserveTempPath = error instanceof AtomicYamlReplaceError && error.preserveTempPath;
711
+ throw error;
712
+ }
713
+ // Revalidate the lexical target AFTER the publication: the native
714
+ // exchange validated only the previously resolved canonical
715
+ // destination, so a config.yml symlink repointed between the
716
+ // pre-publication retarget check and the exchange would update the
717
+ // now-inactive target and report success while the lexical config.yml
718
+ // points at an unchanged file. The old canonical state is restored
719
+ // before failing: retrying must update the new target without leaving
720
+ // the now-inactive profile modified as a side effect.
721
+ if (lexicalConfigPath !== undefined) {
722
+ try {
723
+ assertLexicalTargetUnchanged(lexicalConfigPath, configPath);
724
+ } catch (error) {
725
+ let rollbackFailed: Error | null = null;
726
+ try {
727
+ if (!targetExistedBefore) {
728
+ // Restore absence with an IDENTITY-ATOMIC unlink: the native
729
+ // validates the destination identity in the same syscall
730
+ // sequence as the unlink, so a successor appearing after
731
+ // our earlier verification is retained as a conflict.
732
+ if (publishedIdentity === null) {
733
+ throw new Error("no published identity available to remove");
734
+ }
735
+ // The native exact unlink requires a caller-persisted,
736
+ // single-component quarantine destination (authority survives
737
+ // a post-detach crash); the placeholder is exchanged into it
738
+ // and cleaned up within the same protocol.
739
+ const removed = exactUnlink(configPath, {
740
+ ...publishedIdentity,
741
+ quarantineName: `.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.quarantine`,
742
+ });
743
+ // cleanup_pending with a detached path + the canonical pathname
744
+ // proven absent counts as removed (the native retirement
745
+ // protocol retains the placeholder for crash recovery); the
746
+ // retained artifacts are purged best-effort below.
747
+ const detached =
748
+ removed.ok || (removed.code === "cleanup_pending" && typeof removed.detachedPath === "string");
749
+ if (!detached) {
750
+ throw new Error(
751
+ "the canonical target was replaced by another writer; the first-write publication was not removed",
752
+ );
753
+ }
754
+ if (removed.retainedPlaceholderPath) {
755
+ await fs.rm(removed.retainedPlaceholderPath, { force: true }).catch(() => undefined);
756
+ }
757
+ if (removed.detachedPath) {
758
+ await fs.rm(removed.detachedPath, { force: true }).catch(() => undefined);
759
+ }
760
+ } else {
761
+ // EXISTING target: restore the pre-publication content
762
+ // captured by the CAS state through an identity-checked
763
+ // EXCHANGE. The native validates the destination identity
764
+ // atomically with the replacement, so a concurrent update
765
+ // is a rollback conflict, never clobbered with the stale
766
+ // bytes. The exchange unlinks both paths, so the original
767
+ // temp path is NOT a recovery source.
768
+ if (expectedState?.raw === undefined) {
769
+ throw new Error("no pre-publication content available to restore");
770
+ }
771
+ if (publishedIdentity === null) {
772
+ throw new Error("no published identity available to restore over");
773
+ }
774
+ const restoreTemp = path.join(
775
+ directory,
776
+ `.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.restore.tmp`,
777
+ );
778
+ try {
779
+ // Create the restore staging file with PRIVATE permissions from
780
+ // the outset (same wx + 0o600 pattern as the main temp path):
781
+ // a directory watcher must never observe the previous
782
+ // configuration at mode 0644 before a post-write chmod.
783
+ const restoreHandle = await fs.open(restoreTemp, "wx", 0o600);
784
+ try {
785
+ await restoreHandle.writeFile(expectedState.raw, "utf8");
786
+ } finally {
787
+ await restoreHandle.close();
788
+ }
789
+ const restoreIdentity = await captureExactFileIdentity(restoreTemp);
790
+ if (restoreIdentity === null) {
791
+ throw new Error("staged restore disappeared");
792
+ }
793
+ const replaced = exactReplacePath(restoreTemp, configPath, restoreIdentity, publishedIdentity);
794
+ if (!replaced.ok) {
795
+ throw new Error(
796
+ "the canonical target was replaced by another writer; the old content was not restored",
797
+ );
798
+ }
799
+ } finally {
800
+ await fs.rm(restoreTemp, { force: true }).catch(() => undefined);
801
+ }
802
+ }
803
+ } catch (rollbackError) {
804
+ rollbackFailed = rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError));
805
+ }
806
+ if (rollbackFailed !== null) {
807
+ throw new AtomicYamlReplaceError(
808
+ configPath,
809
+ 1,
810
+ new Error(`Atomic YAML target retargeted and rollback failed: ${rollbackFailed.message}`),
811
+ );
812
+ }
813
+ throw error;
814
+ }
815
+ }
284
816
  await syncParentDirectory(directory);
285
817
  } finally {
286
- await fs.rm(tempPath, { force: true }).catch(() => undefined);
818
+ if (!preserveTempPath) await fs.rm(tempPath, { force: true }).catch(() => undefined);
287
819
  }
820
+ return { exists: true, raw: nextRaw };
288
821
  }
289
822
 
290
823
  function createReceipt(
@@ -302,7 +835,7 @@ function createReceipt(
302
835
  },
303
836
  async restore(): Promise<CasRestoreResult> {
304
837
  if (discarded) return { status: "discarded" };
305
- return await enqueueAtomicYamlOperation(configPath, async canonicalPath => {
838
+ return await enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
306
839
  if (discarded) return { status: "discarded" };
307
840
  return await withFileLock(canonicalPath, async () => {
308
841
  const { current, root } = await readYaml(canonicalPath);
@@ -320,7 +853,15 @@ function createReceipt(
320
853
  : { path: change.path, op: "unset" },
321
854
  );
322
855
  await options.validateRoot?.(root, restorePatches);
323
- const receipt = await applyPatchesUnderLock(canonicalPath, current, restorePatches, options);
856
+ const receipt = await applyPatchesUnderLock(
857
+ canonicalPath,
858
+ current,
859
+ restorePatches,
860
+ options,
861
+ false,
862
+ undefined,
863
+ lexicalPath,
864
+ );
324
865
  await options.onRestored?.(restorePatches);
325
866
  return { status: "restored", receipt };
326
867
  });
@@ -334,6 +875,9 @@ async function applyPatchesUnderLock(
334
875
  current: Record<string, unknown>,
335
876
  patches: readonly AtomicYamlPatch[],
336
877
  options: AtomicYamlPatchOptions,
878
+ skipWrite = false,
879
+ expectedState?: YamlFileState,
880
+ lexicalConfigPath?: string,
337
881
  ): Promise<CasReceipt> {
338
882
  if (patches.length === 0) return createReceipt(configPath, [], options);
339
883
 
@@ -373,7 +917,7 @@ async function applyPatchesUnderLock(
373
917
  }
374
918
  const changes = [...changesByPath.values()];
375
919
 
376
- await writeAtomicYaml(configPath, current, options);
920
+ if (!skipWrite) await writeAtomicYaml(configPath, current, options, expectedState, lexicalConfigPath);
377
921
  return createReceipt(configPath, changes, options);
378
922
  }
379
923
 
@@ -385,14 +929,187 @@ export function applyAtomicYamlPatchesWithCurrent(
385
929
  ) => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
386
930
  options: AtomicYamlPatchOptions = {},
387
931
  ): Promise<CasReceipt> {
388
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
932
+ return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
389
933
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
390
934
  return await withFileLock(canonicalPath, async () => {
391
935
  const { current, root } = await readYaml(canonicalPath);
392
936
  const patches = await buildPatches(current);
393
937
  for (const patch of patches) assertPatch(patch);
394
938
  await options.validateRoot?.(root, patches);
395
- return await applyPatchesUnderLock(canonicalPath, current, patches, options);
939
+ return await applyPatchesUnderLock(canonicalPath, current, patches, options, false, undefined, lexicalPath);
940
+ });
941
+ });
942
+ }
943
+ export interface AtomicYamlConfigTransaction {
944
+ configPath: string;
945
+ root: unknown;
946
+ current: Readonly<Record<string, unknown>>;
947
+ /** True once any write op has durably committed (a later CAS rejection then
948
+ * leaves the target with partial writes, so recovery artifacts must stay). */
949
+ written: boolean;
950
+ applyPatches(patches: readonly AtomicYamlPatch[], options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
951
+ /**
952
+ * Delete top-level keys verbatim, including dotted key names (e.g. a flat
953
+ * `"gjc.ralplan.maxIterations"` key that the patch grammar would otherwise
954
+ * interpret as a nested path). Writes atomically under the same lock.
955
+ */
956
+ removeTopLevelKeys(keys: readonly string[], options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
957
+ /**
958
+ * Apply patches AND delete top-level keys verbatim in a SINGLE atomic
959
+ * write, so an external editor's change cannot land between the two
960
+ * operations (external editors do not participate in the file lock). The
961
+ * returned receipt is not restorable (the deletions are not journaled).
962
+ */
963
+ applyPatchesAndRemoveTopLevelKeys(
964
+ patches: readonly AtomicYamlPatch[],
965
+ topLevelKeys: readonly string[],
966
+ options?: AtomicYamlPatchOptions,
967
+ ): Promise<CasReceipt>;
968
+ /**
969
+ * Replace the whole document (used to revert the target when a later
970
+ * verification fails). Writes atomically under the same lock; the returned
971
+ * receipt is not restorable.
972
+ */
973
+ replaceCurrent(next: Readonly<Record<string, unknown>>, options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
974
+ }
975
+
976
+ /**
977
+ * Run a caller-owned multi-step mutation under the config file's per-file queue
978
+ * and cross-process lock. The current YAML is read once and exposed as
979
+ * `root`/`current`; the callback may inspect it, decide patches, and apply them
980
+ * (or perform adjacent durable actions such as marker/source transitions)
981
+ * without re-acquiring the lock. A YAML parse failure surfaces before the
982
+ * callback runs, so no migration action can execute against a malformed target.
983
+ */
984
+ export function withAtomicYamlConfigTransaction<T>(
985
+ configPath: string,
986
+ operation: (transaction: AtomicYamlConfigTransaction) => Promise<T>,
987
+ ): Promise<T> {
988
+ return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
989
+ await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
990
+ return await withFileLock(canonicalPath, async () => {
991
+ const { current, root, raw, exists } = await readYaml(canonicalPath);
992
+ // External editors do not participate in the file lock, so a save
993
+ // between the initial read and a write would be silently overwritten.
994
+ // Guard every write with a compare-and-swap against the last file state
995
+ // this transaction read/wrote; on mismatch, fail closed.
996
+ const initialState: YamlFileState = { exists, raw };
997
+ let lastKnownState: YamlFileState | null = null;
998
+ const casGuard = async (): Promise<void> => {
999
+ const expected = lastKnownState ?? initialState;
1000
+ const currentState = await currentYamlFileState(canonicalPath);
1001
+ if (currentState.exists !== expected.exists || currentState.raw !== expected.raw) {
1002
+ throw new AtomicYamlConflictError(
1003
+ canonicalPath,
1004
+ nodeCrypto.createHash("sha256").update(expected.raw).digest("hex"),
1005
+ nodeCrypto.createHash("sha256").update(currentState.raw).digest("hex"),
1006
+ );
1007
+ }
1008
+ };
1009
+ let written = false;
1010
+ const markWritten = (): void => {
1011
+ written = true;
1012
+ };
1013
+ return await operation({
1014
+ configPath: canonicalPath,
1015
+ root,
1016
+ current,
1017
+ get written(): boolean {
1018
+ return written;
1019
+ },
1020
+ applyPatches: async (patches, options = {}) => {
1021
+ for (const patch of patches) assertPatch(patch);
1022
+ await options.validateRoot?.(root, patches);
1023
+ await casGuard();
1024
+ const receipt = await applyPatchesUnderLock(
1025
+ canonicalPath,
1026
+ current,
1027
+ patches,
1028
+ options,
1029
+ false,
1030
+ lastKnownState ?? initialState,
1031
+ lexicalPath,
1032
+ );
1033
+ if (patches.length > 0) {
1034
+ lastKnownState = { exists: true, raw: YAML.stringify(current, null, 2) };
1035
+ markWritten();
1036
+ }
1037
+ return receipt;
1038
+ },
1039
+ removeTopLevelKeys: async (keys, options = {}) => {
1040
+ for (const key of keys) delete current[key];
1041
+ await casGuard();
1042
+ lastKnownState = await writeAtomicYaml(
1043
+ canonicalPath,
1044
+ current,
1045
+ options,
1046
+ lastKnownState ?? initialState,
1047
+ lexicalPath,
1048
+ );
1049
+ markWritten();
1050
+ // The deleted top-level key values are not journaled, so a
1051
+ // restore() would vacuously claim success; report honestly that
1052
+ // the receipt is not restorable.
1053
+ let discarded = false;
1054
+ return {
1055
+ revisions: [],
1056
+ discard(): void {
1057
+ discarded = true;
1058
+ },
1059
+ async restore(): Promise<CasRestoreResult> {
1060
+ return discarded ? { status: "discarded" } : { status: "not-restorable" };
1061
+ },
1062
+ };
1063
+ },
1064
+ applyPatchesAndRemoveTopLevelKeys: async (patches, topLevelKeys, options = {}) => {
1065
+ for (const patch of patches) assertPatch(patch);
1066
+ await options.validateRoot?.(root, patches);
1067
+ await casGuard();
1068
+ await applyPatchesUnderLock(canonicalPath, current, patches, options, true, undefined, lexicalPath);
1069
+ for (const key of topLevelKeys) delete current[key];
1070
+ lastKnownState = await writeAtomicYaml(
1071
+ canonicalPath,
1072
+ current,
1073
+ options,
1074
+ lastKnownState ?? initialState,
1075
+ lexicalPath,
1076
+ );
1077
+ markWritten();
1078
+ let discarded = false;
1079
+ return {
1080
+ revisions: [],
1081
+ discard(): void {
1082
+ discarded = true;
1083
+ },
1084
+ async restore(): Promise<CasRestoreResult> {
1085
+ return discarded ? { status: "discarded" } : { status: "not-restorable" };
1086
+ },
1087
+ };
1088
+ },
1089
+ replaceCurrent: async (next, options = {}) => {
1090
+ for (const key of Object.keys(current)) delete current[key];
1091
+ Object.assign(current, next);
1092
+ await casGuard();
1093
+ lastKnownState = await writeAtomicYaml(
1094
+ canonicalPath,
1095
+ current,
1096
+ options,
1097
+ lastKnownState ?? initialState,
1098
+ lexicalPath,
1099
+ );
1100
+ markWritten();
1101
+ let discarded = false;
1102
+ return {
1103
+ revisions: [],
1104
+ discard(): void {
1105
+ discarded = true;
1106
+ },
1107
+ async restore(): Promise<CasRestoreResult> {
1108
+ return discarded ? { status: "discarded" } : { status: "not-restorable" };
1109
+ },
1110
+ };
1111
+ },
1112
+ });
396
1113
  });
397
1114
  });
398
1115
  }
@@ -404,11 +1121,11 @@ export function applyAtomicYamlPatchesWithCurrent(
404
1121
  */
405
1122
  export function enqueueAtomicYamlOperation<T>(
406
1123
  configPath: string,
407
- operation: (canonicalConfigPath: string) => Promise<T>,
1124
+ operation: (canonicalConfigPath: string, lexicalConfigPath: string) => Promise<T>,
408
1125
  ): Promise<T> {
409
1126
  const canonicalPath = canonicalConfigPath(configPath);
410
1127
  const prior = queues.get(canonicalPath) ?? Promise.resolve();
411
- const result = prior.catch(() => undefined).then(() => operation(canonicalPath));
1128
+ const result = prior.catch(() => undefined).then(() => operation(canonicalPath, configPath));
412
1129
  const completion = result.then(
413
1130
  () => undefined,
414
1131
  () => undefined,
@@ -429,14 +1146,27 @@ export function reserveAtomicYamlPatchSlot(
429
1146
  patches: () => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
430
1147
  options: AtomicYamlPatchOptions = {},
431
1148
  ): Promise<CasReceipt> {
432
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
1149
+ return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
433
1150
  const nextPatches = await patches();
434
1151
  for (const patch of nextPatches) assertPatch(patch);
435
1152
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
436
1153
  return await withFileLock(canonicalPath, async () => {
437
- const { current, root } = await readYaml(canonicalPath);
1154
+ const state = await readYaml(canonicalPath);
1155
+ const { current, root } = state;
438
1156
  await options.validateRoot?.(root, nextPatches);
439
- return await applyPatchesUnderLock(canonicalPath, current, nextPatches, options);
1157
+ return await applyPatchesUnderLock(
1158
+ canonicalPath,
1159
+ current,
1160
+ nextPatches,
1161
+ options,
1162
+ false,
1163
+ // Pass the read state as the CAS baseline (like the transaction
1164
+ // path): a symlink retarget after the publication can then restore
1165
+ // the old canonical content, and an external edit during the queue
1166
+ // wait is a CAS conflict instead of a clobber.
1167
+ state,
1168
+ lexicalPath,
1169
+ );
440
1170
  });
441
1171
  });
442
1172
  }
@@ -450,14 +1180,24 @@ export function reserveAtomicYamlUpdateSlot<T>(
450
1180
  update: () => Promise<AtomicYamlUpdate<T>> | AtomicYamlUpdate<T>,
451
1181
  options: AtomicYamlPatchOptions = {},
452
1182
  ): Promise<T> {
453
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
1183
+ return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
454
1184
  const atomicUpdate = await update();
455
1185
  await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
456
1186
  return await withFileLock(canonicalPath, async () => {
457
- const { current } = await readYaml(canonicalPath);
1187
+ const state = await readYaml(canonicalPath);
1188
+ const { current } = state;
458
1189
  const result = await atomicUpdate.apply(current);
459
1190
  if (atomicUpdate.shouldWrite?.(result) !== false) {
460
- await writeAtomicYaml(canonicalPath, current, options);
1191
+ await writeAtomicYaml(
1192
+ canonicalPath,
1193
+ current,
1194
+ options,
1195
+ // The read state as the CAS baseline: a symlink retarget after
1196
+ // the publication can restore the old canonical content, and
1197
+ // an external edit during the queue wait is a conflict.
1198
+ state,
1199
+ lexicalPath,
1200
+ );
461
1201
  }
462
1202
  await atomicUpdate.committed?.(current, result);
463
1203
  return result;