micro-models-agent 2.10.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 (359) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/assets/app.js +22 -4
  3. package/dist/assets/index.html +29 -0
  4. package/dist/assets/js/dom.js +2 -1
  5. package/dist/assets/js/feed.js +38 -26
  6. package/dist/assets/js/format.js +1 -1
  7. package/dist/assets/js/rail.js +114 -0
  8. package/dist/assets/js/settings-controls.js +2 -2
  9. package/dist/assets/js/settings.js +246 -51
  10. package/dist/assets/js/store.js +3 -0
  11. package/dist/assets/js/tabs.js +104 -2
  12. package/dist/assets/styles.css +215 -66
  13. package/dist/cli/cache-line.js +1 -0
  14. package/dist/cli/command-suggest.js +5 -4
  15. package/dist/cli/commands.js +231 -122
  16. package/dist/cli/completer.js +132 -31
  17. package/dist/cli/index.js +1 -0
  18. package/dist/cli/json-payload.js +4 -4
  19. package/dist/cli/main.js +166 -61
  20. package/dist/cli/onboarding-commands.js +126 -0
  21. package/dist/cli/plugin-commands.js +2 -1
  22. package/dist/cli/relaunch.js +16 -13
  23. package/dist/cli/repl-commands-lsp.js +75 -0
  24. package/dist/cli/repl-commands-skills.js +105 -0
  25. package/dist/cli/repl-commands.js +589 -266
  26. package/dist/cli/repl.js +436 -177
  27. package/dist/cli/run-result.js +5 -6
  28. package/dist/cli/security-commands.js +37 -28
  29. package/dist/cli/setup-order.js +4 -55
  30. package/dist/cli/setup-prompt.js +3 -2
  31. package/dist/cli/setup.js +27 -107
  32. package/dist/cli/web-command.js +98 -0
  33. package/dist/config/budget.js +5 -5
  34. package/dist/config/config.js +103 -96
  35. package/dist/config/defaults.js +38 -8
  36. package/dist/config/domains.js +24 -27
  37. package/dist/config/env.js +46 -0
  38. package/dist/config/experts.js +2 -0
  39. package/dist/config/index.js +1 -0
  40. package/dist/config/paths.js +56 -0
  41. package/dist/config/redact.js +44 -0
  42. package/dist/config/registry-extra.js +334 -0
  43. package/dist/config/registry.js +584 -0
  44. package/dist/config/security.js +35 -36
  45. package/dist/config/timeouts.js +74 -0
  46. package/dist/config/utils.js +16 -15
  47. package/dist/core/agent/artifact-progress.js +59 -0
  48. package/dist/core/agent/artifact-resolve.js +79 -0
  49. package/dist/core/agent/audit-gate.js +7 -3
  50. package/dist/core/agent/compaction.js +2 -2
  51. package/dist/core/agent/constants.js +33 -4
  52. package/dist/core/agent/context-log.js +454 -0
  53. package/dist/core/agent/context-renderer.js +5 -0
  54. package/dist/core/agent/critic-gate.js +132 -0
  55. package/dist/core/agent/hallucination-gate.js +148 -38
  56. package/dist/core/agent/loop-state.js +15 -0
  57. package/dist/core/agent/prefix-monitor.js +1 -1
  58. package/dist/core/agent/reasoning-resolver.js +10 -1
  59. package/dist/core/agent/token-tracker.js +7 -4
  60. package/dist/core/agent/tool-batch.js +93 -18
  61. package/dist/core/agent/tool-images.js +18 -0
  62. package/dist/core/agent/tool-output.js +65 -11
  63. package/dist/core/agent/tool-pairing.js +60 -0
  64. package/dist/core/agent-moe.js +25 -24
  65. package/dist/core/agent.js +429 -108
  66. package/dist/core/bootstrap/agent-phase.js +66 -0
  67. package/dist/core/bootstrap/config-phase.js +86 -0
  68. package/dist/core/bootstrap/modules-phase.js +269 -0
  69. package/dist/core/bootstrap/provider-phase.js +183 -0
  70. package/dist/core/bootstrap/session-phase.js +204 -0
  71. package/dist/core/bootstrap/state.js +94 -0
  72. package/dist/core/bootstrap/workspace-phase.js +115 -0
  73. package/dist/core/bootstrap.js +92 -660
  74. package/dist/core/crash-handler.js +17 -7
  75. package/dist/core/environment.js +37 -12
  76. package/dist/core/host-plan.js +43 -0
  77. package/dist/core/host.js +23 -0
  78. package/dist/core/index.js +1 -0
  79. package/dist/core/prompt-builder.js +17 -1
  80. package/dist/core/prompt-overflow.js +42 -43
  81. package/dist/core/session-logger.js +122 -30
  82. package/dist/core/system-prompt.js +117 -0
  83. package/dist/core/version.js +6 -4
  84. package/dist/core/web-host.js +34 -0
  85. package/dist/core/workspace.js +19 -15
  86. package/dist/i18n/en.json +106 -21
  87. package/dist/i18n/index.js +37 -3
  88. package/dist/i18n/ru.json +109 -21
  89. package/dist/index.js +7 -3
  90. package/dist/llm/image-utils.js +55 -64
  91. package/dist/llm/llm-errors.js +9 -0
  92. package/dist/llm/model-loader.js +10 -32
  93. package/dist/llm/models-dev.js +121 -0
  94. package/dist/llm/openai-compat.js +146 -73
  95. package/dist/llm/orchestrator.js +33 -11
  96. package/dist/llm/provider-budget.js +2 -2
  97. package/dist/llm/provider-context.js +233 -0
  98. package/dist/llm/provider.js +1 -1
  99. package/dist/llm/response.js +5 -0
  100. package/dist/llm/token-counter.js +27 -11
  101. package/dist/logger/app-logger.js +48 -22
  102. package/dist/logger/file-log.js +28 -19
  103. package/dist/logger/index.js +1 -0
  104. package/dist/main.js +24285 -22447
  105. package/dist/migration/backup.js +14 -3
  106. package/dist/migration/detect.js +12 -3
  107. package/dist/migration/index.js +1 -0
  108. package/dist/modules/artifacts/store.js +9 -8
  109. package/dist/modules/browser/actions.js +6 -0
  110. package/dist/modules/browser/bridge-client.js +14 -12
  111. package/dist/modules/browser/bridge-path.js +4 -4
  112. package/dist/modules/browser/console-buffer.js +37 -0
  113. package/dist/modules/browser/cookie-store.js +5 -0
  114. package/dist/modules/browser/driver-types.js +1 -0
  115. package/dist/modules/browser/driver.js +19 -11
  116. package/dist/modules/browser/index.js +3 -1
  117. package/dist/modules/browser/module.js +2 -1
  118. package/dist/modules/browser/session.js +10 -30
  119. package/dist/modules/browser/snapshot.js +19 -13
  120. package/dist/modules/browser/types.js +1 -0
  121. package/dist/modules/certification/cli.js +28 -20
  122. package/dist/modules/certification/fact-checker.js +9 -1
  123. package/dist/modules/certification/loader.js +9 -2
  124. package/dist/modules/certification/manifest.js +83 -21
  125. package/dist/modules/certification/runner.js +23 -13
  126. package/dist/modules/certification/scenarios.js +20 -20
  127. package/dist/modules/certification/syntax-scenarios.js +1 -2
  128. package/dist/modules/context/chunk-query.js +11 -9
  129. package/dist/modules/context/fact-extractor.js +37 -39
  130. package/dist/modules/context/index.js +1 -0
  131. package/dist/modules/context/manager.js +106 -80
  132. package/dist/modules/critic/client.js +176 -0
  133. package/dist/modules/critic/evidence.js +81 -0
  134. package/dist/modules/critic/provider.js +45 -0
  135. package/dist/modules/critic/tools.js +78 -0
  136. package/dist/modules/critic/types.js +1 -0
  137. package/dist/modules/execution/audit-runners.js +28 -34
  138. package/dist/modules/execution/auditor.js +53 -49
  139. package/dist/modules/execution/execution-plugin.js +142 -102
  140. package/dist/modules/execution/index.js +1 -1
  141. package/dist/modules/execution/input-from.js +37 -13
  142. package/dist/modules/execution/module.js +368 -191
  143. package/dist/modules/execution/moe-executor.js +53 -26
  144. package/dist/modules/execution/plan-actions.js +320 -202
  145. package/dist/modules/execution/plan-checks.js +35 -0
  146. package/dist/modules/execution/plan-coverage.js +36 -33
  147. package/dist/modules/execution/plan-steps.js +11 -3
  148. package/dist/modules/execution/plan-store.js +83 -17
  149. package/dist/modules/execution/plan-tool.js +89 -35
  150. package/dist/modules/execution/plan-validator.js +30 -10
  151. package/dist/modules/execution/planner.js +22 -13
  152. package/dist/modules/execution/stuck-detector.js +196 -93
  153. package/dist/modules/execution/tracker.js +22 -8
  154. package/dist/modules/execution/verifier.js +69 -48
  155. package/dist/modules/execution/windows-commands.js +12 -12
  156. package/dist/modules/hallucination/artifact-claim.js +93 -0
  157. package/dist/modules/hallucination/confidence.js +13 -11
  158. package/dist/modules/hallucination/consistency.js +10 -2
  159. package/dist/modules/hallucination/detector.js +10 -5
  160. package/dist/modules/hallucination/factual.js +43 -26
  161. package/dist/modules/hallucination/index.js +1 -0
  162. package/dist/modules/hallucination/js-identifiers.js +44 -45
  163. package/dist/modules/hallucination/llm-judge.js +17 -12
  164. package/dist/modules/hallucination/success-claim.js +59 -0
  165. package/dist/modules/indexer/cache.js +52 -4
  166. package/dist/modules/indexer/index.js +1 -0
  167. package/dist/modules/indexer/map-command.js +2 -4
  168. package/dist/modules/indexer/map-select.js +16 -16
  169. package/dist/modules/indexer/module.js +47 -12
  170. package/dist/modules/indexer/project-profile.js +8 -7
  171. package/dist/modules/indexer/symbols.js +15 -15
  172. package/dist/modules/indexer/walker.js +159 -29
  173. package/dist/modules/lsp/check-tool.js +7 -5
  174. package/dist/modules/lsp/client.js +39 -39
  175. package/dist/modules/lsp/command.js +17 -24
  176. package/dist/modules/lsp/config.js +12 -11
  177. package/dist/modules/lsp/index.js +1 -0
  178. package/dist/modules/lsp/module.js +48 -46
  179. package/dist/modules/lsp/probe.js +13 -14
  180. package/dist/modules/lsp/project-root.js +14 -17
  181. package/dist/modules/lsp/startup-check.js +18 -16
  182. package/dist/modules/mcp/client.js +59 -62
  183. package/dist/modules/mcp/index.js +1 -0
  184. package/dist/modules/mcp/module.js +66 -37
  185. package/dist/modules/mcp/registry.js +5 -0
  186. package/dist/modules/memory/index.js +1 -0
  187. package/dist/modules/memory/module.js +12 -8
  188. package/dist/modules/memory/search.js +4 -1
  189. package/dist/modules/memory/store.js +40 -2
  190. package/dist/modules/onboarding/init.js +47 -0
  191. package/dist/modules/onboarding/meet.js +30 -0
  192. package/dist/modules/onboarding/mode.js +30 -0
  193. package/dist/modules/onboarding/prompts.js +43 -0
  194. package/dist/modules/pipelines/engine.js +10 -0
  195. package/dist/modules/pipelines/index.js +1 -0
  196. package/dist/modules/pipelines/parser.js +4 -1
  197. package/dist/modules/pipelines/template.js +4 -0
  198. package/dist/modules/plugins/builtin/lint-on-write.js +80 -67
  199. package/dist/modules/plugins/builtin/notify.js +2 -0
  200. package/dist/modules/plugins/index.js +1 -0
  201. package/dist/modules/plugins/loader.js +18 -3
  202. package/dist/modules/plugins/manager.js +37 -17
  203. package/dist/modules/pricing/index.js +10 -11
  204. package/dist/modules/pricing/prices.js +12 -12
  205. package/dist/modules/processes/index.js +1 -0
  206. package/dist/modules/processes/kill-tree.js +27 -31
  207. package/dist/modules/processes/registry.js +44 -25
  208. package/dist/modules/processes/runner.js +4 -4
  209. package/dist/modules/providers/capabilities.js +28 -0
  210. package/dist/modules/providers/create.js +3 -4
  211. package/dist/modules/providers/factory.js +7 -7
  212. package/dist/modules/providers/fallback.js +12 -12
  213. package/dist/modules/providers/health.js +4 -4
  214. package/dist/modules/providers/manager.js +50 -19
  215. package/dist/modules/providers/naming.js +15 -0
  216. package/dist/modules/providers/preflight.js +71 -0
  217. package/dist/modules/providers/presets.js +39 -8
  218. package/dist/modules/providers/registry.js +7 -2
  219. package/dist/modules/reasoning/policy.js +8 -6
  220. package/dist/modules/reasoning/probe.js +30 -25
  221. package/dist/modules/security/audit-log.js +19 -36
  222. package/dist/modules/security/audit-notifier.js +35 -75
  223. package/dist/modules/security/command-validator.js +99 -51
  224. package/dist/modules/security/content-scanner.js +8 -8
  225. package/dist/modules/security/data-sanitizer.js +9 -16
  226. package/dist/modules/security/encryption.js +32 -52
  227. package/dist/modules/security/index.js +2 -1
  228. package/dist/modules/security/network-validator.js +120 -43
  229. package/dist/modules/security/path-validator.js +61 -46
  230. package/dist/modules/security/protected-paths.js +73 -0
  231. package/dist/modules/security/rate-limiter.js +17 -49
  232. package/dist/modules/security/security-policies.js +25 -57
  233. package/dist/modules/security/session-encryption.js +32 -71
  234. package/dist/modules/security/session-isolation.js +15 -28
  235. package/dist/modules/session/auto-name.js +63 -0
  236. package/dist/modules/session/export.js +98 -0
  237. package/dist/modules/session/index.js +1 -0
  238. package/dist/modules/session/manager.js +79 -28
  239. package/dist/modules/session/module.js +4 -3
  240. package/dist/modules/session/store.js +57 -20
  241. package/dist/modules/setup/answers.js +35 -0
  242. package/dist/modules/setup/apply.js +79 -0
  243. package/dist/modules/setup/discovery.js +72 -0
  244. package/dist/modules/skills/index.js +1 -0
  245. package/dist/modules/skills/loader.js +9 -1
  246. package/dist/modules/skills/module.js +11 -0
  247. package/dist/modules/updater/changelog-reader.js +15 -16
  248. package/dist/modules/updater/checker.js +9 -4
  249. package/dist/modules/updater/dev-detect.js +7 -6
  250. package/dist/modules/updater/index.js +1 -0
  251. package/dist/modules/updater/module.js +17 -15
  252. package/dist/modules/user-profile/compressor.js +2 -0
  253. package/dist/modules/user-profile/index.js +1 -0
  254. package/dist/modules/user-profile/profile.js +10 -1
  255. package/dist/modules/webui/answer-api.js +29 -0
  256. package/dist/modules/webui/assets.js +50 -0
  257. package/dist/modules/webui/body.js +21 -0
  258. package/dist/modules/webui/commands-settings.js +209 -0
  259. package/dist/modules/webui/commands-shared.js +60 -0
  260. package/dist/modules/webui/commands.js +174 -0
  261. package/dist/modules/webui/config-api.js +134 -0
  262. package/dist/modules/webui/file-api.js +202 -0
  263. package/dist/modules/webui/forward.js +159 -0
  264. package/dist/modules/webui/history-api.js +112 -0
  265. package/dist/modules/webui/host-state.js +60 -0
  266. package/dist/modules/webui/i18n-api.js +9 -0
  267. package/dist/modules/webui/limits.js +6 -0
  268. package/dist/modules/webui/log-forward.js +20 -0
  269. package/dist/modules/webui/memory-commands.js +64 -0
  270. package/dist/modules/webui/model-cache.js +18 -0
  271. package/dist/modules/webui/module.js +148 -0
  272. package/dist/modules/webui/persist.js +24 -0
  273. package/dist/modules/webui/prompt-io.js +97 -0
  274. package/dist/modules/webui/providers-api.js +337 -0
  275. package/dist/modules/webui/providers-view.js +56 -0
  276. package/dist/modules/webui/reasoning-view.js +15 -0
  277. package/dist/modules/webui/run-agent.js +81 -0
  278. package/dist/modules/webui/server.js +284 -0
  279. package/dist/modules/webui/sessions-api.js +178 -0
  280. package/dist/modules/webui/settings-api.js +561 -0
  281. package/dist/modules/webui/setup-api.js +201 -0
  282. package/dist/modules/webui/state.js +74 -0
  283. package/dist/modules/webui/util.js +261 -0
  284. package/dist/output/bus.js +7 -2
  285. package/dist/output/channel.js +80 -28
  286. package/dist/output/format.js +2 -1
  287. package/dist/output/index.js +1 -0
  288. package/dist/output/json-sink.js +5 -3
  289. package/dist/output/machine.js +4 -3
  290. package/dist/output/session-sink.js +4 -4
  291. package/dist/tools/approve.js +4 -0
  292. package/dist/tools/attach-image.js +5 -4
  293. package/dist/tools/auto-fixer.js +26 -44
  294. package/dist/tools/auto-tags.js +83 -0
  295. package/dist/tools/bash.js +193 -116
  296. package/dist/tools/browser.js +76 -31
  297. package/dist/tools/chunk-query.js +14 -6
  298. package/dist/tools/create-dir.js +9 -5
  299. package/dist/tools/delete-file.js +9 -5
  300. package/dist/tools/download-file.js +32 -7
  301. package/dist/tools/edit-file.js +52 -14
  302. package/dist/tools/enable-tools.js +6 -6
  303. package/dist/tools/executor.js +132 -27
  304. package/dist/tools/file-info.js +3 -1
  305. package/dist/tools/filter-tools.js +3 -3
  306. package/dist/tools/glob-tool.js +4 -1
  307. package/dist/tools/grep-tool.js +13 -10
  308. package/dist/tools/hidden-tools-block.js +6 -6
  309. package/dist/tools/index.js +16 -8
  310. package/dist/tools/list-dir.js +4 -2
  311. package/dist/tools/load-skill.js +7 -0
  312. package/dist/tools/mcp-call.js +15 -4
  313. package/dist/tools/meet-done.js +14 -0
  314. package/dist/tools/move-file.js +14 -7
  315. package/dist/tools/path-utils.js +29 -29
  316. package/dist/tools/pipeline-run.js +29 -7
  317. package/dist/tools/preview.js +1 -1
  318. package/dist/tools/process-ids.js +31 -0
  319. package/dist/tools/process-kill.js +9 -16
  320. package/dist/tools/process-list.js +1 -0
  321. package/dist/tools/process-log.js +11 -16
  322. package/dist/tools/process-wait.js +63 -0
  323. package/dist/tools/prompt-io.js +4 -3
  324. package/dist/tools/question.js +172 -25
  325. package/dist/tools/read-file.js +55 -34
  326. package/dist/tools/recall.js +14 -8
  327. package/dist/tools/registry.js +6 -5
  328. package/dist/tools/remember.js +8 -4
  329. package/dist/tools/scope-request.js +11 -6
  330. package/dist/tools/search-history.js +5 -2
  331. package/dist/tools/session-info.js +2 -0
  332. package/dist/tools/set-thinking.js +8 -4
  333. package/dist/tools/subagent.js +59 -39
  334. package/dist/tools/syntax-validator.js +292 -90
  335. package/dist/tools/user-input.js +22 -18
  336. package/dist/tools/web-browse.js +9 -3
  337. package/dist/tools/web-fetch.js +12 -23
  338. package/dist/tools/web-search.js +7 -5
  339. package/dist/tools/write-file.js +45 -15
  340. package/dist/ui/action-log.js +13 -0
  341. package/dist/ui/box.js +4 -3
  342. package/dist/ui/colors.js +2 -0
  343. package/dist/ui/diff.js +6 -5
  344. package/dist/ui/index.js +1 -0
  345. package/dist/ui/line-editor.js +230 -122
  346. package/dist/ui/line-math.js +10 -2
  347. package/dist/ui/md-formatter.js +57 -37
  348. package/dist/ui/output.js +8 -3
  349. package/dist/ui/plan-view.js +21 -10
  350. package/dist/ui/renderer.js +393 -67
  351. package/dist/ui/spinner.js +16 -5
  352. package/dist/ui/table.js +15 -17
  353. package/dist/ui/theme.js +143 -0
  354. package/dist/utils/child-run.js +20 -0
  355. package/dist/utils/error.js +1 -1
  356. package/dist/utils/index.js +2 -0
  357. package/dist/utils/retry.js +2 -2
  358. package/dist/utils/truncate.js +1 -1
  359. package/package.json +1 -1
@@ -12,31 +12,31 @@ import { resolve } from "path";
12
12
  import { performWebSearch } from "../../tools/web-search";
13
13
  import { getSessionSecurityConfig } from "../security/session-isolation";
14
14
  import { createPlanToolDefinitions } from "./plan-tool";
15
+ import { isValidCheck } from "./plan-checks";
15
16
  import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, STUCK_WARN_REPEAT_EVERY, } from "./execution-plugin";
17
+ import { EXECUTION_ERROR_SEARCH_MIN_INTERVAL_MS as ERROR_SEARCH_MIN_INTERVAL_MS } from "../../config/timeouts";
16
18
  /**
17
- * Minimum interval between automatic error searches. The spec asked for a
18
- * STUCK_RECOVERY_COOLDOWN-iteration cap, but onAfterTool has no iteration
19
- * counter (executor context), so the same anti-spam protection is
20
- * time-based. Injectable via the constructor for tests (0 disables it).
21
- */
22
- const ERROR_SEARCH_MIN_INTERVAL_MS = 30000;
19
+ * Минимальный интервал между автопоисками ошибок. Спек просил кап
20
+ * STUCK_RECOVERY_COOLDOWN-итераций, но в onAfterTool нет счётчика итераций
21
+ * (контекст экзекутора) — та же анти-спам защита, но по времени. Инжектится
22
+ * через конструктор для тестов (0 отключает).
23
+ */
23
24
  /**
24
- * Tools that may run while a plan step is active WITHOUT a step-alignment
25
- * check. They either cannot touch the filesystem, or are structural/read-only
26
- * inspection tools the agent legitimately needs at any point (diagnostics,
27
- * index/search, memory, process introspection, interactive prompts). Only
28
- * file-mutating tools are pinned to the current step's file tokens.
25
+ * Тулы, которым можно при активном шаге плана БЕЗ проверки step-alignment.
26
+ * Либо не трогают ФС, либо структурно/read-only инспекции, нужные агенту в
27
+ * любой точке (диагностика, индекс/поиск, память, интроспекция процессов,
28
+ * интерактивные промпты). К текущему шагу пинятся только файл-мутирующие.
29
29
  *
30
- * `lsp_check` and `project_map` belong here explicitly: both are alwaysOn and
31
- * the system prompt MANDATES `lsp_check` after every write (including rewrites
32
- * of files owned by already-completed steps). Leaving them off the list made
33
- * every diagnostic call burn a plan warning and could hard-block a legitimate
34
- * write on the 3rd call (observed: "3 вызовов подряд вне текущего шага" while
35
- * the agent was writing the current step's own deliverable).
30
+ * `lsp_check` и `project_map` здесь явно: оба alwaysOn, а системный промпт
31
+ * ТРЕБУЕТ `lsp_check` после каждой записи (включая переписи файлов уже
32
+ * закрытых шагов). Без них каждый диагностический вызов жёг plan-warning и мог
33
+ * hard-block'нуть легитимную запись на 3-м вызове (было: "3 вызовов подряд вне
34
+ * текущего шага", пока агент писал deliverable текущего же шага).
36
35
  */
37
36
  const PLAN_ALIGNMENT_EXEMPT_TOOLS = new Set([
38
37
  // structural / execution control
39
38
  "plan",
39
+ "plan_step",
40
40
  "todo",
41
41
  "verify",
42
42
  "enable_tools",
@@ -67,57 +67,59 @@ const PLAN_ALIGNMENT_EXEMPT_TOOLS = new Set([
67
67
  "load_skill",
68
68
  ]);
69
69
  /**
70
- * Canonical form of a file token for plan alignment: forward slashes + lower
71
- * case. Model tool calls pass absolute Windows paths
72
- * (`C:\…\test\src\main.ts`) while plan steps name files relatively
73
- * (`src/main.ts`); comparing the raw strings made every on-track write look
74
- * off-path (backslash vs forward slash never intersects). Normalizing both
75
- * sides lets the substring/`endsWith` overlap below match.
70
+ * Каноническая форма файлового токена для alignment: прямые слэши + lowercase.
71
+ * Вызовы тулов несут абсолютные Windows-пути (`C:\…\test\src\main.ts`), а шаги
72
+ * плана — относительные (`src/main.ts`); сравнение сырых строк делало каждый
73
+ * on-track write off-path (бэкслэш vs прямой слэш не пересекаются).
74
+ * Нормализация обеих сторон чинит overlap через substring/`endsWith` ниже.
76
75
  */
77
76
  function planPathToken(p) {
78
77
  return toForwardSlash(p).toLowerCase();
79
78
  }
79
+ /** Execution-модуль: план, stuck-детектор, аудитор, гейты выравнивания и автопоиск ошибок. */
80
80
  export class ExecutionModule {
81
81
  name = "execution";
82
82
  tracker = null;
83
+ /** Сессия-владелец текущего трекера. План следует за сессией: смена
84
+ * сессии через switchSession() персистит трекер в слот старой сессии
85
+ * и загружает план новой (см. docs/specs/2026-09-29-session-plan-binding-design.md). */
86
+ sessionId = null;
83
87
  verifier;
84
88
  stuckDetector;
85
89
  auditor;
86
90
  store;
87
91
  baseDir;
88
- // Skip the first audit after restoring a plan from disk — gives the user
89
- // one chance to change the subject before the audit gate locks in.
92
+ // Пропуск первого аудита после восстановления плана с диска — даём юзеру
93
+ // шанс сменить тему, прежде чем audit-gate зафиксируется.
90
94
  _auditSkipsRemaining = 0;
91
95
  /**
92
- * The last plan that was COMPLETED and auto-archived. `plan update` on the
93
- * final step clears the tracker, which used to make runFinalAudit() return
94
- * null and silently skip the final audit (tests + tsc + file checks) for a
95
- * finished task. Keep the completed plan so the audit gate still runs.
96
+ * Последний план, ЗАВЕРШЁННЫЙ и авто-архивированный. `plan update` на
97
+ * финальном шаге чистит трекер — раньше runFinalAudit() возвращал null и
98
+ * молча скипал финальный аудит (тесты + tsc + файлы) готовой задачи.
99
+ * Храним завершённый план, чтобы audit-gate отработал.
96
100
  */
97
101
  completedPlan = null;
98
102
  /**
99
- * Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
100
- * but flushed in onBeforeThink, so they never land between an assistant
101
- * tool_call message and its tool results (which breaks message pairing and
102
- * buries the warnings the model should react to).
103
+ * Отложенные <system-summary>. Инжектятся onBeforeTool/onAfterTool, но
104
+ * флашатся в onBeforeThink — никогда не встают между assistant tool_call и
105
+ * его tool-результатами (рвёт спаривание и хоронит варнинги, на которые
106
+ * модель должна реагировать).
103
107
  */
104
108
  pendingMessages = [];
105
- // Per-command failure counters for forbidden Windows commands (grep/sed/ls/
106
- // find/… invoked through the bash tool). A hard stop after the second
107
- // failure is evidence-based (actual command failures), not task
108
- // classification (rule #10).
109
+ // Per-командные счётчики фейлов запрещённых Windows-команд (grep/sed/ls/
110
+ // find/… через тул bash). Hard stop после второго фейла evidence-based
111
+ // (реальные фейлы команд), не классификация задач (правило #10).
109
112
  forbiddenBashFailures = new Map();
110
113
  searchRunner;
111
- webSearchThreshold;
112
114
  searchesThisSession = 0;
113
115
  retryWebSearchSignatures = new Set();
114
116
  lastErrorSearchAt = 0;
115
117
  errorSearchCooldownMs;
116
118
  hallucinationDetector = null;
117
119
  /**
118
- * The execution-plugin's mutable state. Owned HERE and passed BY REFERENCE
119
- * to the plugin factory — the plugin writes deps.state.* directly, so the
120
- * assignments are visible on the module without any write-back.
120
+ * Мутабельное состояние execution-плагина. Владеем ЗДЕСЬ, передаём ПО ССЫЛКЕ
121
+ * фабрике плагина — плагин пишет deps.state.* напрямую, записи видны модулю
122
+ * без write-back.
121
123
  */
122
124
  state = {
123
125
  lastRecoveryIteration: -STUCK_RECOVERY_COOLDOWN,
@@ -126,17 +128,22 @@ export class ExecutionModule {
126
128
  stuckNotified: false,
127
129
  lastStuckWarnKey: "",
128
130
  lastStuckWarnIter: -STUCK_WARN_REPEAT_EVERY,
131
+ escalationDirectiveSent: false,
129
132
  depsGateHints: new Map(),
130
133
  mutationsWithoutPlan: 0,
131
134
  planNudgeSent: false,
132
135
  typecheckFailures: new Map(),
133
136
  };
134
137
  /**
135
- * Read/write accessor for the tracker, shared with the plan-tool and
136
- * execution-plugin factories. The `plan` tool's abort action clears it
137
- * (current = null) — the setter keeps this.tracker in sync. Object-literal
138
- * getters bind `this` to the literal, so the instance is captured in a
139
- * closure instead.
138
+ * Read/write доступ к трекеру, общий для фабрик plan-tool и execution-plugin.
139
+ * Abort тула `plan` чистит его (current = null) — сеттер держит this.tracker
140
+ * в синхроне. Геттеры литерала биндят `this` на литерал, поэтому инстанс
141
+ * захвачен замыканием.
142
+ *
143
+ * Сеттер зеркалит per-session слот (план следует за сессией): план-тулы
144
+ * (`abort/switch/delete/purge/...`) пишут через trackerRef напрямую, минуя
145
+ * setPlan — без хука слот протух бы и воскрешал отменённый план возвратом
146
+ * в сессию. IO fail-open: слот не должен ронять тулы.
140
147
  */
141
148
  trackerRef = (() => {
142
149
  const self = this;
@@ -146,86 +153,191 @@ export class ExecutionModule {
146
153
  },
147
154
  set current(v) {
148
155
  self.tracker = v;
156
+ self.flushSessionPlan();
149
157
  },
150
158
  };
151
159
  })();
160
+ /** Зеркалит текущий трекер в слот активной сессии (null → clear).
161
+ * Вызывать после прямых присвоений this.tracker вне setPlan/switchSession. */
162
+ flushSessionPlan() {
163
+ if (!this.sessionId)
164
+ return;
165
+ try {
166
+ if (this.tracker)
167
+ this.store.saveSessionActive(this.sessionId, this.tracker.getPlan());
168
+ else
169
+ this.store.clearSessionActive(this.sessionId);
170
+ }
171
+ catch {
172
+ // fail-open: per-session слот не должен ронять тулы/гейты
173
+ }
174
+ }
152
175
  constructor(baseDir, stuckThreshold = 6, webSearchThreshold = 5, searchRunner = performWebSearch, errorSearchCooldownMs = ERROR_SEARCH_MIN_INTERVAL_MS) {
153
176
  this.baseDir = baseDir;
154
177
  this.verifier = new StepVerifier(baseDir);
155
178
  this.stuckDetector = new StuckDetector(stuckThreshold, 3, webSearchThreshold);
156
179
  this.auditor = new Auditor(baseDir);
157
180
  this.store = new PlanStore(baseDir);
158
- this.webSearchThreshold = webSearchThreshold;
159
181
  this.searchRunner = searchRunner;
160
182
  this.errorSearchCooldownMs = errorSearchCooldownMs;
161
183
  }
184
+ /** Ставит план активным (синхрон указателя, персист, сброс завершённого/скипов). */
162
185
  setPlan(plan) {
163
186
  this.tracker = new PlanTracker(plan);
164
- // Advance the pointer past any done/skipped steps of a restored/replanned
165
- // plan so display ("current: step N") and the off-track alignment match
166
- // reality instead of always pointing at step 1.
187
+ // Указатель мимо done/skipped шагов восстановленного/перепланированного
188
+ // плана: display ("current: step N") и off-track alignment матчатся с
189
+ // реальностью, а не вечно указывают на шаг 1.
167
190
  this.tracker.syncCurrentStep();
168
191
  this.store.saveActive(plan);
169
- this.completedPlan = null; // a new plan supersedes the completed one
170
- this._auditSkipsRemaining = 0; // plan explicitly created — arm the audit gate
192
+ if (this.sessionId) {
193
+ try {
194
+ this.store.saveSessionActive(this.sessionId, plan);
195
+ }
196
+ catch {
197
+ // fail-open: per-session слот не должен ронять создание плана
198
+ }
199
+ }
200
+ this.completedPlan = null; // новый план вытесняет завершённый
201
+ this._auditSkipsRemaining = 0; // план явно создан — взводим audit-gate
202
+ }
203
+ /** Переключает трекер на план другой сессии. Персистит текущий трекер
204
+ * в слот старой сессии (пустота тоже запоминается через clear, чтобы
205
+ * возврат не воскрешал чужой план), загружает план новой сессии.
206
+ * Завершённый на диске план — в архив, как в restorePlan. */
207
+ switchSession(id) {
208
+ const prev = this.sessionId;
209
+ if (prev === id)
210
+ return;
211
+ // Персист уходящей стороны. Стартовый трекер без sessionId (restorePlan
212
+ // до известной сессии) усыновляется новой сессией, а не теряется.
213
+ if (this.tracker) {
214
+ try {
215
+ this.store.saveSessionActive(prev ?? id ?? "", this.tracker.getPlan());
216
+ }
217
+ catch {
218
+ // fail-open: битый id слота / ФС — трекер в памяти остаётся
219
+ }
220
+ }
221
+ else if (prev) {
222
+ try {
223
+ this.store.clearSessionActive(prev);
224
+ }
225
+ catch {
226
+ /* best-effort */
227
+ }
228
+ }
229
+ this.sessionId = id;
230
+ this.completedPlan = null;
231
+ if (!id) {
232
+ this.tracker = null;
233
+ this._auditSkipsRemaining = 0;
234
+ return;
235
+ }
236
+ let plan = null;
237
+ try {
238
+ plan = this.store.loadSessionActive(id);
239
+ }
240
+ catch {
241
+ plan = null;
242
+ }
243
+ if (!plan) {
244
+ this.tracker = null;
245
+ this._auditSkipsRemaining = 0;
246
+ return;
247
+ }
248
+ const restored = new PlanTracker(plan);
249
+ if (restored.isComplete()) {
250
+ this.store.archivePlan(plan);
251
+ try {
252
+ this.store.clearSessionActive(id);
253
+ }
254
+ catch {
255
+ /* best-effort */
256
+ }
257
+ this.tracker = null;
258
+ return;
259
+ }
260
+ this.tracker = restored;
261
+ this.tracker.syncCurrentStep();
262
+ this._auditSkipsRemaining = 1; // как восстановленный план
171
263
  }
172
- /** Wire the hallucination detector so the plan tool can check whether
173
- * a file was tracked as deleted (auto-switch kind gate). */
264
+ /** Забывает сессию: чистит её слот плана. Удаляемая сессия не может быть
265
+ * текущей (web гардит 409), но REPL /delete — может: тогда сбрасываем
266
+ * и трекер, чтобы не висел план-призрак. */
267
+ forgetSession(id) {
268
+ try {
269
+ this.store.clearSessionActive(id);
270
+ }
271
+ catch {
272
+ /* best-effort */
273
+ }
274
+ if (this.sessionId === id) {
275
+ this.sessionId = null;
276
+ this.tracker = null;
277
+ this.completedPlan = null;
278
+ this._auditSkipsRemaining = 0;
279
+ }
280
+ }
281
+ /** Вяжет детектор галлюцинаций: тул plan проверяет удалённость файлов (auto-switch kind gate). */
174
282
  setHallucinationDetector(detector) {
175
283
  this.hallucinationDetector = detector;
176
284
  }
177
- /** Remember the last completed plan so the final audit can still verify it
178
- * after the tracker was cleared by the auto-archive. */
285
+ /** Запоминает последний завершённый план: финальный аудит проверит его и
286
+ * после очистки трекера авто-архивом. */
179
287
  recordCompleted(plan) {
180
288
  this.completedPlan = plan;
181
289
  }
182
- /** Forget the completed plan (abort/delete/purge — nothing left to audit). */
290
+ /** Забывает завершённый план (abort/delete/purge — аудитить нечего). */
183
291
  clearCompleted() {
184
292
  this.completedPlan = null;
185
293
  }
294
+ /** Восстанавливает активный план с диска. False, если нет/уже завершён (тогда в архив). */
186
295
  restorePlan() {
187
296
  const plan = this.store.loadActive();
188
297
  if (!plan)
189
298
  return false;
190
299
  const restored = new PlanTracker(plan);
191
- // A plan that was already complete when saved must not come back as the
192
- // active plan — archive it so it cannot re-anchor the agent (restored
193
- // completed plans caused the "continue step 4" loop on restart).
300
+ // План, уже завершённый на момент сохранения, активным не возвращается —
301
+ // архивируем, чтобы не якорил агента (восстановленные готовые планы давали
302
+ // петлю "continue step 4" на рестарте).
194
303
  if (restored.isComplete()) {
195
304
  this.store.archivePlan(plan);
196
305
  this.tracker = null;
197
306
  return false;
198
307
  }
199
308
  this.tracker = restored;
200
- // Advance to the first non-done step
309
+ // Двигаем к первому недонесённому шагу
201
310
  this.tracker.syncCurrentStep();
202
- this._auditSkipsRemaining = 1; // restored plan — one free pass
311
+ this._auditSkipsRemaining = 1; // восстановленный план — один свободный проход
203
312
  return true;
204
313
  }
314
+ /** Трекер (null без активного плана). */
205
315
  getTracker() {
206
316
  return this.tracker;
207
317
  }
208
- /** The currently tracked plan (single source of truth for UI consumers).
209
- * Null in a fresh session — a disk-resident plan is restored only when
210
- * resuming a session with history, so the REPL must never see stale plans. */
318
+ /** Текущий трекаемый план (единый источник правды для UI).
319
+ * Null в свежей сессии — план с диска восстанавливается только при resume
320
+ * сессии с историей, REPL протухших планов не видит. */
211
321
  getActivePlan() {
212
322
  return this.tracker?.getPlan() ?? null;
213
323
  }
324
+ /** Стор планов. */
214
325
  getStore() {
215
326
  return this.store;
216
327
  }
328
+ /** Stuck-детектор. */
217
329
  getStuckDetector() {
218
330
  return this.stuckDetector;
219
331
  }
220
332
  /**
221
- * Automatic web search for a repeatedly failing error (>= errorWebSearch.threshold
222
- * identical signatures). Fires the search fire-and-forget (never blocks the tool
223
- * result) and queues the results as a <system-summary> in pendingMessages.
333
+ * Автопоиск в вебе повторяющейся ошибки (≥ порога errorWebSearch одинаковых
334
+ * сигнатур). Поиск fire-and-forget (результат тула не блокирует), результаты
335
+ * очередятся <system-summary> в pendingMessages.
224
336
  *
225
- * Gates: webSearch master switch + errorWebSearch switch → repeated signature
226
- * → not-searched (or retried) → session cap → meaningful error text →
227
- * cooldown. One search per signature; empty results trigger a single retry
228
- * (unmarkErrorSearched once), then the signature stays searched.
337
+ * Гейты: мастер-свитч webSearch + свитч errorWebSearch → повторная сигнатура
338
+ * → не-исканная (или ретрайная) → сессионный кап → содержательный текст ошибки →
339
+ * кулдаун. Один поиск на сигнатуру; пустые результаты — один ретрай
340
+ * (размаркировка раз), затем сигнатура считается исканной.
229
341
  */
230
342
  maybeSearchError(ctx, call) {
231
343
  if (ctx.config?.webSearch?.enabled === false)
@@ -282,19 +394,19 @@ export class ExecutionModule {
282
394
  .catch((e) => ctx.logger?.warn(`error web search: ${e.message}`));
283
395
  }
284
396
  /**
285
- * Final audit before the agent may declare the task done. Returns null when
286
- * no plan is active; otherwise checks plan completion + artifact existence.
397
+ * Финальный аудит перед правом агента заявить "готово". Null без активного
398
+ * плана; иначе — готовность плана + существование артефактов.
287
399
  */
288
400
  /**
289
- * Evidence-based auto-close of pending plan steps whose deliverables exist
290
- * on disk. Small models routinely finish the actual work but forget the
291
- * `plan update` bookkeeping (observed ses_mthn3c2a: 2/5 steps marked done
292
- * while every artifact existed — the audit gate then rejected a correct
293
- * final answer and burned all retry budget on bookkeeping, not on work).
401
+ * Evidence-based автозакрытие пендинг шагов с deliverables на диске. Мелкие
402
+ * модели рутинно доделывают работу, но забывают бухгалтерию `plan update`
403
+ * (было в ses_mthn3c2a: 2/5 шагов done при всех артефактах на месте — audit-gate
404
+ * реджектил корректный финальный ответ и жёг retry-бюджет на бухгалтерию, не
405
+ * на работу).
294
406
  *
295
- * Standard is the SAME one the auditor already applies to done steps: every
296
- * file-like token in the step description resolves to an existing file.
297
- * Steps without any file token stay pending (nothing to verify).
407
+ * Стандарт ТОТ ЖЕ, что аудитор применяет к done-шагам: каждый файлоподобный
408
+ * токен описания резолвится в существующий файл. Шаги без файловых токенов
409
+ * остаются пендинг (верифицировать нечего).
298
410
  */
299
411
  autoCloseEvidenceSteps(plan) {
300
412
  let closed = 0;
@@ -304,20 +416,43 @@ export class ExecutionModule {
304
416
  const tokens = extractFileLikeTokens(stripUrls(step.description));
305
417
  if (tokens.length === 0)
306
418
  continue;
307
- const allExist = tokens.every((f) => findExistingFile(this.baseDir, f) !== null);
308
- if (!allExist)
309
- continue;
419
+ const resolved = tokens.map((f) => findExistingFile(this.baseDir, f));
420
+ if (step.kind === "delete") {
421
+ // Delete-шаг закрыт, только если каждая именованная цель ОТСУТСТВУЕТ.
422
+ // (Старая existence-проверка маркила done при файлах на месте —
423
+ // deliverable-gate инвертировался.)
424
+ if (resolved.some((r) => r !== null))
425
+ continue;
426
+ }
427
+ else {
428
+ // create: каждая цель существует И непуста. Пустой плейсхолдер — не
429
+ // deliverable, а аудитор смотрит только существование — автозакрытие
430
+ // дало бы ложный "passed".
431
+ if (resolved.some((r) => r === null))
432
+ continue;
433
+ const empty = resolved.some((r) => {
434
+ try {
435
+ return readFileSync(r, "utf-8").trim().length < 10;
436
+ }
437
+ catch {
438
+ return false; // unreadable — fall back to existence
439
+ }
440
+ });
441
+ if (empty)
442
+ continue;
443
+ }
310
444
  step.status = "done";
311
- step.note = `auto-closed by final audit: all named files exist (${tokens.join(", ")})`;
445
+ step.note = `auto-closed by final audit: ${step.kind === "delete" ? "all named files removed" : "all named files exist with content"} (${tokens.join(", ")})`;
312
446
  closed++;
313
447
  }
314
448
  return closed;
315
449
  }
450
+ /** Финальный аудит активного/завершённого плана: файлы, тесты, tsc, пендинги. Null — аудитить нечего. */
316
451
  async runFinalAudit() {
317
- // No active tracker: audit the last completed plan instead of bailing.
318
- // A plan finished via `plan update` auto-archives and clears the tracker;
319
- // without this the final audit (tests + tsc + file checks) was silently
320
- // skipped and the model could declare success on broken artifacts.
452
+ // Нет активного трекера: аудитим последний завершённый план вместо bail.
453
+ // План, закрытый через `plan update`, авто-архивируется и чистит трекер;
454
+ // без этого финальный аудит (тесты + tsc + файлы) молча скипался и модель
455
+ // заявляла успех на битых артефактах.
321
456
  if (!this.tracker) {
322
457
  if (!this.completedPlan)
323
458
  return null;
@@ -328,6 +463,7 @@ export class ExecutionModule {
328
463
  const done = plan.steps.filter((s) => s.status === "done").length;
329
464
  if (autoClosed > 0) {
330
465
  this.store.saveActive(plan);
466
+ this.flushSessionPlan();
331
467
  }
332
468
  return {
333
469
  passed: audit.passed && pendingSteps.length === 0,
@@ -346,6 +482,7 @@ export class ExecutionModule {
346
482
  const autoClosed = this.autoCloseEvidenceSteps(plan);
347
483
  if (autoClosed > 0) {
348
484
  this.store.saveActive(plan);
485
+ this.flushSessionPlan();
349
486
  }
350
487
  const audit = await this.auditor.audit(plan);
351
488
  const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
@@ -360,6 +497,7 @@ export class ExecutionModule {
360
497
  summary: audit.summary,
361
498
  };
362
499
  }
500
+ /** Блок системного промпта с чеклистом плана (null без активного/незавершённого). */
363
501
  getSystemPromptBlock() {
364
502
  if (!this.tracker || this.tracker.isComplete())
365
503
  return null;
@@ -371,14 +509,13 @@ export class ExecutionModule {
371
509
  };
372
510
  }
373
511
  /**
374
- * Short "current plan" line for the compaction summary. Null when no plan
375
- * is active. For 9B models the explicit instruction "continue it, do NOT
376
- * create a new plan" matters — after compaction the plan block is visible
377
- * but the model re-creates the plan anyway (observed: ses_msvuao0h).
512
+ * Короткая строка "текущий план" для сводки компакции. Null без активного
513
+ * плана. Для 9B-моделей важна явная инструкция "continue it, do NOT create
514
+ * a new plan" — после компакции блок плана виден, но модель всё равно
515
+ * пересоздаёт план (было: ses_msvuao0h).
378
516
  *
379
- * When the plan is complete, the summary says the plan is done and the
380
- * model may create a new plan — NOT "continue step 4, do NOT create a
381
- * new plan" which anchored the ses_msxjbnm4 loop.
517
+ * Готовый план в сводке — "план done, можно новый", а НЕ "continue step 4,
518
+ * do NOT create a new plan", якоривший петлю ses_msxjbnm4.
382
519
  */
383
520
  getPlanSummary() {
384
521
  if (!this.tracker)
@@ -387,11 +524,23 @@ export class ExecutionModule {
387
524
  const done = plan.steps.filter((s) => s.status === "done").length;
388
525
  const total = plan.steps.length;
389
526
  if (this.tracker.isComplete()) {
390
- return `[${plan.id}] "${plan.title}" ${done}/${total} done — plan complete. You may create a new plan for new work (plan create), or reply with your final answer.`;
527
+ return t("exec.plan_summary_complete", {
528
+ id: plan.id,
529
+ title: plan.title,
530
+ done: String(done),
531
+ total: String(total),
532
+ });
391
533
  }
392
534
  const current = this.tracker.getCurrentStepIndex() + 1;
393
- return `[${plan.id}] "${plan.title}" ${done}/${total} done, current: step ${current} — continue it (plan show / re-plan), do NOT create a new plan`;
535
+ return t("exec.plan_summary_continue", {
536
+ id: plan.id,
537
+ title: plan.title,
538
+ done: String(done),
539
+ total: String(total),
540
+ current: String(current),
541
+ });
394
542
  }
543
+ /** Определения тулов модуля (plan/todo/verify через фабрику plan-tool). */
395
544
  getToolDefinitions() {
396
545
  return createPlanToolDefinitions({
397
546
  store: this.store,
@@ -405,6 +554,7 @@ export class ExecutionModule {
405
554
  missingStepDeliverables: (s) => this.missingStepDeliverables(s),
406
555
  stillExistingDeliverables: (s) => this.stillExistingDeliverables(s),
407
556
  parseKinds: (a, st) => this.parseKinds(a, st),
557
+ parseChecks: (a, st) => this.parseChecks(a, st),
408
558
  getTaskText: (c) => this.getTaskText(c),
409
559
  typecheckFailures: this.state.typecheckFailures,
410
560
  baseDir: this.baseDir,
@@ -413,6 +563,7 @@ export class ExecutionModule {
413
563
  : undefined,
414
564
  });
415
565
  }
566
+ /** Плагин модуля (хуки цикла из фабрики execution-plugin). */
416
567
  getPlugin() {
417
568
  return createExecutionPlugin({
418
569
  stuckDetector: this.stuckDetector,
@@ -422,7 +573,7 @@ export class ExecutionModule {
422
573
  state: this.state,
423
574
  store: this.store,
424
575
  checkPlanAlignment: (call) => this.checkPlanAlignment(call),
425
- advancePlanIfStepComplete: (cm, sl) => this.advancePlanIfStepComplete(cm, sl),
576
+ advancePlanIfStepComplete: (sl) => this.advancePlanIfStepComplete(sl),
426
577
  maybeSearchError: (ctx, call) => this.maybeSearchError(ctx, call),
427
578
  });
428
579
  }
@@ -448,17 +599,17 @@ export class ExecutionModule {
448
599
  const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map(planPathToken);
449
600
  if (stepPaths.length === 0)
450
601
  return null;
451
- // Only path-ish arguments are inspected. JSON-stringifying the whole args
452
- // blob made write_file/edit_file `content` feed its code tokens into the
453
- // off-path check (e.g. `import "./styles.css"` inside the file body) and
454
- // raised phantom alignment warnings for on-track writes.
602
+ // Инспектируем только path-like аргументы. JSON-стринг всего blob'а args
603
+ // скармливал кодовые токены `content` write_file/edit_file в off-path чек
604
+ // (напр. `import "./styles.css"` в теле файла) и давал фантомные alignment-
605
+ // варнинги на on-track записях.
455
606
  const argStr = this.pathArgStrings(call.arguments).join(" ");
456
607
  const callPaths = extractFileLikeTokens(stripUrls(argStr)).map(planPathToken);
457
608
  if (callPaths.length === 0)
458
609
  return null;
459
- // Files that already belong to COMPLETED (done/skipped) steps are
460
- // legitimate rewrite targets — the agent iterates on them while fixing
461
- // LSP/type errors. Blocking those would stop normal bug-fixing.
610
+ // Файлы уже ЗАКРЫТЫХ (done/skipped) шагов — легитимные цели переписи:
611
+ // агент итерируется по ним, чиня LSP/type-ошибки. Блок здесь остановил бы
612
+ // нормальный багфикс.
462
613
  const plan = this.tracker.getPlan();
463
614
  const finishedPaths = new Set();
464
615
  for (const s of plan.steps) {
@@ -468,11 +619,11 @@ export class ExecutionModule {
468
619
  .forEach((p) => finishedPaths.add(p));
469
620
  }
470
621
  }
471
- // Bidirectional path-token overlap, used for BOTH checks below. A step may
472
- // name "main.ts" while the tool calls "src/main.ts" (or vice versa), so a
473
- // one-directional `s.includes(p)` missed the common nested/partial case
474
- // (observed: rewriting a completed step's src/main.ts was flagged as
475
- // off-path for the current step and helped hard-block the third call).
622
+ // Двунаправленный overlap path-токенов, общий для ОБЕИХ проверок ниже. Шаг
623
+ // может звать "main.ts", а тул — "src/main.ts" (или наоборот): односторонний
624
+ // `s.includes(p)` частый nested/частичный кейс пропускал (было: перепись
625
+ // src/main.ts закрытого шага флаговалась off-path текущего и помогла
626
+ // hard-block'нуть третий вызов).
476
627
  const matchesAny = (p, tokens) => tokens.some((t) => t === p || t.includes(p) || p.includes(t));
477
628
  const finished = [...finishedPaths];
478
629
  const offPath = callPaths.some((p) => {
@@ -488,10 +639,10 @@ export class ExecutionModule {
488
639
  tool: call.name,
489
640
  });
490
641
  }
491
- /** String values of the path-like arguments of a tool call. Limits the
492
- * off-path alignment check to arguments that actually name files (path,
493
- * file, dir, src, dst, source, target, input, output, destination, glob,
494
- * workdir, cwd, command, pattern) and never scans payload content. */
642
+ /** Строковые значения path-like аргументов вызова. Ограничивает off-path
643
+ * чек аргументами, реально именующими файлы (path, file, dir, src, dst,
644
+ * source, target, input, output, destination, glob, workdir, cwd, command,
645
+ * pattern), полезную нагрузку не сканирует. */
495
646
  pathArgStrings(args) {
496
647
  if (!args)
497
648
  return [];
@@ -510,21 +661,21 @@ export class ExecutionModule {
510
661
  }
511
662
  return out;
512
663
  }
513
- advancePlanIfStepComplete(contextManager, sessionLog) {
664
+ advancePlanIfStepComplete(sessionLog) {
514
665
  const step = this.tracker?.getCurrentStep();
515
666
  if (!step)
516
667
  return;
517
668
  const stepText = step.description.toLowerCase();
518
- // Shared file-token filter (allow-list of real extensions, extension-only
519
- // segments rejected). A step like "Удалить .js, оставить .tsx/.ts" must
520
- // NOT produce the phantom token "tsx/.ts" — that never exists, making
521
- // allGone trivially true and auto-advancing a delete step while the real
522
- // files to delete still exist (the raw regex path did exactly that).
669
+ // Общий файловый фильтр (allow-list реальных расширений, сегменты только
670
+ // из расширения отброшены). Шаг "Удалить .js, оставить .tsx/.ts" НЕ даёт
671
+ // фантомный токен "tsx/.ts" — его не существует, allGone тривиально true и
672
+ // автопродвинуло бы delete-шаг при живых файлах на удаление (сырой regex
673
+ // делал ровно так).
523
674
  const stepPaths = extractFileLikeTokens(stripUrls(step.description)) || [];
524
- // Step gate: verify dependencies when mentioned — check BEFORE file paths.
525
- // Requires an actual package-manager verb (rule #10 — no bare keyword
526
- // matching: "init" alone or "setup project" must NOT trigger the lockfile
527
- // hint, only concrete dependency-install commands do).
675
+ // Гейт шагов: зависимости проверяем при упоминании — ДО путей файлов.
676
+ // Нужен реальный глагол пакетного менеджера (правило #10 — без голого
677
+ // keyword-матчинга: один "init" или "setup project" локфайловый хинт НЕ
678
+ // триггерят, только конкретные install-команды).
528
679
  const isDepsStep = /(?:npm|bun|yarn|pnpm|pip|pip3|pipenv|poetry|composer|deno|go|gem|cargo)\s+(?:install|add|init|i|get)\b|bun\s+(?:add|install)|npm\s+(?:i|install|add)|(?:install|установ\w*)\s+(?:dependencies|зависимост\w*|пакет\w*|packages?)/i.test(stepText);
529
680
  if (isDepsStep) {
530
681
  const lockFiles = [
@@ -541,39 +692,36 @@ export class ExecutionModule {
541
692
  ];
542
693
  const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
543
694
  if (!hasLockFile) {
544
- if (contextManager) {
545
- const hints = this.state.depsGateHints.get(step.id) || 0;
546
- this.state.depsGateHints.set(step.id, hints + 1);
547
- const force = hints >= 1;
548
- const content = force
549
- ? t("exec.step_gate_deps_force", { step: String(step.id) })
550
- : t("exec.step_gate_deps", {
551
- step: String(step.id),
552
- description: step.description,
553
- });
554
- contextManager.addMessage({
555
- role: "user",
556
- content: `<system-summary>${content}</system-summary>`,
695
+ const hints = this.state.depsGateHints.get(step.id) || 0;
696
+ this.state.depsGateHints.set(step.id, hints + 1);
697
+ const force = hints >= 1;
698
+ const content = force
699
+ ? t("exec.step_gate_deps_force", { step: String(step.id) })
700
+ : t("exec.step_gate_deps", {
701
+ step: String(step.id),
702
+ description: step.description,
557
703
  });
558
- }
704
+ this.pendingMessages.push({
705
+ role: "user",
706
+ content: `<system-summary>${content}</system-summary>`,
707
+ });
559
708
  return;
560
709
  }
561
710
  }
562
711
  if (stepPaths.length === 0)
563
712
  return;
564
- // Resolve tokens with the SAME subtree walk the deliverable gate and the
565
- // auditor use (findExistingFile), so a nested file
566
- // ("bicycle-shop/src/bicycles.ts" for token "src/bicycles.ts") counts as
567
- // present. The old exact existsSync(resolve()) never matched nested
568
- // projects, so auto-advance stayed silent even after the model wrote the
569
- // real file.
713
+ // Токены резолвим ТЕМ ЖЕ обходом поддерева, что deliverable-gate и аудитор
714
+ // (findExistingFile): вложенный файл ("bicycle-shop/src/bicycles.ts" для
715
+ // токена "src/bicycles.ts") засчитывается. Старый точный
716
+ // existsSync(resolve()) вложенные проекты не матчил — автопродвижение
717
+ // молчало и после записи реального файла.
570
718
  const resolved = stepPaths.map((p) => findExistingFile(this.baseDir, p));
571
719
  const allExist = resolved.every((r) => r !== null);
572
720
  const allGone = resolved.every((r) => r === null);
573
721
  const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
574
722
  if (!satisfied)
575
723
  return;
576
- // Step gate: verify files have content (not empty)
724
+ // Гейт шагов: файлы проверяем на контент (не пустые)
577
725
  if (step.kind !== "delete") {
578
726
  const emptyFiles = [];
579
727
  for (const r of resolved) {
@@ -586,57 +734,57 @@ export class ExecutionModule {
586
734
  }
587
735
  }
588
736
  catch {
589
- // If we can't read it, rely on the existence result
737
+ // Нечитаем — откат на existence
590
738
  }
591
739
  }
592
- if (emptyFiles.length > 0 && contextManager) {
593
- contextManager.addMessage({
740
+ if (emptyFiles.length > 0) {
741
+ this.pendingMessages.push({
594
742
  role: "user",
595
743
  content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
596
744
  });
597
745
  return;
598
746
  }
599
747
  }
600
- // All checks passed — advance
748
+ // Все проверки прошли — двигаем
601
749
  this.tracker?.updateStepStatus(step.id, "done");
602
750
  const hadNext = this.tracker?.advance() ?? false;
603
751
  if (this.tracker) {
604
752
  this.store.saveActive(this.tracker.getPlan());
753
+ this.flushSessionPlan();
605
754
  sessionLog?.plan("auto-advance", `Step ${step.id} auto-completed | ${this.tracker.getProgressString()} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
606
755
  }
607
- if (contextManager) {
608
- const nextStep = hadNext ? this.tracker?.getCurrentStep() : null;
609
- const nextMsg = nextStep
610
- ? t("exec.step_gate_ok", {
611
- step: String(step.id),
612
- nextStep: String(nextStep.id),
613
- nextDesc: nextStep.description,
614
- })
615
- : t("exec.step_gate_last", { step: String(step.id) });
616
- contextManager.addMessage({
617
- role: "user",
618
- content: `<system-summary>${nextMsg}</system-summary>`,
619
- });
620
- }
621
- // Auto-archive a plan that just became complete (all steps done/skipped).
622
- // Keeps a finished plan from staying active forever and anchoring the agent
623
- // to a completed task ("continue step 4, do NOT create a new plan" — the
624
- // ses_msxjbnm4 loop). The per-step deliverable gate already verified files.
756
+ const nextStep = hadNext ? this.tracker?.getCurrentStep() : null;
757
+ const nextMsg = nextStep
758
+ ? t("exec.step_gate_ok", {
759
+ step: String(step.id),
760
+ nextStep: String(nextStep.id),
761
+ nextDesc: nextStep.description,
762
+ })
763
+ : t("exec.step_gate_last", { step: String(step.id) });
764
+ this.pendingMessages.push({
765
+ role: "user",
766
+ content: `<system-summary>${nextMsg}</system-summary>`,
767
+ });
768
+ // Авто-архив только что завершённого плана (все шаги done/skipped).
769
+ // Готовый план не висит активным вечно, якоря агента на готовую задачу
770
+ // ("continue step 4, do NOT create a new plan" — петля ses_msxjbnm4).
771
+ // Per-step deliverable-gate файлы уже проверил.
625
772
  if (this.tracker?.isComplete()) {
626
773
  const completed = this.tracker.getPlan();
627
774
  this.store.archivePlan(completed);
628
775
  this.recordCompleted(completed); // final audit still verifies it
629
776
  this.tracker = null;
777
+ this.flushSessionPlan(); // завершённый план не висит в слоте сессии
630
778
  sessionLog?.plan("auto-archive", `Plan ${completed.id} complete — archived`);
631
779
  }
632
780
  }
633
781
  /**
634
- * File tokens named in a step description that do not resolve to a real
635
- * file. Uses the same shared token extractor (allow-list of real file
636
- * extensions, URLs stripped) and the auditor's subtree resolver, so
637
- * "bicycles.ts" finds "bicycle-shop/src/data/bicycles.ts" and a hostname in
638
- * the description is never reported as a missing deliverable. Steps with no
639
- * file tokens (pure commands, verification runs) cannot be verified.
782
+ * Файловые токены из описания шага, не резолвящиеся в реальный файл.
783
+ * Тот же общий экстрактор токенов (allow-list реальных расширений, URL
784
+ * вырезаны) и subtree-резолвер аудитора: "bicycles.ts" находит
785
+ * "bicycle-shop/src/data/bicycles.ts", а hostname в описании — никогда
786
+ * missing deliverable. Шаги без файловых токенов (чистые команды, прогоны
787
+ * проверок) неверифицируемы.
640
788
  */
641
789
  missingStepDeliverables(step) {
642
790
  const tokens = extractFileLikeTokens(stripUrls(step.description));
@@ -645,9 +793,9 @@ export class ExecutionModule {
645
793
  return tokens.filter((p) => !findExistingFile(this.baseDir, p));
646
794
  }
647
795
  /**
648
- * File tokens named in a step description that still resolve to a real
649
- * file. The inverse of missingStepDeliverables — used by the gate for
650
- * delete-kind steps ("done" means the files are gone).
796
+ * Файловые токены из описания шага, всё ещё резолвящиеся в реальный файл.
797
+ * Инверсия missingStepDeliverables — для гейта delete-шагов ("done" значит
798
+ * файлы пропали).
651
799
  */
652
800
  stillExistingDeliverables(step) {
653
801
  const tokens = extractFileLikeTokens(stripUrls(step.description));
@@ -655,17 +803,17 @@ export class ExecutionModule {
655
803
  return [];
656
804
  return tokens.filter((p) => findExistingFile(this.baseDir, p));
657
805
  }
658
- /** Whether a step description names at least one real file token. Steps
659
- * with no file tokens (pure commands, verification runs) cannot be
660
- * auto-verified — the done-gate is vacuous for them. */
806
+ /** Есть ли в описании шага хоть один реальный файловый токен. Шаги без
807
+ * токенов (чистые команды, прогоны проверок) не автоверифицируются —
808
+ * done-gate для них vacuous. */
661
809
  hasStepDeliverables(step) {
662
810
  return extractFileLikeTokens(stripUrls(step.description)).length > 0;
663
811
  }
664
812
  /**
665
- * Validate the optional per-step `kinds` argument for plan create and
666
- * update-with-steps. Returns length-aligned kinds or an i18n error string.
667
- * Absent kinds → null (caller falls back to all-create semantics). Used by
668
- * BOTH branches so a rebuild via update can never silently drop kinds.
813
+ * Валидирует опциональный пер-шаговый аргумент `kinds` для create плана и
814
+ * update-с-шагами. Возвращает выровненные по длине kinds или i18n-строку
815
+ * ошибки. Нет kinds → null (вызывающий фолбэчится на all-create). Общий для
816
+ * ОБЕИХ веток: перестройка через update не дропает kinds молча.
669
817
  */
670
818
  parseKinds(args, steps) {
671
819
  if (args.kinds === undefined)
@@ -689,18 +837,47 @@ export class ExecutionModule {
689
837
  return { kinds: kindsRaw, error: null };
690
838
  }
691
839
  /**
692
- * Extract the original task statement from the first user message in the
693
- * conversation history. Used to validate plan coverage against requirements.
840
+ * Валидирует пер-шаговый аргумент `checks` для create / re-plan плана.
841
+ * Checks ОБЯЗАТЕЛЬНЫ (шаг без контракта верификации свой done не докажет):
842
+ * отсутствующие, несовпавшие по длине или с битым префиксом возвращают
843
+ * i18n-строку ошибки. Валидные префиксы: file-exists: / command: /
844
+ * manual: (см. plan-checks.ts).
845
+ */
846
+ parseChecks(args, steps) {
847
+ if (args.checks === undefined) {
848
+ return { checks: null, error: t("plan.checks_required") };
849
+ }
850
+ if (!Array.isArray(args.checks)) {
851
+ return { checks: null, error: t("plan.checks_not_array") };
852
+ }
853
+ const raw = args.checks.map(String);
854
+ if (raw.length !== steps.length) {
855
+ return { checks: null, error: t("plan.checks_mismatch") };
856
+ }
857
+ const bad = raw.filter((c) => !isValidCheck(c));
858
+ if (bad.length > 0) {
859
+ return {
860
+ checks: null,
861
+ error: t("plan.checks_invalid", {
862
+ kinds: bad.slice(0, 3).join("; "),
863
+ }),
864
+ };
865
+ }
866
+ return { checks: raw.map((c) => c.trim()), error: null };
867
+ }
868
+ /**
869
+ * Достаёт исходную формулировку задачи из первого user-сообщения истории.
870
+ * Нужно для валидации покрытия плана против требований.
694
871
  */
695
872
  getTaskText(ctx) {
696
873
  const manager = ctx?.contextManager;
697
874
  if (!manager)
698
875
  return "";
699
876
  const history = manager.getActiveHistory();
700
- // The first user message may be an injected <system-summary> (compaction
701
- // block) — it is NOT the task. Coverage must compare the plan against the
702
- // model's real instruction (observed: a compaction summary's stale file
703
- // list produced a bogus "plan is missing files" warning).
877
+ // Первое user-сообщение может быть инжектированным <system-summary>
878
+ // (блок компакции) — это НЕ задача. Покрытие сравнивает план с реальной
879
+ // инструкцией модели (было: протухший файловый список сводки компакции дал
880
+ // bogus-варнинг "в плане не хватает файлов").
704
881
  const firstUser = history.find((m) => {
705
882
  if (m.role !== "user")
706
883
  return false;