micro-models-agent 0.58.2 → 0.60.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 (233) hide show
  1. package/CHANGELOG.md +578 -556
  2. package/README.md +358 -358
  3. package/dist/certification/certifications.json +493 -493
  4. package/dist/i18n/en.json +732 -716
  5. package/dist/i18n/ru.json +732 -716
  6. package/dist/main.js +1243 -448
  7. package/dist/modules/browser/bridge-server.mjs +219 -219
  8. package/dist/skills/builtin/git.md +36 -36
  9. package/dist/skills/builtin/typescript.md +35 -35
  10. package/package.json +51 -51
  11. package/dist/cli/commands.js +0 -447
  12. package/dist/cli/completer.js +0 -167
  13. package/dist/cli/index.js +0 -2
  14. package/dist/cli/main.js +0 -153
  15. package/dist/cli/plugin-commands.js +0 -36
  16. package/dist/cli/repl-commands.js +0 -761
  17. package/dist/cli/repl.js +0 -702
  18. package/dist/cli/run-result.js +0 -33
  19. package/dist/cli/security-commands.js +0 -164
  20. package/dist/cli/setup.js +0 -237
  21. package/dist/config/config.js +0 -276
  22. package/dist/config/defaults.js +0 -141
  23. package/dist/config/domains.js +0 -179
  24. package/dist/config/experts.js +0 -15
  25. package/dist/config/index.js +0 -4
  26. package/dist/config/security.js +0 -213
  27. package/dist/config/types.js +0 -1
  28. package/dist/core/agent-moe.js +0 -102
  29. package/dist/core/agent.js +0 -1018
  30. package/dist/core/bootstrap.js +0 -481
  31. package/dist/core/crash-handler.js +0 -51
  32. package/dist/core/environment.js +0 -199
  33. package/dist/core/index.js +0 -2
  34. package/dist/core/prompt-builder.js +0 -76
  35. package/dist/core/session-logger.js +0 -251
  36. package/dist/core/types.js +0 -1
  37. package/dist/core/version.js +0 -26
  38. package/dist/core/workspace.js +0 -76
  39. package/dist/i18n/index.js +0 -46
  40. package/dist/index.js +0 -22
  41. package/dist/llm/image-utils.js +0 -143
  42. package/dist/llm/index.js +0 -4
  43. package/dist/llm/model-loader.js +0 -78
  44. package/dist/llm/openai-compat.js +0 -497
  45. package/dist/llm/orchestrator.js +0 -200
  46. package/dist/llm/provider.js +0 -10
  47. package/dist/llm/response.js +0 -39
  48. package/dist/llm/token-counter.js +0 -39
  49. package/dist/llm/types.js +0 -1
  50. package/dist/logger/app-logger.js +0 -189
  51. package/dist/logger/file-log.js +0 -151
  52. package/dist/logger/index.js +0 -1
  53. package/dist/migration/backup.js +0 -45
  54. package/dist/migration/detect.js +0 -50
  55. package/dist/migration/index.js +0 -2
  56. package/dist/modules/artifacts/store.js +0 -61
  57. package/dist/modules/browser/actions.js +0 -76
  58. package/dist/modules/browser/bridge-client.js +0 -199
  59. package/dist/modules/browser/bridge-path.js +0 -10
  60. package/dist/modules/browser/cookie-store.js +0 -24
  61. package/dist/modules/browser/driver.js +0 -136
  62. package/dist/modules/browser/index.js +0 -7
  63. package/dist/modules/browser/module.js +0 -29
  64. package/dist/modules/browser/session.js +0 -342
  65. package/dist/modules/browser/snapshot.js +0 -148
  66. package/dist/modules/browser/types.js +0 -12
  67. package/dist/modules/certification/cli.js +0 -213
  68. package/dist/modules/certification/fact-checker.js +0 -82
  69. package/dist/modules/certification/loader.js +0 -106
  70. package/dist/modules/certification/manifest.js +0 -58
  71. package/dist/modules/certification/runner.js +0 -245
  72. package/dist/modules/certification/scenarios.js +0 -407
  73. package/dist/modules/certification/types.js +0 -1
  74. package/dist/modules/context/chunk-query.js +0 -100
  75. package/dist/modules/context/fact-extractor.js +0 -168
  76. package/dist/modules/context/history.js +0 -15
  77. package/dist/modules/context/index.js +0 -1
  78. package/dist/modules/context/manager.js +0 -440
  79. package/dist/modules/execution/audit-runners.js +0 -206
  80. package/dist/modules/execution/auditor.js +0 -218
  81. package/dist/modules/execution/execution-plugin.js +0 -431
  82. package/dist/modules/execution/index.js +0 -8
  83. package/dist/modules/execution/module.js +0 -625
  84. package/dist/modules/execution/moe-executor.js +0 -304
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -196
  88. package/dist/modules/execution/plan-tool.js +0 -677
  89. package/dist/modules/execution/plan-validator.js +0 -153
  90. package/dist/modules/execution/planner.js +0 -94
  91. package/dist/modules/execution/stuck-detector.js +0 -746
  92. package/dist/modules/execution/tracker.js +0 -69
  93. package/dist/modules/execution/types.js +0 -1
  94. package/dist/modules/execution/verifier.js +0 -235
  95. package/dist/modules/execution/windows-commands.js +0 -41
  96. package/dist/modules/hallucination/confidence.js +0 -66
  97. package/dist/modules/hallucination/consistency.js +0 -26
  98. package/dist/modules/hallucination/detector.js +0 -47
  99. package/dist/modules/hallucination/factual.js +0 -169
  100. package/dist/modules/hallucination/index.js +0 -5
  101. package/dist/modules/hallucination/js-identifiers.js +0 -262
  102. package/dist/modules/hallucination/llm-judge.js +0 -101
  103. package/dist/modules/index.js +0 -5
  104. package/dist/modules/indexer/cache.js +0 -40
  105. package/dist/modules/indexer/index.js +0 -3
  106. package/dist/modules/indexer/module.js +0 -246
  107. package/dist/modules/indexer/project-profile.js +0 -183
  108. package/dist/modules/indexer/walker.js +0 -101
  109. package/dist/modules/lsp/check-tool.js +0 -58
  110. package/dist/modules/lsp/client.js +0 -389
  111. package/dist/modules/lsp/command.js +0 -60
  112. package/dist/modules/lsp/config.js +0 -135
  113. package/dist/modules/lsp/index.js +0 -3
  114. package/dist/modules/lsp/module.js +0 -260
  115. package/dist/modules/lsp/probe.js +0 -86
  116. package/dist/modules/lsp/project-root.js +0 -32
  117. package/dist/modules/lsp/startup-check.js +0 -144
  118. package/dist/modules/lsp/types.js +0 -1
  119. package/dist/modules/mcp/client.js +0 -399
  120. package/dist/modules/mcp/index.js +0 -3
  121. package/dist/modules/mcp/module.js +0 -142
  122. package/dist/modules/mcp/registry.js +0 -15
  123. package/dist/modules/memory/index.js +0 -1
  124. package/dist/modules/memory/module.js +0 -96
  125. package/dist/modules/memory/search.js +0 -42
  126. package/dist/modules/memory/store.js +0 -69
  127. package/dist/modules/pipelines/engine.js +0 -60
  128. package/dist/modules/pipelines/index.js +0 -3
  129. package/dist/modules/pipelines/parser.js +0 -56
  130. package/dist/modules/pipelines/template.js +0 -14
  131. package/dist/modules/plugins/builtin/lint-on-write.js +0 -334
  132. package/dist/modules/plugins/builtin/notify.js +0 -9
  133. package/dist/modules/plugins/index.js +0 -1
  134. package/dist/modules/plugins/loader.js +0 -70
  135. package/dist/modules/plugins/manager.js +0 -261
  136. package/dist/modules/plugins/types.js +0 -1
  137. package/dist/modules/pricing/index.js +0 -61
  138. package/dist/modules/pricing/prices.js +0 -129
  139. package/dist/modules/processes/detect.js +0 -34
  140. package/dist/modules/processes/index.js +0 -2
  141. package/dist/modules/processes/registry.js +0 -327
  142. package/dist/modules/processes/runner.js +0 -23
  143. package/dist/modules/providers/create.js +0 -22
  144. package/dist/modules/providers/fallback.js +0 -79
  145. package/dist/modules/providers/health.js +0 -46
  146. package/dist/modules/providers/index.js +0 -5
  147. package/dist/modules/providers/manager.js +0 -161
  148. package/dist/modules/providers/presets.js +0 -128
  149. package/dist/modules/providers/registry.js +0 -22
  150. package/dist/modules/providers/types.js +0 -1
  151. package/dist/modules/registry.js +0 -48
  152. package/dist/modules/security/audit-log.js +0 -136
  153. package/dist/modules/security/audit-notifier.js +0 -292
  154. package/dist/modules/security/command-validator.js +0 -219
  155. package/dist/modules/security/content-scanner.js +0 -53
  156. package/dist/modules/security/data-sanitizer.js +0 -89
  157. package/dist/modules/security/encryption.js +0 -242
  158. package/dist/modules/security/index.js +0 -14
  159. package/dist/modules/security/network-validator.js +0 -88
  160. package/dist/modules/security/path-validator.js +0 -203
  161. package/dist/modules/security/rate-limiter.js +0 -119
  162. package/dist/modules/security/security-policies.js +0 -531
  163. package/dist/modules/security/session-encryption.js +0 -210
  164. package/dist/modules/security/session-isolation.js +0 -95
  165. package/dist/modules/session/index.js +0 -3
  166. package/dist/modules/session/manager.js +0 -172
  167. package/dist/modules/session/module.js +0 -24
  168. package/dist/modules/session/store.js +0 -222
  169. package/dist/modules/session/types.js +0 -1
  170. package/dist/modules/skills/index.js +0 -2
  171. package/dist/modules/skills/loader.js +0 -72
  172. package/dist/modules/skills/matcher.js +0 -27
  173. package/dist/modules/skills/module.js +0 -129
  174. package/dist/modules/types.js +0 -1
  175. package/dist/modules/updater/checker.js +0 -96
  176. package/dist/modules/updater/index.js +0 -2
  177. package/dist/modules/updater/module.js +0 -116
  178. package/dist/modules/user-profile/compressor.js +0 -16
  179. package/dist/modules/user-profile/index.js +0 -1
  180. package/dist/modules/user-profile/profile.js +0 -68
  181. package/dist/tools/approve.js +0 -33
  182. package/dist/tools/attach-image.js +0 -101
  183. package/dist/tools/bash.js +0 -519
  184. package/dist/tools/browser.js +0 -115
  185. package/dist/tools/chunk-query.js +0 -100
  186. package/dist/tools/create-dir.js +0 -56
  187. package/dist/tools/delete-file.js +0 -63
  188. package/dist/tools/download-file.js +0 -117
  189. package/dist/tools/edit-file.js +0 -80
  190. package/dist/tools/enable-tools.js +0 -59
  191. package/dist/tools/executor.js +0 -154
  192. package/dist/tools/file-info.js +0 -47
  193. package/dist/tools/filter-tools.js +0 -17
  194. package/dist/tools/glob-tool.js +0 -27
  195. package/dist/tools/grep-tool.js +0 -125
  196. package/dist/tools/hidden-tools-block.js +0 -37
  197. package/dist/tools/index.js +0 -78
  198. package/dist/tools/list-dir.js +0 -49
  199. package/dist/tools/load-skill.js +0 -43
  200. package/dist/tools/mcp-call.js +0 -69
  201. package/dist/tools/move-file.js +0 -86
  202. package/dist/tools/path-utils.js +0 -101
  203. package/dist/tools/pipeline-run.js +0 -145
  204. package/dist/tools/preview.js +0 -2
  205. package/dist/tools/process-kill.js +0 -40
  206. package/dist/tools/process-list.js +0 -37
  207. package/dist/tools/process-log.js +0 -54
  208. package/dist/tools/question.js +0 -141
  209. package/dist/tools/read-file.js +0 -179
  210. package/dist/tools/recall.js +0 -118
  211. package/dist/tools/registry.js +0 -47
  212. package/dist/tools/remember.js +0 -68
  213. package/dist/tools/scope-check.js +0 -32
  214. package/dist/tools/search-history.js +0 -85
  215. package/dist/tools/subagent.js +0 -196
  216. package/dist/tools/types.js +0 -1
  217. package/dist/tools/user-input.js +0 -123
  218. package/dist/tools/web-browse.js +0 -87
  219. package/dist/tools/web-fetch.js +0 -119
  220. package/dist/tools/web-search.js +0 -105
  221. package/dist/tools/write-file.js +0 -82
  222. package/dist/ui/box.js +0 -77
  223. package/dist/ui/colors.js +0 -4
  224. package/dist/ui/diff.js +0 -178
  225. package/dist/ui/index.js +0 -6
  226. package/dist/ui/line-editor.js +0 -822
  227. package/dist/ui/line-math.js +0 -73
  228. package/dist/ui/md-formatter.js +0 -212
  229. package/dist/ui/output.js +0 -13
  230. package/dist/ui/plan-view.js +0 -103
  231. package/dist/ui/renderer.js +0 -259
  232. package/dist/ui/spinner.js +0 -70
  233. package/dist/ui/table.js +0 -144
@@ -1,218 +0,0 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { resolve, join, basename } from "path";
3
- import { t } from "../../i18n/index";
4
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
5
- import { SKIP_DIRS, findTestFile, findTypecheckRoot, hasTestStep, runTests, runTypecheck, } from "./audit-runners";
6
- export { parseTypecheckErrors } from "./audit-runners";
7
- const MASS_EDIT_THRESHOLD = 10;
8
- /** Subtree walk depth cap for file resolution. */
9
- const RESOLVE_MAX_DEPTH = 8;
10
- /**
11
- * Resolve a plan-step file token to an existing file.
12
- *
13
- * A step description frequently names a file by its bare basename
14
- * ("App.tsx") or a partially-prefixed path ("src/App.tsx") while the real
15
- * file lives in a nested project directory relative to the agent baseDir
16
- * (e.g. `bicycle-shop/src/App.tsx`). Naively resolving against baseDir
17
- * reports the file as missing — observed in session ses_mssl5izw where the
18
- * final audit gate rejected a fully-completed task with "1 files missing"
19
- * forever, sending the model into a read/retry loop.
20
- *
21
- * Strategy: exact resolve first, then a bounded subtree walk matching either
22
- * the exact basename or a path suffix (so both "App.tsx" and
23
- * "src/App.tsx" find "bicycle-shop/src/App.tsx").
24
- */
25
- export function findExistingFile(baseDir, filePath) {
26
- const direct = resolve(baseDir, filePath);
27
- if (existsSync(direct))
28
- return direct;
29
- const name = basename(filePath).toLowerCase();
30
- const suffix = filePath.replace(/\\/g, "/").toLowerCase();
31
- let found = null;
32
- const walk = (dir, depth) => {
33
- if (found || depth > RESOLVE_MAX_DEPTH)
34
- return;
35
- let entries;
36
- try {
37
- entries = readdirSync(dir, { withFileTypes: true });
38
- }
39
- catch {
40
- return;
41
- }
42
- for (const e of entries) {
43
- if (found)
44
- return;
45
- const full = join(dir, e.name);
46
- if (e.isDirectory()) {
47
- if (SKIP_DIRS.has(e.name))
48
- continue;
49
- walk(full, depth + 1);
50
- }
51
- else if (e.name.toLowerCase() === name ||
52
- full
53
- .replace(/\\/g, "/")
54
- .toLowerCase()
55
- .endsWith("/" + suffix)) {
56
- found = full;
57
- return;
58
- }
59
- }
60
- };
61
- walk(baseDir, 0);
62
- return found;
63
- }
64
- export class Auditor {
65
- baseDir;
66
- constructor(baseDir) {
67
- this.baseDir = baseDir;
68
- }
69
- async audit(plan) {
70
- // Only steps the model claims completed may require their named files to
71
- // exist. Skipped/pending steps must never contribute "missing files"
72
- // (observed: a skipped step's prose mention of "type.ts" kept the audit
73
- // gate rejecting a finished task with "1 files missing" forever).
74
- const doneCreate = plan.steps.filter((s) => s.status === "done" && s.kind !== "delete");
75
- const doneDelete = plan.steps.filter((s) => s.status === "done" && s.kind === "delete");
76
- const createText = doneCreate.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
77
- const deleteText = doneDelete.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
78
- // File tokens are extracted with the SAME shared filter as the
79
- // hallucination factual check (js-identifiers.ts). URLs are stripped
80
- // first so a hostname / https://host/repo.git in a step description
81
- // can never be reported as a missing file (observed: a git-remote
82
- // plan flagged `whom-pointing-jewel-maine.trycloudflare.com`).
83
- const createFiles = extractFileLikeTokens(stripUrls(createText));
84
- const deleteFiles = extractFileLikeTokens(stripUrls(deleteText));
85
- const missingFiles = [];
86
- const existingFiles = [];
87
- const leftoverFiles = [];
88
- for (const filePath of createFiles) {
89
- const resolved = findExistingFile(this.baseDir, filePath);
90
- if (resolved) {
91
- // Keep the resolved absolute path: findTypecheckRoot walks up
92
- // from it to the nearest tsconfig, and a relative token would
93
- // resolve against the process cwd instead of baseDir.
94
- existingFiles.push(resolved);
95
- }
96
- else {
97
- missingFiles.push(filePath);
98
- }
99
- }
100
- // A done delete-kind step requires its named files to be GONE.
101
- for (const filePath of deleteFiles) {
102
- if (findExistingFile(this.baseDir, filePath)) {
103
- leftoverFiles.push(filePath);
104
- }
105
- }
106
- // Real test run: when the plan includes a verification step, do not
107
- // trust the model's word (or exit codes) — actually execute the tests
108
- // and check the runner summary. This catches the "all 7 passing ✅"
109
- // final answers that hide 3 failing tests.
110
- let testRun = null;
111
- if (hasTestStep(plan) && findTestFile(this.baseDir)) {
112
- try {
113
- testRun = await runTests(this.baseDir);
114
- }
115
- catch {
116
- testRun = null; // infra failure — don't block on it
117
- }
118
- }
119
- // Real typecheck: the audit must catch "the build is broken" even when
120
- // every file exists (observed: deleting .js files left the .tsx sources
121
- // with 11 tsc errors and the agent still declared success). Runs only
122
- // when a tsconfig.json is reachable from baseDir or the resolved step
123
- // files AND the file checks already passed — an audit that is doomed by
124
- // missing/leftover files must not pay a 90s tsc spawn. Inconclusive
125
- // results never block.
126
- let typecheckError = null;
127
- if (missingFiles.length === 0 && leftoverFiles.length === 0) {
128
- const root = findTypecheckRoot(this.baseDir, existingFiles);
129
- if (root) {
130
- try {
131
- typecheckError = await runTypecheck(root);
132
- }
133
- catch {
134
- typecheckError = null;
135
- }
136
- }
137
- }
138
- const doneSteps = plan.steps.filter((s) => s.status === "done").length;
139
- // done + skipped are both deliberate terminal states: `done` claims the
140
- // work, `skipped` documents a decision that the step is not needed (the
141
- // deliverable gate's documented escape hatch). Neither is a lie the
142
- // audit must catch — a plan with a legitimately skipped step must be
143
- // able to pass (observed: skipped step 1 kept the task rejected forever).
144
- const terminalSteps = plan.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
145
- const totalSteps = plan.steps.length;
146
- const auditedFiles = [...new Set([...createFiles, ...deleteFiles])];
147
- let massEditWarning = null;
148
- if (auditedFiles.length > MASS_EDIT_THRESHOLD) {
149
- massEditWarning = t("exec.mass_edit_warning", {
150
- count: String(auditedFiles.length),
151
- });
152
- }
153
- const testsFailing = testRun !== null && !testRun.passed;
154
- const typecheckFailing = typecheckError !== null;
155
- const passed = missingFiles.length === 0 && leftoverFiles.length === 0 && !testsFailing && !typecheckFailing;
156
- // A "Task complete ✓" summary for an unfinished plan is a lie the model
157
- // reads as a green light (observed: audit gate injected "[✓] Task
158
- // complete: 1/3 steps done" while the plan had 2 pending steps). The
159
- // summary must only claim completion when every step is actually done.
160
- const stepsPending = terminalSteps < totalSteps;
161
- let summary;
162
- if (missingFiles.length > 0) {
163
- summary = t("exec.audit_fail", {
164
- done: doneSteps,
165
- total: totalSteps,
166
- files: missingFiles.length,
167
- });
168
- }
169
- else if (leftoverFiles.length > 0) {
170
- summary = t("exec.audit_leftovers", {
171
- done: doneSteps,
172
- total: totalSteps,
173
- files: leftoverFiles.length,
174
- });
175
- }
176
- else if (testsFailing) {
177
- summary = t("exec.audit_fail_tests", {
178
- done: doneSteps,
179
- total: totalSteps,
180
- failed: String(testRun.failed),
181
- passed: String(testRun.passedCount),
182
- detail: testRun.detail,
183
- });
184
- }
185
- else if (typecheckFailing) {
186
- summary = t("exec.audit_fail_typecheck", {
187
- done: doneSteps,
188
- total: totalSteps,
189
- missing: String(missingFiles.length),
190
- typeError: typecheckError,
191
- });
192
- }
193
- else if (stepsPending) {
194
- summary = t("exec.audit_pending", {
195
- done: doneSteps,
196
- total: totalSteps,
197
- });
198
- }
199
- else {
200
- summary = t("exec.audit_pass", {
201
- done: doneSteps,
202
- total: totalSteps,
203
- files: existingFiles.length,
204
- });
205
- }
206
- return {
207
- passed,
208
- missingFiles,
209
- leftoverFiles,
210
- createdFiles: existingFiles,
211
- modifiedFiles: [],
212
- summary,
213
- massEditWarning,
214
- testRun,
215
- typecheckError,
216
- };
217
- }
218
- }
@@ -1,431 +0,0 @@
1
- import { t } from "../../i18n/index";
2
- import { detectTestResults } from "../../tools/bash";
3
- import { forbiddenWindowsCommand } from "./windows-commands";
4
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
5
- import { platform } from "os";
6
- /**
7
- * Cooldown (in iterations) between stuck-recovery injections. Exported so the
8
- * ExecutionModule can seed its state's lastRecoveryIteration with the same
9
- * offset the plugin compares against.
10
- */
11
- export const STUCK_RECOVERY_COOLDOWN = 5;
12
- const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
13
- const FORCE_SKIP_THRESHOLD = 10;
14
- /** Re-log the SAME stuck-warning key at most every N iterations (a new key
15
- * always logs immediately). Prevents the identical warning from flooding
16
- * every iteration (ses_mt4fn58c: "Tool bash failed N times" after ~30
17
- * successful writes, "No write/exec" on every read). */
18
- export const STUCK_WARN_REPEAT_EVERY = 5;
19
- /** How many file-mutating tool calls (write/edit/bash/download) without an
20
- * active plan trigger the plan-creation nudge. Evidence-based (rule #10):
21
- * the agent is clearly working on files, so a plan is warranted. */
22
- const PLAN_NUDGE_THRESHOLD = 2;
23
- /** Tools whose success can have changed the filesystem — the plan auto-advance
24
- * re-checks the current step's deliverables after them. Read-only tools are
25
- * deliberately excluded (see the onAfterTool advance call below). */
26
- const FS_MUTATING_TOOLS = new Set([
27
- "write_file",
28
- "edit_file",
29
- "delete_file",
30
- "move_file",
31
- "create_dir",
32
- "bash",
33
- "download_file",
34
- "subagent",
35
- "mcp_call",
36
- "pipeline_run",
37
- "browser",
38
- ]);
39
- function normalizeBrokenPath(p) {
40
- return p.replace(/\\/g, "/").replace(/^\.\//, "");
41
- }
42
- /** Parse a tsc / bun build / node --check error line into its file path.
43
- * Returns null when the line carries no file anchor (e.g. `error: Could not
44
- * resolve "..."`). */
45
- export function parseBrokenFile(line) {
46
- const trimmed = line.trim();
47
- // tsc: src/index.tsx(17,15): error TS2322: ...
48
- const tsc = /^(.+?)\s*\(\d+,\d+\)\s*:\s*error TS\d+/.exec(trimmed);
49
- if (tsc)
50
- return normalizeBrokenPath(tsc[1]);
51
- // bun build / node --check: /abs/file.tsx:1:10: error: ...
52
- const bn = /^(.+?):\d+:\d+\s*:\s*(?:error|SyntaxError)/.exec(trimmed);
53
- if (bn)
54
- return normalizeBrokenPath(bn[1]);
55
- return null;
56
- }
57
- /** Extract per-file compile failures from a write/edit result output. Only
58
- * the marker lines are inspected (`[Project typecheck failed]: <line>` /
59
- * `[Syntax check failed]: <line>`) so a diff body can never false-positive. */
60
- export function extractBrokenFiles(output) {
61
- const out = [];
62
- for (const line of output.split("\n")) {
63
- const marker = /\[(?:Project typecheck failed|Syntax check failed)\]:\s*(.+)$/.exec(line);
64
- if (!marker)
65
- continue;
66
- const errLine = marker[1].trim();
67
- out.push({ file: parseBrokenFile(errLine) ?? "", error: errLine.slice(0, 300) });
68
- }
69
- return out;
70
- }
71
- /**
72
- * Gate for `plan update status=done`: refuse while the last write/edit still
73
- * reports a compile error. A failure with no file anchor blocks the whole
74
- * project; otherwise a step's named file tokens are matched against the
75
- * broken files (path-suffix match, case-insensitive). A step naming no files
76
- * cannot be blamed — its done stays vacuous. Returns the first error to show
77
- * the model, or null to allow the done.
78
- */
79
- export function stepTypecheckGate(failures, stepDescription) {
80
- if (failures.size === 0)
81
- return null;
82
- const projectError = failures.get("");
83
- if (projectError)
84
- return projectError;
85
- const stepTokens = extractFileLikeTokens(stripUrls(stepDescription)).map((p) => normalizeBrokenPath(p).toLowerCase());
86
- if (stepTokens.length === 0)
87
- return null;
88
- for (const [file, err] of failures) {
89
- const f = normalizeBrokenPath(file).toLowerCase();
90
- if (stepTokens.some((tok) => f.endsWith(tok) || tok.endsWith(f)))
91
- return err;
92
- }
93
- return null;
94
- }
95
- /**
96
- * The execution plugin hooks, extracted verbatim from ExecutionModule.getPlugin().
97
- * All shared mutable state (pendingMessages, forbiddenBashFailures, state,
98
- * tracker) is owned by the module and mutated by reference through `deps`.
99
- */
100
- export function createExecutionPlugin(deps) {
101
- return {
102
- name: "execution",
103
- onBeforeThink: (ctx) => {
104
- // Flush deferred <system-summary> messages first so they are visible
105
- // to the next model call but never interleave tool messages.
106
- if (ctx.contextManager && deps.pendingMessages.length > 0) {
107
- for (const m of deps.pendingMessages.splice(0)) {
108
- ctx.contextManager.addMessage(m);
109
- }
110
- }
111
- if (deps.trackerRef.current?.isComplete()) {
112
- // All steps done/skipped — stop counting "no progress" against
113
- // the last step and silence step-stuck warnings for a finished
114
- // plan, BUT keep the tool-call history so a read-without-write
115
- // loop is still caught (observed: plan complete + false-positive
116
- // audit → the model re-read files for 50+ iterations with zero
117
- // writes and no recovery hint).
118
- deps.stuckDetector.resetStepProgress();
119
- deps.state.consecutivePlanWarnings = 0;
120
- deps.state.lastStepId = -1;
121
- deps.state.stuckNotified = false;
122
- deps.state.mutationsWithoutPlan = 0;
123
- deps.state.planNudgeSent = false;
124
- }
125
- else {
126
- const step = deps.trackerRef.current?.getCurrentStep();
127
- if (deps.trackerRef.current && step) {
128
- if (step.id !== deps.state.lastStepId) {
129
- deps.state.consecutivePlanWarnings = 0;
130
- deps.state.lastStepId = step.id;
131
- deps.state.stuckNotified = false;
132
- }
133
- deps.stuckDetector.setCurrentStep(step.id, step.description);
134
- deps.stuckDetector.recordIteration(step.id);
135
- // A plan is active — any mutation counter no longer applies.
136
- deps.state.mutationsWithoutPlan = 0;
137
- deps.state.planNudgeSent = false;
138
- }
139
- else {
140
- deps.stuckDetector.reset();
141
- deps.state.consecutivePlanWarnings = 0;
142
- deps.state.lastStepId = -1;
143
- deps.state.stuckNotified = false;
144
- // Evidence-based plan nudge (rule #10 — no bare iteration counter,
145
- // no keyword task classification): only after the agent actually
146
- // made file-mutating tool calls (write/edit/bash/download) without
147
- // a plan does the agent need a plan.
148
- const mutations = deps.state.mutationsWithoutPlan;
149
- if (mutations >= PLAN_NUDGE_THRESHOLD &&
150
- !deps.state.planNudgeSent &&
151
- ctx.contextManager) {
152
- deps.state.planNudgeSent = true;
153
- ctx.contextManager.addMessage({
154
- role: "user",
155
- content: `<system-summary>${t("exec.plan_nudge", {
156
- count: String(mutations),
157
- })}</system-summary>`,
158
- });
159
- }
160
- }
161
- }
162
- const stuckReason = deps.stuckDetector.getStuckReason();
163
- if (stuckReason) {
164
- // Log once per warning episode instead of spamming every iteration:
165
- // a new warning key logs immediately; the SAME key re-logs only every
166
- // STUCK_WARN_REPEAT_EVERY iterations. The step-stuck reason keeps its
167
- // dedicated once-per-episode flag.
168
- const isStuck = deps.stuckDetector.isStuck();
169
- const warnKey = deps.stuckDetector.getWarningKey();
170
- const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
171
- const logIt = isStuck
172
- ? !deps.state.stuckNotified
173
- : warnKey !== deps.state.lastStuckWarnKey ||
174
- iter - deps.state.lastStuckWarnIter >= STUCK_WARN_REPEAT_EVERY;
175
- if (logIt) {
176
- ctx.logger?.warn(stuckReason);
177
- ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
178
- deps.state.lastStuckWarnKey = warnKey;
179
- deps.state.lastStuckWarnIter = iter;
180
- if (isStuck)
181
- deps.state.stuckNotified = true;
182
- }
183
- }
184
- else {
185
- deps.state.lastStuckWarnKey = "";
186
- }
187
- if (deps.stuckDetector.isStuck() ||
188
- deps.stuckDetector.hasRepetitiveToolCalls() ||
189
- deps.stuckDetector.hasReadOnlyLoop()) {
190
- const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
191
- if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
192
- const recovery = deps.stuckDetector.getRecoveryMessage();
193
- if (recovery && ctx.contextManager) {
194
- // Include tsc error context so the model knows WHY it is stuck.
195
- let tscContext = "";
196
- if (deps.state.typecheckFailures.size > 0) {
197
- const currentStep = deps.trackerRef.current?.getCurrentStep();
198
- if (currentStep) {
199
- const stepFiles = extractFileLikeTokens(stripUrls(currentStep.description));
200
- for (const [file, error] of deps.state.typecheckFailures) {
201
- if (!file || stepFiles.some((f) => file.endsWith(f) || f.endsWith(file))) {
202
- tscContext = `\nLast compile error: ${error}`;
203
- break;
204
- }
205
- }
206
- if (!tscContext) {
207
- const first = deps.state.typecheckFailures.entries().next().value;
208
- if (first)
209
- tscContext = `\nLast compile error: ${first[1]}`;
210
- }
211
- }
212
- }
213
- ctx.contextManager.addMessage({
214
- role: "user",
215
- content: `<system-summary>${recovery}${tscContext}</system-summary>`,
216
- });
217
- }
218
- deps.stuckDetector.recordEscalation();
219
- deps.state.lastRecoveryIteration = currentIter;
220
- if (deps.stuckDetector.shouldEscalate() && ctx.onMeta) {
221
- const escalation = t("exec.escalation", {
222
- stepId: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
223
- description: deps.trackerRef.current?.getCurrentStep()?.description ?? "",
224
- });
225
- ctx.onMeta(escalation);
226
- }
227
- // Force skip after too many iterations on the same step
228
- if (deps.stuckDetector.getIterationsOnCurrentStep() >= FORCE_SKIP_THRESHOLD &&
229
- ctx.contextManager) {
230
- const step = deps.trackerRef.current?.getCurrentStep();
231
- ctx.contextManager.addMessage({
232
- role: "user",
233
- content: `<system-summary>${t("exec.stop_directive", {
234
- stepId: String(step?.id ?? "?"),
235
- description: step?.description ?? "",
236
- iterations: String(deps.stuckDetector.getIterationsOnCurrentStep()),
237
- })}</system-summary>`,
238
- });
239
- }
240
- }
241
- }
242
- },
243
- onBeforeTool: (_ctx, call) => {
244
- const warning = deps.checkPlanAlignment(call);
245
- if (warning) {
246
- deps.pendingMessages.push({
247
- role: "user",
248
- content: `<system-summary>${warning}</system-summary>`,
249
- });
250
- deps.state.consecutivePlanWarnings++;
251
- if (deps.state.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
252
- deps.pendingMessages.push({
253
- role: "user",
254
- content: `<system-summary>${t("exec.plan_blocked", { step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`,
255
- });
256
- // Block with an explicit reason so the model sees WHY the call
257
- // was blocked (executor shows it via tool.blocked_reason).
258
- return t("exec.plan_blocked", {
259
- step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
260
- max: MAX_PLAN_WARNINGS_BEFORE_BLOCK,
261
- });
262
- }
263
- }
264
- else {
265
- deps.state.consecutivePlanWarnings = 0;
266
- }
267
- return true;
268
- },
269
- onToolCall: (ctx) => {
270
- const toolName = ctx?.toolName;
271
- const args = ctx?.args;
272
- if (toolName && args) {
273
- deps.stuckDetector.recordToolCall(toolName, args);
274
- // Count file-mutating calls made with NO active plan — feeds the
275
- // evidence-based plan-creation nudge in onBeforeThink.
276
- if (!deps.trackerRef.current &&
277
- (toolName === "write_file" ||
278
- toolName === "edit_file" ||
279
- toolName === "bash" ||
280
- toolName === "download_file")) {
281
- deps.state.mutationsWithoutPlan++;
282
- }
283
- }
284
- },
285
- onAfterTool: (ctx, call, result) => {
286
- // Remember every bash invocation (successful or not) so stuck
287
- // detection can reason about e.g. repeated runs with empty output.
288
- if (call.name === "bash") {
289
- deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
290
- }
291
- // Auto-detect delete intent from model actions: when delete_file is
292
- // called, find the matching plan step and set kind="delete". This is
293
- // language-agnostic — based on what the model DID, not what it said.
294
- if (call.name === "delete_file" && result.success && deps.trackerRef.current) {
295
- const deletedPath = String(call.arguments?.path ?? "");
296
- if (deletedPath) {
297
- const plan = deps.trackerRef.current.getPlan();
298
- const tokens = extractFileLikeTokens(stripUrls(deletedPath));
299
- for (const step of plan.steps) {
300
- if (step.status !== "pending" && step.status !== "in_progress")
301
- continue;
302
- const stepTokens = extractFileLikeTokens(stripUrls(step.description));
303
- const overlap = tokens.some((t) => stepTokens.some((st) => t.endsWith(st) || st.endsWith(t)));
304
- if (overlap && step.kind !== "delete") {
305
- step.kind = "delete";
306
- deps.store.saveActive(plan);
307
- break;
308
- }
309
- }
310
- }
311
- }
312
- // A write/edit/bash that reports success but still carries a
313
- // type/syntax error in its output is NOT a success — the model must
314
- // fix the actual error, not keep rewriting the file blindly. Feed it
315
- // to the stuck detector so the rewrite-loop hint fires.
316
- const toolText = String(result.output ?? "");
317
- const hasTypeError = /error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText);
318
- if (hasTypeError) {
319
- deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
320
- }
321
- // Track compile failures per-file so the plan done-gate can refuse
322
- // "done" while the last edit still doesn't compile (observed: the agent
323
- // marked plan steps done while the project typecheck failed on every
324
- // write — a "fix it later" cascade). A CLEAN result clears the map: the
325
- // debounced project tsc re-ran and passed (or no tsconfig applies), so
326
- // old failures must not block completion forever.
327
- if (call.name === "write_file" || call.name === "edit_file") {
328
- const broken = extractBrokenFiles(toolText);
329
- if (broken.length > 0) {
330
- for (const b of broken)
331
- deps.state.typecheckFailures.set(b.file, b.error);
332
- }
333
- else if (result.success) {
334
- deps.state.typecheckFailures.clear();
335
- }
336
- }
337
- if (!result.success) {
338
- // Feed the bash attempt outcome into the flailing window BEFORE the
339
- // per-command handling — rotation across different failing commands
340
- // (wc → cat → Get-Content → …) must still trip the detector.
341
- if (call.name === "bash") {
342
- deps.stuckDetector.recordBashAttempt(false);
343
- }
344
- // Hard stop for forbidden Windows commands: after the second
345
- // failure of the same forbidden command (grep/sed/ls/find/…),
346
- // inject a STOP message so the session stops burning bash calls
347
- // on commands cmd.exe does not have. Mirrors the bash tool's own
348
- // win32-gated hard-stop (src/tools/bash.ts) — on macOS/Linux
349
- // these commands exist and fail for legit reasons.
350
- if (call.name === "bash" && platform() === "win32") {
351
- const cmd = String(call.arguments?.command ?? "");
352
- const forbidden = forbiddenWindowsCommand(cmd);
353
- if (forbidden) {
354
- const n = (deps.forbiddenBashFailures.get(forbidden) || 0) + 1;
355
- deps.forbiddenBashFailures.set(forbidden, n);
356
- if (n === 2) {
357
- deps.pendingMessages.push({
358
- role: "user",
359
- content: `<system-summary>${t("exec.forbidden_cmd", { cmd: forbidden })}</system-summary>`,
360
- });
361
- }
362
- }
363
- }
364
- // A type/syntax error in an otherwise "successful" result was already
365
- // recorded above. A failing call (result.success === false) would
366
- // otherwise double-count the same error (typecheck branch + failure
367
- // branch), inflating the error-signature count and triggering the
368
- // web search at half the intended threshold.
369
- if (!hasTypeError) {
370
- deps.stuckDetector.recordToolError(call.name, result.output);
371
- }
372
- }
373
- else {
374
- deps.stuckDetector.recordToolSuccess();
375
- if (call.name === "bash") {
376
- deps.stuckDetector.recordBashAttempt(true);
377
- }
378
- // When a bash command runs code successfully, suggest marking the
379
- // step done — unless the output shows failing tests, which must
380
- // never be reported as a clean success.
381
- if (call.name === "bash" && result.success) {
382
- const cmd = String(call.arguments?.command ?? "");
383
- const testRun = detectTestResults(String(result.output ?? ""));
384
- if (testRun && testRun.failed > 0) {
385
- deps.pendingMessages.push({
386
- role: "user",
387
- content: `<system-summary>${testRun.framework} reported ${testRun.failed} FAILING test(s) (${testRun.passed} passing). Do NOT mark the current step as done — fix the failing tests (read the failure output, correct the code) and re-run them until all pass.</system-summary>`,
388
- });
389
- }
390
- else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
391
- deps.pendingMessages.push({
392
- role: "user",
393
- content: `<system-summary>${testRun.framework}: all ${testRun.passed} test(s) passed for "${cmd}". You may mark the current step as done via plan update step=N status=done.</system-summary>`,
394
- });
395
- }
396
- }
397
- }
398
- if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
399
- const filePath = call.arguments?.path;
400
- if (filePath) {
401
- deps.stuckDetector.recordFileRewrite(filePath);
402
- if (deps.stuckDetector.hasExcessiveRewrites()) {
403
- const file = deps.stuckDetector.getExcessiveRewriteFile();
404
- const count = deps.stuckDetector.getFileRewriteCount(file);
405
- if (ctx.onMeta) {
406
- ctx.onMeta(t("exec.file_rewrite_warning", {
407
- file: file,
408
- count: String(count),
409
- }));
410
- }
411
- }
412
- }
413
- }
414
- // Auto-advance fires after every successful FILESYSTEM-MUTATING tool —
415
- // not just write_file/edit_file. Small models often create files through
416
- // bash (`echo ... > f`, heredocs) or delete/move via shell instead of the
417
- // dedicated tools, and the plan then never advances past the step even
418
- // though its deliverables exist (certification 3.5/3.8: files on disk,
419
- // plan stuck at 1/4 and 4/5). Read-only tools MUST stay excluded: a
420
- // successful read/glob would otherwise complete an "Update X" step whose
421
- // target file already exists from an earlier step — without any edit.
422
- // advancePlanIfStepComplete is a no-op when tokens don't resolve.
423
- if (result.success && FS_MUTATING_TOOLS.has(call.name)) {
424
- deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
425
- }
426
- // Automatic web search for a repeatedly failing error (fire-and-forget,
427
- // results land in pendingMessages on the next onBeforeThink flush).
428
- deps.maybeSearchError(ctx, call);
429
- },
430
- };
431
- }
@@ -1,8 +0,0 @@
1
- export { ExecutionModule } from "./module";
2
- export { PlanCreator } from "./planner";
3
- export { PlanTracker } from "./tracker";
4
- export { StepVerifier } from "./verifier";
5
- export { StuckDetector } from "./stuck-detector";
6
- export { Auditor } from "./auditor";
7
- export { PlanPersister } from "./plan-persister";
8
- export { PlanStore } from "./plan-store";