micro-models-agent 0.52.0 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +358 -358
  2. package/dist/main.js +244 -122
  3. package/dist/modules/browser/bridge-server.mjs +202 -202
  4. package/dist/skills/builtin/git.md +36 -36
  5. package/dist/skills/builtin/typescript.md +35 -35
  6. package/package.json +1 -1
  7. package/dist/cli/commands.js +0 -447
  8. package/dist/cli/completer.js +0 -167
  9. package/dist/cli/index.js +0 -2
  10. package/dist/cli/main.js +0 -153
  11. package/dist/cli/plugin-commands.js +0 -36
  12. package/dist/cli/repl-commands.js +0 -761
  13. package/dist/cli/repl.js +0 -702
  14. package/dist/cli/run-result.js +0 -33
  15. package/dist/cli/security-commands.js +0 -164
  16. package/dist/cli/setup.js +0 -237
  17. package/dist/config/config.js +0 -276
  18. package/dist/config/defaults.js +0 -141
  19. package/dist/config/domains.js +0 -179
  20. package/dist/config/experts.js +0 -15
  21. package/dist/config/index.js +0 -4
  22. package/dist/config/security.js +0 -213
  23. package/dist/config/types.js +0 -1
  24. package/dist/core/agent-moe.js +0 -102
  25. package/dist/core/agent.js +0 -1018
  26. package/dist/core/bootstrap.js +0 -481
  27. package/dist/core/crash-handler.js +0 -51
  28. package/dist/core/environment.js +0 -199
  29. package/dist/core/index.js +0 -2
  30. package/dist/core/prompt-builder.js +0 -76
  31. package/dist/core/session-logger.js +0 -251
  32. package/dist/core/types.js +0 -1
  33. package/dist/core/version.js +0 -26
  34. package/dist/core/workspace.js +0 -76
  35. package/dist/i18n/en.json +0 -679
  36. package/dist/i18n/index.js +0 -46
  37. package/dist/i18n/ru.json +0 -679
  38. package/dist/index.js +0 -22
  39. package/dist/llm/image-utils.js +0 -143
  40. package/dist/llm/index.js +0 -4
  41. package/dist/llm/model-loader.js +0 -78
  42. package/dist/llm/openai-compat.js +0 -497
  43. package/dist/llm/orchestrator.js +0 -200
  44. package/dist/llm/provider.js +0 -10
  45. package/dist/llm/response.js +0 -39
  46. package/dist/llm/token-counter.js +0 -39
  47. package/dist/llm/types.js +0 -1
  48. package/dist/logger/app-logger.js +0 -189
  49. package/dist/logger/file-log.js +0 -151
  50. package/dist/logger/index.js +0 -1
  51. package/dist/migration/backup.js +0 -45
  52. package/dist/migration/detect.js +0 -50
  53. package/dist/migration/index.js +0 -2
  54. package/dist/modules/artifacts/store.js +0 -61
  55. package/dist/modules/browser/actions.js +0 -76
  56. package/dist/modules/browser/bridge-client.js +0 -199
  57. package/dist/modules/browser/bridge-path.js +0 -10
  58. package/dist/modules/browser/cookie-store.js +0 -24
  59. package/dist/modules/browser/driver.js +0 -136
  60. package/dist/modules/browser/index.js +0 -7
  61. package/dist/modules/browser/module.js +0 -29
  62. package/dist/modules/browser/session.js +0 -342
  63. package/dist/modules/browser/snapshot.js +0 -148
  64. package/dist/modules/browser/types.js +0 -12
  65. package/dist/modules/certification/cli.js +0 -213
  66. package/dist/modules/certification/fact-checker.js +0 -82
  67. package/dist/modules/certification/loader.js +0 -106
  68. package/dist/modules/certification/manifest.js +0 -58
  69. package/dist/modules/certification/runner.js +0 -245
  70. package/dist/modules/certification/scenarios.js +0 -407
  71. package/dist/modules/certification/types.js +0 -1
  72. package/dist/modules/context/chunk-query.js +0 -100
  73. package/dist/modules/context/fact-extractor.js +0 -168
  74. package/dist/modules/context/history.js +0 -15
  75. package/dist/modules/context/index.js +0 -1
  76. package/dist/modules/context/manager.js +0 -440
  77. package/dist/modules/execution/audit-runners.js +0 -206
  78. package/dist/modules/execution/auditor.js +0 -218
  79. package/dist/modules/execution/execution-plugin.js +0 -431
  80. package/dist/modules/execution/index.js +0 -8
  81. package/dist/modules/execution/module.js +0 -625
  82. package/dist/modules/execution/moe-executor.js +0 -304
  83. package/dist/modules/execution/plan-coverage.js +0 -68
  84. package/dist/modules/execution/plan-persister.js +0 -46
  85. package/dist/modules/execution/plan-store.js +0 -196
  86. package/dist/modules/execution/plan-tool.js +0 -677
  87. package/dist/modules/execution/plan-validator.js +0 -153
  88. package/dist/modules/execution/planner.js +0 -94
  89. package/dist/modules/execution/stuck-detector.js +0 -746
  90. package/dist/modules/execution/tracker.js +0 -69
  91. package/dist/modules/execution/types.js +0 -1
  92. package/dist/modules/execution/verifier.js +0 -235
  93. package/dist/modules/execution/windows-commands.js +0 -41
  94. package/dist/modules/hallucination/confidence.js +0 -66
  95. package/dist/modules/hallucination/consistency.js +0 -26
  96. package/dist/modules/hallucination/detector.js +0 -47
  97. package/dist/modules/hallucination/factual.js +0 -169
  98. package/dist/modules/hallucination/index.js +0 -5
  99. package/dist/modules/hallucination/js-identifiers.js +0 -262
  100. package/dist/modules/hallucination/llm-judge.js +0 -101
  101. package/dist/modules/index.js +0 -5
  102. package/dist/modules/indexer/cache.js +0 -40
  103. package/dist/modules/indexer/index.js +0 -3
  104. package/dist/modules/indexer/module.js +0 -246
  105. package/dist/modules/indexer/project-profile.js +0 -183
  106. package/dist/modules/indexer/walker.js +0 -101
  107. package/dist/modules/lsp/check-tool.js +0 -58
  108. package/dist/modules/lsp/client.js +0 -389
  109. package/dist/modules/lsp/command.js +0 -60
  110. package/dist/modules/lsp/config.js +0 -135
  111. package/dist/modules/lsp/index.js +0 -3
  112. package/dist/modules/lsp/module.js +0 -260
  113. package/dist/modules/lsp/probe.js +0 -86
  114. package/dist/modules/lsp/project-root.js +0 -32
  115. package/dist/modules/lsp/startup-check.js +0 -144
  116. package/dist/modules/lsp/types.js +0 -1
  117. package/dist/modules/mcp/client.js +0 -399
  118. package/dist/modules/mcp/index.js +0 -3
  119. package/dist/modules/mcp/module.js +0 -142
  120. package/dist/modules/mcp/registry.js +0 -15
  121. package/dist/modules/memory/index.js +0 -1
  122. package/dist/modules/memory/module.js +0 -96
  123. package/dist/modules/memory/search.js +0 -42
  124. package/dist/modules/memory/store.js +0 -69
  125. package/dist/modules/pipelines/engine.js +0 -60
  126. package/dist/modules/pipelines/index.js +0 -3
  127. package/dist/modules/pipelines/parser.js +0 -56
  128. package/dist/modules/pipelines/template.js +0 -14
  129. package/dist/modules/plugins/builtin/lint-on-write.js +0 -334
  130. package/dist/modules/plugins/builtin/notify.js +0 -9
  131. package/dist/modules/plugins/index.js +0 -1
  132. package/dist/modules/plugins/loader.js +0 -70
  133. package/dist/modules/plugins/manager.js +0 -261
  134. package/dist/modules/plugins/types.js +0 -1
  135. package/dist/modules/pricing/index.js +0 -61
  136. package/dist/modules/pricing/prices.js +0 -129
  137. package/dist/modules/processes/detect.js +0 -34
  138. package/dist/modules/processes/index.js +0 -2
  139. package/dist/modules/processes/registry.js +0 -327
  140. package/dist/modules/processes/runner.js +0 -23
  141. package/dist/modules/providers/create.js +0 -22
  142. package/dist/modules/providers/fallback.js +0 -79
  143. package/dist/modules/providers/health.js +0 -46
  144. package/dist/modules/providers/index.js +0 -5
  145. package/dist/modules/providers/manager.js +0 -161
  146. package/dist/modules/providers/presets.js +0 -128
  147. package/dist/modules/providers/registry.js +0 -22
  148. package/dist/modules/providers/types.js +0 -1
  149. package/dist/modules/registry.js +0 -48
  150. package/dist/modules/security/audit-log.js +0 -136
  151. package/dist/modules/security/audit-notifier.js +0 -292
  152. package/dist/modules/security/command-validator.js +0 -219
  153. package/dist/modules/security/content-scanner.js +0 -53
  154. package/dist/modules/security/data-sanitizer.js +0 -89
  155. package/dist/modules/security/encryption.js +0 -242
  156. package/dist/modules/security/index.js +0 -14
  157. package/dist/modules/security/network-validator.js +0 -88
  158. package/dist/modules/security/path-validator.js +0 -203
  159. package/dist/modules/security/rate-limiter.js +0 -119
  160. package/dist/modules/security/security-policies.js +0 -531
  161. package/dist/modules/security/session-encryption.js +0 -210
  162. package/dist/modules/security/session-isolation.js +0 -95
  163. package/dist/modules/session/index.js +0 -3
  164. package/dist/modules/session/manager.js +0 -172
  165. package/dist/modules/session/module.js +0 -24
  166. package/dist/modules/session/store.js +0 -222
  167. package/dist/modules/session/types.js +0 -1
  168. package/dist/modules/skills/index.js +0 -2
  169. package/dist/modules/skills/loader.js +0 -72
  170. package/dist/modules/skills/matcher.js +0 -27
  171. package/dist/modules/skills/module.js +0 -129
  172. package/dist/modules/types.js +0 -1
  173. package/dist/modules/updater/checker.js +0 -96
  174. package/dist/modules/updater/index.js +0 -2
  175. package/dist/modules/updater/module.js +0 -116
  176. package/dist/modules/user-profile/compressor.js +0 -16
  177. package/dist/modules/user-profile/index.js +0 -1
  178. package/dist/modules/user-profile/profile.js +0 -68
  179. package/dist/tools/approve.js +0 -33
  180. package/dist/tools/attach-image.js +0 -101
  181. package/dist/tools/bash.js +0 -519
  182. package/dist/tools/browser.js +0 -115
  183. package/dist/tools/chunk-query.js +0 -100
  184. package/dist/tools/create-dir.js +0 -56
  185. package/dist/tools/delete-file.js +0 -63
  186. package/dist/tools/download-file.js +0 -117
  187. package/dist/tools/edit-file.js +0 -80
  188. package/dist/tools/enable-tools.js +0 -59
  189. package/dist/tools/executor.js +0 -154
  190. package/dist/tools/file-info.js +0 -47
  191. package/dist/tools/filter-tools.js +0 -17
  192. package/dist/tools/glob-tool.js +0 -27
  193. package/dist/tools/grep-tool.js +0 -125
  194. package/dist/tools/hidden-tools-block.js +0 -37
  195. package/dist/tools/index.js +0 -78
  196. package/dist/tools/list-dir.js +0 -49
  197. package/dist/tools/load-skill.js +0 -43
  198. package/dist/tools/mcp-call.js +0 -69
  199. package/dist/tools/move-file.js +0 -86
  200. package/dist/tools/path-utils.js +0 -101
  201. package/dist/tools/pipeline-run.js +0 -145
  202. package/dist/tools/preview.js +0 -2
  203. package/dist/tools/process-kill.js +0 -40
  204. package/dist/tools/process-list.js +0 -37
  205. package/dist/tools/process-log.js +0 -54
  206. package/dist/tools/question.js +0 -141
  207. package/dist/tools/read-file.js +0 -179
  208. package/dist/tools/recall.js +0 -118
  209. package/dist/tools/registry.js +0 -47
  210. package/dist/tools/remember.js +0 -68
  211. package/dist/tools/scope-check.js +0 -32
  212. package/dist/tools/search-history.js +0 -85
  213. package/dist/tools/subagent.js +0 -196
  214. package/dist/tools/types.js +0 -1
  215. package/dist/tools/user-input.js +0 -123
  216. package/dist/tools/web-browse.js +0 -87
  217. package/dist/tools/web-fetch.js +0 -119
  218. package/dist/tools/web-search.js +0 -105
  219. package/dist/tools/write-file.js +0 -82
  220. package/dist/ui/box.js +0 -77
  221. package/dist/ui/colors.js +0 -4
  222. package/dist/ui/diff.js +0 -178
  223. package/dist/ui/index.js +0 -6
  224. package/dist/ui/line-editor.js +0 -822
  225. package/dist/ui/line-math.js +0 -73
  226. package/dist/ui/md-formatter.js +0 -212
  227. package/dist/ui/output.js +0 -13
  228. package/dist/ui/plan-view.js +0 -103
  229. package/dist/ui/renderer.js +0 -259
  230. package/dist/ui/spinner.js +0 -70
  231. 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";