@gajae-code/coding-agent 0.17.2 → 0.17.4

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 (349) hide show
  1. package/CHANGELOG.md +194 -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 +18 -0
  18. package/dist/types/config/file-lock.d.ts +9 -1
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +11 -2
  21. package/dist/types/config/settings-schema.d.ts +11 -0
  22. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  23. package/dist/types/daemon/operator-contract.d.ts +5 -1
  24. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  26. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  28. package/dist/types/extensibility/shared-events.d.ts +3 -1
  29. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  30. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  31. package/dist/types/internal-urls/index.d.ts +1 -0
  32. package/dist/types/internal-urls/parse.d.ts +9 -1
  33. package/dist/types/main.d.ts +22 -0
  34. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  35. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  36. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  37. package/dist/types/modes/components/session-selector.d.ts +5 -2
  38. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  39. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +1 -1
  42. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  43. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  44. package/dist/types/modes/types.d.ts +2 -2
  45. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  46. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  47. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  48. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  49. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  50. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  51. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  52. package/dist/types/sdk/broker/broker.d.ts +13 -2
  53. package/dist/types/sdk/broker/identity.d.ts +3 -1
  54. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  55. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  56. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  57. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  58. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  59. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  60. package/dist/types/sdk/bus/index.d.ts +19 -0
  61. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  62. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  63. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  64. package/dist/types/sdk/cli/rows.d.ts +19 -1
  65. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  66. package/dist/types/sdk/client/client.d.ts +2 -0
  67. package/dist/types/sdk/guides/cli.d.ts +3 -4
  68. package/dist/types/sdk/guides/verify.d.ts +2 -0
  69. package/dist/types/sdk/host/host.d.ts +9 -0
  70. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  71. package/dist/types/sdk/host/types.d.ts +10 -0
  72. package/dist/types/sdk/index.d.ts +1 -0
  73. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  74. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  75. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  76. package/dist/types/sdk/router/session-router.d.ts +2 -0
  77. package/dist/types/sdk/startup-capability.d.ts +2 -0
  78. package/dist/types/sdk/transport/relay.d.ts +1 -0
  79. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  80. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  81. package/dist/types/session/agent-session.d.ts +168 -21
  82. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  83. package/dist/types/session/fold-coordinator.d.ts +17 -3
  84. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  85. package/dist/types/session/session-manager.d.ts +64 -1
  86. package/dist/types/session/session-work-lease.d.ts +14 -0
  87. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  88. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  89. package/dist/types/tools/bash.d.ts +37 -11
  90. package/dist/types/tools/job.d.ts +1 -1
  91. package/dist/types/tools/steer-fold.d.ts +31 -0
  92. package/dist/types/utils/git.d.ts +28 -2
  93. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  94. package/dist/types/utils/mupdf.d.ts +4 -0
  95. package/dist/types/utils/session-title-generation.d.ts +6 -0
  96. package/dist/types/utils/title-generator.d.ts +11 -2
  97. package/dist/types/web/scrapers/types.d.ts +3 -0
  98. package/package.json +18 -10
  99. package/scripts/build-binary.ts +9 -3
  100. package/scripts/compile-args.ts +0 -9
  101. package/scripts/embed-mupdf.ts +52 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  103. package/scripts/vendor-markit.ts +196 -0
  104. package/src/async/job-manager.ts +22 -1
  105. package/src/cli/daemon-cli.ts +53 -18
  106. package/src/cli/github-release.ts +87 -5
  107. package/src/cli/mcp-cli.ts +38 -4
  108. package/src/cli/public-command-entry.ts +450 -0
  109. package/src/cli/public-command-errors.ts +581 -0
  110. package/src/cli/public-command-evidence.ts +717 -0
  111. package/src/cli/public-command-help.ts +360 -0
  112. package/src/cli/public-command-registry.ts +535 -0
  113. package/src/cli/session-picker.ts +2 -1
  114. package/src/cli/setup-cli.ts +9 -1
  115. package/src/cli/skills-cli.ts +46 -2
  116. package/src/cli.ts +61 -1
  117. package/src/commands/daemon.ts +65 -69
  118. package/src/commands/sdk.ts +175 -117
  119. package/src/commands/setup.ts +2 -0
  120. package/src/commands/skills.ts +11 -0
  121. package/src/config/autorouting-tier-map.ts +14 -0
  122. package/src/config/file-lock.ts +147 -51
  123. package/src/config/model-profile-activation.ts +334 -43
  124. package/src/config/model-registry.ts +183 -25
  125. package/src/config/settings-schema.ts +26 -2
  126. package/src/coordinator-mcp/policy.ts +10 -5
  127. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  128. package/src/coordinator-mcp/question-state.ts +175 -8
  129. package/src/coordinator-mcp/server.ts +507 -81
  130. package/src/coordinator-mcp/session-reaper.ts +6 -4
  131. package/src/crash/record-loader.ts +6 -2
  132. package/src/daemon/operator-contract.ts +26 -12
  133. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  134. package/src/eval/js/context-manager.ts +8 -8
  135. package/src/eval/js/worker-core.ts +9 -4
  136. package/src/eval/js/worker-protocol.ts +2 -0
  137. package/src/extensibility/extensions/runner.ts +52 -4
  138. package/src/extensibility/extensions/types.ts +10 -0
  139. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  140. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  141. package/src/extensibility/shared-events.ts +3 -1
  142. package/src/gjc-runtime/session-state-lock.ts +165 -24
  143. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  144. package/src/internal-urls/docs-index.generated.ts +20 -19
  145. package/src/internal-urls/embedded-protocol.ts +83 -0
  146. package/src/internal-urls/index.ts +1 -0
  147. package/src/internal-urls/parse.ts +21 -1
  148. package/src/internal-urls/router.ts +4 -2
  149. package/src/main.ts +123 -55
  150. package/src/modes/acp/acp-agent.ts +408 -35
  151. package/src/modes/acp/startup-provenance.ts +59 -0
  152. package/src/modes/components/custom-provider-wizard.ts +308 -7
  153. package/src/modes/components/oauth-selector.ts +9 -0
  154. package/src/modes/components/session-selector.ts +47 -4
  155. package/src/modes/components/user-message-selector.ts +57 -15
  156. package/src/modes/controllers/command-controller.ts +58 -32
  157. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  158. package/src/modes/controllers/input-controller.ts +4 -0
  159. package/src/modes/controllers/selector-controller.ts +161 -15
  160. package/src/modes/interactive-mode.ts +21 -7
  161. package/src/modes/runtime-init.ts +1 -0
  162. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  163. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  164. package/src/modes/types.ts +2 -2
  165. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  166. package/src/prompts/system/title-system.md +1 -1
  167. package/src/prompts/tools/bash.md +1 -1
  168. package/src/prompts/tools/job.md +1 -1
  169. package/src/prompts/tools/read.md +2 -0
  170. package/src/runtime/network-prewarm-service.test.ts +36 -1
  171. package/src/runtime/network-prewarm-service.ts +28 -3
  172. package/src/runtime/process-lifecycle.ts +22 -11
  173. package/src/runtime-mcp/config.ts +16 -1
  174. package/src/runtime-mcp/manager.ts +689 -55
  175. package/src/runtime-mcp/pool.test.ts +31 -0
  176. package/src/runtime-mcp/pool.ts +304 -28
  177. package/src/runtime-mcp/redaction.ts +42 -0
  178. package/src/runtime-mcp/startup-policy.ts +7 -0
  179. package/src/runtime-mcp/tool-bridge.ts +71 -0
  180. package/src/runtime-mcp/transports/http.ts +108 -25
  181. package/src/sdk/acp/adapter.ts +27 -8
  182. package/src/sdk/broker/broker.ts +378 -52
  183. package/src/sdk/broker/identity.ts +9 -1
  184. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  185. package/src/sdk/broker/lifecycle.ts +690 -141
  186. package/src/sdk/broker/runtime.ts +1 -1
  187. package/src/sdk/broker/session-index.ts +97 -4
  188. package/src/sdk/broker/spawn-authority.ts +35 -7
  189. package/src/sdk/broker/stderr-drainer.ts +59 -0
  190. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  191. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  192. package/src/sdk/bus/index.ts +690 -80
  193. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  194. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  195. package/src/sdk/bus/telegram-daemon.ts +10 -3
  196. package/src/sdk/cli/master-cli.ts +47 -36
  197. package/src/sdk/cli/rows.ts +30 -7
  198. package/src/sdk/cli/session-cli.ts +687 -184
  199. package/src/sdk/client/client.ts +18 -0
  200. package/src/sdk/guides/catalog.ts +2 -2
  201. package/src/sdk/guides/cli.ts +20 -62
  202. package/src/sdk/guides/verify.ts +14 -0
  203. package/src/sdk/host/host.ts +49 -5
  204. package/src/sdk/host/session-runtime.test.ts +138 -2
  205. package/src/sdk/host/session-runtime.ts +311 -97
  206. package/src/sdk/host/types.ts +12 -1
  207. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  208. package/src/sdk/host/websocket-transport.ts +40 -6
  209. package/src/sdk/index.ts +11 -0
  210. package/src/sdk/prompt-deadline-flush.ts +334 -0
  211. package/src/sdk/prompt-deadline-manager.ts +133 -1
  212. package/src/sdk/prompt-tool-boundary.ts +121 -0
  213. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  214. package/src/sdk/router/session-router.ts +9 -1
  215. package/src/sdk/session.ts +182 -7
  216. package/src/sdk/startup-capability.ts +1 -1
  217. package/src/sdk/transport/relay.ts +71 -6
  218. package/src/sdk/transport/serve-cli.ts +397 -42
  219. package/src/sdk/transport/socket.ts +12 -11
  220. package/src/sdk/transport/stdio.ts +7 -3
  221. package/src/session/agent-session.ts +2437 -400
  222. package/src/session/fallback-chain-controller.ts +6 -7
  223. package/src/session/fold-coordinator.ts +24 -5
  224. package/src/session/internal/managed-session-scope.ts +58 -18
  225. package/src/session/internal/managed-session-storage.ts +304 -11
  226. package/src/session/session-manager.ts +382 -44
  227. package/src/session/session-work-lease.ts +34 -0
  228. package/src/setup/model-onboarding-guidance.ts +1 -1
  229. package/src/setup/provider-onboarding.ts +397 -24
  230. package/src/slash-commands/builtin-registry.ts +258 -14
  231. package/src/task/index.ts +14 -2
  232. package/src/tools/bash.ts +98 -78
  233. package/src/tools/fetch.ts +119 -20
  234. package/src/tools/job.ts +151 -25
  235. package/src/tools/output-meta.ts +15 -2
  236. package/src/tools/path-utils.ts +27 -3
  237. package/src/tools/read.ts +3 -3
  238. package/src/tools/steer-fold.ts +107 -0
  239. package/src/tools/tool-catalog.generated.ts +29 -2
  240. package/src/tools/tool-errors.ts +4 -0
  241. package/src/utils/git.ts +53 -3
  242. package/src/utils/markit.ts +55 -41
  243. package/src/utils/mupdf-assets.d.ts +4 -0
  244. package/src/utils/mupdf-embedded.ts +4 -0
  245. package/src/utils/mupdf.ts +129 -0
  246. package/src/utils/session-title-generation.ts +29 -0
  247. package/src/utils/title-generator.ts +46 -2
  248. package/src/web/scrapers/types.ts +24 -3
  249. package/vendor/markit-ai/LICENSE +21 -0
  250. package/vendor/markit-ai/MANIFEST.json +412 -0
  251. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  252. package/vendor/markit-ai/dist/commands/config.js +139 -0
  253. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  254. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  255. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  256. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  257. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  258. package/vendor/markit-ai/dist/commands/init.js +29 -0
  259. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  260. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  261. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  262. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  263. package/vendor/markit-ai/dist/config.d.ts +26 -0
  264. package/vendor/markit-ai/dist/config.js +42 -0
  265. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  266. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  267. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  268. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  269. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  270. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  271. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  272. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  273. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  274. package/vendor/markit-ai/dist/converters/github.js +148 -0
  275. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  276. package/vendor/markit-ai/dist/converters/html.js +30 -0
  277. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  278. package/vendor/markit-ai/dist/converters/image.js +124 -0
  279. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  280. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  281. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  282. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  283. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  284. package/vendor/markit-ai/dist/converters/json.js +21 -0
  285. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  286. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  287. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  288. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  289. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  290. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  291. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  292. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  293. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  294. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  295. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  296. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  297. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  298. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  299. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  300. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  301. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  302. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  303. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  304. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  305. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  306. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  307. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  308. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  309. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  310. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  312. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  313. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  314. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  315. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  316. package/vendor/markit-ai/dist/index.d.ts +30 -0
  317. package/vendor/markit-ai/dist/index.js +26 -0
  318. package/vendor/markit-ai/dist/main.d.ts +2 -0
  319. package/vendor/markit-ai/dist/main.js +191 -0
  320. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  321. package/vendor/markit-ai/dist/markit.js +267 -0
  322. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  323. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  324. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  325. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  326. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  327. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  328. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  329. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  330. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  331. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  332. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  333. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  334. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  335. package/vendor/markit-ai/dist/providers/index.js +56 -0
  336. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  337. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  338. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  339. package/vendor/markit-ai/dist/providers/types.js +1 -0
  340. package/vendor/markit-ai/dist/types.d.ts +36 -0
  341. package/vendor/markit-ai/dist/types.js +1 -0
  342. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  343. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  344. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  345. package/vendor/markit-ai/dist/utils/output.js +31 -0
  346. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  347. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  348. package/vendor/markit-ai/package.json +74 -0
  349. package/vendor/markit-ai.patch +49 -0
@@ -54,6 +54,16 @@ export interface RuntimeSkillDiscoveryResult {
54
54
  candidates: RuntimeSkillDiscoveryCandidate[];
55
55
  /** Deduped skill metadata the query filter ran against, including bundled entries; candidates.length <= scanned. */
56
56
  scanned: number;
57
+ /**
58
+ * Skills left after the query filter, before the page slice: the denominator
59
+ * a page is a page *of*. `scanned` counts everything the filter ran against,
60
+ * so it cannot serve this role.
61
+ */
62
+ matching: number;
63
+ /** Normalized zero-based offset the returned page starts at. */
64
+ offset: number;
65
+ /** Offset of the next page, present only while one exists; omitted on the final page. */
66
+ nextOffset?: number;
57
67
  diagnostics: RuntimeSkillDiscoveryDiagnostics;
58
68
  }
59
69
 
@@ -65,6 +75,8 @@ export interface DiscoverRuntimeSkillsOptions {
65
75
  profileAuthority?: "default" | "custom";
66
76
  query?: string;
67
77
  limit?: number;
78
+ /** Zero-based index into the matching set the returned page starts at; defaults to 0. */
79
+ offset?: number;
68
80
  source?: RuntimeSkillDiscoveryScope | "all";
69
81
  policy?: SkillsSettings;
70
82
  }
@@ -74,11 +86,26 @@ function getRuntimeHome(): string {
74
86
  }
75
87
 
76
88
  const DEFAULT_LIMIT = 20;
77
- const MAX_LIMIT = 50;
89
+ /**
90
+ * Upper bound every caller's `limit` clamps to. Exported so `gjc skills discover`
91
+ * can default a human to the widest page the library will serve instead of the
92
+ * model-context-sized `DEFAULT_LIMIT`.
93
+ */
94
+ export const SKILL_DISCOVERY_MAX_LIMIT = 50;
78
95
  const MAX_DIAGNOSTICS = 10;
79
96
  function normalizeLimit(limit: number | undefined): number {
80
97
  if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_LIMIT;
81
- return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(limit)));
98
+ return Math.max(1, Math.min(SKILL_DISCOVERY_MAX_LIMIT, Math.trunc(limit)));
99
+ }
100
+
101
+ /**
102
+ * Offsets are clamped from below only. An offset past the end is a legitimate
103
+ * empty final page (the catalog shrank between pages), not an error, so it is
104
+ * reported rather than silently snapped back onto the last populated page.
105
+ */
106
+ function normalizeOffset(offset: number | undefined): number {
107
+ if (offset === undefined || !Number.isFinite(offset) || offset < 0) return 0;
108
+ return Math.trunc(offset);
82
109
  }
83
110
 
84
111
  interface ProjectScanDir {
@@ -558,9 +585,40 @@ export async function discoverRuntimeSkills(
558
585
  seenNames,
559
586
  diagnostics,
560
587
  );
588
+ const limit = normalizeLimit(options.limit);
589
+ const offset = normalizeOffset(options.offset);
590
+ const matching = candidates.length;
591
+ const page = candidates.slice(offset, offset + limit);
592
+ // Continuation is omitted on the final page, so "is there more" is the
593
+ // presence of the field rather than a value the caller has to compare.
594
+ const nextOffset = offset + page.length < matching ? offset + page.length : undefined;
595
+ // Appended directly instead of through pushDiagnostic: these are the only
596
+ // messages that explain why a skill the caller can see on disk is absent
597
+ // from the result, and the reported case (several stale
598
+ // skills.customDirectories entries, one "does not exist" message each)
599
+ // fills the MAX_DIAGNOSTICS budget before the slice is ever reached. A
600
+ // dropped paging notice makes the truncation invisible again.
601
+ if (page.length === 0) {
602
+ if (offset > 0 && matching > 0) {
603
+ diagnostics.push(
604
+ `no skills at offset ${offset}; ${matching} matching skill${matching === 1 ? "" : "s"} total, pass --offset 0 to start over`,
605
+ );
606
+ }
607
+ } else if (nextOffset !== undefined) {
608
+ diagnostics.push(
609
+ `showing ${offset + 1}-${offset + page.length} of ${matching} matching skills; pass --offset ${nextOffset} for the next page, or narrow the query`,
610
+ );
611
+ } else if (offset > 0) {
612
+ // Final page of a multi-page walk: no continuation to offer, but the
613
+ // caller still needs to know this page is not the whole matching set.
614
+ diagnostics.push(`showing ${offset + 1}-${offset + page.length} of ${matching} matching skills`);
615
+ }
561
616
  return {
562
- candidates: candidates.slice(0, normalizeLimit(options.limit)),
617
+ candidates: page,
563
618
  scanned,
619
+ matching,
620
+ offset,
621
+ ...(nextOffset === undefined ? {} : { nextOffset }),
564
622
  diagnostics: { messages: diagnostics },
565
623
  };
566
624
  }
@@ -93,9 +93,11 @@ export interface SessionCompactEvent {
93
93
  fromExtension: boolean;
94
94
  }
95
95
 
96
- /** Fired on process exit (SIGINT/SIGTERM) */
96
+ /** Fired during session teardown, including process exit and SDK host self-reap. */
97
97
  export interface SessionShutdownEvent {
98
98
  type: "session_shutdown";
99
+ /** Set only when the SDK host reaped itself after detached-idle grace. */
100
+ reason?: "detached_idle";
99
101
  }
100
102
 
101
103
  /** Preparation data for tree navigation (used by session_before_tree event) */
@@ -14,7 +14,7 @@ import {
14
14
  readFileLockObservationForGc,
15
15
  } from "../config/file-lock";
16
16
  import { loadInstallationHostId, loadLegacyInstallationHostId } from "../config/machine-identity";
17
- import { readLinuxProcStartTimeSync } from "./linux-proc";
17
+ import { type LinuxProcPidProbeResult, probeLinuxProcPidSync, readLinuxProcStartTimeSync } from "./linux-proc";
18
18
 
19
19
  /** SHA-256 of the empty payload; the constant identity of every verified-empty receipt. */
20
20
  const EMPTY_FILE_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
@@ -122,6 +122,10 @@ function lockRetryExhausted(budget: LockRetryBudget): boolean {
122
122
  return lockRetryRemainingMs(budget) <= 0;
123
123
  }
124
124
 
125
+ function lockRetryTotalWaitExhausted(budget: LockRetryBudget): boolean {
126
+ return Math.max(0, performance.now() - budget.firstAttemptAt) >= LOCK_ACQUIRE_MAX_WAIT_MS;
127
+ }
128
+
125
129
  async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
126
130
  budget.attempts++;
127
131
  const remainingMs = lockRetryRemainingMs(budget);
@@ -501,6 +505,13 @@ export const SessionStateLockTestHooks: {
501
505
  * configuration.
502
506
  */
503
507
  probeProcessSignal?: (pid: number) => void;
508
+ /**
509
+ * @internal How an owner pid's `/proc` entry is read. Whether a killed pid is still an
510
+ * unreaped zombie depends on when its parent happens to reap it, so it cannot be
511
+ * arranged deterministically from a test. Production code never sets it, and it is
512
+ * never serialized or exposed as runtime configuration.
513
+ */
514
+ probeLinuxProcPid?: (pid: number) => LinuxProcPidProbeResult;
504
515
  /**
505
516
  * @internal Runs inside a NEW owner record's write, after the exclusive create has
506
517
  * taken the pathname and before the record's bytes land. Throwing from it fails that
@@ -560,6 +571,33 @@ export type SessionStateLockUnavailableReason =
560
571
  | "transition_claim_timeout"
561
572
  | "unsafe_lock_path_type";
562
573
 
574
+ /**
575
+ * Why a stale-transition reclaim refused, as a bounded classifier.
576
+ *
577
+ * `transition_claim_timeout` collapsed several structurally different refusals into
578
+ * one message, so an operator could not tell a foreign-host tombstone from a
579
+ * live owner from a directory an external process was stamping — the gap #5606
580
+ * asked for. Every member is a fixed enum: never a path, token, host id, pid or
581
+ * raw errno, because this rides on an error message users paste into reports.
582
+ */
583
+ export type TransitionReclaimRefusal =
584
+ /** The claim vanished, or its owner sidecar was absent/unparseable/invalid. */
585
+ | "claim_or_owner_unreadable"
586
+ /** A released tombstone carrying no `owner_host_id` at all. */
587
+ | "released_owner_unprovenanced"
588
+ /** A released tombstone belonging to a different installation. */
589
+ | "released_owner_foreign_host"
590
+ /** A released tombstone still inside `RELEASED_TRANSITION_GRACE_MS`. */
591
+ | "released_owner_within_grace"
592
+ /** The owner is alive, or its liveness could not be disproven. */
593
+ | "owner_live_or_unverifiable"
594
+ /** The claim changed between inspection and capture — an external writer. */
595
+ | "claim_generation_changed"
596
+ /** The claim was not empty when captured, so it is not ours to remove. */
597
+ | "claim_not_empty"
598
+ /** The claim's canonical path or directory snapshot could not be taken. */
599
+ | "claim_capture_failed";
600
+
563
601
  interface SessionStateLockUnavailableDetails {
564
602
  lockPath: string;
565
603
  reason: SessionStateLockUnavailableReason;
@@ -750,6 +788,28 @@ function lockUnavailable(
750
788
  });
751
789
  }
752
790
 
791
+ /**
792
+ * `transition_claim_timeout` with the reclaim refusal that kept repeating.
793
+ *
794
+ * The bare classifier could not distinguish several structurally different
795
+ * refusals, so an operator had no way to tell which action would help (#5606).
796
+ * The suffix is a fixed enum member and nothing else — this string reaches user
797
+ * reports, so it must never carry a path, token, host id, pid or errno.
798
+ */
799
+ function transitionClaimTimeout(
800
+ transitionDir: string,
801
+ budget: LockRetryBudget,
802
+ cause: unknown,
803
+ refusal: TransitionReclaimRefusal | undefined,
804
+ ): SessionStateLockUnavailableError {
805
+ const error = lockUnavailable(transitionDir, "transition_claim_timeout", budget, cause);
806
+ if (refusal === undefined) return error;
807
+ return Object.assign(error, {
808
+ reclaimRefusal: refusal,
809
+ message: `${error.message.replace(/\.$/, "")} (reclaim refused: ${refusal}).`,
810
+ });
811
+ }
812
+
753
813
  function lockDiagnostic(
754
814
  error: unknown,
755
815
  lockPath: string,
@@ -941,7 +1001,17 @@ function probeOwnerProcess(pid: number): OwnerProcessLiveness {
941
1001
  try {
942
1002
  const probe = SessionStateLockTestHooks.probeProcessSignal;
943
1003
  if (probe) probe(pid);
944
- else process.kill(pid, 0);
1004
+ else {
1005
+ // Linux keeps a killed child as a zombie until its parent reaps it. `kill(pid, 0)`
1006
+ // still succeeds for that zombie even though it can no longer hold the lock, so
1007
+ // inspect `/proc` first and treat the terminal `Z` state as proof of death.
1008
+ if (process.platform === "linux") {
1009
+ const proc = (SessionStateLockTestHooks.probeLinuxProcPid ?? probeLinuxProcPidSync)(pid);
1010
+ if (proc.kind === "absent") return "dead";
1011
+ if (proc.kind === "live" && proc.state === "Z") return "dead";
1012
+ }
1013
+ process.kill(pid, 0);
1014
+ }
945
1015
  return "alive";
946
1016
  } catch (error) {
947
1017
  const code = (error as NodeJS.ErrnoException).code;
@@ -1846,12 +1916,27 @@ async function releaseTransitionClaim(
1846
1916
  }
1847
1917
  }
1848
1918
 
1849
- async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName: string): Promise<boolean> {
1919
+ /**
1920
+ * Outcome of one stale-transition reclaim attempt. `reclaimed` drives the
1921
+ * caller's retry accounting exactly as the previous boolean did; `refusal`
1922
+ * exists only so the eventual `transition_claim_timeout` can name which
1923
+ * precondition said no (#5606).
1924
+ */
1925
+ interface TransitionReclaimOutcome {
1926
+ reclaimed: boolean;
1927
+ reclaimReason?: "released_handoff" | "dead_owner";
1928
+ refusal?: TransitionReclaimRefusal;
1929
+ }
1930
+
1931
+ async function reclaimStaleTransitionClaim(
1932
+ transitionDir: string,
1933
+ quarantineName: string,
1934
+ ): Promise<TransitionReclaimOutcome> {
1850
1935
  let stat: fsSync.BigIntStats;
1851
1936
  try {
1852
1937
  stat = await fs.lstat(transitionDir, { bigint: true });
1853
1938
  } catch {
1854
- return false;
1939
+ return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
1855
1940
  }
1856
1941
  // Regular-file claims belong to the superseded protocol. They retain the old
1857
1942
  // exact-identity stale path; released PID-1 tombstones deliberately require
@@ -1865,41 +1950,46 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
1865
1950
  },
1866
1951
  quarantineName,
1867
1952
  );
1868
- return reclaimed === "owner_removed";
1953
+ return {
1954
+ reclaimed: reclaimed === "owner_removed",
1955
+ ...(reclaimed === "owner_removed" ? { reclaimReason: "dead_owner" as const } : {}),
1956
+ };
1869
1957
  }
1870
1958
  if (!stat.isDirectory()) throw new SessionStateLockUnavailableError();
1871
1959
  const ownerSnapshot = await captureRegularLockOwner(`${transitionDir}.owner`);
1872
- if (!ownerSnapshot) return false;
1960
+ if (!ownerSnapshot) return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
1873
1961
  let owner: unknown;
1874
1962
  try {
1875
1963
  owner = JSON.parse(ownerSnapshot.bytes);
1876
1964
  } catch {
1877
- return false;
1965
+ return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
1878
1966
  }
1879
- if (!validLockOwner(owner)) return false;
1967
+ if (!validLockOwner(owner)) return { reclaimed: false, refusal: "claim_or_owner_unreadable" };
1880
1968
  await SessionStateLockTestHooks.afterTransitionStaleInspection?.(transitionDir);
1881
1969
  if (owner.released === true) {
1882
1970
  // A released record is safe only when it is qualified by this installation.
1883
1971
  // Never let a shared-volume tombstone, an absent identity, or a legacy foreign
1884
1972
  // identity authorize removal of a claim this process cannot own.
1885
- if (owner.owner_host_id === undefined) return false;
1973
+ if (owner.owner_host_id === undefined) return { reclaimed: false, refusal: "released_owner_unprovenanced" };
1886
1974
  const currentHost = await currentOwnerHostId();
1887
1975
  const legacyHost = await currentLegacyOwnerHostId();
1888
- if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost) return false;
1889
- if (Date.now() - Number(ownerSnapshot.mtimeNs / 1_000_000n) < RELEASED_TRANSITION_GRACE_MS) return false;
1976
+ if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost)
1977
+ return { reclaimed: false, refusal: "released_owner_foreign_host" };
1978
+ if (Date.now() - Number(ownerSnapshot.mtimeNs / 1_000_000n) < RELEASED_TRANSITION_GRACE_MS)
1979
+ return { reclaimed: false, refusal: "released_owner_within_grace" };
1890
1980
  } else if (await lockOwnerIsAlive(owner)) {
1891
1981
  // Only a host-qualified owner whose pid is PROVEN dead (ESRCH, or a live pid
1892
1982
  // with a provably different incarnation) authorizes reclaim. The generation
1893
1983
  // + exact-tree checks below then bind the removal to the very directory
1894
1984
  // inspected here, on every platform; a claim stranded by a force-quit
1895
1985
  // (`postmortem.quit`) would otherwise wall the session directory forever.
1896
- return false;
1986
+ return { reclaimed: false, refusal: "owner_live_or_unverifiable" };
1897
1987
  }
1898
1988
  const generation = transitionGenerationFromStat(stat);
1899
1989
  const nativePath = await canonicalOwnedTransitionPath(transitionDir).catch(() => null);
1900
- if (!nativePath) return false;
1990
+ if (!nativePath) return { reclaimed: false, refusal: "claim_capture_failed" };
1901
1991
  const captured = nativeSessionStateLock().snapshotDirectoryTree(nativePath);
1902
- if (!captured.ok || !captured.snapshot) return false;
1992
+ if (!captured.ok || !captured.snapshot) return { reclaimed: false, refusal: "claim_capture_failed" };
1903
1993
  const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
1904
1994
  if (
1905
1995
  root?.kind !== "directory" ||
@@ -1909,12 +1999,15 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
1909
1999
  root.mtimeNs !== String(generation.mtimeNs) ||
1910
2000
  root.ctimeNs !== String(generation.ctimeNs)
1911
2001
  )
1912
- return false;
1913
- if (captured.snapshot.entries.length !== 1) return false;
2002
+ return { reclaimed: false, refusal: "claim_generation_changed" };
2003
+ if (captured.snapshot.entries.length !== 1) return { reclaimed: false, refusal: "claim_not_empty" };
1914
2004
  const removed = nativeSessionStateLock().exactRemoveDirectoryTree(nativePath, captured.snapshot);
1915
2005
  if (removed.ok || removed.code === "not_found") {
1916
2006
  exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
1917
- return removed.ok;
2007
+ return {
2008
+ reclaimed: removed.ok,
2009
+ ...(removed.ok ? { reclaimReason: owner.released === true ? "released_handoff" : "dead_owner" } : {}),
2010
+ };
1918
2011
  }
1919
2012
  if (
1920
2013
  removed.code === "cleanup_pending" &&
@@ -1931,7 +2024,7 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
1931
2024
  // successor populated the detached path.
1932
2025
  await removeTransitionDir(removed.detachedPath);
1933
2026
  exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
1934
- return true;
2027
+ return { reclaimed: true, reclaimReason: owner.released === true ? "released_handoff" : "dead_owner" };
1935
2028
  }
1936
2029
  throw new SessionStateLockUnavailableError(
1937
2030
  new Error(`Stale transition claim could not be reclaimed (${removed.code ?? "unknown"}).`),
@@ -2310,6 +2403,7 @@ async function runLockPathTransition<T>(
2310
2403
  throw new SessionStateLockUnavailableError(new Error("Safe transition ownership is unsupported."));
2311
2404
  const transitionDir = `${lockFile}${LOCK_TRANSITION_RESOURCE_SUFFIX}`;
2312
2405
  const ownerFile = `${transitionDir}.owner`;
2406
+ let lastReclaimRefusal: TransitionReclaimRefusal | undefined;
2313
2407
 
2314
2408
  for (;;) {
2315
2409
  if (ownerAccessStrategy() === "unsupported" && fsSync.existsSync(transitionDir))
@@ -2332,18 +2426,37 @@ async function runLockPathTransition<T>(
2332
2426
  // Claims are held only for pathname bookkeeping, so a changed claim identity is
2333
2427
  // proof that contenders are cycling through rather than that one is wedged.
2334
2428
  const observedClaim = await observedLockHolderIdentity(transitionDir);
2335
- const reclaimed = await reclaimStaleTransitionClaim(transitionDir, quarantineName);
2336
- creditLockHandoffProgress(budget, observedClaim, reclaimed);
2429
+ const outcome = await reclaimStaleTransitionClaim(transitionDir, quarantineName);
2430
+ creditLockHandoffProgress(budget, observedClaim, outcome.reclaimed);
2431
+ // Remember the LAST refusal: on a wedged claim every retry refuses for the
2432
+ // same reason, and that reason is the actionable one. Carrying it onto the
2433
+ // timeout is the whole point — `transition_claim_timeout` alone cannot tell
2434
+ // a foreign-host tombstone from a live owner from an external writer
2435
+ // restamping the claim directory (#5606).
2436
+ if (outcome.refusal !== undefined) lastReclaimRefusal = outcome.refusal;
2337
2437
  // One safe reclaim may make progress, but a try-acquire never follows a
2338
2438
  // succession of contenders. A later maintenance pass can take the freed claim.
2339
2439
  if (budget.nonBlocking) throw SESSION_STATE_LOCK_BUSY;
2340
- if (reclaimed) {
2341
- if (lockRetryExhausted(budget))
2342
- throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
2440
+ if (outcome.reclaimed) {
2441
+ if (lockRetryExhausted(budget)) {
2442
+ // Only a released tombstone is the previous holder's explicit handoff.
2443
+ // Ordinary dead-owner reclaims remain bounded by the original deadline,
2444
+ // even though both reclaim paths unlink their owner sidecar.
2445
+ const ownerStillPresent = await fs.lstat(`${transitionDir}.owner`).then(
2446
+ () => true,
2447
+ error => (error as NodeJS.ErrnoException).code !== "ENOENT",
2448
+ );
2449
+ if (
2450
+ outcome.reclaimReason !== "released_handoff" ||
2451
+ ownerStillPresent ||
2452
+ lockRetryTotalWaitExhausted(budget)
2453
+ )
2454
+ throw transitionClaimTimeout(transitionDir, budget, error, lastReclaimRefusal);
2455
+ }
2343
2456
  continue;
2344
2457
  }
2345
2458
  if (!(await waitForLockRetry(budget)))
2346
- throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
2459
+ throw transitionClaimTimeout(transitionDir, budget, error, lastReclaimRefusal);
2347
2460
  continue;
2348
2461
  }
2349
2462
  const pendingSetup: PendingTransitionRelease = {
@@ -2753,6 +2866,34 @@ async function observedLockHolderIdentity(lockPath: string): Promise<string | un
2753
2866
  return `d:${stat.dev}:${stat.ino}:${stat.ctimeNs}`;
2754
2867
  }
2755
2868
 
2869
+ /**
2870
+ * Classify one stale-transition reclaim attempt.
2871
+ *
2872
+ * @internal exported as the seam the refusal classifier is tested through. Driving
2873
+ * a real `transition_claim_timeout` would mean waiting out `LOCK_ACQUIRE_MAX_WAIT_MS`
2874
+ * (60s) per case, and `tryWithSessionStateFileLock` throws BUSY before the timeout
2875
+ * is ever built, so the per-branch reasons have no other reachable surface (#5606).
2876
+ */
2877
+ export async function classifyTransitionReclaimForTests(
2878
+ transitionDir: string,
2879
+ quarantineName: string,
2880
+ ): Promise<TransitionReclaimOutcome> {
2881
+ return reclaimStaleTransitionClaim(transitionDir, quarantineName);
2882
+ }
2883
+
2884
+ /**
2885
+ * Build the timeout error exactly as the transition loop does.
2886
+ *
2887
+ * @internal exported so the message contract — a bounded enum suffix and nothing
2888
+ * else — is pinned without a 60s wait (#5606).
2889
+ */
2890
+ export function transitionClaimTimeoutForTests(
2891
+ transitionDir: string,
2892
+ refusal: TransitionReclaimRefusal | undefined,
2893
+ ): SessionStateLockUnavailableError {
2894
+ return transitionClaimTimeout(transitionDir, lockRetryBudget(), new Error("EEXIST"), refusal);
2895
+ }
2896
+
2756
2897
  /**
2757
2898
  * Decide what actually occupies the lock path, without following it.
2758
2899
  *
@@ -382,6 +382,26 @@ function repositoryIdentity(binding: RepositoryBinding): Record<string, unknown>
382
382
  };
383
383
  }
384
384
 
385
+ /**
386
+ * Self-transfer guards are narrower than repository identity guards: linked
387
+ * worktrees share a common dir but carry independent `.gjc` state, so they are
388
+ * valid succession peers. `captureRepositoryBinding` records a realpath for
389
+ * active roots, but persisted plan bindings are operator-controlled and may
390
+ * contain a symlink alias.
391
+ * Resolve both roots through the filesystem before comparing them; a failed
392
+ * resolution is a refusal, never a fallback to lexical path comparison.
393
+ */
394
+ async function canonicalWorktreeRoot(binding: RepositoryBinding, side: "source" | "target"): Promise<string> {
395
+ try {
396
+ return await fs.realpath(binding.worktreeRoot);
397
+ } catch {
398
+ throw new UltragoalSuccessionError(
399
+ "unsafe_target",
400
+ `Cannot resolve the ${side} worktree root physically (${binding.worktreeRoot}); refusing succession.`,
401
+ );
402
+ }
403
+ }
404
+
385
405
  function successionOperationId(input: {
386
406
  sourceRepository: RepositoryBinding;
387
407
  sourceSessionId: string;
@@ -781,11 +801,15 @@ async function resolveTargetRepositoryBinding(
781
801
  throw new UltragoalSuccessionError("unsafe_target", `--target-repo is not a directory: ${raw}`);
782
802
  }
783
803
  const target = publicRepositoryBinding(await captureRepositoryBinding(resolved, { displayPath: resolved }));
784
- if (repositoryBindingsMatch(target, sourceBinding)) {
804
+ const [targetRoot, sourceRoot] = await Promise.all([
805
+ canonicalWorktreeRoot(target, "target"),
806
+ canonicalWorktreeRoot(sourceBinding, "source"),
807
+ ]);
808
+ if (targetRoot === sourceRoot) {
785
809
  throw new UltragoalSuccessionError(
786
810
  "unsafe_target",
787
811
  `--target-repo resolves to the source repository itself (${target.worktreeRoot}). ` +
788
- "Succession moves work across repositories; it is not a rebind of the original run.",
812
+ "Succession requires a distinct worktree or repository; it is not a rebind of the original run.",
789
813
  );
790
814
  }
791
815
  return target;
@@ -1277,7 +1301,11 @@ export async function adoptUltragoalSuccession(
1277
1301
  "adoption is pinned to the exact worktree the offer named.",
1278
1302
  );
1279
1303
  }
1280
- if (repositoryBindingsMatch(targetActual, sourceRepository)) {
1304
+ const [targetRoot, sourceRoot] = await Promise.all([
1305
+ canonicalWorktreeRoot(targetActual, "target"),
1306
+ canonicalWorktreeRoot(sourceRepository, "source"),
1307
+ ]);
1308
+ if (targetRoot === sourceRoot) {
1281
1309
  throw new UltragoalSuccessionError(
1282
1310
  "unsafe_target",
1283
1311
  "Refusing to adopt a succession offer into its own source repository.",