micro-models-agent 0.40.1 → 0.41.2

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 +9 -19
  3. package/dist/cli/completer.js +36 -37
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +48 -23
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +40 -12
  8. package/dist/cli/repl.js +217 -87
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +5 -7
  11. package/dist/cli/setup.js +8 -26
  12. package/dist/config/config.js +52 -5
  13. package/dist/config/defaults.js +29 -5
  14. package/dist/config/experts.js +1 -1
  15. package/dist/config/index.js +3 -3
  16. package/dist/config/security.js +3 -10
  17. package/dist/core/agent-moe.js +2 -10
  18. package/dist/core/agent.js +273 -82
  19. package/dist/core/bootstrap.js +80 -13
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/prompt-builder.js +23 -2
  22. package/dist/core/session-logger.js +46 -4
  23. package/dist/core/version.js +24 -0
  24. package/dist/i18n/en.json +75 -2
  25. package/dist/i18n/ru.json +74 -1
  26. package/dist/index.js +1 -1
  27. package/dist/llm/image-utils.js +4 -5
  28. package/dist/llm/index.js +4 -4
  29. package/dist/llm/model-loader.js +6 -6
  30. package/dist/llm/openai-compat.js +40 -34
  31. package/dist/llm/orchestrator.js +33 -29
  32. package/dist/llm/response.js +9 -9
  33. package/dist/logger/app-logger.js +1 -1
  34. package/dist/logger/index.js +1 -1
  35. package/dist/main.js +2489 -2186
  36. package/dist/migration/backup.js +13 -13
  37. package/dist/migration/detect.js +11 -11
  38. package/dist/migration/index.js +2 -2
  39. package/dist/modules/artifacts/store.js +61 -0
  40. package/dist/modules/browser/actions.js +34 -4
  41. package/dist/modules/browser/bridge-client.js +199 -0
  42. package/dist/modules/browser/bridge-path.js +10 -0
  43. package/dist/modules/browser/bridge-server.mjs +202 -202
  44. package/dist/modules/browser/cookie-store.js +6 -6
  45. package/dist/modules/browser/driver.js +136 -0
  46. package/dist/modules/browser/index.js +7 -5
  47. package/dist/modules/browser/module.js +8 -7
  48. package/dist/modules/browser/session.js +87 -84
  49. package/dist/modules/browser/snapshot.js +92 -58
  50. package/dist/modules/browser/types.js +4 -1
  51. package/dist/modules/certification/cli.js +2 -4
  52. package/dist/modules/certification/fact-checker.js +1 -3
  53. package/dist/modules/certification/loader.js +3 -9
  54. package/dist/modules/certification/runner.js +1 -4
  55. package/dist/modules/context/chunk-query.js +100 -0
  56. package/dist/modules/context/fact-extractor.js +162 -0
  57. package/dist/modules/context/history.js +15 -0
  58. package/dist/modules/context/index.js +1 -1
  59. package/dist/modules/context/manager.js +160 -86
  60. package/dist/modules/execution/audit-runners.js +152 -0
  61. package/dist/modules/execution/auditor.js +177 -25
  62. package/dist/modules/execution/execution-plugin.js +272 -0
  63. package/dist/modules/execution/module.js +201 -544
  64. package/dist/modules/execution/moe-executor.js +25 -0
  65. package/dist/modules/execution/plan-store.js +1 -3
  66. package/dist/modules/execution/plan-tool.js +508 -0
  67. package/dist/modules/execution/plan-validator.js +10 -10
  68. package/dist/modules/execution/planner.js +6 -1
  69. package/dist/modules/execution/stuck-detector.js +173 -10
  70. package/dist/modules/execution/verifier.js +86 -42
  71. package/dist/modules/execution/windows-commands.js +41 -0
  72. package/dist/modules/hallucination/confidence.js +8 -1
  73. package/dist/modules/hallucination/detector.js +2 -5
  74. package/dist/modules/hallucination/factual.js +3 -64
  75. package/dist/modules/hallucination/index.js +1 -1
  76. package/dist/modules/hallucination/js-identifiers.js +190 -0
  77. package/dist/modules/hallucination/llm-judge.js +1 -3
  78. package/dist/modules/indexer/cache.js +9 -7
  79. package/dist/modules/indexer/index.js +3 -3
  80. package/dist/modules/indexer/module.js +95 -42
  81. package/dist/modules/indexer/project-profile.js +183 -0
  82. package/dist/modules/indexer/walker.js +17 -17
  83. package/dist/modules/lsp/check-tool.js +58 -0
  84. package/dist/modules/lsp/client.js +74 -31
  85. package/dist/modules/lsp/command.js +60 -0
  86. package/dist/modules/lsp/config.js +87 -33
  87. package/dist/modules/lsp/index.js +3 -3
  88. package/dist/modules/lsp/module.js +185 -21
  89. package/dist/modules/lsp/probe.js +76 -0
  90. package/dist/modules/lsp/project-root.js +32 -0
  91. package/dist/modules/lsp/startup-check.js +141 -0
  92. package/dist/modules/mcp/module.js +2 -6
  93. package/dist/modules/memory/index.js +1 -1
  94. package/dist/modules/memory/module.js +71 -23
  95. package/dist/modules/memory/search.js +11 -9
  96. package/dist/modules/memory/store.js +13 -13
  97. package/dist/modules/pipelines/engine.js +10 -10
  98. package/dist/modules/pipelines/index.js +3 -3
  99. package/dist/modules/pipelines/parser.js +17 -14
  100. package/dist/modules/pipelines/template.js +1 -1
  101. package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
  102. package/dist/modules/plugins/builtin/notify.js +3 -2
  103. package/dist/modules/plugins/index.js +1 -1
  104. package/dist/modules/plugins/loader.js +59 -17
  105. package/dist/modules/plugins/manager.js +73 -17
  106. package/dist/modules/processes/detect.js +34 -0
  107. package/dist/modules/processes/index.js +1 -1
  108. package/dist/modules/processes/registry.js +135 -46
  109. package/dist/modules/registry.js +4 -2
  110. package/dist/modules/security/audit-notifier.js +39 -39
  111. package/dist/modules/security/command-validator.js +2 -8
  112. package/dist/modules/security/data-sanitizer.js +1 -9
  113. package/dist/modules/security/encryption.js +58 -56
  114. package/dist/modules/security/network-validator.js +1 -9
  115. package/dist/modules/security/path-validator.js +1 -3
  116. package/dist/modules/security/security-policies.js +3 -19
  117. package/dist/modules/security/session-encryption.js +1 -1
  118. package/dist/modules/security/session-isolation.js +8 -8
  119. package/dist/modules/session/index.js +3 -3
  120. package/dist/modules/session/module.js +5 -5
  121. package/dist/modules/session/store.js +3 -9
  122. package/dist/modules/skills/matcher.js +27 -0
  123. package/dist/modules/skills/module.js +1 -2
  124. package/dist/modules/updater/checker.js +70 -6
  125. package/dist/modules/updater/index.js +2 -1
  126. package/dist/modules/updater/module.js +116 -0
  127. package/dist/modules/user-profile/compressor.js +2 -2
  128. package/dist/modules/user-profile/index.js +1 -1
  129. package/dist/modules/user-profile/profile.js +9 -9
  130. package/dist/tools/attach-image.js +1 -1
  131. package/dist/tools/bash.js +178 -19
  132. package/dist/tools/browser.js +46 -29
  133. package/dist/tools/chunk-query.js +99 -0
  134. package/dist/tools/download-file.js +116 -0
  135. package/dist/tools/enable-tools.js +58 -0
  136. package/dist/tools/executor.js +4 -5
  137. package/dist/tools/file-info.js +13 -12
  138. package/dist/tools/filter-tools.js +9 -2
  139. package/dist/tools/glob-tool.js +11 -11
  140. package/dist/tools/grep-tool.js +1 -3
  141. package/dist/tools/hidden-tools-block.js +37 -0
  142. package/dist/tools/index.js +13 -2
  143. package/dist/tools/list-dir.js +18 -17
  144. package/dist/tools/load-skill.js +1 -3
  145. package/dist/tools/path-utils.js +4 -4
  146. package/dist/tools/pipeline-run.js +25 -25
  147. package/dist/tools/process-kill.js +11 -11
  148. package/dist/tools/process-list.js +20 -22
  149. package/dist/tools/process-log.js +22 -18
  150. package/dist/tools/question.js +1 -3
  151. package/dist/tools/read-file.js +10 -2
  152. package/dist/tools/recall.js +44 -37
  153. package/dist/tools/registry.js +15 -4
  154. package/dist/tools/remember.js +29 -29
  155. package/dist/tools/scope-check.js +9 -9
  156. package/dist/tools/subagent.js +54 -9
  157. package/dist/tools/user-input.js +1 -1
  158. package/dist/tools/web-browse.js +3 -3
  159. package/dist/tools/web-fetch.js +3 -3
  160. package/dist/tools/web-search.js +3 -3
  161. package/dist/tools/write-file.js +1 -3
  162. package/dist/ui/box.js +1 -5
  163. package/dist/ui/index.js +6 -6
  164. package/dist/ui/line-editor.js +703 -0
  165. package/dist/ui/line-math.js +69 -0
  166. package/dist/ui/md-formatter.js +33 -33
  167. package/dist/ui/output.js +5 -5
  168. package/dist/ui/plan-view.js +103 -0
  169. package/dist/ui/renderer.js +15 -10
  170. package/dist/ui/table.js +1 -1
  171. package/package.json +48 -48
@@ -35,6 +35,36 @@ 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;
38
68
  export class StuckDetector {
39
69
  threshold;
40
70
  errorThreshold;
@@ -52,6 +82,10 @@ export class StuckDetector {
52
82
  escalationThreshold = 3;
53
83
  fileRewriteCount = new Map();
54
84
  fileRewriteThreshold = 3;
85
+ lastBashCommand = "";
86
+ lastBashOutput = "";
87
+ emptyBashRunCount = 0;
88
+ readOnlyStreak = 0;
55
89
  constructor(threshold = 6, errorThreshold = 3) {
56
90
  this.threshold = threshold;
57
91
  this.errorThreshold = errorThreshold;
@@ -70,6 +104,22 @@ export class StuckDetector {
70
104
  if (this.recentToolCalls.length > this.maxRecentCalls) {
71
105
  this.recentToolCalls.shift();
72
106
  }
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;
73
123
  }
74
124
  recordToolError(toolName, output) {
75
125
  this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
@@ -81,6 +131,28 @@ export class StuckDetector {
81
131
  getLastErrorOutput() {
82
132
  return this.lastErrorOutput;
83
133
  }
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
+ }
84
156
  getLastFailedTool() {
85
157
  return this.lastFailedTool;
86
158
  }
@@ -158,9 +230,7 @@ export class StuckDetector {
158
230
  getHints() {
159
231
  const hints = [];
160
232
  const desc = this.currentStepDescription.toLowerCase();
161
- if (desc.includes("install") ||
162
- desc.includes("npm") ||
163
- desc.includes("pip")) {
233
+ if (desc.includes("install") || desc.includes("npm") || desc.includes("pip")) {
164
234
  hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
165
235
  }
166
236
  if (desc.includes("test") || desc.includes("spec")) {
@@ -172,17 +242,32 @@ export class StuckDetector {
172
242
  if (desc.includes("deploy") || desc.includes("publish")) {
173
243
  hints.push("Verify credentials and network access before deploying.");
174
244
  }
175
- if (this.hasRepetitiveToolCalls()) {
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") {
176
252
  hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
177
253
  }
178
- if (this.hasConsecutiveFailures()) {
254
+ if (this.hasConsecutiveFailures() && primary !== "consecutive") {
179
255
  hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
180
256
  }
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
+ }
181
260
  return hints;
182
261
  }
183
262
  getActionableHints() {
184
263
  const hints = [];
185
264
  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
+ }
186
271
  if (!error)
187
272
  return hints;
188
273
  // Runtime incompatibility — tool/module crashes on this Node version
@@ -220,6 +305,25 @@ export class StuckDetector {
220
305
  const file = this.getExcessiveRewriteFile();
221
306
  hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
222
307
  }
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
+ }
223
327
  return hints;
224
328
  }
225
329
  getToolAlternative() {
@@ -287,10 +391,16 @@ export class StuckDetector {
287
391
  if (errorTool) {
288
392
  return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
289
393
  }
394
+ if (this.hasReadOnlyLoop()) {
395
+ return t("exec.read_only_loop", {
396
+ count: String(this.readOnlyStreak),
397
+ });
398
+ }
290
399
  return "";
291
400
  }
292
401
  getRecoveryMessage() {
293
- if (this.isStuck()) {
402
+ const reason = this.getPrimaryReason();
403
+ if (reason === "stuck") {
294
404
  return t("exec.stuck_recovery", {
295
405
  iterations: this.iterationsOnCurrentStep,
296
406
  stepId: String(this.currentStepId ?? "?"),
@@ -298,24 +408,50 @@ export class StuckDetector {
298
408
  });
299
409
  }
300
410
  // Per-tool error recovery (more specific — e.g., "bash failed 3 times")
301
- const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
302
- if (errorTool) {
411
+ if (reason === "tool-errors") {
412
+ const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
303
413
  return t("exec.tool_errors_recovery", {
304
414
  tool: errorTool[0],
305
415
  count: errorTool[1],
306
416
  });
307
417
  }
308
418
  // Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
309
- if (this.hasConsecutiveFailures()) {
419
+ if (reason === "consecutive") {
310
420
  return t("exec.consecutive_failures_recovery", {
311
421
  count: this.consecutiveFailures,
312
422
  });
313
423
  }
314
- if (this.hasRepetitiveToolCalls()) {
424
+ if (reason === "repetitive") {
315
425
  return this.getRepetitiveToolMessage();
316
426
  }
427
+ if (reason === "read-only") {
428
+ return t("exec.read_only_loop_recovery", {
429
+ count: String(this.readOnlyStreak),
430
+ });
431
+ }
317
432
  return "";
318
433
  }
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
+ }
319
455
  reset() {
320
456
  this.currentStepId = null;
321
457
  this.iterationsOnCurrentStep = 0;
@@ -326,6 +462,29 @@ export class StuckDetector {
326
462
  this.recentToolCalls = [];
327
463
  this.fileRewriteCount.clear();
328
464
  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;
329
488
  }
330
489
  /**
331
490
  * Reset all per-step state when moving to a new step. Prevents errors and
@@ -341,6 +500,10 @@ export class StuckDetector {
341
500
  this.lastErrorOutput = "";
342
501
  this.recentToolCalls = [];
343
502
  this.fileRewriteCount.clear();
503
+ this.lastBashCommand = "";
504
+ this.lastBashOutput = "";
505
+ this.emptyBashRunCount = 0;
506
+ this.readOnlyStreak = 0;
344
507
  // Preserve escalationCount — prevents agent from "gaming" stuck detection
345
508
  // by marking a step done and moving to the next one
346
509
  }
@@ -1,8 +1,11 @@
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';
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";
6
9
  export class StepVerifier {
7
10
  baseDir;
8
11
  constructor(baseDir) {
@@ -13,26 +16,51 @@ export class StepVerifier {
13
16
  const exists = existsSync(resolved);
14
17
  return {
15
18
  passed: exists,
16
- message: exists ? t('verify.file_exists', { path }) : t('verify.file_not_found', { path }),
19
+ message: exists ? t("verify.file_exists", { path }) : t("verify.file_not_found", { path }),
17
20
  };
18
21
  }
19
22
  async runScript(scriptName) {
20
23
  try {
21
24
  await this.runAsync(`bun run ${scriptName}`, this.baseDir, 60_000);
22
- return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
25
+ return { passed: true, message: t("verify.script_passed", { script: scriptName }) };
23
26
  }
24
27
  catch (e) {
25
- return { passed: false, message: t('verify.script_failed', { script: scriptName, message: e.message }) };
28
+ return {
29
+ passed: false,
30
+ message: t("verify.script_failed", { script: scriptName, message: e.message }),
31
+ };
26
32
  }
27
33
  }
28
34
  async runTypeCheck() {
29
- const tsconfigPath = resolve(this.baseDir, 'tsconfig.json');
35
+ const tsconfigPath = resolve(this.baseDir, "tsconfig.json");
30
36
  if (!existsSync(tsconfigPath)) {
31
- return { passed: true, message: 'No tsconfig.json found — skipping type check' };
37
+ return { passed: true, message: "No tsconfig.json found — skipping type check" };
32
38
  }
33
39
  try {
34
- await this.runAsync('npx tsc --noEmit', this.baseDir, 60_000);
35
- return { passed: true, message: 'TypeScript type check passed' };
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" };
36
64
  }
37
65
  catch (e) {
38
66
  const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
@@ -40,21 +68,23 @@ export class StepVerifier {
40
68
  }
41
69
  }
42
70
  async runTests() {
43
- const pkgPath = resolve(this.baseDir, 'package.json');
71
+ const pkgPath = resolve(this.baseDir, "package.json");
44
72
  if (!existsSync(pkgPath)) {
45
- return { passed: true, message: 'No package.json found — skipping tests' };
73
+ return { passed: true, message: "No package.json found — skipping tests" };
46
74
  }
47
75
  try {
48
- const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
76
+ const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf-8"));
49
77
  if (!pkg.scripts?.test) {
50
- return { passed: true, message: 'No test script defined — skipping tests' };
78
+ return { passed: true, message: "No test script defined — skipping tests" };
51
79
  }
52
80
  }
53
- catch { /* fall through — attempt to run */ }
54
- return this.runScript('test');
81
+ catch {
82
+ /* fall through — attempt to run */
83
+ }
84
+ return this.runScript("test");
55
85
  }
56
86
  async verifyArtifactFiles(files) {
57
- return Promise.all(files.map(f => this.checkFileExists(f)));
87
+ return Promise.all(files.map((f) => this.checkFileExists(f)));
58
88
  }
59
89
  async verifyMoEManifest(plan, config, allToolTags) {
60
90
  const errors = [];
@@ -87,45 +117,55 @@ export class StepVerifier {
87
117
  };
88
118
  }
89
119
  async verifyStep(stepDescription) {
90
- const fileMatches = stepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [];
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
+ }
91
127
  const results = [];
92
128
  let syntaxValid = true;
93
129
  for (const filePath of fileMatches) {
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
- }
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 }) });
102
139
  }
103
140
  }
104
141
  return {
105
- passed: results.every(r => r.passed),
142
+ passed: results.every((r) => r.passed),
106
143
  syntaxValid,
107
- failed: results.filter(r => !r.passed),
144
+ failed: results.filter((r) => !r.passed),
145
+ noFiles: false,
108
146
  };
109
147
  }
110
148
  async validateSyntax(filePath) {
111
149
  const ext = extname(filePath);
112
- if (ext === '.ts' || ext === '.tsx') {
150
+ if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
113
151
  try {
114
152
  await this.runAsync(`npx tsc --noEmit --skipLibCheck ${filePath}`, this.baseDir, 10000);
115
153
  return true;
116
154
  }
117
155
  catch (err) {
118
- if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
156
+ if (err.status === 127 ||
157
+ err.message.includes("not found") ||
158
+ err.message.includes("ENOENT")) {
119
159
  return true;
120
160
  }
121
- const stderr = err.stderr?.toString() || '';
122
- if (stderr.includes('error TS') && !stderr.includes('Cannot find module')) {
161
+ const stderr = err.stderr?.toString() || "";
162
+ if (stderr.includes("error TS") && !stderr.includes("Cannot find module")) {
123
163
  return false;
124
164
  }
125
165
  return true;
126
166
  }
127
167
  }
128
- if (ext === '.js' || ext === '.jsx') {
168
+ if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
129
169
  try {
130
170
  await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
131
171
  return true;
@@ -146,21 +186,25 @@ export class StepVerifier {
146
186
  cwd,
147
187
  shell: true,
148
188
  windowsHide: true,
149
- stdio: ['ignore', 'pipe', 'pipe'],
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();
150
198
  });
151
- let stdout = '';
152
- let stderr = '';
153
- child.stdout?.on('data', (d) => { stdout += d.toString(); });
154
- child.stderr?.on('data', (d) => { stderr += d.toString(); });
155
199
  const timer = setTimeout(() => {
156
200
  child.kill();
157
201
  reject(new Error(`Command timed out after ${timeoutMs}ms`));
158
202
  }, timeoutMs);
159
- child.on('error', (err) => {
203
+ child.on("error", (err) => {
160
204
  clearTimeout(timer);
161
205
  reject(err);
162
206
  });
163
- child.on('close', (code) => {
207
+ child.on("close", (code) => {
164
208
  clearTimeout(timer);
165
209
  if (code === 0) {
166
210
  resolve({ stdout, stderr });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Unix commands that do not exist (or have incompatible semantics) in Windows
3
+ * cmd.exe. The 9B model keeps reaching for grep/sed/ls/find via the bash tool;
4
+ * each failure returns a hint, but the model retries.
5
+ *
6
+ * This is a session-level, module-side companion to the bash tool's own
7
+ * per-command hard-stop (`UNIX_TO_WIN_HINTS` / `FAILING_FIRST_WORDS` /
8
+ * `HARD_BLOCK_THRESHOLD` in src/tools/bash.ts). That mechanism injects
9
+ * "STOP using X" INTO the tool output, which a 9B model tends to ignore; this
10
+ * one, gated to win32 like bash.ts, queues a <system-summary> that lands
11
+ * before the next model call. Keep the two command lists consistent.
12
+ *
13
+ * This is a command allow/deny list (like the security module's command
14
+ * blacklist), NOT task classification (rule #10).
15
+ */
16
+ const FORBIDDEN_COMMANDS = new Set([
17
+ "grep",
18
+ "sed",
19
+ "ls",
20
+ "find",
21
+ "rm",
22
+ "touch",
23
+ "which",
24
+ "diff",
25
+ "cp",
26
+ "mv",
27
+ ]);
28
+ function firstWord(command) {
29
+ const segs = command.trim().split(/[\s;|&]+/);
30
+ return (segs[0] || "").replace(/[^\w-]/g, "").toLowerCase();
31
+ }
32
+ /** The forbidden command name if `command` starts with one (handles `cd x && …`). */
33
+ export function forbiddenWindowsCommand(command) {
34
+ const m = command.match(/^\s*cd\s+\S+\s*&&\s*(\S+)/);
35
+ if (m)
36
+ return forbiddenWindowsCommand(m[1]);
37
+ const word = firstWord(command);
38
+ if (word && FORBIDDEN_COMMANDS.has(word))
39
+ return word;
40
+ return null;
41
+ }
@@ -8,7 +8,11 @@ export class ConfidenceCheck {
8
8
  }
9
9
  validate(response) {
10
10
  if (!response || response.length < MIN_CHARS) {
11
- return { status: "retry", reason: t("hall.short_response") };
11
+ return {
12
+ status: "retry",
13
+ kind: "short",
14
+ reason: t("hall.short_response"),
15
+ };
12
16
  }
13
17
  // Language-agnostic: very short response with no structured content
14
18
  const wordCount = response.split(/\s+/).filter(Boolean).length;
@@ -16,6 +20,7 @@ export class ConfidenceCheck {
16
20
  if (wordCount < MIN_WORDS && !hasStructure) {
17
21
  return {
18
22
  status: "warn",
23
+ kind: "short",
19
24
  reason: t("hall.short_response"),
20
25
  };
21
26
  }
@@ -25,6 +30,7 @@ export class ConfidenceCheck {
25
30
  if (overlap > 0.5) {
26
31
  return {
27
32
  status: "retry",
33
+ kind: "repetition",
28
34
  reason: t("hall.repetitive", { pct: Math.round(overlap * 100) }),
29
35
  };
30
36
  }
@@ -40,6 +46,7 @@ export class ConfidenceCheck {
40
46
  if (diversity < 0.25) {
41
47
  return {
42
48
  status: "warn",
49
+ kind: "diversity",
43
50
  reason: t("hall.repetitive", {
44
51
  pct: Math.round((1 - diversity) * 100),
45
52
  }),
@@ -21,8 +21,7 @@ export class HallucinationDetector {
21
21
  }
22
22
  async validate(response) {
23
23
  const confidenceResult = this.confidence.validate(response);
24
- if (confidenceResult.status === "retry" ||
25
- confidenceResult.status === "block") {
24
+ if (confidenceResult.status === "retry" || confidenceResult.status === "block") {
26
25
  return confidenceResult;
27
26
  }
28
27
  const factualResult = this.factual?.validate(response);
@@ -30,9 +29,7 @@ export class HallucinationDetector {
30
29
  return factualResult;
31
30
  }
32
31
  // LLM-as-judge consistency: fast path (no decisions) skips any LLM call.
33
- const judgeResult = this.judge
34
- ? await this.judge.validate(response, this.consistency)
35
- : null;
32
+ const judgeResult = this.judge ? await this.judge.validate(response, this.consistency) : null;
36
33
  const warnings = [];
37
34
  if (judgeResult && judgeResult.status === "warn")
38
35
  warnings.push(judgeResult.reason || "");