@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
@@ -269,7 +269,7 @@ export interface CompactionCandidateFailure {
269
269
  readonly message: string;
270
270
  }
271
271
 
272
- function sanitizeCompactionCandidateFailureMessage(message: string): string {
272
+ export function sanitizeCompactionCandidateFailureMessage(message: string): string {
273
273
  const cleaned = cleanReason(message) ?? "Compaction candidate failed.";
274
274
  return cleaned.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>'"]+/gi, "[redacted URL]");
275
275
  }
@@ -277,12 +277,11 @@ function sanitizeCompactionCandidateFailureMessage(message: string): string {
277
277
  /**
278
278
  * Error for an auto-compaction run whose every candidate failed.
279
279
  *
280
- * The candidate chain starts at the session model and ends on a same-provider
281
- * largest-context fallback, so surfacing only the final error named a model
282
- * the user never chose and hid that their own model had already failed the
283
- * same way. The message leads with the first failure (the one the user can act
284
- * on), then lists every candidate that was tried with its own error. `cause`
285
- * is the final error so callers that classify by type keep the same object.
280
+ * The candidate chain starts at the session model and includes only explicitly
281
+ * configured role candidates. The message leads with the first failure (the
282
+ * one the user can act on), then lists every candidate that was tried with its
283
+ * own error. `cause` is the final error so callers that classify by type keep
284
+ * the same object.
286
285
  */
287
286
  export function describeCompactionCandidateFailures(
288
287
  failures: readonly CompactionCandidateFailure[],
@@ -38,7 +38,20 @@ import foldReceiptPrompt from "../prompts/tools/fold-receipt.md" with { type: "t
38
38
  export const FOLD_WAKE_MERGE_WINDOW_MS = 800;
39
39
 
40
40
  /** The wait kinds that can be folded. `task`/`subagent` awaits are a non-goal. */
41
- export type FoldWaitKind = "bash-managed" | "client-terminal" | "bash-pty";
41
+ export type FoldWaitKind = "bash-managed" | "client-terminal" | "bash-pty" | "job-await";
42
+
43
+ /**
44
+ * Whether a wait may be picked as the implicit fold target by the chord and
45
+ * the SDK `bash.background` control. A `job-await` watches jobs that are
46
+ * already in the background, so "move the foreground bash into a background
47
+ * job" has nothing to move; it folds only when the `job` tool's own steer
48
+ * watcher names it explicitly. Keeping it out of implicit targeting also stops
49
+ * a `job poll` registered after a foreground bash from taking the chord away
50
+ * from the command the user is actually watching.
51
+ */
52
+ export function isImplicitFoldTarget(adapter: FoldAdapter): boolean {
53
+ return adapter.kind !== "job-await";
54
+ }
42
55
 
43
56
  /** Outcome of settling the foreground caller. Exactly one party may settle it. */
44
57
  export type ForegroundSettleOutcome = "resolved" | "already-settled";
@@ -237,15 +250,21 @@ export class FoldCoordinator {
237
250
  };
238
251
  }
239
252
 
240
- /** Whether any wait is currently foldable, for the key-availability gate. */
253
+ /** Whether any implicitly targetable wait is currently foldable, for the key-availability gate. */
241
254
  hasFoldableParticipant(): boolean {
242
- return this.#participants.size > 0;
255
+ return this.resolveTarget() !== undefined;
243
256
  }
244
257
 
245
- /** Deterministic target: newest registration wins, so the chord folds what the user is watching. */
258
+ /**
259
+ * Deterministic implicit target: newest implicitly targetable registration
260
+ * wins, so the chord folds what the user is watching. `job-await` waits are
261
+ * skipped; they fold only through an explicit adapter.
262
+ */
246
263
  resolveTarget(): FoldAdapter | undefined {
247
264
  let target: FoldAdapter | undefined;
248
- for (const adapter of this.#participants.values()) target = adapter;
265
+ for (const adapter of this.#participants.values()) {
266
+ if (isImplicitFoldTarget(adapter)) target = adapter;
267
+ }
249
268
  return target;
250
269
  }
251
270
 
@@ -195,27 +195,31 @@ export type ManagedScopeResolution =
195
195
  };
196
196
 
197
197
  /**
198
- * Classify a failure for the operator-visible `cause`.
199
- *
200
- * Must agree with {@link managedScopeErrorCode}: the reported `code` and the
201
- * `cause.classification` printed in the startup error are produced by separate
202
- * helpers, so a message recognized by one and not the other still surfaces to
203
- * the operator as `binding_invalid`.
198
+ * Fixed, path-free identity tokens thrown as plain errors by the managed
199
+ * storage guards (`assertManagedPathIdentity`). They carry no pathname, so
200
+ * propagating them keeps startup redaction intact while telling the operator
201
+ * which invariant actually failed instead of a blanket `binding_invalid`.
204
202
  */
205
- function managedScopeFailureCause(error: unknown): { readonly classification: string } {
206
- const security = managedSecurityFailureClassification(error);
207
- if (security) return { classification: security };
208
- return { classification: managedScopeErrorCode(error instanceof Error ? error.message : "") };
209
- }
203
+ const managedScopeIdentityFailureCodes = new Set(["identity_mismatch", "reparse_point", "not_directory", "not_file"]);
210
204
 
211
205
  /**
212
- * A scope whose directory outgrew a scan budget. The binding is untouched and
213
- * canonical; only the surrounding entry count is over budget, so this must not
214
- * be reported as binding corruption.
206
+ * Fixed, path-free errno tokens. A managed scope that fails to prepare because
207
+ * another process holds the object (AV scanners, indexers, cloud-sync agents)
208
+ * is operationally distinct from a corrupted binding, and only the errno
209
+ * distinguishes them. Node places the pathname in `.message`/`.path`, never in
210
+ * `.code`, so the token alone is safe to surface.
215
211
  */
216
- function isManagedScopeCapacityMessage(message: string): boolean {
217
- return message === "artifact_capacity_exceeded" || message === "managed_replace_cleanup_receipt_limit_exceeded";
218
- }
212
+ const managedScopeErrnoFailureCodes = new Set([
213
+ "EACCES",
214
+ "EPERM",
215
+ "EBUSY",
216
+ "EROFS",
217
+ "ENOSPC",
218
+ "EMFILE",
219
+ "ENFILE",
220
+ "ELOOP",
221
+ "ENOTDIR",
222
+ ]);
219
223
 
220
224
  const managedScopeFailureCodes = new Set<ManagedScopeErrorCode>([
221
225
  "atomic_unavailable",
@@ -226,6 +230,31 @@ const managedScopeFailureCodes = new Set<ManagedScopeErrorCode>([
226
230
  "capacity_exceeded",
227
231
  ]);
228
232
 
233
+ /**
234
+ * A scope whose directory outgrew a scan budget. The binding is untouched and
235
+ * canonical; only the surrounding entry count is over budget, so this must not
236
+ * be reported as binding corruption.
237
+ */
238
+ function isManagedScopeCapacityMessage(message: string): boolean {
239
+ return message === "artifact_capacity_exceeded" || message === "managed_replace_cleanup_receipt_limit_exceeded";
240
+ }
241
+
242
+ /**
243
+ * Recover the path-free classification for a failure that carries no security
244
+ * classification of its own.
245
+ *
246
+ * `managedScopeFailureCodes` deliberately stays out of this: those tokens are
247
+ * already reported through `code`, and the `code`/`classification` split is
248
+ * pinned behaviour. This only rescues the identity and errno failures that
249
+ * otherwise reach the operator as an undifferentiated `binding_invalid`.
250
+ */
251
+ function managedScopeResidualClassification(error: unknown): string | undefined {
252
+ if (!(error instanceof Error)) return undefined;
253
+ if (managedScopeIdentityFailureCodes.has(error.message)) return error.message;
254
+ const errno = (error as NodeJS.ErrnoException).code;
255
+ return errno !== undefined && managedScopeErrnoFailureCodes.has(errno) ? errno : undefined;
256
+ }
257
+
229
258
  /**
230
259
  * Preserve a recognized native/publication failure as its own error code.
231
260
  *
@@ -240,12 +269,23 @@ function managedScopeErrorCode(message: string): ManagedScopeErrorCode {
240
269
  : "binding_invalid";
241
270
  }
242
271
 
272
+ function managedScopeFailureCause(error: unknown): { readonly classification: string } {
273
+ const security = managedSecurityFailureClassification(error);
274
+ if (security) return { classification: security };
275
+ const residual = managedScopeResidualClassification(error);
276
+ if (residual) return { classification: residual };
277
+ return {
278
+ classification: managedScopeErrorCode(error instanceof Error ? error.message : ""),
279
+ };
280
+ }
281
+
243
282
  function managedScopeFailureMessage(error: unknown, fallback: string): string {
244
283
  const classification = managedSecurityFailureClassification(error);
245
284
  if (classification) return classification;
246
285
  if (!(error instanceof Error)) return fallback;
247
286
  if (error.message === "content_too_large" || isManagedScopeCapacityMessage(error.message)) return error.message;
248
- return managedScopeFailureCodes.has(error.message as ManagedScopeErrorCode) ? error.message : fallback;
287
+ if (managedScopeFailureCodes.has(error.message as ManagedScopeErrorCode)) return error.message;
288
+ return managedScopeResidualClassification(error) ?? fallback;
249
289
  }
250
290
 
251
291
  export interface ManagedCandidate {
@@ -4,6 +4,7 @@ import * as fsp from "node:fs/promises";
4
4
  import * as path from "node:path";
5
5
  import type {
6
6
  NativeDirectoryTreeSnapshot,
7
+ NativeExactFileIdentity,
7
8
  NativeExactUnlinkResult,
8
9
  NativeOwnerOnlySecurityResult,
9
10
  RecoveryFsIdentity,
@@ -25,6 +26,7 @@ type NativeManagedSessionStorage = Pick<
25
26
  | "exactRemoveDirectoryTree"
26
27
  | "exactReplacePath"
27
28
  | "exactUnlink"
29
+ | "exactUnlinkDirect"
28
30
  | "linkNoReplacePath"
29
31
  | "linkNoReplacePathAsync"
30
32
  | "openRecoveryFsRoot"
@@ -569,6 +571,28 @@ const SCRUBBED_REMNANT_PREFIXES = [
569
571
  ".gjc-receipt-placeholder-remove-",
570
572
  ".gjc-replace-retry-",
571
573
  ] as const;
574
+ const EMPTY_FILE_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
575
+
576
+ /**
577
+ * Replacement staging is named `.<destination-basename>.<uuid>.replacement` and
578
+ * is published by exchanging it with the destination, so the staging NAME
579
+ * survives publication while pointing at the retired predecessor. On a POSIX
580
+ * exchange that leaves the successor inode reachable under two names until the
581
+ * staging name is unlinked, and a crash between exchange and unlink strands it.
582
+ *
583
+ * A stranded staging name that still resolves to the LIVE transcript inode is
584
+ * not evidence of anything: every managed capture opens no-follow and rejects
585
+ * `nlink > 1` (`captureManagedFileNoFollowLimit`, `inspectManagedFileNoFollow`),
586
+ * so the extra name makes the transcript permanently unlistable and unopenable
587
+ * — `--resume <id>` reports "Session not found." and `--continue` reports
588
+ * "Could not inspect managed session: read-failed", while the bytes are intact
589
+ * and resume by explicit path still works. Reaping the surplus NAME (never the
590
+ * inode) restores both paths without touching transcript content.
591
+ */
592
+ const REPLACEMENT_STAGING_NAME =
593
+ /^\.(?<destination>.+)\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.replacement$/;
594
+ const REAPER_QUARANTINE_NAME =
595
+ /^\.gjc-remnant-reap-(?<destination>[0-9a-f]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
572
596
 
573
597
  /** In-flight protocol steps complete in milliseconds; anything older is abandoned. */
574
598
  const SCRUBBED_REMNANT_MIN_AGE_MS = 15 * 60 * 1000;
@@ -590,6 +614,237 @@ function reportScrubbedProtocolRemnantReap(reaped: number, failures: number): Sc
590
614
  return { reaped, failures };
591
615
  }
592
616
 
617
+ type ReplacementAliasObservation = {
618
+ destination: string;
619
+ staging: fs.BigIntStats;
620
+ };
621
+
622
+ function replacementStagingDestination(name: string): string | undefined {
623
+ return REPLACEMENT_STAGING_NAME.exec(name)?.groups?.destination;
624
+ }
625
+
626
+ function remnantReapDestination(name: string): string | undefined {
627
+ const encoded = REAPER_QUARANTINE_NAME.exec(name)?.groups?.destination;
628
+ if (!encoded || encoded.length % 2 !== 0) return undefined;
629
+ const destination = Buffer.from(encoded, "hex").toString("utf8");
630
+ return destination &&
631
+ Buffer.from(destination).toString("hex") === encoded &&
632
+ path.basename(destination) === destination
633
+ ? destination
634
+ : undefined;
635
+ }
636
+
637
+ function remnantReapQuarantineName(pathname: string): string {
638
+ const basename = path.basename(pathname);
639
+ const destination = replacementStagingDestination(basename) ?? remnantReapDestination(basename) ?? basename;
640
+ return `.gjc-remnant-reap-${Buffer.from(destination).toString("hex")}-${randomUUID()}`;
641
+ }
642
+
643
+ function isRegularReaperFile(stat: fs.BigIntStats): boolean {
644
+ return stat.isFile() && !stat.isSymbolicLink();
645
+ }
646
+
647
+ function sameReaperFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
648
+ return (
649
+ left.dev === right.dev &&
650
+ left.ino === right.ino &&
651
+ left.nlink === right.nlink &&
652
+ left.size === right.size &&
653
+ left.mtimeNs === right.mtimeNs &&
654
+ left.ctimeNs === right.ctimeNs
655
+ );
656
+ }
657
+
658
+ /**
659
+ * True when `name` is replacement staging whose inode is ALSO reachable under
660
+ * its own published destination in the same directory. Publication exchanged
661
+ * the two names, so the surplus staging name aliases the live successor and
662
+ * pins its `nlink` above one; reaping the staging name restores the
663
+ * single-link invariant every managed capture requires.
664
+ *
665
+ * Two distinct names resolving to one inode already prove `nlink > 1`, so
666
+ * identity equality is the whole test — the staging name always carries a
667
+ * leading dot the destination does not, and can never be its own destination.
668
+ *
669
+ * A staging name that does NOT alias its destination is left untouched: it is
670
+ * either an unpublished attempt or a retired predecessor, and both are
671
+ * recovery evidence owned by receipt reconciliation.
672
+ */
673
+ function aliasesPublishedDestinationSync(
674
+ directory: string,
675
+ name: string,
676
+ destination: string,
677
+ ): ReplacementAliasObservation | undefined {
678
+ try {
679
+ const staging = fs.lstatSync(path.join(directory, name), { bigint: true });
680
+ if (!isRegularReaperFile(staging) || staging.nlink < 2n) return undefined;
681
+ const published = fs.lstatSync(path.join(directory, destination), { bigint: true });
682
+ if (!isRegularReaperFile(published) || published.dev !== staging.dev || published.ino !== staging.ino)
683
+ return undefined;
684
+ return { destination, staging };
685
+ } catch {
686
+ return undefined;
687
+ }
688
+ }
689
+
690
+ function stagingAliasesPublishedDestinationSync(
691
+ directory: string,
692
+ name: string,
693
+ ): ReplacementAliasObservation | undefined {
694
+ const destination = replacementStagingDestination(name);
695
+ return destination ? aliasesPublishedDestinationSync(directory, name, destination) : undefined;
696
+ }
697
+
698
+ function remnantReapAliasesPublishedDestinationSync(
699
+ directory: string,
700
+ name: string,
701
+ ): ReplacementAliasObservation | undefined {
702
+ const destination = remnantReapDestination(name);
703
+ return destination ? aliasesPublishedDestinationSync(directory, name, destination) : undefined;
704
+ }
705
+
706
+ async function aliasesPublishedDestination(
707
+ directory: string,
708
+ name: string,
709
+ destination: string,
710
+ ): Promise<ReplacementAliasObservation | undefined> {
711
+ try {
712
+ const staging = await fsp.lstat(path.join(directory, name), { bigint: true });
713
+ if (!isRegularReaperFile(staging) || staging.nlink < 2n) return undefined;
714
+ const published = await fsp.lstat(path.join(directory, destination), { bigint: true });
715
+ if (!isRegularReaperFile(published) || published.dev !== staging.dev || published.ino !== staging.ino)
716
+ return undefined;
717
+ return { destination, staging };
718
+ } catch {
719
+ return undefined;
720
+ }
721
+ }
722
+
723
+ async function stagingAliasesPublishedDestination(
724
+ directory: string,
725
+ name: string,
726
+ ): Promise<ReplacementAliasObservation | undefined> {
727
+ const destination = replacementStagingDestination(name);
728
+ return destination ? aliasesPublishedDestination(directory, name, destination) : undefined;
729
+ }
730
+
731
+ async function remnantReapAliasesPublishedDestination(
732
+ directory: string,
733
+ name: string,
734
+ ): Promise<ReplacementAliasObservation | undefined> {
735
+ const destination = remnantReapDestination(name);
736
+ return destination ? aliasesPublishedDestination(directory, name, destination) : undefined;
737
+ }
738
+
739
+ function hashReaperFileSync(pathname: string, expected: fs.BigIntStats): string | undefined {
740
+ let descriptor: number | undefined;
741
+ try {
742
+ descriptor = fs.openSync(
743
+ pathname,
744
+ fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0) | (fs.constants.O_NOFOLLOW ?? 0),
745
+ );
746
+ const opened = fs.fstatSync(descriptor, { bigint: true });
747
+ if (!isRegularReaperFile(opened) || !sameReaperFileIdentity(opened, expected)) return undefined;
748
+ const digest = createHash("sha256");
749
+ const chunk = Buffer.alloc(64 * 1024);
750
+ for (;;) {
751
+ const count = fs.readSync(descriptor, chunk, 0, chunk.byteLength, null);
752
+ if (count === 0) break;
753
+ digest.update(chunk.subarray(0, count));
754
+ }
755
+ const after = fs.fstatSync(descriptor, { bigint: true });
756
+ return isRegularReaperFile(after) && sameReaperFileIdentity(after, expected) ? digest.digest("hex") : undefined;
757
+ } catch (error) {
758
+ if (isEnoent(error) || (error as NodeJS.ErrnoException).code === "ELOOP") return undefined;
759
+ throw error;
760
+ } finally {
761
+ if (descriptor !== undefined) fs.closeSync(descriptor);
762
+ }
763
+ }
764
+
765
+ async function hashReaperFile(pathname: string, expected: fs.BigIntStats): Promise<string | undefined> {
766
+ let handle: fsp.FileHandle | undefined;
767
+ try {
768
+ handle = await fsp.open(
769
+ pathname,
770
+ fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0) | (fs.constants.O_NOFOLLOW ?? 0),
771
+ );
772
+ const opened = await handle.stat({ bigint: true });
773
+ if (!isRegularReaperFile(opened) || !sameReaperFileIdentity(opened, expected)) return undefined;
774
+ const digest = createHash("sha256");
775
+ const chunk = Buffer.alloc(64 * 1024);
776
+ let offset = 0;
777
+ for (;;) {
778
+ const { bytesRead } = await handle.read(chunk, 0, chunk.byteLength, offset);
779
+ if (bytesRead === 0) break;
780
+ digest.update(chunk.subarray(0, bytesRead));
781
+ offset += bytesRead;
782
+ }
783
+ const after = await handle.stat({ bigint: true });
784
+ return isRegularReaperFile(after) && sameReaperFileIdentity(after, expected) ? digest.digest("hex") : undefined;
785
+ } catch (error) {
786
+ if (isEnoent(error) || (error as NodeJS.ErrnoException).code === "ELOOP") return undefined;
787
+ throw error;
788
+ } finally {
789
+ await handle?.close();
790
+ }
791
+ }
792
+
793
+ function exactReaperUnlinkSync(
794
+ pathname: string,
795
+ stat: fs.BigIntStats,
796
+ sha256: string,
797
+ allowHardLink: boolean,
798
+ ): NativeExactUnlinkResult {
799
+ const parent = fs.lstatSync(path.dirname(pathname), { bigint: true });
800
+ if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
801
+ const identity: NativeExactFileIdentity = {
802
+ dev: stat.dev,
803
+ ino: stat.ino,
804
+ nlink: stat.nlink,
805
+ parentDev: parent.dev,
806
+ parentIno: parent.ino,
807
+ size: stat.size,
808
+ mtimeNs: stat.mtimeNs,
809
+ sha256,
810
+ quarantineName: remnantReapQuarantineName(pathname),
811
+ ...(allowHardLink ? { allowHardLink: true, requireHardLink: true } : {}),
812
+ };
813
+ return nativeSessionStorage().exactUnlinkDirect(pathname, identity);
814
+ }
815
+
816
+ async function exactReaperUnlink(
817
+ pathname: string,
818
+ stat: fs.BigIntStats,
819
+ sha256: string,
820
+ allowHardLink: boolean,
821
+ ): Promise<NativeExactUnlinkResult> {
822
+ const parent = await fsp.lstat(path.dirname(pathname), { bigint: true });
823
+ if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
824
+ const identity: NativeExactFileIdentity = {
825
+ dev: stat.dev,
826
+ ino: stat.ino,
827
+ nlink: stat.nlink,
828
+ parentDev: parent.dev,
829
+ parentIno: parent.ino,
830
+ size: stat.size,
831
+ mtimeNs: stat.mtimeNs,
832
+ sha256,
833
+ quarantineName: remnantReapQuarantineName(pathname),
834
+ ...(allowHardLink ? { allowHardLink: true, requireHardLink: true } : {}),
835
+ };
836
+ return nativeSessionStorage().exactUnlinkDirect(pathname, identity);
837
+ }
838
+
839
+ function isBenignReaperRace(result: NativeExactUnlinkResult): boolean {
840
+ return (
841
+ result.code === "not_found" ||
842
+ result.code === "identity_mismatch" ||
843
+ result.code === "reparse_point" ||
844
+ result.code === "not_regular_file"
845
+ );
846
+ }
847
+
593
848
  /**
594
849
  * Best-effort removal of scrubbed write-protocol remnants from one managed
595
850
  * directory. Only zero-length, single-link, non-symlink regular files whose
@@ -599,6 +854,11 @@ function reportScrubbedProtocolRemnantReap(reaped: number, failures: number): Sc
599
854
  * or detached receipts retained as evidence) are never touched. A proven
600
855
  * concurrent disappearance (ENOENT) is benign; all other I/O failures are
601
856
  * returned and logged once without aborting scope preparation.
857
+ *
858
+ * Stranded replacement staging that aliases its own published destination is
859
+ * reaped by the same pass. That entry is neither zero-length nor single-link,
860
+ * so the terminal-remnant filters above can never reach it, yet leaving it in
861
+ * place bricks resume for the session it aliases.
602
862
  */
603
863
  export function reapScrubbedProtocolRemnantsSync(
604
864
  directory: string,
@@ -614,14 +874,31 @@ export function reapScrubbedProtocolRemnantsSync(
614
874
  let reaped = 0;
615
875
  let failures = 0;
616
876
  for (const name of names) {
617
- if (!SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix))) continue;
877
+ const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
878
+ const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
879
+ const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
880
+ if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
881
+ const alias = terminalRemnant
882
+ ? undefined
883
+ : replacementCandidate
884
+ ? stagingAliasesPublishedDestinationSync(directory, name)
885
+ : remnantReapAliasesPublishedDestinationSync(directory, name);
886
+ if (!terminalRemnant && !alias) continue;
618
887
  const pathname = path.join(directory, name);
619
888
  try {
620
- const named = fs.lstatSync(pathname);
621
- if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size !== 0) continue;
889
+ // The alias probe already stat'ed the staging entry; reuse that exact
890
+ // observation rather than re-stating the pathname, which would only add
891
+ // another window. Identity is enforced at removal by the native
892
+ // primitive, not by how many times the name is looked up.
893
+ const named = terminalRemnant ? fs.lstatSync(pathname, { bigint: true }) : alias?.staging;
894
+ if (!named || !isRegularReaperFile(named)) continue;
895
+ if (terminalRemnant && (named.nlink !== 1n || named.size !== 0n)) continue;
622
896
  if (named.mtimeMs > cutoff) continue;
623
- fs.unlinkSync(pathname);
624
- reaped += 1;
897
+ const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : hashReaperFileSync(pathname, named);
898
+ if (!sha256) continue;
899
+ const removed = exactReaperUnlinkSync(pathname, named, sha256, !terminalRemnant);
900
+ if (removed.ok) reaped += 1;
901
+ else if (!isBenignReaperRace(removed)) failures += 1;
625
902
  } catch (error) {
626
903
  if (!isEnoent(error)) failures += 1;
627
904
  }
@@ -635,7 +912,8 @@ const SCRUBBED_REMNANT_REAP_BATCH_SIZE = 256;
635
912
  /**
636
913
  * Async twin of {@link reapScrubbedProtocolRemnantsSync} with the same safety
637
914
  * filters (terminal remnant prefix, zero-length, single-link, non-symlink,
638
- * older than the age gate), yielding between bounded batches. Long-lived
915
+ * older than the age gate, plus stranded staging that aliases its own
916
+ * published destination), yielding between bounded batches. Long-lived
639
917
  * processes reap per-session descendant directories through this path so a
640
918
  * legacy oversized directory cannot starve timers or sibling subagents while
641
919
  * it is being drained (issue #4394).
@@ -655,15 +933,30 @@ export async function reapScrubbedProtocolRemnants(
655
933
  let failures = 0;
656
934
  let scanned = 0;
657
935
  for (const name of names) {
658
- if (!SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix))) continue;
936
+ const terminalRemnant = SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix));
937
+ const replacementCandidate = REPLACEMENT_STAGING_NAME.test(name);
938
+ const quarantineCandidate = REAPER_QUARANTINE_NAME.test(name);
939
+ if (!terminalRemnant && !replacementCandidate && !quarantineCandidate) continue;
659
940
  if (++scanned % SCRUBBED_REMNANT_REAP_BATCH_SIZE === 0) await Bun.sleep(0);
941
+ const alias = terminalRemnant
942
+ ? undefined
943
+ : replacementCandidate
944
+ ? await stagingAliasesPublishedDestination(directory, name)
945
+ : await remnantReapAliasesPublishedDestination(directory, name);
946
+ if (!terminalRemnant && !alias) continue;
660
947
  const pathname = path.join(directory, name);
661
948
  try {
662
- const named = await fsp.lstat(pathname);
663
- if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size !== 0) continue;
949
+ // Same reuse as the sync twin: one probe observation, one identity-bound
950
+ // removal.
951
+ const named = terminalRemnant ? await fsp.lstat(pathname, { bigint: true }) : alias?.staging;
952
+ if (!named || !isRegularReaperFile(named)) continue;
953
+ if (terminalRemnant && (named.nlink !== 1n || named.size !== 0n)) continue;
664
954
  if (named.mtimeMs > cutoff) continue;
665
- await fsp.unlink(pathname);
666
- reaped += 1;
955
+ const sha256 = terminalRemnant ? EMPTY_FILE_SHA256 : await hashReaperFile(pathname, named);
956
+ if (!sha256) continue;
957
+ const removed = await exactReaperUnlink(pathname, named, sha256, !terminalRemnant);
958
+ if (removed.ok) reaped += 1;
959
+ else if (!isBenignReaperRace(removed)) failures += 1;
667
960
  } catch (error) {
668
961
  if (!isEnoent(error)) failures += 1;
669
962
  }