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
@@ -1,6 +1,7 @@
1
1
  import { t } from "../../i18n/index";
2
2
  import { detectTestResults } from "../../tools/bash";
3
3
  import { forbiddenWindowsCommand } from "./windows-commands";
4
+ import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
4
5
  import { platform } from "os";
5
6
  /**
6
7
  * Cooldown (in iterations) between stuck-recovery injections. Exported so the
@@ -10,6 +11,87 @@ import { platform } from "os";
10
11
  export const STUCK_RECOVERY_COOLDOWN = 5;
11
12
  const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
12
13
  const FORCE_SKIP_THRESHOLD = 10;
14
+ /** Re-log the SAME stuck-warning key at most every N iterations (a new key
15
+ * always logs immediately). Prevents the identical warning from flooding
16
+ * every iteration (ses_mt4fn58c: "Tool bash failed N times" after ~30
17
+ * successful writes, "No write/exec" on every read). */
18
+ export const STUCK_WARN_REPEAT_EVERY = 5;
19
+ /** How many file-mutating tool calls (write/edit/bash/download) without an
20
+ * active plan trigger the plan-creation nudge. Evidence-based (rule #10):
21
+ * the agent is clearly working on files, so a plan is warranted. */
22
+ const PLAN_NUDGE_THRESHOLD = 2;
23
+ /** Tools whose success can have changed the filesystem — the plan auto-advance
24
+ * re-checks the current step's deliverables after them. Read-only tools are
25
+ * deliberately excluded (see the onAfterTool advance call below). */
26
+ const FS_MUTATING_TOOLS = new Set([
27
+ "write_file",
28
+ "edit_file",
29
+ "delete_file",
30
+ "move_file",
31
+ "create_dir",
32
+ "bash",
33
+ "download_file",
34
+ "subagent",
35
+ "mcp_call",
36
+ "pipeline_run",
37
+ "browser",
38
+ ]);
39
+ function normalizeBrokenPath(p) {
40
+ return p.replace(/\\/g, "/").replace(/^\.\//, "");
41
+ }
42
+ /** Parse a tsc / bun build / node --check error line into its file path.
43
+ * Returns null when the line carries no file anchor (e.g. `error: Could not
44
+ * resolve "..."`). */
45
+ export function parseBrokenFile(line) {
46
+ const trimmed = line.trim();
47
+ // tsc: src/index.tsx(17,15): error TS2322: ...
48
+ const tsc = /^(.+?)\s*\(\d+,\d+\)\s*:\s*error TS\d+/.exec(trimmed);
49
+ if (tsc)
50
+ return normalizeBrokenPath(tsc[1]);
51
+ // bun build / node --check: /abs/file.tsx:1:10: error: ...
52
+ const bn = /^(.+?):\d+:\d+\s*:\s*(?:error|SyntaxError)/.exec(trimmed);
53
+ if (bn)
54
+ return normalizeBrokenPath(bn[1]);
55
+ return null;
56
+ }
57
+ /** Extract per-file compile failures from a write/edit result output. Only
58
+ * the marker lines are inspected (`[Project typecheck failed]: <line>` /
59
+ * `[Syntax check failed]: <line>`) so a diff body can never false-positive. */
60
+ export function extractBrokenFiles(output) {
61
+ const out = [];
62
+ for (const line of output.split("\n")) {
63
+ const marker = /\[(?:Project typecheck failed|Syntax check failed)\]:\s*(.+)$/.exec(line);
64
+ if (!marker)
65
+ continue;
66
+ const errLine = marker[1].trim();
67
+ out.push({ file: parseBrokenFile(errLine) ?? "", error: errLine.slice(0, 300) });
68
+ }
69
+ return out;
70
+ }
71
+ /**
72
+ * Gate for `plan update status=done`: refuse while the last write/edit still
73
+ * reports a compile error. A failure with no file anchor blocks the whole
74
+ * project; otherwise a step's named file tokens are matched against the
75
+ * broken files (path-suffix match, case-insensitive). A step naming no files
76
+ * cannot be blamed — its done stays vacuous. Returns the first error to show
77
+ * the model, or null to allow the done.
78
+ */
79
+ export function stepTypecheckGate(failures, stepDescription) {
80
+ if (failures.size === 0)
81
+ return null;
82
+ const projectError = failures.get("");
83
+ if (projectError)
84
+ return projectError;
85
+ const stepTokens = extractFileLikeTokens(stripUrls(stepDescription)).map((p) => normalizeBrokenPath(p).toLowerCase());
86
+ if (stepTokens.length === 0)
87
+ return null;
88
+ for (const [file, err] of failures) {
89
+ const f = normalizeBrokenPath(file).toLowerCase();
90
+ if (stepTokens.some((tok) => f.endsWith(tok) || tok.endsWith(f)))
91
+ return err;
92
+ }
93
+ return null;
94
+ }
13
95
  /**
14
96
  * The execution plugin hooks, extracted verbatim from ExecutionModule.getPlugin().
15
97
  * All shared mutable state (pendingMessages, forbiddenBashFailures, state,
@@ -37,6 +119,8 @@ export function createExecutionPlugin(deps) {
37
119
  deps.state.consecutivePlanWarnings = 0;
38
120
  deps.state.lastStepId = -1;
39
121
  deps.state.stuckNotified = false;
122
+ deps.state.mutationsWithoutPlan = 0;
123
+ deps.state.planNudgeSent = false;
40
124
  }
41
125
  else {
42
126
  const step = deps.trackerRef.current?.getCurrentStep();
@@ -48,38 +132,58 @@ export function createExecutionPlugin(deps) {
48
132
  }
49
133
  deps.stuckDetector.setCurrentStep(step.id, step.description);
50
134
  deps.stuckDetector.recordIteration(step.id);
135
+ // A plan is active — any mutation counter no longer applies.
136
+ deps.state.mutationsWithoutPlan = 0;
137
+ deps.state.planNudgeSent = false;
51
138
  }
52
139
  else {
53
140
  deps.stuckDetector.reset();
54
141
  deps.state.consecutivePlanWarnings = 0;
55
142
  deps.state.lastStepId = -1;
56
143
  deps.state.stuckNotified = false;
57
- const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
58
- if (iter === 3 && !deps.trackerRef.current && ctx.contextManager) {
59
- ctx.contextManager.addMessage({
60
- role: "user",
61
- content: `<system-summary>You have made 3 tool calls without creating a plan. For any task that involves creating files, installing packages, or multiple steps — you MUST use plan create BEFORE continuing. Use the plan tool now with concrete steps (exact filenames, commands, deliverables). Do NOT make any more write/edit/bash calls until you have a plan.</system-summary>`,
62
- });
63
- }
64
- if (iter >= 6 && !deps.trackerRef.current && ctx.contextManager) {
144
+ // Evidence-based plan nudge (rule #10 no bare iteration counter,
145
+ // no keyword task classification): only after the agent actually
146
+ // made file-mutating tool calls (write/edit/bash/download) without
147
+ // a plan does the agent need a plan.
148
+ const mutations = deps.state.mutationsWithoutPlan;
149
+ if (mutations >= PLAN_NUDGE_THRESHOLD &&
150
+ !deps.state.planNudgeSent &&
151
+ ctx.contextManager) {
152
+ deps.state.planNudgeSent = true;
65
153
  ctx.contextManager.addMessage({
66
154
  role: "user",
67
- content: `<system-summary>STOP. ${iter} iterations without a plan. You MUST call plan create RIGHT NOW. No more tool calls until you create a plan.</system-summary>`,
155
+ content: `<system-summary>${t("exec.plan_nudge", {
156
+ count: String(mutations),
157
+ })}</system-summary>`,
68
158
  });
69
159
  }
70
160
  }
71
161
  }
72
162
  const stuckReason = deps.stuckDetector.getStuckReason();
73
163
  if (stuckReason) {
74
- // Log once per stuck episode instead of spamming every iteration.
75
- const logIt = deps.stuckDetector.isStuck() ? !deps.state.stuckNotified : true;
164
+ // Log once per warning episode instead of spamming every iteration:
165
+ // a new warning key logs immediately; the SAME key re-logs only every
166
+ // STUCK_WARN_REPEAT_EVERY iterations. The step-stuck reason keeps its
167
+ // dedicated once-per-episode flag.
168
+ const isStuck = deps.stuckDetector.isStuck();
169
+ const warnKey = deps.stuckDetector.getWarningKey();
170
+ const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
171
+ const logIt = isStuck
172
+ ? !deps.state.stuckNotified
173
+ : warnKey !== deps.state.lastStuckWarnKey ||
174
+ iter - deps.state.lastStuckWarnIter >= STUCK_WARN_REPEAT_EVERY;
76
175
  if (logIt) {
77
176
  ctx.logger?.warn(stuckReason);
78
177
  ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
79
- if (deps.stuckDetector.isStuck())
178
+ deps.state.lastStuckWarnKey = warnKey;
179
+ deps.state.lastStuckWarnIter = iter;
180
+ if (isStuck)
80
181
  deps.state.stuckNotified = true;
81
182
  }
82
183
  }
184
+ else {
185
+ deps.state.lastStuckWarnKey = "";
186
+ }
83
187
  if (deps.stuckDetector.isStuck() ||
84
188
  deps.stuckDetector.hasRepetitiveToolCalls() ||
85
189
  deps.stuckDetector.hasReadOnlyLoop()) {
@@ -87,33 +191,28 @@ export function createExecutionPlugin(deps) {
87
191
  if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
88
192
  const recovery = deps.stuckDetector.getRecoveryMessage();
89
193
  if (recovery && ctx.contextManager) {
90
- const lastError = deps.stuckDetector.getLastErrorOutput();
91
- const skillHint = lastError
92
- ? "\nIf you have relevant skills available, consider loading one with load_skill for expert guidance."
93
- : "";
94
- // Actionable hints based on actual error output
95
- const actionableHints = deps.stuckDetector.getActionableHints();
96
- const actionableHintStr = actionableHints.length > 0
97
- ? `\n${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join("\n") })}`
98
- : "";
99
- // Tool alternative suggestion
100
- const alternative = deps.stuckDetector.getToolAlternative();
101
- const altHint = alternative
102
- ? `\nTool "${deps.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.`
103
- : "";
104
- ctx.contextManager.addMessage({
105
- role: "user",
106
- content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
107
- });
108
- }
109
- const hints = deps.stuckDetector.getHints();
110
- if (hints.length > 0 && ctx.contextManager) {
111
- const hintMsg = t("exec.hints", {
112
- hints: hints.map((h) => `- ${h}`).join("\n"),
113
- });
194
+ // Include tsc error context so the model knows WHY it is stuck.
195
+ let tscContext = "";
196
+ if (deps.state.typecheckFailures.size > 0) {
197
+ const currentStep = deps.trackerRef.current?.getCurrentStep();
198
+ if (currentStep) {
199
+ const stepFiles = extractFileLikeTokens(stripUrls(currentStep.description));
200
+ for (const [file, error] of deps.state.typecheckFailures) {
201
+ if (!file || stepFiles.some((f) => file.endsWith(f) || f.endsWith(file))) {
202
+ tscContext = `\nLast compile error: ${error}`;
203
+ break;
204
+ }
205
+ }
206
+ if (!tscContext) {
207
+ const first = deps.state.typecheckFailures.entries().next().value;
208
+ if (first)
209
+ tscContext = `\nLast compile error: ${first[1]}`;
210
+ }
211
+ }
212
+ }
114
213
  ctx.contextManager.addMessage({
115
214
  role: "user",
116
- content: `<system-summary>${hintMsg}</system-summary>`,
215
+ content: `<system-summary>${recovery}${tscContext}</system-summary>`,
117
216
  });
118
217
  }
119
218
  deps.stuckDetector.recordEscalation();
@@ -131,7 +230,11 @@ export function createExecutionPlugin(deps) {
131
230
  const step = deps.trackerRef.current?.getCurrentStep();
132
231
  ctx.contextManager.addMessage({
133
232
  role: "user",
134
- content: `<system-summary>STOP. Step ${step?.id ?? "?"} ("${step?.description ?? ""}") took ${deps.stuckDetector.getIterationsOnCurrentStep()} iterations with no progress. DO NOT continue this step. Immediately call: plan update step=${step?.id ?? "?"} status=done (if code works despite warnings) OR plan update step=${step?.id ?? "?"} status=skipped note="reason". Do NOT make any other tool calls before updating the plan.</system-summary>`,
233
+ content: `<system-summary>${t("exec.stop_directive", {
234
+ stepId: String(step?.id ?? "?"),
235
+ description: step?.description ?? "",
236
+ iterations: String(deps.stuckDetector.getIterationsOnCurrentStep()),
237
+ })}</system-summary>`,
135
238
  });
136
239
  }
137
240
  }
@@ -168,6 +271,15 @@ export function createExecutionPlugin(deps) {
168
271
  const args = ctx?.args;
169
272
  if (toolName && args) {
170
273
  deps.stuckDetector.recordToolCall(toolName, args);
274
+ // Count file-mutating calls made with NO active plan — feeds the
275
+ // evidence-based plan-creation nudge in onBeforeThink.
276
+ if (!deps.trackerRef.current &&
277
+ (toolName === "write_file" ||
278
+ toolName === "edit_file" ||
279
+ toolName === "bash" ||
280
+ toolName === "download_file")) {
281
+ deps.state.mutationsWithoutPlan++;
282
+ }
171
283
  }
172
284
  },
173
285
  onAfterTool: (ctx, call, result) => {
@@ -176,15 +288,59 @@ export function createExecutionPlugin(deps) {
176
288
  if (call.name === "bash") {
177
289
  deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
178
290
  }
291
+ // Auto-detect delete intent from model actions: when delete_file is
292
+ // called, find the matching plan step and set kind="delete". This is
293
+ // language-agnostic — based on what the model DID, not what it said.
294
+ if (call.name === "delete_file" && result.success && deps.trackerRef.current) {
295
+ const deletedPath = String(call.arguments?.path ?? "");
296
+ if (deletedPath) {
297
+ const plan = deps.trackerRef.current.getPlan();
298
+ const tokens = extractFileLikeTokens(stripUrls(deletedPath));
299
+ for (const step of plan.steps) {
300
+ if (step.status !== "pending" && step.status !== "in_progress")
301
+ continue;
302
+ const stepTokens = extractFileLikeTokens(stripUrls(step.description));
303
+ const overlap = tokens.some((t) => stepTokens.some((st) => t.endsWith(st) || st.endsWith(t)));
304
+ if (overlap && step.kind !== "delete") {
305
+ step.kind = "delete";
306
+ deps.store.saveActive(plan);
307
+ break;
308
+ }
309
+ }
310
+ }
311
+ }
179
312
  // A write/edit/bash that reports success but still carries a
180
313
  // type/syntax error in its output is NOT a success — the model must
181
314
  // fix the actual error, not keep rewriting the file blindly. Feed it
182
315
  // to the stuck detector so the rewrite-loop hint fires.
183
316
  const toolText = String(result.output ?? "");
184
- if (/error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText)) {
317
+ const hasTypeError = /error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText);
318
+ if (hasTypeError) {
185
319
  deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
186
320
  }
321
+ // Track compile failures per-file so the plan done-gate can refuse
322
+ // "done" while the last edit still doesn't compile (observed: the agent
323
+ // marked plan steps done while the project typecheck failed on every
324
+ // write — a "fix it later" cascade). A CLEAN result clears the map: the
325
+ // debounced project tsc re-ran and passed (or no tsconfig applies), so
326
+ // old failures must not block completion forever.
327
+ if (call.name === "write_file" || call.name === "edit_file") {
328
+ const broken = extractBrokenFiles(toolText);
329
+ if (broken.length > 0) {
330
+ for (const b of broken)
331
+ deps.state.typecheckFailures.set(b.file, b.error);
332
+ }
333
+ else if (result.success) {
334
+ deps.state.typecheckFailures.clear();
335
+ }
336
+ }
187
337
  if (!result.success) {
338
+ // Feed the bash attempt outcome into the flailing window BEFORE the
339
+ // per-command handling — rotation across different failing commands
340
+ // (wc → cat → Get-Content → …) must still trip the detector.
341
+ if (call.name === "bash") {
342
+ deps.stuckDetector.recordBashAttempt(false);
343
+ }
188
344
  // Hard stop for forbidden Windows commands: after the second
189
345
  // failure of the same forbidden command (grep/sed/ls/find/…),
190
346
  // inject a STOP message so the session stops burning bash calls
@@ -205,25 +361,20 @@ export function createExecutionPlugin(deps) {
205
361
  }
206
362
  }
207
363
  }
208
- deps.stuckDetector.recordToolError(call.name, result.output);
209
- // Immediately queue an actionable hint on failure (don't wait for
210
- // the stuck threshold). Flushed in onBeforeThink so it does not
211
- // land between tool messages.
212
- const actionableHints = deps.stuckDetector.getActionableHints();
213
- const alternative = deps.stuckDetector.getToolAlternative();
214
- if (actionableHints.length > 0 || alternative) {
215
- const parts = [...actionableHints];
216
- if (alternative) {
217
- parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
218
- }
219
- deps.pendingMessages.push({
220
- role: "user",
221
- content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
222
- });
364
+ // A type/syntax error in an otherwise "successful" result was already
365
+ // recorded above. A failing call (result.success === false) would
366
+ // otherwise double-count the same error (typecheck branch + failure
367
+ // branch), inflating the error-signature count and triggering the
368
+ // web search at half the intended threshold.
369
+ if (!hasTypeError) {
370
+ deps.stuckDetector.recordToolError(call.name, result.output);
223
371
  }
224
372
  }
225
373
  else {
226
374
  deps.stuckDetector.recordToolSuccess();
375
+ if (call.name === "bash") {
376
+ deps.stuckDetector.recordBashAttempt(true);
377
+ }
227
378
  // When a bash command runs code successfully, suggest marking the
228
379
  // step done — unless the output shows failing tests, which must
229
380
  // never be reported as a clean success.
@@ -242,12 +393,6 @@ export function createExecutionPlugin(deps) {
242
393
  content: `<system-summary>${testRun.framework}: all ${testRun.passed} test(s) passed for "${cmd}". You may mark the current step as done via plan update step=N status=done.</system-summary>`,
243
394
  });
244
395
  }
245
- else if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
246
- deps.pendingMessages.push({
247
- role: "user",
248
- content: `<system-summary>The command "${cmd}" completed successfully. If this was testing your code, mark the current step as done via plan update step=N status=done.</system-summary>`,
249
- });
250
- }
251
396
  }
252
397
  }
253
398
  if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
@@ -265,8 +410,22 @@ export function createExecutionPlugin(deps) {
265
410
  }
266
411
  }
267
412
  }
413
+ }
414
+ // Auto-advance fires after every successful FILESYSTEM-MUTATING tool —
415
+ // not just write_file/edit_file. Small models often create files through
416
+ // bash (`echo ... > f`, heredocs) or delete/move via shell instead of the
417
+ // dedicated tools, and the plan then never advances past the step even
418
+ // though its deliverables exist (certification 3.5/3.8: files on disk,
419
+ // plan stuck at 1/4 and 4/5). Read-only tools MUST stay excluded: a
420
+ // successful read/glob would otherwise complete an "Update X" step whose
421
+ // target file already exists from an earlier step — without any edit.
422
+ // advancePlanIfStepComplete is a no-op when tokens don't resolve.
423
+ if (result.success && FS_MUTATING_TOOLS.has(call.name)) {
268
424
  deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
269
425
  }
426
+ // Automatic web search for a repeatedly failing error (fire-and-forget,
427
+ // results land in pendingMessages on the next onBeforeThink flush).
428
+ deps.maybeSearchError(ctx, call);
270
429
  },
271
430
  };
272
431
  }