@gajae-code/coding-agent 0.17.2 → 0.17.5

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 (390) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
  18. package/dist/types/config/file-lock.d.ts +13 -3
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +14 -3
  21. package/dist/types/config/model-resolver.d.ts +1 -1
  22. package/dist/types/config/settings-schema.d.ts +11 -0
  23. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  24. package/dist/types/daemon/operator-contract.d.ts +5 -1
  25. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  26. package/dist/types/exec/bash-executor.d.ts +2 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  28. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  29. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -1
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  33. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  34. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  35. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  36. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  37. package/dist/types/internal-urls/index.d.ts +1 -0
  38. package/dist/types/internal-urls/parse.d.ts +9 -1
  39. package/dist/types/main.d.ts +23 -1
  40. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  41. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  42. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  43. package/dist/types/modes/components/session-selector.d.ts +5 -2
  44. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  45. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  46. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  47. package/dist/types/modes/interactive-mode.d.ts +1 -1
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  49. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  50. package/dist/types/modes/types.d.ts +2 -2
  51. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  52. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  53. package/dist/types/runtime-mcp/manager.d.ts +69 -6
  54. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  55. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  56. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  57. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  58. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  59. package/dist/types/sdk/broker/broker.d.ts +17 -4
  60. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  61. package/dist/types/sdk/broker/identity.d.ts +5 -1
  62. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  63. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  64. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  65. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  66. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  67. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  68. package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
  69. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  70. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  71. package/dist/types/sdk/bus/index.d.ts +19 -0
  72. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  73. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  74. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  75. package/dist/types/sdk/cli/rows.d.ts +19 -1
  76. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  77. package/dist/types/sdk/client/client.d.ts +2 -0
  78. package/dist/types/sdk/guides/cli.d.ts +3 -4
  79. package/dist/types/sdk/guides/verify.d.ts +2 -0
  80. package/dist/types/sdk/host/host.d.ts +9 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  82. package/dist/types/sdk/host/types.d.ts +10 -0
  83. package/dist/types/sdk/index.d.ts +1 -0
  84. package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  86. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  87. package/dist/types/sdk/router/session-router.d.ts +2 -0
  88. package/dist/types/sdk/session.d.ts +3 -3
  89. package/dist/types/sdk/startup-capability.d.ts +2 -0
  90. package/dist/types/sdk/transport/relay.d.ts +1 -0
  91. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  92. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  93. package/dist/types/session/agent-session.d.ts +175 -23
  94. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  95. package/dist/types/session/fold-coordinator.d.ts +17 -3
  96. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  97. package/dist/types/session/session-manager.d.ts +64 -1
  98. package/dist/types/session/session-work-lease.d.ts +14 -0
  99. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  100. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  101. package/dist/types/task/types.d.ts +2 -0
  102. package/dist/types/tools/bash-interactive.d.ts +1 -0
  103. package/dist/types/tools/bash.d.ts +37 -11
  104. package/dist/types/tools/fetch.d.ts +10 -1
  105. package/dist/types/tools/job.d.ts +1 -1
  106. package/dist/types/tools/steer-fold.d.ts +31 -0
  107. package/dist/types/tools/subagent.d.ts +8 -1
  108. package/dist/types/utils/git.d.ts +28 -2
  109. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  110. package/dist/types/utils/mupdf.d.ts +4 -0
  111. package/dist/types/utils/session-title-generation.d.ts +6 -0
  112. package/dist/types/utils/title-generator.d.ts +11 -2
  113. package/dist/types/web/scrapers/types.d.ts +3 -0
  114. package/package.json +18 -10
  115. package/scripts/build-binary.ts +9 -3
  116. package/scripts/compile-args.ts +0 -9
  117. package/scripts/embed-mupdf.ts +52 -0
  118. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  119. package/scripts/vendor-markit.ts +196 -0
  120. package/src/async/job-manager.ts +22 -1
  121. package/src/cli/daemon-cli.ts +53 -18
  122. package/src/cli/github-release.ts +87 -5
  123. package/src/cli/mcp-cli.ts +38 -4
  124. package/src/cli/public-command-entry.ts +450 -0
  125. package/src/cli/public-command-errors.ts +581 -0
  126. package/src/cli/public-command-evidence.ts +717 -0
  127. package/src/cli/public-command-help.ts +360 -0
  128. package/src/cli/public-command-registry.ts +541 -0
  129. package/src/cli/session-picker.ts +2 -1
  130. package/src/cli/setup-cli.ts +10 -1
  131. package/src/cli/skills-cli.ts +46 -2
  132. package/src/cli/stats-cli.ts +11 -1
  133. package/src/cli.ts +61 -1
  134. package/src/commands/daemon.ts +65 -69
  135. package/src/commands/sdk.ts +175 -117
  136. package/src/commands/setup.ts +2 -0
  137. package/src/commands/skills.ts +11 -0
  138. package/src/config/autorouting-tier-map.ts +543 -3
  139. package/src/config/file-lock.ts +206 -65
  140. package/src/config/model-discovery-manager.ts +4 -4
  141. package/src/config/model-profile-activation.ts +417 -66
  142. package/src/config/model-registry.ts +254 -36
  143. package/src/config/model-resolver.ts +6 -1
  144. package/src/config/settings-schema.ts +26 -2
  145. package/src/coordinator-mcp/policy.ts +10 -5
  146. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  147. package/src/coordinator-mcp/question-state.ts +179 -9
  148. package/src/coordinator-mcp/server.ts +521 -87
  149. package/src/coordinator-mcp/session-reaper.ts +6 -4
  150. package/src/crash/record-loader.ts +6 -2
  151. package/src/daemon/operator-contract.ts +26 -12
  152. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  153. package/src/eval/js/context-manager.ts +8 -8
  154. package/src/eval/js/worker-core.ts +9 -4
  155. package/src/eval/js/worker-protocol.ts +2 -0
  156. package/src/exec/bash-executor.ts +3 -0
  157. package/src/extensibility/extensions/runner.ts +52 -4
  158. package/src/extensibility/extensions/types.ts +10 -0
  159. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  161. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  162. package/src/extensibility/shared-events.ts +3 -1
  163. package/src/gjc-runtime/session-state-lock.ts +165 -24
  164. package/src/gjc-runtime/state-writer.ts +317 -6
  165. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  166. package/src/harness-control-plane/control-endpoint.ts +24 -6
  167. package/src/harness-control-plane/finalize.ts +312 -25
  168. package/src/harness-control-plane/owner.ts +323 -89
  169. package/src/internal-urls/docs-index.generated.ts +23 -20
  170. package/src/internal-urls/embedded-protocol.ts +83 -0
  171. package/src/internal-urls/index.ts +1 -0
  172. package/src/internal-urls/parse.ts +21 -1
  173. package/src/internal-urls/router.ts +4 -2
  174. package/src/main.ts +192 -74
  175. package/src/modes/acp/acp-agent.ts +973 -56
  176. package/src/modes/acp/startup-provenance.ts +59 -0
  177. package/src/modes/components/custom-provider-wizard.ts +308 -7
  178. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  179. package/src/modes/components/oauth-selector.ts +9 -0
  180. package/src/modes/components/session-selector.ts +47 -4
  181. package/src/modes/components/tool-status-header.ts +72 -6
  182. package/src/modes/components/user-message-selector.ts +57 -15
  183. package/src/modes/controllers/command-controller.ts +58 -32
  184. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  185. package/src/modes/controllers/input-controller.ts +4 -0
  186. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  187. package/src/modes/controllers/selector-controller.ts +161 -15
  188. package/src/modes/interactive-mode.ts +21 -7
  189. package/src/modes/runtime-init.ts +1 -0
  190. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  191. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  192. package/src/modes/types.ts +2 -2
  193. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  194. package/src/prompts/system/title-system.md +1 -1
  195. package/src/prompts/tools/bash.md +1 -1
  196. package/src/prompts/tools/job.md +1 -1
  197. package/src/prompts/tools/read.md +2 -0
  198. package/src/runtime/network-prewarm-service.test.ts +36 -1
  199. package/src/runtime/network-prewarm-service.ts +28 -3
  200. package/src/runtime/process-lifecycle.ts +22 -11
  201. package/src/runtime-mcp/config.ts +16 -1
  202. package/src/runtime-mcp/loader.ts +6 -3
  203. package/src/runtime-mcp/manager.ts +845 -109
  204. package/src/runtime-mcp/pool.test.ts +31 -0
  205. package/src/runtime-mcp/pool.ts +304 -28
  206. package/src/runtime-mcp/redaction.ts +42 -0
  207. package/src/runtime-mcp/startup-policy.ts +7 -0
  208. package/src/runtime-mcp/tool-bridge.ts +71 -0
  209. package/src/runtime-mcp/transports/http.ts +108 -25
  210. package/src/sdk/acp/adapter.ts +27 -8
  211. package/src/sdk/broker/broker.ts +1431 -198
  212. package/src/sdk/broker/ensure.ts +21 -3
  213. package/src/sdk/broker/identity.ts +22 -1
  214. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  215. package/src/sdk/broker/lifecycle.ts +690 -141
  216. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  217. package/src/sdk/broker/managed-task-verification.ts +642 -0
  218. package/src/sdk/broker/runtime.ts +1 -1
  219. package/src/sdk/broker/session-index.ts +97 -4
  220. package/src/sdk/broker/spawn-authority.ts +175 -14
  221. package/src/sdk/broker/startup-failure.ts +80 -1
  222. package/src/sdk/broker/stderr-drainer.ts +59 -0
  223. package/src/sdk/broker/transport.ts +1 -0
  224. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  225. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  226. package/src/sdk/bus/index.ts +707 -81
  227. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  228. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  229. package/src/sdk/bus/telegram-daemon.ts +10 -3
  230. package/src/sdk/cli/master-cli.ts +47 -36
  231. package/src/sdk/cli/rows.ts +30 -7
  232. package/src/sdk/cli/session-cli.ts +705 -184
  233. package/src/sdk/client/client.ts +18 -0
  234. package/src/sdk/guides/catalog.ts +2 -2
  235. package/src/sdk/guides/cli.ts +20 -62
  236. package/src/sdk/guides/verify.ts +14 -0
  237. package/src/sdk/host/host.ts +49 -5
  238. package/src/sdk/host/session-runtime.test.ts +184 -2
  239. package/src/sdk/host/session-runtime.ts +322 -101
  240. package/src/sdk/host/types.ts +12 -1
  241. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  242. package/src/sdk/host/websocket-transport.ts +40 -6
  243. package/src/sdk/index.ts +11 -0
  244. package/src/sdk/lifecycle/service.ts +2 -0
  245. package/src/sdk/prompt-deadline-flush.ts +356 -0
  246. package/src/sdk/prompt-deadline-manager.ts +133 -1
  247. package/src/sdk/prompt-tool-boundary.ts +121 -0
  248. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  249. package/src/sdk/router/session-router.ts +9 -1
  250. package/src/sdk/session.ts +763 -116
  251. package/src/sdk/startup-capability.ts +1 -1
  252. package/src/sdk/transport/relay.ts +71 -6
  253. package/src/sdk/transport/serve-cli.ts +397 -42
  254. package/src/sdk/transport/socket.ts +12 -11
  255. package/src/sdk/transport/stdio.ts +7 -3
  256. package/src/session/agent-session.ts +2472 -407
  257. package/src/session/fallback-chain-controller.ts +6 -7
  258. package/src/session/fold-coordinator.ts +24 -5
  259. package/src/session/internal/managed-session-scope.ts +68 -18
  260. package/src/session/internal/managed-session-storage.ts +334 -12
  261. package/src/session/session-manager.ts +382 -44
  262. package/src/session/session-work-lease.ts +34 -0
  263. package/src/setup/model-onboarding-guidance.ts +1 -1
  264. package/src/setup/provider-onboarding.ts +397 -24
  265. package/src/setup/provider-presets.json +2 -1
  266. package/src/slash-commands/builtin-registry.ts +259 -15
  267. package/src/task/executor.ts +46 -17
  268. package/src/task/index.ts +14 -2
  269. package/src/task/types.ts +2 -0
  270. package/src/tools/bash-interactive.ts +2 -0
  271. package/src/tools/bash.ts +118 -78
  272. package/src/tools/fetch.ts +140 -25
  273. package/src/tools/job.ts +151 -25
  274. package/src/tools/output-meta.ts +15 -2
  275. package/src/tools/path-utils.ts +27 -3
  276. package/src/tools/read.ts +3 -3
  277. package/src/tools/steer-fold.ts +107 -0
  278. package/src/tools/subagent-render.ts +24 -1
  279. package/src/tools/subagent.ts +27 -1
  280. package/src/tools/tool-catalog.generated.ts +29 -2
  281. package/src/tools/tool-errors.ts +4 -0
  282. package/src/utils/git.ts +53 -3
  283. package/src/utils/markit.ts +55 -41
  284. package/src/utils/mupdf-assets.d.ts +4 -0
  285. package/src/utils/mupdf-embedded.ts +4 -0
  286. package/src/utils/mupdf.ts +136 -0
  287. package/src/utils/session-title-generation.ts +29 -0
  288. package/src/utils/title-generator.ts +46 -2
  289. package/src/web/scrapers/types.ts +24 -3
  290. package/vendor/markit-ai/LICENSE +21 -0
  291. package/vendor/markit-ai/MANIFEST.json +412 -0
  292. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  293. package/vendor/markit-ai/dist/commands/config.js +139 -0
  294. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  295. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  296. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  297. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  298. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  299. package/vendor/markit-ai/dist/commands/init.js +29 -0
  300. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  301. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  302. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  303. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  304. package/vendor/markit-ai/dist/config.d.ts +26 -0
  305. package/vendor/markit-ai/dist/config.js +42 -0
  306. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  307. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  308. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  309. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  310. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  312. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  313. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  314. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  315. package/vendor/markit-ai/dist/converters/github.js +148 -0
  316. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  317. package/vendor/markit-ai/dist/converters/html.js +30 -0
  318. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  319. package/vendor/markit-ai/dist/converters/image.js +124 -0
  320. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  321. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  322. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  323. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  324. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  325. package/vendor/markit-ai/dist/converters/json.js +21 -0
  326. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  327. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  328. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  329. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  330. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  331. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  332. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  333. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  334. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  335. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  336. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  337. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  338. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  339. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  340. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  341. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  342. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  343. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  344. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  345. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  346. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  347. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  348. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  349. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  350. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  351. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  352. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  353. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  354. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  355. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  356. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  357. package/vendor/markit-ai/dist/index.d.ts +30 -0
  358. package/vendor/markit-ai/dist/index.js +26 -0
  359. package/vendor/markit-ai/dist/main.d.ts +2 -0
  360. package/vendor/markit-ai/dist/main.js +191 -0
  361. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  362. package/vendor/markit-ai/dist/markit.js +267 -0
  363. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  364. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  365. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  366. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  367. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  368. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  369. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  370. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  371. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  372. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  373. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  374. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  375. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  376. package/vendor/markit-ai/dist/providers/index.js +56 -0
  377. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  378. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  379. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  380. package/vendor/markit-ai/dist/providers/types.js +1 -0
  381. package/vendor/markit-ai/dist/types.d.ts +36 -0
  382. package/vendor/markit-ai/dist/types.js +1 -0
  383. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  384. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  385. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  386. package/vendor/markit-ai/dist/utils/output.js +31 -0
  387. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  388. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  389. package/vendor/markit-ai/package.json +74 -0
  390. package/vendor/markit-ai.patch +49 -0
@@ -57,10 +57,12 @@ export function selectReapableSessions(
57
57
  /**
58
58
  * Only a stale/absent endpoint makes a session permanently unreapable, so it is
59
59
  * the sole failure that advances the force-eviction counter. Every other reap
60
- * failure (close_failed, broker unavailability, filesystem errors) is transient
61
- * and must keep retrying without ever escalating to force eviction. The server
62
- * wires `reapSession` to throw `new Error(reason)`, so the reason code is the
63
- * error message here.
60
+ * failure (a close_failed without a stale endpoint, broker unavailability,
61
+ * filesystem errors) is transient and must keep retrying without ever escalating
62
+ * to force eviction. The server wires `reapSession` to throw `new Error(reason)`,
63
+ * except that a close the broker answered with endpoint_stale (close_failed with
64
+ * that detail) is thrown as endpoint_stale, so the reason code is the error
65
+ * message here.
64
66
  */
65
67
  function isEndpointStaleReapError(err: unknown): boolean {
66
68
  return (err instanceof Error ? err.message : String(err)) === "endpoint_stale";
@@ -17,6 +17,7 @@ import {
17
17
  type CrashFingerprint,
18
18
  type CrashProvenance,
19
19
  computeCrashFingerprint,
20
+ computeHandledErrorFingerprint,
20
21
  parseCrashRecordMarker,
21
22
  } from "@gajae-code/utils";
22
23
 
@@ -60,8 +61,11 @@ function findBoundFingerprint(
60
61
  // suffix omission against the marker rather than trusting its syntax.
61
62
  if (remaining.length > 0) stackCandidates.push(remaining.slice(0, -1).join("\n").trimEnd());
62
63
  for (const stack of stackCandidates) {
63
- const fingerprint = computeCrashFingerprint({ name, message, stack });
64
- if (fingerprint.fingerprint === markerFingerprint) return fingerprint;
64
+ const input = { name, message, stack };
65
+ const fatalFingerprint = computeCrashFingerprint(input);
66
+ if (fatalFingerprint.fingerprint === markerFingerprint) return fatalFingerprint;
67
+ const handledFingerprint = computeHandledErrorFingerprint(input);
68
+ if (handledFingerprint.fingerprint === markerFingerprint) return handledFingerprint;
65
69
  }
66
70
  }
67
71
  }
@@ -12,6 +12,8 @@
12
12
  * (automation); the default human output stays a concise per-daemon result.
13
13
  */
14
14
 
15
+ import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
16
+ import { sanitizeDisplayLine } from "@gajae-code/utils";
15
17
  import type { DaemonAction, DaemonOperationResult, DaemonRecovery, DaemonStatus } from "./control-types";
16
18
 
17
19
  /** Canonical daemon actions accepted as the leading verb. */
@@ -39,8 +41,14 @@ export function resolveDaemonAction(token: string | undefined): DaemonAction | u
39
41
  return DAEMON_ACTION_ALIASES[token];
40
42
  }
41
43
 
42
- /** Exit codes for `gjc daemon`. Backward compatible: success 0, any failure 1. */
43
- export const DAEMON_EXIT = { ok: 0, failure: 1 } as const;
44
+ /** Public input failures exit 2; operational failures retain exit 1. */
45
+ export const DAEMON_EXIT = { ok: 0, failure: 1, usage: 2 } as const;
46
+
47
+ /** A refused result may follow a successful stop or spawn; snapshots and prose
48
+ * do not prove that no effect occurred. Only completed results prove applied. */
49
+ export function daemonOperationOutcome(result: DaemonOperationResult): "applied" | "unknown" {
50
+ return result.ok ? "applied" : "unknown";
51
+ }
44
52
 
45
53
  /** Human-facing headline when a spawn/reload is refused by a live foreign identity. */
46
54
  export const OWNERSHIP_MISMATCH_MESSAGE =
@@ -59,6 +67,10 @@ export function ownershipMismatchRecovery(): DaemonRecovery {
59
67
  };
60
68
  }
61
69
 
70
+ function safeDaemonText(value: string): string {
71
+ return truncateToWidth(replaceTabs(sanitizeDisplayLine(value).replaceAll(/[\u2028\u2029]/gu, " ")), 512);
72
+ }
73
+
62
74
  function timestamp(ms: number | undefined): string | undefined {
63
75
  if (ms === undefined) return undefined;
64
76
  const date = new Date(ms);
@@ -73,31 +85,31 @@ export function formatDaemonStatus(status: DaemonStatus, opts: { verbose?: boole
73
85
  const lines: string[] = [];
74
86
  if (!status.configured) {
75
87
  lines.push(`${status.kind}: not configured`);
76
- if (status.runtime.warning) lines.push(` warning: ${status.runtime.warning}`);
88
+ if (status.runtime.warning) lines.push(` warning: ${safeDaemonText(status.runtime.warning)}`);
77
89
  return lines.join("\n");
78
90
  }
79
91
 
80
92
  const meta: string[] = [];
81
93
  if (status.pid !== undefined) meta.push(`pid ${status.pid}`);
82
- if (status.ownerId) meta.push(`owner ${status.ownerId}`);
94
+ if (status.ownerId) meta.push(`owner ${safeDaemonText(status.ownerId)}`);
83
95
  const rootCount = status.rootCount ?? status.roots?.length ?? 0;
84
96
  if (rootCount > 0) meta.push(`${rootCount} root${rootCount === 1 ? "" : "s"}`);
85
97
 
86
98
  let head = `${status.kind}: ${status.health}`;
87
99
  if (meta.length > 0) head += ` (${meta.join(", ")})`;
88
- if (status.detail) head += ` — ${status.detail}`;
100
+ if (status.detail) head += ` — ${safeDaemonText(status.detail)}`;
89
101
  lines.push(head);
90
- if (status.runtime.warning) lines.push(` warning: ${status.runtime.warning}`);
102
+ if (status.runtime.warning) lines.push(` warning: ${safeDaemonText(status.runtime.warning)}`);
91
103
 
92
104
  if (opts.verbose) {
93
- lines.push(` runtime: ${status.runtime.mode} (${status.runtime.execPath})`);
105
+ lines.push(` runtime: ${status.runtime.mode} (${safeDaemonText(status.runtime.execPath)})`);
94
106
  const started = timestamp(status.startedAt);
95
107
  if (started) lines.push(` started: ${started}`);
96
108
  const heartbeat = timestamp(status.heartbeatAt);
97
109
  if (heartbeat) lines.push(` heartbeat: ${heartbeat}`);
98
110
  const roots = status.roots ?? [];
99
111
  lines.push(` roots: ${rootCount}`);
100
- for (const root of roots) lines.push(` - ${root}`);
112
+ for (const root of roots) lines.push(` - ${safeDaemonText(root)}`);
101
113
  }
102
114
 
103
115
  return lines.join("\n");
@@ -108,13 +120,15 @@ export function formatDaemonStatus(status: DaemonStatus, opts: { verbose?: boole
108
120
  * <message>` head line, then any warnings, then actionable recovery steps.
109
121
  */
110
122
  export function formatDaemonResult(result: DaemonOperationResult): string {
111
- const lines = [`${result.kind} ${result.action}: ${result.ok ? "ok" : "failed"} — ${result.message}`];
112
- for (const warning of result.warnings) lines.push(` warning: ${warning}`);
123
+ const lines = [
124
+ `${result.kind} ${result.action}: ${result.ok ? "ok" : "failed"} — ${safeDaemonText(result.message)}`,
125
+ ];
126
+ for (const warning of result.warnings) lines.push(` warning: ${safeDaemonText(warning)}`);
113
127
  if (result.recovery) {
114
- lines.push(` ${result.recovery.summary}`);
128
+ lines.push(` ${safeDaemonText(result.recovery.summary)}`);
115
129
  lines.push(" to recover:");
116
130
  result.recovery.steps.forEach((step, i) => {
117
- lines.push(` ${i + 1}. ${step}`);
131
+ lines.push(` ${i + 1}. ${safeDaemonText(step)}`);
118
132
  });
119
133
  }
120
134
  return lines.join("\n");
@@ -458,7 +458,7 @@ Because unselected goals stay schedulable, ordinary source progress moves whole-
458
458
 
459
459
  `adopt` runs in the target repository and starts a genuinely fresh run: new pending goals, fresh timestamps, the target repository's own binding, and no inherited receipts, quality gates, validation batches, steering or activated objective. The source brief and every selected objective are carried verbatim, together with the unresolved obligations recorded against those goals — requirements are carried forward in full, never summarized down. Dependency groups travel as explicit obligations with source-to-target id mapping: the source's batch metadata and receipts are not inherited, but the requirement to validate those goals together is.
460
460
 
461
- Selection and authorization must be explicit. Only unfinished goals (`pending`, `failed`, `blocked`, `review_blocked`) may be handed off; the source run must be quiescent with no `active` goal; there is no implicit or wildcard selection; and a selection that takes only part of a validation batch, or splits a review blocker from the goal it still blocks, is rejected rather than silently dropping that obligation. Adoption fails closed on changed offered work, an altered or unfenced offer, an offer path that escapes the source worktree, a target the offer does not name, a linked worktree that is not the exact named target, an occupied target session, and any duplicate or simultaneous adoption anywhere in the target repository. A retry reconciles the exact recorded operation or fails closed — a matching operation id is never authority to overwrite target work it did not write, and a published plan that has disappeared is never reconstructed.
461
+ Selection and authorization must be explicit. Only unfinished goals (`pending`, `failed`, `blocked`, `review_blocked`) may be handed off; the source run must be quiescent with no `active` goal; there is no implicit or wildcard selection; and a selection that takes only part of a validation batch, or splits a review blocker from the goal it still blocks, is rejected rather than silently dropping that obligation. Adoption fails closed on changed offered work, an altered or unfenced offer, an offer path that escapes the source worktree, a target the offer does not name, a different linked worktree from the exact named target, an occupied target session, and any duplicate or simultaneous adoption anywhere in the target repository. A linked worktree is a valid target when the offer names that exact worktree; the source and target may therefore be sibling worktrees, but aliases of the same source worktree remain self-transfers and are rejected using physical path identity, and an unavailable bound worktree root is refused rather than compared lexically. A retry reconciles the exact recorded operation or fails closed — a matching operation id is never authority to overwrite target work it did not write, and a published plan that has disappeared is never reconstructed.
462
462
 
463
463
  Until publication completes, the target refuses to execute: a visible `goals.json` is not admission evidence, the adoption record is. `start`/`complete-goals` and `checkpoint` reject while an adoption remains pending.
464
464
 
@@ -1,4 +1,5 @@
1
1
  import { isCompiledBinary, logger, Snowflake } from "@gajae-code/utils";
2
+ import { isDesignedError } from "@gajae-code/utils/error-classification";
2
3
  import { registerResourceOwner } from "../../runtime/process-lifecycle";
3
4
  import type { ToolSession } from "../../tools";
4
5
  import { ToolAbortError, ToolError } from "../../tools/tool-errors";
@@ -460,13 +461,11 @@ function reasonToError(reason: unknown, fallback: string): Error {
460
461
  }
461
462
 
462
463
  function errorFromPayload(payload: RunErrorPayload): Error {
463
- if (payload.isAbort) {
464
- const err = new ToolAbortError(payload.message || "Execution aborted");
465
- if (payload.stack) err.stack = payload.stack;
466
- return err;
467
- }
468
- const ctor = payload.isToolError ? ToolError : Error;
469
- const error = new ctor(payload.message);
464
+ const error = payload.isDesigned
465
+ ? payload.isAbort
466
+ ? new ToolAbortError(payload.message || "Execution aborted")
467
+ : new ToolError(payload.message)
468
+ : new Error(payload.message);
470
469
  if (payload.name) error.name = payload.name;
471
470
  if (payload.stack) error.stack = payload.stack;
472
471
  return error;
@@ -479,7 +478,8 @@ function toErrorPayload(error: unknown): RunErrorPayload {
479
478
  message: error.message,
480
479
  stack: error.stack,
481
480
  isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
482
- isToolError: error instanceof ToolError || error.name === "ToolError",
481
+ isToolError: error instanceof ToolError,
482
+ isDesigned: isDesignedError(error),
483
483
  };
484
484
  }
485
485
  return { message: String(error) };
@@ -1,4 +1,5 @@
1
- import { ToolError } from "../../tools/tool-errors";
1
+ import { isDesignedError } from "@gajae-code/utils/error-classification";
2
+ import { ToolAbortError, ToolError } from "../../tools/tool-errors";
2
3
  import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
3
4
  import type { RunErrorPayload, SessionSnapshot, ToolReply, Transport, WorkerInbound } from "./worker-protocol";
4
5
 
@@ -24,15 +25,19 @@ function errorPayload(error: unknown): RunErrorPayload {
24
25
  message: error.message,
25
26
  stack: error.stack,
26
27
  isAbort: error.name === "AbortError" || error.name === "ToolAbortError",
27
- isToolError: error.name === "ToolError" || error instanceof ToolError,
28
+ isToolError: error instanceof ToolError,
29
+ isDesigned: isDesignedError(error),
28
30
  };
29
31
  }
30
32
  return { message: String(error) };
31
33
  }
32
34
 
33
35
  function errorFromPayload(payload: RunErrorPayload): Error {
34
- const ctor = payload.isToolError ? ToolError : Error;
35
- const error = new ctor(payload.message);
36
+ const error = payload.isDesigned
37
+ ? payload.isAbort
38
+ ? new ToolAbortError(payload.message)
39
+ : new ToolError(payload.message)
40
+ : new Error(payload.message);
36
41
  if (payload.name) error.name = payload.name;
37
42
  if (payload.stack) error.stack = payload.stack;
38
43
  return error;
@@ -13,6 +13,8 @@ export interface RunErrorPayload {
13
13
  stack?: string;
14
14
  isAbort?: boolean;
15
15
  isToolError?: boolean;
16
+ /** Set only when the originating error carried the trusted designed marker. */
17
+ isDesigned?: boolean;
16
18
  }
17
19
 
18
20
  export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
@@ -319,6 +319,8 @@ export interface BashExecutorOptions {
319
319
  sessionKey?: string;
320
320
  /** Additional environment variables to inject */
321
321
  env?: Record<string, string>;
322
+ /** Environment names to hide from this command's inherited environment. */
323
+ unsetEnv?: string[];
322
324
  /** Artifact path/id for full output storage */
323
325
  artifactPath?: string;
324
326
  artifactId?: string;
@@ -578,6 +580,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
578
580
  command: finalCommand,
579
581
  cwd: commandCwd,
580
582
  env: commandEnv,
583
+ unsetEnv: options?.unsetEnv,
581
584
  timeoutMs: executionTimeoutMs,
582
585
  signal: runAbortController.signal,
583
586
  };
@@ -192,6 +192,7 @@ type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "
192
192
  type Handler = Extension["handlers"] extends Map<string, Array<infer T>> ? T : never;
193
193
  type IndexedHandler = { ext: Extension; handler: Handler; registrationOrder: number };
194
194
  type IndexedFunctionHook = IndexedHandler & { registration: FunctionHookRegistration };
195
+ type CommandAliasTarget = { extensionPath: string; commandName: string };
195
196
 
196
197
  export type FunctionHookDispatchResult<TEvent extends ExtensionEvent> =
197
198
  | { action: "continue"; event: TEvent; transformed?: boolean }
@@ -272,6 +273,7 @@ export class ExtensionRunner {
272
273
  #getCredentialSessionId: () => string = () => "";
273
274
  #isIdleFn: () => boolean = () => true;
274
275
  #getActivePromptHandleFn: () => string | undefined = () => undefined;
276
+ #getSessionWorkLeaseFn: ExtensionContextActions["getSessionWorkLease"] = undefined;
275
277
  #waitForIdleFn: () => Promise<void> = async () => {};
276
278
  #abortFn: () => void | Promise<void> = () => {};
277
279
  #abortPromptAndWaitFn: NonNullable<ExtensionContextActions["abortPromptAndWait"]> = async () => {
@@ -328,6 +330,9 @@ export class ExtensionRunner {
328
330
  #reloadHandler: () => Promise<void> = async () => {};
329
331
  #shutdownHandler: ShutdownHandler = () => {};
330
332
  #commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
333
+ #commandAliases = new Map<string, RegisteredCommand>();
334
+ #commandAliasAssignments = new Map<string, string>();
335
+ #commandAliasTargets = new Map<string, CommandAliasTarget>();
331
336
  #initialized = false;
332
337
  /**
333
338
  * Buffer for `credential_disabled` events received via {@link emitCredentialDisabled}
@@ -424,6 +429,7 @@ export class ExtensionRunner {
424
429
  this.#getCredentialSessionId = contextActions.getCredentialSessionId ?? (() => "");
425
430
  this.#isIdleFn = contextActions.isIdle;
426
431
  this.#getActivePromptHandleFn = contextActions.getActivePromptHandle ?? (() => undefined);
432
+ this.#getSessionWorkLeaseFn = contextActions.getSessionWorkLease;
427
433
  this.#abortFn = contextActions.abort;
428
434
  this.#abortPromptAndWaitFn =
429
435
  contextActions.abortPromptAndWait ??
@@ -701,16 +707,49 @@ export class ExtensionRunner {
701
707
 
702
708
  getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
703
709
  this.#commandDiagnostics = [];
710
+ this.#commandAliases.clear();
704
711
 
705
712
  const commands = new Map<string, RegisteredCommand>();
706
713
  for (const ext of this.extensions) {
707
714
  for (const command of ext.commands.values()) {
708
715
  if (reserved?.has(command.name)) {
709
- const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`;
710
- this.#commandDiagnostics.push({ type: "warning", message, path: ext.path });
711
- if (!this.hasUI()) {
712
- logger.warn(message);
716
+ const assignmentKey = `${ext.path}\u0000${command.name}`;
717
+ let alias = this.#commandAliasAssignments.get(assignmentKey);
718
+ const aliasTarget = alias ? this.#commandAliasTargets.get(alias) : undefined;
719
+ const aliasOwnedByCommand =
720
+ aliasTarget?.extensionPath === ext.path && aliasTarget.commandName === command.name;
721
+ if (
722
+ !alias ||
723
+ reserved.has(alias) ||
724
+ commands.has(alias) ||
725
+ (aliasTarget !== undefined && !aliasOwnedByCommand)
726
+ ) {
727
+ let candidate = alias ?? `extension:${command.name}`;
728
+ while (
729
+ reserved.has(candidate) ||
730
+ commands.has(candidate) ||
731
+ this.#commandAliases.has(candidate) ||
732
+ (this.#commandAliasTargets.has(candidate) &&
733
+ (this.#commandAliasTargets.get(candidate)?.extensionPath !== ext.path ||
734
+ this.#commandAliasTargets.get(candidate)?.commandName !== command.name))
735
+ )
736
+ candidate = `extension:${candidate}`;
737
+ if (
738
+ alias &&
739
+ this.#commandAliasTargets.get(alias)?.extensionPath === ext.path &&
740
+ this.#commandAliasTargets.get(alias)?.commandName === command.name
741
+ ) {
742
+ this.#commandAliasTargets.delete(alias);
743
+ }
744
+ alias = candidate;
745
+ this.#commandAliasAssignments.set(assignmentKey, alias);
713
746
  }
747
+ this.#commandAliasTargets.set(alias, { extensionPath: ext.path, commandName: command.name });
748
+ const namespaced = { ...command, name: alias };
749
+ this.#commandAliases.set(alias, namespaced);
750
+ commands.set(alias, namespaced);
751
+ const message = `Extension command '${command.name}' from ${ext.path} was renamed to '${alias}' to avoid a built-in command collision.`;
752
+ this.#commandDiagnostics.push({ type: "info", message, path: ext.path });
714
753
  continue;
715
754
  }
716
755
 
@@ -725,6 +764,14 @@ export class ExtensionRunner {
725
764
  }
726
765
 
727
766
  getCommand(name: string): RegisteredCommand | undefined {
767
+ const aliased = this.#commandAliases.get(name);
768
+ if (aliased) return aliased;
769
+ const aliasTarget = this.#commandAliasTargets.get(name);
770
+ if (aliasTarget) {
771
+ const extension = this.extensions.find(ext => ext.path === aliasTarget.extensionPath);
772
+ const command = extension?.commands.get(aliasTarget.commandName);
773
+ if (command) return { ...command, name };
774
+ }
728
775
  for (let index = this.extensions.length - 1; index >= 0; index -= 1) {
729
776
  const command = this.extensions[index]?.commands.get(name);
730
777
  if (command) {
@@ -754,6 +801,7 @@ export class ExtensionRunner {
754
801
  return getModel();
755
802
  },
756
803
  getActivePromptHandle: () => this.#getActivePromptHandleFn(),
804
+ getSessionWorkLease: () => this.#getSessionWorkLeaseFn?.(),
757
805
  isIdle: () => this.#isIdleFn(),
758
806
  abort: () => this.#abortFn(),
759
807
  abortPromptAndWait: (handle, options) => this.#abortPromptAndWaitFn(handle, options),
@@ -58,6 +58,7 @@ import type {
58
58
  } from "../../session/client-bridge";
59
59
  import type { CustomMessage } from "../../session/messages";
60
60
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
61
+ import type { SessionWorkLease } from "../../session/session-work-lease";
61
62
  import type {
62
63
  BashToolDetails,
63
64
  BashToolInput,
@@ -482,6 +483,8 @@ export interface ExtensionContext {
482
483
  isIdle(): boolean;
483
484
  /** Stable resource ownership identifier for the active prompt run. */
484
485
  getActivePromptHandle(): string | undefined;
486
+ /** The session's authoritative host-liveness lease. */
487
+ getSessionWorkLease?(): SessionWorkLease | undefined;
485
488
  /** Abort the current agent operation */
486
489
  abort(): void | Promise<void>;
487
490
  /** Abort and prove whether resources for a specific prompt settled. */
@@ -1569,6 +1572,11 @@ export type SendUserMessageHandler = (
1569
1572
  * grants root abort ownership.
1570
1573
  */
1571
1574
  onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
1575
+ /**
1576
+ * Fired synchronously when a plain prompt's dispatch resolves at queue
1577
+ * time instead of run end (diverted into an in-flight run's steering).
1578
+ */
1579
+ onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
1572
1580
  preflightSignal?: AbortSignal;
1573
1581
  /** Internal SDK correlation owner for an exact queued follow-up. */
1574
1582
  },
@@ -1660,6 +1668,8 @@ export interface ExtensionContextActions {
1660
1668
  isIdle: () => boolean;
1661
1669
  /** Stable resource ownership identifier for the active prompt run. */
1662
1670
  getActivePromptHandle?: () => string | undefined;
1671
+ /** The session's authoritative host-liveness lease. */
1672
+ getSessionWorkLease?: () => SessionWorkLease | undefined;
1663
1673
  abort: () => void | Promise<void>;
1664
1674
  abortPromptAndWait?: (handle: string, options: { graceMs: number }) => Promise<RunSettlementProof>;
1665
1675
 
@@ -1,6 +1,14 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
5
+ import {
6
+ exactUnlink,
7
+ linkNoReplacePath,
8
+ type NativeExactFileIdentity,
9
+ type NativeNoReplaceResult,
10
+ renameNoReplacePath,
11
+ } from "@gajae-code/natives";
4
12
  import { compileGjcPluginBundle } from "./compiler";
5
13
  import { migrateGjcPluginEntries } from "./migration";
6
14
  import { gjcPluginProjectRoot, gjcPluginUserRoot } from "./paths";
@@ -11,6 +19,11 @@ const LOCK_FILENAME = "registry.lock";
11
19
  const LOCK_TIMEOUT_MS = 5000;
12
20
  const LOCK_RETRY_MS = 50;
13
21
 
22
+ export const RegistryLockTestHooks: {
23
+ beforeEviction?: (lockPath: string, raw: string) => void | Promise<void>;
24
+ beforePublish?: (stagingPath: string, lockPath: string) => void | Promise<void>;
25
+ } = {};
26
+
14
27
  export function registryRootForScope(scope: GjcPluginScope, cwd: string): string {
15
28
  return scope === "user" ? gjcPluginUserRoot() : gjcPluginProjectRoot(cwd);
16
29
  }
@@ -323,57 +336,203 @@ export async function readRegistry(
323
336
  if (!migrated.changed && discovered.length === 0) return registry;
324
337
  // Re-check under the lock before persisting. Migration and legacy-root
325
338
  // discovery are one transaction, never a normal runtime loader path.
326
- return await withRegistryLock(scope, cwd, async () => {
327
- const latest = await readRegistryRaw(scope, cwd);
328
- const latestDiscovered = await discoverLegacyEntries(scope, cwd, latest.plugins);
329
- const latestMigrated = await migrateGjcPluginEntries([...latest.plugins, ...latestDiscovered]);
330
- if (latestMigrated.changed || latestDiscovered.length > 0) {
331
- const next: GjcPluginRegistry = { ...latest, plugins: sortRegistryEntries(latestMigrated.entries) };
332
- await writeRegistryUnlocked(next, cwd, scope);
333
- return next;
339
+ // Contention (a concurrent install elsewhere) must not fail startup reads:
340
+ // the effective registry is already computed, so degrade to returning it
341
+ // unpersisted and let a later uncontended session write the migrated
342
+ // state. Mutating install paths keep their fail-loud install_conflict.
343
+ try {
344
+ return await withRegistryLock(scope, cwd, async () => {
345
+ const latest = await readRegistryRaw(scope, cwd);
346
+ const latestDiscovered = await discoverLegacyEntries(scope, cwd, latest.plugins);
347
+ const latestMigrated = await migrateGjcPluginEntries([...latest.plugins, ...latestDiscovered]);
348
+ if (latestMigrated.changed || latestDiscovered.length > 0) {
349
+ const next: GjcPluginRegistry = { ...latest, plugins: sortRegistryEntries(latestMigrated.entries) };
350
+ await writeRegistryUnlocked(next, cwd, scope);
351
+ return next;
352
+ }
353
+ return latest;
354
+ });
355
+ } catch (error) {
356
+ if (error instanceof GjcPluginLoadError && error.code === "install_conflict") {
357
+ return { ...registry, plugins: sortRegistryEntries(migrated.entries) };
334
358
  }
335
- return latest;
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ const LOCK_NONCE_PATTERN = /^[0-9a-f]{16}$/;
364
+
365
+ interface RegistryLockToken {
366
+ pid: number;
367
+ /** Host segment of a host-tagged token; null for legacy `pid-nonce` tokens. */
368
+ host: string | null;
369
+ }
370
+
371
+ function parseRegistryLockToken(raw: string): RegistryLockToken | null {
372
+ const parts = raw.trim().split("-");
373
+ if (parts.length < 2) return null;
374
+ const pid = Number(parts[0]);
375
+ const nonce = parts[parts.length - 1];
376
+ if (!Number.isInteger(pid) || pid <= 0 || !LOCK_NONCE_PATTERN.test(nonce)) return null;
377
+ // Hostnames may contain '-', so everything between the first and last
378
+ // segment is the host; a single middle segment that is empty (or absent)
379
+ // means a legacy token.
380
+ const host = parts.slice(1, -1).join("-");
381
+ return { pid, host: host.length > 0 ? host : null };
382
+ }
383
+
384
+ /**
385
+ * A holder is assumed alive unless provably dead: ourselves, a live PID
386
+ * (EPERM counts — the process exists under another user), or a lock written
387
+ * on another host (a project registry can live on shared storage, where a
388
+ * local PID probe would be meaningless) all stay fail-closed.
389
+ */
390
+ function registryLockHolderAlive(holder: RegistryLockToken): boolean {
391
+ if (holder.pid === process.pid) return true;
392
+ if (holder.host !== null && holder.host !== os.hostname()) return true;
393
+ try {
394
+ process.kill(holder.pid, 0);
395
+ return true;
396
+ } catch (error) {
397
+ return (error as NodeJS.ErrnoException)?.code === "EPERM";
398
+ }
399
+ }
400
+
401
+ async function captureRegistryLockIdentity(lockPath: string): Promise<NativeExactFileIdentity | null> {
402
+ try {
403
+ const [bytes, stat, parent] = await Promise.all([
404
+ fs.readFile(lockPath),
405
+ fs.lstat(lockPath, { bigint: true }),
406
+ fs.stat(path.dirname(lockPath), { bigint: true }),
407
+ ]);
408
+ if (!stat.isFile()) return null;
409
+ return {
410
+ dev: stat.dev,
411
+ ino: stat.ino,
412
+ nlink: stat.nlink,
413
+ parentDev: parent.dev,
414
+ parentIno: parent.ino,
415
+ size: stat.size,
416
+ mtimeNs: stat.mtimeNs,
417
+ sha256: createHash("sha256").update(bytes).digest("hex"),
418
+ };
419
+ } catch (error) {
420
+ if (isEnoent(error)) return null;
421
+ throw error;
422
+ }
423
+ }
424
+
425
+ async function evictRegistryLock(lockPath: string, raw: string): Promise<boolean> {
426
+ const identity = await captureRegistryLockIdentity(lockPath);
427
+ if (!identity) return false;
428
+ const expectedHash = createHash("sha256").update(raw).digest("hex");
429
+ if (identity.sha256 !== expectedHash) return false;
430
+ await RegistryLockTestHooks.beforeEviction?.(lockPath, raw);
431
+ const result = exactUnlink(lockPath, {
432
+ ...identity,
433
+ quarantineName: `${LOCK_FILENAME}.evict-${process.pid}-${randomBytes(4).toString("hex")}`,
336
434
  });
435
+ return result.ok || result.code === "cleanup_pending";
436
+ }
437
+
438
+ function publishRegistryLock(stagingPath: string, lockPath: string): NativeNoReplaceResult {
439
+ let result = renameNoReplacePath(stagingPath, lockPath);
440
+ if (
441
+ !result.ok &&
442
+ result.mutationState === "not_committed" &&
443
+ (result.reason === "atomic_unavailable" || result.reason === "invalid_request")
444
+ ) {
445
+ // linkat is the no-overwrite fallback on filesystems without a
446
+ // no-replace rename. Both paths expose the complete token only after the
447
+ // atomic claim, so a crash cannot publish an empty or partial lock.
448
+ result = linkNoReplacePath(stagingPath, lockPath);
449
+ }
450
+ return result;
337
451
  }
338
452
 
339
453
  async function acquireLock(lockPath: string): Promise<() => Promise<void>> {
340
454
  await fs.mkdir(path.dirname(lockPath), { recursive: true });
341
- const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
455
+ const token = `${process.pid}-${os.hostname()}-${randomBytes(8).toString("hex")}`;
342
456
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
343
457
  for (;;) {
458
+ const stagingPath = `${lockPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
459
+ let publication: NativeNoReplaceResult | undefined;
344
460
  try {
345
- const handle = await fs.open(lockPath, "wx");
346
- try {
347
- await handle.writeFile(token);
348
- } finally {
349
- await handle.close();
461
+ await fs.writeFile(stagingPath, token, { encoding: "utf8", flag: "wx", mode: 0o600 });
462
+ await RegistryLockTestHooks.beforePublish?.(stagingPath, lockPath);
463
+ publication = publishRegistryLock(stagingPath, lockPath);
464
+ if (publication.ok) {
465
+ let released = false;
466
+ return async () => {
467
+ if (released) return;
468
+ released = true;
469
+ // Owner-safe release: only remove the lock if it is still ours.
470
+ try {
471
+ const current = await fs.readFile(lockPath, "utf8");
472
+ if (current === token) await fs.rm(lockPath, { force: true });
473
+ } catch {
474
+ // Lock already gone; nothing to release.
475
+ }
476
+ };
477
+ }
478
+ if (publication.reason !== "destination_exists") {
479
+ throw new Error(
480
+ `Failed to publish GJC plugin registry lock at ${lockPath}: ${publication.code ?? publication.reason}`,
481
+ );
350
482
  }
351
- let released = false;
352
- return async () => {
353
- if (released) return;
354
- released = true;
355
- // Owner-safe release: only remove the lock if it is still ours.
356
- try {
357
- const current = await fs.readFile(lockPath, "utf8");
358
- if (current === token) await fs.rm(lockPath, { force: true });
359
- } catch {
360
- // Lock already gone; nothing to release.
361
- }
362
- };
363
483
  } catch (error) {
364
- if ((error as NodeJS.ErrnoException)?.code !== "EEXIST") throw error;
365
- // Fail-closed: never auto-evict an existing lock (a live holder may run
366
- // longer than the timeout). Time out instead and leave the lock for
367
- // diagnostics/manual cleanup. A lease/heartbeat protocol can be added
368
- // later if automatic stale recovery becomes necessary.
369
- if (Date.now() > deadline) {
370
- throw new GjcPluginLoadError(
371
- "install_conflict",
372
- `Timed out acquiring GJC plugin registry lock at ${lockPath}; remove it manually if no install is running`,
484
+ if (publication?.reason === "destination_exists") {
485
+ // Continue into stale inspection below.
486
+ } else if (
487
+ publication === undefined &&
488
+ ["EACCES", "EPERM"].includes((error as NodeJS.ErrnoException)?.code ?? "")
489
+ ) {
490
+ // A read-only registry directory can still contain a lock left by a
491
+ // dead writer. Inspect that lock and return the normal bounded
492
+ // install_conflict rather than surfacing the staging-file permission
493
+ // error immediately.
494
+ const lockExists = await fs.stat(lockPath).then(
495
+ () => true,
496
+ (error: unknown) => !isEnoent(error),
373
497
  );
498
+ if (!lockExists) throw error;
499
+ } else throw error;
500
+ } finally {
501
+ if (publication?.mutationState !== "unknown") await fs.rm(stagingPath, { force: true }).catch(() => {});
502
+ }
503
+
504
+ // Stale recovery: a writer that died mid-transaction never releases
505
+ // its lock, and without recovery that lock poisons every later
506
+ // session until manual cleanup. Evict only a provably dead holder:
507
+ // a host-tagged token on this host. Legacy `pid-nonce` tokens have no
508
+ // trustworthy host identity, so they always fail closed: a local PID
509
+ // probe cannot prove that a remote holder on shared storage is dead.
510
+ // Live holders still fail closed below — they may legitimately outlive
511
+ // our timeout.
512
+ const raw = await fs.readFile(lockPath, "utf8").catch(() => null);
513
+ const holder = raw === null ? null : parseRegistryLockToken(raw);
514
+ if (raw !== null && holder !== null && holder.host !== null && !registryLockHolderAlive(holder)) {
515
+ try {
516
+ // exactUnlink verifies the observed inode, parent, size, mtime,
517
+ // and bytes inside the removal primitive. A replacement published
518
+ // after this read is therefore never consumed by stale recovery.
519
+ await evictRegistryLock(lockPath, raw);
520
+ } catch {
521
+ // Raced with another evictor or a fresh holder, or the removal
522
+ // itself failed (lockfile owned by another user, read-only mount,
523
+ // EBUSY/EPERM on Windows). Fall through to the shared deadline
524
+ // and backoff below either way: a removal that keeps failing must
525
+ // end in the bounded install_conflict timeout, never an unbounded
526
+ // spin.
374
527
  }
375
- await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
376
528
  }
529
+ if (Date.now() > deadline) {
530
+ throw new GjcPluginLoadError(
531
+ "install_conflict",
532
+ `Timed out acquiring GJC plugin registry lock at ${lockPath}; remove it manually if no install is running`,
533
+ );
534
+ }
535
+ await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
377
536
  }
378
537
  }
379
538