micro-models-agent 0.2.92 → 0.4.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 (393) hide show
  1. package/README.md +436 -436
  2. package/dist/agent/error-escalation.d.ts +18 -0
  3. package/dist/agent/error-escalation.d.ts.map +1 -0
  4. package/dist/agent/error-escalation.js +122 -0
  5. package/dist/agent/error-escalation.js.map +1 -0
  6. package/dist/agent/loop.d.ts +6 -5
  7. package/dist/agent/loop.d.ts.map +1 -1
  8. package/dist/agent/loop.js +433 -510
  9. package/dist/agent/loop.js.map +1 -1
  10. package/dist/agent/modes/applier.d.ts +2 -2
  11. package/dist/agent/modes/applier.d.ts.map +1 -1
  12. package/dist/agent/modes/registry.d.ts.map +1 -1
  13. package/dist/agent/modes/registry.js.map +1 -1
  14. package/dist/agent/modes/types.d.ts +12 -2
  15. package/dist/agent/modes/types.d.ts.map +1 -1
  16. package/dist/agent/react-loop.d.ts +4 -0
  17. package/dist/agent/react-loop.d.ts.map +1 -1
  18. package/dist/agent/react-loop.js +5 -23
  19. package/dist/agent/react-loop.js.map +1 -1
  20. package/dist/agent/system-prompt.d.ts +4 -3
  21. package/dist/agent/system-prompt.d.ts.map +1 -1
  22. package/dist/agent/system-prompt.js +392 -278
  23. package/dist/agent/system-prompt.js.map +1 -1
  24. package/dist/agent/tool-executor.d.ts +2 -0
  25. package/dist/agent/tool-executor.d.ts.map +1 -1
  26. package/dist/agent/tool-executor.js +1 -1
  27. package/dist/agent/tool-executor.js.map +1 -1
  28. package/dist/agent/write-enforcer.d.ts +11 -0
  29. package/dist/agent/write-enforcer.d.ts.map +1 -0
  30. package/dist/agent/write-enforcer.js +92 -0
  31. package/dist/agent/write-enforcer.js.map +1 -0
  32. package/dist/cli/config.d.ts +1 -1
  33. package/dist/cli/config.d.ts.map +1 -1
  34. package/dist/cli/config.js +53 -28
  35. package/dist/cli/config.js.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +19 -16
  38. package/dist/cli.js.map +1 -1
  39. package/dist/config/config.d.ts +2 -1
  40. package/dist/config/config.d.ts.map +1 -1
  41. package/dist/config/config.js +78 -54
  42. package/dist/config/config.js.map +1 -1
  43. package/dist/config/types.d.ts +8 -1
  44. package/dist/config/types.d.ts.map +1 -1
  45. package/dist/logger.d.ts +1 -0
  46. package/dist/logger.d.ts.map +1 -1
  47. package/dist/logger.js +5 -0
  48. package/dist/logger.js.map +1 -1
  49. package/dist/plugins/builtin/lint-on-write/index.ts +96 -96
  50. package/dist/plugins/builtin/notify/index.js +6 -6
  51. package/dist/plugins/builtin/notify/index.ts +104 -104
  52. package/dist/repl/commands.js +0 -5
  53. package/dist/repl/commands.js.map +1 -1
  54. package/dist/repl/history.d.ts +2 -1
  55. package/dist/repl/history.d.ts.map +1 -1
  56. package/dist/repl/history.js.map +1 -1
  57. package/dist/repl/index.d.ts +2 -0
  58. package/dist/repl/index.d.ts.map +1 -1
  59. package/dist/repl/index.js +2 -2
  60. package/dist/repl/index.js.map +1 -1
  61. package/dist/setup-wizard.d.ts +1 -1
  62. package/dist/setup-wizard.d.ts.map +1 -1
  63. package/dist/setup-wizard.js +137 -105
  64. package/dist/setup-wizard.js.map +1 -1
  65. package/dist/skills/builtin/agent-test/SKILL.md +68 -68
  66. package/dist/skills/builtin/grill-me/SKILL.md +46 -46
  67. package/dist/skills/builtin/web-scraping/SKILL.md +422 -422
  68. package/dist/skills/loader.d.ts +16 -0
  69. package/dist/skills/loader.d.ts.map +1 -1
  70. package/dist/skills/loader.js +110 -9
  71. package/dist/skills/loader.js.map +1 -1
  72. package/dist/tools/id-gen.d.ts +4 -0
  73. package/dist/tools/id-gen.d.ts.map +1 -0
  74. package/dist/tools/id-gen.js +12 -0
  75. package/dist/tools/id-gen.js.map +1 -0
  76. package/dist/tools/native/project-map.d.ts +2 -2
  77. package/dist/tools/native/project-map.d.ts.map +1 -1
  78. package/dist/tools/native/project-map.js +85 -57
  79. package/dist/tools/native/project-map.js.map +1 -1
  80. package/dist/tools/native/scaffold.js +113 -113
  81. package/dist/tools/native/theme.d.ts.map +1 -1
  82. package/dist/tools/native/theme.js +0 -5
  83. package/dist/tools/native/theme.js.map +1 -1
  84. package/dist/tools/parse-tool-call.d.ts +11 -0
  85. package/dist/tools/parse-tool-call.d.ts.map +1 -0
  86. package/dist/tools/parse-tool-call.js +85 -0
  87. package/dist/tools/parse-tool-call.js.map +1 -0
  88. package/dist/tools/plan.d.ts +2 -0
  89. package/dist/tools/plan.d.ts.map +1 -1
  90. package/dist/tools/plan.js +8 -2
  91. package/dist/tools/plan.js.map +1 -1
  92. package/dist/tools/subagent.d.ts +0 -8
  93. package/dist/tools/subagent.d.ts.map +1 -1
  94. package/dist/tools/subagent.js +15 -94
  95. package/dist/tools/subagent.js.map +1 -1
  96. package/dist/tools/todo.d.ts +2 -0
  97. package/dist/tools/todo.d.ts.map +1 -1
  98. package/dist/tools/todo.js +8 -2
  99. package/dist/tools/todo.js.map +1 -1
  100. package/dist/tools/web-browse.js +7 -7
  101. package/dist/ui/index.d.ts +1 -15
  102. package/dist/ui/index.d.ts.map +1 -1
  103. package/dist/ui/index.js +1 -12
  104. package/dist/ui/index.js.map +1 -1
  105. package/dist/ui/ink/components/App.d.ts +0 -1
  106. package/dist/ui/ink/components/App.d.ts.map +1 -1
  107. package/dist/ui/ink/components/App.js +2 -8
  108. package/dist/ui/ink/components/App.js.map +1 -1
  109. package/dist/ui/ink/components/Conversation.d.ts +1 -2
  110. package/dist/ui/ink/components/Conversation.d.ts.map +1 -1
  111. package/dist/ui/ink/components/Conversation.js +8 -2
  112. package/dist/ui/ink/components/Conversation.js.map +1 -1
  113. package/dist/ui/ink/components/MessageBlock.d.ts +13 -1
  114. package/dist/ui/ink/components/MessageBlock.d.ts.map +1 -1
  115. package/dist/ui/ink/components/MessageBlock.js +15 -27
  116. package/dist/ui/ink/components/MessageBlock.js.map +1 -1
  117. package/dist/ui/ink/components/PromptInput.d.ts +11 -0
  118. package/dist/ui/ink/components/PromptInput.d.ts.map +1 -1
  119. package/dist/ui/ink/components/PromptInput.js +18 -141
  120. package/dist/ui/ink/components/PromptInput.js.map +1 -1
  121. package/dist/ui/ink/components/SelectInput.d.ts.map +1 -1
  122. package/dist/ui/ink/components/SelectInput.js +3 -2
  123. package/dist/ui/ink/components/SelectInput.js.map +1 -1
  124. package/dist/ui/ink/components/StatusBar.d.ts.map +1 -1
  125. package/dist/ui/ink/components/StatusBar.js +14 -6
  126. package/dist/ui/ink/components/StatusBar.js.map +1 -1
  127. package/dist/ui/ink/components/StreamBlock.d.ts.map +1 -1
  128. package/dist/ui/ink/components/StreamBlock.js +3 -3
  129. package/dist/ui/ink/components/StreamBlock.js.map +1 -1
  130. package/dist/ui/ink/components/Tags.d.ts +8 -11
  131. package/dist/ui/ink/components/Tags.d.ts.map +1 -1
  132. package/dist/ui/ink/components/Tags.js +4 -16
  133. package/dist/ui/ink/components/Tags.js.map +1 -1
  134. package/dist/ui/ink/components/ToolCallBlock.d.ts +12 -0
  135. package/dist/ui/ink/components/ToolCallBlock.d.ts.map +1 -1
  136. package/dist/ui/ink/components/ToolCallBlock.js +8 -24
  137. package/dist/ui/ink/components/ToolCallBlock.js.map +1 -1
  138. package/dist/ui/ink/renderer.d.ts +0 -1
  139. package/dist/ui/ink/renderer.d.ts.map +1 -1
  140. package/dist/ui/ink/renderer.js +1 -4
  141. package/dist/ui/ink/renderer.js.map +1 -1
  142. package/dist/ui/renderer.d.ts +2 -2
  143. package/dist/ui/renderer.d.ts.map +1 -1
  144. package/dist/ui/renderer.js +1 -1
  145. package/package.json +4 -8
  146. package/dist/async-map.d.ts +0 -6
  147. package/dist/async-map.d.ts.map +0 -1
  148. package/dist/async-map.js +0 -16
  149. package/dist/async-map.js.map +0 -1
  150. package/dist/blog-api/types/index.d.ts +0 -2
  151. package/dist/blog-api/types/index.d.ts.map +0 -1
  152. package/dist/blog-api/types/index.js +0 -2
  153. package/dist/blog-api/types/index.js.map +0 -1
  154. package/dist/examples.d.ts +0 -22
  155. package/dist/examples.d.ts.map +0 -1
  156. package/dist/examples.js +0 -45
  157. package/dist/examples.js.map +0 -1
  158. package/dist/fibonacci.d.ts +0 -19
  159. package/dist/fibonacci.d.ts.map +0 -1
  160. package/dist/fibonacci.js +0 -61
  161. package/dist/fibonacci.js.map +0 -1
  162. package/dist/parfymerie-landing/src/App.d.ts +0 -4
  163. package/dist/parfymerie-landing/src/App.d.ts.map +0 -1
  164. package/dist/parfymerie-landing/src/App.js +0 -12
  165. package/dist/parfymerie-landing/src/App.js.map +0 -1
  166. package/dist/parfymerie-landing/src/main.d.ts +0 -2
  167. package/dist/parfymerie-landing/src/main.d.ts.map +0 -1
  168. package/dist/parfymerie-landing/src/main.js +0 -7
  169. package/dist/parfymerie-landing/src/main.js.map +0 -1
  170. package/dist/parfymerie-landing/vite.config.d.ts +0 -3
  171. package/dist/parfymerie-landing/vite.config.d.ts.map +0 -1
  172. package/dist/parfymerie-landing/vite.config.js +0 -7
  173. package/dist/parfymerie-landing/vite.config.js.map +0 -1
  174. package/dist/repl.d.ts +0 -5
  175. package/dist/repl.d.ts.map +0 -1
  176. package/dist/repl.js +0 -472
  177. package/dist/repl.js.map +0 -1
  178. package/dist/sort.d.ts +0 -8
  179. package/dist/sort.d.ts.map +0 -1
  180. package/dist/sort.js +0 -36
  181. package/dist/sort.js.map +0 -1
  182. package/dist/stack.d.ts +0 -9
  183. package/dist/stack.d.ts.map +0 -1
  184. package/dist/stack.js +0 -22
  185. package/dist/stack.js.map +0 -1
  186. package/dist/test-run.d.ts +0 -2
  187. package/dist/test-run.d.ts.map +0 -1
  188. package/dist/test-run.js +0 -13
  189. package/dist/test-run.js.map +0 -1
  190. package/dist/ui/components/diff-view.d.ts +0 -8
  191. package/dist/ui/components/diff-view.d.ts.map +0 -1
  192. package/dist/ui/components/diff-view.js +0 -25
  193. package/dist/ui/components/diff-view.js.map +0 -1
  194. package/dist/ui/double-press.d.ts +0 -9
  195. package/dist/ui/double-press.d.ts.map +0 -1
  196. package/dist/ui/double-press.js +0 -18
  197. package/dist/ui/double-press.js.map +0 -1
  198. package/dist/ui/ink/components/ActivityLog.d.ts +0 -12
  199. package/dist/ui/ink/components/ActivityLog.d.ts.map +0 -1
  200. package/dist/ui/ink/components/ActivityLog.js +0 -11
  201. package/dist/ui/ink/components/ActivityLog.js.map +0 -1
  202. package/dist/ui/ink/components/AgentStatus.d.ts +0 -13
  203. package/dist/ui/ink/components/AgentStatus.d.ts.map +0 -1
  204. package/dist/ui/ink/components/AgentStatus.js +0 -20
  205. package/dist/ui/ink/components/AgentStatus.js.map +0 -1
  206. package/dist/ui/ink/components/BottomBar.d.ts +0 -12
  207. package/dist/ui/ink/components/BottomBar.d.ts.map +0 -1
  208. package/dist/ui/ink/components/BottomBar.js +0 -22
  209. package/dist/ui/ink/components/BottomBar.js.map +0 -1
  210. package/dist/ui/ink/components/CheckboxInput.d.ts +0 -21
  211. package/dist/ui/ink/components/CheckboxInput.d.ts.map +0 -1
  212. package/dist/ui/ink/components/CheckboxInput.js +0 -36
  213. package/dist/ui/ink/components/CheckboxInput.js.map +0 -1
  214. package/dist/ui/ink/components/InputBox.d.ts +0 -25
  215. package/dist/ui/ink/components/InputBox.d.ts.map +0 -1
  216. package/dist/ui/ink/components/InputBox.js +0 -91
  217. package/dist/ui/ink/components/InputBox.js.map +0 -1
  218. package/dist/ui/ink/components/ModeSelector.d.ts +0 -8
  219. package/dist/ui/ink/components/ModeSelector.d.ts.map +0 -1
  220. package/dist/ui/ink/components/ModeSelector.js +0 -13
  221. package/dist/ui/ink/components/ModeSelector.js.map +0 -1
  222. package/dist/ui/ink/components/QuestionBox.d.ts +0 -14
  223. package/dist/ui/ink/components/QuestionBox.d.ts.map +0 -1
  224. package/dist/ui/ink/components/QuestionBox.js +0 -35
  225. package/dist/ui/ink/components/QuestionBox.js.map +0 -1
  226. package/dist/ui/ink/components/RadioInput.d.ts +0 -17
  227. package/dist/ui/ink/components/RadioInput.d.ts.map +0 -1
  228. package/dist/ui/ink/components/RadioInput.js +0 -30
  229. package/dist/ui/ink/components/RadioInput.js.map +0 -1
  230. package/dist/ui/ink/components/SliderInput.d.ts +0 -20
  231. package/dist/ui/ink/components/SliderInput.d.ts.map +0 -1
  232. package/dist/ui/ink/components/SliderInput.js +0 -40
  233. package/dist/ui/ink/components/SliderInput.js.map +0 -1
  234. package/dist/ui/ink/components/StartupScreen.d.ts +0 -19
  235. package/dist/ui/ink/components/StartupScreen.d.ts.map +0 -1
  236. package/dist/ui/ink/components/StartupScreen.js +0 -9
  237. package/dist/ui/ink/components/StartupScreen.js.map +0 -1
  238. package/dist/ui/ink/components/WelcomeScreen.d.ts +0 -10
  239. package/dist/ui/ink/components/WelcomeScreen.d.ts.map +0 -1
  240. package/dist/ui/ink/components/WelcomeScreen.js +0 -8
  241. package/dist/ui/ink/components/WelcomeScreen.js.map +0 -1
  242. package/dist/ui/ink/components/Window.d.ts +0 -30
  243. package/dist/ui/ink/components/Window.d.ts.map +0 -1
  244. package/dist/ui/ink/components/Window.js +0 -9
  245. package/dist/ui/ink/components/Window.js.map +0 -1
  246. package/dist/ui/ink/components/ui/_core.d.ts +0 -113
  247. package/dist/ui/ink/components/ui/_core.d.ts.map +0 -1
  248. package/dist/ui/ink/components/ui/_core.js +0 -83
  249. package/dist/ui/ink/components/ui/_core.js.map +0 -1
  250. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.d.ts +0 -23
  251. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.d.ts.map +0 -1
  252. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.js +0 -56
  253. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.js.map +0 -1
  254. package/dist/ui/ink/components/ui/autocomplete/index.d.ts +0 -3
  255. package/dist/ui/ink/components/ui/autocomplete/index.d.ts.map +0 -1
  256. package/dist/ui/ink/components/ui/autocomplete/index.js +0 -2
  257. package/dist/ui/ink/components/ui/autocomplete/index.js.map +0 -1
  258. package/dist/ui/ink/components/ui/badge/Badge.d.ts +0 -13
  259. package/dist/ui/ink/components/ui/badge/Badge.d.ts.map +0 -1
  260. package/dist/ui/ink/components/ui/badge/Badge.js +0 -17
  261. package/dist/ui/ink/components/ui/badge/Badge.js.map +0 -1
  262. package/dist/ui/ink/components/ui/badge/index.d.ts +0 -3
  263. package/dist/ui/ink/components/ui/badge/index.d.ts.map +0 -1
  264. package/dist/ui/ink/components/ui/badge/index.js +0 -2
  265. package/dist/ui/ink/components/ui/badge/index.js.map +0 -1
  266. package/dist/ui/ink/components/ui/divider/Divider.d.ts +0 -12
  267. package/dist/ui/ink/components/ui/divider/Divider.d.ts.map +0 -1
  268. package/dist/ui/ink/components/ui/divider/Divider.js +0 -27
  269. package/dist/ui/ink/components/ui/divider/Divider.js.map +0 -1
  270. package/dist/ui/ink/components/ui/divider/index.d.ts +0 -3
  271. package/dist/ui/ink/components/ui/divider/index.d.ts.map +0 -1
  272. package/dist/ui/ink/components/ui/divider/index.js +0 -2
  273. package/dist/ui/ink/components/ui/divider/index.js.map +0 -1
  274. package/dist/ui/ink/components/ui/header/Header.d.ts +0 -13
  275. package/dist/ui/ink/components/ui/header/Header.d.ts.map +0 -1
  276. package/dist/ui/ink/components/ui/header/Header.js +0 -33
  277. package/dist/ui/ink/components/ui/header/Header.js.map +0 -1
  278. package/dist/ui/ink/components/ui/header/index.d.ts +0 -3
  279. package/dist/ui/ink/components/ui/header/index.d.ts.map +0 -1
  280. package/dist/ui/ink/components/ui/header/index.js +0 -2
  281. package/dist/ui/ink/components/ui/header/index.js.map +0 -1
  282. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.d.ts +0 -14
  283. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.d.ts.map +0 -1
  284. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.js +0 -144
  285. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.js.map +0 -1
  286. package/dist/ui/ink/components/ui/json-viewer/index.d.ts +0 -3
  287. package/dist/ui/ink/components/ui/json-viewer/index.d.ts.map +0 -1
  288. package/dist/ui/ink/components/ui/json-viewer/index.js +0 -2
  289. package/dist/ui/ink/components/ui/json-viewer/index.js.map +0 -1
  290. package/dist/ui/ink/components/ui/key-hint/KeyHint.d.ts +0 -14
  291. package/dist/ui/ink/components/ui/key-hint/KeyHint.d.ts.map +0 -1
  292. package/dist/ui/ink/components/ui/key-hint/KeyHint.js +0 -5
  293. package/dist/ui/ink/components/ui/key-hint/KeyHint.js.map +0 -1
  294. package/dist/ui/ink/components/ui/key-hint/index.d.ts +0 -3
  295. package/dist/ui/ink/components/ui/key-hint/index.d.ts.map +0 -1
  296. package/dist/ui/ink/components/ui/key-hint/index.js +0 -2
  297. package/dist/ui/ink/components/ui/key-hint/index.js.map +0 -1
  298. package/dist/ui/ink/components/ui/markdown/Markdown.d.ts +0 -11
  299. package/dist/ui/ink/components/ui/markdown/Markdown.d.ts.map +0 -1
  300. package/dist/ui/ink/components/ui/markdown/Markdown.js +0 -129
  301. package/dist/ui/ink/components/ui/markdown/Markdown.js.map +0 -1
  302. package/dist/ui/ink/components/ui/markdown/index.d.ts +0 -3
  303. package/dist/ui/ink/components/ui/markdown/index.d.ts.map +0 -1
  304. package/dist/ui/ink/components/ui/markdown/index.js +0 -2
  305. package/dist/ui/ink/components/ui/markdown/index.js.map +0 -1
  306. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.d.ts +0 -21
  307. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.d.ts.map +0 -1
  308. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.js +0 -42
  309. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.js.map +0 -1
  310. package/dist/ui/ink/components/ui/scroll-area/index.d.ts +0 -3
  311. package/dist/ui/ink/components/ui/scroll-area/index.d.ts.map +0 -1
  312. package/dist/ui/ink/components/ui/scroll-area/index.js +0 -2
  313. package/dist/ui/ink/components/ui/scroll-area/index.js.map +0 -1
  314. package/dist/ui/ink/components/ui/select/Select.d.ts +0 -19
  315. package/dist/ui/ink/components/ui/select/Select.d.ts.map +0 -1
  316. package/dist/ui/ink/components/ui/select/Select.js +0 -73
  317. package/dist/ui/ink/components/ui/select/Select.js.map +0 -1
  318. package/dist/ui/ink/components/ui/select/index.d.ts +0 -3
  319. package/dist/ui/ink/components/ui/select/index.d.ts.map +0 -1
  320. package/dist/ui/ink/components/ui/select/index.js +0 -2
  321. package/dist/ui/ink/components/ui/select/index.js.map +0 -1
  322. package/dist/ui/ink/components/ui/spinner/Spinner.d.ts +0 -14
  323. package/dist/ui/ink/components/ui/spinner/Spinner.d.ts.map +0 -1
  324. package/dist/ui/ink/components/ui/spinner/Spinner.js +0 -16
  325. package/dist/ui/ink/components/ui/spinner/Spinner.js.map +0 -1
  326. package/dist/ui/ink/components/ui/spinner/index.d.ts +0 -3
  327. package/dist/ui/ink/components/ui/spinner/index.d.ts.map +0 -1
  328. package/dist/ui/ink/components/ui/spinner/index.js +0 -2
  329. package/dist/ui/ink/components/ui/spinner/index.js.map +0 -1
  330. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.d.ts +0 -12
  331. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.d.ts.map +0 -1
  332. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.js +0 -39
  333. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.js.map +0 -1
  334. package/dist/ui/ink/components/ui/status-indicator/index.d.ts +0 -3
  335. package/dist/ui/ink/components/ui/status-indicator/index.d.ts.map +0 -1
  336. package/dist/ui/ink/components/ui/status-indicator/index.js +0 -2
  337. package/dist/ui/ink/components/ui/status-indicator/index.js.map +0 -1
  338. package/dist/ui/ink/components/ui/streaming-text/StreamingText.d.ts +0 -13
  339. package/dist/ui/ink/components/ui/streaming-text/StreamingText.d.ts.map +0 -1
  340. package/dist/ui/ink/components/ui/streaming-text/StreamingText.js +0 -22
  341. package/dist/ui/ink/components/ui/streaming-text/StreamingText.js.map +0 -1
  342. package/dist/ui/ink/components/ui/streaming-text/index.d.ts +0 -3
  343. package/dist/ui/ink/components/ui/streaming-text/index.d.ts.map +0 -1
  344. package/dist/ui/ink/components/ui/streaming-text/index.js +0 -2
  345. package/dist/ui/ink/components/ui/streaming-text/index.js.map +0 -1
  346. package/dist/ui/ink/components/ui/tabs/Tabs.d.ts +0 -20
  347. package/dist/ui/ink/components/ui/tabs/Tabs.d.ts.map +0 -1
  348. package/dist/ui/ink/components/ui/tabs/Tabs.js +0 -53
  349. package/dist/ui/ink/components/ui/tabs/Tabs.js.map +0 -1
  350. package/dist/ui/ink/components/ui/tabs/index.d.ts +0 -3
  351. package/dist/ui/ink/components/ui/tabs/index.d.ts.map +0 -1
  352. package/dist/ui/ink/components/ui/tabs/index.js +0 -2
  353. package/dist/ui/ink/components/ui/tabs/index.js.map +0 -1
  354. package/dist/ui/ink/components/ui/text-input/TextInput.d.ts +0 -22
  355. package/dist/ui/ink/components/ui/text-input/TextInput.d.ts.map +0 -1
  356. package/dist/ui/ink/components/ui/text-input/TextInput.js +0 -57
  357. package/dist/ui/ink/components/ui/text-input/TextInput.js.map +0 -1
  358. package/dist/ui/ink/components/ui/text-input/index.d.ts +0 -3
  359. package/dist/ui/ink/components/ui/text-input/index.d.ts.map +0 -1
  360. package/dist/ui/ink/components/ui/text-input/index.js +0 -2
  361. package/dist/ui/ink/components/ui/text-input/index.js.map +0 -1
  362. package/dist/ui/ink/components/ui/token-counter/TokenCounter.d.ts +0 -17
  363. package/dist/ui/ink/components/ui/token-counter/TokenCounter.d.ts.map +0 -1
  364. package/dist/ui/ink/components/ui/token-counter/TokenCounter.js +0 -38
  365. package/dist/ui/ink/components/ui/token-counter/TokenCounter.js.map +0 -1
  366. package/dist/ui/ink/components/ui/token-counter/index.d.ts +0 -3
  367. package/dist/ui/ink/components/ui/token-counter/index.d.ts.map +0 -1
  368. package/dist/ui/ink/components/ui/token-counter/index.js +0 -2
  369. package/dist/ui/ink/components/ui/token-counter/index.js.map +0 -1
  370. package/dist/ui/ink/components/ui/typewriter/Typewriter.d.ts +0 -14
  371. package/dist/ui/ink/components/ui/typewriter/Typewriter.d.ts.map +0 -1
  372. package/dist/ui/ink/components/ui/typewriter/Typewriter.js +0 -57
  373. package/dist/ui/ink/components/ui/typewriter/Typewriter.js.map +0 -1
  374. package/dist/ui/ink/components/ui/typewriter/index.d.ts +0 -3
  375. package/dist/ui/ink/components/ui/typewriter/index.d.ts.map +0 -1
  376. package/dist/ui/ink/components/ui/typewriter/index.js +0 -2
  377. package/dist/ui/ink/components/ui/typewriter/index.js.map +0 -1
  378. package/dist/ui/opentui-renderer.d.ts +0 -53
  379. package/dist/ui/opentui-renderer.d.ts.map +0 -1
  380. package/dist/ui/opentui-renderer.js +0 -463
  381. package/dist/ui/opentui-renderer.js.map +0 -1
  382. package/dist/utils/memoize.d.ts +0 -14
  383. package/dist/utils/memoize.d.ts.map +0 -1
  384. package/dist/utils/memoize.js +0 -51
  385. package/dist/utils/memoize.js.map +0 -1
  386. package/dist/utils/types.d.ts +0 -6
  387. package/dist/utils/types.d.ts.map +0 -1
  388. package/dist/utils/types.js +0 -2
  389. package/dist/utils/types.js.map +0 -1
  390. package/dist/utils.d.ts +0 -22
  391. package/dist/utils.d.ts.map +0 -1
  392. package/dist/utils.js +0 -81
  393. package/dist/utils.js.map +0 -1
@@ -1,85 +1,55 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { jsonrepair } from 'jsonrepair';
4
- import { parseFileRefs } from '../file-loader.js';
5
- import { ContextManager } from './context.js';
6
- import { autoLearnFromError } from './memory.js';
7
- import { collectCodingStyle } from './user-profile.js';
8
- import { ModeRegistry } from './modes/registry.js';
9
- import { applyMode } from './modes/applier.js';
10
- import { loadSkill, loadSkillsFromRefs, enforceSkillBudget, preloadSkillsByKeywords, isSkillRequest, listInstalledSkills } from '../skills/loader.js';
11
- import { runPreToolUseHooks, runPostToolUseHooks } from '../tools/hooks.js';
12
- import { isMCPCommand, parseMCPCommand, callMCPTool } from '../tools/mcp.js';
13
- import { showPlan, getPlanPendingCount, clearPlan, getNextPendingStep, updatePlanStep, extractDirFromStepTitle } from '../tools/plan.js';
14
- import { checkFileSyntax } from '../tools/syntax-check.js';
15
- import { loadConfig, getLLMConfig, getSpecialistConfig, getGroundingConfig } from '../config/config.js';
16
- import { buildSystemPrompt } from './system-prompt.js';
17
- import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus } from '../logger.js';
18
- import { buildSubagentContext, buildSpecialistTask, isTrivialError, runIsolatedLoop, extractAllToolCalls, validateToolCall, doChat } from '../tools/subagent.js';
19
- import { SubagentRenderer } from '../ui/subagent-renderer.js';
20
- import { ToolExecutor } from './tool-executor.js';
21
- import { PluginManager } from '../plugins/manager.js';
22
- import { loadAllPluginsWithInfo } from '../plugins/loader.js';
23
- import { handleToolAfter as lintHandleToolAfter } from '../plugins/builtin/lint-on-write/index.js';
24
- import { handleAgentDone, handleToolBlocked, handleToolAfter as notifyHandleToolAfter } from '../plugins/builtin/notify/index.js';
25
- import { planHandler } from '../tools/native/plan.js';
26
- import { todoHandler } from '../tools/native/todo.js';
27
- import { questionHandler } from '../tools/native/question.js';
28
- import { approveHandler } from '../tools/native/approve.js';
29
- import { userProfileHandler } from '../tools/native/user-profile.js';
30
- import { themeHandler } from '../tools/native/theme.js';
31
- import { scaffoldHandler } from '../tools/native/scaffold.js';
32
- import { projectMapHandler } from '../tools/native/project-map.js';
33
- import { bashHandler } from '../tools/bash.js';
34
- import { readFileHandler } from '../tools/fs/read-file.js';
35
- import { writeFileHandler } from '../tools/fs/write-file.js';
36
- import { editFileHandler } from '../tools/fs/edit-file.js';
37
- import { listDirHandler } from '../tools/fs/list-dir.js';
38
- import { createDirHandler } from '../tools/fs/create-dir.js';
39
- import { deleteFileHandler } from '../tools/fs/delete-file.js';
40
- import { copyFileHandler } from '../tools/fs/copy-file.js';
41
- import { moveFileHandler } from '../tools/fs/move-file.js';
42
- import { fileInfoHandler } from '../tools/fs/file-info.js';
43
- import { globSearchHandler } from '../tools/fs/glob-search.js';
44
- import { grepSearchHandler } from '../tools/fs/grep-search.js';
45
- import { treeHandler } from '../tools/fs/tree.js';
46
- import { webSearchHandler } from '../tools/web-search.js';
47
- import { webFetchHandler } from '../tools/web-fetch.js';
48
- import { webBrowseHandler } from '../tools/web-browse.js';
49
- /**
50
- * Умная обрезка tool-результатов для экономии токенов в контексте.
51
- *
52
- * Полные результаты сохраняются в файл сессии, в контексте — только
53
- * достаточно информации для модели чтобы продолжить работу.
54
- *
55
- * Лимиты подобраны эмпирически: достаточно для маленьких моделей
56
- * понять что произошло, но не занимают весь контекст.
57
- */
58
- function truncateToolOutput(toolName, output, exitCode) {
59
- // Ошибки обрезаем мягче — модели нужен контекст для диагностики
60
- if (exitCode !== 0) {
61
- return output.length > 3000 ? output.slice(0, 3000) + '\n...[truncated]' : output;
62
- }
63
- // Лимиты для успешных операций (в символах)
64
- const LIMITS = {
65
- read_file: 2000, // модель может ссылаться на строки
66
- write_file: 200, // достаточно "Written X bytes"
67
- edit_file: 300, // нужно знать что изменилось
68
- bash: 1500, // build output, git status
69
- list_dir: 1500, // структура обычно компактна
70
- glob_search: 1000, // список файлов
71
- grep_search: 1500, // результаты поиска
72
- tree: 1500, // дерево файлов
73
- subagent: 500, // краткое резюме от субагента
74
- web_search: 1000, // результаты поиска
75
- web_fetch: 1500, // контент страницы
76
- web_browse: 2000, // browser interaction results
77
- };
78
- const limit = LIMITS[toolName] ?? 800;
79
- if (output.length <= limit)
80
- return output;
81
- return output.slice(0, limit) + '\n...[truncated]';
82
- }
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, isAbsolute, relative } from "node:path";
3
+ import { parseToolCalls } from "../tools/parse-tool-call.js";
4
+ import { parseFileRefs } from "../file-loader.js";
5
+ import { ContextManager } from "./context.js";
6
+ import { autoLearnFromError } from "./memory.js";
7
+ import { collectCodingStyle } from "./user-profile.js";
8
+ import { ModeRegistry } from "./modes/registry.js";
9
+ import { applyMode } from "./modes/applier.js";
10
+ import { loadSkill, loadSkillsFromRefs, enforceSkillBudget, preloadSkillsByKeywords, isSkillRequest, listInstalledSkills, } from "../skills/loader.js";
11
+ import { runPreToolUseHooks, runPostToolUseHooks } from "../tools/hooks.js";
12
+ import { isMCPCommand, parseMCPCommand, callMCPTool } from "../tools/mcp.js";
13
+ import { showPlan, getPlanPendingCount, clearPlan, getNextPendingStep, updatePlanStep, extractDirFromStepTitle, } from "../tools/plan.js";
14
+ import { generateToolCallId } from "../tools/id-gen.js";
15
+ import { checkFileSyntax } from "../tools/syntax-check.js";
16
+ import { loadConfig, getLLMConfig, getGroundingConfig, } from "../config/config.js";
17
+ import { buildSystemPrompt } from "./system-prompt.js";
18
+ import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus, } from "../logger.js";
19
+ import { buildSubagentContext, runIsolatedLoop, doChat, } from "../tools/subagent.js";
20
+ import { SubagentRenderer } from "../ui/subagent-renderer.js";
21
+ import { ToolExecutor } from "./tool-executor.js";
22
+ import { ErrorEscalator } from "./error-escalation.js";
23
+ import { WriteEnforcer } from "./write-enforcer.js";
24
+ import { PluginManager } from "../plugins/manager.js";
25
+ import { loadAllPluginsWithInfo } from "../plugins/loader.js";
26
+ import { handleToolAfter as lintHandleToolAfter } from "../plugins/builtin/lint-on-write/index.js";
27
+ import { handleAgentDone, handleToolBlocked, handleToolAfter as notifyHandleToolAfter, } from "../plugins/builtin/notify/index.js";
28
+ import { planHandler } from "../tools/native/plan.js";
29
+ import { todoHandler } from "../tools/native/todo.js";
30
+ import { questionHandler } from "../tools/native/question.js";
31
+ import { approveHandler } from "../tools/native/approve.js";
32
+ import { userProfileHandler } from "../tools/native/user-profile.js";
33
+ import { themeHandler } from "../tools/native/theme.js";
34
+ import { scaffoldHandler } from "../tools/native/scaffold.js";
35
+ import { projectMapHandler } from "../tools/native/project-map.js";
36
+ import { bashHandler } from "../tools/bash.js";
37
+ import { readFileHandler } from "../tools/fs/read-file.js";
38
+ import { writeFileHandler } from "../tools/fs/write-file.js";
39
+ import { editFileHandler } from "../tools/fs/edit-file.js";
40
+ import { listDirHandler } from "../tools/fs/list-dir.js";
41
+ import { createDirHandler } from "../tools/fs/create-dir.js";
42
+ import { deleteFileHandler } from "../tools/fs/delete-file.js";
43
+ import { copyFileHandler } from "../tools/fs/copy-file.js";
44
+ import { moveFileHandler } from "../tools/fs/move-file.js";
45
+ import { fileInfoHandler } from "../tools/fs/file-info.js";
46
+ import { globSearchHandler } from "../tools/fs/glob-search.js";
47
+ import { grepSearchHandler } from "../tools/fs/grep-search.js";
48
+ import { treeHandler } from "../tools/fs/tree.js";
49
+ import { webSearchHandler } from "../tools/web-search.js";
50
+ import { webFetchHandler } from "../tools/web-fetch.js";
51
+ import { webBrowseHandler } from "../tools/web-browse.js";
52
+ import { truncateOutput } from "./tool-executor.js";
83
53
  /**
84
54
  * Основной цикл ReAct-агента.
85
55
  *
@@ -115,64 +85,16 @@ function extractToolCallFallback(response) {
115
85
  const m = response.match(re);
116
86
  if (m) {
117
87
  let cmd = m[1];
118
- cmd = cmd.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
119
- return { name: 'bash', cmd };
88
+ cmd = cmd
89
+ .replace(/\\n/g, "\n")
90
+ .replace(/\\t/g, "\t")
91
+ .replace(/\\"/g, '"')
92
+ .replace(/\\\\/g, "\\");
93
+ return { name: "bash", cmd };
120
94
  }
121
95
  }
122
96
  return null;
123
97
  }
124
- /**
125
- * Запускает specialist'а для решения повторяющейся ошибки.
126
- *
127
- * Specialist — это изолированный суб-агент со свежим контекстом,
128
- * который получает задачу "диагностируй и исправь ошибку инструмента X".
129
- *
130
- * Алгоритм:
131
- * 1. Формирует задачу через buildSpecialistTask()
132
- * 2. Собирает контекст проекта через buildSubagentContext()
133
- * 3. Запускает runIsolatedLoop() с ограничением maxSteps
134
- * 4. Заменяет результат ошибки в messages[] на результат specialist'а
135
- * 5. Трекает созданные specialist'ом файлы
136
- *
137
- * @returns true если specialist отработал (основной цикл должен continue),
138
- * false если specialist упал (ошибка остаётся в messages)
139
- */
140
- async function spawnSpecialist(p) {
141
- if (!p.quiet) {
142
- p.renderer?.writeBracketTag('specialist', `solving '${p.errorOutput.slice(0, 60).replace(/\n/g, ' ')}' error...`);
143
- }
144
- const task = buildSpecialistTask(p.toolName, p.toolArgs, p.errorOutput, p.enrichedPrompt, p.errorCount);
145
- const ctx = buildSubagentContext(process.cwd());
146
- const sub = new SubagentRenderer();
147
- try {
148
- logInfo('LOOP', `spawning specialist for ${p.toolName} (attempt #${p.errorCount})`);
149
- const r = await runIsolatedLoop(p.provider, p.registry, task, ctx, process.cwd(), p.quiet, sub, p.toolExecutor, p.modeConfig.subagentMode, p.llmCfg.toolModel, p.specialistCfg.maxSteps);
150
- for (let i = p.messages.length - 1; i >= 0; i--) {
151
- if (p.messages[i].role === 'tool' && p.messages[i].tool_call_id === p.toolId) {
152
- const text = r.errors.length > 0
153
- ? `[Specialist attempted to fix "${p.toolName}" but failed:\n${r.errors.join('\n')}\n${r.summary}]\nTry a different approach.`
154
- : `[Specialist resolved "${p.toolName}" error.\n${r.summary}]\nContinue with the task.`;
155
- p.messages[i].content = truncateToolOutput(p.toolName, text, 0);
156
- break;
157
- }
158
- }
159
- for (const f of r.createdFiles) {
160
- if (!p.createdFiles.includes(f))
161
- p.createdFiles.push(f);
162
- }
163
- const buf = sub.getBuffer();
164
- if (!p.quiet && p.renderer?.writeToolBuffer)
165
- p.renderer.writeToolBuffer({ lines: buf.lines, toolCalls: buf.toolCalls });
166
- logInfo('LOOP', `specialist done | files: ${r.createdFiles.length} | turns: ${r.turns} | errors: ${r.errors.length}`);
167
- return true;
168
- }
169
- catch (e) {
170
- logError('LOOP', `specialist error: ${e.message}`);
171
- if (!p.quiet)
172
- p.renderer?.writeError(` [specialist failed: ${e.message}]`);
173
- return false;
174
- }
175
- }
176
98
  /**
177
99
  * Запускает основной цикл ReAct-агента.
178
100
  *
@@ -196,30 +118,39 @@ async function spawnSpecialist(p) {
196
118
  * @param options - дополнительные опции (quiet, showThinking, mode)
197
119
  */
198
120
  export async function runAgentLoop(provider, registry, prompt, contextManager, previousMessages, renderer, providerName, options) {
199
- logInfo('LOOP', `runAgentLoop started: "${prompt.slice(0, 50)}"`);
121
+ logInfo("LOOP", `runAgentLoop started: "${prompt.slice(0, 50)}"`);
200
122
  renderer?.resetAbort?.();
201
123
  // Парсинг ссылок на файлы в промпте (формат @path/to/file)
202
124
  const { prompt: enrichedPrompt, files } = await parseFileRefs(prompt);
203
- logDebug('LOOP', `parseFileRefs: ${files.length} files, prompt len: ${enrichedPrompt.length}`);
125
+ logDebug("LOOP", `parseFileRefs: ${files.length} files, prompt len: ${enrichedPrompt.length}`);
204
126
  const quiet = options?.quiet ?? false;
205
127
  // ─── ModeRegistry: конфигурация режима ──
206
128
  const modeRegistry = new ModeRegistry();
207
- const modeConfig = modeRegistry.getOrFallback(options?.mode || 'base');
208
- const applied = applyMode(modeConfig, { quiet, showThinking: options?.showThinking });
129
+ const modeConfig = modeRegistry.getOrFallback(options?.mode || "base");
130
+ const applied = applyMode(modeConfig, {
131
+ quiet,
132
+ showThinking: options?.showThinking,
133
+ });
209
134
  // ─── Behaviors из ModeConfig ──────────────────────────
210
135
  const behaviors = modeConfig.behaviors;
211
136
  const planEnforcement = behaviors.planEnforcement !== false;
212
- const directWriteStreakMax = typeof behaviors.directWriteStreakLimit === 'number'
213
- ? behaviors.directWriteStreakLimit : (behaviors.directWriteStreak !== false ? 3 : Infinity);
214
- const continuationNudgeMax = typeof behaviors.continuationNudgeLimit === 'number'
215
- ? behaviors.continuationNudgeLimit : (behaviors.continuationNudge !== false ? 2 : 0);
137
+ const directWriteStreakMax = typeof behaviors.directWriteStreakLimit === "number"
138
+ ? behaviors.directWriteStreakLimit
139
+ : behaviors.directWriteStreak !== false
140
+ ? 3
141
+ : Infinity;
142
+ const continuationNudgeMax = typeof behaviors.continuationNudgeLimit === "number"
143
+ ? behaviors.continuationNudgeLimit
144
+ : behaviors.continuationNudge !== false
145
+ ? 2
146
+ : 0;
216
147
  const readConsolidation = behaviors.readConsolidation !== false;
217
148
  // Автозагрузка скилов из промпта (если пользователь указал @skill-name)
218
149
  const loadedSkills = loadSkillsFromRefs(prompt, process.cwd());
219
- const loadedSkillNames = new Set(loadedSkills.map(s => s.name));
150
+ const loadedSkillNames = new Set(loadedSkills.map((s) => s.name));
220
151
  if (loadedSkills.length > 0) {
221
- logInfo('LOOP', `loaded ${loadedSkills.length} skills: ${loadedSkills.map(s => s.name).join(', ')}`);
222
- renderer?.writeInfo(` Loaded ${loadedSkills.length} skill(s): ${loadedSkills.map(s => s.name).join(', ')}`);
152
+ logInfo("LOOP", `loaded ${loadedSkills.length} skills: ${loadedSkills.map((s) => s.name).join(", ")}`);
153
+ renderer?.writeInfo(` Loaded ${loadedSkills.length} skill(s): ${loadedSkills.map((s) => s.name).join(", ")}`);
223
154
  }
224
155
  // Принудительная загрузка скилов из ModeConfig
225
156
  for (const skillName of applied.forcedSkills) {
@@ -228,11 +159,11 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
228
159
  if (skill) {
229
160
  loadedSkills.push(skill);
230
161
  loadedSkillNames.add(skillName);
231
- logInfo('LOOP', `auto-loaded ${skillName} skill for ${modeConfig.id} mode`);
162
+ logInfo("LOOP", `auto-loaded ${skillName} skill for ${modeConfig.id} mode`);
232
163
  renderer?.writeInfo(` Auto-loaded @${skillName} skill for ${modeConfig.id} mode`);
233
164
  }
234
165
  else {
235
- logWarn('LOOP', `${skillName} skill not found for ${modeConfig.id} mode`);
166
+ logWarn("LOOP", `${skillName} skill not found for ${modeConfig.id} mode`);
236
167
  renderer?.writeWarning(` Warning: @${skillName} skill not found (required for ${modeConfig.id} mode)`);
237
168
  }
238
169
  }
@@ -244,8 +175,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
244
175
  const budget = applied.skillTokenBudget;
245
176
  const { allowed, dropped } = enforceSkillBudget(loadedSkills, budget);
246
177
  if (dropped.length > 0) {
247
- renderer?.writeWarning(` Skill limit (${maxSkills} max in ${modeConfig.id} mode): dropped ${dropped.join(', ')}`);
248
- logInfo('LOOP', `skill limit: dropped ${dropped.join(', ')} (budget: ${budget} tokens)`);
178
+ renderer?.writeWarning(` Skill limit (${maxSkills} max in ${modeConfig.id} mode): dropped ${dropped.join(", ")}`);
179
+ logInfo("LOOP", `skill limit: dropped ${dropped.join(", ")} (budget: ${budget} tokens)`);
249
180
  loadedSkills.length = 0;
250
181
  loadedSkills.push(...allowed);
251
182
  for (const s of dropped)
@@ -253,33 +184,41 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
253
184
  }
254
185
  }
255
186
  // Pre-load скиллов по ключевым словам (без language-specific хардкода)
256
- if (loadedSkills.length === 0 && options?.mode !== 'fast') {
187
+ if (loadedSkills.length === 0 && options?.mode !== "fast") {
257
188
  const keywordSkills = preloadSkillsByKeywords(prompt, process.cwd());
258
189
  for (const skill of keywordSkills) {
259
190
  if (!loadedSkillNames.has(skill.name)) {
260
191
  loadedSkills.push(skill);
261
192
  loadedSkillNames.add(skill.name);
262
- logInfo('LOOP', `keyword preloaded skill: ${skill.name}`);
193
+ logInfo("LOOP", `keyword preloaded skill: ${skill.name}`);
263
194
  renderer?.writeInfo(` Auto-loaded @${skill.name} (keyword match)`);
264
195
  }
265
196
  }
266
197
  // Если пользователь явно просит "используй скилл", но ничего не нашлось — подсказываем
267
198
  if (keywordSkills.length === 0 && isSkillRequest(prompt)) {
268
199
  const allSkills = listInstalledSkills(process.cwd());
269
- renderer?.writeWarning(` You asked for a skill but none matched. Use @skill-name in your prompt to load one. Installed: ${allSkills.slice(0, 10).map(s => '@' + s).join(', ')}... (${allSkills.length} total)`);
200
+ renderer?.writeWarning(` You asked for a skill but none matched. Use @skill-name in your prompt to load one. Installed: ${allSkills
201
+ .slice(0, 10)
202
+ .map((s) => "@" + s)
203
+ .join(", ")}... (${allSkills.length} total)`);
270
204
  }
271
205
  }
272
206
  if (files.length > 0) {
273
207
  const totalTokens = files.reduce((sum, f) => sum + f.tokens, 0);
274
- const totalLines = files.reduce((sum, f) => sum + f.content.split('\n').length, 0);
275
- logInfo('LOOP', `loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
208
+ const totalLines = files.reduce((sum, f) => sum + f.content.split("\n").length, 0);
209
+ logInfo("LOOP", `loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
276
210
  renderer?.writeInfo(` Loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
277
211
  }
278
212
  setStatusWriter(renderer ? (msg) => renderer.writeDim(` ${msg}`) : null);
279
- writeStatus('Initializing...');
213
+ writeStatus("Initializing...");
280
214
  // ─── PluginManager: загрузка и активация плагинов ──
281
215
  const pluginManager = new PluginManager();
282
- const pluginLogger = { info: logInfo.bind(null, 'PLUGIN'), warn: logWarn.bind(null, 'PLUGIN'), error: logError.bind(null, 'PLUGIN'), debug: logDebug.bind(null, 'PLUGIN') };
216
+ const pluginLogger = {
217
+ info: logInfo.bind(null, "PLUGIN"),
218
+ warn: logWarn.bind(null, "PLUGIN"),
219
+ error: logError.bind(null, "PLUGIN"),
220
+ debug: logDebug.bind(null, "PLUGIN"),
221
+ };
283
222
  const pluginCtx = { cwd: process.cwd(), config: {}, logger: pluginLogger };
284
223
  // Загружаем все плагины (builtin + project + user)
285
224
  const pluginInfos = await loadAllPluginsWithInfo(pluginManager);
@@ -288,7 +227,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
288
227
  await pluginManager.activateAll(modeConfig.plugins, pluginCtx);
289
228
  }
290
229
  // Авто-активация builtin плагинов (все моды)
291
- const builtinPlugins = pluginInfos.filter(p => p.source === 'builtin');
230
+ const builtinPlugins = pluginInfos.filter((p) => p.source === "builtin");
292
231
  for (const bp of builtinPlugins) {
293
232
  if (!pluginManager.isActive(bp.name)) {
294
233
  await pluginManager.activate(bp.name, pluginCtx);
@@ -302,45 +241,38 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
302
241
  }
303
242
  }
304
243
  // Регистрация обработчиков событий для всех активных плагинов
305
- if (pluginManager.isActive('lint-on-write')) {
306
- pluginManager.on('tool:after', 'lint-on-write', (toolName, args, result) => {
307
- return lintHandleToolAfter(toolName, args, result, process.cwd(), '.lint-detected');
244
+ if (pluginManager.isActive("lint-on-write")) {
245
+ pluginManager.on("tool:after", "lint-on-write", (toolName, args, result) => {
246
+ return lintHandleToolAfter(toolName, args, result, process.cwd(), ".lint-detected");
308
247
  });
309
248
  }
310
- if (pluginManager.isActive('notify')) {
311
- pluginManager.on('agent:done', 'notify', () => handleAgentDone());
312
- pluginManager.on('tool:blocked', 'notify', (toolName, args) => handleToolBlocked(toolName, args));
313
- pluginManager.on('tool:after', 'notify', (toolName, args, result) => notifyHandleToolAfter(toolName, args, result));
249
+ if (pluginManager.isActive("notify")) {
250
+ pluginManager.on("agent:done", "notify", () => handleAgentDone());
251
+ pluginManager.on("tool:blocked", "notify", (toolName, args) => handleToolBlocked(toolName, args));
252
+ pluginManager.on("tool:after", "notify", (toolName, args, result) => notifyHandleToolAfter(toolName, args, result));
314
253
  }
315
254
  const pluginSections = await pluginManager.getPromptSections();
316
255
  // Сборка system prompt (включает инструкции, информацию о проекте, скилы, память, плагины)
317
256
  const sysStart = performance.now();
318
- const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined, options?.mode, pluginSections);
257
+ const systemPrompt = buildSystemPrompt(loadedSkills.length > 0 ? loadedSkills : undefined, options?.mode, pluginSections, options?.flags);
319
258
  const sysElapsed = ((performance.now() - sysStart) / 1000).toFixed(1);
320
259
  writeStatus(`Done (${sysElapsed}s)`);
321
260
  setStatusWriter(null);
322
261
  // Начальное состояние истории: system prompt + предыдущие сообщения + текущий промпт
323
262
  const messages = [
324
- { role: 'system', content: systemPrompt },
263
+ { role: "system", content: systemPrompt },
325
264
  ...(previousMessages || []),
326
- { role: 'user', content: enrichedPrompt },
265
+ { role: "user", content: enrichedPrompt },
327
266
  ];
328
267
  const ctx = contextManager ?? new ContextManager(provider.contextWindow);
329
268
  const maxSteps = config.agent.maxSteps ?? 100;
330
269
  let consecutiveEmpty = 0; // счётчик пустых ответов подряд
331
- let pendingSyntaxError = '';
270
+ let pendingSyntaxError = "";
332
271
  let aborted = false;
333
272
  let continuationNudges = 0; // счётчик подсказок "продолжай работать"
334
273
  let consecutiveMissingGrounding = 0; // счётчик пропущенных grounding-маркеров подряд
335
- let directWriteStreak = 0; // количество прямых write_file без subagent
336
- /** Трекер повторяющихся tool errors: { normalizedError → count } */
337
- const consecutiveToolErrors = new Map();
338
- /** Общий счётчик ошибок за сессию */
339
- let totalToolErrors = 0;
340
- /** Стратегия обработки ошибок: 'normal' | 'subagent-hint' | 'force-subagent' */
341
- let errorStrategy = 'normal';
342
- /** Флаг: specialist уже был запущен для текущей ошибки (чтобы не дублировать) */
343
- let specialistSpawnedFor = null;
274
+ let groundingRetryCount = 0; // сколько раз был CRITICAL retry подряд
275
+ let consecutiveInvalidCalls = 0; // счётчик невалидных tool call подряд
344
276
  /** Счётчик бесполезных действий (list_dir, glob_search, todo list без результата) */
345
277
  let uselessActionCount = 0;
346
278
  /** История последних чтений файлов для.detect повторных чтений */
@@ -349,17 +281,39 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
349
281
  const recentGrepPatterns = [];
350
282
  /** История последних вопросов для detect повторных вопросов */
351
283
  const recentQuestions = [];
284
+ /** История последних bash-команд для detect циклических вызовов */
285
+ const recentBashCommands = [];
352
286
  // ─── ToolExecutor: единый диспетчер инструментов ──
353
287
  const toolExecutor = new ToolExecutor();
354
288
  toolExecutor.setPluginManager(pluginManager);
355
289
  toolExecutor.registerAll([
356
- planHandler, todoHandler, questionHandler, approveHandler,
357
- userProfileHandler, themeHandler, scaffoldHandler, projectMapHandler, bashHandler,
358
- readFileHandler, writeFileHandler, editFileHandler, listDirHandler,
359
- createDirHandler, deleteFileHandler, copyFileHandler, moveFileHandler,
360
- fileInfoHandler, globSearchHandler, grepSearchHandler, treeHandler,
361
- webSearchHandler, webFetchHandler, webBrowseHandler,
290
+ planHandler,
291
+ todoHandler,
292
+ questionHandler,
293
+ approveHandler,
294
+ userProfileHandler,
295
+ themeHandler,
296
+ scaffoldHandler,
297
+ projectMapHandler,
298
+ bashHandler,
299
+ readFileHandler,
300
+ writeFileHandler,
301
+ editFileHandler,
302
+ listDirHandler,
303
+ createDirHandler,
304
+ deleteFileHandler,
305
+ copyFileHandler,
306
+ moveFileHandler,
307
+ fileInfoHandler,
308
+ globSearchHandler,
309
+ grepSearchHandler,
310
+ treeHandler,
311
+ webSearchHandler,
312
+ webFetchHandler,
313
+ webBrowseHandler,
362
314
  ]);
315
+ const errorEscalator = new ErrorEscalator();
316
+ const writeEnforcer = new WriteEnforcer();
363
317
  // Reset abort signal for this run
364
318
  if (renderer?.abortSignal === undefined) {
365
319
  // Renderer doesn't support abort — fine
@@ -379,7 +333,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
379
333
  }
380
334
  let skillsAutoLoaded = false;
381
335
  const createdFiles = []; // отслеживание созданных/изменённых файлов
382
- logDebug('LOOP', `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
336
+ logDebug("LOOP", `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
383
337
  // Инкрементальный подсчёт — считаем только новые сообщения (быстрее в ~100x на больших историях)
384
338
  if (step === 0) {
385
339
  await ctx.updateTokens(messages);
@@ -387,8 +341,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
387
341
  else {
388
342
  await ctx.updateTokensIncremental(messages);
389
343
  }
390
- const skillNames = loadedSkills.map(s => s.name).join(', ');
391
- const usageText = ctx.formatUsage() + (skillNames ? ` │ skills: ${skillNames}` : '');
344
+ const skillNames = loadedSkills.map((s) => s.name).join(", ");
345
+ const usageText = ctx.formatUsage() + (skillNames ? ` │ skills: ${skillNames}` : "");
392
346
  renderer?.updateContextUsage(usageText);
393
347
  renderer?.updateStatus({
394
348
  provider: providerName,
@@ -404,7 +358,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
404
358
  const before = messages.length;
405
359
  messages.splice(0, messages.length, ...ctx.compact(messages));
406
360
  if (!quiet) {
407
- renderer?.writeBracketTag('compact', `${before} → ${messages.length} messages`);
361
+ renderer?.writeBracketTag("compact", `${before} → ${messages.length} messages`);
408
362
  }
409
363
  await ctx.updateTokens(messages);
410
364
  }
@@ -413,13 +367,13 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
413
367
  const before = messages.length;
414
368
  messages.splice(0, messages.length, ...ctx.emergencyCompact(messages));
415
369
  if (!quiet) {
416
- renderer?.writeBracketTag('compact', `EMERGENCY: ${before} → ${messages.length}`);
370
+ renderer?.writeBracketTag("compact", `EMERGENCY: ${before} → ${messages.length}`);
417
371
  }
418
372
  await ctx.updateTokens(messages);
419
373
  }
420
374
  // ─── Запрос к LLM ────────────────────────────────────
421
375
  const llmCfg = getLLMConfig(config);
422
- logInfo('LOOP', `calling doChat, messages: ${messages.length}, options.showThinking: ${options?.showThinking}, retries: ${llmCfg.retries}`);
376
+ logInfo("LOOP", `calling doChat, messages: ${messages.length}, options.showThinking: ${options?.showThinking}, retries: ${llmCfg.retries}`);
423
377
  const { response } = await doChat(provider, messages, renderer, renderer?.abortSignal, llmCfg.retries, options?.showThinking);
424
378
  // Проверка прерывания во время генерации
425
379
  if (renderer?.abortSignal?.aborted) {
@@ -428,12 +382,21 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
428
382
  }
429
383
  // ─── Автозагрузка скилов по @упоминаниям ─────────────
430
384
  // Если ответ содержит @skill-name, загружаем скил и пересобираем system prompt
431
- if (options?.mode !== 'fast' && !skillsAutoLoaded) {
385
+ if (options?.mode !== "fast" && !skillsAutoLoaded) {
432
386
  const suggestedSkillRefs = response.matchAll(/@(\w[\w-]*)/g);
433
387
  const newSkillNames = [];
434
388
  for (const match of suggestedSkillRefs) {
435
389
  const name = match[1];
436
- if (['email', 'api', 'id', 'json', 'url', 'http', 'https', 'localhost'].includes(name))
390
+ if ([
391
+ "email",
392
+ "api",
393
+ "id",
394
+ "json",
395
+ "url",
396
+ "http",
397
+ "https",
398
+ "localhost",
399
+ ].includes(name))
437
400
  continue;
438
401
  if (!loadedSkillNames.has(name)) {
439
402
  const skill = loadSkill(name, process.cwd());
@@ -451,16 +414,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
451
414
  if (autoBudget > 0 && loadedSkills.length > 0) {
452
415
  const { allowed, dropped } = enforceSkillBudget(loadedSkills, autoBudget);
453
416
  if (dropped.length > 0) {
454
- renderer?.writeWarning(` Auto-load budget: dropped ${dropped.join(', ')} (budget: ${autoBudget} tokens)`);
455
- logInfo('LOOP', `auto-load budget: dropped ${dropped.join(', ')} (budget: ${autoBudget} tokens)`);
417
+ renderer?.writeWarning(` Auto-load budget: dropped ${dropped.join(", ")} (budget: ${autoBudget} tokens)`);
418
+ logInfo("LOOP", `auto-load budget: dropped ${dropped.join(", ")} (budget: ${autoBudget} tokens)`);
456
419
  loadedSkills.length = 0;
457
420
  loadedSkills.push(...allowed);
458
421
  for (const s of dropped)
459
422
  loadedSkillNames.delete(s);
460
423
  }
461
424
  }
462
- renderer?.writeInfo(` Auto-loaded ${loadedSkills.filter(s => newSkillNames.includes(s.name)).length} skill(s): ${newSkillNames.join(', ')}`);
463
- messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections) };
425
+ renderer?.writeInfo(` Auto-loaded ${loadedSkills.filter((s) => newSkillNames.includes(s.name)).length} skill(s): ${newSkillNames.join(", ")}`);
426
+ messages[0] = {
427
+ role: "system",
428
+ content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections, options?.flags),
429
+ };
464
430
  // Не удаляем ответ ассистента — skills загружаются для следующих шагов цикла.
465
431
  // Модель увидит обновлённый system prompt на следующей итерации и сможет
466
432
  // использовать загруженные скилы без retry (экономит ~один LLM-запрос).
@@ -469,140 +435,124 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
469
435
  // ─── Обработка пустых ответов ────────────────────────
470
436
  // Маленькие модели иногда возвращают пустой ответ — повторяем с подсказкой
471
437
  if (!response.trim()) {
472
- logWarn('LOOP', `empty response #${consecutiveEmpty + 1}`);
438
+ logWarn("LOOP", `empty response #${consecutiveEmpty + 1}`);
473
439
  consecutiveEmpty++;
474
440
  // После компактинга — более информативное сообщение о пустом ответе
475
441
  if (ctx.compactCount > 0 && consecutiveEmpty <= 3) {
476
442
  let lastUserMsg;
477
443
  for (let i = messages.length - 1; i >= 0; i--) {
478
- if (messages[i].role === 'user') {
444
+ if (messages[i].role === "user") {
479
445
  lastUserMsg = messages[i];
480
446
  break;
481
447
  }
482
448
  }
483
449
  const contextHint = lastUserMsg
484
- ? `[System: You returned an empty response. The conversation was compressed. Continue working on: "${(lastUserMsg.content || '').slice(0, 150)}..."]`
485
- : '[System: You returned an empty response. Continue with the task.]';
486
- messages.push({ role: 'user', content: contextHint });
450
+ ? `[System: You returned an empty response. The conversation was compressed. Continue working on: "${(lastUserMsg.content || "").slice(0, 150)}..."]`
451
+ : "[System: You returned an empty response. Continue with the task.]";
452
+ messages.push({ role: "user", content: contextHint });
487
453
  if (!quiet) {
488
454
  renderer?.writeDim(` [empty response, retrying with context hint]`);
489
455
  }
490
456
  continue;
491
457
  }
492
458
  const lastMsg = messages[messages.length - 1];
493
- if ((lastMsg?.role === 'user' || lastMsg?.role === 'system') && consecutiveEmpty <= 2) {
459
+ if ((lastMsg?.role === "user" || lastMsg?.role === "system") &&
460
+ consecutiveEmpty <= 2) {
494
461
  // После system-сообщений (например "Skills loaded") — более конкретная подсказка
495
- const isAfterSystem = lastMsg?.role === 'system';
462
+ const isAfterSystem = lastMsg?.role === "system";
496
463
  const hint = isAfterSystem
497
- ? '[System: You returned an empty response. The skills are loaded. Now create a plan and start working on the task. Do NOT wait for further instructions.]'
498
- : 'Continue.';
499
- messages.push({ role: 'user', content: hint });
464
+ ? "[System: You returned an empty response. The skills are loaded. Now create a plan and start working on the task. Do NOT wait for further instructions.]"
465
+ : "Continue.";
466
+ messages.push({ role: "user", content: hint });
500
467
  if (!quiet) {
501
- renderer?.writeDim(` [empty response, retrying${isAfterSystem ? ' with skills hint' : ''}]`);
468
+ renderer?.writeDim(` [empty response, retrying${isAfterSystem ? " with skills hint" : ""}]`);
502
469
  }
503
470
  continue;
504
471
  }
505
472
  renderer?.writeError(consecutiveEmpty > 3
506
473
  ? `[model returned empty response ${consecutiveEmpty} times in a row — stopping]`
507
- : '[model returned empty response]');
474
+ : "[model returned empty response]");
508
475
  return { messages };
509
476
  }
510
477
  consecutiveEmpty = 0;
511
- messages.push({ role: 'assistant', content: response });
512
- // ─── Парсинг tool calls ─────────────────────────────
513
- // Извлекаем JSON из тегов <tool_call>...</tool_call> и валидируем
514
- const allJsonRaw = extractAllToolCalls(response);
515
- const validCalls = [];
516
- const invalidCalls = [];
517
- for (const jsonRaw of allJsonRaw) {
518
- let p = null;
519
- try {
520
- p = JSON.parse(jsonrepair(jsonRaw));
521
- }
522
- catch {
523
- try {
524
- p = JSON.parse(jsonRaw);
525
- }
526
- catch { /* continue */ }
527
- }
528
- if (p) {
529
- const v = validateToolCall(p);
530
- if (v.valid) {
531
- validCalls.push({ parsed: p, cmd: v.cmd });
532
- }
533
- else if (v.error) {
534
- invalidCalls.push(v.error);
535
- }
536
- }
537
- }
538
- // ─── Fallback: regex-извлечение при невалидном JSON ───
539
- if (validCalls.length === 0) {
540
- const fallback = extractToolCallFallback(response);
541
- if (fallback) {
542
- validCalls.push({ parsed: { name: 'bash', arguments: { cmd: fallback.cmd } }, cmd: fallback.cmd });
543
- if (!quiet) {
544
- renderer?.writeBracketTag('fallback', 'regex extracted command');
545
- }
546
- }
547
- }
478
+ messages.push({ role: "assistant", content: response });
479
+ const { validCalls, invalidCalls } = parseToolCalls(response);
548
480
  if (validCalls.length === 0) {
549
481
  if (invalidCalls.length > 0) {
550
482
  // If the response has substantial text outside tool_call tags,
551
483
  // it's a description with examples — not a failed tool call attempt.
552
- const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
484
+ const textOutside = response
485
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
486
+ .trim();
553
487
  if (textOutside.length < 100) {
554
- const feedback = `[Tool call error(s)]\n${invalidCalls.join('\n')}\n\nFix the tool call format and try again. Use ONLY valid tool names: bash, read_file, write_file, edit_file, list_dir, create_dir, delete_file, copy_file, move_file, file_info, glob_search, grep_search, tree, web_search, web_fetch, web_browse, plan, todo, question, approve, subagent, scaffold.`;
488
+ consecutiveInvalidCalls++;
489
+ const feedback = `[Tool call error(s)]\n${invalidCalls.join("\n")}\n\nFix the tool call format and try again. Use ONLY valid tool names: bash, read_file, write_file, edit_file, list_dir, create_dir, delete_file, copy_file, move_file, file_info, glob_search, grep_search, tree, web_search, web_fetch, web_browse, plan, todo, question, approve, subagent, scaffold.`;
555
490
  if (!quiet) {
556
491
  renderer?.writeError(` [invalid tool call: ${invalidCalls[0]}]`);
557
492
  }
558
- messages.push({ role: 'tool', content: feedback, tool_call_id: `invalid-${Date.now()}` });
493
+ messages.push({
494
+ role: "tool",
495
+ content: feedback,
496
+ tool_call_id: `invalid-${Date.now()}`,
497
+ });
559
498
  continue;
560
499
  }
561
500
  // Text response with example tool calls — just log and proceed
562
- logWarn('LOOP', `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
501
+ logWarn("LOOP", `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
563
502
  }
564
503
  // Nudge model to continue working after a tool result — but only if
565
504
  // the agent didn't already write a meaningful text response.
566
- if (continuationNudgeMax > 0 && continuationNudges < continuationNudgeMax) {
505
+ if (continuationNudgeMax > 0 &&
506
+ continuationNudges < continuationNudgeMax) {
567
507
  const lastThree = messages.slice(-3);
568
- const hasRecentToolResult = lastThree.some(m => m.role === 'tool');
569
- const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim().length > 20;
508
+ const hasRecentToolResult = lastThree.some((m) => m.role === "tool");
509
+ const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "").trim()
510
+ .length > 20;
570
511
  if (hasRecentToolResult && !alreadyResponded) {
571
512
  continuationNudges++;
572
513
  if (!quiet) {
573
514
  renderer?.writeDim(` [nudge ${continuationNudges}/${continuationNudgeMax}: continue working]`);
574
515
  }
575
- messages.push({ role: 'user', content: '[Tool result received. Write a response to the user. Do NOT call additional tools unless there is actual work to do.]' });
516
+ messages.push({
517
+ role: "user",
518
+ content: "[Tool result received. Write a response to the user. Do NOT call additional tools unless there is actual work to do.]",
519
+ });
576
520
  continue;
577
521
  }
578
522
  }
579
523
  if (!skillsAutoLoaded) {
580
- const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
524
+ const textOutside = response
525
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
526
+ .trim();
581
527
  const looksLikeDone = /done|готово|создан|выполн|completed|finished/i.test(textOutside);
582
528
  // Проверка: план с незавершёнными шагами — не даём агенту остановиться
583
529
  const pendingSteps = getPlanPendingCount();
584
530
  if (pendingSteps > 0 && looksLikeDone) {
585
531
  const planText = showPlan();
586
- const pendingLines = planText.split('\n').filter(l => l.includes('○') || l.includes('►'));
587
- const nextStep = pendingLines[0] || '';
532
+ const pendingLines = planText
533
+ .split("\n")
534
+ .filter((l) => l.includes("○") || l.includes("►"));
535
+ const nextStep = pendingLines[0] || "";
588
536
  const nextStepMatch = nextStep.match(/\d+\.\s*(.+)/);
589
- const nextStepTitle = nextStepMatch ? nextStepMatch[1].trim() : '';
537
+ const nextStepTitle = nextStepMatch ? nextStepMatch[1].trim() : "";
590
538
  if (!quiet) {
591
539
  renderer?.writeWarning(` [plan has ${pendingSteps} incomplete steps — pushing agent to continue]`);
592
540
  }
593
541
  const nudge = nextStepTitle
594
542
  ? `Plan is NOT complete! ${pendingSteps} step(s) remaining.\nNext step: ${nextStepTitle}\nUse subagent to execute this step, then mark done: plan({action:"done", step:N})`
595
- : `Plan is NOT complete! ${pendingSteps} step(s) remaining:\n${pendingLines.join('\n')}\n\nContinue with the next step using subagent.`;
596
- messages.push({ role: 'user', content: nudge });
543
+ : `Plan is NOT complete! ${pendingSteps} step(s) remaining:\n${pendingLines.join("\n")}\n\nContinue with the next step using subagent.`;
544
+ messages.push({ role: "user", content: nudge });
597
545
  continuationNudges = 0;
598
546
  continue;
599
547
  }
600
548
  // Phase 4.1: Аудит файлов при завершении плана
601
549
  if (pendingSteps === 0 && looksLikeDone) {
602
550
  const plan = showPlan();
603
- if (!plan.startsWith('No active plan')) {
551
+ if (!plan.startsWith("No active plan")) {
604
552
  // Извлекаем заголовки шагов и проверяем наличие файлов
605
- const stepLines = plan.split('\n').filter(l => l.match(/\d+\.\s/));
553
+ const stepLines = plan
554
+ .split("\n")
555
+ .filter((l) => l.match(/\d+\.\s/));
606
556
  const missingFiles = [];
607
557
  for (const line of stepLines) {
608
558
  const titleMatch = line.match(/\d+\.\s*(.+)/);
@@ -610,22 +560,23 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
610
560
  continue;
611
561
  const title = titleMatch[1].trim();
612
562
  // Извлекаем имя файла из заголовка (паттерн: "Create file.ext" или "file.ext")
613
- const fileMatch = title.match(/(?:Create|Создать|Add|Добавить)\s+(\S+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i)
614
- || title.match(/(\w+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i);
563
+ const fileMatch = title.match(/(?:Create|Создать|Add|Добавить)\s+(\S+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i) ||
564
+ title.match(/(\w+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i);
615
565
  if (fileMatch) {
616
566
  const fileName = fileMatch[1];
617
- if (!existsSync(fileName) && !createdFiles.some(f => f.endsWith(fileName))) {
567
+ if (!existsSync(fileName) &&
568
+ !createdFiles.some((f) => f.endsWith(fileName))) {
618
569
  missingFiles.push(fileName);
619
570
  }
620
571
  }
621
572
  }
622
573
  if (missingFiles.length > 0) {
623
574
  if (!quiet) {
624
- renderer?.writeWarning(` [plan complete but missing files: ${missingFiles.join(', ')}]`);
575
+ renderer?.writeWarning(` [plan complete but missing files: ${missingFiles.join(", ")}]`);
625
576
  }
626
577
  messages.push({
627
- role: 'user',
628
- content: `Plan claims all steps done, but these files are missing: ${missingFiles.join(', ')}. Create them before finishing.`,
578
+ role: "user",
579
+ content: `Plan claims all steps done, but these files are missing: ${missingFiles.join(", ")}. Create them before finishing.`,
629
580
  });
630
581
  continuationNudges = 0;
631
582
  continue;
@@ -633,16 +584,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
633
584
  }
634
585
  }
635
586
  // Stuck detection: если модель делает бесполезные действия
636
- const isUselessAction = validCalls.some(c => c.parsed.name === 'list_dir' || c.parsed.name === 'tree' ||
637
- (c.parsed.name === 'todo' && c.parsed.arguments?.action === 'list') ||
638
- (c.parsed.name === 'question' && !c.parsed.arguments?.question));
639
- // Phase 5.1: Расширенное detect — повторные чтения, поиски и вопросы
587
+ const isUselessAction = validCalls.some((c) => c.parsed.name === "list_dir" ||
588
+ c.parsed.name === "tree" ||
589
+ (c.parsed.name === "todo" &&
590
+ c.parsed.arguments?.action === "list") ||
591
+ (c.parsed.name === "question" && !c.parsed.arguments?.question));
592
+ // Phase 5.1: Расширенное detect — повторные чтения, поиски, вопросы и bash
640
593
  let isRepeatedRead = false;
641
594
  let isRepeatedSearch = false;
642
595
  let isRepeatedQuestion = false;
596
+ let isRepeatedBash = false;
643
597
  for (const c of validCalls) {
644
- if (c.parsed.name === 'read_file') {
645
- const filePath = String(c.parsed.arguments?.path || '');
598
+ if (c.parsed.name === "read_file") {
599
+ const filePath = String(c.parsed.arguments?.path || "");
646
600
  if (recentReadFiles.includes(filePath)) {
647
601
  isRepeatedRead = true;
648
602
  }
@@ -650,8 +604,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
650
604
  if (recentReadFiles.length > 5)
651
605
  recentReadFiles.shift();
652
606
  }
653
- if (c.parsed.name === 'grep_search') {
654
- const pattern = String(c.parsed.arguments?.pattern || '');
607
+ if (c.parsed.name === "grep_search") {
608
+ const pattern = String(c.parsed.arguments?.pattern || "");
655
609
  if (recentGrepPatterns.includes(pattern)) {
656
610
  isRepeatedSearch = true;
657
611
  }
@@ -659,8 +613,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
659
613
  if (recentGrepPatterns.length > 5)
660
614
  recentGrepPatterns.shift();
661
615
  }
662
- if (c.parsed.name === 'question') {
663
- const questionText = String(c.parsed.arguments?.question || '');
616
+ if (c.parsed.name === "question") {
617
+ const questionText = String(c.parsed.arguments?.question || "");
664
618
  if (recentQuestions.includes(questionText)) {
665
619
  isRepeatedQuestion = true;
666
620
  }
@@ -668,8 +622,22 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
668
622
  if (recentQuestions.length > 3)
669
623
  recentQuestions.shift();
670
624
  }
625
+ if (c.parsed.name === "bash") {
626
+ const cmdText = String(c.parsed.arguments?.cmd || "");
627
+ const normalized = cmdText.trim().replace(/\s+/g, " ");
628
+ if (recentBashCommands.includes(normalized)) {
629
+ isRepeatedBash = true;
630
+ }
631
+ recentBashCommands.push(normalized);
632
+ if (recentBashCommands.length > 5)
633
+ recentBashCommands.shift();
634
+ }
671
635
  }
672
- const isStuckAction = isUselessAction || isRepeatedRead || isRepeatedSearch || isRepeatedQuestion;
636
+ const isStuckAction = isUselessAction ||
637
+ isRepeatedRead ||
638
+ isRepeatedSearch ||
639
+ isRepeatedQuestion ||
640
+ isRepeatedBash;
673
641
  if (isStuckAction) {
674
642
  uselessActionCount++;
675
643
  if (uselessActionCount >= 3) {
@@ -678,7 +646,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
678
646
  }
679
647
  clearPlan();
680
648
  messages.push({
681
- role: 'user',
649
+ role: "user",
682
650
  content: `[System: You are stuck in a loop. STOP calling the same tool. The user already answered your question — use their answer and proceed with the task. Do NOT ask the same question again.]`,
683
651
  });
684
652
  uselessActionCount = 0;
@@ -686,17 +654,20 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
686
654
  continue;
687
655
  }
688
656
  }
689
- else if (validCalls.some(c => c.parsed.name === 'subagent' || c.parsed.name === 'write_file' || c.parsed.name === 'bash')) {
657
+ else if (validCalls.some((c) => c.parsed.name === "subagent" ||
658
+ c.parsed.name === "write_file" ||
659
+ c.parsed.name === "bash")) {
690
660
  uselessActionCount = 0;
691
661
  recentReadFiles.length = 0;
692
662
  recentGrepPatterns.length = 0;
693
663
  recentQuestions.length = 0;
664
+ recentBashCommands.length = 0;
694
665
  }
695
666
  // Если модель claim-ит done но есть созданные файлы — напоминаем о полноте
696
667
  if (createdFiles.length > 0 && looksLikeDone) {
697
668
  messages.push({
698
- role: 'user',
699
- content: `Files created: ${createdFiles.join(', ')}. Before finishing — verify all files are complete and connected to each other. If any file is missing or not imported, continue working.`,
669
+ role: "user",
670
+ content: `Files created: ${createdFiles.join(", ")}. Before finishing — verify all files are complete and connected to each other. If any file is missing or not imported, continue working.`,
700
671
  });
701
672
  continuationNudges = 0;
702
673
  continue;
@@ -711,48 +682,75 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
711
682
  const groundingCfg = getGroundingConfig(config);
712
683
  if (groundingCfg.enabled) {
713
684
  const groundMatches = Array.from(response.matchAll(/@grounding\[файл:\s*([^\]]+)\]/g));
714
- const groundPaths = groundMatches.map(m => m[1].trim());
715
- const pathCalls = validCalls.filter(c => c.parsed.arguments?.path);
685
+ const groundPaths = groundMatches.map((m) => m[1].trim());
686
+ const pathCalls = validCalls.filter((c) => c.parsed.arguments?.path);
716
687
  if (pathCalls.length > groundPaths.length) {
717
688
  consecutiveMissingGrounding++;
718
689
  const maxMissing = groundingCfg.maxMissingGrounding ?? 3;
690
+ const maxRetries = groundingCfg.maxConsecutiveGroundingErrors ?? 5;
719
691
  if (consecutiveMissingGrounding >= maxMissing) {
692
+ // CRITICAL — сброс счётчика + учёт retry
693
+ consecutiveMissingGrounding = 0;
694
+ groundingRetryCount++;
695
+ if (groundingRetryCount > maxRetries) {
696
+ // Полный bypass — выполняем tool_call без маркера
697
+ if (!quiet)
698
+ renderer?.writeWarning(` [grounding: exceeded limit, bypassing]`);
699
+ groundingRetryCount = 0;
700
+ messages.push({
701
+ role: "user",
702
+ content: `[Note: Grounding check disabled after repeated failures. Proceeding without marker. To re-enable, reset the agent.]`,
703
+ });
704
+ // fall through — tool call будет выполнен
705
+ }
706
+ else {
707
+ if (!quiet)
708
+ renderer?.writeError(` [grounding: ${groundingRetryCount}/${maxRetries} retries]`);
709
+ messages.push({
710
+ role: "user",
711
+ content: `[CRITICAL: Output @grounding[файл: <path>] BEFORE each <tool_call>.\nExample:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
712
+ });
713
+ continue;
714
+ }
715
+ }
716
+ else if (groundingCfg.strictMode) {
720
717
  if (!quiet)
721
- renderer?.writeError(` [grounding: ${consecutiveMissingGrounding}/${maxMissing} missing markers]`);
718
+ renderer?.writeError(` [grounding: strict mode, retrying]`);
722
719
  messages.push({
723
- role: 'user',
724
- content: `[Grounding Error: Missing @grounding[файл: path] marker before tool call. CRITICAL: Output the marker on its own line BEFORE every <tool_call>.]`,
720
+ role: "user",
721
+ content: `[Grounding Error: Missing @grounding[файл: path] marker. Output the marker BEFORE each <tool_call>. Example:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
725
722
  });
726
723
  continue;
727
724
  }
728
- if (groundingCfg.strictMode) {
725
+ else {
729
726
  if (!quiet)
730
- renderer?.writeError(` [grounding: strict mode, retrying]`);
727
+ renderer?.writeDim(` [grounding: missing marker, reminder ${consecutiveMissingGrounding}/${maxMissing}]`);
731
728
  messages.push({
732
- role: 'user',
733
- content: `[Grounding Error: Missing @grounding[файл: path] marker. Try again with proper grounding marker before each tool call.]`,
729
+ role: "user",
730
+ content: `[Reminder: Include @grounding[файл: path] marker before each <tool_call> to prevent path hallucinations. Example:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
734
731
  });
735
732
  continue;
736
733
  }
737
- if (!quiet)
738
- renderer?.writeDim(` [grounding: missing marker, reminder ${consecutiveMissingGrounding}/${maxMissing}]`);
739
- messages.push({
740
- role: 'user',
741
- content: `[Reminder: Include @grounding[файл: path] marker before each <tool_call> to prevent path hallucinations.]`,
742
- });
743
- continue;
744
734
  }
745
735
  else {
746
736
  consecutiveMissingGrounding = 0;
737
+ groundingRetryCount = 0;
747
738
  // Проверка соответствия пути в маркере и в аргументах tool_call
748
739
  for (let i = 0; i < pathCalls.length && i < groundPaths.length; i++) {
749
- const callPath = String(pathCalls[i].parsed.arguments.path || '').replace(/\\/g, '/').replace(/^\.\//, '');
750
- const groundPath = groundPaths[i].replace(/\\/g, '/').replace(/^\.\//, '');
751
- if (callPath && groundPath && !callPath.endsWith(groundPath) && !groundPath.endsWith(callPath)) {
740
+ const callPath = String(pathCalls[i].parsed.arguments.path || "")
741
+ .replace(/\\/g, "/")
742
+ .replace(/^\.\//, "");
743
+ const groundPath = groundPaths[i]
744
+ .replace(/\\/g, "/")
745
+ .replace(/^\.\//, "");
746
+ if (callPath &&
747
+ groundPath &&
748
+ !callPath.endsWith(groundPath) &&
749
+ !groundPath.endsWith(callPath)) {
752
750
  if (!quiet)
753
751
  renderer?.writeWarning(` [grounding: path mismatch — marker says "${groundPath}", tool says "${callPath}"]`);
754
752
  messages.push({
755
- role: 'user',
753
+ role: "user",
756
754
  content: `[Warning: Grounding marker path "${groundPath}" doesn't match tool call path "${callPath}". Fix the marker to match the actual path.]`,
757
755
  });
758
756
  break;
@@ -764,155 +762,97 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
764
762
  // ─── Enforce: лимиты на запись файлов ────────────────
765
763
  // Отслеживание повторных чтений одного файла для консолидации
766
764
  let lastReadFile = null;
767
- // Блокировка прямых write_file когда есть план с 3+ шагами — force subagent
768
- const planSteps = showPlan();
769
- const hasMultiStepPlan = planEnforcement && !planSteps.startsWith('No active plan') && (planSteps.match(/\n/g) || []).length >= 3;
770
- if (hasMultiStepPlan) {
771
- const directWrites = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
772
- const hasSubagent = validCalls.some(c => c.parsed.name === 'subagent');
773
- const hasBash = validCalls.some(c => c.parsed.name === 'bash');
774
- if (directWrites.length > 0 && !hasSubagent) {
775
- // Block direct writes — model must use subagent
776
- for (const wc of directWrites) {
777
- const idx = validCalls.indexOf(wc);
778
- if (idx !== -1)
779
- validCalls.splice(idx, 1);
780
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
781
- messages.push({
782
- role: 'tool',
783
- content: `BLOCKED: Multi-step plan active. Use subagent for each step. Example: subagent({task:"Create file X", existingFiles:"Y.ts"}). After subagent completes, mark step done: plan({action:"done", step:N})`,
784
- tool_call_id: blockId,
785
- });
786
- }
787
- if (!quiet) {
788
- renderer?.writeWarning(` [blocked ${directWrites.length} direct writes — use subagent for multi-step plans]`);
789
- }
790
- }
791
- // Allow bash for verification (tsc --noEmit, npm run build) — don't block these
792
- }
793
- // ESCALATION: block write_file when creating N+ files without subagent (no plan case)
794
- if (directWriteStreakMax < Infinity && !hasMultiStepPlan) {
795
- const hasWrite = validCalls.some(c => c.parsed.name === 'write_file');
796
- const hasSubagentNow = validCalls.some(c => c.parsed.name === 'subagent');
797
- if (hasSubagentNow) {
798
- directWriteStreak = 0;
799
- }
800
- else if (hasWrite) {
801
- const writeCount = validCalls.filter(c => c.parsed.name === 'write_file').length;
802
- directWriteStreak += writeCount;
803
- if (directWriteStreak >= directWriteStreakMax) {
804
- const directWrites = validCalls.filter(c => c.parsed.name === 'write_file');
805
- for (const wc of directWrites) {
806
- const idx = validCalls.indexOf(wc);
807
- if (idx !== -1)
808
- validCalls.splice(idx, 1);
809
- }
810
- if (directWrites.length > 0) {
811
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
812
- messages.push({
813
- role: 'tool',
814
- content: `BLOCKED: You're creating file #${directWriteStreak} without subagent. For 3+ new files, use subagent for each one. Example: subagent("Create file X. Context: Y already exists.")`,
815
- tool_call_id: blockId,
816
- });
817
- }
818
- if (!quiet) {
819
- renderer?.writeWarning(` [blocked ${directWrites.length} writes: ${directWriteStreak} files without subagent — use subagent for multi-file projects]`);
820
- }
821
- }
822
- }
823
- }
824
- // Блокировка нескольких записей файлов за один ответ — максимум одна
825
- const writeCalls = validCalls.filter(c => c.parsed.name === 'write_file' || c.parsed.name === 'edit_file');
826
- if (writeCalls.length > 1) {
827
- // Keep only the first write call, block the rest
828
- const blocked = writeCalls.slice(1);
829
- validCalls.splice(validCalls.indexOf(blocked[0]), blocked.length);
830
- for (const bc of blocked) {
831
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
832
- messages.push({
833
- role: 'tool',
834
- content: `BLOCKED: Only one file write per response is allowed. You wrote ${writeCalls.length} files at once. Complete the first file, then write the next one.`,
835
- tool_call_id: blockId,
836
- });
837
- }
838
- if (!quiet) {
839
- renderer?.writeWarning(` [blocked ${blocked.length} extra file writes — one at a time]`);
840
- }
841
- }
765
+ writeEnforcer.enforcePlan(validCalls, messages, planEnforcement, quiet, renderer);
766
+ writeEnforcer.enforceWriteStreak(validCalls, messages, directWriteStreakMax, quiet, renderer);
767
+ writeEnforcer.enforceSingleWrite(validCalls, messages, quiet, renderer);
842
768
  // ─── Исполнение tool calls ──────────────────────────
843
769
  for (const call of validCalls) {
844
770
  const { parsed, cmd } = call;
845
- const isBash = parsed.name === 'bash';
771
+ const isBash = parsed.name === "bash";
846
772
  const preview = isBash
847
- ? (cmd.length > 200 ? cmd.slice(0, 200) + '...' : cmd)
773
+ ? cmd.length > 200
774
+ ? cmd.slice(0, 200) + "..."
775
+ : cmd
848
776
  : JSON.stringify(parsed.arguments);
849
- const toolId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
777
+ const toolId = generateToolCallId();
850
778
  logToolCall(parsed.name, preview);
851
779
  if (!quiet) {
852
780
  renderer?.writeToolHeader(parsed.name);
853
781
  renderer?.writeToolPreview(parsed.name, preview);
854
782
  }
855
- const hookCmd = isBash ? cmd : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
783
+ const hookCmd = isBash
784
+ ? cmd
785
+ : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
856
786
  const preHook = runPreToolUseHooks(process.cwd(), parsed.name, hookCmd);
857
787
  if (!preHook.continue) {
858
788
  if (!quiet) {
859
- renderer?.writeBracketTag('hook blocked', preHook.message || '');
789
+ renderer?.writeBracketTag("hook blocked", preHook.message || "");
860
790
  }
861
- messages.push({ role: 'tool', content: `Hook blocked: ${preHook.message || ''}`, tool_call_id: toolId });
791
+ messages.push({
792
+ role: "tool",
793
+ content: `Hook blocked: ${preHook.message || ""}`,
794
+ tool_call_id: toolId,
795
+ });
862
796
  continue;
863
797
  }
864
798
  if (pendingSyntaxError && !isBash) {
865
- const isWriteOp = parsed.name === 'write_file' || parsed.name === 'edit_file';
866
- const newPath = String(parsed.arguments.path ?? '');
799
+ const isWriteOp = parsed.name === "write_file" || parsed.name === "edit_file";
800
+ const newPath = String(parsed.arguments.path ?? "");
867
801
  // Allow editing config files (tsconfig.json, package.json) since they
868
802
  // can fix syntax errors (e.g. missing jsx flag in tsconfig)
869
- const isConfig = newPath === 'tsconfig.json' || newPath === 'package.json';
803
+ const isConfig = newPath === "tsconfig.json" || newPath === "package.json";
870
804
  if (isWriteOp && newPath !== pendingSyntaxError && !isConfig) {
871
805
  const blocked = `BLOCKED: You have a syntax error in ${pendingSyntaxError}. Fix it first before writing new files. If the error is about jsx, module resolution, or missing types — edit tsconfig.json (it is exempt from this block).`;
872
806
  if (!quiet) {
873
807
  renderer?.writeError(` [blocked: fix syntax error in ${pendingSyntaxError} first]`);
874
808
  }
875
- messages.push({ role: 'tool', content: blocked, tool_call_id: toolId });
809
+ messages.push({
810
+ role: "tool",
811
+ content: blocked,
812
+ tool_call_id: toolId,
813
+ });
876
814
  continue;
877
815
  }
878
816
  }
879
- let toolOutput = '';
817
+ let toolOutput = "";
880
818
  let toolExitCode = 0;
881
819
  const toolStart = performance.now();
882
820
  // ─── Субагент: изолированный контекст для задачи ──
883
- if (parsed.name === 'subagent') {
884
- const task = String(parsed.arguments.task || '');
821
+ if (parsed.name === "subagent") {
822
+ const task = String(parsed.arguments.task || "");
885
823
  if (!task) {
886
- toolOutput = 'Error: task is required';
824
+ toolOutput = "Error: task is required";
887
825
  toolExitCode = 1;
888
826
  }
889
827
  else {
890
828
  // Авто-пометка следующего pending шага как in_progress
891
829
  const nextPending = getNextPendingStep();
892
830
  if (nextPending) {
893
- updatePlanStep(nextPending.id, 'in_progress');
831
+ updatePlanStep(nextPending.id, "in_progress");
894
832
  }
895
833
  // Phase 2.3: Авто-подстановка полного пути в задачу subagent
896
834
  let finalTask = task;
897
835
  if (nextPending) {
898
836
  const expectedDir = extractDirFromStepTitle(nextPending.title);
899
- if (expectedDir && !task.includes(expectedDir + '/')) {
837
+ if (expectedDir && !task.includes(expectedDir + "/")) {
900
838
  finalTask = `${task} (directory: ${expectedDir}/)`;
901
- logInfo('AGENT', `injected dir path into subagent task: "${expectedDir}"`);
839
+ logInfo("AGENT", `injected dir path into subagent task: "${expectedDir}"`);
902
840
  }
903
841
  }
904
842
  const autoContext = buildSubagentContext(process.cwd());
905
- logInfo('AGENT', `spawning subagent | task: "${finalTask.slice(0, 150)}"`);
843
+ logInfo("AGENT", `spawning subagent | task: "${finalTask.slice(0, 150)}"`);
906
844
  // Показываем спиннер на основном рендерере пока субагент работает
907
845
  if (!quiet) {
908
- const taskPreview = finalTask.length > 60 ? finalTask.slice(0, 57) + '...' : finalTask;
846
+ const taskPreview = finalTask.length > 60
847
+ ? finalTask.slice(0, 57) + "..."
848
+ : finalTask;
909
849
  renderer?.writeSpinner(` [subagent] ${taskPreview}`);
910
850
  }
911
851
  const subRenderer = new SubagentRenderer();
912
852
  try {
913
853
  const childResult = await runIsolatedLoop(provider, registry, finalTask, autoContext, process.cwd(), quiet, subRenderer, toolExecutor, modeConfig.subagentMode, llmCfg.toolModel);
914
854
  toolOutput = childResult.summary;
915
- logInfo('AGENT', `subagent returned | files: ${childResult.createdFiles.length} | turns: ${childResult.turns} | errors: ${childResult.errors.length}`);
855
+ logInfo("AGENT", `subagent returned | files: ${childResult.createdFiles.length} | turns: ${childResult.turns} | errors: ${childResult.errors.length}`);
916
856
  if (childResult.createdFiles.length > 0) {
917
857
  for (const f of childResult.createdFiles) {
918
858
  if (!createdFiles.includes(f))
@@ -923,26 +863,30 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
923
863
  if (nextPending && childResult.createdFiles.length > 0) {
924
864
  const expectedDir = extractDirFromStepTitle(nextPending.title);
925
865
  if (expectedDir) {
926
- const wrongDirFiles = childResult.createdFiles.filter(f => !f.startsWith(expectedDir + '/') && !f.startsWith(expectedDir + '\\'));
866
+ const wrongDirFiles = childResult.createdFiles.filter((f) => !f.startsWith(expectedDir + "/") &&
867
+ !f.startsWith(expectedDir + "\\"));
927
868
  if (wrongDirFiles.length > 0) {
928
- const correction = `⚠ Files created in wrong directory: ${wrongDirFiles.join(', ')}. ` +
869
+ const correction = `⚠ Files created in wrong directory: ${wrongDirFiles.join(", ")}. ` +
929
870
  `They should be in ${expectedDir}/. Use write_file with full path: ${expectedDir}/${wrongDirFiles[0]}`;
930
- toolOutput += '\n\n' + correction;
871
+ toolOutput += "\n\n" + correction;
931
872
  if (!quiet) {
932
- renderer?.writeWarning(` [subagent created files in wrong directory: ${wrongDirFiles.join(', ')}]`);
873
+ renderer?.writeWarning(` [subagent created files in wrong directory: ${wrongDirFiles.join(", ")}]`);
933
874
  }
934
875
  }
935
876
  }
936
877
  }
937
878
  const buf = subRenderer.getBuffer();
938
879
  if (!quiet && renderer?.writeToolBuffer) {
939
- renderer.writeToolBuffer({ lines: buf.lines, toolCalls: buf.toolCalls });
880
+ renderer.writeToolBuffer({
881
+ lines: buf.lines,
882
+ toolCalls: buf.toolCalls,
883
+ });
940
884
  }
941
885
  }
942
886
  catch (e) {
943
887
  toolOutput = `Subagent error: ${e.message}`;
944
888
  toolExitCode = 1;
945
- logError('AGENT', `subagent error: ${e.message}`);
889
+ logError("AGENT", `subagent error: ${e.message}`);
946
890
  }
947
891
  }
948
892
  const subTime = ((performance.now() - toolStart) / 1000).toFixed(1);
@@ -956,12 +900,13 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
956
900
  const mcpCmd = parseMCPCommand(cmd);
957
901
  if (mcpCmd) {
958
902
  if (!quiet) {
959
- renderer?.writeBracketTag('mcp', `${mcpCmd.server}/${mcpCmd.tool}`);
903
+ renderer?.writeBracketTag("mcp", `${mcpCmd.server}/${mcpCmd.tool}`);
960
904
  }
961
905
  toolOutput = callMCPTool(process.cwd(), mcpCmd.server, mcpCmd.tool, mcpCmd.args);
962
906
  }
963
907
  else {
964
- toolOutput = 'Invalid MCP command. Format: mcp <server> <tool> [args_json]';
908
+ toolOutput =
909
+ "Invalid MCP command. Format: mcp <server> <tool> [args_json]";
965
910
  toolExitCode = 1;
966
911
  }
967
912
  // ─── Все инструменты через ToolExecutor ──────────────
@@ -972,19 +917,25 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
972
917
  let executorArgs = parsed.arguments;
973
918
  // Bash-команды
974
919
  if (isBash) {
975
- executorName = 'bash';
920
+ executorName = "bash";
976
921
  executorArgs = { cmd, timeout: parsed.arguments?.timeout ?? 120 };
977
922
  }
978
- const hookCmd = isBash ? cmd : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
979
- const executorCall = { id: toolId, name: executorName, arguments: executorArgs };
923
+ const hookCmd = isBash
924
+ ? cmd
925
+ : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
926
+ const executorCall = {
927
+ id: toolId,
928
+ name: executorName,
929
+ arguments: executorArgs,
930
+ };
980
931
  const toolCtx = { cwd: process.cwd(), renderer, quiet };
981
932
  const { result, elapsed } = await toolExecutor.execute(executorCall, toolCtx, hookCmd);
982
- toolOutput = result.stdout || result.stderr || '(no output)';
933
+ toolOutput = result.stdout || result.stderr || "(no output)";
983
934
  toolExitCode = result.exitCode;
984
935
  // Обработка question/approve: приостанавливаем цикл
985
936
  if (result.questionPending) {
986
937
  messages.push({
987
- role: 'tool',
938
+ role: "tool",
988
939
  content: toolOutput,
989
940
  tool_call_id: toolId,
990
941
  });
@@ -995,76 +946,99 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
995
946
  }
996
947
  }
997
948
  const toolTime = ((performance.now() - toolStart) / 1000).toFixed(1);
998
- logToolCall(parsed.name, preview, toolExitCode !== 0 ? `exit:${toolExitCode} ${toolOutput.slice(0, 100)}` : `ok (${toolTime}s)`);
949
+ logToolCall(parsed.name, preview, toolExitCode !== 0
950
+ ? `exit:${toolExitCode} ${toolOutput.slice(0, 100)}`
951
+ : `ok (${toolTime}s)`);
999
952
  if (toolExitCode !== 0) {
1000
953
  const errText = toolOutput;
1001
954
  if (!quiet) {
1002
- for (const line of errText.split('\n').slice(0, 5)) {
955
+ for (const line of errText.split("\n").slice(0, 5)) {
1003
956
  renderer?.writeError(` ${line}`);
1004
957
  }
1005
958
  }
1006
- autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : '', '');
959
+ autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : "", "");
1007
960
  }
1008
961
  runPostToolUseHooks(process.cwd(), parsed.name, hookCmd, toolOutput, toolExitCode);
1009
962
  // Track sequential read_file calls for consolidation
1010
- if (readConsolidation && !isBash && parsed.name === 'read_file' && toolExitCode === 0) {
1011
- const filePath = String(parsed.arguments.path ?? '');
963
+ if (readConsolidation &&
964
+ !isBash &&
965
+ parsed.name === "read_file" &&
966
+ toolExitCode === 0) {
967
+ const filePath = String(parsed.arguments.path ?? "");
1012
968
  const offset = Math.max(1, Number(parsed.arguments.offset ?? 1));
1013
969
  const limit = Math.max(1, Number(parsed.arguments.limit ?? 500));
1014
- if (lastReadFile && lastReadFile.path === filePath && offset === lastReadFile.offset + lastReadFile.limit) {
970
+ if (lastReadFile &&
971
+ lastReadFile.path === filePath &&
972
+ offset === lastReadFile.offset + lastReadFile.limit) {
1015
973
  // Sequential read — merge with previous result
1016
- lastReadFile.content += '\n' + toolOutput;
974
+ lastReadFile.content += "\n" + toolOutput;
1017
975
  lastReadFile.offset = offset;
1018
976
  lastReadFile.limit = limit;
1019
977
  // Replace the last tool message content with merged content
1020
978
  for (let i = messages.length - 1; i >= 0; i--) {
1021
- if (messages[i].role === 'tool' && messages[i].tool_call_id === lastReadFile.toolId) {
979
+ if (messages[i].role === "tool" &&
980
+ messages[i].tool_call_id === lastReadFile.toolId) {
1022
981
  messages[i].content = lastReadFile.content;
1023
982
  break;
1024
983
  }
1025
984
  }
1026
985
  // Remove the trailing "Continue." nudge + the new tool result that would be pushed
1027
- while (messages.length > 0 && messages[messages.length - 1].role === 'user' && messages[messages.length - 1].content === 'Continue.') {
986
+ while (messages.length > 0 &&
987
+ messages[messages.length - 1].role === "user" &&
988
+ messages[messages.length - 1].content === "Continue.") {
1028
989
  messages.pop();
1029
990
  }
1030
991
  if (!quiet) {
1031
- renderer?.writeBracketTag('consolidate', `merged read_file chunks for ${filePath}`);
992
+ renderer?.writeBracketTag("consolidate", `merged read_file chunks for ${filePath}`);
1032
993
  }
1033
994
  continue; // skip pushing this result separately
1034
995
  }
1035
996
  else {
1036
997
  // New file or non-sequential — start fresh
1037
- lastReadFile = { path: filePath, offset, limit, content: toolOutput, toolId };
998
+ lastReadFile = {
999
+ path: filePath,
1000
+ offset,
1001
+ limit,
1002
+ content: toolOutput,
1003
+ toolId,
1004
+ };
1038
1005
  }
1039
1006
  }
1040
- else if (!isBash && parsed.name !== 'read_file') {
1007
+ else if (!isBash && parsed.name !== "read_file") {
1041
1008
  lastReadFile = null;
1042
1009
  }
1043
1010
  // ─── Проверка синтаксиса после записи файлов ────────
1044
1011
  let hasSyntaxError = false;
1045
1012
  if (toolExitCode === 0) {
1046
1013
  const isWriteOp = isBash
1047
- ? (cmd.match(/Set-Content|Add-Content/))
1048
- : (parsed.name === 'write_file' || parsed.name === 'edit_file');
1014
+ ? cmd.match(/Set-Content|Add-Content/)
1015
+ : parsed.name === "write_file" || parsed.name === "edit_file";
1049
1016
  if (isWriteOp) {
1050
1017
  const rawPath = isBash
1051
- ? cmd.match(/-Path\s+['"]?([^'"]+\.(?:ts|tsx|js|jsx|json))['"]?/i)?.[1]?.replace(/\\/g, '/')
1052
- : String(parsed.arguments.path ?? '');
1018
+ ? cmd
1019
+ .match(/-Path\s+['"]?([^'"]+\.(?:ts|tsx|js|jsx|json))['"]?/i)?.[1]
1020
+ ?.replace(/\\/g, "/")
1021
+ : String(parsed.arguments.path ?? "");
1053
1022
  if (rawPath && /\.(ts|tsx|js|jsx|json)$/.test(rawPath)) {
1054
- const { isAbsolute, relative } = await import('node:path');
1055
- const filePath = isAbsolute(rawPath) ? relative(process.cwd(), rawPath).replace(/\\/g, '/') : rawPath;
1023
+ const filePath = isAbsolute(rawPath)
1024
+ ? relative(process.cwd(), rawPath).replace(/\\/g, "/")
1025
+ : rawPath;
1056
1026
  const syntaxResult = checkFileSyntax(filePath, process.cwd());
1057
- if (syntaxResult !== 'OK') {
1058
- const errorLines = syntaxResult.split('\n').filter(l => l.trim());
1027
+ if (syntaxResult !== "OK") {
1028
+ const errorLines = syntaxResult
1029
+ .split("\n")
1030
+ .filter((l) => l.trim());
1059
1031
  const shown = errorLines.slice(0, 3);
1060
- const extra = errorLines.length > 3 ? ` (+${errorLines.length - 3} more)` : '';
1032
+ const extra = errorLines.length > 3
1033
+ ? ` (+${errorLines.length - 3} more)`
1034
+ : "";
1061
1035
  if (!quiet) {
1062
- renderer?.writeBracketTag('syntax', `${shown.join('; ')}${extra}`);
1036
+ renderer?.writeBracketTag("syntax", `${shown.join("; ")}${extra}`);
1063
1037
  }
1064
1038
  const fullPath = join(process.cwd(), filePath);
1065
- let contextLines = '';
1039
+ let contextLines = "";
1066
1040
  try {
1067
- const fileContent = readFileSync(fullPath, 'utf-8').split('\n');
1041
+ const fileContent = readFileSync(fullPath, "utf-8").split("\n");
1068
1042
  for (const errLine of shown) {
1069
1043
  const lineMatch = errLine.match(/\((\d+),\d+\)/);
1070
1044
  if (lineMatch) {
@@ -1074,21 +1048,23 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1074
1048
  const end = Math.min(fileContent.length, lineNum + 1);
1075
1049
  contextLines += `\n--- Line ${lineNum} ---\n`;
1076
1050
  for (let i = start; i < end; i++) {
1077
- const marker = i === lineNum - 1 ? ' >> ' : ' ';
1051
+ const marker = i === lineNum - 1 ? " >> " : " ";
1078
1052
  contextLines += `${marker}${i + 1}: ${fileContent[i]}\n`;
1079
1053
  }
1080
1054
  }
1081
1055
  }
1082
1056
  }
1083
1057
  }
1084
- catch { /* ignore read errors */ }
1058
+ catch {
1059
+ /* ignore read errors */
1060
+ }
1085
1061
  toolOutput = `SYNTAX ERROR in ${filePath}:\n${syntaxResult}${contextLines}`;
1086
1062
  // Detect config-related TS errors (missing jsx, module issues, etc.)
1087
- const isConfigError = syntaxResult.includes('TS17004')
1088
- || syntaxResult.includes('TS2307')
1089
- || syntaxResult.includes('Cannot find module')
1090
- || syntaxResult.includes('jsx')
1091
- || syntaxResult.includes('moduleResolution');
1063
+ const isConfigError = syntaxResult.includes("TS17004") ||
1064
+ syntaxResult.includes("TS2307") ||
1065
+ syntaxResult.includes("Cannot find module") ||
1066
+ syntaxResult.includes("jsx") ||
1067
+ syntaxResult.includes("moduleResolution");
1092
1068
  if (isConfigError) {
1093
1069
  toolOutput += `\n\nThis looks like a tsconfig.json configuration issue, NOT a code syntax error.\nCheck tsconfig.json — you may need to add "jsx": "react-jsx" or fix module settings.\nDo NOT modify the source code to work around config errors. Edit tsconfig.json instead.`;
1094
1070
  }
@@ -1100,16 +1076,18 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1100
1076
  }
1101
1077
  else {
1102
1078
  if (pendingSyntaxError === filePath) {
1103
- pendingSyntaxError = '';
1079
+ pendingSyntaxError = "";
1104
1080
  }
1105
1081
  }
1106
1082
  // Автодетекция стиля кодирования из содержимого файла
1107
1083
  if (toolExitCode === 0 && filePath) {
1108
1084
  try {
1109
- const fullContent = readFileSync(join(process.cwd(), filePath), 'utf-8');
1085
+ const fullContent = readFileSync(join(process.cwd(), filePath), "utf-8");
1110
1086
  collectCodingStyle(filePath, fullContent);
1111
1087
  }
1112
- catch { /* ignore */ }
1088
+ catch {
1089
+ /* ignore */
1090
+ }
1113
1091
  // Track created/modified files for completion reporting
1114
1092
  if (!hasSyntaxError && !createdFiles.includes(filePath)) {
1115
1093
  createdFiles.push(filePath);
@@ -1119,73 +1097,18 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1119
1097
  }
1120
1098
  }
1121
1099
  messages.push({
1122
- role: 'tool',
1123
- content: truncateToolOutput(parsed.name, toolOutput, toolExitCode),
1100
+ role: "tool",
1101
+ content: truncateOutput(parsed.name, toolOutput, toolExitCode),
1124
1102
  tool_call_id: toolId,
1125
1103
  });
1126
1104
  // ─── Error escalation + specialist auto-delegation ──────────
1127
1105
  if (toolExitCode !== 0 && toolOutput) {
1128
- totalToolErrors++;
1129
- const errKey = `${parsed.name}::${toolOutput.slice(0, 100).replace(/[A-Z]:\\[^\s"']+/gi, '<PATH>').replace(/\/[^\s"']+/g, '<PATH>')}`;
1130
- const prevCount = consecutiveToolErrors.get(errKey) ?? 0;
1131
- consecutiveToolErrors.set(errKey, prevCount + 1);
1132
- const errorCount = prevCount + 1;
1133
- // ── Specialist: авто-делегация при повторяющихся ошибках ──
1134
- const specialistCfg = getSpecialistConfig(config);
1135
- if (specialistCfg.enabled &&
1136
- errorCount === specialistCfg.triggerThreshold &&
1137
- specialistSpawnedFor !== errKey &&
1138
- !isTrivialError(parsed.name, toolOutput) &&
1139
- toolOutput.length >= specialistCfg.minErrorLength) {
1140
- specialistSpawnedFor = errKey;
1141
- const spawned = await spawnSpecialist({
1142
- provider, registry, toolExecutor, modeConfig, llmCfg, specialistCfg,
1143
- quiet, renderer, messages, toolId, createdFiles,
1144
- toolName: parsed.name, toolArgs: parsed.arguments,
1145
- errorOutput: toolOutput, errorCount, enrichedPrompt,
1146
- });
1147
- if (spawned)
1148
- continue;
1149
- }
1150
- // ── Escalation: hint / strategy switch / block ──
1151
- if (errorCount > specialistCfg.triggerThreshold) {
1152
- // Веб-специфичные подсказки
1153
- let webHint = '';
1154
- if (parsed.name === 'web_browse' && String(parsed.arguments?.action) === 'evaluate') {
1155
- webHint = ' For web scraping: use web_browse(action:"discover") to find real CSS selectors, or web_browse(action:"get_links") to see all links, or web_browse(action:"extract", format:"html") to inspect the page structure.';
1156
- }
1157
- else if (parsed.name === 'web_browse' && String(parsed.arguments?.action) === 'navigate' && toolOutput.includes('Status: 40')) {
1158
- webHint = ' Page returned HTTP error. Try navigating to the site homepage first, then use web_browse(action:"get_links") or web_browse(action:"discover") to explore available categories and sections instead of guessing URLs.';
1159
- }
1160
- const msg = errorCount === specialistCfg.triggerThreshold + 1
1161
- ? `[System: Tool "${parsed.name}" failed with the SAME error ${errorCount} times. Read the error message carefully. The input is likely wrong — try a completely different approach, or use subagent for a fresh context.]${webHint}`
1162
- : `[System: Tool "${parsed.name}" failed ${errorCount} times with the same error. STOP. Do NOT retry the same call. Use subagent(task="...") for a fresh context, or tell the user you're stuck.]${webHint}`;
1163
- messages.push({ role: 'user', content: msg });
1164
- if (!quiet)
1165
- renderer?.writeWarning(` [error escalation: ${parsed.name} failed ${errorCount} times]`);
1166
- }
1167
- if (totalToolErrors >= 5 && errorStrategy === 'normal') {
1168
- errorStrategy = 'subagent-hint';
1169
- messages.push({ role: 'user', content: `[System: ${totalToolErrors} tool errors. Use subagent(task="...") for a fresh context.]` });
1170
- if (!quiet)
1171
- renderer?.writeWarning(` [strategy: subagent-hint after ${totalToolErrors} errors]`);
1172
- }
1173
- if (errorCount >= 4) {
1174
- const blockId = `call-${Date.now()}-${step}-${Math.random().toString(36).slice(2, 6)}`;
1175
- messages.push({ role: 'tool', content: `BLOCKED: Tool "${parsed.name}" failed ${errorCount} times. Temporarily blocked.`, tool_call_id: blockId });
1176
- const idx = validCalls.indexOf(call);
1177
- if (idx !== -1)
1178
- validCalls.splice(idx, 1);
1179
- if (!quiet)
1180
- renderer?.writeWarning(` [blocked ${parsed.name}: too many repeated errors]`);
1181
- }
1106
+ const action = await errorEscalator.handleToolError(parsed.name, toolOutput, parsed.arguments, toolId, messages, createdFiles, enrichedPrompt, renderer, quiet, provider, registry, toolExecutor, modeConfig, llmCfg);
1107
+ if (action === "continue")
1108
+ continue;
1182
1109
  }
1183
1110
  else {
1184
- for (const [key] of consecutiveToolErrors) {
1185
- if (key.startsWith(parsed.name + '::'))
1186
- consecutiveToolErrors.delete(key);
1187
- }
1188
- specialistSpawnedFor = null;
1111
+ errorEscalator.resetForTool(parsed.name);
1189
1112
  }
1190
1113
  // НЕ добавляем "Continue." после каждого tool-вызова — это нарушает
1191
1114
  // структуру диалога. Bounded nudge (строка 418) уже покрывает
@@ -1196,12 +1119,12 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
1196
1119
  }
1197
1120
  }
1198
1121
  if (aborted) {
1199
- renderer?.writeWarning('Agent stopped (double ESC)');
1122
+ renderer?.writeWarning("Agent stopped (double ESC)");
1200
1123
  }
1201
- logInfo('LOOP', `runAgentLoop done: ${messages.length} total messages, aborted=${aborted}`);
1124
+ logInfo("LOOP", `runAgentLoop done: ${messages.length} total messages, aborted=${aborted}`);
1202
1125
  // Agent:done event for notify plugin
1203
- if (pluginManager.isActive('notify')) {
1204
- pluginManager.emit('agent:done').catch(() => { });
1126
+ if (pluginManager.isActive("notify")) {
1127
+ pluginManager.emit("agent:done").catch(() => { });
1205
1128
  }
1206
1129
  return { messages };
1207
1130
  }