@chances-ai/engine 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/dist/agents/discover.d.ts +30 -0
  2. package/dist/agents/discover.d.ts.map +1 -0
  3. package/dist/agents/discover.js +183 -0
  4. package/dist/agents/discover.js.map +1 -0
  5. package/dist/agents/index.d.ts +20 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/index.js +52 -0
  8. package/dist/agents/index.js.map +1 -0
  9. package/dist/agents/parse.d.ts +61 -0
  10. package/dist/agents/parse.d.ts.map +1 -0
  11. package/dist/agents/parse.js +527 -0
  12. package/dist/agents/parse.js.map +1 -0
  13. package/dist/agents/types.d.ts +52 -0
  14. package/dist/agents/types.d.ts.map +1 -0
  15. package/dist/agents/types.js +8 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
  18. package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
  19. package/dist/ai/adapters/ai-sdk-stream.js +125 -0
  20. package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
  21. package/dist/ai/adapters/ai-sdk.d.ts +56 -0
  22. package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
  23. package/dist/ai/adapters/ai-sdk.js +112 -0
  24. package/dist/ai/adapters/ai-sdk.js.map +1 -0
  25. package/dist/ai/adapters/mock.d.ts +13 -0
  26. package/dist/ai/adapters/mock.d.ts.map +1 -0
  27. package/dist/ai/adapters/mock.js +54 -0
  28. package/dist/ai/adapters/mock.js.map +1 -0
  29. package/dist/ai/adapters/openai-compatible.d.ts +23 -0
  30. package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
  31. package/dist/ai/adapters/openai-compatible.js +45 -0
  32. package/dist/ai/adapters/openai-compatible.js.map +1 -0
  33. package/dist/ai/cost.d.ts +3 -0
  34. package/dist/ai/cost.d.ts.map +1 -0
  35. package/dist/ai/cost.js +5 -0
  36. package/dist/ai/cost.js.map +1 -0
  37. package/dist/ai/index.d.ts +12 -0
  38. package/dist/ai/index.d.ts.map +1 -0
  39. package/dist/ai/index.js +11 -0
  40. package/dist/ai/index.js.map +1 -0
  41. package/dist/ai/known-models.d.ts +20 -0
  42. package/dist/ai/known-models.d.ts.map +1 -0
  43. package/dist/ai/known-models.js +129 -0
  44. package/dist/ai/known-models.js.map +1 -0
  45. package/dist/ai/registry.d.ts +12 -0
  46. package/dist/ai/registry.d.ts.map +1 -0
  47. package/dist/ai/registry.js +24 -0
  48. package/dist/ai/registry.js.map +1 -0
  49. package/dist/ai/retry.d.ts +11 -0
  50. package/dist/ai/retry.d.ts.map +1 -0
  51. package/dist/ai/retry.js +14 -0
  52. package/dist/ai/retry.js.map +1 -0
  53. package/dist/ai/router.d.ts +25 -0
  54. package/dist/ai/router.d.ts.map +1 -0
  55. package/dist/ai/router.js +36 -0
  56. package/dist/ai/router.js.map +1 -0
  57. package/dist/ai/setup.d.ts +23 -0
  58. package/dist/ai/setup.d.ts.map +1 -0
  59. package/dist/ai/setup.js +47 -0
  60. package/dist/ai/setup.js.map +1 -0
  61. package/dist/ai/summarizer.d.ts +24 -0
  62. package/dist/ai/summarizer.d.ts.map +1 -0
  63. package/dist/ai/summarizer.js +56 -0
  64. package/dist/ai/summarizer.js.map +1 -0
  65. package/dist/ai/types.d.ts +83 -0
  66. package/dist/ai/types.d.ts.map +1 -0
  67. package/dist/ai/types.js +2 -0
  68. package/dist/ai/types.js.map +1 -0
  69. package/dist/core/compaction/circuit-breaker.d.ts +32 -0
  70. package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
  71. package/dist/core/compaction/circuit-breaker.js +42 -0
  72. package/dist/core/compaction/circuit-breaker.js.map +1 -0
  73. package/dist/core/compaction/compactor.d.ts +75 -0
  74. package/dist/core/compaction/compactor.d.ts.map +1 -0
  75. package/dist/core/compaction/compactor.js +261 -0
  76. package/dist/core/compaction/compactor.js.map +1 -0
  77. package/dist/core/compaction/estimate.d.ts +39 -0
  78. package/dist/core/compaction/estimate.d.ts.map +1 -0
  79. package/dist/core/compaction/estimate.js +74 -0
  80. package/dist/core/compaction/estimate.js.map +1 -0
  81. package/dist/core/compaction/index.d.ts +5 -0
  82. package/dist/core/compaction/index.d.ts.map +1 -0
  83. package/dist/core/compaction/index.js +5 -0
  84. package/dist/core/compaction/index.js.map +1 -0
  85. package/dist/core/compaction/prune.d.ts +43 -0
  86. package/dist/core/compaction/prune.d.ts.map +1 -0
  87. package/dist/core/compaction/prune.js +51 -0
  88. package/dist/core/compaction/prune.js.map +1 -0
  89. package/dist/core/engine.d.ts +268 -0
  90. package/dist/core/engine.d.ts.map +1 -0
  91. package/dist/core/engine.js +767 -0
  92. package/dist/core/engine.js.map +1 -0
  93. package/dist/core/index.d.ts +6 -0
  94. package/dist/core/index.d.ts.map +1 -0
  95. package/dist/core/index.js +6 -0
  96. package/dist/core/index.js.map +1 -0
  97. package/dist/core/task-tool.d.ts +175 -0
  98. package/dist/core/task-tool.d.ts.map +1 -0
  99. package/dist/core/task-tool.js +901 -0
  100. package/dist/core/task-tool.js.map +1 -0
  101. package/dist/core/workspace-query.d.ts +83 -0
  102. package/dist/core/workspace-query.d.ts.map +1 -0
  103. package/dist/core/workspace-query.js +217 -0
  104. package/dist/core/workspace-query.js.map +1 -0
  105. package/dist/core/worktree/active-marker.d.ts +31 -0
  106. package/dist/core/worktree/active-marker.d.ts.map +1 -0
  107. package/dist/core/worktree/active-marker.js +109 -0
  108. package/dist/core/worktree/active-marker.js.map +1 -0
  109. package/dist/core/worktree/create.d.ts +40 -0
  110. package/dist/core/worktree/create.d.ts.map +1 -0
  111. package/dist/core/worktree/create.js +121 -0
  112. package/dist/core/worktree/create.js.map +1 -0
  113. package/dist/core/worktree/errors.d.ts +7 -0
  114. package/dist/core/worktree/errors.d.ts.map +1 -0
  115. package/dist/core/worktree/errors.js +11 -0
  116. package/dist/core/worktree/errors.js.map +1 -0
  117. package/dist/core/worktree/gc.d.ts +39 -0
  118. package/dist/core/worktree/gc.d.ts.map +1 -0
  119. package/dist/core/worktree/gc.js +146 -0
  120. package/dist/core/worktree/gc.js.map +1 -0
  121. package/dist/core/worktree/git.d.ts +53 -0
  122. package/dist/core/worktree/git.d.ts.map +1 -0
  123. package/dist/core/worktree/git.js +166 -0
  124. package/dist/core/worktree/git.js.map +1 -0
  125. package/dist/core/worktree/index.d.ts +8 -0
  126. package/dist/core/worktree/index.d.ts.map +1 -0
  127. package/dist/core/worktree/index.js +8 -0
  128. package/dist/core/worktree/index.js.map +1 -0
  129. package/dist/core/worktree/paths.d.ts +26 -0
  130. package/dist/core/worktree/paths.d.ts.map +1 -0
  131. package/dist/core/worktree/paths.js +57 -0
  132. package/dist/core/worktree/paths.js.map +1 -0
  133. package/dist/core/worktree/slug.d.ts +6 -0
  134. package/dist/core/worktree/slug.d.ts.map +1 -0
  135. package/dist/core/worktree/slug.js +21 -0
  136. package/dist/core/worktree/slug.js.map +1 -0
  137. package/dist/local-vault/file-store.d.ts +64 -0
  138. package/dist/local-vault/file-store.d.ts.map +1 -0
  139. package/dist/local-vault/file-store.js +225 -0
  140. package/dist/local-vault/file-store.js.map +1 -0
  141. package/dist/local-vault/index.d.ts +57 -0
  142. package/dist/local-vault/index.d.ts.map +1 -0
  143. package/dist/local-vault/index.js +68 -0
  144. package/dist/local-vault/index.js.map +1 -0
  145. package/dist/local-vault/keychain.d.ts +58 -0
  146. package/dist/local-vault/keychain.d.ts.map +1 -0
  147. package/dist/local-vault/keychain.js +200 -0
  148. package/dist/local-vault/keychain.js.map +1 -0
  149. package/dist/local-vault/mutex.d.ts +20 -0
  150. package/dist/local-vault/mutex.d.ts.map +1 -0
  151. package/dist/local-vault/mutex.js +44 -0
  152. package/dist/local-vault/mutex.js.map +1 -0
  153. package/dist/local-vault/passphrase.d.ts +30 -0
  154. package/dist/local-vault/passphrase.d.ts.map +1 -0
  155. package/dist/local-vault/passphrase.js +72 -0
  156. package/dist/local-vault/passphrase.js.map +1 -0
  157. package/dist/lsp/config.d.ts +34 -0
  158. package/dist/lsp/config.d.ts.map +1 -0
  159. package/dist/lsp/config.js +68 -0
  160. package/dist/lsp/config.js.map +1 -0
  161. package/dist/lsp/detect.d.ts +7 -0
  162. package/dist/lsp/detect.d.ts.map +1 -0
  163. package/dist/lsp/detect.js +78 -0
  164. package/dist/lsp/detect.js.map +1 -0
  165. package/dist/lsp/errors.d.ts +11 -0
  166. package/dist/lsp/errors.d.ts.map +1 -0
  167. package/dist/lsp/errors.js +11 -0
  168. package/dist/lsp/errors.js.map +1 -0
  169. package/dist/lsp/formatters.d.ts +147 -0
  170. package/dist/lsp/formatters.d.ts.map +1 -0
  171. package/dist/lsp/formatters.js +259 -0
  172. package/dist/lsp/formatters.js.map +1 -0
  173. package/dist/lsp/index.d.ts +31 -0
  174. package/dist/lsp/index.d.ts.map +1 -0
  175. package/dist/lsp/index.js +31 -0
  176. package/dist/lsp/index.js.map +1 -0
  177. package/dist/lsp/instance.d.ts +72 -0
  178. package/dist/lsp/instance.d.ts.map +1 -0
  179. package/dist/lsp/instance.js +489 -0
  180. package/dist/lsp/instance.js.map +1 -0
  181. package/dist/lsp/lazy-load.d.ts +27 -0
  182. package/dist/lsp/lazy-load.d.ts.map +1 -0
  183. package/dist/lsp/lazy-load.js +57 -0
  184. package/dist/lsp/lazy-load.js.map +1 -0
  185. package/dist/lsp/manager.d.ts +59 -0
  186. package/dist/lsp/manager.d.ts.map +1 -0
  187. package/dist/lsp/manager.js +242 -0
  188. package/dist/lsp/manager.js.map +1 -0
  189. package/dist/lsp/ops.d.ts +13 -0
  190. package/dist/lsp/ops.d.ts.map +1 -0
  191. package/dist/lsp/ops.js +225 -0
  192. package/dist/lsp/ops.js.map +1 -0
  193. package/dist/lsp/rpc.d.ts +47 -0
  194. package/dist/lsp/rpc.d.ts.map +1 -0
  195. package/dist/lsp/rpc.js +134 -0
  196. package/dist/lsp/rpc.js.map +1 -0
  197. package/dist/lsp/safe-uri.d.ts +18 -0
  198. package/dist/lsp/safe-uri.d.ts.map +1 -0
  199. package/dist/lsp/safe-uri.js +96 -0
  200. package/dist/lsp/safe-uri.js.map +1 -0
  201. package/dist/lsp/types.d.ts +70 -0
  202. package/dist/lsp/types.d.ts.map +1 -0
  203. package/dist/lsp/types.js +16 -0
  204. package/dist/lsp/types.js.map +1 -0
  205. package/dist/mcp/bridge.d.ts +57 -0
  206. package/dist/mcp/bridge.d.ts.map +1 -0
  207. package/dist/mcp/bridge.js +98 -0
  208. package/dist/mcp/bridge.js.map +1 -0
  209. package/dist/mcp/category.d.ts +22 -0
  210. package/dist/mcp/category.d.ts.map +1 -0
  211. package/dist/mcp/category.js +11 -0
  212. package/dist/mcp/category.js.map +1 -0
  213. package/dist/mcp/client.d.ts +228 -0
  214. package/dist/mcp/client.d.ts.map +1 -0
  215. package/dist/mcp/client.js +352 -0
  216. package/dist/mcp/client.js.map +1 -0
  217. package/dist/mcp/content.d.ts +86 -0
  218. package/dist/mcp/content.d.ts.map +1 -0
  219. package/dist/mcp/content.js +147 -0
  220. package/dist/mcp/content.js.map +1 -0
  221. package/dist/mcp/env.d.ts +19 -0
  222. package/dist/mcp/env.d.ts.map +1 -0
  223. package/dist/mcp/env.js +50 -0
  224. package/dist/mcp/env.js.map +1 -0
  225. package/dist/mcp/host.d.ts +199 -0
  226. package/dist/mcp/host.d.ts.map +1 -0
  227. package/dist/mcp/host.js +530 -0
  228. package/dist/mcp/host.js.map +1 -0
  229. package/dist/mcp/index.d.ts +18 -0
  230. package/dist/mcp/index.d.ts.map +1 -0
  231. package/dist/mcp/index.js +17 -0
  232. package/dist/mcp/index.js.map +1 -0
  233. package/dist/mcp/load-mcp-host.d.ts +32 -0
  234. package/dist/mcp/load-mcp-host.d.ts.map +1 -0
  235. package/dist/mcp/load-mcp-host.js +49 -0
  236. package/dist/mcp/load-mcp-host.js.map +1 -0
  237. package/dist/mcp/oauth/callback-server.d.ts +73 -0
  238. package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
  239. package/dist/mcp/oauth/callback-server.js +280 -0
  240. package/dist/mcp/oauth/callback-server.js.map +1 -0
  241. package/dist/mcp/oauth/config-hash.d.ts +24 -0
  242. package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
  243. package/dist/mcp/oauth/config-hash.js +55 -0
  244. package/dist/mcp/oauth/config-hash.js.map +1 -0
  245. package/dist/mcp/oauth/error-normalize.d.ts +39 -0
  246. package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
  247. package/dist/mcp/oauth/error-normalize.js +91 -0
  248. package/dist/mcp/oauth/error-normalize.js.map +1 -0
  249. package/dist/mcp/oauth/provider.d.ts +190 -0
  250. package/dist/mcp/oauth/provider.d.ts.map +1 -0
  251. package/dist/mcp/oauth/provider.js +305 -0
  252. package/dist/mcp/oauth/provider.js.map +1 -0
  253. package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
  254. package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
  255. package/dist/mcp/oauth/refresh-coalescer.js +77 -0
  256. package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
  257. package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
  258. package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
  259. package/dist/mcp/oauth/sdk-shapes.js +21 -0
  260. package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
  261. package/dist/mcp/parse.d.ts +28 -0
  262. package/dist/mcp/parse.d.ts.map +1 -0
  263. package/dist/mcp/parse.js +209 -0
  264. package/dist/mcp/parse.js.map +1 -0
  265. package/dist/mcp/prompts.d.ts +31 -0
  266. package/dist/mcp/prompts.d.ts.map +1 -0
  267. package/dist/mcp/prompts.js +71 -0
  268. package/dist/mcp/prompts.js.map +1 -0
  269. package/dist/mcp/redact.d.ts +62 -0
  270. package/dist/mcp/redact.d.ts.map +1 -0
  271. package/dist/mcp/redact.js +87 -0
  272. package/dist/mcp/redact.js.map +1 -0
  273. package/dist/mcp/resources.d.ts +70 -0
  274. package/dist/mcp/resources.d.ts.map +1 -0
  275. package/dist/mcp/resources.js +170 -0
  276. package/dist/mcp/resources.js.map +1 -0
  277. package/dist/mcp/types.d.ts +123 -0
  278. package/dist/mcp/types.d.ts.map +1 -0
  279. package/dist/mcp/types.js +2 -0
  280. package/dist/mcp/types.js.map +1 -0
  281. package/dist/memory/frontmatter.d.ts +18 -0
  282. package/dist/memory/frontmatter.d.ts.map +1 -0
  283. package/dist/memory/frontmatter.js +81 -0
  284. package/dist/memory/frontmatter.js.map +1 -0
  285. package/dist/memory/index.d.ts +5 -0
  286. package/dist/memory/index.d.ts.map +1 -0
  287. package/dist/memory/index.js +5 -0
  288. package/dist/memory/index.js.map +1 -0
  289. package/dist/memory/store.d.ts +44 -0
  290. package/dist/memory/store.d.ts.map +1 -0
  291. package/dist/memory/store.js +237 -0
  292. package/dist/memory/store.js.map +1 -0
  293. package/dist/memory/tools.d.ts +11 -0
  294. package/dist/memory/tools.d.ts.map +1 -0
  295. package/dist/memory/tools.js +159 -0
  296. package/dist/memory/tools.js.map +1 -0
  297. package/dist/memory/types.d.ts +32 -0
  298. package/dist/memory/types.d.ts.map +1 -0
  299. package/dist/memory/types.js +20 -0
  300. package/dist/memory/types.js.map +1 -0
  301. package/dist/plugin-api/index.d.ts +167 -0
  302. package/dist/plugin-api/index.d.ts.map +1 -0
  303. package/dist/plugin-api/index.js +151 -0
  304. package/dist/plugin-api/index.js.map +1 -0
  305. package/dist/plugin-logger/index.d.ts +21 -0
  306. package/dist/plugin-logger/index.d.ts.map +1 -0
  307. package/dist/plugin-logger/index.js +59 -0
  308. package/dist/plugin-logger/index.js.map +1 -0
  309. package/dist/session/index.d.ts +125 -0
  310. package/dist/session/index.d.ts.map +1 -0
  311. package/dist/session/index.js +202 -0
  312. package/dist/session/index.js.map +1 -0
  313. package/dist/tools/approval.d.ts +33 -0
  314. package/dist/tools/approval.d.ts.map +1 -0
  315. package/dist/tools/approval.js +53 -0
  316. package/dist/tools/approval.js.map +1 -0
  317. package/dist/tools/builtins/_shared.d.ts +94 -0
  318. package/dist/tools/builtins/_shared.d.ts.map +1 -0
  319. package/dist/tools/builtins/_shared.js +246 -0
  320. package/dist/tools/builtins/_shared.js.map +1 -0
  321. package/dist/tools/builtins/ask-user-question.d.ts +27 -0
  322. package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
  323. package/dist/tools/builtins/ask-user-question.js +191 -0
  324. package/dist/tools/builtins/ask-user-question.js.map +1 -0
  325. package/dist/tools/builtins/bash.d.ts +3 -0
  326. package/dist/tools/builtins/bash.d.ts.map +1 -0
  327. package/dist/tools/builtins/bash.js +158 -0
  328. package/dist/tools/builtins/bash.js.map +1 -0
  329. package/dist/tools/builtins/diff.d.ts +3 -0
  330. package/dist/tools/builtins/diff.d.ts.map +1 -0
  331. package/dist/tools/builtins/diff.js +83 -0
  332. package/dist/tools/builtins/diff.js.map +1 -0
  333. package/dist/tools/builtins/edit.d.ts +3 -0
  334. package/dist/tools/builtins/edit.d.ts.map +1 -0
  335. package/dist/tools/builtins/edit.js +40 -0
  336. package/dist/tools/builtins/edit.js.map +1 -0
  337. package/dist/tools/builtins/glob.d.ts +3 -0
  338. package/dist/tools/builtins/glob.d.ts.map +1 -0
  339. package/dist/tools/builtins/glob.js +37 -0
  340. package/dist/tools/builtins/glob.js.map +1 -0
  341. package/dist/tools/builtins/grep.d.ts +3 -0
  342. package/dist/tools/builtins/grep.d.ts.map +1 -0
  343. package/dist/tools/builtins/grep.js +81 -0
  344. package/dist/tools/builtins/grep.js.map +1 -0
  345. package/dist/tools/builtins/lsp.d.ts +3 -0
  346. package/dist/tools/builtins/lsp.d.ts.map +1 -0
  347. package/dist/tools/builtins/lsp.js +102 -0
  348. package/dist/tools/builtins/lsp.js.map +1 -0
  349. package/dist/tools/builtins/pty.d.ts +64 -0
  350. package/dist/tools/builtins/pty.d.ts.map +1 -0
  351. package/dist/tools/builtins/pty.js +536 -0
  352. package/dist/tools/builtins/pty.js.map +1 -0
  353. package/dist/tools/builtins/read.d.ts +3 -0
  354. package/dist/tools/builtins/read.d.ts.map +1 -0
  355. package/dist/tools/builtins/read.js +18 -0
  356. package/dist/tools/builtins/read.js.map +1 -0
  357. package/dist/tools/builtins/web-fetch.d.ts +4 -0
  358. package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
  359. package/dist/tools/builtins/web-fetch.js +353 -0
  360. package/dist/tools/builtins/web-fetch.js.map +1 -0
  361. package/dist/tools/builtins/write.d.ts +3 -0
  362. package/dist/tools/builtins/write.d.ts.map +1 -0
  363. package/dist/tools/builtins/write.js +48 -0
  364. package/dist/tools/builtins/write.js.map +1 -0
  365. package/dist/tools/builtins.d.ts +9 -0
  366. package/dist/tools/builtins.d.ts.map +1 -0
  367. package/dist/tools/builtins.js +29 -0
  368. package/dist/tools/builtins.js.map +1 -0
  369. package/dist/tools/diff.d.ts +18 -0
  370. package/dist/tools/diff.d.ts.map +1 -0
  371. package/dist/tools/diff.js +57 -0
  372. package/dist/tools/diff.js.map +1 -0
  373. package/dist/tools/index.d.ts +10 -0
  374. package/dist/tools/index.d.ts.map +1 -0
  375. package/dist/tools/index.js +9 -0
  376. package/dist/tools/index.js.map +1 -0
  377. package/dist/tools/permission.d.ts +120 -0
  378. package/dist/tools/permission.d.ts.map +1 -0
  379. package/dist/tools/permission.js +208 -0
  380. package/dist/tools/permission.js.map +1 -0
  381. package/dist/tools/registry.d.ts +12 -0
  382. package/dist/tools/registry.d.ts.map +1 -0
  383. package/dist/tools/registry.js +19 -0
  384. package/dist/tools/registry.js.map +1 -0
  385. package/dist/tools/types.d.ts +244 -0
  386. package/dist/tools/types.d.ts.map +1 -0
  387. package/dist/tools/types.js +15 -0
  388. package/dist/tools/types.js.map +1 -0
  389. package/package.json +109 -0
@@ -0,0 +1,901 @@
1
+ import { relative } from "node:path";
2
+ import { AppError, ErrorCode, ModelSelection, createId, tokenFromSignal, } from "@chances-ai/runtime";
3
+ import { SessionManager } from "../session/index.js";
4
+ import { ToolRegistry, createPtyTool } from "../tools/index.js";
5
+ import { renderCatalogForToolDescription } from "../agents/index.js";
6
+ import { AgentEngine, DEFAULT_MAX_TURNS } from "./engine.js";
7
+ import { estimateTokens } from "./compaction/estimate.js";
8
+ import { createAgentWorktree, WorktreeError, } from "./worktree/index.js";
9
+ import { gitDiffShortstat, gitStatusEmpty, gitUnmergedCommitCount, } from "./worktree/git.js";
10
+ /** Public tool name. Exported so callers building child registries (or codex
11
+ * review!) can refer to the constant rather than re-spelling "task". */
12
+ export const TASK_TOOL_NAME = "task";
13
+ /** (5.1) Sibling constant — both the parent's `pty` (registered by
14
+ * `buildEngine`) and any per-subagent re-registration (here) use the
15
+ * same well-known name. */
16
+ const PTY_TOOL_NAME = "pty";
17
+ /** Upper bound on the child's `maxTurns`. The parent's config-supplied value
18
+ * is honored; this clamp only ensures a misconfigured 1 000-turn budget
19
+ * doesn't let a single `task` invocation wedge the agent for hours. */
20
+ const CHILD_MAX_TURNS_CEILING = 50;
21
+ /** (5.5) Fraction of the effective child model's context window a `context:
22
+ * "fork"` subagent may fill with inherited history. The remainder is left for
23
+ * the child's own multi-turn work, system prompt, tool schemas, and responses.
24
+ * `config.agent.forkMaxContextTokens` overrides the derived budget entirely. */
25
+ const FORK_WINDOW_FRACTION = 0.5;
26
+ /** (5.5) Budget used only when the effective child model's window can't be
27
+ * resolved. Deliberately conservative (small windows exist — the estimator
28
+ * itself warns of 32K/65K providers) so an unknown model never gets an
29
+ * optimistic high cap. */
30
+ const CONSERVATIVE_FORK_DEFAULT = 24_000;
31
+ /** (5.5 / R2 MUST-FIX) Flat allowance added to the fork estimate for the parts
32
+ * of the child's first request we don't size exactly: the base system prompt
33
+ * and any plan-mode / worktree system reminders (the per-agent `pty` tool
34
+ * schema lands here too). Memory context + the other tool schemas + the
35
+ * directive + inherited history ARE counted explicitly; this covers the rest so
36
+ * the preflight errs toward refusing a borderline fork rather than overflowing. */
37
+ const SYSTEM_BASE_RESERVE_TOKENS = 1_500;
38
+ /** (5.5) Flatten a message's content parts to text for fork token estimation.
39
+ * Tool-call args and tool-result outputs count toward the inherited size, so
40
+ * we serialize them rather than only counting `text` parts. */
41
+ function messageToText(m) {
42
+ return m.content
43
+ .map((p) => {
44
+ if (p.type === "text")
45
+ return p.text;
46
+ if (p.type === "tool-call")
47
+ return `${p.name} ${JSON.stringify(p.args)}`;
48
+ if (p.type === "tool-result")
49
+ return p.output;
50
+ return "";
51
+ })
52
+ .join("\n");
53
+ }
54
+ /** (5.5 / R2 SHOULD-FIX) Rough token size of a session's full transcript — the
55
+ * consent-relevant "how much am I sharing" figure shown in the fork permission
56
+ * summary. The execute-time guard uses a fuller estimate (tools + system); this
57
+ * is just the inherited history. */
58
+ function inheritedForkTokens(session) {
59
+ return estimateTokens(session.messages().map(messageToText).join("\n"));
60
+ }
61
+ /** (5.5 / D6) Frame the inherited history so the child treats it as background
62
+ * context, not a conversation to continue, and surface the worktree caveat when
63
+ * the child is filesystem-isolated. Lands as the child's first user turn (after
64
+ * the forked history). */
65
+ function buildForkDirective(prompt, isWorktree) {
66
+ const worktreeNote = isWorktree
67
+ ? " Note: you run in an isolated git worktree, so the parent's *uncommitted* working-tree edits referenced above are NOT visible here."
68
+ : "";
69
+ return ("[FORKED CONTEXT] The conversation above was inherited from the agent that " +
70
+ "delegated this task — it is background context only; do not continue it." +
71
+ worktreeNote +
72
+ "\n\nYour task:\n\n" +
73
+ prompt);
74
+ }
75
+ function strArg(args, key, required) {
76
+ const v = args[key];
77
+ if (v === undefined || v === null) {
78
+ if (required)
79
+ throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
80
+ return undefined;
81
+ }
82
+ if (typeof v !== "string")
83
+ throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
84
+ return v;
85
+ }
86
+ /**
87
+ * Builds the `task` built-in: a model-callable tool that spins up a child
88
+ * `AgentEngine` with its own ephemeral session, runs to completion, and
89
+ * returns the child's final assistant text.
90
+ *
91
+ * **Register per-engine, not at app boot.** This factory must be called
92
+ * from inside `buildEngine` (per session), not once when the app starts.
93
+ * Two reasons:
94
+ * 1. The child engine consumes whatever tools are in the registry *at the
95
+ * moment the child runs* — MCP-bridged tools come and go between
96
+ * sessions, and the child should see the same surface the parent sees
97
+ * *now*, not the surface from boot.
98
+ * 2. The closure captures the parent's `bus` / `gate` / `memory`
99
+ * references; rebuilds replace those references when the engine is
100
+ * re-created (e.g. on `/resume`). Boot-time registration would freeze
101
+ * stale dependencies and quietly drift across respawns.
102
+ *
103
+ * **Anti-recursion.** The child's tool registry excludes `task` itself —
104
+ * a model that can call `task` recursively without depth limits will
105
+ * fan out subagents until the process exhausts memory. v1 keeps the
106
+ * blanket exclusion; v2+ may relax it with an explicit depth cap.
107
+ *
108
+ * **No `PluginHost` passthrough.** Plugins already observe the parent's
109
+ * activity via bus events; running their hooks on every child turn would
110
+ * either double-fire (afterToolCall for the same parent-level decision)
111
+ * or silently confuse plugins that assume a single conversation lifecycle.
112
+ * Revisit if a real need emerges.
113
+ *
114
+ * **Shared `EventBus`.** Child token usage, tool calls, assistant deltas
115
+ * fire on the parent's bus. `chances stats` and the TUI therefore already
116
+ * surface subagent cost without any new channels — the only signal that
117
+ * an event came from a child is the surrounding tool-call frame.
118
+ */
119
+ export function createTaskTool(deps) {
120
+ const childMaxTurns = Math.min(Math.max(1, deps.maxTurns ?? DEFAULT_MAX_TURNS), CHILD_MAX_TURNS_CEILING);
121
+ const baseDescription = "Delegate a self-contained sub-task to a child agent. By default the child starts with a FRESH, empty conversation (it sees only your `prompt`) — use this when a task benefits from a clean context: focused file exploration, isolated refactor planning, scoped research. Set `context: \"fork\"` to instead give the child a copy of THIS conversation's completed history (everything you've already read/run), for \"continue this exact line of work\" tasks. The child has access to the same tools as you (except `task` itself — no recursion) and shares your permission gate, so session approvals carry through. The child runs to completion and returns its final assistant text as a single payload. Token usage / cost flow back to this session's telemetry.";
122
+ // Append the catalog block when agents are configured. Each entry's
123
+ // description is hard-capped + control-stripped + framed as metadata (not
124
+ // instructions) by `renderCatalogForToolDescription` — codex Round-1 #7/#8.
125
+ const catalog = deps.agents;
126
+ const rendered = catalog && catalog.size > 0 ? renderCatalogForToolDescription(catalog) : null;
127
+ const description = rendered && rendered.text
128
+ ? `${baseDescription}\n\n${rendered.text}`
129
+ : baseDescription;
130
+ // Schema. `subagent_type` is open string (the catalog is dynamic; static
131
+ // enum would be stale the moment a user edits a file). All three ref
132
+ // repos converge on this shape. Built as a JSONValue object explicitly
133
+ // so the conditional `subagent_type` doesn't end up as a union type the
134
+ // surrounding `Tool.parameters: JSONValue` field can't accept.
135
+ const properties = {
136
+ description: {
137
+ type: "string",
138
+ description: "Short label (1–3 words) describing what this subtask does. Surfaces in the permission prompt so the user sees what they're approving.",
139
+ },
140
+ prompt: {
141
+ type: "string",
142
+ description: "Full task description for the child agent. Treat this like the very first user message — make it self-contained. Even with context:'fork', the child does NOT see your CURRENT turn (only completed prior turns), so always restate the immediate ask and paste any current-turn content (e.g. a resource you just @-mentioned) the child needs.",
143
+ },
144
+ };
145
+ if (catalog && catalog.size > 0) {
146
+ properties["subagent_type"] = {
147
+ type: "string",
148
+ description: "Optional persona from the catalog (see this tool's description). Omit to spawn the default subagent (full tool inheritance from the parent).",
149
+ };
150
+ }
151
+ if (deps.backgroundTasks) {
152
+ properties["run_in_background"] = {
153
+ type: "boolean",
154
+ description: "Set to true to launch this subagent in the background and return immediately with a task id. You will receive the result as a <task-notification> block on a future turn. Use this when you want to fan out parallel work (issue multiple background launches in one turn) or when you want to keep working while a long-running investigation continues. Do not poll, do not relaunch.",
155
+ };
156
+ }
157
+ // (4.1) Worktree isolation, model-callable.
158
+ properties["isolation"] = {
159
+ type: "string",
160
+ enum: ["worktree", "none"],
161
+ description: "Optional FS isolation. 'worktree' runs this subagent inside a fresh git worktree under .chances/worktrees/; writes are not visible to the parent until the task completes. 'none' (the default) shares the parent's working tree. Overrides the agent catalog frontmatter when set.",
162
+ };
163
+ // (5.5) Conversation-context mode, model-callable (per-call only — no
164
+ // frontmatter default, since fork changes transcript disclosure + cost).
165
+ properties["context"] = {
166
+ type: "string",
167
+ enum: ["fork", "fresh"],
168
+ description: "Conversation context for the child. 'fresh' (the default) = empty context, child sees only `prompt`. 'fork' = the child inherits a copy of THIS conversation's completed turns as background context (for continuing the current line of work). Forking shares your full transcript with the child's model; it is refused if the inherited context is too large for that model, or if the child's model is a different provider than this session (unless config.agent.allowCrossProviderFork).",
169
+ };
170
+ return {
171
+ name: TASK_TOOL_NAME,
172
+ description,
173
+ category: "integration",
174
+ parameters: {
175
+ type: "object",
176
+ properties,
177
+ required: ["prompt"],
178
+ },
179
+ summarize: (args) => {
180
+ const desc = strArg(args, "description", false);
181
+ const prompt = strArg(args, "prompt", true);
182
+ const subType = strArg(args, "subagent_type", false);
183
+ const head = prompt.length > 80 ? prompt.slice(0, 80) + "…" : prompt;
184
+ // When the model targets a persona, surface the effective tool list
185
+ // in the permission summary so the user sees the narrowed surface
186
+ // before approving the spawn (codex Round-1 #17).
187
+ const personaTail = subType && catalog?.has(subType)
188
+ ? ` (tools: ${formatToolListForSummary(catalog.get(subType).tools)})`
189
+ : "";
190
+ const label = subType ? `task[${subType}]` : desc ? `task[${desc}]` : "task";
191
+ // (5.5 / D9) Surface fork transcript-disclosure + the effective
192
+ // model/provider in the permission summary so the user approving the
193
+ // spawn sees that the full conversation is handed to the child (and to
194
+ // which provider). Best-effort: never let summary resolution throw.
195
+ let forkTail = "";
196
+ if (strArg(args, "context", false) === "fork") {
197
+ try {
198
+ const personaModel = subType && catalog?.has(subType) ? catalog.get(subType).model : undefined;
199
+ const parentChoice = deps.parentSelection?.get() ?? {};
200
+ const parentProvider = deps.router.pick({
201
+ preferredModel: parentChoice.model,
202
+ preferredProvider: parentChoice.provider,
203
+ needsTools: true,
204
+ }).model.provider;
205
+ const childModel = (personaModel
206
+ ? deps.router.pick({ preferredModel: personaModel, needsTools: true })
207
+ : deps.router.pick({
208
+ preferredModel: parentChoice.model,
209
+ preferredProvider: parentChoice.provider,
210
+ needsTools: true,
211
+ })).model;
212
+ const differs = childModel.provider !== parentProvider;
213
+ const tokTail = deps.parentSession
214
+ ? ` ~${inheritedForkTokens(deps.parentSession)}tok`
215
+ : "";
216
+ forkTail = ` (fork: full transcript${tokTail} → ${childModel.id}/${childModel.provider}${differs ? " ⚠ differs from session" : ""})`;
217
+ }
218
+ catch {
219
+ forkTail = " (fork: full transcript)";
220
+ }
221
+ }
222
+ return `${label}: ${head}${personaTail}${forkTail}`;
223
+ },
224
+ async execute(args, ctx) {
225
+ const prompt = strArg(args, "prompt", true);
226
+ const subagentType = strArg(args, "subagent_type", false);
227
+ let agent;
228
+ if (subagentType !== undefined && subagentType.length > 0) {
229
+ if (!catalog || catalog.size === 0) {
230
+ return {
231
+ ok: false,
232
+ output: `subagent_type '${subagentType}' was provided but no agent catalog is configured for this session.`,
233
+ };
234
+ }
235
+ agent = catalog.get(subagentType);
236
+ if (!agent) {
237
+ const available = [...catalog.keys()].sort().join(", ");
238
+ return {
239
+ ok: false,
240
+ output: `Unknown agent type '${subagentType}'. Available: ${available}`,
241
+ };
242
+ }
243
+ }
244
+ // Build the child registry from a single computed survivor list.
245
+ // 1. Snapshot parent → drop `task` (recursion still blocked in 3.3).
246
+ // 2. If persona has explicit `tools`, intersect (fail-closed on
247
+ // unknown names — codex Round-1 #5).
248
+ // 3. If persona has `disallowedTools`, subtract (same fail-closed
249
+ // semantics; unknown disallow name → ok:false rather than
250
+ // leaving the real tool reachable).
251
+ // 4. Construct one ToolRegistry from the final list. ToolRegistry's
252
+ // v1 surface only exposes `register` + `list`, so we never
253
+ // register-then-remove — we compute survivors first, then build.
254
+ const parentNames = new Set();
255
+ const parentByName = new Map();
256
+ for (const t of deps.parentTools.list()) {
257
+ if (t.name === TASK_TOOL_NAME)
258
+ continue;
259
+ parentNames.add(t.name);
260
+ parentByName.set(t.name, t);
261
+ }
262
+ if (agent) {
263
+ if (agent.tools !== "*") {
264
+ const unknown = agent.tools.filter((n) => !parentNames.has(n));
265
+ if (unknown.length > 0) {
266
+ const available = [...parentNames].sort().join(", ");
267
+ return {
268
+ ok: false,
269
+ output: `Agent '${agent.name}' references unknown tool(s) in 'tools': ${unknown.join(", ")}. Available tools: ${available}`,
270
+ };
271
+ }
272
+ }
273
+ const unknownDisallowed = agent.disallowedTools.filter((n) => !parentNames.has(n));
274
+ if (unknownDisallowed.length > 0) {
275
+ const available = [...parentNames].sort().join(", ");
276
+ return {
277
+ ok: false,
278
+ output: `Agent '${agent.name}' references unknown tool(s) in 'disallowedTools': ${unknownDisallowed.join(", ")}. Available tools: ${available}`,
279
+ };
280
+ }
281
+ }
282
+ const allowSet = agent && agent.tools !== "*"
283
+ ? new Set(agent.tools)
284
+ : parentNames; // `*` or no agent → full surface (minus `task`)
285
+ const denySet = new Set(agent?.disallowedTools ?? []);
286
+ // (4.1) Resolve effective isolation mode. Order:
287
+ // 1) `task` tool input `isolation` (model-callable override)
288
+ // 2) agent-catalog frontmatter `isolation`
289
+ // 3) default 'none'
290
+ // The schema enum makes this self-documenting; we still validate
291
+ // the value defensively because `JSONValue` is permissive.
292
+ const inputIsolation = strArg(args, "isolation", false);
293
+ let isolationMode = "none";
294
+ if (inputIsolation !== undefined) {
295
+ if (inputIsolation !== "worktree" && inputIsolation !== "none") {
296
+ return {
297
+ ok: false,
298
+ output: `Invalid 'isolation' value '${inputIsolation}'. Expected 'worktree' or 'none'.`,
299
+ };
300
+ }
301
+ isolationMode = inputIsolation;
302
+ }
303
+ else if (agent?.isolation === "worktree") {
304
+ isolationMode = "worktree";
305
+ }
306
+ const childTools = new ToolRegistry();
307
+ const mcpFilterActive = isolationMode === "worktree" && !agent?.unsafeAllowMutatingMcp;
308
+ let mcpToolsFiltered = 0;
309
+ for (const t of parentByName.values()) {
310
+ if (!allowSet.has(t.name))
311
+ continue;
312
+ if (denySet.has(t.name))
313
+ continue;
314
+ // (5.1) Parent's `pty` instance is closed over the parent's
315
+ // agentId — registering it as-is for the child would create
316
+ // child sessions in the parent's ownership bucket, defeating
317
+ // D10 symmetric isolation and the `drainOwnedBy(childAgentId)`
318
+ // contract. Skip here and re-register with the child's id below.
319
+ if (t.name === PTY_TOOL_NAME)
320
+ continue;
321
+ // (4.1 — Round-1 MUST-FIX #2) MCP tools run in their own
322
+ // process; the worktree's cwd does NOT confine their writes.
323
+ // Inside an isolated subagent, filter MCP-source tools (named
324
+ // `mcp__<server>__<tool>`) to those whose declared category is
325
+ // in the read-only set. Unannotated tools default to
326
+ // `integration` which we treat as potentially mutating.
327
+ // Opt-out via frontmatter `unsafeAllowMutatingMcp: true`.
328
+ if (mcpFilterActive && isMcpToolName(t.name) && !isReadOnlyMcpCategory(t.category)) {
329
+ mcpToolsFiltered++;
330
+ continue;
331
+ }
332
+ childTools.register(t);
333
+ }
334
+ // Resolve model override. `agent.model` MUST resolve through the
335
+ // registry; misses fail closed (codex Round-1 #4).
336
+ let childSelection;
337
+ if (agent?.model) {
338
+ if (!deps.registry) {
339
+ return {
340
+ ok: false,
341
+ output: `Agent '${agent.name}' specifies model '${agent.model}' but the task tool was wired without a ModelRegistry; cannot validate.`,
342
+ };
343
+ }
344
+ const resolved = deps.registry.get(agent.model);
345
+ if (!resolved) {
346
+ return {
347
+ ok: false,
348
+ output: `Agent '${agent.name}' specifies model '${agent.model}' which is not in the registry. Run 'chances doctor' to see configured providers and known models.`,
349
+ };
350
+ }
351
+ childSelection = new ModelSelection({ model: agent.model });
352
+ }
353
+ const childTurnsBudget = agent?.maxTurns
354
+ ? Math.min(Math.max(1, agent.maxTurns), CHILD_MAX_TURNS_CEILING)
355
+ : childMaxTurns;
356
+ // (5.5) fork-from-parent resolution. Runs BEFORE any worktree is created
357
+ // so a gate refusal returns cleanly with nothing to tear down. All the
358
+ // fork-only state (the deep-cloned child session + the model selection
359
+ // the child runs under) is computed once here, at execute time, so the
360
+ // background path's deferred `run()` closure forks the parent at SPAWN
361
+ // time, not at run time (D7).
362
+ const inputContext = strArg(args, "context", false);
363
+ if (inputContext !== undefined && inputContext !== "fork" && inputContext !== "fresh") {
364
+ return {
365
+ ok: false,
366
+ output: `Invalid 'context' value '${inputContext}'. Expected 'fork' or 'fresh'.`,
367
+ };
368
+ }
369
+ const forkRequested = inputContext === "fork";
370
+ // `effectiveSelection` is what the child engine runs under: a persona
371
+ // `model` override always wins; a fork with no override inherits the
372
+ // parent's CURRENT selection (pinned now); a fresh subagent keeps today's
373
+ // behaviour (undefined ⇒ engine default).
374
+ let effectiveSelection = childSelection;
375
+ let forkedChildSession;
376
+ // (D6) The prompt the child actually receives. For a fork it is wrapped
377
+ // with the directive (built here so the budget estimate counts it too —
378
+ // it depends only on `isolationMode`, not the not-yet-created worktree);
379
+ // a fresh subagent passes the prompt as-is.
380
+ let childPrompt = prompt;
381
+ if (forkRequested) {
382
+ if (!deps.parentSession) {
383
+ return {
384
+ ok: false,
385
+ output: "context:'fork' was requested but the task tool was wired without a parent session; fork is unavailable. Use a fresh subagent with a self-contained prompt.",
386
+ };
387
+ }
388
+ // Resolve effective parent + child model descriptors through the router
389
+ // (same fallback the engine uses), so the budget + cross-provider gate
390
+ // see the model the child will actually run on.
391
+ const parentChoice = deps.parentSelection?.get() ?? {};
392
+ const parentModel = deps.router.pick({
393
+ preferredModel: parentChoice.model,
394
+ preferredProvider: parentChoice.provider,
395
+ needsTools: true,
396
+ }).model;
397
+ if (!effectiveSelection) {
398
+ // No persona override: inherit the parent's current model, pinned at
399
+ // execute time so a later `/model` switch (or background defer) can't
400
+ // move the child off the model the user forked on.
401
+ effectiveSelection = new ModelSelection({
402
+ provider: parentChoice.provider,
403
+ model: parentChoice.model,
404
+ });
405
+ }
406
+ const childChoice = effectiveSelection.get();
407
+ const childModel = deps.router.pick({
408
+ preferredModel: childChoice.model,
409
+ preferredProvider: childChoice.provider,
410
+ needsTools: true,
411
+ }).model;
412
+ // (D11) Cross-provider transcript-disclosure gate (fail-closed; not
413
+ // bypassable by auto-approve modes since it refuses before the gate).
414
+ if (childModel.provider !== parentModel.provider && !deps.allowCrossProviderFork) {
415
+ return {
416
+ ok: false,
417
+ output: `context:'fork' would send the full conversation transcript to provider '${childModel.provider}' (model ${childModel.id}), which differs from this session's provider '${parentModel.provider}'. Set config.agent.allowCrossProviderFork=true to permit cross-provider forks, or drop the persona model override.`,
418
+ };
419
+ }
420
+ // (D5 / R2 MUST-FIX) Model-aware budget. Fork the parent ONCE here (used
421
+ // for both the estimate and as the child's seeded session) and build the
422
+ // directive now so the estimate covers the SAME shape the child engine
423
+ // will send (`engine.ts` runTurnImpl: system + tool schemas + inherited
424
+ // messages + the directive-wrapped user turn) — not just the inherited
425
+ // history. Under-counting the tool schemas / directive would let a fork
426
+ // pass this guard and still overflow the model's window on its first
427
+ // request, which is exactly what this preflight exists to prevent.
428
+ forkedChildSession = SessionManager.forkFrom(deps.parentSession, "subagent");
429
+ childPrompt = buildForkDirective(prompt, isolationMode === "worktree");
430
+ const inheritedText = forkedChildSession.messages().map(messageToText).join("\n");
431
+ // `childTools` is fully populated here except the per-agent `pty`
432
+ // instance (registered in each branch below) — a one-tool undercount
433
+ // the window fraction + base reserve absorb. Memory context is the
434
+ // other model-visible system addition we can size cheaply; plan/worktree
435
+ // reminders + the base prompt are covered by SYSTEM_BASE_RESERVE_TOKENS.
436
+ const toolDefsText = childTools
437
+ .list()
438
+ .map((t) => `${t.name}\n${t.description}\n${JSON.stringify(t.parameters)}`)
439
+ .join("\n");
440
+ const systemText = deps.memory?.asSystemContext() ?? "";
441
+ const estimate = estimateTokens(`${systemText}\n${toolDefsText}\n${inheritedText}\n${childPrompt}`) +
442
+ SYSTEM_BASE_RESERVE_TOKENS;
443
+ const budget = deps.forkMaxContextTokens ??
444
+ (childModel.contextWindow
445
+ ? Math.floor(childModel.contextWindow * FORK_WINDOW_FRACTION)
446
+ : CONSERVATIVE_FORK_DEFAULT);
447
+ if (estimate > budget) {
448
+ return {
449
+ ok: false,
450
+ output: `Fork context (~${estimate} tokens, incl. tools + system) exceeds the budget (${budget} tokens) for model ${childModel.id}. Spawn a fresh subagent with an explicit prompt, /compact the conversation first, or raise config.agent.forkMaxContextTokens.`,
451
+ };
452
+ }
453
+ }
454
+ // (5.1) Drain helper: called on every subagent termination
455
+ // path (sync return / sync throw / background return). The
456
+ // registry only kills sessions still owned by `childAgentId`
457
+ // — sessions the parent has `pty.adopt()`-ed are re-bound to
458
+ // the parent's agentId and are no-ops here. Failure to drain
459
+ // is logged once on the bus but never blocks the caller; a
460
+ // 2-second deadline matches `AsyncTaskRegistry.killAll` from
461
+ // 3.4 so engine teardown shape is uniform.
462
+ const drainChildPty = async (childAgentId) => {
463
+ if (!deps.ptySessions)
464
+ return;
465
+ try {
466
+ // (5.1 codex Round-2 SHOULD-FIX #1) Capture survivors and
467
+ // surface them on the bus so operators see when a subagent
468
+ // PTY ignored TERM-then-KILL past the 2 s deadline. Matches
469
+ // the CLI shutdown's `pty dispose` survivor log so the two
470
+ // teardown paths report the same shape.
471
+ const { survivors } = await deps.ptySessions.drainOwnedBy(childAgentId, 2_000);
472
+ if (survivors.length > 0) {
473
+ deps.bus.emit({
474
+ type: "log",
475
+ level: "warn",
476
+ message: `subagent ${childAgentId}: ${survivors.length} pty session(s) past 2s drain deadline: ${survivors.join(", ")}`,
477
+ });
478
+ }
479
+ }
480
+ catch (e) {
481
+ deps.bus.emit({
482
+ type: "log",
483
+ level: "warn",
484
+ message: `pty drainOwnedBy(${childAgentId}) failed: ${e.message ?? e}`,
485
+ });
486
+ }
487
+ };
488
+ // 3.4 branch: `run_in_background:true` ONLY when the registry was
489
+ // wired. When undefined, the field is silently ignored (codex
490
+ // Round-1 SHOULD-FIX #1 — see design "Registry-absent fallback").
491
+ const wantBackground = deps.backgroundTasks !== undefined && boolArg(args, "run_in_background") === true;
492
+ const agentName = agent?.name ?? "default";
493
+ // (4.1) Create the worktree BEFORE spawning the child engine —
494
+ // synchronously for the sync branch, before launching for the
495
+ // background branch. The child engine receives the worktree
496
+ // path as `worktreeCwd`. On any failure path (including
497
+ // cancellation), `worktreeHandle.cleanup()` is invoked from a
498
+ // `finally` block (sync) or the registry's settle handler
499
+ // (background). Cleanup is idempotent so double-invocation is
500
+ // safe. Round-1 MUST-FIX #3 — `signal` flows into the spawn.
501
+ let worktreeHandle;
502
+ if (isolationMode === "worktree") {
503
+ try {
504
+ worktreeHandle = await createAgentWorktree(deps.workspaceRoot, {
505
+ signal: ctx.signal,
506
+ });
507
+ }
508
+ catch (e) {
509
+ if (e instanceof WorktreeError) {
510
+ return {
511
+ ok: false,
512
+ output: `Isolation worktree could not be created (${e.code}): ${e.message}. Hint: ensure the workspace is a git repository and 'git' is on PATH.`,
513
+ };
514
+ }
515
+ throw e;
516
+ }
517
+ }
518
+ const worktreeCwd = worktreeHandle?.path;
519
+ // (5.5 / D6) `childPrompt` was finalised in the fork block above (directive
520
+ // for a fork, raw prompt for a fresh subagent). `isolationMode` — which
521
+ // the directive's worktree caveat depends on — is resolved before that
522
+ // block, so no recompute is needed here even though the worktree itself is
523
+ // created later.
524
+ if (wantBackground) {
525
+ // The registry runs `run(signal)` under its own unlinked
526
+ // AbortController — parent's `ctx.signal` does NOT bind. The
527
+ // child engine's loop honors `signal` via `tokenFromSignal`,
528
+ // same path 3.3 sync subagents use.
529
+ //
530
+ // Closure capture of the task id: `launch()` is synchronous and
531
+ // returns the handle before the registry kicks `run()` into
532
+ // microtask land. The child engine's `agentContext` is a SHARED
533
+ // mutable object — we set `.agentId` after `launch()` returns,
534
+ // and because `AgentEngine.emit` re-reads `ctx.agentId` per
535
+ // emit, the engine will pick up the registry-issued id by the
536
+ // time the child's first data frame fires.
537
+ const agentContextObj = {
538
+ agentId: "",
539
+ agentName,
540
+ };
541
+ // (5.1) PTY-ownership agentId for the child. Minted here, NOT
542
+ // re-using `handle.taskId` (that's pinned to AsyncTaskRegistry
543
+ // semantics; PTY ownership is a separate concern). The drain
544
+ // path at the end of `run()` calls `drainOwnedBy(ptyAgentId)`,
545
+ // matching sessions the model started through this subagent's
546
+ // `pty` tool. Adopted sessions have re-bound to the parent's
547
+ // agentId and are no-ops there.
548
+ const childPtyAgentId = createId("sub");
549
+ if (deps.ptySessions && deps.nativeCreatePtySession) {
550
+ childTools.register(createPtyTool({
551
+ registry: deps.ptySessions,
552
+ agentId: childPtyAgentId,
553
+ agentName,
554
+ workspaceRoot: deps.workspaceRoot,
555
+ // (5.1 codex Round-2 MUST-FIX #3) Isolated subagent's
556
+ // PTY commands default to the worktree path, not the
557
+ // parent's. Without this override, `pty.start({command:
558
+ // "pwd"})` from inside an isolated subagent would
559
+ // execute against the parent checkout.
560
+ defaultCwd: worktreeCwd ?? deps.workspaceRoot,
561
+ worktreeCwd,
562
+ createHandle: deps.nativeCreatePtySession,
563
+ }));
564
+ }
565
+ try {
566
+ const handle = deps.backgroundTasks.launch({
567
+ name: agentName,
568
+ prompt,
569
+ // (5.7) Carry the worktree's RELATIVE path + branch (PII-free) so
570
+ // the OTel exporter can stamp `chances.gen_ai.worktree.*` on the
571
+ // synthesized background span. Absolute path is never carried.
572
+ ...(worktreeHandle
573
+ ? {
574
+ worktree: {
575
+ // RELATIVE to the worktree's OWN `canonicalRoot` (codex R2
576
+ // Q4) — not `deps.workspaceRoot`, which can differ when the
577
+ // caller's cwd sits inside a pre-existing user worktree, in
578
+ // which case `relative()` could emit `..` and leak an
579
+ // absolute-ish path. Against canonicalRoot this is always a
580
+ // clean `.chances/worktrees/agent-…`. The exporter guards
581
+ // again defensively before stamping.
582
+ relativePath: relative(worktreeHandle.canonicalRoot, worktreeHandle.path),
583
+ branch: worktreeHandle.branch,
584
+ },
585
+ }
586
+ : {}),
587
+ run: async (signal) => {
588
+ const startedAt = Date.now();
589
+ const childSession = forkedChildSession ?? SessionManager.create("subagent");
590
+ const childEngine = new AgentEngine({
591
+ bus: deps.bus,
592
+ router: deps.router,
593
+ tools: childTools,
594
+ gate: deps.gate,
595
+ getApprovalMode: deps.getApprovalMode,
596
+ // (5.4 D12) inherit the parent's MCP mention resolver.
597
+ resolveMcpMentions: deps.resolveMcpMentions,
598
+ session: childSession,
599
+ memory: deps.memory,
600
+ workspaceRoot: deps.workspaceRoot,
601
+ maxTurns: childTurnsBudget,
602
+ suppressTerminalErrors: true,
603
+ // 3.4: background child suppresses lifecycle events
604
+ // (turn:start/turn:end/error) so the TUI's `busy` flag
605
+ // doesn't flip when the background advances a turn.
606
+ suppressLifecycleEvents: true,
607
+ systemBaseOverride: agent?.systemPrompt,
608
+ selection: effectiveSelection,
609
+ // 3.4: stamp every data-frame event the child emits
610
+ // with the registry-issued task id + agent name so
611
+ // subscribers can demultiplex parent vs. child output.
612
+ agentContext: agentContextObj,
613
+ // (4.1) Worktree isolation — child engine flips its
614
+ // `ToolContext.workspaceRoot`/`cwd` to the worktree
615
+ // path through AsyncLocalStorage.
616
+ worktreeCwd,
617
+ });
618
+ const childToken = tokenFromSignal(signal);
619
+ try {
620
+ const result = await childEngine.runTurn(childPrompt, childToken, { expandMentions: false });
621
+ // (Round-2 SHOULD-FIX #2) Re-check cancellation BEFORE
622
+ // finalize. If a `kill` arrived after the model finished
623
+ // its last tool call but before this point, the registry
624
+ // will record the task as cancelled — the worktree
625
+ // should be cleaned up, not retained as success.
626
+ if (signal.aborted) {
627
+ if (worktreeHandle)
628
+ await worktreeHandle.cleanup();
629
+ return {
630
+ ok: false,
631
+ text: "Subagent cancelled",
632
+ durationMs: Date.now() - startedAt,
633
+ };
634
+ }
635
+ const finalText = result.text.trim() || "(subagent finished without producing any text)";
636
+ const wrapped = await finalizeWorktree(worktreeHandle, deps.workspaceRoot, finalText, mcpToolsFiltered);
637
+ return {
638
+ ok: true,
639
+ text: wrapped,
640
+ durationMs: Date.now() - startedAt,
641
+ tokens: {
642
+ input: result.inputTokens,
643
+ output: result.outputTokens,
644
+ },
645
+ };
646
+ }
647
+ catch (e) {
648
+ // Failure / cancel: tear down the worktree (idempotent).
649
+ if (worktreeHandle)
650
+ await worktreeHandle.cleanup();
651
+ if (e instanceof AppError && e.code === ErrorCode.Cancelled) {
652
+ return {
653
+ ok: false,
654
+ text: "Subagent cancelled",
655
+ durationMs: Date.now() - startedAt,
656
+ };
657
+ }
658
+ const msg = e instanceof AppError
659
+ ? `Subagent failed (${e.code}): ${e.message}`
660
+ : `Subagent failed: ${e.message || String(e)}`;
661
+ return { ok: false, text: msg, durationMs: Date.now() - startedAt };
662
+ }
663
+ finally {
664
+ // (5.1) Drain every still-running PTY session owned by
665
+ // the child. Adopted sessions re-bound to the parent
666
+ // are no-ops here. Fires before the registry surfaces
667
+ // the completion notification so the model never sees
668
+ // a `<task-notification>` for a subagent that still
669
+ // holds live shells in the background.
670
+ await drainChildPty(childPtyAgentId);
671
+ }
672
+ },
673
+ });
674
+ agentContextObj.agentId = handle.taskId;
675
+ const body = [
676
+ `(background-launched) task_id=${handle.taskId} name=${agentName}`,
677
+ ...(isolationMode === "worktree" && worktreeHandle
678
+ ? [`isolation=worktree path=${relative(deps.workspaceRoot, worktreeHandle.path) || worktreeHandle.path} branch=${worktreeHandle.branch}`]
679
+ : []),
680
+ "",
681
+ "You will receive a <task-notification> block on a future turn when this task completes. Do not poll; do not relaunch unless explicitly asked.",
682
+ ].join("\n");
683
+ return { ok: true, output: body };
684
+ }
685
+ catch (e) {
686
+ // launch() failed — if we already created a worktree, tear it
687
+ // down so we don't leak it into the next stale-GC pass.
688
+ if (worktreeHandle)
689
+ await worktreeHandle.cleanup();
690
+ // Capacity refusal or registry-disposed surfaces as a labeled
691
+ // AppError(Tool); fold it into ok:false so the model sees the
692
+ // recovery message.
693
+ if (e instanceof AppError && e.code === ErrorCode.Tool) {
694
+ return { ok: false, output: e.message };
695
+ }
696
+ throw e;
697
+ }
698
+ }
699
+ // Synchronous path. (3.6 codex Round-1 MUST-FIX #3) The 3.4
700
+ // background path correctly suppresses lifecycle frames and tags
701
+ // data frames with the agent identity; the sync path was missed
702
+ // — child `turn:start`/`turn:end`/`usage:turn` flowed onto the
703
+ // shared bus untagged, which (a) would flip TUI `busy` state
704
+ // for any subscriber that toggles on the bare event, and (b)
705
+ // would corrupt the 3.6 OTel exporter's `Map<turnId, Span>` by
706
+ // opening a second `invoke_agent` span inside the parent turn.
707
+ // Same shape as background: suppress lifecycle, tag data frames
708
+ // with `{agentName}` (no agentId — sync subagents complete before
709
+ // the parent's `tool:call`/`tool:result` boundary closes, so the
710
+ // parent already labels them at that level).
711
+ const syncChildPtyAgentId = createId("sub");
712
+ if (deps.ptySessions && deps.nativeCreatePtySession) {
713
+ childTools.register(createPtyTool({
714
+ registry: deps.ptySessions,
715
+ agentId: syncChildPtyAgentId,
716
+ agentName,
717
+ workspaceRoot: deps.workspaceRoot,
718
+ // (5.1 codex Round-2 MUST-FIX #3) Same worktree-cwd default
719
+ // override as the background branch above.
720
+ defaultCwd: worktreeCwd ?? deps.workspaceRoot,
721
+ worktreeCwd,
722
+ createHandle: deps.nativeCreatePtySession,
723
+ }));
724
+ }
725
+ const childSession = forkedChildSession ?? SessionManager.create("subagent");
726
+ const child = new AgentEngine({
727
+ bus: deps.bus,
728
+ router: deps.router,
729
+ tools: childTools,
730
+ gate: deps.gate,
731
+ getApprovalMode: deps.getApprovalMode,
732
+ // (5.4 D12) inherit the parent's MCP mention resolver.
733
+ resolveMcpMentions: deps.resolveMcpMentions,
734
+ session: childSession,
735
+ memory: deps.memory,
736
+ workspaceRoot: deps.workspaceRoot,
737
+ maxTurns: childTurnsBudget,
738
+ suppressTerminalErrors: true,
739
+ suppressLifecycleEvents: true,
740
+ agentContext: { agentName },
741
+ systemBaseOverride: agent?.systemPrompt,
742
+ selection: effectiveSelection,
743
+ // (4.1) Worktree isolation — same wiring as the background
744
+ // branch above.
745
+ worktreeCwd,
746
+ });
747
+ const childToken = tokenFromSignal(ctx.signal);
748
+ try {
749
+ const result = await child.runTurn(childPrompt, childToken, { expandMentions: false });
750
+ const body = result.text.trim();
751
+ if (!body) {
752
+ if (worktreeHandle)
753
+ await worktreeHandle.cleanup();
754
+ return { ok: false, output: "Subagent finished without producing any text" };
755
+ }
756
+ const wrapped = await finalizeWorktree(worktreeHandle, deps.workspaceRoot, body, mcpToolsFiltered);
757
+ return { ok: true, output: wrapped };
758
+ }
759
+ catch (e) {
760
+ if (worktreeHandle)
761
+ await worktreeHandle.cleanup();
762
+ if (e instanceof AppError && e.code === ErrorCode.Cancelled)
763
+ throw e;
764
+ if (e instanceof AppError) {
765
+ return { ok: false, output: `Subagent failed (${e.code}): ${e.message}` };
766
+ }
767
+ return { ok: false, output: `Subagent failed: ${e.message || String(e)}` };
768
+ }
769
+ finally {
770
+ // (5.1) Drain every PTY session owned by the sync subagent.
771
+ // Same drain shape as the background branch — adopted
772
+ // sessions re-bound to the parent are no-ops here. Fires
773
+ // before this `execute` returns so the parent's next turn
774
+ // never sees `pty.list()` results for a defunct sync child.
775
+ await drainChildPty(syncChildPtyAgentId);
776
+ }
777
+ },
778
+ };
779
+ }
780
+ /** (4.1) MCP-bridged tools follow the `mcp__<server>__<tool>` naming
781
+ * convention established in 3.1. Source-of-truth lives in `@chances-ai/mcp`
782
+ * but matching by prefix keeps task-tool free of an mcp import. */
783
+ function isMcpToolName(name) {
784
+ return name.startsWith("mcp__");
785
+ }
786
+ /** (4.1) Read-only chances tool categories. MCP tools whose declared
787
+ * category falls outside this set are filtered out of isolated
788
+ * subagents by default; `unsafeAllowMutatingMcp: true` opts back in. */
789
+ function isReadOnlyMcpCategory(category) {
790
+ return (category === "file-read" ||
791
+ category === "memory-read" ||
792
+ category === "network-read" ||
793
+ category === "search");
794
+ }
795
+ /**
796
+ * (4.1) Decides what to do with the worktree after the child finishes
797
+ * cleanly. Codex Round-2 caught three independent retention bugs in the
798
+ * v1 implementation; folded fixes:
799
+ *
800
+ * - MUST-FIX #1: `git diff --shortstat HEAD` is empty when the child
801
+ * COMMITTED its work (HEAD moved). Solution: also probe
802
+ * `gitUnmergedCommitCount(branch ^HEAD)` — any commit on the child's
803
+ * branch that isn't reachable from HEAD counts as "has work to
804
+ * keep".
805
+ * - MUST-FIX #2: `git diff` doesn't show untracked files. A subagent
806
+ * that creates new files only would look clean. Solution: also
807
+ * probe `gitStatusEmpty` — any untracked/modified/staged file
808
+ * counts as "has work to keep".
809
+ * - MUST-FIX #4: silently mapping probe failures to "empty diff"
810
+ * drives destructive cleanup on transient flakiness. Solution: any
811
+ * probe that throws marks `probeFailed = true` and we retain the
812
+ * worktree.
813
+ *
814
+ * Net rule: a worktree is silently removed ONLY when all three probes
815
+ * succeed AND all three report no effect. Otherwise we keep it, surface
816
+ * the path + branch + diff-stat banner to the parent, and let the user
817
+ * decide. Matches claude-code's belt-and-suspenders `AgentTool.tsx`
818
+ * policy.
819
+ *
820
+ * Also folds Round-2 SHOULD-FIX #3: when MCP tools were filtered, the
821
+ * notice is appended on BOTH the retained and the clean-removed
822
+ * branches (previously only retained).
823
+ */
824
+ export async function __finalizeWorktreeForTest(handle, parentWorkspace, body, mcpToolsFiltered) {
825
+ return finalizeWorktree(handle, parentWorkspace, body, mcpToolsFiltered);
826
+ }
827
+ async function finalizeWorktree(handle, parentWorkspace, body, mcpToolsFiltered) {
828
+ if (!handle) {
829
+ return appendMcpFilterNotice(body, mcpToolsFiltered);
830
+ }
831
+ let shortstat = "";
832
+ let statusEmpty = true;
833
+ let unmergedCommits = 0;
834
+ let probeFailed = false;
835
+ try {
836
+ shortstat = await gitDiffShortstat(handle.path);
837
+ }
838
+ catch {
839
+ probeFailed = true;
840
+ }
841
+ try {
842
+ statusEmpty = await gitStatusEmpty(handle.path);
843
+ }
844
+ catch {
845
+ probeFailed = true;
846
+ }
847
+ try {
848
+ unmergedCommits = await gitUnmergedCommitCount(handle.canonicalRoot, handle.branch);
849
+ }
850
+ catch {
851
+ probeFailed = true;
852
+ }
853
+ const hasChanges = shortstat.length > 0 || !statusEmpty || unmergedCommits > 0;
854
+ if (!hasChanges && !probeFailed) {
855
+ await handle.cleanup();
856
+ return appendMcpFilterNotice(body, mcpToolsFiltered);
857
+ }
858
+ const relPath = relative(parentWorkspace, handle.path) || handle.path;
859
+ const reasons = [];
860
+ if (shortstat.length > 0)
861
+ reasons.push(shortstat);
862
+ if (!statusEmpty)
863
+ reasons.push("untracked or staged files present");
864
+ if (unmergedCommits > 0)
865
+ reasons.push(`${unmergedCommits} commit(s) on ${handle.branch} not on HEAD`);
866
+ if (probeFailed && reasons.length === 0)
867
+ reasons.push("probe error during retention check — worktree kept defensively");
868
+ const banner = [
869
+ `[worktree retained: ${relPath} on branch ${handle.branch}]`,
870
+ `[diff: ${reasons.join("; ")}]`,
871
+ mcpToolsFiltered > 0
872
+ ? `[notice: ${mcpToolsFiltered} mutating MCP tool(s) were hidden from this subagent due to active isolation]`
873
+ : null,
874
+ ]
875
+ .filter(Boolean)
876
+ .join("\n");
877
+ return `${banner}\n\n${body}`;
878
+ }
879
+ function appendMcpFilterNotice(body, n) {
880
+ if (n <= 0)
881
+ return body;
882
+ return `${body}\n\n[notice: ${n} mutating MCP tool(s) were hidden from this subagent due to active isolation]`;
883
+ }
884
+ /** Boolean arg reader. Returns true only when the value is literal `true`;
885
+ * everything else (false, missing, wrong type) returns false. We avoid
886
+ * coercion (truthy strings etc.) because schema-level safety matters here. */
887
+ function boolArg(args, key) {
888
+ const v = args[key];
889
+ return v === true;
890
+ }
891
+ /** Format an agent's tool spec into the one-line permission summary. */
892
+ function formatToolListForSummary(tools) {
893
+ if (tools === "*")
894
+ return "*";
895
+ if (tools.length === 0)
896
+ return "(none)";
897
+ if (tools.length <= 5)
898
+ return tools.join(", ");
899
+ return `${tools.slice(0, 5).join(", ")}, +${tools.length - 5} more`;
900
+ }
901
+ //# sourceMappingURL=task-tool.js.map