@pentoshi/clai 2.0.62 → 3.5.1

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 (475) hide show
  1. package/README.md +262 -368
  2. package/dist/agent/classic-renderer.js +9 -1
  3. package/dist/agent/classic-renderer.js.map +1 -1
  4. package/dist/agent/context-manager.d.ts +5 -0
  5. package/dist/agent/context-manager.js +14 -2
  6. package/dist/agent/context-manager.js.map +1 -1
  7. package/dist/agent/events.d.ts +9 -1
  8. package/dist/agent/plan-tool.js +80 -8
  9. package/dist/agent/plan-tool.js.map +1 -1
  10. package/dist/agent/runner.js +261 -122
  11. package/dist/agent/runner.js.map +1 -1
  12. package/dist/agent/tool-call-parser.d.ts +20 -0
  13. package/dist/agent/tool-call-parser.js +186 -2
  14. package/dist/agent/tool-call-parser.js.map +1 -1
  15. package/dist/app/adapters/agent-event-adapter.d.ts +29 -0
  16. package/dist/app/adapters/agent-event-adapter.js +148 -0
  17. package/dist/app/adapters/agent-event-adapter.js.map +1 -0
  18. package/dist/app/adapters/current-agent-adapter.d.ts +7 -0
  19. package/dist/app/adapters/current-agent-adapter.js +27 -0
  20. package/dist/app/adapters/current-agent-adapter.js.map +1 -0
  21. package/dist/app/adapters/current-jobs-adapter.d.ts +4 -0
  22. package/dist/app/adapters/current-jobs-adapter.js +13 -0
  23. package/dist/app/adapters/current-jobs-adapter.js.map +1 -0
  24. package/dist/app/adapters/current-store-adapter.d.ts +3 -0
  25. package/dist/app/adapters/current-store-adapter.js +22 -0
  26. package/dist/app/adapters/current-store-adapter.js.map +1 -0
  27. package/dist/app/adapters/current-terminal-adapter.d.ts +9 -0
  28. package/dist/app/adapters/current-terminal-adapter.js +31 -0
  29. package/dist/app/adapters/current-terminal-adapter.js.map +1 -0
  30. package/dist/app/adapters/current-updates-adapter.d.ts +7 -0
  31. package/dist/app/adapters/current-updates-adapter.js +31 -0
  32. package/dist/app/adapters/current-updates-adapter.js.map +1 -0
  33. package/dist/app/adapters/in-memory-clipboard-adapter.d.ts +10 -0
  34. package/dist/app/adapters/in-memory-clipboard-adapter.js +20 -0
  35. package/dist/app/adapters/in-memory-clipboard-adapter.js.map +1 -0
  36. package/dist/app/commands/command.d.ts +24 -0
  37. package/dist/app/commands/command.js +4 -0
  38. package/dist/app/commands/command.js.map +1 -0
  39. package/dist/app/commands/registry.d.ts +45 -0
  40. package/dist/app/commands/registry.js +164 -0
  41. package/dist/app/commands/registry.js.map +1 -0
  42. package/dist/app/controllers/disposable.d.ts +15 -0
  43. package/dist/app/controllers/disposable.js +38 -0
  44. package/dist/app/controllers/disposable.js.map +1 -0
  45. package/dist/app/controllers/job-controller.d.ts +22 -0
  46. package/dist/app/controllers/job-controller.js +35 -0
  47. package/dist/app/controllers/job-controller.js.map +1 -0
  48. package/dist/app/controllers/plan-controller.d.ts +31 -0
  49. package/dist/app/controllers/plan-controller.js +69 -0
  50. package/dist/app/controllers/plan-controller.js.map +1 -0
  51. package/dist/app/controllers/session-controller.d.ts +167 -0
  52. package/dist/app/controllers/session-controller.js +467 -0
  53. package/dist/app/controllers/session-controller.js.map +1 -0
  54. package/dist/app/controllers/turn-controller.d.ts +52 -0
  55. package/dist/app/controllers/turn-controller.js +118 -0
  56. package/dist/app/controllers/turn-controller.js.map +1 -0
  57. package/dist/app/events/app-event.d.ts +122 -0
  58. package/dist/app/events/app-event.js +25 -0
  59. package/dist/app/events/app-event.js.map +1 -0
  60. package/dist/app/events/event-buffer.d.ts +51 -0
  61. package/dist/app/events/event-buffer.js +109 -0
  62. package/dist/app/events/event-buffer.js.map +1 -0
  63. package/dist/app/events/sequencer.d.ts +32 -0
  64. package/dist/app/events/sequencer.js +55 -0
  65. package/dist/app/events/sequencer.js.map +1 -0
  66. package/dist/app/ports/agent-port.d.ts +29 -0
  67. package/dist/app/ports/agent-port.js +2 -0
  68. package/dist/app/ports/agent-port.js.map +1 -0
  69. package/dist/app/ports/clipboard-port.d.ts +9 -0
  70. package/dist/app/ports/clipboard-port.js +2 -0
  71. package/dist/app/ports/clipboard-port.js.map +1 -0
  72. package/dist/app/ports/confirm-port.d.ts +17 -0
  73. package/dist/app/ports/confirm-port.js +2 -0
  74. package/dist/app/ports/confirm-port.js.map +1 -0
  75. package/dist/app/ports/jobs-port.d.ts +18 -0
  76. package/dist/app/ports/jobs-port.js +2 -0
  77. package/dist/app/ports/jobs-port.js.map +1 -0
  78. package/dist/app/ports/persistence-port.d.ts +21 -0
  79. package/dist/app/ports/persistence-port.js +2 -0
  80. package/dist/app/ports/persistence-port.js.map +1 -0
  81. package/dist/app/ports/secret-port.d.ts +12 -0
  82. package/dist/app/ports/secret-port.js +2 -0
  83. package/dist/app/ports/secret-port.js.map +1 -0
  84. package/dist/app/ports/terminal-port.d.ts +18 -0
  85. package/dist/app/ports/terminal-port.js +2 -0
  86. package/dist/app/ports/terminal-port.js.map +1 -0
  87. package/dist/app/ports/updates-port.d.ts +12 -0
  88. package/dist/app/ports/updates-port.js +2 -0
  89. package/dist/app/ports/updates-port.js.map +1 -0
  90. package/dist/commands/doctor.js +19 -0
  91. package/dist/commands/doctor.js.map +1 -1
  92. package/dist/commands/update.js +1 -1
  93. package/dist/commands/update.js.map +1 -1
  94. package/dist/index.js +47 -12
  95. package/dist/index.js.map +1 -1
  96. package/dist/llm/router.js +4 -2
  97. package/dist/llm/router.js.map +1 -1
  98. package/dist/prompts/index.d.ts +1 -1
  99. package/dist/prompts/index.js +11 -8
  100. package/dist/prompts/index.js.map +1 -1
  101. package/dist/repl/slash-commands.js +1 -1
  102. package/dist/repl/slash-commands.js.map +1 -1
  103. package/dist/repl.js +35 -107
  104. package/dist/repl.js.map +1 -1
  105. package/dist/store/config.js +3 -1
  106. package/dist/store/config.js.map +1 -1
  107. package/dist/store/history.d.ts +26 -2
  108. package/dist/store/history.js +383 -72
  109. package/dist/store/history.js.map +1 -1
  110. package/dist/store/logs.js +3 -3
  111. package/dist/store/logs.js.map +1 -1
  112. package/dist/store/paths.d.ts +6 -0
  113. package/dist/store/paths.js +38 -0
  114. package/dist/store/paths.js.map +1 -0
  115. package/dist/store/plan.js +8 -5
  116. package/dist/store/plan.js.map +1 -1
  117. package/dist/tools/fs.d.ts +6 -0
  118. package/dist/tools/fs.js +56 -6
  119. package/dist/tools/fs.js.map +1 -1
  120. package/dist/tools/jobs.js +2 -2
  121. package/dist/tools/jobs.js.map +1 -1
  122. package/dist/tools/registry.js +189 -67
  123. package/dist/tools/registry.js.map +1 -1
  124. package/dist/tools/shell.js +2 -2
  125. package/dist/tools/shell.js.map +1 -1
  126. package/dist/tui/runtime.d.ts +19 -0
  127. package/dist/tui/runtime.js +92 -0
  128. package/dist/tui/runtime.js.map +1 -0
  129. package/dist/tui/state.js +22 -17
  130. package/dist/tui/state.js.map +1 -1
  131. package/dist/tui-v2/actions/action-id.d.ts +14 -0
  132. package/dist/tui-v2/actions/action-id.js +109 -0
  133. package/dist/tui-v2/actions/action-id.js.map +1 -0
  134. package/dist/tui-v2/actions/action-router.d.ts +18 -0
  135. package/dist/tui-v2/actions/action-router.js +62 -0
  136. package/dist/tui-v2/actions/action-router.js.map +1 -0
  137. package/dist/tui-v2/actions/chord-from-key.d.ts +26 -0
  138. package/dist/tui-v2/actions/chord-from-key.js +45 -0
  139. package/dist/tui-v2/actions/chord-from-key.js.map +1 -0
  140. package/dist/tui-v2/actions/keymap.d.ts +24 -0
  141. package/dist/tui-v2/actions/keymap.js +134 -0
  142. package/dist/tui-v2/actions/keymap.js.map +1 -0
  143. package/dist/tui-v2/app/App.d.ts +13 -0
  144. package/dist/tui-v2/app/App.js +332 -0
  145. package/dist/tui-v2/app/App.js.map +1 -0
  146. package/dist/tui-v2/app/command-handlers.d.ts +8 -0
  147. package/dist/tui-v2/app/command-handlers.js +66 -0
  148. package/dist/tui-v2/app/command-handlers.js.map +1 -0
  149. package/dist/tui-v2/app/commands/config-commands.d.ts +14 -0
  150. package/dist/tui-v2/app/commands/config-commands.js +161 -0
  151. package/dist/tui-v2/app/commands/config-commands.js.map +1 -0
  152. package/dist/tui-v2/app/commands/key-commands.d.ts +9 -0
  153. package/dist/tui-v2/app/commands/key-commands.js +263 -0
  154. package/dist/tui-v2/app/commands/key-commands.js.map +1 -0
  155. package/dist/tui-v2/app/commands/picker-commands.d.ts +24 -0
  156. package/dist/tui-v2/app/commands/picker-commands.js +423 -0
  157. package/dist/tui-v2/app/commands/picker-commands.js.map +1 -0
  158. package/dist/tui-v2/app/commands/session-commands.d.ts +19 -0
  159. package/dist/tui-v2/app/commands/session-commands.js +162 -0
  160. package/dist/tui-v2/app/commands/session-commands.js.map +1 -0
  161. package/dist/tui-v2/app/plan-lifecycle.d.ts +22 -0
  162. package/dist/tui-v2/app/plan-lifecycle.js +95 -0
  163. package/dist/tui-v2/app/plan-lifecycle.js.map +1 -0
  164. package/dist/tui-v2/app/providers.d.ts +17 -0
  165. package/dist/tui-v2/app/providers.js +32 -0
  166. package/dist/tui-v2/app/providers.js.map +1 -0
  167. package/dist/tui-v2/bootstrap/capabilities.d.ts +35 -0
  168. package/dist/tui-v2/bootstrap/capabilities.js +93 -0
  169. package/dist/tui-v2/bootstrap/capabilities.js.map +1 -0
  170. package/dist/tui-v2/bootstrap/composition-root.d.ts +85 -0
  171. package/dist/tui-v2/bootstrap/composition-root.js +131 -0
  172. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -0
  173. package/dist/tui-v2/bootstrap/disable-native-selection.d.ts +11 -0
  174. package/dist/tui-v2/bootstrap/disable-native-selection.js +13 -0
  175. package/dist/tui-v2/bootstrap/disable-native-selection.js.map +1 -0
  176. package/dist/tui-v2/bootstrap/lifecycle.d.ts +62 -0
  177. package/dist/tui-v2/bootstrap/lifecycle.js +123 -0
  178. package/dist/tui-v2/bootstrap/lifecycle.js.map +1 -0
  179. package/dist/tui-v2/bootstrap/osc52-clipboard.d.ts +28 -0
  180. package/dist/tui-v2/bootstrap/osc52-clipboard.js +44 -0
  181. package/dist/tui-v2/bootstrap/osc52-clipboard.js.map +1 -0
  182. package/dist/tui-v2/bootstrap/overlay-ports.d.ts +12 -0
  183. package/dist/{tui/confirm.js → tui-v2/bootstrap/overlay-ports.js} +17 -25
  184. package/dist/tui-v2/bootstrap/overlay-ports.js.map +1 -0
  185. package/dist/tui-v2/bootstrap/pager-export.d.ts +24 -0
  186. package/dist/tui-v2/bootstrap/pager-export.js +87 -0
  187. package/dist/tui-v2/bootstrap/pager-export.js.map +1 -0
  188. package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +15 -0
  189. package/dist/tui-v2/bootstrap/start-tui-v2.js +95 -0
  190. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -0
  191. package/dist/tui-v2/bootstrap/ui-selection.d.ts +30 -0
  192. package/dist/tui-v2/bootstrap/ui-selection.js +59 -0
  193. package/dist/tui-v2/bootstrap/ui-selection.js.map +1 -0
  194. package/dist/tui-v2/components/completion/completion-menu.d.ts +23 -0
  195. package/dist/tui-v2/components/completion/completion-menu.js +84 -0
  196. package/dist/tui-v2/components/completion/completion-menu.js.map +1 -0
  197. package/dist/tui-v2/components/jobs/jobs-panel.d.ts +16 -0
  198. package/dist/tui-v2/components/jobs/jobs-panel.js +92 -0
  199. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -0
  200. package/dist/tui-v2/components/modal/confirm-modal.d.ts +20 -0
  201. package/dist/tui-v2/components/modal/confirm-modal.js +110 -0
  202. package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -0
  203. package/dist/tui-v2/components/modal/prompt-actions-modal.d.ts +15 -0
  204. package/dist/tui-v2/components/modal/prompt-actions-modal.js +80 -0
  205. package/dist/tui-v2/components/modal/prompt-actions-modal.js.map +1 -0
  206. package/dist/tui-v2/components/modal/secret-modal.d.ts +18 -0
  207. package/dist/tui-v2/components/modal/secret-modal.js +50 -0
  208. package/dist/tui-v2/components/modal/secret-modal.js.map +1 -0
  209. package/dist/tui-v2/components/overlay/overlay-host.d.ts +26 -0
  210. package/dist/tui-v2/components/overlay/overlay-host.js +43 -0
  211. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -0
  212. package/dist/tui-v2/components/pager/pager.d.ts +20 -0
  213. package/dist/tui-v2/components/pager/pager.js +396 -0
  214. package/dist/tui-v2/components/pager/pager.js.map +1 -0
  215. package/dist/tui-v2/components/picker/picker.d.ts +17 -0
  216. package/dist/tui-v2/components/picker/picker.js +222 -0
  217. package/dist/tui-v2/components/picker/picker.js.map +1 -0
  218. package/dist/tui-v2/components/plan/plan-view.d.ts +20 -0
  219. package/dist/tui-v2/components/plan/plan-view.js +120 -0
  220. package/dist/tui-v2/components/plan/plan-view.js.map +1 -0
  221. package/dist/tui-v2/components/queue/queue-panel.d.ts +23 -0
  222. package/dist/tui-v2/components/queue/queue-panel.js +58 -0
  223. package/dist/tui-v2/components/queue/queue-panel.js.map +1 -0
  224. package/dist/tui-v2/components/status/status-line.d.ts +27 -0
  225. package/dist/tui-v2/components/status/status-line.js +167 -0
  226. package/dist/tui-v2/components/status/status-line.js.map +1 -0
  227. package/dist/tui-v2/components/toast/toast-host.d.ts +15 -0
  228. package/dist/tui-v2/components/toast/toast-host.js +48 -0
  229. package/dist/tui-v2/components/toast/toast-host.js.map +1 -0
  230. package/dist/tui-v2/components/transcript/assistant-message.d.ts +20 -0
  231. package/dist/tui-v2/components/transcript/assistant-message.js +41 -0
  232. package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -0
  233. package/dist/tui-v2/components/transcript/compacted-row.d.ts +19 -0
  234. package/dist/tui-v2/components/transcript/compacted-row.js +69 -0
  235. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -0
  236. package/dist/tui-v2/components/transcript/intro-card.d.ts +19 -0
  237. package/dist/tui-v2/components/transcript/intro-card.js +50 -0
  238. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -0
  239. package/dist/tui-v2/components/transcript/linkable-text.d.ts +21 -0
  240. package/dist/tui-v2/components/transcript/linkable-text.js +46 -0
  241. package/dist/tui-v2/components/transcript/linkable-text.js.map +1 -0
  242. package/dist/tui-v2/components/transcript/notice-row.d.ts +14 -0
  243. package/dist/tui-v2/components/transcript/notice-row.js +40 -0
  244. package/dist/tui-v2/components/transcript/notice-row.js.map +1 -0
  245. package/dist/tui-v2/components/transcript/search-bar.d.ts +19 -0
  246. package/dist/tui-v2/components/transcript/search-bar.js +7 -0
  247. package/dist/tui-v2/components/transcript/search-bar.js.map +1 -0
  248. package/dist/tui-v2/components/transcript/thinking-block.d.ts +22 -0
  249. package/dist/tui-v2/components/transcript/thinking-block.js +27 -0
  250. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -0
  251. package/dist/tui-v2/components/transcript/tool-card.d.ts +26 -0
  252. package/dist/tui-v2/components/transcript/tool-card.js +205 -0
  253. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -0
  254. package/dist/tui-v2/components/transcript/transcript-row.d.ts +25 -0
  255. package/dist/tui-v2/components/transcript/transcript-row.js +37 -0
  256. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -0
  257. package/dist/tui-v2/components/transcript/transcript-scroll-port.d.ts +52 -0
  258. package/dist/tui-v2/components/transcript/transcript-scroll-port.js +86 -0
  259. package/dist/tui-v2/components/transcript/transcript-scroll-port.js.map +1 -0
  260. package/dist/tui-v2/components/transcript/transcript-view.d.ts +24 -0
  261. package/dist/tui-v2/components/transcript/transcript-view.js +403 -0
  262. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -0
  263. package/dist/tui-v2/components/transcript/use-click-without-drag.d.ts +13 -0
  264. package/dist/tui-v2/components/transcript/use-click-without-drag.js +35 -0
  265. package/dist/tui-v2/components/transcript/use-click-without-drag.js.map +1 -0
  266. package/dist/tui-v2/components/transcript/use-native-selection-copy.d.ts +10 -0
  267. package/dist/tui-v2/components/transcript/use-native-selection-copy.js +37 -0
  268. package/dist/tui-v2/components/transcript/use-native-selection-copy.js.map +1 -0
  269. package/dist/tui-v2/components/transcript/use-transcript-selection.d.ts +23 -0
  270. package/dist/tui-v2/components/transcript/use-transcript-selection.js +136 -0
  271. package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -0
  272. package/dist/tui-v2/components/transcript/user-message.d.ts +10 -0
  273. package/dist/tui-v2/components/transcript/user-message.js +23 -0
  274. package/dist/tui-v2/components/transcript/user-message.js.map +1 -0
  275. package/dist/tui-v2/composer/arrow-intent.d.ts +23 -0
  276. package/dist/tui-v2/composer/arrow-intent.js +35 -0
  277. package/dist/tui-v2/composer/arrow-intent.js.map +1 -0
  278. package/dist/tui-v2/composer/completion.d.ts +37 -0
  279. package/dist/tui-v2/composer/completion.js +67 -0
  280. package/dist/tui-v2/composer/completion.js.map +1 -0
  281. package/dist/tui-v2/composer/composer-editor.d.ts +33 -0
  282. package/dist/tui-v2/composer/composer-editor.js +620 -0
  283. package/dist/tui-v2/composer/composer-editor.js.map +1 -0
  284. package/dist/tui-v2/composer/composer-height.d.ts +21 -0
  285. package/dist/tui-v2/composer/composer-height.js +29 -0
  286. package/dist/tui-v2/composer/composer-height.js.map +1 -0
  287. package/dist/tui-v2/composer/composer-meta.d.ts +9 -0
  288. package/dist/tui-v2/composer/composer-meta.js +38 -0
  289. package/dist/tui-v2/composer/composer-meta.js.map +1 -0
  290. package/dist/tui-v2/composer/history-nav.d.ts +15 -0
  291. package/dist/tui-v2/composer/history-nav.js +16 -0
  292. package/dist/tui-v2/composer/history-nav.js.map +1 -0
  293. package/dist/tui-v2/composer/newline-hint.d.ts +13 -0
  294. package/dist/tui-v2/composer/newline-hint.js +13 -0
  295. package/dist/tui-v2/composer/newline-hint.js.map +1 -0
  296. package/dist/tui-v2/composer/paste-placeholder.d.ts +35 -0
  297. package/dist/tui-v2/composer/paste-placeholder.js +55 -0
  298. package/dist/tui-v2/composer/paste-placeholder.js.map +1 -0
  299. package/dist/tui-v2/composer/prompt-history.d.ts +22 -0
  300. package/dist/tui-v2/composer/prompt-history.js +51 -0
  301. package/dist/tui-v2/composer/prompt-history.js.map +1 -0
  302. package/dist/tui-v2/composer/secret-buffer.d.ts +20 -0
  303. package/dist/tui-v2/composer/secret-buffer.js +42 -0
  304. package/dist/tui-v2/composer/secret-buffer.js.map +1 -0
  305. package/dist/tui-v2/composer/textarea-keybindings.d.ts +22 -0
  306. package/dist/tui-v2/composer/textarea-keybindings.js +27 -0
  307. package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -0
  308. package/dist/tui-v2/controllers/focus-controller.d.ts +30 -0
  309. package/dist/tui-v2/controllers/focus-controller.js +80 -0
  310. package/dist/tui-v2/controllers/focus-controller.js.map +1 -0
  311. package/dist/tui-v2/controllers/overlay-controller.d.ts +99 -0
  312. package/dist/tui-v2/controllers/overlay-controller.js +195 -0
  313. package/dist/tui-v2/controllers/overlay-controller.js.map +1 -0
  314. package/dist/tui-v2/controllers/selection-controller.d.ts +76 -0
  315. package/dist/tui-v2/controllers/selection-controller.js +238 -0
  316. package/dist/tui-v2/controllers/selection-controller.js.map +1 -0
  317. package/dist/tui-v2/controllers/toast-controller.d.ts +35 -0
  318. package/dist/tui-v2/controllers/toast-controller.js +88 -0
  319. package/dist/tui-v2/controllers/toast-controller.js.map +1 -0
  320. package/dist/tui-v2/layout/compute-layout.d.ts +59 -0
  321. package/dist/tui-v2/layout/compute-layout.js +170 -0
  322. package/dist/tui-v2/layout/compute-layout.js.map +1 -0
  323. package/dist/tui-v2/rendering/ansi-to-styled.d.ts +17 -0
  324. package/dist/tui-v2/rendering/ansi-to-styled.js +217 -0
  325. package/dist/tui-v2/rendering/ansi-to-styled.js.map +1 -0
  326. package/dist/tui-v2/rendering/batch-sections.d.ts +57 -0
  327. package/dist/tui-v2/rendering/batch-sections.js +177 -0
  328. package/dist/tui-v2/rendering/batch-sections.js.map +1 -0
  329. package/dist/tui-v2/rendering/link-detector.d.ts +16 -0
  330. package/dist/tui-v2/rendering/link-detector.js +43 -0
  331. package/dist/tui-v2/rendering/link-detector.js.map +1 -0
  332. package/dist/tui-v2/rendering/open-tool-output.d.ts +28 -0
  333. package/dist/tui-v2/rendering/open-tool-output.js +138 -0
  334. package/dist/tui-v2/rendering/open-tool-output.js.map +1 -0
  335. package/dist/tui-v2/rendering/picker-filter.d.ts +22 -0
  336. package/dist/tui-v2/rendering/picker-filter.js +68 -0
  337. package/dist/tui-v2/rendering/picker-filter.js.map +1 -0
  338. package/dist/tui-v2/rendering/plan-view.d.ts +32 -0
  339. package/dist/tui-v2/rendering/plan-view.js +196 -0
  340. package/dist/tui-v2/rendering/plan-view.js.map +1 -0
  341. package/dist/tui-v2/rendering/render-markdown-lines.d.ts +29 -0
  342. package/dist/tui-v2/rendering/render-markdown-lines.js +46 -0
  343. package/dist/tui-v2/rendering/render-markdown-lines.js.map +1 -0
  344. package/dist/tui-v2/rendering/sanitize-display.d.ts +13 -0
  345. package/dist/tui-v2/rendering/sanitize-display.js +24 -0
  346. package/dist/tui-v2/rendering/sanitize-display.js.map +1 -0
  347. package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
  348. package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
  349. package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
  350. package/dist/tui-v2/rendering/theme.d.ts +67 -0
  351. package/dist/tui-v2/rendering/theme.js +74 -0
  352. package/dist/tui-v2/rendering/theme.js.map +1 -0
  353. package/dist/tui-v2/rendering/tool-presenter.d.ts +39 -0
  354. package/dist/tui-v2/rendering/tool-presenter.js +209 -0
  355. package/dist/tui-v2/rendering/tool-presenter.js.map +1 -0
  356. package/dist/tui-v2/rendering/transcript-export.d.ts +11 -0
  357. package/dist/tui-v2/rendering/transcript-export.js +18 -0
  358. package/dist/tui-v2/rendering/transcript-export.js.map +1 -0
  359. package/dist/tui-v2/rendering/transcript-semantic.d.ts +17 -0
  360. package/dist/tui-v2/rendering/transcript-semantic.js +67 -0
  361. package/dist/tui-v2/rendering/transcript-semantic.js.map +1 -0
  362. package/dist/tui-v2/state/pager-search.d.ts +22 -0
  363. package/dist/tui-v2/state/pager-search.js +78 -0
  364. package/dist/tui-v2/state/pager-search.js.map +1 -0
  365. package/dist/tui-v2/state/semantic-document.d.ts +34 -0
  366. package/dist/tui-v2/state/semantic-document.js +201 -0
  367. package/dist/tui-v2/state/semantic-document.js.map +1 -0
  368. package/dist/tui-v2/state/transcript-compaction.d.ts +24 -0
  369. package/dist/tui-v2/state/transcript-compaction.js +79 -0
  370. package/dist/tui-v2/state/transcript-compaction.js.map +1 -0
  371. package/dist/tui-v2/state/transcript-hydrate.d.ts +27 -0
  372. package/dist/tui-v2/state/transcript-hydrate.js +276 -0
  373. package/dist/tui-v2/state/transcript-hydrate.js.map +1 -0
  374. package/dist/tui-v2/state/transcript-reducer.d.ts +10 -0
  375. package/dist/tui-v2/state/transcript-reducer.js +339 -0
  376. package/dist/tui-v2/state/transcript-reducer.js.map +1 -0
  377. package/dist/tui-v2/state/transcript-search.d.ts +17 -0
  378. package/dist/tui-v2/state/transcript-search.js +38 -0
  379. package/dist/tui-v2/state/transcript-search.js.map +1 -0
  380. package/dist/tui-v2/state/transcript-store.d.ts +32 -0
  381. package/dist/tui-v2/state/transcript-store.js +66 -0
  382. package/dist/tui-v2/state/transcript-store.js.map +1 -0
  383. package/dist/tui-v2/state/transcript-struct.d.ts +15 -0
  384. package/dist/tui-v2/state/transcript-struct.js +133 -0
  385. package/dist/tui-v2/state/transcript-struct.js.map +1 -0
  386. package/dist/tui-v2/state/transcript-types.d.ts +76 -0
  387. package/dist/tui-v2/state/transcript-types.js +65 -0
  388. package/dist/tui-v2/state/transcript-types.js.map +1 -0
  389. package/dist/tui-v2/state/use-overlay.d.ts +3 -0
  390. package/dist/tui-v2/state/use-overlay.js +6 -0
  391. package/dist/tui-v2/state/use-overlay.js.map +1 -0
  392. package/dist/tui-v2/state/use-plan.d.ts +4 -0
  393. package/dist/tui-v2/state/use-plan.js +6 -0
  394. package/dist/tui-v2/state/use-plan.js.map +1 -0
  395. package/dist/tui-v2/state/use-session-state.d.ts +2 -0
  396. package/dist/tui-v2/state/use-session-state.js +7 -0
  397. package/dist/tui-v2/state/use-session-state.js.map +1 -0
  398. package/dist/tui-v2/state/use-toast.d.ts +3 -0
  399. package/dist/tui-v2/state/use-toast.js +15 -0
  400. package/dist/tui-v2/state/use-toast.js.map +1 -0
  401. package/dist/tui-v2/state/use-transcript-store.d.ts +10 -0
  402. package/dist/tui-v2/state/use-transcript-store.js +12 -0
  403. package/dist/tui-v2/state/use-transcript-store.js.map +1 -0
  404. package/dist/ui/intro-card.js +3 -3
  405. package/dist/ui/intro-card.js.map +1 -1
  406. package/dist/ui/intro-header.d.ts +23 -0
  407. package/dist/ui/intro-header.js +232 -0
  408. package/dist/ui/intro-header.js.map +1 -0
  409. package/dist/ui/markdown.js +0 -1
  410. package/dist/ui/markdown.js.map +1 -1
  411. package/dist/ui/mentions.d.ts +9 -2
  412. package/dist/ui/mentions.js +102 -4
  413. package/dist/ui/mentions.js.map +1 -1
  414. package/dist/ui/output-pane.js +1 -1
  415. package/dist/ui/output-pane.js.map +1 -1
  416. package/dist/ui/plan-pane.d.ts +8 -0
  417. package/dist/ui/plan-pane.js +38 -0
  418. package/dist/ui/plan-pane.js.map +1 -1
  419. package/dist/ui/wordmark.d.ts +5 -0
  420. package/dist/ui/wordmark.js +6 -1
  421. package/dist/ui/wordmark.js.map +1 -1
  422. package/package.json +25 -12
  423. package/dist/tui/App.d.ts +0 -12
  424. package/dist/tui/App.js +0 -2174
  425. package/dist/tui/App.js.map +0 -1
  426. package/dist/tui/components/ConfirmModal.d.ts +0 -9
  427. package/dist/tui/components/ConfirmModal.js +0 -55
  428. package/dist/tui/components/ConfirmModal.js.map +0 -1
  429. package/dist/tui/components/JobsPanel.d.ts +0 -11
  430. package/dist/tui/components/JobsPanel.js +0 -55
  431. package/dist/tui/components/JobsPanel.js.map +0 -1
  432. package/dist/tui/components/Pager.d.ts +0 -14
  433. package/dist/tui/components/Pager.js +0 -56
  434. package/dist/tui/components/Pager.js.map +0 -1
  435. package/dist/tui/components/PickerPanel.d.ts +0 -15
  436. package/dist/tui/components/PickerPanel.js +0 -126
  437. package/dist/tui/components/PickerPanel.js.map +0 -1
  438. package/dist/tui/components/PlanSidebar.d.ts +0 -6
  439. package/dist/tui/components/PlanSidebar.js +0 -99
  440. package/dist/tui/components/PlanSidebar.js.map +0 -1
  441. package/dist/tui/components/SecretInputPanel.d.ts +0 -7
  442. package/dist/tui/components/SecretInputPanel.js +0 -38
  443. package/dist/tui/components/SecretInputPanel.js.map +0 -1
  444. package/dist/tui/confirm.d.ts +0 -16
  445. package/dist/tui/confirm.js.map +0 -1
  446. package/dist/tui/default.d.ts +0 -6
  447. package/dist/tui/default.js +0 -11
  448. package/dist/tui/default.js.map +0 -1
  449. package/dist/tui/hooks/useAgentRunner.d.ts +0 -73
  450. package/dist/tui/hooks/useAgentRunner.js +0 -277
  451. package/dist/tui/hooks/useAgentRunner.js.map +0 -1
  452. package/dist/tui/hooks/useJobs.d.ts +0 -7
  453. package/dist/tui/hooks/useJobs.js +0 -39
  454. package/dist/tui/hooks/useJobs.js.map +0 -1
  455. package/dist/tui/hooks/useSpinner.d.ts +0 -5
  456. package/dist/tui/hooks/useSpinner.js +0 -21
  457. package/dist/tui/hooks/useSpinner.js.map +0 -1
  458. package/dist/tui/hooks/useTerminalSize.d.ts +0 -9
  459. package/dist/tui/hooks/useTerminalSize.js +0 -26
  460. package/dist/tui/hooks/useTerminalSize.js.map +0 -1
  461. package/dist/tui/index.d.ts +0 -10
  462. package/dist/tui/index.js +0 -37
  463. package/dist/tui/index.js.map +0 -1
  464. package/dist/tui/key-intent.d.ts +0 -36
  465. package/dist/tui/key-intent.js +0 -54
  466. package/dist/tui/key-intent.js.map +0 -1
  467. package/dist/tui/mouse.d.ts +0 -8
  468. package/dist/tui/mouse.js +0 -34
  469. package/dist/tui/mouse.js.map +0 -1
  470. package/dist/tui/plan-sidebar-lines.d.ts +0 -2
  471. package/dist/tui/plan-sidebar-lines.js +0 -140
  472. package/dist/tui/plan-sidebar-lines.js.map +0 -1
  473. package/dist/tui/render-lines.d.ts +0 -22
  474. package/dist/tui/render-lines.js +0 -611
  475. package/dist/tui/render-lines.js.map +0 -1
@@ -11,7 +11,7 @@ import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall,
11
11
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
12
12
  import { looksInteractiveStdin } from "../tools/shell.js";
13
13
  import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
14
- import { compactMessagesWithSummary, estimateTokens, estimateMessagesTokens, COMPACTION_MEMORY_PREFIX, } from "./context-manager.js";
14
+ import { compactMessagesWithSummary, estimateTokens, estimateMessagesTokens, AUTO_COMPACT_TOKEN_BUDGET, COMPACTION_MEMORY_PREFIX, } from "./context-manager.js";
15
15
  import { auditLog } from "../store/logs.js";
16
16
  import { loadProjectContext } from "../store/project.js";
17
17
  import { loadScope, isScopeActive, targetInScope } from "../store/scope.js";
@@ -24,7 +24,7 @@ import { analyzeTask } from "./task-analyzer.js";
24
24
  import { LoopGuard } from "./loop-guard.js";
25
25
  import { loadPlan } from "../store/plan.js";
26
26
  import { pathInsideSandbox, fsWrite } from "../tools/fs.js";
27
- import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeActionNarration, looksLikePlanNarration, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, pentestWorkflowDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
27
+ import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
28
28
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
29
29
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
30
30
  import { planContextMessage, handlePlanTool, } from "./plan-tool.js";
@@ -128,7 +128,17 @@ export async function runAgentLoop(prompt, options = {}) {
128
128
  stop: () => { },
129
129
  };
130
130
  const writeStatus = (text, rendered = chalk.dim(text)) => {
131
- emit({ type: "status", text });
131
+ // Footer activity is single-line; strip classic stdout newlines/indents.
132
+ // Never surface /output path hints as activity (garbles the status bar).
133
+ let cleaned = text.replace(/\s+/g, " ").trim();
134
+ if (/\/output\b|open full output|Ctrl\+O or|\.clai\/outputs/i.test(cleaned)) {
135
+ return;
136
+ }
137
+ if (cleaned.length > 64) {
138
+ const short = cleaned.match(/^[\w./-]+/);
139
+ cleaned = short ? short[0] : cleaned.slice(0, 61) + "…";
140
+ }
141
+ emit({ type: "status", text: cleaned || "working" });
132
142
  if (writesDirectly)
133
143
  process.stdout.write(rendered);
134
144
  };
@@ -154,9 +164,14 @@ export async function runAgentLoop(prompt, options = {}) {
154
164
  if (writesDirectly)
155
165
  process.stdout.write(`${renderThinkingSummary(content)}\n`);
156
166
  };
157
- const writeToolOutput = (id, chunk, rendered) => {
158
- emit({ type: "tool-output", id, chunk });
159
- if (writesDirectly)
167
+ const writeToolOutput = (id, chunk, rendered, options) => {
168
+ emit({
169
+ type: "tool-output",
170
+ id,
171
+ chunk,
172
+ ...(options?.replace ? { replace: true } : {}),
173
+ });
174
+ if (writesDirectly && rendered)
160
175
  process.stdout.write(rendered);
161
176
  };
162
177
  const writeToolCall = (id, call, rendered) => {
@@ -257,8 +272,13 @@ export async function runAgentLoop(prompt, options = {}) {
257
272
  // brand-new plan (the exact failure where "what do u know till now"
258
273
  // triggered explore→plan and created an unrelated "Enhance clai" plan).
259
274
  const informationalQuery = looksLikeInformationalQuery(prompt);
275
+ // Greetings / thanks / short acks must never force tools, plans, or
276
+ // freshness retries — a false "act don't narrate" path burned tokens on
277
+ // web.search recovery loops after a simple "hi".
278
+ const idleOrSocialPrompt = looksLikeIdleOrSocialPrompt(prompt);
260
279
  const freshWebSearchRequired = !buildLikeTurn &&
261
280
  !pentestLikeTurn &&
281
+ !idleOrSocialPrompt &&
262
282
  toolNames.includes("web.search") &&
263
283
  requiresFreshWebSearch(prompt);
264
284
  let provider = options.provider ?? config.defaultProvider;
@@ -307,7 +327,10 @@ export async function runAgentLoop(prompt, options = {}) {
307
327
  // what's already there, create a comprehensive multi-task plan, then
308
328
  // implement task by task until the goal is met. This mirrors how a careful
309
329
  // coding agent (Claude Code) operates.
310
- if (buildLikeTurn && !activePlan && !informationalQuery) {
330
+ if (buildLikeTurn &&
331
+ !activePlan &&
332
+ !informationalQuery &&
333
+ !idleOrSocialPrompt) {
311
334
  systemSections.push(buildWorkflowDirective());
312
335
  }
313
336
  // Pentest / security engagements need a different shape than a coding
@@ -316,9 +339,22 @@ export async function runAgentLoop(prompt, options = {}) {
316
339
  // directive is only injected before a plan exists; once a plan is in
317
340
  // place (or being refined), the ACTIVE PLAN block already carries the
318
341
  // current task state and recon-vs-active-tool guidance.
319
- if (pentestLikeTurn && !activePlan && !informationalQuery) {
342
+ if (pentestLikeTurn &&
343
+ !activePlan &&
344
+ !informationalQuery &&
345
+ !idleOrSocialPrompt) {
320
346
  systemSections.push(pentestWorkflowDirective());
321
347
  }
348
+ // Any remote/security engagement (even with an existing or completed plan)
349
+ // must never fall into the coding "start localhost dev server" habit.
350
+ const pentestSession = pentestLikeTurn ||
351
+ activePlan?.kind === "pentest" ||
352
+ (activePlan?.kind !== "coding" &&
353
+ Boolean(activePlan?.goal &&
354
+ /pentest|vulnerab|recon|security assess|attack surface|red team/i.test(activePlan.goal)));
355
+ if (pentestSession && !idleOrSocialPrompt) {
356
+ systemSections.push(pentestNoLocalServerDirective());
357
+ }
322
358
  const renderedSystemPrompt = systemSections.join("\n\n");
323
359
  // Reserve most of a constrained model's input budget for the user message,
324
360
  // recent conversation, tool results, and provider framing. Dynamic project
@@ -711,6 +747,9 @@ export async function runAgentLoop(prompt, options = {}) {
711
747
  }
712
748
  parentSignal.throwIfAborted();
713
749
  options.onToolStart?.(call);
750
+ // Card was "queued" since writeToolCall; flip to running only when work starts.
751
+ emit({ type: "tool-start", id: toolEventId });
752
+ writeStatus(call.name, chalk.dim(` → ${call.name}\n`));
714
753
  const interactiveCommand = (call.name === "shell.exec" &&
715
754
  typeof call.args.command === "string" &&
716
755
  looksInteractiveStdin(call.args.command)) ||
@@ -724,38 +763,20 @@ export async function runAgentLoop(prompt, options = {}) {
724
763
  parentSignal.addEventListener("abort", onParentAbort);
725
764
  let result;
726
765
  let liveBytes = 0;
727
- const liveCap = 16_000;
728
- let liveTruncatedNotified = false;
729
- let lastProgressAt = 0;
730
766
  const shouldDimLive = !interactiveCommand;
731
- const writeToolInfo = (text) => {
732
- writeToolOutput(toolEventId, `${text}\n`, chalk.dim(` ${text}\n`));
733
- };
767
+ // Stream every live byte — never drop mid-run. After the tool finishes we
768
+ // still replace the spool with the authoritative full `result.output`.
734
769
  const printLive = (chunk) => {
735
770
  if (call.name === "fs.read" ||
736
771
  call.name === "fs.list" ||
737
772
  call.name === "fs.search")
738
773
  return;
739
- if (liveBytes >= liveCap) {
740
- if (!liveTruncatedNotified) {
741
- liveTruncatedNotified = true;
742
- writeToolInfo("... live preview truncated, full output saved");
743
- writeToolInfo("(tool still running — ESC or Ctrl+C to abort)");
744
- lastProgressAt = Date.now();
745
- }
746
- const now = Date.now();
747
- if (now - lastProgressAt > 5_000) {
748
- lastProgressAt = now;
749
- writeToolOutput(toolEventId, ".", chalk.dim("."));
750
- }
774
+ if (!chunk)
751
775
  return;
752
- }
753
- const remaining = liveCap - liveBytes;
754
- const slice = chunk.length > remaining ? chunk.slice(0, remaining) : chunk;
755
- liveBytes += slice.length;
756
- const indented = slice.replace(/\r/g, "").replace(/\n(?!$)/g, "\n ");
776
+ liveBytes += chunk.length;
777
+ const indented = chunk.replace(/\r/g, "").replace(/\n(?!$)/g, "\n ");
757
778
  const body = indented.startsWith("\n") ? indented : ` ${indented}`;
758
- writeToolOutput(toolEventId, slice, shouldDimLive ? chalk.dim(body) : body);
779
+ writeToolOutput(toolEventId, chunk, shouldDimLive ? chalk.dim(body) : body);
759
780
  };
760
781
  const jobId = randomUUID().slice(0, 8);
761
782
  const backgroundJob = {
@@ -798,7 +819,7 @@ export async function runAgentLoop(prompt, options = {}) {
798
819
  confirmed: true,
799
820
  userPrompt: prompt,
800
821
  });
801
- if (liveBytes > 0 || liveTruncatedNotified) {
822
+ if (liveBytes > 0) {
802
823
  writeToolOutput(toolEventId, "\n", "\n");
803
824
  }
804
825
  jobManager.updateJobStatus(jobId, result.ok ? "exited" : "failed", result.exitCode);
@@ -839,11 +860,10 @@ export async function runAgentLoop(prompt, options = {}) {
839
860
  parentSignal.removeEventListener("abort", onParentAbort);
840
861
  }
841
862
  const output = result.output.trim();
842
- const displayMax = 6_000;
863
+ // Always keep a full on-disk copy of tool output (any size) so the
864
+ // pager never depends on a truncated in-memory preview.
843
865
  const savedOutputPath = result.outputPath ??
844
- (output.length > displayMax
845
- ? await saveToolOutput(call, output)
846
- : undefined);
866
+ (output ? await saveToolOutput(call, output) : undefined);
847
867
  const resultWithArtifact = {
848
868
  ...result,
849
869
  outputPath: savedOutputPath,
@@ -878,20 +898,30 @@ export async function runAgentLoop(prompt, options = {}) {
878
898
  }
879
899
  }
880
900
  const statusIcon = result.ok ? chalk.green(" ✓") : chalk.red(" ✗");
881
- writeToolOutput(toolEventId, result.ok ? "ok\n" : "failed\n", statusIcon + "\n");
901
+ // Classic stdout gets a short status glyph; the event stream / spool
902
+ // must NOT be polluted with "ok"/"failed" (the card already shows status).
903
+ if (writesDirectly) {
904
+ process.stdout.write(statusIcon + "\n");
905
+ }
882
906
  if (output) {
883
- const displaySummary = summarizeOutput(output, displayMax);
884
- const displayText = displaySummary.truncated
885
- ? `${displaySummary.text}${savedOutputPath ? chalk.dim(`\n ... full output saved to ${savedOutputPath}`) : chalk.dim("\n ... output truncated")}`
886
- : displaySummary.text;
887
- if (liveBytes > 0) {
888
- if (savedOutputPath) {
889
- writeToolInfo(`full output saved to ${savedOutputPath}`);
890
- }
907
+ // Authoritative FULL body — replace any live stream so the pager
908
+ // never shows a truncated mid-run preview. Never cap for the UI.
909
+ const fullChunk = output.endsWith("\n") ? output : `${output}\n`;
910
+ if (!writesDirectly) {
911
+ writeToolOutput(toolEventId, fullChunk, "", { replace: true });
891
912
  }
892
913
  else {
914
+ // Classic stdout: short preview only; full text is on disk.
915
+ const displayMax = 6_000;
916
+ const displaySummary = summarizeOutput(output, displayMax);
917
+ const displayText = displaySummary.truncated
918
+ ? `${displaySummary.text}${savedOutputPath ? chalk.dim(`\n ... full output saved to ${savedOutputPath}`) : ""}`
919
+ : displaySummary.text;
893
920
  const renderedOutput = indentAndWrapText(displayText);
894
- writeToolOutput(toolEventId, displayText, styleToolChatter(call, renderedOutput) + "\n");
921
+ process.stdout.write(styleToolChatter(call, renderedOutput) + "\n");
922
+ if (savedOutputPath && displaySummary.truncated) {
923
+ process.stdout.write(chalk.dim(` full output saved to ${savedOutputPath}\n`));
924
+ }
895
925
  }
896
926
  }
897
927
  if (output) {
@@ -901,9 +931,16 @@ export async function runAgentLoop(prompt, options = {}) {
901
931
  artifactPath: savedOutputPath,
902
932
  summary: contextOutput,
903
933
  });
904
- if (savedOutputPath) {
934
+ // Classic REPL only: short path notice. Never push long
935
+ // "Ctrl+O or /output last … (path)" strings into the TUI status
936
+ // footer — they collide with activity and garble the chrome.
937
+ if (writesDirectly && savedOutputPath) {
938
+ const short = chalk.dim(` saved ${savedOutputPath}\n`);
939
+ process.stdout.write(short);
940
+ }
941
+ else if (writesDirectly) {
905
942
  const viewportHint = `${formatViewportHint(viewport)}\n`;
906
- writeStatus(viewportHint, viewportHint);
943
+ process.stdout.write(viewportHint);
907
944
  }
908
945
  }
909
946
  return { ok: result.ok, call, result, contextOutput };
@@ -917,7 +954,6 @@ export async function runAgentLoop(prompt, options = {}) {
917
954
  // what is done, and what remains. The estimate is chars/4; the budget is
918
955
  // deliberately conservative so we compact a little early rather than hit a
919
956
  // provider context-window error mid-task.
920
- const AUTO_COMPACT_TOKEN_BUDGET = 150_000;
921
957
  const AUTO_COMPACT_KEEP_RECENT = 6;
922
958
  let lastCompactionMsgCount = 0;
923
959
  const summarizeForCompaction = async (summaryPrompt) => {
@@ -953,6 +989,8 @@ export async function runAgentLoop(prompt, options = {}) {
953
989
  return;
954
990
  messages.splice(0, messages.length, ...result.messages);
955
991
  loopGuard.resetReadOnly();
992
+ // Token stats BEFORE plan re-injection so the reduction is accurate.
993
+ const compactedTokens = estimateMessagesTokens(messages);
956
994
  // Re-inject the live plan so the model keeps full plan awareness even
957
995
  // after older turns (which carried the plan context) were summarized.
958
996
  const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
@@ -963,13 +1001,11 @@ export async function runAgentLoop(prompt, options = {}) {
963
1001
  });
964
1002
  }
965
1003
  lastCompactionMsgCount = messages.length;
966
- // Report the compacted token count BEFORE plan re-injection so the
967
- // compaction stats accurately show the reduction. Then report the
968
- // final count (which is what the model actually receives) separately.
969
- const compactedTokens = estimateMessagesTokens(messages);
1004
+ // Final count the model actually receives (may include re-injected plan).
1005
+ const afterTokens = estimateMessagesTokens(messages);
970
1006
  await auditLog("agent.compact", {
971
1007
  newLength: messages.length,
972
- estimatedTokens: compactedTokens,
1008
+ estimatedTokens: afterTokens,
973
1009
  reason,
974
1010
  });
975
1011
  // Extract the inserted compaction memory so we can surface the
@@ -981,12 +1017,12 @@ export async function runAgentLoop(prompt, options = {}) {
981
1017
  const summaryText = insertedSummary.startsWith(`${COMPACTION_MEMORY_PREFIX}\n\n`)
982
1018
  ? insertedText(insertedSummary, `${COMPACTION_MEMORY_PREFIX}\n\n`)
983
1019
  : insertedText(insertedSummary, COMPACTION_MEMORY_PREFIX);
984
- const afterTokens = estimateMessagesTokens(messages);
985
- writeCompacted(summaryText, beforeTokens, afterTokens);
1020
+ // Card shows pre/post of the summarization; plan re-injection is noted.
1021
+ writeCompacted(summaryText, beforeTokens, compactedTokens);
986
1022
  const planNote = afterTokens > compactedTokens
987
1023
  ? ` (compacted to ~${compactedTokens.toLocaleString()}, +plan → ~${afterTokens.toLocaleString()})`
988
1024
  : "";
989
- writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens${planNote})`, chalk.dim(` ℹ context auto-compacted (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens${planNote})\n`));
1025
+ writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${compactedTokens.toLocaleString()} tokens${planNote})`, chalk.dim(` ℹ context auto-compacted (~${beforeTokens.toLocaleString()} → ~${compactedTokens.toLocaleString()} tokens${planNote})\n`));
990
1026
  }
991
1027
  catch (error) {
992
1028
  if (error instanceof Error &&
@@ -1111,10 +1147,11 @@ export async function runAgentLoop(prompt, options = {}) {
1111
1147
  completion = await streamWithProvider({
1112
1148
  provider,
1113
1149
  model,
1114
- // The selected model is the first choice, but an agent must not
1115
- // end a turn with no answer if it only emits reasoning. Honor
1116
- // the user's providerFallback setting for that recovery path.
1117
- allowModelFallback: true,
1150
+ // Stay on the user's selected provider/model. Cross-provider
1151
+ // hops (e.g. bynara gemini on a 429) only happen when the
1152
+ // user has explicitly enabled `providerFallback` AND this is
1153
+ // the provider's default model. Never force fallback mid-turn.
1154
+ allowModelFallback: false,
1118
1155
  messages,
1119
1156
  // MiniMax M3 degenerates at the generic agent temperature. The
1120
1157
  // HTTP layer also applies its `top_p` override for both the
@@ -1379,15 +1416,19 @@ export async function runAgentLoop(prompt, options = {}) {
1379
1416
  const lineCount = salvaged.content.split("\n").length;
1380
1417
  writeNotice("info", `tool call was truncated — salvaged ${lineCount} lines and wrote to ${salvaged.path}`, chalk.cyan(` ℹ tool call was truncated — salvaged ${lineCount} lines to ${salvaged.path}\n`));
1381
1418
  pushAssistantHistory(stripThinking(assistantText.visible).visible);
1419
+ const priorBytes = Buffer.byteLength(salvaged.content, "utf8");
1382
1420
  messages.push({
1383
1421
  role: "user",
1384
- content: `Your fs.write tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines to ${salvaged.path}. ` +
1385
- `The file now exists with content up to: "${salvaged.lastLine}"\n\n` +
1386
- `CONTINUE writing the rest of the content using fs.append:\n` +
1387
- '```tool\n{"name":"fs.append","args":{"path":"' + salvaged.path + '","content":"...remaining content..."}}\n```\n' +
1388
- `Write the NEXT section of content starting from where it was cut off. ` +
1389
- `Keep each fs.append call to ~100 lines max so it fits in the output window. ` +
1390
- `Use multiple fs.append calls if needed. Do NOT re-write content that was already saved.`,
1422
+ content: `Your fs.write tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines (${priorBytes} bytes) to ${salvaged.path}. ` +
1423
+ `The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
1424
+ `CONTINUE with ONE large fs.append of the remaining content (prefer hundreds of lines per call — do NOT use tiny ~100-line chunks):\n` +
1425
+ '```tool\n{"name":"fs.append","args":{"path":' +
1426
+ JSON.stringify(salvaged.path) +
1427
+ ',"expectedPriorBytes":' +
1428
+ priorBytes +
1429
+ ',"content":"...ONLY the remaining content not already on disk..."}}\n```\n' +
1430
+ `expectedPriorBytes must match the receipt so append cannot double-write. ` +
1431
+ `Do NOT re-read the full file; do NOT re-send content already saved.`,
1391
1432
  });
1392
1433
  continue;
1393
1434
  }
@@ -1402,12 +1443,12 @@ export async function runAgentLoop(prompt, options = {}) {
1402
1443
  messages.push({
1403
1444
  role: "user",
1404
1445
  content: "Your previous tool call was cut off before it finished — the JSON was incomplete, so NOTHING ran. " +
1405
- "Your output token limit is ~32k tokens. For LARGE files (reports, docs, long code), you MUST write in chunks:\n" +
1406
- "1. Use fs.write to create the file with the FIRST ~100 lines\n" +
1407
- "2. Use fs.append to add the NEXT ~100 lines\n" +
1408
- "3. Repeat fs.append for each remaining section\n" +
1409
- "Keep your reasoning SHORT emit the ```tool block as early as possible to maximize content space. " +
1410
- "Do NOT try to write the entire file in one call. Do NOT claim any file was written until a tool call actually succeeds.",
1446
+ "Prefer ONE complete fs.write when it fits (~32k output tokens is a lot of file content if reasoning stays short). " +
1447
+ "If the file is too large for one call:\n" +
1448
+ "1. fs.write the first large section (as much as fits — hundreds+ of lines)\n" +
1449
+ "2. fs.append the rest with expectedPriorBytes from the write receipt\n" +
1450
+ "3. Repeat append only if still incomplete large chunks, not ~100-line drips\n" +
1451
+ "Keep reasoning SHORT emit the ```tool block early. Do NOT claim a file was written until a tool call succeeds.",
1411
1452
  });
1412
1453
  continue;
1413
1454
  }
@@ -1442,7 +1483,7 @@ export async function runAgentLoop(prompt, options = {}) {
1442
1483
  content: `The system extracted and wrote ${lineCount} lines to ${salvaged.path} from your malformed tool call. ` +
1443
1484
  `The file content ends at: "${salvaged.lastLine}"\n\n` +
1444
1485
  `If the file is complete, proceed with the next step. ` +
1445
- `If more content is needed, use fs.append to add the remaining sections (~100 lines per call).`,
1486
+ `If more content is needed, use one large fs.append with expectedPriorBytes from the write receipt (not tiny chunks).`,
1446
1487
  });
1447
1488
  continue;
1448
1489
  }
@@ -1460,10 +1501,8 @@ export async function runAgentLoop(prompt, options = {}) {
1460
1501
  content: "Your previous message contained a ```tool block, but its JSON was INVALID, so NOTHING ran. " +
1461
1502
  "Common causes: unescaped newlines or quotes inside a string value, an extra or missing `}` / `]`, or content too large for the output window. " +
1462
1503
  'Re-emit ONE valid ```tool block of the exact form {"name":"<tool>","args":{...}} with balanced braces. ' +
1463
- "IMPORTANT: For large file content (reports, docs), write in chunks:\n" +
1464
- "1. fs.write with the FIRST ~100 lines only\n" +
1465
- "2. fs.append for each subsequent ~100-line section\n" +
1466
- "Keep reasoning SHORT to maximize output space for the tool call JSON. " +
1504
+ "IMPORTANT: Prefer ONE complete fs.write when it fits. Keep reasoning SHORT. " +
1505
+ "Only if the output window cuts you off, continue with large fs.append chunks + expectedPriorBytes. " +
1467
1506
  "Do NOT claim any file was written until a tool call actually succeeds.",
1468
1507
  });
1469
1508
  continue;
@@ -1481,6 +1520,7 @@ export async function runAgentLoop(prompt, options = {}) {
1481
1520
  // final answer ends the turn with nothing done and no real plan saved.
1482
1521
  // Nudge it to emit a real tool call, with a concrete example.
1483
1522
  const narratedAction = looksLikeActionNarration(cleaned);
1523
+ const narratedWebAction = looksLikeWebActionNarration(cleaned);
1484
1524
  // `activePlan` is the snapshot loaded at the start of this turn.
1485
1525
  // A final task.update mutates and persists a separate plan instance,
1486
1526
  // so re-read it here before deciding whether a final response should
@@ -1493,15 +1533,24 @@ export async function runAgentLoop(prompt, options = {}) {
1493
1533
  const completedPlanDuringThisTurn = activePlan?.status !== "completed" &&
1494
1534
  planStatusAtCompletion === "completed";
1495
1535
  const planHasOpenWorkNow = planHasOpenWork(planStatusAtCompletion);
1536
+ // User-driven reasons to require tools this turn (build/pentest/
1537
+ // approved plan / freshness). Idle greetings never qualify.
1538
+ const userExpectsWork = freshWebSearchRequired ||
1539
+ (planHasOpenWorkNow && session.planApproved.value) ||
1540
+ (!informationalQuery &&
1541
+ !idleOrSocialPrompt &&
1542
+ (buildLikeTurn || pentestLikeTurn));
1496
1543
  // History-inherited build/pentest intent only forces action when
1497
- // THIS prompt is not itself a plain question. Once this turn has
1498
- // completed its plan, however, its next visible response is the
1499
- // final answer not narration to recover into more work.
1544
+ // THIS prompt is not itself a plain question/idle turn. Narration
1545
+ // stalls ("I'll list the files") still force a tool when the user
1546
+ // is not asking an informational question but capability menus
1547
+ // and greetings are filtered out by looksLikeActionNarration /
1548
+ // idleOrSocialPrompt so they cannot burn recovery turns.
1500
1549
  const wantsAction = !completedPlanDuringThisTurn &&
1501
- (narratedAction ||
1502
- freshWebSearchRequired ||
1503
- (planHasOpenWorkNow && session.planApproved.value) ||
1504
- (!informationalQuery && (buildLikeTurn || pentestLikeTurn)));
1550
+ !idleOrSocialPrompt &&
1551
+ (userExpectsWork ||
1552
+ (narratedAction && !informationalQuery) ||
1553
+ (narratedWebAction && !informationalQuery));
1505
1554
  const planNarrated = (buildLikeTurn || pentestLikeTurn) &&
1506
1555
  !activePlan &&
1507
1556
  looksLikePlanNarration(cleaned);
@@ -1528,14 +1577,20 @@ export async function runAgentLoop(prompt, options = {}) {
1528
1577
  "Every turn MUST contain a ```tool block until the task is done.";
1529
1578
  writeNotice("warn", "described a security/pentest action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described a security/pentest action but emitted no tool call — nudging it to run one\n"));
1530
1579
  }
1531
- else if (!buildLikeTurn) {
1580
+ else if (freshWebSearchRequired || narratedWebAction) {
1581
+ // Web-specific recovery ONLY when the user asked for current
1582
+ // info or the model explicitly claimed a fetch/search step.
1583
+ // Previously every non-build stall used this path, so a "hi"
1584
+ // greeting that said "I'll start executing" was forced into
1585
+ // pointless web.search recovery loops.
1532
1586
  nudge =
1533
1587
  "You wrote that you would fetch/search/read something but emitted NO ```tool block, so NOTHING ran. Do NOT narrate the next browsing step — DO it. Emit exactly one valid ```tool block now. If you know the exact page, use:\n" +
1534
1588
  '```tool\n{"name":"web.fetch","args":{"url":"https://example.com/page","responseMode":"readable"}}\n```\n' +
1535
1589
  "If you do not know the exact page URL, use web.search first. After the tool output, answer from the fetched page content.";
1536
1590
  writeNotice("warn", "described a web action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described a web action but emitted no tool call — nudging it to run one\n"));
1537
1591
  }
1538
- else if (planNarrated || productiveSteps > 0) {
1592
+ else if (buildLikeTurn &&
1593
+ (planNarrated || productiveSteps > 0)) {
1539
1594
  const kind = pentestLikeTurn ? "pentest" : "coding";
1540
1595
  nudge =
1541
1596
  "You wrote the plan as PROSE but did NOT call plan.create, so no plan was saved and the user cannot /implement it. Emit it as a real tool call NOW — exactly one ```tool block:\n" +
@@ -1543,17 +1598,17 @@ export async function runAgentLoop(prompt, options = {}) {
1543
1598
  "Do not describe the plan again in prose — just emit the plan.create tool block.";
1544
1599
  writeNotice("warn", "plan was written as text, not created — nudging it to call plan.create", chalk.yellow(" ⚠ plan was written as text, not created — nudging it to call plan.create\n"));
1545
1600
  }
1601
+ else if (buildLikeTurn) {
1602
+ nudge =
1603
+ "You described what you will do but emitted NO ```tool block, so NOTHING actually happened — narration is not action. Emit a real tool call NOW. For this build task, explore first like this:\n" +
1604
+ '```tool\n{"name":"fs.list","args":{"path":"."}}\n```\n' +
1605
+ "Then read key files, and once you understand the directory, call plan.create. Every turn MUST contain a ```tool block until the task is done.";
1606
+ writeNotice("warn", "described an action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described an action but emitted no tool call — nudging it to run one\n"));
1607
+ }
1546
1608
  else {
1547
- if (pentestLikeTurn) {
1548
- nudge =
1549
- "You described what you will do but emitted NO ```tool block, so NOTHING actually happened — narration is not action. Emit a real tool call NOW. For this pentest task, explore/recon first, then call plan.create. Every turn MUST contain a ```tool block until the task is done.";
1550
- }
1551
- else {
1552
- nudge =
1553
- "You described what you will do but emitted NO ```tool block, so NOTHING actually happened — narration is not action. Emit a real tool call NOW. For this build task, explore first like this:\n" +
1554
- '```tool\n{"name":"fs.list","args":{"path":"."}}\n```\n' +
1555
- "Then read key files, and once you understand the directory, call plan.create. Every turn MUST contain a ```tool block until the task is done.";
1556
- }
1609
+ // Generic non-build, non-web stall (e.g. "I'll list the files").
1610
+ nudge =
1611
+ "You described what you will do but emitted NO ```tool block, so NOTHING actually happened — narration is not action. Emit a real tool call NOW for the step you just described. Every turn that claims an action MUST contain a ```tool block.";
1557
1612
  writeNotice("warn", "described an action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described an action but emitted no tool call — nudging it to run one\n"));
1558
1613
  }
1559
1614
  pushAssistantHistory(assistantText.visible);
@@ -1574,29 +1629,28 @@ export async function runAgentLoop(prompt, options = {}) {
1574
1629
  continue;
1575
1630
  }
1576
1631
  // A passing build is not evidence that an app is serving requests.
1577
- // On completed build plans, require start → logs → HTTP verification
1578
- // before accepting the model's final claim.
1632
+ // On completed CODING build plans only, require start → logs → HTTP
1633
+ // verification. NEVER apply this to pentest/remote engagements — that
1634
+ // forced "npm run dev on the clai repo" after a finished web assessment.
1579
1635
  if (buildLike &&
1636
+ !pentestLike &&
1637
+ !pentestSession &&
1580
1638
  session.planApproved.value &&
1581
1639
  (!sawServerStart || !sawServerTail || !sawLocalHttpProbe) &&
1582
1640
  runtimeVerificationRetries < 2) {
1583
1641
  const runtimePlan = await loadPlan(session.sessionId).catch(() => undefined);
1584
- const tasksFinished = Boolean(runtimePlan &&
1642
+ const codingPlanFinished = Boolean(runtimePlan &&
1643
+ runtimePlan.kind !== "pentest" &&
1585
1644
  runtimePlan.tasks.length > 0 &&
1586
1645
  runtimePlan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
1587
- if (tasksFinished) {
1646
+ if (codingPlanFinished) {
1588
1647
  runtimeVerificationRetries += 1;
1589
1648
  pushAssistantHistory(assistantText.visible);
1590
- const missing = [
1591
- !sawServerStart ? "shell.start" : "",
1592
- !sawServerTail ? "shell.tail" : "",
1593
- !sawLocalHttpProbe
1594
- ? "a successful bounded localhost HTTP probe"
1595
- : "",
1596
- ].filter(Boolean);
1597
1649
  messages.push({
1598
1650
  role: "user",
1599
- content: "Run the missing checks now. Keep the dev server/job running in the background so that the user can interact with the live application, and print the localhost link. Report whether it remains running truthfully.",
1651
+ content: "This is a CODING project only: run the missing local checks (shell.start + shell.tail + localhost HTTP probe). " +
1652
+ "Keep the dev server running and print the localhost link. " +
1653
+ "If this was a remote pentest, ignore this and finalize the report with no local server.",
1600
1654
  });
1601
1655
  continue;
1602
1656
  }
@@ -1615,20 +1669,26 @@ export async function runAgentLoop(prompt, options = {}) {
1615
1669
  const next = unfinished[0];
1616
1670
  writeNotice("warn", `${unfinished.length} plan task(s) still unfinished — not accepting a "done" claim; resuming execution`, chalk.yellow(` ⚠ ${unfinished.length} plan task(s) still unfinished — not accepting a "done" claim; resuming execution\n`));
1617
1671
  pushAssistantHistory(assistantText.visible);
1672
+ const isPentestPlan = livePlan.kind === "pentest" || pentestSession;
1618
1673
  let instruction = `Resume now with the NEXT task ${next.id} ("${next.title}"): `;
1619
- if (next.state === "pending") {
1620
- instruction += `call task.update {taskId:"${next.id}", state:"in_progress"}, then do the real work with a tool call (fs.writeMany / shell.exec / shell.start), VERIFY it, and mark it done. `;
1674
+ if (isPentestPlan) {
1675
+ instruction +=
1676
+ `call task.update {taskId:"${next.id}", state:"in_progress"}, then do the recon/testing work ` +
1677
+ `(dns/http/net.scan/http.fetch/tool.batch — NOT a local dev server), VERIFY with real tool output, ` +
1678
+ `then task.update done. Do NOT shell.start / npm run dev / explore the clai workspace. `;
1679
+ }
1680
+ else if (next.state === "pending") {
1681
+ instruction += `call task.update {taskId:"${next.id}", state:"in_progress"}, then do the real work with a tool call (fs.writeMany / shell.exec / shell.start when building a local app), VERIFY it, and mark it done. `;
1621
1682
  }
1622
1683
  else {
1623
- instruction += `do the real work with a tool call (fs.writeMany / shell.exec / shell.start) to complete it, VERIFY it, and mark it done (call task.update {taskId:"${next.id}", state:"done"}). `;
1684
+ instruction += `do the real work with a tool call (fs.writeMany / shell.exec / shell.start when building a local app) to complete it, VERIFY it, and mark it done (call task.update {taskId:"${next.id}", state:"done"}). `;
1624
1685
  }
1625
1686
  instruction += `Continue task by task until EVERY task is actually finished.`;
1626
1687
  messages.push({
1627
1688
  role: "user",
1628
1689
  content: `You have NOT finished the approved plan: ${unfinished.length} task(s) remain ` +
1629
1690
  `(${unfinished.map((t) => `[${t.id}] ${t.title}`).join("; ")}). ` +
1630
- `Do NOT claim the work is complete, that files were created, or that a server is running ` +
1631
- `unless a tool call actually succeeded and you saw the output. ` +
1691
+ `Do NOT claim the work is complete unless a tool call actually succeeded and you saw the output. ` +
1632
1692
  instruction,
1633
1693
  });
1634
1694
  continue;
@@ -1740,7 +1800,12 @@ export async function runAgentLoop(prompt, options = {}) {
1740
1800
  `${omittedCallCount} were not run. After reviewing results, issue a small, prioritized next batch.`,
1741
1801
  });
1742
1802
  }
1743
- // Now emit only the calls that will actually execute, after thinking
1803
+ // Notice BEFORE tool cards so the transcript reads:
1804
+ // thinking → response → "N tool calls…" → tool cards (not tools then info).
1805
+ if (allCalls.length > 1) {
1806
+ writeNotice("info", `${allCalls.length} tool calls in this message — running scoped (independent read-only lookups in parallel, everything else in order)`, chalk.dim(` ℹ ${allCalls.length} tool calls — read-only lookups in parallel, the rest in order\n`));
1807
+ }
1808
+ // Emit only the calls that will actually execute, after thinking
1744
1809
  // + assistant text so transcript order remains correct.
1745
1810
  for (const deferred of activeDeferredToolCalls.slice(0, allCalls.length)) {
1746
1811
  writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
@@ -1750,9 +1815,6 @@ export async function runAgentLoop(prompt, options = {}) {
1750
1815
  .map((c) => `\`\`\`tool\n${JSON.stringify(c)}\n\`\`\``)
1751
1816
  .join("\n\n");
1752
1817
  pushAssistantHistory(standardizedContent);
1753
- if (allCalls.length > 1) {
1754
- writeNotice("info", `${allCalls.length} tool calls in this message — running scoped (independent read-only lookups in parallel, everything else in order)`, chalk.dim(` ℹ ${allCalls.length} tool calls — read-only lookups in parallel, the rest in order\n`));
1755
- }
1756
1818
  // Scoped-parallel batch execution
1757
1819
  // The model may emit several calls in one message. We partition them,
1758
1820
  // IN DOCUMENT ORDER, into segments:
@@ -1772,7 +1834,22 @@ export async function runAgentLoop(prompt, options = {}) {
1772
1834
  // later recon from running; aborts, blocks, and sequential-barrier
1773
1835
  // failures still stop the batch so the model can react safely.
1774
1836
  const scopeForBatch = await loadScope().catch(() => undefined);
1837
+ /**
1838
+ * Tools that may share a concurrent wave. Pure read-only BATCH_SAFE
1839
+ * tools always qualify when classified safe. Heavy discovery wrappers
1840
+ * (pentest.recon, net.context) also run concurrent with dns/http so
1841
+ * nmap does not serialize the entire recon wave.
1842
+ * net.scan stays a barrier when it needs confirm/sudo UX.
1843
+ */
1775
1844
  const isParallelSafe = (c) => {
1845
+ if (c.name === "pentest.recon" ||
1846
+ c.name === "net.context" ||
1847
+ c.name === "tool.batch" ||
1848
+ c.name === "tool.check" ||
1849
+ c.name === "shell.jobs" ||
1850
+ c.name === "shell.tail") {
1851
+ return true;
1852
+ }
1776
1853
  if (!BATCH_SAFE_TOOLS.has(c.name))
1777
1854
  return false;
1778
1855
  try {
@@ -1782,13 +1859,36 @@ export async function runAgentLoop(prompt, options = {}) {
1782
1859
  return false;
1783
1860
  }
1784
1861
  };
1785
- const PARALLEL_LIMIT = 4;
1862
+ /** Tools whose failure must NOT cancel sibling calls in this turn. */
1863
+ const shouldSoftFailTool = (name) => {
1864
+ if (name === "tool.batch")
1865
+ return true;
1866
+ if (BATCH_SAFE_TOOLS.has(name))
1867
+ return true;
1868
+ if (name === "pentest.recon" ||
1869
+ name === "net.scan" ||
1870
+ name === "net.pingSweep" ||
1871
+ name === "net.context" ||
1872
+ name === "shell.jobs" ||
1873
+ name === "shell.tail" ||
1874
+ name === "tool.check") {
1875
+ return true;
1876
+ }
1877
+ return false;
1878
+ };
1879
+ // Recon waves often emit 6–10 lookups; 4 forced a second sequential wave.
1880
+ const PARALLEL_LIMIT = 8;
1786
1881
  let aborted = false;
1787
1882
  let blocked = false;
1788
1883
  let blockedResult = null;
1789
1884
  let failed = false;
1790
1885
  let awaitingPlanApproval = false;
1886
+ /** Indices into allCalls that actually ran (got a tool-result). */
1887
+ const executedIndices = new Set();
1791
1888
  const recordResult = (res, continueAfterFailure = false) => {
1889
+ const idx = allCalls.indexOf(res.call);
1890
+ if (idx >= 0)
1891
+ executedIndices.add(idx);
1792
1892
  messages.push({
1793
1893
  role: "tool",
1794
1894
  content: `Tool ${res.call.name} result (exit=${res.result.exitCode ?? 0}, ok=${res.result.ok}):\n${res.contextOutput}`,
@@ -1833,7 +1933,11 @@ export async function runAgentLoop(prompt, options = {}) {
1833
1933
  }
1834
1934
  const id = (idx >= 0 ? callIds[idx] : undefined) ?? `tool-${++nextToolEventId}`;
1835
1935
  const res = await executeSingleTool(call, id, options.signal || new AbortController().signal);
1836
- recordResult(res);
1936
+ // Soft-fail recon / read-only / discovery tools so a stalled
1937
+ // whois or failed lookup never cancels the rest of the turn with
1938
+ // "Cancelled — earlier tool in this batch failed."
1939
+ const softFail = shouldSoftFailTool(call.name);
1940
+ recordResult(res, softFail);
1837
1941
  }
1838
1942
  else {
1839
1943
  // Concurrent group — assign ids in document order, then push their
@@ -1853,6 +1957,41 @@ export async function runAgentLoop(prompt, options = {}) {
1853
1957
  recordResult(res, true);
1854
1958
  }
1855
1959
  }
1960
+ // Cards were emitted for every call up front. If the batch stopped
1961
+ // early (failure / abort / plan gate), any card still on "running"
1962
+ // must get a terminal result so the TUI never spins forever.
1963
+ for (let i = 0; i < allCalls.length; i += 1) {
1964
+ if (executedIndices.has(i))
1965
+ continue;
1966
+ const call = allCalls[i];
1967
+ if (i >= 0 && !callIds[i]) {
1968
+ callIds[i] = `tool-${++nextToolEventId}`;
1969
+ }
1970
+ const id = callIds[i];
1971
+ if (!alreadyPrintedIds.has(id)) {
1972
+ // Never shown in the transcript — skip.
1973
+ continue;
1974
+ }
1975
+ const reason = aborted
1976
+ ? "Cancelled — turn aborted before this call ran."
1977
+ : blocked
1978
+ ? "Cancelled — earlier tool was blocked or declined."
1979
+ : awaitingPlanApproval
1980
+ ? "Deferred — waiting for plan approval."
1981
+ : failed
1982
+ ? "Cancelled — earlier tool in this batch failed."
1983
+ : "Cancelled — not executed.";
1984
+ const result = {
1985
+ ok: false,
1986
+ output: reason,
1987
+ exitCode: 130,
1988
+ };
1989
+ emitToolResult(id, result, reason);
1990
+ messages.push({
1991
+ role: "tool",
1992
+ content: `Tool ${call.name} result (exit=130, ok=false):\n${reason}`,
1993
+ });
1994
+ }
1856
1995
  // plan.create is a hard transaction boundary. Its successful handler
1857
1996
  // persists and displays the plan; returning immediately prevents a
1858
1997
  // stale pre-loop activePlan snapshot from nudging a duplicate plan and