micro-models-agent 2.11.0 → 2.12.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 (347) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/js/tabs.js +22 -3
  3. package/dist/cli/cache-line.js +1 -0
  4. package/dist/cli/command-suggest.js +5 -4
  5. package/dist/cli/commands.js +231 -122
  6. package/dist/cli/completer.js +132 -31
  7. package/dist/cli/index.js +1 -0
  8. package/dist/cli/json-payload.js +4 -4
  9. package/dist/cli/main.js +166 -61
  10. package/dist/cli/onboarding-commands.js +126 -0
  11. package/dist/cli/plugin-commands.js +2 -1
  12. package/dist/cli/relaunch.js +16 -13
  13. package/dist/cli/repl-commands-lsp.js +75 -0
  14. package/dist/cli/repl-commands-skills.js +105 -0
  15. package/dist/cli/repl-commands.js +589 -266
  16. package/dist/cli/repl.js +436 -177
  17. package/dist/cli/run-result.js +5 -6
  18. package/dist/cli/security-commands.js +37 -28
  19. package/dist/cli/setup-order.js +4 -55
  20. package/dist/cli/setup-prompt.js +3 -2
  21. package/dist/cli/setup.js +27 -107
  22. package/dist/cli/web-command.js +98 -0
  23. package/dist/config/budget.js +5 -5
  24. package/dist/config/config.js +103 -96
  25. package/dist/config/defaults.js +38 -8
  26. package/dist/config/domains.js +24 -27
  27. package/dist/config/env.js +46 -0
  28. package/dist/config/experts.js +2 -0
  29. package/dist/config/index.js +1 -0
  30. package/dist/config/paths.js +56 -0
  31. package/dist/config/redact.js +44 -0
  32. package/dist/config/registry-extra.js +334 -0
  33. package/dist/config/registry.js +584 -0
  34. package/dist/config/security.js +35 -36
  35. package/dist/config/timeouts.js +74 -0
  36. package/dist/config/utils.js +16 -15
  37. package/dist/core/agent/artifact-progress.js +59 -0
  38. package/dist/core/agent/artifact-resolve.js +79 -0
  39. package/dist/core/agent/audit-gate.js +7 -3
  40. package/dist/core/agent/compaction.js +2 -2
  41. package/dist/core/agent/constants.js +33 -4
  42. package/dist/core/agent/context-log.js +454 -0
  43. package/dist/core/agent/context-renderer.js +5 -0
  44. package/dist/core/agent/critic-gate.js +132 -0
  45. package/dist/core/agent/hallucination-gate.js +148 -38
  46. package/dist/core/agent/loop-state.js +15 -0
  47. package/dist/core/agent/prefix-monitor.js +1 -1
  48. package/dist/core/agent/reasoning-resolver.js +10 -1
  49. package/dist/core/agent/token-tracker.js +7 -4
  50. package/dist/core/agent/tool-batch.js +93 -18
  51. package/dist/core/agent/tool-images.js +18 -0
  52. package/dist/core/agent/tool-output.js +65 -11
  53. package/dist/core/agent/tool-pairing.js +60 -0
  54. package/dist/core/agent-moe.js +25 -24
  55. package/dist/core/agent.js +429 -108
  56. package/dist/core/bootstrap/agent-phase.js +66 -0
  57. package/dist/core/bootstrap/config-phase.js +86 -0
  58. package/dist/core/bootstrap/modules-phase.js +269 -0
  59. package/dist/core/bootstrap/provider-phase.js +183 -0
  60. package/dist/core/bootstrap/session-phase.js +204 -0
  61. package/dist/core/bootstrap/state.js +94 -0
  62. package/dist/core/bootstrap/workspace-phase.js +115 -0
  63. package/dist/core/bootstrap.js +92 -660
  64. package/dist/core/crash-handler.js +17 -7
  65. package/dist/core/environment.js +37 -12
  66. package/dist/core/host-plan.js +43 -0
  67. package/dist/core/host.js +23 -0
  68. package/dist/core/index.js +1 -0
  69. package/dist/core/prompt-builder.js +17 -1
  70. package/dist/core/prompt-overflow.js +42 -43
  71. package/dist/core/session-logger.js +122 -30
  72. package/dist/core/system-prompt.js +117 -0
  73. package/dist/core/version.js +6 -4
  74. package/dist/core/web-host.js +34 -0
  75. package/dist/core/workspace.js +19 -15
  76. package/dist/i18n/index.js +37 -3
  77. package/dist/index.js +7 -3
  78. package/dist/llm/image-utils.js +55 -64
  79. package/dist/llm/llm-errors.js +9 -0
  80. package/dist/llm/model-loader.js +10 -32
  81. package/dist/llm/models-dev.js +121 -0
  82. package/dist/llm/openai-compat.js +146 -73
  83. package/dist/llm/orchestrator.js +33 -11
  84. package/dist/llm/provider-budget.js +2 -2
  85. package/dist/llm/provider-context.js +233 -0
  86. package/dist/llm/provider.js +1 -1
  87. package/dist/llm/response.js +5 -0
  88. package/dist/llm/token-counter.js +27 -11
  89. package/dist/logger/app-logger.js +48 -22
  90. package/dist/logger/file-log.js +28 -19
  91. package/dist/logger/index.js +1 -0
  92. package/dist/main.js +273 -47
  93. package/dist/migration/backup.js +14 -3
  94. package/dist/migration/detect.js +12 -3
  95. package/dist/migration/index.js +1 -0
  96. package/dist/modules/artifacts/store.js +9 -8
  97. package/dist/modules/browser/actions.js +6 -0
  98. package/dist/modules/browser/bridge-client.js +14 -12
  99. package/dist/modules/browser/bridge-path.js +4 -4
  100. package/dist/modules/browser/console-buffer.js +37 -0
  101. package/dist/modules/browser/cookie-store.js +5 -0
  102. package/dist/modules/browser/driver-types.js +1 -0
  103. package/dist/modules/browser/driver.js +19 -11
  104. package/dist/modules/browser/index.js +3 -1
  105. package/dist/modules/browser/module.js +2 -1
  106. package/dist/modules/browser/session.js +10 -30
  107. package/dist/modules/browser/snapshot.js +19 -13
  108. package/dist/modules/browser/types.js +1 -0
  109. package/dist/modules/certification/cli.js +28 -20
  110. package/dist/modules/certification/fact-checker.js +9 -1
  111. package/dist/modules/certification/loader.js +9 -2
  112. package/dist/modules/certification/manifest.js +83 -21
  113. package/dist/modules/certification/runner.js +23 -13
  114. package/dist/modules/certification/scenarios.js +20 -20
  115. package/dist/modules/certification/syntax-scenarios.js +1 -2
  116. package/dist/modules/context/chunk-query.js +11 -9
  117. package/dist/modules/context/fact-extractor.js +37 -39
  118. package/dist/modules/context/index.js +1 -0
  119. package/dist/modules/context/manager.js +106 -80
  120. package/dist/modules/critic/client.js +176 -0
  121. package/dist/modules/critic/evidence.js +81 -0
  122. package/dist/modules/critic/provider.js +45 -0
  123. package/dist/modules/critic/tools.js +78 -0
  124. package/dist/modules/critic/types.js +1 -0
  125. package/dist/modules/execution/audit-runners.js +28 -34
  126. package/dist/modules/execution/auditor.js +53 -49
  127. package/dist/modules/execution/execution-plugin.js +142 -102
  128. package/dist/modules/execution/index.js +1 -1
  129. package/dist/modules/execution/input-from.js +37 -13
  130. package/dist/modules/execution/module.js +368 -191
  131. package/dist/modules/execution/moe-executor.js +53 -26
  132. package/dist/modules/execution/plan-actions.js +320 -202
  133. package/dist/modules/execution/plan-checks.js +35 -0
  134. package/dist/modules/execution/plan-coverage.js +36 -33
  135. package/dist/modules/execution/plan-steps.js +11 -3
  136. package/dist/modules/execution/plan-store.js +83 -17
  137. package/dist/modules/execution/plan-tool.js +89 -35
  138. package/dist/modules/execution/plan-validator.js +30 -10
  139. package/dist/modules/execution/planner.js +22 -13
  140. package/dist/modules/execution/stuck-detector.js +196 -93
  141. package/dist/modules/execution/tracker.js +22 -8
  142. package/dist/modules/execution/verifier.js +69 -48
  143. package/dist/modules/execution/windows-commands.js +12 -12
  144. package/dist/modules/hallucination/artifact-claim.js +93 -0
  145. package/dist/modules/hallucination/confidence.js +13 -11
  146. package/dist/modules/hallucination/consistency.js +10 -2
  147. package/dist/modules/hallucination/detector.js +10 -5
  148. package/dist/modules/hallucination/factual.js +43 -26
  149. package/dist/modules/hallucination/index.js +1 -0
  150. package/dist/modules/hallucination/js-identifiers.js +44 -45
  151. package/dist/modules/hallucination/llm-judge.js +17 -12
  152. package/dist/modules/hallucination/success-claim.js +59 -0
  153. package/dist/modules/indexer/cache.js +52 -4
  154. package/dist/modules/indexer/index.js +1 -0
  155. package/dist/modules/indexer/map-command.js +2 -4
  156. package/dist/modules/indexer/map-select.js +16 -16
  157. package/dist/modules/indexer/module.js +47 -12
  158. package/dist/modules/indexer/project-profile.js +8 -7
  159. package/dist/modules/indexer/symbols.js +15 -15
  160. package/dist/modules/indexer/walker.js +159 -29
  161. package/dist/modules/lsp/check-tool.js +7 -5
  162. package/dist/modules/lsp/client.js +39 -39
  163. package/dist/modules/lsp/command.js +17 -24
  164. package/dist/modules/lsp/config.js +12 -11
  165. package/dist/modules/lsp/index.js +1 -0
  166. package/dist/modules/lsp/module.js +48 -46
  167. package/dist/modules/lsp/probe.js +13 -14
  168. package/dist/modules/lsp/project-root.js +14 -17
  169. package/dist/modules/lsp/startup-check.js +18 -16
  170. package/dist/modules/mcp/client.js +59 -62
  171. package/dist/modules/mcp/index.js +1 -0
  172. package/dist/modules/mcp/module.js +66 -37
  173. package/dist/modules/mcp/registry.js +5 -0
  174. package/dist/modules/memory/index.js +1 -0
  175. package/dist/modules/memory/module.js +12 -8
  176. package/dist/modules/memory/search.js +4 -1
  177. package/dist/modules/memory/store.js +40 -2
  178. package/dist/modules/onboarding/init.js +47 -0
  179. package/dist/modules/onboarding/meet.js +30 -0
  180. package/dist/modules/onboarding/mode.js +30 -0
  181. package/dist/modules/onboarding/prompts.js +43 -0
  182. package/dist/modules/pipelines/engine.js +10 -0
  183. package/dist/modules/pipelines/index.js +1 -0
  184. package/dist/modules/pipelines/parser.js +4 -1
  185. package/dist/modules/pipelines/template.js +4 -0
  186. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  187. package/dist/modules/plugins/builtin/notify.js +2 -0
  188. package/dist/modules/plugins/index.js +1 -0
  189. package/dist/modules/plugins/loader.js +18 -3
  190. package/dist/modules/plugins/manager.js +37 -17
  191. package/dist/modules/pricing/index.js +10 -11
  192. package/dist/modules/pricing/prices.js +12 -12
  193. package/dist/modules/processes/index.js +1 -0
  194. package/dist/modules/processes/kill-tree.js +27 -31
  195. package/dist/modules/processes/registry.js +44 -25
  196. package/dist/modules/processes/runner.js +4 -4
  197. package/dist/modules/providers/capabilities.js +28 -0
  198. package/dist/modules/providers/create.js +3 -4
  199. package/dist/modules/providers/factory.js +7 -7
  200. package/dist/modules/providers/fallback.js +12 -12
  201. package/dist/modules/providers/health.js +4 -4
  202. package/dist/modules/providers/manager.js +50 -19
  203. package/dist/modules/providers/naming.js +15 -0
  204. package/dist/modules/providers/preflight.js +71 -0
  205. package/dist/modules/providers/presets.js +39 -8
  206. package/dist/modules/providers/registry.js +7 -2
  207. package/dist/modules/reasoning/policy.js +8 -6
  208. package/dist/modules/reasoning/probe.js +30 -25
  209. package/dist/modules/security/audit-log.js +19 -36
  210. package/dist/modules/security/audit-notifier.js +35 -75
  211. package/dist/modules/security/command-validator.js +99 -51
  212. package/dist/modules/security/content-scanner.js +8 -8
  213. package/dist/modules/security/data-sanitizer.js +9 -16
  214. package/dist/modules/security/encryption.js +32 -52
  215. package/dist/modules/security/index.js +2 -1
  216. package/dist/modules/security/network-validator.js +120 -43
  217. package/dist/modules/security/path-validator.js +61 -46
  218. package/dist/modules/security/protected-paths.js +73 -0
  219. package/dist/modules/security/rate-limiter.js +17 -49
  220. package/dist/modules/security/security-policies.js +25 -57
  221. package/dist/modules/security/session-encryption.js +32 -71
  222. package/dist/modules/security/session-isolation.js +15 -28
  223. package/dist/modules/session/auto-name.js +63 -0
  224. package/dist/modules/session/export.js +98 -0
  225. package/dist/modules/session/index.js +1 -0
  226. package/dist/modules/session/manager.js +79 -28
  227. package/dist/modules/session/module.js +4 -3
  228. package/dist/modules/session/store.js +57 -20
  229. package/dist/modules/setup/answers.js +35 -0
  230. package/dist/modules/setup/apply.js +79 -0
  231. package/dist/modules/setup/discovery.js +72 -0
  232. package/dist/modules/skills/index.js +1 -0
  233. package/dist/modules/skills/loader.js +9 -1
  234. package/dist/modules/skills/module.js +11 -0
  235. package/dist/modules/updater/changelog-reader.js +15 -16
  236. package/dist/modules/updater/checker.js +9 -4
  237. package/dist/modules/updater/dev-detect.js +7 -6
  238. package/dist/modules/updater/index.js +1 -0
  239. package/dist/modules/updater/module.js +17 -15
  240. package/dist/modules/user-profile/compressor.js +2 -0
  241. package/dist/modules/user-profile/index.js +1 -0
  242. package/dist/modules/user-profile/profile.js +10 -1
  243. package/dist/modules/webui/answer-api.js +29 -0
  244. package/dist/modules/webui/assets.js +50 -0
  245. package/dist/modules/webui/body.js +21 -0
  246. package/dist/modules/webui/commands-settings.js +209 -0
  247. package/dist/modules/webui/commands-shared.js +60 -0
  248. package/dist/modules/webui/commands.js +174 -0
  249. package/dist/modules/webui/config-api.js +134 -0
  250. package/dist/modules/webui/file-api.js +202 -0
  251. package/dist/modules/webui/forward.js +159 -0
  252. package/dist/modules/webui/history-api.js +112 -0
  253. package/dist/modules/webui/host-state.js +60 -0
  254. package/dist/modules/webui/i18n-api.js +9 -0
  255. package/dist/modules/webui/limits.js +6 -0
  256. package/dist/modules/webui/log-forward.js +20 -0
  257. package/dist/modules/webui/memory-commands.js +64 -0
  258. package/dist/modules/webui/model-cache.js +18 -0
  259. package/dist/modules/webui/module.js +148 -0
  260. package/dist/modules/webui/persist.js +24 -0
  261. package/dist/modules/webui/prompt-io.js +97 -0
  262. package/dist/modules/webui/providers-api.js +337 -0
  263. package/dist/modules/webui/providers-view.js +56 -0
  264. package/dist/modules/webui/reasoning-view.js +15 -0
  265. package/dist/modules/webui/run-agent.js +81 -0
  266. package/dist/modules/webui/server.js +284 -0
  267. package/dist/modules/webui/sessions-api.js +178 -0
  268. package/dist/modules/webui/settings-api.js +561 -0
  269. package/dist/modules/webui/setup-api.js +201 -0
  270. package/dist/modules/webui/state.js +74 -0
  271. package/dist/modules/webui/util.js +261 -0
  272. package/dist/output/bus.js +7 -2
  273. package/dist/output/channel.js +80 -28
  274. package/dist/output/format.js +2 -1
  275. package/dist/output/index.js +1 -0
  276. package/dist/output/json-sink.js +5 -3
  277. package/dist/output/machine.js +4 -3
  278. package/dist/output/session-sink.js +4 -4
  279. package/dist/tools/approve.js +4 -0
  280. package/dist/tools/attach-image.js +5 -4
  281. package/dist/tools/auto-fixer.js +26 -44
  282. package/dist/tools/auto-tags.js +83 -0
  283. package/dist/tools/bash.js +193 -116
  284. package/dist/tools/browser.js +76 -31
  285. package/dist/tools/chunk-query.js +14 -6
  286. package/dist/tools/create-dir.js +9 -5
  287. package/dist/tools/delete-file.js +9 -5
  288. package/dist/tools/download-file.js +32 -7
  289. package/dist/tools/edit-file.js +52 -14
  290. package/dist/tools/enable-tools.js +6 -6
  291. package/dist/tools/executor.js +132 -27
  292. package/dist/tools/file-info.js +3 -1
  293. package/dist/tools/filter-tools.js +3 -3
  294. package/dist/tools/glob-tool.js +4 -1
  295. package/dist/tools/grep-tool.js +13 -10
  296. package/dist/tools/hidden-tools-block.js +6 -6
  297. package/dist/tools/index.js +16 -8
  298. package/dist/tools/list-dir.js +4 -2
  299. package/dist/tools/load-skill.js +7 -0
  300. package/dist/tools/mcp-call.js +15 -4
  301. package/dist/tools/meet-done.js +14 -0
  302. package/dist/tools/move-file.js +14 -7
  303. package/dist/tools/path-utils.js +29 -29
  304. package/dist/tools/pipeline-run.js +29 -7
  305. package/dist/tools/preview.js +1 -1
  306. package/dist/tools/process-ids.js +31 -0
  307. package/dist/tools/process-kill.js +9 -16
  308. package/dist/tools/process-list.js +1 -0
  309. package/dist/tools/process-log.js +11 -16
  310. package/dist/tools/process-wait.js +63 -0
  311. package/dist/tools/prompt-io.js +4 -3
  312. package/dist/tools/question.js +172 -25
  313. package/dist/tools/read-file.js +55 -34
  314. package/dist/tools/recall.js +14 -8
  315. package/dist/tools/registry.js +6 -5
  316. package/dist/tools/remember.js +8 -4
  317. package/dist/tools/scope-request.js +11 -6
  318. package/dist/tools/search-history.js +5 -2
  319. package/dist/tools/session-info.js +2 -0
  320. package/dist/tools/set-thinking.js +8 -4
  321. package/dist/tools/subagent.js +59 -39
  322. package/dist/tools/syntax-validator.js +292 -90
  323. package/dist/tools/user-input.js +22 -18
  324. package/dist/tools/web-browse.js +9 -3
  325. package/dist/tools/web-fetch.js +12 -23
  326. package/dist/tools/web-search.js +7 -5
  327. package/dist/tools/write-file.js +45 -15
  328. package/dist/ui/action-log.js +13 -0
  329. package/dist/ui/box.js +4 -3
  330. package/dist/ui/colors.js +2 -0
  331. package/dist/ui/diff.js +6 -5
  332. package/dist/ui/index.js +1 -0
  333. package/dist/ui/line-editor.js +230 -122
  334. package/dist/ui/line-math.js +10 -2
  335. package/dist/ui/md-formatter.js +57 -37
  336. package/dist/ui/output.js +8 -3
  337. package/dist/ui/plan-view.js +21 -10
  338. package/dist/ui/renderer.js +393 -67
  339. package/dist/ui/spinner.js +16 -5
  340. package/dist/ui/table.js +15 -17
  341. package/dist/ui/theme.js +143 -0
  342. package/dist/utils/child-run.js +20 -0
  343. package/dist/utils/error.js +1 -1
  344. package/dist/utils/index.js +2 -0
  345. package/dist/utils/retry.js +2 -2
  346. package/dist/utils/truncate.js +1 -1
  347. package/package.json +1 -1
@@ -17,12 +17,12 @@ function summarizeArgs(args) {
17
17
  }
18
18
  }
19
19
  function looksLikeErrorPaste(text) {
20
- // Error markers.
20
+ // Маркеры ошибок.
21
21
  if (/(?:^|\s)ERROR|Error:|error TS\d+|Transform failed|\[plugin:|SyntaxError|Cannot find|Uncaught|exception/i.test(text)) {
22
22
  return true;
23
23
  }
24
- // Structural signals of pasted compiler/runtime output: source frames
25
- // ("270| }_s(...)"), path:line:col references, and stack-trace lines.
24
+ // Структурные сигналы вставленного вывода компилятора/рантайма: фреймы
25
+ // исходников ("270| }_s(...)"), ссылки path:line:col и строки стектрейсов.
26
26
  if (/(?:^|\n)\s*\d+\s*\|/.test(text))
27
27
  return true;
28
28
  if (/[\w./\\-]+\.[a-z]{1,6}:\d+:\d+/.test(text))
@@ -31,14 +31,15 @@ function looksLikeErrorPaste(text) {
31
31
  return true;
32
32
  return false;
33
33
  }
34
+ /** Извлекает повторы "пробовали и не вышло" (≥2 фейлов одного тула+args). */
34
35
  export function extractTriedAndFailed(messages) {
35
36
  const failures = new Map();
36
37
  for (const msg of messages) {
37
38
  if (msg.role === "tool" && msg.name && msg.success === false) {
38
- // Plugin-blocked calls are guard decisions, not failed attempts: a
39
- // transient guard (or a guard bug) must never be remembered as
40
- // "tried & failed — do NOT repeat" (observed: a false plan-alignment
41
- // block made the model refuse to rewrite its own step's file).
39
+ // Заблокированные плагином вызовы — решения гардов, не попытки: транзиентный
40
+ // гард (или его баг) нельзя запоминать как "пробовали и не вышло — НЕ повторять"
41
+ // (было: ложный plan-alignment блок заставил модель отказываться переписывать
42
+ // файл собственного шага).
42
43
  if (msg.blocked)
43
44
  continue;
44
45
  const key = `${msg.name}:${summarizeArgs(msg.arguments)}`;
@@ -59,6 +60,7 @@ export function extractTriedAndFailed(messages) {
59
60
  }
60
61
  return Array.from(failures.values()).filter((f) => f.count >= 2);
61
62
  }
63
+ /** Менеджер контекста: история, бюджеты, компакция, трекинг задачи/миссии сессии. */
62
64
  export class ContextManager {
63
65
  contextWindow;
64
66
  messages = [];
@@ -68,36 +70,44 @@ export class ContextManager {
68
70
  peakTokens = 0;
69
71
  budget;
70
72
  compactionThreshold;
73
+ /**
74
+ * Минимальная загрузка истории (доля бюджета), нужная интервалу итераций для
75
+ * срабатывания компакции. Интервал — защита от частоты, не триггер: слабо
76
+ * загруженный контекст нельзя уплотнять лишь потому, что прошло N итераций
77
+ * (было: компакция на ~40% бюджета стирала 33-44 полезных хода посреди задачи).
78
+ */
79
+ compactionMinLoad;
71
80
  facts = new FactExtractor();
72
- /** Latest real user instruction (skips <system-summary> blocks). */
81
+ /** Последняя реальная инструкция юзера (мимо <system-summary>). */
73
82
  lastUserTask = "";
74
- /** First real user instruction of the session — survives resetUserTurn(). */
83
+ /** Первая реальная инструкция сессии — переживает resetUserTurn(). */
75
84
  sessionMission = "";
76
- /** Latest user message that looks like a pasted error/log (never a task). */
85
+ /** Последнее сообщение юзера, похожее на вставленную ошибку/лог (не задача). */
77
86
  lastUserFeedback = "";
78
87
  tokenCounter;
79
88
  pendingImageParts = [];
80
89
  toolTokens = 0;
81
- /** Current reasoning level (set by agent loop, included in compaction summary). */
90
+ /** Текущий reasoning-уровень (ставит цикл агента, входит в сводку компакции). */
82
91
  thinkingLevel = null;
83
92
  onCompact = null;
84
93
  /**
85
- * Optional short line appended to the compaction summary so the model knows
86
- * an active plan exists with progress. Wired from bootstrap to the execution
87
- * module — without it, a 9B model re-creates a plan from scratch right after
88
- * compaction (observed: ses_msvuao0h — plan_e07pb2 (1/6) was discarded for a
89
- * fresh plan_xzq9xe the same iteration the old plan was still visible).
94
+ * Опциональная короткая строка в сводку компакции, чтобы модель знала про
95
+ * активный план с прогрессом. Прокидывается из bootstrap в execution-модуль —
96
+ * без неё 9B-модель пересоздаёт план с нуля сразу после компакции (было:
97
+ * ses_msvuao0h — plan_e07pb2 (1/6) выброшен ради свежего plan_xzq9xe в ту же
98
+ * итерацию, когда старый план ещё был виден).
90
99
  */
91
100
  planSummaryProvider = null;
92
101
  constructor(contextWindow, contextBudget, tokenCounter) {
93
102
  this.contextWindow = contextWindow;
94
103
  this.compactionThreshold = contextBudget?.compactionThreshold ?? 0.75;
104
+ this.compactionMinLoad = contextBudget?.compactionMinLoad ?? 0.5;
95
105
  this.budget = this.calculateBudget(contextWindow, contextBudget);
96
106
  this.tokenCounter = tokenCounter ?? null;
97
107
  }
98
108
  /**
99
- * Provide a short "current plan" line for the compaction summary, or null
100
- * when no plan is active. Injected once at bootstrap (not per-turn state).
109
+ * Задаёт короткую строку "текущий план" для сводки компакции, или null если
110
+ * плана нет. Инжектится один раз на bootstrap (не per-turn состояние).
101
111
  */
102
112
  setPlanSummaryProvider(fn) {
103
113
  this.planSummaryProvider = fn;
@@ -109,57 +119,61 @@ export class ContextManager {
109
119
  const history = Math.max(0, window - systemPrompt - responseReserve);
110
120
  return { systemPrompt, responseReserve, history };
111
121
  }
112
- // Fallback for backwards compatibility (tests, subagents without explicit budget)
122
+ // Фолбэк для обратной совместимости (тесты, сабагенты без явного бюджета)
113
123
  return {
114
124
  systemPrompt: Math.floor(window * 0.25),
115
125
  responseReserve: Math.floor(window * 0.12),
116
126
  history: Math.floor(window * 0.63),
117
127
  };
118
128
  }
129
+ /** Копия текущего бюджета. */
119
130
  getBudget() {
120
131
  return { ...this.budget };
121
132
  }
133
+ /** Число выполненных компакций. */
122
134
  getCompactionCount() {
123
135
  return this.compactionCount;
124
136
  }
137
+ /** Итераций с последней компакции. */
125
138
  getIterationsSinceCompaction() {
126
139
  return this.iterationsSinceCompaction;
127
140
  }
128
- /** Files known from compaction summaries — for seeding the hallucination
129
- * checker's known-files set so bare filenames survive context resets. */
141
+ /** Файлы, известные из сводок компакции — сидируют known-files проверки
142
+ * галлюцинаций, чтобы голые имена переживали сбросы контекста. */
130
143
  getKnownFiles() {
131
144
  return this.facts.getKnownFiles();
132
145
  }
146
+ /** Отмечает прошедшую итерацию (счётчик для интервала компакции). */
133
147
  noteIteration() {
134
148
  this.iterationsSinceCompaction++;
135
149
  }
136
150
  /**
137
- * Start a fresh user turn. The 15-iteration compaction interval must not
138
- * leak across user messages — otherwise a turn that ends at iteration 14
139
- * forces a compaction on the very first iteration of the next user message
140
- * (observed: compaction fired on iteration 8 of a new turn, deleting the
141
- * user's freshly-sent task alongside 40+ old turns). Facts are kept.
151
+ * Начинает свежий ход юзера. 15-итерационный интервал компакции не должен
152
+ * течь через сообщения юзера — иначе ход, кончившийся на итерации 14,
153
+ * форсит компакцию на первой же итерации следующего сообщения (было:
154
+ * компакция на итерации 8 нового хода снесла свежеприсланную задачу юзера
155
+ * вместе с 40+ старыми ходами). Факты сохраняются.
142
156
  */
143
157
  resetUserTurn() {
144
158
  this.iterationsSinceCompaction = 0;
145
- // A new user message begins a new turn — drop the previous turn's task
146
- // so the fresh instruction becomes the one preserved at compaction. The
147
- // session mission survives: an error/log paste in the new turn must not
148
- // erase the original goal.
159
+ // Новое сообщение юзера начинает новый ход — сбрасываем задачу прошлого хода,
160
+ // чтобы сводка сохранила свежую инструкцию. Миссия сессии выживает: вставка
161
+ // ошибки/лога в новом ходе не должна стирать исходную цель.
149
162
  this.lastUserTask = "";
150
163
  this.lastUserFeedback = "";
151
164
  }
165
+ /** Оценка качества контекста 0–100 (нагрузка, компакции, ошибки, свежесть). */
152
166
  getQuality() {
153
167
  const usedTokens = this.getEstimatedTokens();
154
168
  const tokenLoad = this.budget.history > 0
155
169
  ? Math.max(0, 1 - usedTokens / this.budget.history)
156
170
  : 0;
157
- // Recoverable penalty: each compaction loses some information, but the
158
- // penalty is CAPPED so cumulative compactions can never permanently pin
159
- // quality below the forced-compaction trigger (40%). Before this cap, a
160
- // session with ~7+ compactions clamped the term to 0 forever; combined
161
- // with heavy token load + error density, quality stuck below 40 and a
162
- // compaction fired on EVERY iteration (observed: 56 compactions in 28 min).
171
+ // Восстанавливаемый штраф: каждая компакция теряет часть информации, но штраф
172
+ // ОГРАНИЧЕН СВЕРХУ, чтобы кумулятивные компакции не прикалывали качество ниже
173
+ // триггера форсированной компакции (40%). До капа сессия с ~7+ компакциями
174
+ // клампила слагаемое в 0 навсегда; вместе с тяжёлой токенной нагрузкой и
175
+ // плотностью ошибок качество застревало ниже 40 и компакция срабатывала на
176
+ // КАЖДОЙ итерации (было: 56 компакций за 28 минут).
163
177
  const COMPACTION_PENALTY_FLOOR = 0.5;
164
178
  const compactionLoss = Math.max(COMPACTION_PENALTY_FLOOR, 1 - this.compactionCount * 0.15);
165
179
  const msgCount = this.messages.length || 1;
@@ -168,13 +182,13 @@ export class ContextManager {
168
182
  const score = tokenLoad * 0.4 + compactionLoss * 0.25 + errorDensity * 0.2 + freshness * 0.15;
169
183
  return Math.round(Math.min(100, Math.max(0, score * 100)));
170
184
  }
185
+ /** Добавляет сообщение в историю (трекает задачу, аттачит pending-картинки, пик токенов). */
171
186
  addMessage(msg) {
172
- // Track the task the agent is working on so it survives compaction. The
173
- // user's real instruction is the FIRST non-system-summary user message of
174
- // the turn; everything after it (tool results, retry nudges) are injected
175
- // `user` envelopes that must not overwrite it (observed: after compaction
176
- // the model had forgotten "удали /bicycles, почини стили" and re-read
177
- // files aimlessly for 40+ iterations).
187
+ // Трекаем задачу, над которой работает агент, чтобы пережила компакцию.
188
+ // Реальная инструкция юзера — ПЕРВОЕ не-system-summary user-сообщение хода;
189
+ // всё после (результаты тулов, retry-nudge'и) — инжектированные `user`-
190
+ // конверты, которые не должны её затирать (было: после компакции модель
191
+ // забывала "удали /bicycles, почини стили" и 40+ итераций бесцельно читала файлы).
178
192
  if (msg.role === "user") {
179
193
  const text = typeof msg.content === "string" ? msg.content : getMessageText(msg.content);
180
194
  if (text.trim() && !text.startsWith("<system-summary>") && !text.includes("[Compressed:")) {
@@ -182,16 +196,16 @@ export class ContextManager {
182
196
  this.lastUserFeedback = text.trim().slice(0, 400);
183
197
  }
184
198
  else if (!this.lastUserTask && !this.lastUserFeedback) {
185
- // No new task inside a turn that already captured an error/log paste:
186
- // the paste's follow-up messages are tool-result continuations, not a
187
- // fresh instruction, so the session mission must win at compaction.
199
+ // Нет новой задачи внутри хода, где уже поймана вставка ошибки/лога:
200
+ // последующие сообщения — продолжения результатов тулов, не свежая
201
+ // инструкция, поэтому на компакции побеждает миссия сессии.
188
202
  this.lastUserTask = text.trim();
189
203
  if (!this.sessionMission)
190
204
  this.sessionMission = text.trim();
191
205
  }
192
206
  }
193
207
  }
194
- // Auto-attach pending images to the next user message
208
+ // Авто-аттач pending-картинок к следующему user-сообщению
195
209
  if (msg.role === "user" && this.pendingImageParts.length > 0) {
196
210
  const textPart = {
197
211
  type: "text",
@@ -209,35 +223,36 @@ export class ContextManager {
209
223
  this.peakTokens = tokens;
210
224
  }
211
225
  /**
212
- * Queue an image part to be attached to the next user message.
226
+ * Кладёт картинку в очередь на аттач к следующему user-сообщению.
213
227
  */
214
228
  addPendingImage(part) {
215
229
  this.pendingImageParts.push(part);
216
230
  }
217
231
  /**
218
- * Check if there are pending images waiting to be attached.
232
+ * True, если есть картинки, ждущие аттача.
219
233
  */
220
234
  hasPendingImages() {
221
235
  return this.pendingImageParts.length > 0;
222
236
  }
223
237
  /**
224
- * Get pending image parts without clearing them.
238
+ * Возвращает pending-картинки, не очищая очередь.
225
239
  */
226
240
  getPendingImages() {
227
241
  return [...this.pendingImageParts];
228
242
  }
229
243
  /**
230
- * Clear pending images (e.g., if user sends a text-only message).
244
+ * Очищает pending-картинки (напр., юзер шлёт чисто текстовое сообщение).
231
245
  */
232
246
  clearPendingImages() {
233
247
  this.pendingImageParts = [];
234
248
  }
249
+ /** Число сообщений в истории. */
235
250
  getMessageCount() {
236
251
  return this.messages.length;
237
252
  }
238
- // Per-message token cache: messages are treated as immutable once added
239
- // (compaction/updateSystemPrompt create new objects), so a WeakMap is safe
240
- // and turns repeated full-history estimation from O(n) encodes into O(1).
253
+ // Токенный кэш на сообщение: сообщения считаются иммутабельными после добавления
254
+ // (компакция/updateSystemPrompt создают новые объекты), поэтому WeakMap безопасен
255
+ // и превращает повторную оценку всей истории из O(n) кодирований в O(1).
241
256
  tokenCache = new WeakMap();
242
257
  estimateMessageTokensCached(m) {
243
258
  let t = this.tokenCache.get(m);
@@ -251,14 +266,14 @@ export class ContextManager {
251
266
  const text = getMessageText(m.content);
252
267
  if (this.tokenCounter) {
253
268
  let t = this.tokenCounter.count(text);
254
- // Image tokens: base64 ~130 tokens per 512x512 tile; rough estimate
269
+ // Токены картинок: base64 ~130 токенов на тайл 512x512; грубая оценка
255
270
  if (Array.isArray(m.content)) {
256
271
  for (const part of m.content) {
257
272
  if (part.type === "image_url" && part.image_url?.url) {
258
273
  const b64Len = part.image_url.url.includes(",")
259
274
  ? (part.image_url.url.split(",")[1]?.length ?? 0)
260
275
  : part.image_url.url.length;
261
- // ~130 tokens per 512 bytes of base64
276
+ // ~130 токенов на 512 байт base64
262
277
  t += Math.ceil(b64Len / 512) * 130;
263
278
  }
264
279
  }
@@ -294,29 +309,36 @@ export class ContextManager {
294
309
  }
295
310
  return t;
296
311
  }
312
+ /** True, если истории нужна компакция (бюджет превышен или интервал + загрузка). */
297
313
  needsCompaction() {
298
- if (this.iterationsSinceCompaction >= COMPACTION_INTERVAL)
299
- return true;
300
314
  const totalTokens = this.messages.reduce((sum, m) => sum + this.estimateMessageTokensCached(m), 0);
301
- return totalTokens > this.budget.history * this.compactionThreshold;
315
+ if (totalTokens > this.budget.history * this.compactionThreshold)
316
+ return true;
317
+ // Интервал итераций уплотняет только значимо загруженную историю.
318
+ // Уплотнять слабозагруженный контекст — терять рабочее состояние без
319
+ // бюджетной причины (см. `compactionMinLoad`).
320
+ if (this.iterationsSinceCompaction < COMPACTION_INTERVAL)
321
+ return false;
322
+ const load = this.budget.history > 0 ? totalTokens / this.budget.history : 0;
323
+ return load >= this.compactionMinLoad;
302
324
  }
325
+ /** Уплотняет историю: старые ходы → сводка фактов, свежие — дословно. Null, если уплотнять нечего. */
303
326
  compact() {
304
- // Capture the pre-compaction state BEFORE any counters reset or rise —
305
- // qualityBefore must reflect the context the model actually saw.
327
+ // Снимаем состояние ДО компакции ПЕРЕД сбросом/ростом счётчиков —
328
+ // qualityBefore обязан отражать контекст, который реально видела модель.
306
329
  const tokensBefore = this.getEstimatedTokens();
307
330
  const qualityBefore = this.getQuality();
308
331
  const messagesBefore = this.messages.length;
309
- // Reset the counter even when nothing to compact — otherwise
310
- // needsCompaction() returns true forever after 15 iterations with few messages.
332
+ // Сбрасываем счётчик, даже если уплотнять нечего — иначе
333
+ // needsCompaction() вечно true после 15 итераций с малым числом сообщений.
311
334
  this.iterationsSinceCompaction = 0;
312
335
  if (this.messages.length <= KEEP_LAST_N * 2)
313
336
  return null;
314
337
  this.compactionCount++;
315
338
  let cutoff = this.messages.length - KEEP_LAST_N * 2;
316
- // Never split an assistant(tool_calls) group across the cutoff: the kept
317
- // history must not start with orphaned role:"tool" messages, and the
318
- // assistant half must not stay in the compacted part (OpenAI-compatible
319
- // backends reject such histories).
339
+ // Не режем группу assistant(tool_calls) по cutoff: сохранённая история не
340
+ // должна начинаться с сиротских role:"tool", а assistant-половина — остаться
341
+ // в уплотняемой части (OpenAI-совместимые бэкенды такие истории отвергают).
320
342
  while (cutoff > 0 &&
321
343
  (this.messages[cutoff]?.role === "tool" || this.messages[cutoff - 1]?.tool_calls?.length)) {
322
344
  cutoff--;
@@ -326,18 +348,18 @@ export class ContextManager {
326
348
  this.facts.extract(oldTurns);
327
349
  const parts = [];
328
350
  parts.push(`[Compressed: ${oldTurns.length} old turns removed]`);
329
- // Carry the user's actual instruction forward — the model must never lose
330
- // the task it is working on just because 40 turns got compacted away. The
331
- // per-turn task falls back to the session mission (first real instruction)
332
- // so a pasted error/log in the current turn cannot erase the goal.
351
+ // Проносим реальную инструкцию юзера — модель не должна терять задачу,
352
+ // над которой работает, лишь потому что уплотнилось 40 ходов. Задача хода
353
+ // фолбэчится на миссию сессии (первую реальную инструкцию), чтобы вставка
354
+ // ошибки/лога в текущем ходе не стирала цель.
333
355
  const task = (this.lastUserTask || this.sessionMission).slice(0, 400);
334
356
  if (task)
335
357
  parts.push(`[Task: ${task}]`);
336
358
  if (this.lastUserFeedback && !task.startsWith(this.lastUserFeedback)) {
337
359
  parts.push(`[User feedback: ${this.lastUserFeedback}]`);
338
360
  }
339
- // Orient the model after compaction: an active plan must not be re-created
340
- // from scratch (see planSummaryProvider doc).
361
+ // Ориентируем модель после компакции: активный план нельзя пересоздавать
362
+ // с нуля (см. доку planSummaryProvider).
341
363
  const planLine = this.planSummaryProvider?.();
342
364
  if (planLine)
343
365
  parts.push(`[Plan: ${planLine}]`);
@@ -369,7 +391,7 @@ export class ContextManager {
369
391
  content: `<system-summary>${this.compactedBlock}</system-summary>`,
370
392
  };
371
393
  const firstSystem = this.messages.find((m) => m.role === "system");
372
- // Filter out stale system-summary messages from recent turns to prevent nesting
394
+ // Чистим протухшие system-summary из свежих ходов против вложенности
373
395
  const freshRecent = recentTurns.filter((m) => {
374
396
  if (m.role !== "user")
375
397
  return true;
@@ -394,8 +416,8 @@ export class ContextManager {
394
416
  };
395
417
  }
396
418
  /**
397
- * Snapshot of the current context state — used to log the initial context
398
- * structure at session start and per-iteration context stats.
419
+ * Снапшот текущего состояния контекста — для лога начальной структуры
420
+ * контекста на старте сессии и per-iteration статистики.
399
421
  */
400
422
  getSnapshot() {
401
423
  return {
@@ -410,8 +432,8 @@ export class ContextManager {
410
432
  };
411
433
  }
412
434
  /**
413
- * Replace the system prompt in place (keeps it first) or prepend a new one.
414
- * Used to refresh dynamic prompt blocks (e.g. the plan checklist) mid-run.
435
+ * Заменяет системный промпт на месте (остаётся первым) или пренпендит новый.
436
+ * Обновляет динамические блоки промпта (напр. чеклист плана) посреди прогона.
415
437
  */
416
438
  updateSystemPrompt(content) {
417
439
  const idx = this.messages.findIndex((m) => m.role === "system");
@@ -422,13 +444,14 @@ export class ContextManager {
422
444
  this.messages.unshift({ role: "system", content });
423
445
  }
424
446
  }
447
+ /** Копия активной истории сообщений. */
425
448
  getActiveHistory() {
426
449
  return [...this.messages];
427
450
  }
428
451
  /**
429
- * Reset everything that belongs to the current conversation turn.
430
- * Fact lists (FactExtractor) intentionally survive clear() — they are
431
- * session-scoped compaction state, not per-turn.
452
+ * Сбрасывает всё, что принадлежит текущему ходу диалога.
453
+ * Списки фактов (FactExtractor) осознанно переживают clear() — это
454
+ * session-scoped состояние компакции, не per-turn.
432
455
  */
433
456
  clear() {
434
457
  this.messages = [];
@@ -440,12 +463,15 @@ export class ContextManager {
440
463
  this.lastUserFeedback = "";
441
464
  this.sessionMission = "";
442
465
  }
466
+ /** Оценка токенов истории (сообщения + токены тулов). */
443
467
  getEstimatedTokens() {
444
468
  return (this.messages.reduce((sum, m) => sum + this.estimateMessageTokensCached(m), 0) + this.toolTokens);
445
469
  }
470
+ /** Задаёт токены определений тулов (прибавляются к оценке). */
446
471
  setToolTokens(tokens) {
447
472
  this.toolTokens = tokens;
448
473
  }
474
+ /** Пересчитывает бюджеты под новое окно контекста (и опционально счётчик). */
449
475
  resize(contextWindow, contextBudget, tokenCounter) {
450
476
  this.contextWindow = contextWindow;
451
477
  this.budget = this.calculateBudget(contextWindow, contextBudget);
@@ -0,0 +1,176 @@
1
+ // Клиент контр-агента: верификация пакета доказательств с опциональным read-only контуром.
2
+ import { jsonrepair } from "jsonrepair";
3
+ import { parseChecks, runCriticChecks, renderCheckResults, CRITIC_READONLY_TOOLS } from "./tools";
4
+ export const CRITIC_SYSTEM_PROMPT = `You are a strict but fair verifier ("counter-agent") reviewing an AI agent's in-progress work.
5
+ Rules:
6
+ - Flag ONLY issues that can be verified from the evidence provided. Do not speculate or invent.
7
+ - Do not nitpick style, wording, or preferences unless the user's task demanded it.
8
+ - If the evidence is insufficient to judge, return status "ok" (prefer silence over noise).
9
+ - "critical" = the task's stated result is demonstrably not delivered. "concern" = a concrete, checkable gap. "info" = minor.
10
+ - Every issue must reference concrete evidence (file, command, output).
11
+ Reply with JSON ONLY:
12
+ {"status":"ok|concern|critical","summary":"one line","issues":[{"severity":"info|concern|critical","claim":"...","problem":"...","suggestion":"...","evidence":"..."}]}`;
13
+ const CHECK_REQUEST_INSTRUCTION = `If your verdict is concern/critical and a read-only file check could confirm or refute an issue, request up to 3 checks by adding "checks":[{"tool":"...","args":{...}}] to your JSON. Available tools ONLY: ${CRITIC_READONLY_TOOLS.join(", ")} (read-only; no writes, shell, web, or memory exist — do not request anything else). Checks run locally and their outputs return to you for a FINAL verdict. If no check would help, omit "checks".`;
14
+ const FINAL_VERDICT_INSTRUCTION = `Check outputs above are ground truth from the workspace. Reply with your FINAL verdict as JSON (same shape, no more "checks"): confirm each issue the checks support, drop the ones they refute, and set status accordingly.`;
15
+ /** Клиент критика: судит пакет доказательств, при concern/critical — до 3 read-only проверок. */
16
+ export class CriticClient {
17
+ opts;
18
+ constructor(opts) {
19
+ this.opts = opts;
20
+ }
21
+ isEnabled() {
22
+ return true;
23
+ }
24
+ /**
25
+ * Верифицирует пакет доказательств (Phase 2: опциональный ограниченный тул-контур).
26
+ *
27
+ * Проход 1 судит пакет как есть (поведение v1). При concern/critical с
28
+ * раннером `runCheck` проход 2 даёт критику до 3 read-only проверок,
29
+ * проход 3 выносит финальный вердикт по их выводам. Вся цепочка делит
30
+ * один таймаут-бюджет и откатывается к вердикту прохода 1
31
+ * (или null, если упал сам проход 1) — глубина верификации best-effort,
32
+ * никогда не блокирует.
33
+ */
34
+ async verify(pack, signal, opts) {
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), this.opts.timeoutMs ?? 15000);
37
+ const onAbort = () => controller.abort();
38
+ signal?.addEventListener("abort", onAbort);
39
+ try {
40
+ const messages = [
41
+ { role: "system", content: CRITIC_SYSTEM_PROMPT },
42
+ { role: "user", content: renderPack(pack) },
43
+ ];
44
+ const firstRaw = await this.chat(messages, controller.signal);
45
+ const first = parseVerdict(firstRaw);
46
+ if (!first || first.status === "ok" || !opts?.runCheck)
47
+ return first;
48
+ return await this.verifyWithChecks(messages, firstRaw, first, controller.signal, opts.runCheck);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ finally {
54
+ clearTimeout(timer);
55
+ signal?.removeEventListener("abort", onAbort);
56
+ }
57
+ }
58
+ /**
59
+ * Проходы 2–3: запрашиваем ограниченные read-only проверки (доп. LLM-вызовы
60
+ * только если первый вердикт actionable), выполняем их и заземляем финальный
61
+ * вердикт на выводах. История сохраняется, чтобы "твой вердикт" резолвился в проход 1.
62
+ * Любой сбой — откат к вердикту прохода 1.
63
+ */
64
+ async verifyWithChecks(messages, firstRaw, first, signal, runCheck) {
65
+ try {
66
+ messages.push({ role: "assistant", content: firstRaw });
67
+ messages.push({ role: "user", content: CHECK_REQUEST_INSTRUCTION });
68
+ const checksRaw = await this.chat(messages, signal);
69
+ const checks = parseChecks(parseVerdict(checksRaw));
70
+ if (checks.length === 0)
71
+ return first;
72
+ const results = await runCriticChecks(checks, runCheck);
73
+ if (results.length === 0)
74
+ return first;
75
+ messages.push({ role: "assistant", content: checksRaw });
76
+ messages.push({
77
+ role: "user",
78
+ content: `CHECK RESULTS (ground truth):\n${renderCheckResults(results)}\n\n${FINAL_VERDICT_INSTRUCTION}`,
79
+ });
80
+ return parseVerdict(await this.chat(messages, signal)) ?? first;
81
+ }
82
+ catch {
83
+ return first;
84
+ }
85
+ }
86
+ async chat(messages, signal) {
87
+ let text = "";
88
+ for await (const chunk of this.opts.provider.chat(messages, undefined, signal, {
89
+ maxTokens: this.opts.maxTokens ?? 512,
90
+ reasoningEffort: "none",
91
+ })) {
92
+ if (chunk.type === "text" && chunk.content)
93
+ text += chunk.content;
94
+ }
95
+ return text;
96
+ }
97
+ }
98
+ function renderPack(pack) {
99
+ const cmdLines = pack.commands.map((c) => {
100
+ const base = `${c.command} (exit ${c.exitCode ?? "?"})`;
101
+ return c.output ? `${base}\n output: ${c.output}` : base;
102
+ });
103
+ const files = (pack.fileExcerpts ?? []).map((f) => `--- ${f.path} ---\n${f.excerpt}`);
104
+ return [
105
+ `TASK: ${pack.task}`,
106
+ pack.planSummary ? `PLAN: ${pack.planSummary}` : "",
107
+ `CHANGED FILES: ${pack.changedFiles.join(", ") || "(none)"}`,
108
+ `COMMANDS: ${cmdLines.join("; ") || "(none)"}`,
109
+ "TOOL TRAIL:",
110
+ pack.toolDigest || "(empty)",
111
+ files.length > 0 ? `FILE CONTENTS:\n${files.join("\n")}` : "",
112
+ ]
113
+ .filter(Boolean)
114
+ .join("\n");
115
+ }
116
+ const SEVERITIES = new Set(["info", "concern", "critical"]);
117
+ /** Парсит вердикт критика из сырого текста (строгий JSON → jsonrepair → вырезание блока). Null, если не вердикт. */
118
+ export function parseVerdict(text) {
119
+ let parsed = null;
120
+ try {
121
+ parsed = JSON.parse(text);
122
+ }
123
+ catch {
124
+ try {
125
+ parsed = JSON.parse(jsonrepair(text));
126
+ }
127
+ catch {
128
+ const match = text.match(/\{[\s\S]*\}/);
129
+ if (!match)
130
+ return null;
131
+ try {
132
+ parsed = JSON.parse(jsonrepair(match[0]));
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ }
139
+ if (!parsed || typeof parsed !== "object")
140
+ return null;
141
+ const obj = parsed;
142
+ const status = obj.status;
143
+ if (status !== "ok" && status !== "concern" && status !== "critical")
144
+ return null;
145
+ const issues = parseIssuesList(obj.issues);
146
+ // Запрошенные проверки верификации (Phase 2): несём сырыми, кап здесь;
147
+ // форма + allowlist enforced в parseChecks, вслепую не доверяем.
148
+ const checks = parseRawChecks(obj.checks);
149
+ return { status, summary: String(obj.summary ?? ""), issues, ...(checks ? { checks } : {}) };
150
+ }
151
+ /** Проблемы вердикта с санитизированными severity; записи без claim+problem дропаются. */
152
+ function parseIssuesList(raw) {
153
+ if (!Array.isArray(raw))
154
+ return [];
155
+ return raw
156
+ .filter((i) => !!i && typeof i === "object")
157
+ .map((i) => ({
158
+ severity: (SEVERITIES.has(String(i.severity)) ? String(i.severity) : "concern"),
159
+ claim: String(i.claim ?? ""),
160
+ problem: String(i.problem ?? ""),
161
+ suggestion: i.suggestion ? String(i.suggestion) : undefined,
162
+ evidence: i.evidence ? String(i.evidence) : undefined,
163
+ }))
164
+ .filter((i) => i.claim || i.problem);
165
+ }
166
+ /** Сырые запрошенные проверки (с капом); форма + allowlist enforced в parseChecks. */
167
+ function parseRawChecks(raw) {
168
+ if (!Array.isArray(raw))
169
+ return undefined;
170
+ const out = raw
171
+ .filter((c) => !!c &&
172
+ typeof c === "object" &&
173
+ typeof c.tool === "string")
174
+ .map((c) => ({ tool: c.tool, args: c.args }));
175
+ return out.length > 0 ? out : undefined;
176
+ }