micro-models-agent 0.51.1 → 0.52.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 (107) hide show
  1. package/dist/cli/commands.js +162 -38
  2. package/dist/cli/completer.js +5 -5
  3. package/dist/cli/main.js +42 -54
  4. package/dist/cli/repl-commands.js +138 -38
  5. package/dist/cli/repl.js +175 -89
  6. package/dist/cli/run-result.js +11 -0
  7. package/dist/cli/security-commands.js +6 -6
  8. package/dist/cli/setup.js +21 -15
  9. package/dist/config/config.js +54 -27
  10. package/dist/config/defaults.js +17 -0
  11. package/dist/config/domains.js +179 -0
  12. package/dist/config/index.js +2 -1
  13. package/dist/config/security.js +28 -8
  14. package/dist/core/agent.js +162 -30
  15. package/dist/core/bootstrap.js +94 -17
  16. package/dist/core/crash-handler.js +51 -0
  17. package/dist/core/environment.js +199 -0
  18. package/dist/core/session-logger.js +60 -6
  19. package/dist/core/version.js +2 -0
  20. package/dist/i18n/en.json +120 -39
  21. package/dist/i18n/ru.json +91 -10
  22. package/dist/llm/openai-compat.js +191 -53
  23. package/dist/llm/orchestrator.js +5 -3
  24. package/dist/logger/app-logger.js +50 -4
  25. package/dist/main.js +1288 -635
  26. package/dist/modules/browser/session.js +4 -0
  27. package/dist/modules/certification/cli.js +58 -19
  28. package/dist/modules/certification/loader.js +2 -1
  29. package/dist/modules/certification/manifest.js +22 -14
  30. package/dist/modules/certification/runner.js +91 -5
  31. package/dist/modules/certification/scenarios.js +290 -7
  32. package/dist/modules/context/fact-extractor.js +6 -0
  33. package/dist/modules/context/manager.js +19 -2
  34. package/dist/modules/execution/audit-runners.js +61 -7
  35. package/dist/modules/execution/execution-plugin.js +219 -60
  36. package/dist/modules/execution/module.js +207 -18
  37. package/dist/modules/execution/moe-executor.js +33 -20
  38. package/dist/modules/execution/plan-store.js +39 -0
  39. package/dist/modules/execution/plan-tool.js +188 -19
  40. package/dist/modules/execution/planner.js +27 -23
  41. package/dist/modules/execution/stuck-detector.js +244 -8
  42. package/dist/modules/execution/tracker.js +8 -6
  43. package/dist/modules/execution/verifier.js +15 -2
  44. package/dist/modules/hallucination/detector.js +4 -0
  45. package/dist/modules/hallucination/factual.js +45 -5
  46. package/dist/modules/indexer/module.js +1 -0
  47. package/dist/modules/lsp/client.js +123 -12
  48. package/dist/modules/lsp/index.js +1 -1
  49. package/dist/modules/lsp/module.js +30 -2
  50. package/dist/modules/lsp/probe.js +11 -1
  51. package/dist/modules/lsp/startup-check.js +5 -2
  52. package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
  53. package/dist/modules/plugins/manager.js +57 -13
  54. package/dist/modules/pricing/index.js +61 -0
  55. package/dist/modules/pricing/prices.js +129 -0
  56. package/dist/modules/providers/create.js +22 -0
  57. package/dist/modules/providers/fallback.js +79 -0
  58. package/dist/modules/providers/health.js +46 -0
  59. package/dist/modules/providers/index.js +5 -0
  60. package/dist/modules/providers/manager.js +161 -0
  61. package/dist/modules/providers/presets.js +128 -0
  62. package/dist/modules/providers/registry.js +22 -0
  63. package/dist/modules/providers/types.js +1 -0
  64. package/dist/modules/registry.js +1 -0
  65. package/dist/modules/security/command-validator.js +14 -0
  66. package/dist/modules/security/encryption.js +6 -6
  67. package/dist/modules/security/network-validator.js +17 -0
  68. package/dist/modules/security/path-validator.js +22 -26
  69. package/dist/modules/session/store.js +10 -10
  70. package/dist/tools/approve.js +1 -0
  71. package/dist/tools/attach-image.js +12 -0
  72. package/dist/tools/bash.js +27 -4
  73. package/dist/tools/browser.js +1 -0
  74. package/dist/tools/chunk-query.js +1 -0
  75. package/dist/tools/create-dir.js +1 -0
  76. package/dist/tools/delete-file.js +1 -0
  77. package/dist/tools/download-file.js +1 -0
  78. package/dist/tools/edit-file.js +2 -1
  79. package/dist/tools/enable-tools.js +1 -0
  80. package/dist/tools/executor.js +17 -7
  81. package/dist/tools/file-info.js +1 -0
  82. package/dist/tools/glob-tool.js +1 -0
  83. package/dist/tools/grep-tool.js +54 -13
  84. package/dist/tools/list-dir.js +1 -0
  85. package/dist/tools/load-skill.js +1 -0
  86. package/dist/tools/mcp-call.js +1 -0
  87. package/dist/tools/move-file.js +1 -0
  88. package/dist/tools/path-utils.js +51 -1
  89. package/dist/tools/pipeline-run.js +1 -0
  90. package/dist/tools/process-kill.js +11 -0
  91. package/dist/tools/process-list.js +1 -0
  92. package/dist/tools/process-log.js +9 -0
  93. package/dist/tools/question.js +1 -0
  94. package/dist/tools/read-file.js +94 -6
  95. package/dist/tools/recall.js +1 -0
  96. package/dist/tools/remember.js +1 -0
  97. package/dist/tools/scope-check.js +7 -5
  98. package/dist/tools/search-history.js +1 -0
  99. package/dist/tools/subagent.js +4 -4
  100. package/dist/tools/web-browse.js +1 -0
  101. package/dist/tools/web-fetch.js +27 -6
  102. package/dist/tools/web-search.js +70 -43
  103. package/dist/tools/write-file.js +1 -0
  104. package/dist/ui/line-editor.js +142 -23
  105. package/dist/ui/line-math.js +8 -4
  106. package/dist/ui/renderer.js +57 -7
  107. package/package.json +50 -48
@@ -65,6 +65,136 @@ const READ_ONLY_TOOLS = new Set([
65
65
  ]);
66
66
  /** Consecutive read-only tool calls that constitute a loop. */
67
67
  const READ_ONLY_LOOP_THRESHOLD = 10;
68
+ /**
69
+ * Bash flailing: ≥BASH_FLAIL_THRESHOLD failures within the last
70
+ * BASH_FLAIL_WINDOW bash attempts, regardless of interleaved successes.
71
+ * The per-word hard-stop (bash.ts) is evaded by rotating commands
72
+ * (wc → cat → Get-Content → …); this window catches the rotation itself
73
+ * (observed in ses_mt4x3zbo: 8 failed shell tricks across ~12 iterations
74
+ * trying to count/merge files on Windows).
75
+ */
76
+ const BASH_FLAIL_WINDOW = 6;
77
+ const BASH_FLAIL_THRESHOLD = 4;
78
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
79
+ /** Пути (абсолютные/относительные) + опционально :line:col. */
80
+ const ERROR_LOC_RE = /(?:[A-Za-z]:[\\/])?(?:[\w.@+-]+[\\/])+[\w.@+-]+\.[a-zA-Z0-9]{1,6}(?::\d+(?::\d+)?)?/g;
81
+ const SIGNATURE_MAX_CHARS = 120;
82
+ const SEARCHABLE_PATTERNS = [
83
+ /error TS\d+/i,
84
+ /ERR_[A-Z_]+/,
85
+ /ENOENT|EACCES|EPERM|ECONNREFUSED|ETIMEDOUT|ECONNRESET|ENOTFOUND|EADDRINUSE/i,
86
+ /Cannot find module|Module not found|ERR_MODULE_NOT_FOUND/i,
87
+ /TypeError|ReferenceError|SyntaxError|RangeError/i,
88
+ /is not a function|is not defined|is not a constructor/i,
89
+ /Cannot read properties of/i,
90
+ /failed to (compile|build|resolve|parse)/i,
91
+ /fatal error|panic/i,
92
+ ];
93
+ const RUNTIME_WORDS = [
94
+ "bun",
95
+ "node",
96
+ "deno",
97
+ "npm",
98
+ "yarn",
99
+ "pnpm",
100
+ "pip",
101
+ "pip3",
102
+ "python",
103
+ "python3",
104
+ "tsc",
105
+ "tsx",
106
+ "npx",
107
+ "go",
108
+ "cargo",
109
+ "gradle",
110
+ "maven",
111
+ "make",
112
+ ];
113
+ const FRAMEWORK_WORDS = [
114
+ "react",
115
+ "vue",
116
+ "angular",
117
+ "svelte",
118
+ "vite",
119
+ "next",
120
+ "nest",
121
+ "express",
122
+ "fastify",
123
+ "flask",
124
+ "django",
125
+ "spring",
126
+ "webpack",
127
+ "tailwind",
128
+ ];
129
+ /**
130
+ * Stable identity for an error: tool + normalized first error lines (ANSI
131
+ * stripped, paths/locations replaced, whitespace collapsed, capped at 120
132
+ * chars). Two runs failing with the same root cause produce the same
133
+ * signature — that is what triggers the automatic web search.
134
+ */
135
+ export function normalizeErrorSignature(toolName, output) {
136
+ if (!output)
137
+ return null;
138
+ const cleaned = output
139
+ .replace(ANSI_RE, "")
140
+ .replace(ERROR_LOC_RE, " <path>")
141
+ .split("\n")
142
+ .map((l) => l.trim())
143
+ .filter(Boolean)
144
+ .join(" ")
145
+ .replace(/\s+/g, " ")
146
+ .trim();
147
+ if (!cleaned)
148
+ return null;
149
+ return `${toolName}::${cleaned.slice(0, SIGNATURE_MAX_CHARS)}`;
150
+ }
151
+ /**
152
+ * Gate for the automatic web search: only real, meaningful error text is
153
+ * worth searching for. "Command failed with exit code 1" without details is
154
+ * noise — searching it would just burn a query.
155
+ */
156
+ export function isSearchableError(output) {
157
+ if (!output || !output.trim())
158
+ return false;
159
+ const text = output.trim();
160
+ return SEARCHABLE_PATTERNS.some((p) => p.test(text));
161
+ }
162
+ /**
163
+ * Build the search query for a failing error: up to 3 cleaned error lines
164
+ * (paths stripped), prefixed with the runtime and framework detected from the
165
+ * step/bash context, suffixed with "how to fix". Falls back to `tool step`
166
+ * when the error text is empty. Capped at maxQueryChars.
167
+ */
168
+ export function buildSearchQuery(stepDescription, lastBashCommand, lastErrorOutput, toolName, maxQueryChars) {
169
+ const errorText = (lastErrorOutput || "")
170
+ .replace(ANSI_RE, "")
171
+ .replace(ERROR_LOC_RE, " <path>")
172
+ .split("\n")
173
+ .map((l) => l.trim())
174
+ .filter(Boolean)
175
+ .slice(0, 3)
176
+ .join(" ")
177
+ .replace(/\s+/g, " ")
178
+ .trim();
179
+ const context = `${stepDescription} ${lastBashCommand}`.toLowerCase();
180
+ const runtime = RUNTIME_WORDS.find((w) => context.includes(w)) || "";
181
+ const framework = FRAMEWORK_WORDS.find((w) => context.includes(w)) || "";
182
+ let query;
183
+ if (errorText) {
184
+ query =
185
+ runtime && !errorText.toLowerCase().includes(runtime) ? `${runtime} ${errorText}` : errorText;
186
+ if (framework && !query.toLowerCase().includes(framework))
187
+ query = `${query} ${framework}`;
188
+ query = `${query} how to fix`;
189
+ }
190
+ else if (runtime || framework) {
191
+ query = `${[runtime, framework, "error", "how to fix"].filter(Boolean).join(" ")}`;
192
+ }
193
+ else {
194
+ query = `${toolName} ${stepDescription} how to fix`;
195
+ }
196
+ return query.slice(0, maxQueryChars);
197
+ }
68
198
  export class StuckDetector {
69
199
  threshold;
70
200
  errorThreshold;
@@ -86,9 +216,14 @@ export class StuckDetector {
86
216
  lastBashOutput = "";
87
217
  emptyBashRunCount = 0;
88
218
  readOnlyStreak = 0;
89
- constructor(threshold = 6, errorThreshold = 3) {
219
+ bashRecent = [];
220
+ errorSignatureCounts = new Map();
221
+ searchedErrorSignatures = new Set();
222
+ webSearchThreshold;
223
+ constructor(threshold = 6, errorThreshold = 3, webSearchThreshold = 5) {
90
224
  this.threshold = threshold;
91
225
  this.errorThreshold = errorThreshold;
226
+ this.webSearchThreshold = webSearchThreshold;
92
227
  }
93
228
  recordIteration(stepId) {
94
229
  if (stepId === this.currentStepId) {
@@ -127,6 +262,10 @@ export class StuckDetector {
127
262
  this.lastFailedTool = toolName;
128
263
  if (output)
129
264
  this.lastErrorOutput = output;
265
+ const sig = normalizeErrorSignature(toolName, output);
266
+ if (sig) {
267
+ this.errorSignatureCounts.set(sig, (this.errorSignatureCounts.get(sig) || 0) + 1);
268
+ }
130
269
  }
131
270
  getLastErrorOutput() {
132
271
  return this.lastErrorOutput;
@@ -147,6 +286,22 @@ export class StuckDetector {
147
286
  this.lastBashCommand = command;
148
287
  this.lastBashOutput = output;
149
288
  }
289
+ /** Outcome of one bash attempt (success or failure), for flailing detection. */
290
+ recordBashAttempt(success) {
291
+ this.bashRecent.push(success);
292
+ if (this.bashRecent.length > BASH_FLAIL_WINDOW)
293
+ this.bashRecent.shift();
294
+ }
295
+ /** True when most recent bash attempts failed — the model is flailing. */
296
+ hasBashFlailing() {
297
+ if (this.bashRecent.length < BASH_FLAIL_THRESHOLD)
298
+ return false;
299
+ let failures = 0;
300
+ for (const ok of this.bashRecent)
301
+ if (!ok)
302
+ failures++;
303
+ return failures >= BASH_FLAIL_THRESHOLD;
304
+ }
150
305
  getLastBashCommand() {
151
306
  return this.lastBashCommand;
152
307
  }
@@ -156,6 +311,32 @@ export class StuckDetector {
156
311
  getLastFailedTool() {
157
312
  return this.lastFailedTool;
158
313
  }
314
+ /**
315
+ * First error signature whose repetition count reached the web-search
316
+ * threshold and that has NOT been searched yet (or was unmarked for a
317
+ * retry). This is what triggers the automatic web search — not iterations
318
+ * and not the per-tool error count.
319
+ */
320
+ getRepeatedErrorSignature() {
321
+ for (const [sig, count] of this.errorSignatureCounts) {
322
+ if (count >= this.webSearchThreshold && !this.searchedErrorSignatures.has(sig)) {
323
+ return sig;
324
+ }
325
+ }
326
+ return null;
327
+ }
328
+ getErrorSignatureCount(sig) {
329
+ return this.errorSignatureCounts.get(sig) || 0;
330
+ }
331
+ markErrorSearched(sig) {
332
+ this.searchedErrorSignatures.add(sig);
333
+ }
334
+ unmarkErrorSearched(sig) {
335
+ this.searchedErrorSignatures.delete(sig);
336
+ }
337
+ hasErrorSearched(sig) {
338
+ return this.searchedErrorSignatures.has(sig);
339
+ }
159
340
  getIterationsOnCurrentStep() {
160
341
  return this.iterationsOnCurrentStep;
161
342
  }
@@ -163,6 +344,23 @@ export class StuckDetector {
163
344
  this.consecutiveFailures = 0;
164
345
  this.lastFailedTool = "";
165
346
  this.escalationCount = 0;
347
+ // Decay per-tool error counters on productive calls: a tool that failed
348
+ // early must not keep the "Tool X failed N times" warning alive forever
349
+ // (ses_mt4fn58c — bash failed 3x, warning re-logged after 30 successful
350
+ // writes). Fresh failures rebuild the count.
351
+ for (const [tool, count] of this.toolErrors) {
352
+ if (count <= 1)
353
+ this.toolErrors.delete(tool);
354
+ else
355
+ this.toolErrors.set(tool, count - 1);
356
+ }
357
+ // Decay iteration counter on productive tool calls — reading new files,
358
+ // writing, editing all count as progress. Prevents false-positive stuck
359
+ // warnings during legitimate file exploration (e.g. reading 12 files in
360
+ // an analysis step).
361
+ if (this.iterationsOnCurrentStep > 0) {
362
+ this.iterationsOnCurrentStep = Math.max(0, this.iterationsOnCurrentStep - 1);
363
+ }
166
364
  }
167
365
  setCurrentStep(stepId, description) {
168
366
  if (stepId !== this.currentStepId) {
@@ -231,16 +429,16 @@ export class StuckDetector {
231
429
  const hints = [];
232
430
  const desc = this.currentStepDescription.toLowerCase();
233
431
  if (desc.includes("install") || desc.includes("npm") || desc.includes("pip")) {
234
- hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
432
+ hints.push(t("exec.hint_deps"));
235
433
  }
236
434
  if (desc.includes("test") || desc.includes("spec")) {
237
- hints.push("Make sure the source files exist and have real code before running tests.");
435
+ hints.push(t("exec.hint_test"));
238
436
  }
239
437
  if (desc.includes("build") || desc.includes("compile")) {
240
- hints.push("Check that all dependencies are installed and source files are not empty.");
438
+ hints.push(t("exec.hint_build"));
241
439
  }
242
440
  if (desc.includes("deploy") || desc.includes("publish")) {
243
- hints.push("Verify credentials and network access before deploying.");
441
+ hints.push(t("exec.hint_deploy"));
244
442
  }
245
443
  // State-based bullets. The recovery message (getRecoveryMessage) already
246
444
  // reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
@@ -249,13 +447,13 @@ export class StuckDetector {
249
447
  // bullet when the recovery message is NOT already covering that state.
250
448
  const primary = this.getPrimaryReason();
251
449
  if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
252
- hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
450
+ hints.push(t("exec.hint_repetitive"));
253
451
  }
254
452
  if (this.hasConsecutiveFailures() && primary !== "consecutive") {
255
- hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
453
+ hints.push(t("exec.hint_consecutive"));
256
454
  }
257
455
  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.");
456
+ hints.push(t("exec.hint_read_only"));
259
457
  }
260
458
  return hints;
261
459
  }
@@ -379,6 +577,26 @@ export class StuckDetector {
379
577
  count: this.repetitionThreshold,
380
578
  });
381
579
  }
580
+ /**
581
+ * Stable warning identity for anti-spam dedup: the same key re-logged every
582
+ * iteration is spam, a NEW key (or a long gap) deserves a re-warn.
583
+ */
584
+ getWarningKey() {
585
+ if (this.isStuck())
586
+ return "stuck";
587
+ const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
588
+ if (errorTool)
589
+ return `tool-errors:${errorTool[0]}`;
590
+ if (this.hasBashFlailing())
591
+ return "bash-flailing";
592
+ if (this.hasConsecutiveFailures())
593
+ return "consecutive";
594
+ if (this.hasRepetitiveToolCalls())
595
+ return "repetitive";
596
+ if (this.hasReadOnlyLoop())
597
+ return "read-only";
598
+ return "";
599
+ }
382
600
  getStuckReason() {
383
601
  if (this.isStuck()) {
384
602
  return t("exec.stuck", {
@@ -421,6 +639,13 @@ export class StuckDetector {
421
639
  count: this.consecutiveFailures,
422
640
  });
423
641
  }
642
+ if (reason === "bash-flailing") {
643
+ const fails = this.bashRecent.filter((ok) => !ok).length;
644
+ return t("exec.bash_flailing_recovery", {
645
+ fails: String(fails),
646
+ window: String(this.bashRecent.length),
647
+ });
648
+ }
424
649
  if (reason === "repetitive") {
425
650
  return this.getRepetitiveToolMessage();
426
651
  }
@@ -444,6 +669,8 @@ export class StuckDetector {
444
669
  const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
445
670
  if (errorTool)
446
671
  return "tool-errors";
672
+ if (this.hasBashFlailing())
673
+ return "bash-flailing";
447
674
  if (this.hasConsecutiveFailures())
448
675
  return "consecutive";
449
676
  if (this.hasRepetitiveToolCalls())
@@ -465,7 +692,10 @@ export class StuckDetector {
465
692
  this.lastBashCommand = "";
466
693
  this.lastBashOutput = "";
467
694
  this.emptyBashRunCount = 0;
695
+ this.bashRecent = [];
468
696
  this.readOnlyStreak = 0;
697
+ this.errorSignatureCounts.clear();
698
+ this.searchedErrorSignatures.clear();
469
699
  }
470
700
  /**
471
701
  * Reset only step-progress state, keeping the tool-call history and the
@@ -485,6 +715,9 @@ export class StuckDetector {
485
715
  this.lastBashCommand = "";
486
716
  this.lastBashOutput = "";
487
717
  this.emptyBashRunCount = 0;
718
+ this.bashRecent = [];
719
+ this.errorSignatureCounts.clear();
720
+ this.searchedErrorSignatures.clear();
488
721
  }
489
722
  /**
490
723
  * Reset all per-step state when moving to a new step. Prevents errors and
@@ -503,7 +736,10 @@ export class StuckDetector {
503
736
  this.lastBashCommand = "";
504
737
  this.lastBashOutput = "";
505
738
  this.emptyBashRunCount = 0;
739
+ this.bashRecent = [];
506
740
  this.readOnlyStreak = 0;
741
+ this.errorSignatureCounts.clear();
742
+ this.searchedErrorSignatures.clear();
507
743
  // Preserve escalationCount — prevents agent from "gaming" stuck detection
508
744
  // by marking a step done and moving to the next one
509
745
  }
@@ -1,4 +1,4 @@
1
- import { PlanCreator } from "./planner";
1
+ import { PlanCreator, getPlanProgress } from "./planner";
2
2
  export class PlanTracker {
3
3
  plan;
4
4
  currentStepIndex = 0;
@@ -53,13 +53,15 @@ export class PlanTracker {
53
53
  return this.plan.steps.every((s) => s.status === "done" || s.status === "skipped");
54
54
  }
55
55
  getProgressString() {
56
- const done = this.plan.steps.filter((s) => s.status === "done").length;
57
- const total = this.plan.steps.length;
58
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
56
+ const p = getPlanProgress(this.plan);
57
+ const total = p.total;
58
+ const settled = p.settled;
59
+ const pct = total > 0 ? Math.round((settled / total) * 100) : 0;
59
60
  const barWidth = 10;
60
- const filled = Math.round((done / total) * barWidth);
61
+ const filled = Math.round((settled / total) * barWidth);
61
62
  const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
62
- return `[${this.plan.id}] ${this.plan.title} ${done}/${total} ${bar} ${pct}%`;
63
+ const skippedPart = p.skipped > 0 ? ` (${p.skipped} skipped)` : "";
64
+ return `[${this.plan.id}] ${this.plan.title} ${settled}/${total}${skippedPart} ${bar} ${pct}%`;
63
65
  }
64
66
  toPromptBlock() {
65
67
  return PlanCreator.toPromptBlock(this.plan, this.currentStepIndex);
@@ -116,7 +116,7 @@ export class StepVerifier {
116
116
  details,
117
117
  };
118
118
  }
119
- async verifyStep(stepDescription) {
119
+ async verifyStep(stepDescription, kind) {
120
120
  const fileMatches = extractFileLikeTokens(stripUrls(stepDescription));
121
121
  // No named files — this step cannot be verified by file existence. Do NOT
122
122
  // trivially pass (empty every() = true); signal noFiles so the tool guides
@@ -128,7 +128,20 @@ export class StepVerifier {
128
128
  let syntaxValid = true;
129
129
  for (const filePath of fileMatches) {
130
130
  const resolved = findExistingFile(this.baseDir, filePath);
131
- if (!resolved) {
131
+ const exists = !!resolved;
132
+ if (kind === "delete") {
133
+ // For delete steps: file NOT existing is a pass
134
+ results.push({
135
+ passed: !exists,
136
+ message: exists
137
+ ? t("verify.file_still_exists", { path: filePath })
138
+ : t("verify.file_not_found", { path: filePath }),
139
+ });
140
+ // Don't syntax-check a deleted file
141
+ continue;
142
+ }
143
+ // Create steps: file must exist + valid syntax
144
+ if (!exists) {
132
145
  results.push({ passed: false, message: t("verify.file_not_found", { path: filePath }) });
133
146
  continue;
134
147
  }
@@ -19,6 +19,10 @@ export class HallucinationDetector {
19
19
  getConfidenceCheck() {
20
20
  return this.confidence;
21
21
  }
22
+ /** Add files known from compaction summaries to the factual checker. */
23
+ addKnownFiles(files) {
24
+ this.factual?.addKnownFiles(files);
25
+ }
22
26
  async validate(response) {
23
27
  const confidenceResult = this.confidence.validate(response);
24
28
  if (confidenceResult.status === "retry" || confidenceResult.status === "block") {
@@ -20,9 +20,18 @@ export class FactualCheck {
20
20
  static NAME_CACHE_TTL_MS = 5_000;
21
21
  static RESCAN_COOLDOWN_MS = 2_000;
22
22
  static MAX_INDEXED_FILES = 20_000;
23
+ /** Files known from compaction summaries — survive context resets. */
24
+ knownFiles = new Set();
23
25
  constructor(baseDir) {
24
26
  this.baseDir = baseDir;
25
27
  }
28
+ /** Add file paths known from compaction summaries. These are checked
29
+ * before the filesystem scan so bare filenames from compacted turns
30
+ * are recognized even when the directory is in IGNORED_DIRS. */
31
+ addKnownFiles(files) {
32
+ for (const f of files)
33
+ this.knownFiles.add(f);
34
+ }
26
35
  validate(response) {
27
36
  const filePaths = this.extractFilePaths(response);
28
37
  if (filePaths.length === 0)
@@ -50,19 +59,50 @@ export class FactualCheck {
50
59
  pathExists(fp) {
51
60
  if (isAbsolute(fp))
52
61
  return existsSync(fp);
62
+ // Check known files from compaction summaries first
63
+ if (this.knownFiles.has(fp))
64
+ return true;
65
+ for (const cand of this.dotfileVariants(fp)) {
66
+ if (this.knownFiles.has(cand))
67
+ return true;
68
+ if (existsSync(resolve(this.baseDir, cand)))
69
+ return true;
70
+ }
53
71
  // "foo.json" has no directory part — treat it as a bare name that may
54
72
  // live anywhere under baseDir (e.g. src/data/bikes.json). Resolving it
55
73
  // against the workspace root alone produced false "unknown file" warnings.
56
74
  if (!fp.includes("/") && !fp.includes("\\")) {
57
75
  return this.bareNameExists(fp);
58
76
  }
59
- return existsSync(resolve(this.baseDir, fp));
77
+ return false;
78
+ }
79
+ /**
80
+ * The token regex cannot anchor a leading dot (`\b` never matches between a
81
+ * non-word char and `.`), so dotfiles (.prettierrc.json, .env, …) are
82
+ * extracted as "prettierrc.json". Try the dot-prefixed basename too before
83
+ * declaring the file missing. "src/config.json" also yields
84
+ * "src/.config.json".
85
+ */
86
+ dotfileVariants(fp) {
87
+ const idx = Math.max(fp.lastIndexOf("/"), fp.lastIndexOf("\\"));
88
+ const base = idx >= 0 ? fp.slice(idx + 1) : fp;
89
+ if (base.startsWith("."))
90
+ return [fp];
91
+ return idx >= 0 ? [fp, `${fp.slice(0, idx + 1)}.${base}`] : [fp, `.${fp}`];
60
92
  }
61
93
  bareNameExists(name) {
62
- if (existsSync(resolve(this.baseDir, name)))
63
- return true;
64
- if (this.indexHas(name))
65
- return true;
94
+ for (const cand of this.dotfileVariants(name)) {
95
+ if (existsSync(resolve(this.baseDir, cand)))
96
+ return true;
97
+ if (this.indexHas(cand))
98
+ return true;
99
+ // Check known files — a bare name may match a full path from compaction
100
+ for (const known of this.knownFiles) {
101
+ if (known.endsWith(`/${cand}`) || known.endsWith(`\\${cand}`) || known === cand) {
102
+ return true;
103
+ }
104
+ }
105
+ }
66
106
  // A write_file from this session may have created the file after the
67
107
  // index was built. Do a one-off rescan (rate-limited) before warning.
68
108
  const now = Date.now();
@@ -180,6 +180,7 @@ export class IndexerModule {
180
180
  createProjectMapTool() {
181
181
  return {
182
182
  name: "project_map",
183
+ icon: "🗺️",
183
184
  alwaysOn: true,
184
185
  description: t("indexer.project_map_desc"),
185
186
  parameters: {