micro-models-agent 0.41.2 → 0.43.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 (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +19 -9
  3. package/dist/cli/completer.js +37 -36
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +23 -48
  6. package/dist/cli/repl-commands.js +12 -40
  7. package/dist/cli/repl.js +87 -217
  8. package/dist/cli/security-commands.js +7 -5
  9. package/dist/cli/setup.js +26 -8
  10. package/dist/config/config.js +5 -52
  11. package/dist/config/defaults.js +5 -29
  12. package/dist/config/experts.js +1 -1
  13. package/dist/config/index.js +3 -3
  14. package/dist/config/security.js +10 -3
  15. package/dist/core/agent-moe.js +10 -2
  16. package/dist/core/agent.js +82 -273
  17. package/dist/core/bootstrap.js +13 -80
  18. package/dist/core/index.js +2 -2
  19. package/dist/core/prompt-builder.js +2 -23
  20. package/dist/core/session-logger.js +4 -46
  21. package/dist/i18n/en.json +2 -75
  22. package/dist/i18n/ru.json +1 -74
  23. package/dist/index.js +1 -1
  24. package/dist/llm/image-utils.js +5 -4
  25. package/dist/llm/index.js +4 -4
  26. package/dist/llm/model-loader.js +6 -6
  27. package/dist/llm/openai-compat.js +34 -40
  28. package/dist/llm/orchestrator.js +29 -33
  29. package/dist/llm/response.js +9 -9
  30. package/dist/logger/app-logger.js +1 -1
  31. package/dist/logger/index.js +1 -1
  32. package/dist/main.js +648 -183
  33. package/dist/migration/backup.js +13 -13
  34. package/dist/migration/detect.js +11 -11
  35. package/dist/migration/index.js +2 -2
  36. package/dist/modules/browser/actions.js +4 -34
  37. package/dist/modules/browser/bridge-server.mjs +202 -202
  38. package/dist/modules/browser/cookie-store.js +6 -6
  39. package/dist/modules/browser/index.js +5 -7
  40. package/dist/modules/browser/module.js +7 -8
  41. package/dist/modules/browser/session.js +84 -87
  42. package/dist/modules/browser/snapshot.js +58 -92
  43. package/dist/modules/browser/types.js +1 -4
  44. package/dist/modules/certification/cli.js +4 -2
  45. package/dist/modules/certification/fact-checker.js +3 -1
  46. package/dist/modules/certification/loader.js +9 -3
  47. package/dist/modules/certification/runner.js +4 -1
  48. package/dist/modules/context/index.js +1 -1
  49. package/dist/modules/context/manager.js +86 -160
  50. package/dist/modules/execution/auditor.js +25 -177
  51. package/dist/modules/execution/module.js +544 -201
  52. package/dist/modules/execution/moe-executor.js +0 -25
  53. package/dist/modules/execution/plan-store.js +3 -1
  54. package/dist/modules/execution/plan-validator.js +10 -10
  55. package/dist/modules/execution/planner.js +1 -6
  56. package/dist/modules/execution/stuck-detector.js +10 -173
  57. package/dist/modules/execution/verifier.js +42 -86
  58. package/dist/modules/hallucination/confidence.js +1 -8
  59. package/dist/modules/hallucination/detector.js +5 -2
  60. package/dist/modules/hallucination/factual.js +64 -3
  61. package/dist/modules/hallucination/index.js +1 -1
  62. package/dist/modules/hallucination/js-identifiers.js +0 -190
  63. package/dist/modules/hallucination/llm-judge.js +3 -1
  64. package/dist/modules/indexer/cache.js +7 -9
  65. package/dist/modules/indexer/index.js +3 -3
  66. package/dist/modules/indexer/module.js +42 -95
  67. package/dist/modules/indexer/walker.js +17 -17
  68. package/dist/modules/lsp/client.js +31 -74
  69. package/dist/modules/lsp/config.js +33 -87
  70. package/dist/modules/lsp/index.js +3 -3
  71. package/dist/modules/lsp/module.js +21 -185
  72. package/dist/modules/mcp/module.js +6 -2
  73. package/dist/modules/memory/index.js +1 -1
  74. package/dist/modules/memory/module.js +23 -71
  75. package/dist/modules/memory/search.js +9 -11
  76. package/dist/modules/memory/store.js +13 -13
  77. package/dist/modules/pipelines/engine.js +10 -10
  78. package/dist/modules/pipelines/index.js +3 -3
  79. package/dist/modules/pipelines/parser.js +14 -17
  80. package/dist/modules/pipelines/template.js +1 -1
  81. package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
  82. package/dist/modules/plugins/builtin/notify.js +2 -3
  83. package/dist/modules/plugins/index.js +1 -1
  84. package/dist/modules/plugins/loader.js +17 -59
  85. package/dist/modules/plugins/manager.js +17 -73
  86. package/dist/modules/processes/index.js +1 -1
  87. package/dist/modules/processes/registry.js +46 -135
  88. package/dist/modules/registry.js +2 -4
  89. package/dist/modules/security/audit-notifier.js +39 -39
  90. package/dist/modules/security/command-validator.js +8 -2
  91. package/dist/modules/security/data-sanitizer.js +9 -1
  92. package/dist/modules/security/encryption.js +56 -58
  93. package/dist/modules/security/network-validator.js +9 -1
  94. package/dist/modules/security/path-validator.js +3 -1
  95. package/dist/modules/security/security-policies.js +19 -3
  96. package/dist/modules/security/session-encryption.js +1 -1
  97. package/dist/modules/security/session-isolation.js +8 -8
  98. package/dist/modules/session/index.js +3 -3
  99. package/dist/modules/session/module.js +5 -5
  100. package/dist/modules/session/store.js +9 -3
  101. package/dist/modules/skills/module.js +2 -1
  102. package/dist/modules/updater/checker.js +6 -70
  103. package/dist/modules/updater/index.js +1 -2
  104. package/dist/modules/user-profile/compressor.js +2 -2
  105. package/dist/modules/user-profile/index.js +1 -1
  106. package/dist/modules/user-profile/profile.js +9 -9
  107. package/dist/tools/attach-image.js +1 -1
  108. package/dist/tools/bash.js +19 -178
  109. package/dist/tools/browser.js +29 -46
  110. package/dist/tools/executor.js +5 -4
  111. package/dist/tools/file-info.js +12 -13
  112. package/dist/tools/filter-tools.js +2 -9
  113. package/dist/tools/glob-tool.js +11 -11
  114. package/dist/tools/grep-tool.js +3 -1
  115. package/dist/tools/index.js +2 -13
  116. package/dist/tools/list-dir.js +17 -18
  117. package/dist/tools/load-skill.js +3 -1
  118. package/dist/tools/path-utils.js +4 -4
  119. package/dist/tools/pipeline-run.js +25 -25
  120. package/dist/tools/process-kill.js +11 -11
  121. package/dist/tools/process-list.js +22 -20
  122. package/dist/tools/process-log.js +18 -22
  123. package/dist/tools/question.js +3 -1
  124. package/dist/tools/read-file.js +2 -10
  125. package/dist/tools/recall.js +37 -44
  126. package/dist/tools/registry.js +4 -15
  127. package/dist/tools/remember.js +29 -29
  128. package/dist/tools/scope-check.js +9 -9
  129. package/dist/tools/subagent.js +9 -54
  130. package/dist/tools/user-input.js +1 -1
  131. package/dist/tools/web-browse.js +3 -3
  132. package/dist/tools/web-fetch.js +3 -3
  133. package/dist/tools/web-search.js +3 -3
  134. package/dist/tools/write-file.js +3 -1
  135. package/dist/ui/box.js +5 -1
  136. package/dist/ui/index.js +6 -6
  137. package/dist/ui/md-formatter.js +33 -33
  138. package/dist/ui/output.js +5 -5
  139. package/dist/ui/renderer.js +10 -15
  140. package/dist/ui/table.js +1 -1
  141. package/package.json +48 -48
  142. package/dist/cli/plugin-commands.js +0 -36
  143. package/dist/cli/run-result.js +0 -22
  144. package/dist/core/version.js +0 -24
  145. package/dist/modules/artifacts/store.js +0 -61
  146. package/dist/modules/browser/bridge-client.js +0 -199
  147. package/dist/modules/browser/bridge-path.js +0 -10
  148. package/dist/modules/browser/driver.js +0 -136
  149. package/dist/modules/context/chunk-query.js +0 -100
  150. package/dist/modules/context/fact-extractor.js +0 -162
  151. package/dist/modules/context/history.js +0 -15
  152. package/dist/modules/execution/audit-runners.js +0 -152
  153. package/dist/modules/execution/execution-plugin.js +0 -272
  154. package/dist/modules/execution/plan-tool.js +0 -508
  155. package/dist/modules/execution/windows-commands.js +0 -41
  156. package/dist/modules/indexer/project-profile.js +0 -183
  157. package/dist/modules/lsp/check-tool.js +0 -58
  158. package/dist/modules/lsp/command.js +0 -60
  159. package/dist/modules/lsp/probe.js +0 -76
  160. package/dist/modules/lsp/project-root.js +0 -32
  161. package/dist/modules/lsp/startup-check.js +0 -141
  162. package/dist/modules/processes/detect.js +0 -34
  163. package/dist/modules/skills/matcher.js +0 -27
  164. package/dist/modules/updater/module.js +0 -116
  165. package/dist/tools/chunk-query.js +0 -99
  166. package/dist/tools/download-file.js +0 -116
  167. package/dist/tools/enable-tools.js +0 -58
  168. package/dist/tools/hidden-tools-block.js +0 -37
  169. package/dist/ui/line-editor.js +0 -703
  170. package/dist/ui/line-math.js +0 -69
  171. package/dist/ui/plan-view.js +0 -103
@@ -2,7 +2,6 @@ import { getExpertConfig } from "../../config/experts";
2
2
  import { filterToolsByTags } from "../../tools/filter-tools";
3
3
  import { StuckDetector } from "./stuck-detector";
4
4
  import { t } from "../../i18n/index";
5
- import { ArtifactStore } from "../artifacts/store";
6
5
  const TRANSIENT_ERROR_PATTERNS = [
7
6
  "timeout",
8
7
  "Timeout",
@@ -103,38 +102,14 @@ async function executeSubtask(subtask, deps, _sharedContext) {
103
102
  read_only_files: readOnlyFiles,
104
103
  tool_tags: expertConfig.tool_tags,
105
104
  };
106
- if (deps.config.subagent?.resultMode === "file") {
107
- subagentArgs.result_mode = "file";
108
- subagentArgs.artifact_name = subtask.id;
109
- }
110
105
  const result = await subagentTool.handler({
111
106
  config: deps.config,
112
107
  baseDir: deps.baseDir,
113
108
  logger: deps.logger,
114
109
  llmProvider: deps.llmProvider,
115
110
  toolExecutor: deps.toolExecutor,
116
- sessionId: "moe",
117
111
  }, subagentArgs);
118
112
  if (result.success) {
119
- if (deps.config.subagent?.resultMode === "file") {
120
- const pathMatch = result.output.match(/saved to artifact: (.+)/);
121
- const abs = pathMatch ? pathMatch[1].trim() : "";
122
- if (abs) {
123
- const store = new ArtifactStore({
124
- baseDir: deps.baseDir,
125
- sessionId: "moe",
126
- });
127
- const full = store.read(abs.split(/[\\/]/).pop() || ""); // file name only, scoped to root
128
- deps.logger.debug(`MoE subtask "${subtask.id}" attached artifact ${abs} (${full?.length ?? 0} chars) to Router`);
129
- return {
130
- subtaskId: subtask.id,
131
- success: true,
132
- summary: `Artifact: ${abs}`,
133
- result: full ?? result.output,
134
- durationMs: Date.now() - startTime,
135
- };
136
- }
137
- }
138
113
  return {
139
114
  subtaskId: subtask.id,
140
115
  success: true,
@@ -29,7 +29,9 @@ function listDir(dir, baseDir) {
29
29
  if (!existsSync(dir))
30
30
  return [];
31
31
  const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
32
- return files.map((f) => readPlanFile(join(dir, f), baseDir)).filter((p) => p !== null);
32
+ return files
33
+ .map((f) => readPlanFile(join(dir, f), baseDir))
34
+ .filter((p) => p !== null);
33
35
  }
34
36
  function toMeta(plan, status) {
35
37
  return {
@@ -3,7 +3,7 @@ export function validatePlan(plan, config) {
3
3
  const warnings = [];
4
4
  const autoFixes = [];
5
5
  if (!plan.subtasks || plan.subtasks.length === 0) {
6
- return { valid: false, errors: ["Plan has no subtasks"], warnings: [], autoFixes: [] };
6
+ return { valid: false, errors: ['Plan has no subtasks'], warnings: [], autoFixes: [] };
7
7
  }
8
8
  errors.push(...expertTagsExist(plan.subtasks, config));
9
9
  errors.push(...hasCycle(plan.subtasks));
@@ -30,7 +30,7 @@ export function expertTagsExist(subtasks, config) {
30
30
  const knownExperts = new Set(Object.keys(config.experts || {}));
31
31
  for (const subtask of subtasks) {
32
32
  if (!knownExperts.has(subtask.expert_tag)) {
33
- errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(", ")}`);
33
+ errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(', ')}`);
34
34
  }
35
35
  }
36
36
  return errors;
@@ -81,12 +81,12 @@ export function hasAllowedFilesOverlap(subtasks) {
81
81
  }
82
82
  for (const [file, writers] of writeFiles) {
83
83
  if (writers.length > 1) {
84
- const noDep = writers.filter((w) => {
85
- const sub = subtasks.find((s) => s.id === w);
86
- return sub && !sub.depends_on?.some((d) => writers.includes(d));
84
+ const noDep = writers.filter(w => {
85
+ const sub = subtasks.find(s => s.id === w);
86
+ return sub && !sub.depends_on?.some(d => writers.includes(d));
87
87
  });
88
88
  if (noDep.length > 1) {
89
- errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(", ")} without dependency chain`);
89
+ errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(', ')} without dependency chain`);
90
90
  }
91
91
  }
92
92
  }
@@ -104,7 +104,7 @@ export function deleteReadOverlap(subtasks) {
104
104
  continue;
105
105
  const readFiles = new Set(reader.read_only_files);
106
106
  const writeFiles = writer.allowed_files;
107
- const overlapping = writeFiles.filter((f) => {
107
+ const overlapping = writeFiles.filter(f => {
108
108
  for (const rf of readFiles) {
109
109
  if (f.startsWith(rf) || rf.startsWith(f))
110
110
  return true;
@@ -125,7 +125,7 @@ export function deleteReadOverlap(subtasks) {
125
125
  subtaskId: reader.id,
126
126
  dependsOn: writer.id,
127
127
  canAutoFix: false,
128
- error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(", ")}" — add depends_on: [${writer.id}]`,
128
+ error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(', ')}" — add depends_on: [${writer.id}]`,
129
129
  });
130
130
  }
131
131
  }
@@ -134,9 +134,9 @@ export function deleteReadOverlap(subtasks) {
134
134
  return results;
135
135
  }
136
136
  export function applyAutoFixes(plan, autoFixes) {
137
- const fixed = { ...plan, subtasks: plan.subtasks.map((s) => ({ ...s })) };
137
+ const fixed = { ...plan, subtasks: plan.subtasks.map(s => ({ ...s })) };
138
138
  for (const fix of autoFixes) {
139
- const sub = fixed.subtasks.find((s) => s.id === fix.subtaskId);
139
+ const sub = fixed.subtasks.find(s => s.id === fix.subtaskId);
140
140
  if (!sub)
141
141
  continue;
142
142
  const dependsOnMatch = fix.fix.match(/depends_on:\s*(\S+)/);
@@ -26,7 +26,7 @@ export class PlanCreator {
26
26
  const hasActionWord = actionWords.some((w) => task.toLowerCase().includes(w));
27
27
  return hasActionWord && words.length > 8;
28
28
  }
29
- static createPlan(title, stepDescriptions, baseDir, kinds) {
29
+ static createPlan(title, stepDescriptions, baseDir) {
30
30
  const stepCount = stepDescriptions.length;
31
31
  return {
32
32
  id: generatePlanId(),
@@ -35,7 +35,6 @@ export class PlanCreator {
35
35
  id: i + 1,
36
36
  description: desc,
37
37
  status: "pending",
38
- kind: kinds?.[i] ?? "create",
39
38
  })),
40
39
  createdAt: new Date().toISOString(),
41
40
  baseDir,
@@ -50,7 +49,6 @@ export class PlanCreator {
50
49
  id: kept.length + i + 1,
51
50
  description: desc,
52
51
  status: "pending",
53
- kind: "create",
54
52
  }));
55
53
  return {
56
54
  id: plan.id,
@@ -81,9 +79,6 @@ export class PlanCreator {
81
79
  : "[ ]";
82
80
  const note = step.note ? ` — ${step.note}` : "";
83
81
  lines.push(`${icon} ${step.id}. ${step.description}${note}`);
84
- for (const sub of step.subtasks ?? []) {
85
- lines.push(` ${sub.done ? "[x]" : "[ ]"} ${sub.text}`);
86
- }
87
82
  }
88
83
  return lines.join("\n");
89
84
  }
@@ -35,36 +35,6 @@ export function classifyError(output) {
35
35
  }
36
36
  return "systematic";
37
37
  }
38
- /**
39
- * Tools that only observe state and never change it. A long run of only these
40
- * with no write/exec in between means the model is reading/exploring without
41
- * producing anything — a read-only loop that the stuck detector must surface
42
- * (observed: 50+ read_file iterations with zero writes after a false-positive
43
- * audit rejected a completed task).
44
- */
45
- const READ_ONLY_TOOLS = new Set([
46
- "read_file",
47
- "glob",
48
- "grep",
49
- "list_dir",
50
- "file_info",
51
- "browser",
52
- "web_search",
53
- "web_fetch",
54
- "web_browse",
55
- "search_history",
56
- "recall",
57
- "project_map",
58
- "chunk_query",
59
- "process_list",
60
- "process_log",
61
- "plan",
62
- "todo",
63
- "verify",
64
- "load_skill",
65
- ]);
66
- /** Consecutive read-only tool calls that constitute a loop. */
67
- const READ_ONLY_LOOP_THRESHOLD = 10;
68
38
  export class StuckDetector {
69
39
  threshold;
70
40
  errorThreshold;
@@ -82,10 +52,6 @@ export class StuckDetector {
82
52
  escalationThreshold = 3;
83
53
  fileRewriteCount = new Map();
84
54
  fileRewriteThreshold = 3;
85
- lastBashCommand = "";
86
- lastBashOutput = "";
87
- emptyBashRunCount = 0;
88
- readOnlyStreak = 0;
89
55
  constructor(threshold = 6, errorThreshold = 3) {
90
56
  this.threshold = threshold;
91
57
  this.errorThreshold = errorThreshold;
@@ -104,22 +70,6 @@ export class StuckDetector {
104
70
  if (this.recentToolCalls.length > this.maxRecentCalls) {
105
71
  this.recentToolCalls.shift();
106
72
  }
107
- // Track the consecutive read-only streak. A write/exec tool resets it;
108
- // plan/todo bookkeeping counts as read-only (it is not real progress).
109
- if (READ_ONLY_TOOLS.has(name)) {
110
- this.readOnlyStreak++;
111
- }
112
- else {
113
- this.readOnlyStreak = 0;
114
- }
115
- }
116
- /**
117
- * True after a long run of read-only tool calls with no write in between.
118
- * The model is exploring/looping without producing anything — inject a
119
- * recovery hint so it stops re-reading and actually edits files.
120
- */
121
- hasReadOnlyLoop() {
122
- return this.readOnlyStreak >= READ_ONLY_LOOP_THRESHOLD;
123
73
  }
124
74
  recordToolError(toolName, output) {
125
75
  this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
@@ -131,28 +81,6 @@ export class StuckDetector {
131
81
  getLastErrorOutput() {
132
82
  return this.lastErrorOutput;
133
83
  }
134
- /**
135
- * Record every bash invocation (successful or not) so stuck detection can
136
- * reason about repeated runs with empty output, entry-point issues, and
137
- * platform mistakes (PowerShell cmdlets in cmd.exe, bash heredocs, ...).
138
- */
139
- recordBashOutput(command, output) {
140
- const trimmed = output.trim();
141
- if (command === this.lastBashCommand && !trimmed) {
142
- this.emptyBashRunCount++;
143
- }
144
- else {
145
- this.emptyBashRunCount = 0;
146
- }
147
- this.lastBashCommand = command;
148
- this.lastBashOutput = output;
149
- }
150
- getLastBashCommand() {
151
- return this.lastBashCommand;
152
- }
153
- getLastBashOutput() {
154
- return this.lastBashOutput;
155
- }
156
84
  getLastFailedTool() {
157
85
  return this.lastFailedTool;
158
86
  }
@@ -230,7 +158,9 @@ export class StuckDetector {
230
158
  getHints() {
231
159
  const hints = [];
232
160
  const desc = this.currentStepDescription.toLowerCase();
233
- if (desc.includes("install") || desc.includes("npm") || desc.includes("pip")) {
161
+ if (desc.includes("install") ||
162
+ desc.includes("npm") ||
163
+ desc.includes("pip")) {
234
164
  hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
235
165
  }
236
166
  if (desc.includes("test") || desc.includes("spec")) {
@@ -242,32 +172,17 @@ export class StuckDetector {
242
172
  if (desc.includes("deploy") || desc.includes("publish")) {
243
173
  hints.push("Verify credentials and network access before deploying.");
244
174
  }
245
- // State-based bullets. The recovery message (getRecoveryMessage) already
246
- // reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
247
- // > repetitive > read-only). Repeating that same state here as a hint
248
- // would duplicate the guidance in the same cooldown block. Only add the
249
- // bullet when the recovery message is NOT already covering that state.
250
- const primary = this.getPrimaryReason();
251
- if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
175
+ if (this.hasRepetitiveToolCalls()) {
252
176
  hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
253
177
  }
254
- if (this.hasConsecutiveFailures() && primary !== "consecutive") {
178
+ if (this.hasConsecutiveFailures()) {
255
179
  hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
256
180
  }
257
- if (this.hasReadOnlyLoop() && primary !== "read-only") {
258
- hints.push("You have made many read-only tool calls (read_file/glob/grep/browser) without writing anything. Stop exploring — make the edit/write the task needs, or use the plan tool to decide next steps.");
259
- }
260
181
  return hints;
261
182
  }
262
183
  getActionableHints() {
263
184
  const hints = [];
264
185
  const error = this.lastErrorOutput;
265
- // Bash-specific hints do not depend on a tool failure — they fire even
266
- // when the command "succeeded" with empty output (missing entry point).
267
- const isScriptRun = /(^|[\s&])(bun|node|tsx|ts-node|deno|python|python3)\S*\s+[^|&]+\s+[^\s]+$/.test(this.lastBashCommand);
268
- if (isScriptRun && !this.lastBashOutput.trim() && this.emptyBashRunCount >= 2) {
269
- hints.push(`The command "${this.lastBashCommand}" ran ${this.emptyBashRunCount} times with EMPTY output. The program likely has no entry point — read the file with read_file and check that it actually calls its main function with process.argv / CLI arguments and prints results (console.log). Then run it again.`);
270
- }
271
186
  if (!error)
272
187
  return hints;
273
188
  // Runtime incompatibility — tool/module crashes on this Node version
@@ -305,25 +220,6 @@ export class StuckDetector {
305
220
  const file = this.getExcessiveRewriteFile();
306
221
  hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
307
222
  }
308
- // Repeated identical bash runs with empty output — likely a missing
309
- // entry point (the program never calls its main function).
310
- // PowerShell cmdlets used inside cmd.exe (Windows shell mismatch).
311
- if (/Write-Host|Get-Content|Select-String|Out-File|Set-Content/i.test(error)) {
312
- hints.push("That looks like a PowerShell cmdlet — the shell here is cmd.exe. Use echo/type for output or the read_file/write_file tools instead.");
313
- }
314
- // Model invented a Bun API that does not exist.
315
- if (/Bun\.\w+ is not a function|Bun\.\w+ is not defined|Bun\.\w+ is not a constructor/i.test(error)) {
316
- hints.push("That Bun API does not exist. Verify the API name in the Bun docs — common ones are Bun.file, Bun.write, Bun.spawn, Bun.serve. For file checks use fs.existsSync from node:fs.");
317
- }
318
- // The model keeps writing a file whose output shows a type/syntax
319
- // error — read the actual error and fix it, don't rewrite blindly.
320
- if (/error TS\d+|typecheck failed|syntax check failed/i.test(error)) {
321
- hints.push("The file still has a type/syntax error (see the error line in the output). Read the file with read_file around the reported line, fix the actual error, then re-run — rewriting the whole file blindly usually makes it worse.");
322
- }
323
- // Bash heredoc in cmd.exe.
324
- if (/unexpected.*<<|Непредвиденное появление/i.test(error)) {
325
- hints.push("Heredoc (<< EOF) is a bash feature — this shell is cmd.exe and does not support it. Write the file with write_file instead.");
326
- }
327
223
  return hints;
328
224
  }
329
225
  getToolAlternative() {
@@ -391,16 +287,10 @@ export class StuckDetector {
391
287
  if (errorTool) {
392
288
  return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
393
289
  }
394
- if (this.hasReadOnlyLoop()) {
395
- return t("exec.read_only_loop", {
396
- count: String(this.readOnlyStreak),
397
- });
398
- }
399
290
  return "";
400
291
  }
401
292
  getRecoveryMessage() {
402
- const reason = this.getPrimaryReason();
403
- if (reason === "stuck") {
293
+ if (this.isStuck()) {
404
294
  return t("exec.stuck_recovery", {
405
295
  iterations: this.iterationsOnCurrentStep,
406
296
  stepId: String(this.currentStepId ?? "?"),
@@ -408,50 +298,24 @@ export class StuckDetector {
408
298
  });
409
299
  }
410
300
  // Per-tool error recovery (more specific — e.g., "bash failed 3 times")
411
- if (reason === "tool-errors") {
412
- const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
301
+ const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
302
+ if (errorTool) {
413
303
  return t("exec.tool_errors_recovery", {
414
304
  tool: errorTool[0],
415
305
  count: errorTool[1],
416
306
  });
417
307
  }
418
308
  // Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
419
- if (reason === "consecutive") {
309
+ if (this.hasConsecutiveFailures()) {
420
310
  return t("exec.consecutive_failures_recovery", {
421
311
  count: this.consecutiveFailures,
422
312
  });
423
313
  }
424
- if (reason === "repetitive") {
314
+ if (this.hasRepetitiveToolCalls()) {
425
315
  return this.getRepetitiveToolMessage();
426
316
  }
427
- if (reason === "read-only") {
428
- return t("exec.read_only_loop_recovery", {
429
- count: String(this.readOnlyStreak),
430
- });
431
- }
432
317
  return "";
433
318
  }
434
- /**
435
- * Single source of truth for the recovery reason priority. Used by BOTH
436
- * getRecoveryMessage() and getHints() so the two cannot drift apart: the
437
- * state bullet that getRecoveryMessage already reports is NOT repeated as a
438
- * hint (priority: stuck > tool-errors > consecutive > repetitive >
439
- * read-only).
440
- */
441
- getPrimaryReason() {
442
- if (this.isStuck())
443
- return "stuck";
444
- const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
445
- if (errorTool)
446
- return "tool-errors";
447
- if (this.hasConsecutiveFailures())
448
- return "consecutive";
449
- if (this.hasRepetitiveToolCalls())
450
- return "repetitive";
451
- if (this.hasReadOnlyLoop())
452
- return "read-only";
453
- return null;
454
- }
455
319
  reset() {
456
320
  this.currentStepId = null;
457
321
  this.iterationsOnCurrentStep = 0;
@@ -462,29 +326,6 @@ export class StuckDetector {
462
326
  this.recentToolCalls = [];
463
327
  this.fileRewriteCount.clear();
464
328
  this.escalationCount = 0;
465
- this.lastBashCommand = "";
466
- this.lastBashOutput = "";
467
- this.emptyBashRunCount = 0;
468
- this.readOnlyStreak = 0;
469
- }
470
- /**
471
- * Reset only step-progress state, keeping the tool-call history and the
472
- * read-only streak. Used when the plan is complete but the loop continues
473
- * (e.g. the audit gate rejected a finished task): step-stuck warnings are
474
- * silenced, but a read-without-write loop is still detected.
475
- */
476
- resetStepProgress() {
477
- this.currentStepId = null;
478
- this.iterationsOnCurrentStep = 0;
479
- this.toolErrors.clear();
480
- this.consecutiveFailures = 0;
481
- this.lastFailedTool = "";
482
- this.lastErrorOutput = "";
483
- this.fileRewriteCount.clear();
484
- this.escalationCount = 0;
485
- this.lastBashCommand = "";
486
- this.lastBashOutput = "";
487
- this.emptyBashRunCount = 0;
488
329
  }
489
330
  /**
490
331
  * Reset all per-step state when moving to a new step. Prevents errors and
@@ -500,10 +341,6 @@ export class StuckDetector {
500
341
  this.lastErrorOutput = "";
501
342
  this.recentToolCalls = [];
502
343
  this.fileRewriteCount.clear();
503
- this.lastBashCommand = "";
504
- this.lastBashOutput = "";
505
- this.emptyBashRunCount = 0;
506
- this.readOnlyStreak = 0;
507
344
  // Preserve escalationCount — prevents agent from "gaming" stuck detection
508
345
  // by marking a step done and moving to the next one
509
346
  }
@@ -1,11 +1,8 @@
1
- import { existsSync } from "fs";
2
- import { resolve, extname, join } from "path";
3
- import { spawn } from "child_process";
4
- import { t } from "../../i18n/index";
5
- import { validateExpertConfig } from "../../config/experts";
6
- import { findProjectRoot } from "../lsp/project-root";
7
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
8
- import { findExistingFile } from "./auditor";
1
+ import { existsSync } from 'fs';
2
+ import { resolve, extname } from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { t } from '../../i18n/index';
5
+ import { validateExpertConfig } from '../../config/experts';
9
6
  export class StepVerifier {
10
7
  baseDir;
11
8
  constructor(baseDir) {
@@ -16,51 +13,26 @@ export class StepVerifier {
16
13
  const exists = existsSync(resolved);
17
14
  return {
18
15
  passed: exists,
19
- message: exists ? t("verify.file_exists", { path }) : t("verify.file_not_found", { path }),
16
+ message: exists ? t('verify.file_exists', { path }) : t('verify.file_not_found', { path }),
20
17
  };
21
18
  }
22
19
  async runScript(scriptName) {
23
20
  try {
24
21
  await this.runAsync(`bun run ${scriptName}`, this.baseDir, 60_000);
25
- return { passed: true, message: t("verify.script_passed", { script: scriptName }) };
22
+ return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
26
23
  }
27
24
  catch (e) {
28
- return {
29
- passed: false,
30
- message: t("verify.script_failed", { script: scriptName, message: e.message }),
31
- };
25
+ return { passed: false, message: t('verify.script_failed', { script: scriptName, message: e.message }) };
32
26
  }
33
27
  }
34
28
  async runTypeCheck() {
35
- const tsconfigPath = resolve(this.baseDir, "tsconfig.json");
29
+ const tsconfigPath = resolve(this.baseDir, 'tsconfig.json');
36
30
  if (!existsSync(tsconfigPath)) {
37
- return { passed: true, message: "No tsconfig.json found — skipping type check" };
31
+ return { passed: true, message: 'No tsconfig.json found — skipping type check' };
38
32
  }
39
33
  try {
40
- await this.runAsync("npx tsc --noEmit", this.baseDir, 60_000);
41
- return { passed: true, message: "TypeScript type check passed" };
42
- }
43
- catch (e) {
44
- const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
45
- return { passed: false, message: `TypeScript type check failed: ${stderr.slice(0, 500)}` };
46
- }
47
- }
48
- /**
49
- * Type-check a specific file, resolving the project root from the FILE's
50
- * location (not the agent baseDir). For nested layouts (`baseDir/proj/` with
51
- * its own tsconfig.json) the old baseDir-based check silently skipped every
52
- * type error (observed in ses_mst2r0r5: zero type feedback for the whole
53
- * session). Markers mirror the TypeScript LSP server config.
54
- */
55
- async runTypeCheckForFile(filePath) {
56
- const projectRoot = findProjectRoot(filePath, this.baseDir, ["tsconfig.json", "package.json"]);
57
- const tsconfigPath = join(projectRoot, "tsconfig.json");
58
- if (!existsSync(tsconfigPath)) {
59
- return { passed: true, message: "No tsconfig.json found — skipping type check" };
60
- }
61
- try {
62
- await this.runAsync("npx tsc --noEmit --skipLibCheck", projectRoot, 60_000);
63
- return { passed: true, message: "TypeScript type check passed" };
34
+ await this.runAsync('npx tsc --noEmit', this.baseDir, 60_000);
35
+ return { passed: true, message: 'TypeScript type check passed' };
64
36
  }
65
37
  catch (e) {
66
38
  const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
@@ -68,23 +40,21 @@ export class StepVerifier {
68
40
  }
69
41
  }
70
42
  async runTests() {
71
- const pkgPath = resolve(this.baseDir, "package.json");
43
+ const pkgPath = resolve(this.baseDir, 'package.json');
72
44
  if (!existsSync(pkgPath)) {
73
- return { passed: true, message: "No package.json found — skipping tests" };
45
+ return { passed: true, message: 'No package.json found — skipping tests' };
74
46
  }
75
47
  try {
76
- const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf-8"));
48
+ const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
77
49
  if (!pkg.scripts?.test) {
78
- return { passed: true, message: "No test script defined — skipping tests" };
50
+ return { passed: true, message: 'No test script defined — skipping tests' };
79
51
  }
80
52
  }
81
- catch {
82
- /* fall through — attempt to run */
83
- }
84
- return this.runScript("test");
53
+ catch { /* fall through — attempt to run */ }
54
+ return this.runScript('test');
85
55
  }
86
56
  async verifyArtifactFiles(files) {
87
- return Promise.all(files.map((f) => this.checkFileExists(f)));
57
+ return Promise.all(files.map(f => this.checkFileExists(f)));
88
58
  }
89
59
  async verifyMoEManifest(plan, config, allToolTags) {
90
60
  const errors = [];
@@ -117,55 +87,45 @@ export class StepVerifier {
117
87
  };
118
88
  }
119
89
  async verifyStep(stepDescription) {
120
- const fileMatches = extractFileLikeTokens(stripUrls(stepDescription));
121
- // No named files — this step cannot be verified by file existence. Do NOT
122
- // trivially pass (empty every() = true); signal noFiles so the tool guides
123
- // the model to run a real check instead.
124
- if (fileMatches.length === 0) {
125
- return { passed: false, syntaxValid: true, failed: [], noFiles: true };
126
- }
90
+ const fileMatches = stepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [];
127
91
  const results = [];
128
92
  let syntaxValid = true;
129
93
  for (const filePath of fileMatches) {
130
- const resolved = findExistingFile(this.baseDir, filePath);
131
- if (!resolved) {
132
- results.push({ passed: false, message: t("verify.file_not_found", { path: filePath }) });
133
- continue;
134
- }
135
- results.push({ passed: true, message: t("verify.file_exists", { path: filePath }) });
136
- if (!(await this.validateSyntax(resolved))) {
137
- syntaxValid = false;
138
- results.push({ passed: false, message: t("verify.syntax_error", { path: filePath }) });
94
+ const result = await this.checkFileExists(filePath);
95
+ results.push(result);
96
+ if (result.passed) {
97
+ const fullPath = resolve(this.baseDir, filePath);
98
+ if (!(await this.validateSyntax(fullPath))) {
99
+ syntaxValid = false;
100
+ results.push({ passed: false, message: t('verify.syntax_error', { path: filePath }) });
101
+ }
139
102
  }
140
103
  }
141
104
  return {
142
- passed: results.every((r) => r.passed),
105
+ passed: results.every(r => r.passed),
143
106
  syntaxValid,
144
- failed: results.filter((r) => !r.passed),
145
- noFiles: false,
107
+ failed: results.filter(r => !r.passed),
146
108
  };
147
109
  }
148
110
  async validateSyntax(filePath) {
149
111
  const ext = extname(filePath);
150
- if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
112
+ if (ext === '.ts' || ext === '.tsx') {
151
113
  try {
152
114
  await this.runAsync(`npx tsc --noEmit --skipLibCheck ${filePath}`, this.baseDir, 10000);
153
115
  return true;
154
116
  }
155
117
  catch (err) {
156
- if (err.status === 127 ||
157
- err.message.includes("not found") ||
158
- err.message.includes("ENOENT")) {
118
+ if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
159
119
  return true;
160
120
  }
161
- const stderr = err.stderr?.toString() || "";
162
- if (stderr.includes("error TS") && !stderr.includes("Cannot find module")) {
121
+ const stderr = err.stderr?.toString() || '';
122
+ if (stderr.includes('error TS') && !stderr.includes('Cannot find module')) {
163
123
  return false;
164
124
  }
165
125
  return true;
166
126
  }
167
127
  }
168
- if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
128
+ if (ext === '.js' || ext === '.jsx') {
169
129
  try {
170
130
  await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
171
131
  return true;
@@ -186,25 +146,21 @@ export class StepVerifier {
186
146
  cwd,
187
147
  shell: true,
188
148
  windowsHide: true,
189
- stdio: ["ignore", "pipe", "pipe"],
190
- });
191
- let stdout = "";
192
- let stderr = "";
193
- child.stdout?.on("data", (d) => {
194
- stdout += d.toString();
195
- });
196
- child.stderr?.on("data", (d) => {
197
- stderr += d.toString();
149
+ stdio: ['ignore', 'pipe', 'pipe'],
198
150
  });
151
+ let stdout = '';
152
+ let stderr = '';
153
+ child.stdout?.on('data', (d) => { stdout += d.toString(); });
154
+ child.stderr?.on('data', (d) => { stderr += d.toString(); });
199
155
  const timer = setTimeout(() => {
200
156
  child.kill();
201
157
  reject(new Error(`Command timed out after ${timeoutMs}ms`));
202
158
  }, timeoutMs);
203
- child.on("error", (err) => {
159
+ child.on('error', (err) => {
204
160
  clearTimeout(timer);
205
161
  reject(err);
206
162
  });
207
- child.on("close", (code) => {
163
+ child.on('close', (code) => {
208
164
  clearTimeout(timer);
209
165
  if (code === 0) {
210
166
  resolve({ stdout, stderr });