micro-models-agent 0.28.17 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cli/commands.js +3 -116
  2. package/dist/cli/main.js +8 -35
  3. package/dist/cli/repl.js +611 -110
  4. package/dist/cli/setup.js +12 -32
  5. package/dist/config/config.js +30 -46
  6. package/dist/config/defaults.js +1 -10
  7. package/dist/config/security.js +8 -15
  8. package/dist/core/agent-moe.js +12 -24
  9. package/dist/core/agent.js +47 -281
  10. package/dist/core/bootstrap.js +36 -52
  11. package/dist/core/session-logger.js +2 -35
  12. package/dist/i18n/en.json +15 -79
  13. package/dist/i18n/index.js +9 -12
  14. package/dist/i18n/ru.json +15 -79
  15. package/dist/index.js +13 -13
  16. package/dist/llm/openai-compat.js +10 -39
  17. package/dist/logger/app-logger.js +16 -83
  18. package/dist/main.js +625 -243
  19. package/dist/modules/browser/session.js +60 -108
  20. package/dist/modules/context/history.js +15 -0
  21. package/dist/modules/context/manager.js +10 -119
  22. package/dist/modules/execution/auditor.js +39 -33
  23. package/dist/modules/execution/index.js +6 -8
  24. package/dist/modules/execution/module.js +32 -474
  25. package/dist/modules/execution/moe-executor.js +40 -97
  26. package/dist/modules/execution/planner.js +13 -63
  27. package/dist/modules/execution/stuck-detector.js +39 -252
  28. package/dist/modules/execution/tracker.js +7 -21
  29. package/dist/modules/execution/verifier.js +17 -46
  30. package/dist/modules/hallucination/confidence.js +2 -7
  31. package/dist/modules/hallucination/consistency.js +42 -8
  32. package/dist/modules/hallucination/detector.js +21 -26
  33. package/dist/modules/hallucination/factual.js +150 -170
  34. package/dist/modules/hallucination/index.js +4 -5
  35. package/dist/modules/index.js +5 -5
  36. package/dist/modules/mcp/client.js +2 -8
  37. package/dist/modules/memory/store.js +0 -4
  38. package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
  39. package/dist/modules/processes/detect.js +34 -0
  40. package/dist/modules/processes/index.js +2 -1
  41. package/dist/modules/processes/registry.js +35 -125
  42. package/dist/modules/processes/runner.js +110 -9
  43. package/dist/modules/security/audit-log.js +10 -30
  44. package/dist/modules/security/command-validator.js +16 -42
  45. package/dist/modules/security/content-scanner.js +8 -9
  46. package/dist/modules/security/network-validator.js +2 -2
  47. package/dist/modules/security/path-validator.js +10 -64
  48. package/dist/modules/security/security-policies.js +67 -221
  49. package/dist/modules/security/session-encryption.js +25 -42
  50. package/dist/modules/session/manager.js +10 -15
  51. package/dist/modules/session/store.js +8 -62
  52. package/dist/modules/skills/index.js +3 -2
  53. package/dist/modules/skills/matcher.js +27 -0
  54. package/dist/modules/skills/module.js +23 -10
  55. package/dist/tools/bash.js +90 -287
  56. package/dist/tools/create-dir.js +1 -0
  57. package/dist/tools/delete-file.js +1 -0
  58. package/dist/tools/edit-file.js +8 -10
  59. package/dist/tools/executor.js +7 -57
  60. package/dist/tools/grep-tool.js +29 -51
  61. package/dist/tools/index.js +40 -55
  62. package/dist/tools/load-skill.js +18 -14
  63. package/dist/tools/move-file.js +2 -3
  64. package/dist/tools/pipeline-run.js +1 -1
  65. package/dist/tools/read-file.js +5 -15
  66. package/dist/tools/search-history.js +22 -42
  67. package/dist/tools/subagent.js +12 -21
  68. package/dist/tools/web-browse.js +25 -54
  69. package/dist/tools/web-fetch.js +34 -60
  70. package/dist/tools/web-search.js +20 -39
  71. package/dist/tools/write-file.js +10 -13
  72. package/dist/ui/diff.js +16 -9
  73. package/dist/ui/renderer.js +6 -69
  74. package/package.json +1 -1
  75. package/dist/cli/repl-commands.js +0 -633
  76. package/dist/core/workspace.js +0 -76
  77. package/dist/logger/file-log.js +0 -151
  78. package/dist/modules/certification/cli.js +0 -176
  79. package/dist/modules/certification/fact-checker.js +0 -84
  80. package/dist/modules/certification/loader.js +0 -111
  81. package/dist/modules/certification/manifest.js +0 -50
  82. package/dist/modules/certification/runner.js +0 -162
  83. package/dist/modules/certification/scenarios.js +0 -124
  84. package/dist/modules/certification/types.js +0 -1
  85. package/dist/modules/execution/plan-coverage.js +0 -68
  86. package/dist/modules/execution/plan-persister.js +0 -46
  87. package/dist/modules/execution/plan-store.js +0 -159
  88. package/dist/modules/hallucination/js-identifiers.js +0 -72
  89. package/dist/modules/hallucination/llm-judge.js +0 -103
  90. package/dist/modules/lsp/client.js +0 -235
  91. package/dist/modules/lsp/config.js +0 -81
  92. package/dist/modules/lsp/index.js +0 -3
  93. package/dist/modules/lsp/module.js +0 -68
  94. package/dist/modules/lsp/types.js +0 -1
@@ -4,68 +4,29 @@ import { PlanTracker } from "./tracker";
4
4
  import { StepVerifier } from "./verifier";
5
5
  import { StuckDetector } from "./stuck-detector";
6
6
  import { Auditor } from "./auditor";
7
- import { PlanStore } from "./plan-store";
8
- import { checkPlanCoverage } from "./plan-coverage";
9
- import { getMessageText } from "../../llm/provider";
10
- import { existsSync, readFileSync } from "fs";
7
+ import { existsSync } from "fs";
11
8
  import { resolve } from "path";
12
9
  const STUCK_RECOVERY_COOLDOWN = 5;
13
- const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
14
- const FORCE_SKIP_THRESHOLD = 10;
15
10
  export class ExecutionModule {
16
11
  name = "execution";
17
12
  tracker = null;
18
13
  verifier;
19
14
  stuckDetector;
20
15
  auditor;
21
- store;
22
16
  baseDir;
23
17
  lastRecoveryIteration = -STUCK_RECOVERY_COOLDOWN;
24
- consecutivePlanWarnings = 0;
25
- lastStepId = -1;
26
- // How many times the deps-step gate has warned for each step id — used to
27
- // escalate from a soft hint to a MUST-update instruction (models otherwise
28
- // spin on "install dependencies" steps they invented themselves).
29
- depsGateHints = new Map();
30
- // Skip the first audit after restoring a plan from disk — gives the user
31
- // one chance to change the subject before the audit gate locks in.
32
- _auditSkipsRemaining = 0;
33
- stuckNotified = false;
34
- /**
35
- * Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
36
- * but flushed in onBeforeThink, so they never land between an assistant
37
- * tool_call message and its tool results (which breaks message pairing and
38
- * buries the warnings the model should react to).
39
- */
40
- pendingMessages = [];
41
- constructor(baseDir, stuckThreshold = 6) {
18
+ constructor(baseDir, stuckThreshold = 8) {
42
19
  this.baseDir = baseDir;
43
20
  this.verifier = new StepVerifier(baseDir);
44
21
  this.stuckDetector = new StuckDetector(stuckThreshold);
45
22
  this.auditor = new Auditor(baseDir);
46
- this.store = new PlanStore(baseDir);
47
23
  }
48
24
  setPlan(plan) {
49
25
  this.tracker = new PlanTracker(plan);
50
- this.store.saveActive(plan);
51
- this._auditSkipsRemaining = 0; // plan explicitly created — arm the audit gate
52
- }
53
- restorePlan() {
54
- const plan = this.store.loadActive();
55
- if (!plan)
56
- return false;
57
- this.tracker = new PlanTracker(plan);
58
- // Advance to the first non-done step
59
- this.tracker.syncCurrentStep();
60
- this._auditSkipsRemaining = 1; // restored plan — one free pass
61
- return true;
62
26
  }
63
27
  getTracker() {
64
28
  return this.tracker;
65
29
  }
66
- getStore() {
67
- return this.store;
68
- }
69
30
  getStuckDetector() {
70
31
  return this.stuckDetector;
71
32
  }
@@ -76,15 +37,11 @@ export class ExecutionModule {
76
37
  async runFinalAudit() {
77
38
  if (!this.tracker)
78
39
  return null;
79
- if (this._auditSkipsRemaining > 0) {
80
- this._auditSkipsRemaining--;
81
- return null;
82
- }
83
40
  const plan = this.tracker.getPlan();
84
41
  const audit = await this.auditor.audit(plan);
85
- const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
86
- ? [`${s.id}. ${s.description}`]
87
- : []);
42
+ const pendingSteps = plan.steps
43
+ .filter((s) => s.status !== "done" && s.status !== "skipped")
44
+ .map((s) => `${s.id}. ${s.description}`);
88
45
  const done = plan.steps.filter((s) => s.status === "done").length;
89
46
  const passed = audit.passed && pendingSteps.length === 0;
90
47
  return {
@@ -110,106 +67,24 @@ export class ExecutionModule {
110
67
  return [
111
68
  {
112
69
  name: "plan",
113
- description: 'Create, update, show, abort, list, switch, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive current plan and clear active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)',
70
+ description: 'Create, update, show, or abort a multi-step plan. Use "create" at the start of complex tasks. Use "update" after completing each step to track progress. Use "show" to re-print the current plan checklist.',
114
71
  parameters: {
115
72
  type: "object",
116
73
  properties: {
117
74
  action: {
118
75
  type: "string",
119
- enum: [
120
- "create",
121
- "update",
122
- "show",
123
- "abort",
124
- "list",
125
- "switch",
126
- "re-plan",
127
- ],
76
+ enum: ["create", "update", "show", "abort"],
128
77
  },
129
78
  title: { type: "string" },
130
79
  steps: { type: "array", items: { type: "string" } },
131
80
  step: { type: "number" },
132
81
  status: { type: "string", enum: ["done", "failed", "skipped"] },
133
82
  note: { type: "string" },
134
- id: { type: "string", description: "Plan id (for switch action)" },
135
83
  },
136
84
  required: ["action"],
137
85
  },
138
86
  handler: async (_ctx, args) => {
139
87
  const action = String(args.action);
140
- if (action === "list") {
141
- const metas = this.store.listAll();
142
- if (metas.length === 0) {
143
- return { success: true, output: t("plan.list_empty") };
144
- }
145
- const lines = metas.map((m) => {
146
- const icon = m.status === "active"
147
- ? "[*]"
148
- : m.status === "draft"
149
- ? "[ ]"
150
- : "[-]";
151
- const namePart = m.name ? ` (${m.name})` : "";
152
- return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
153
- });
154
- return {
155
- success: true,
156
- output: `${t("plan.list_header")}\n${lines.join("\n")}`,
157
- };
158
- }
159
- if (action === "switch") {
160
- const planId = String(args.id || "");
161
- if (!planId) {
162
- return { success: false, output: t("plan.switch_no_id") };
163
- }
164
- const found = this.store.find(planId);
165
- if (!found) {
166
- return {
167
- success: false,
168
- output: t("plan.not_found", { id: planId }),
169
- };
170
- }
171
- this.preserveActive();
172
- if (found.status === "draft") {
173
- this.store.removeDraft(found.plan.id);
174
- }
175
- else if (found.status === "archived") {
176
- this.store.removeArchived(found.plan.id);
177
- }
178
- this.setPlan(found.plan);
179
- const display = PlanCreator.toPromptBlock(found.plan, 0);
180
- return {
181
- success: true,
182
- output: t("plan.switched", {
183
- id: found.plan.id,
184
- title: found.plan.title,
185
- }),
186
- display,
187
- };
188
- }
189
- if (action === "re-plan") {
190
- if (!this.tracker) {
191
- return { success: false, output: t("plan.no_active") };
192
- }
193
- const newSteps = Array.isArray(args.steps)
194
- ? args.steps.map(String)
195
- : [];
196
- if (newSteps.length === 0) {
197
- return { success: false, output: t("plan.replan_no_steps") };
198
- }
199
- const oldPlan = this.tracker.getPlan();
200
- const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
201
- const keptCount = replanned.steps.length - newSteps.length;
202
- this.setPlan(replanned);
203
- const display = PlanCreator.toPromptBlock(replanned, keptCount);
204
- return {
205
- success: true,
206
- output: t("plan.replanned", {
207
- kept: String(keptCount),
208
- steps: String(newSteps.length),
209
- }),
210
- display,
211
- };
212
- }
213
88
  if (action === "create") {
214
89
  const title = String(args.title || "Task Plan");
215
90
  const steps = Array.isArray(args.steps)
@@ -218,30 +93,13 @@ export class ExecutionModule {
218
93
  if (steps.length === 0) {
219
94
  return { success: false, output: t("plan.no_steps") };
220
95
  }
221
- this.preserveActive();
222
- const plan = PlanCreator.createPlan(title, steps, this.baseDir);
96
+ const plan = PlanCreator.createPlan(title, steps);
223
97
  this.setPlan(plan);
224
98
  const display = PlanCreator.toPromptBlock(plan, 0);
225
- let output = t("plan.created", {
226
- title,
227
- steps: String(steps.length),
228
- });
229
- let displayOut = display;
230
- const taskText = this.getTaskText(_ctx);
231
- if (taskText) {
232
- const { missing } = checkPlanCoverage(taskText, steps);
233
- if (missing.length > 0) {
234
- const warn = t("plan.coverage_warning", {
235
- missing: missing.join(", "),
236
- });
237
- output += `\n⚠️ ${warn}`;
238
- displayOut = `${display}\n⚠️ ${warn}`;
239
- }
240
- }
241
99
  return {
242
100
  success: true,
243
- output,
244
- display: displayOut,
101
+ output: t("plan.created", { title, steps: String(steps.length) }),
102
+ display,
245
103
  };
246
104
  }
247
105
  if (action === "show") {
@@ -259,42 +117,16 @@ export class ExecutionModule {
259
117
  this.tracker.updateStepStatus(Number(args.step), args.status || "done");
260
118
  if (args.note)
261
119
  this.tracker.addNote(Number(args.step), String(args.note));
262
- this.tracker.syncCurrentStep();
263
- this.store.saveActive(this.tracker.getPlan());
264
120
  const progress = this.tracker.getProgressString();
265
121
  const display = this.tracker.toPromptBlock();
266
- _ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
267
122
  return {
268
123
  success: true,
269
124
  output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
270
125
  display,
271
126
  };
272
127
  }
273
- if (action === "update" &&
274
- Array.isArray(args.steps) &&
275
- args.steps.length > 0 &&
276
- this.tracker) {
277
- const title = String(args.title || this.tracker.getPlan().title);
278
- const steps = args.steps.map(String);
279
- const plan = PlanCreator.createPlan(title, steps, this.baseDir);
280
- this.setPlan(plan);
281
- const display = PlanCreator.toPromptBlock(plan, 0);
282
- const output = t("plan.updated", {
283
- title,
284
- steps: String(steps.length),
285
- });
286
- return {
287
- success: true,
288
- output,
289
- display,
290
- };
291
- }
292
128
  if (action === "abort") {
293
- if (this.tracker) {
294
- this.store.archivePlan(this.tracker.getPlan());
295
- }
296
129
  this.tracker = null;
297
- this.store.clearActive();
298
130
  return { success: true, output: t("plan.aborted") };
299
131
  }
300
132
  if (!this.tracker) {
@@ -340,20 +172,9 @@ export class ExecutionModule {
340
172
  };
341
173
  }
342
174
  if (action === "done") {
343
- if (!currentStep) {
344
- return { success: false, output: t("plan.step_not_found") };
345
- }
346
- this.tracker.updateStepStatus(currentStep.id, "done");
347
- this.tracker.syncCurrentStep();
348
- this.store.saveActive(this.tracker.getPlan());
349
- const display = this.tracker.toPromptBlock();
350
175
  return {
351
176
  success: true,
352
- output: t("plan.step_status", {
353
- step: String(currentStep.id),
354
- status: "done",
355
- }),
356
- display,
177
+ output: t("todo.marked_done", { count: "1" }),
357
178
  };
358
179
  }
359
180
  if (action === "list") {
@@ -403,64 +224,17 @@ export class ExecutionModule {
403
224
  return {
404
225
  name: "execution",
405
226
  onBeforeThink: (ctx) => {
406
- // Flush deferred <system-summary> messages first so they are visible
407
- // to the next model call but never interleave tool messages.
408
- if (ctx.contextManager && this.pendingMessages.length > 0) {
409
- for (const m of this.pendingMessages.splice(0)) {
410
- ctx.contextManager.addMessage(m);
411
- }
412
- }
413
- if (this.tracker?.isComplete()) {
414
- // All steps done/skipped — stop counting "no progress" against the
415
- // last step and silence stuck warnings for a finished plan.
416
- this.stuckDetector.reset();
417
- this.consecutivePlanWarnings = 0;
418
- this.lastStepId = -1;
419
- this.stuckNotified = false;
227
+ const step = this.tracker?.getCurrentStep();
228
+ if (this.tracker && step) {
229
+ this.stuckDetector.setCurrentStep(step.id, step.description);
230
+ this.stuckDetector.recordIteration(step.id);
420
231
  }
421
232
  else {
422
- const step = this.tracker?.getCurrentStep();
423
- if (this.tracker && step) {
424
- if (step.id !== this.lastStepId) {
425
- this.consecutivePlanWarnings = 0;
426
- this.lastStepId = step.id;
427
- this.stuckNotified = false;
428
- }
429
- this.stuckDetector.setCurrentStep(step.id, step.description);
430
- this.stuckDetector.recordIteration(step.id);
431
- }
432
- else {
433
- this.stuckDetector.reset();
434
- this.consecutivePlanWarnings = 0;
435
- this.lastStepId = -1;
436
- this.stuckNotified = false;
437
- const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
438
- if (iter === 3 && !this.tracker && ctx.contextManager) {
439
- ctx.contextManager.addMessage({
440
- role: "user",
441
- 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>`,
442
- });
443
- }
444
- if (iter >= 6 && !this.tracker && ctx.contextManager) {
445
- ctx.contextManager.addMessage({
446
- role: "user",
447
- 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>`,
448
- });
449
- }
450
- }
233
+ this.stuckDetector.reset();
451
234
  }
452
235
  const stuckReason = this.stuckDetector.getStuckReason();
453
236
  if (stuckReason) {
454
- // Log once per stuck episode instead of spamming every iteration.
455
- const logIt = this.stuckDetector.isStuck()
456
- ? !this.stuckNotified
457
- : true;
458
- if (logIt) {
459
- ctx.logger?.warn(stuckReason);
460
- ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
461
- if (this.stuckDetector.isStuck())
462
- this.stuckNotified = true;
463
- }
237
+ ctx.logger?.warn(stuckReason);
464
238
  }
465
239
  if (this.stuckDetector.isStuck() ||
466
240
  this.stuckDetector.hasRepetitiveToolCalls()) {
@@ -469,80 +243,22 @@ export class ExecutionModule {
469
243
  STUCK_RECOVERY_COOLDOWN) {
470
244
  const recovery = this.stuckDetector.getRecoveryMessage();
471
245
  if (recovery && ctx.contextManager) {
472
- const lastError = this.stuckDetector.getLastErrorOutput();
473
- const skillHint = lastError
474
- ? "\nIf you have relevant skills available, consider loading one with load_skill for expert guidance."
475
- : "";
476
- // Actionable hints based on actual error output
477
- const actionableHints = this.stuckDetector.getActionableHints();
478
- const actionableHintStr = actionableHints.length > 0
479
- ? `\n${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join("\n") })}`
480
- : "";
481
- // Tool alternative suggestion
482
- const alternative = this.stuckDetector.getToolAlternative();
483
- const altHint = alternative
484
- ? `\nTool "${this.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.`
485
- : "";
486
246
  ctx.contextManager.addMessage({
487
247
  role: "user",
488
- content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
248
+ content: `<system-summary>${recovery}</system-summary>`,
489
249
  });
490
250
  }
491
- const hints = this.stuckDetector.getHints();
492
- if (hints.length > 0 && ctx.contextManager) {
493
- const hintMsg = t("exec.hints", {
494
- hints: hints.map((h) => `- ${h}`).join("\n"),
495
- });
496
- ctx.contextManager.addMessage({
497
- role: "user",
498
- content: `<system-summary>${hintMsg}</system-summary>`,
499
- });
500
- }
501
- this.stuckDetector.recordEscalation();
502
251
  this.lastRecoveryIteration = currentIter;
503
- if (this.stuckDetector.shouldEscalate() && ctx.onMeta) {
504
- const escalation = t("exec.escalation", {
505
- stepId: String(this.tracker?.getCurrentStep()?.id ?? "?"),
506
- description: this.tracker?.getCurrentStep()?.description ?? "",
507
- });
508
- ctx.onMeta(escalation);
509
- }
510
- // Force skip after too many iterations on the same step
511
- if (this.stuckDetector.getIterationsOnCurrentStep() >=
512
- FORCE_SKIP_THRESHOLD &&
513
- ctx.contextManager) {
514
- const step = this.tracker?.getCurrentStep();
515
- ctx.contextManager.addMessage({
516
- role: "user",
517
- content: `<system-summary>STOP. Step ${step?.id ?? "?"} ("${step?.description ?? ""}") took ${this.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>`,
518
- });
519
- }
520
252
  }
521
253
  }
522
254
  },
523
- onBeforeTool: (_ctx, call) => {
255
+ onBeforeTool: (ctx, call) => {
524
256
  const warning = this.checkPlanAlignment(call);
525
- if (warning) {
526
- this.pendingMessages.push({
257
+ if (warning && ctx.contextManager) {
258
+ ctx.contextManager.addMessage({
527
259
  role: "user",
528
260
  content: `<system-summary>${warning}</system-summary>`,
529
261
  });
530
- this.consecutivePlanWarnings++;
531
- if (this.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
532
- this.pendingMessages.push({
533
- role: "user",
534
- content: `<system-summary>${t("exec.plan_blocked", { step: String(this.tracker?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`,
535
- });
536
- // Block with an explicit reason so the model sees WHY the call
537
- // was blocked (executor shows it via tool.blocked_reason).
538
- return t("exec.plan_blocked", {
539
- step: String(this.tracker?.getCurrentStep()?.id ?? "?"),
540
- max: MAX_PLAN_WARNINGS_BEFORE_BLOCK,
541
- });
542
- }
543
- }
544
- else {
545
- this.consecutivePlanWarnings = 0;
546
262
  }
547
263
  return true;
548
264
  },
@@ -555,68 +271,19 @@ export class ExecutionModule {
555
271
  },
556
272
  onAfterTool: (ctx, call, result) => {
557
273
  if (!result.success) {
558
- this.stuckDetector.recordToolError(call.name, result.output);
559
- // Immediately queue an actionable hint on failure (don't wait for
560
- // the stuck threshold). Flushed in onBeforeThink so it does not
561
- // land between tool messages.
562
- const actionableHints = this.stuckDetector.getActionableHints();
563
- const alternative = this.stuckDetector.getToolAlternative();
564
- if (actionableHints.length > 0 || alternative) {
565
- const parts = [...actionableHints];
566
- if (alternative) {
567
- parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
568
- }
569
- this.pendingMessages.push({
570
- role: "user",
571
- content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
572
- });
573
- }
274
+ this.stuckDetector.recordToolError(call.name);
574
275
  }
575
276
  else {
576
277
  this.stuckDetector.recordToolSuccess();
577
- // When a bash command runs code successfully, suggest marking step as done
578
- if (call.name === "bash" && result.success) {
579
- const cmd = String(call.arguments?.command ?? "");
580
- if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
581
- this.pendingMessages.push({
582
- role: "user",
583
- 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>`,
584
- });
585
- }
586
- }
587
278
  }
588
- if (result.success &&
279
+ if (this.tracker &&
280
+ result.success &&
589
281
  (call.name === "write_file" || call.name === "edit_file")) {
590
- const filePath = call.arguments?.path;
591
- if (filePath) {
592
- this.stuckDetector.recordFileRewrite(filePath);
593
- if (this.stuckDetector.hasExcessiveRewrites()) {
594
- const file = this.stuckDetector.getExcessiveRewriteFile();
595
- const count = this.stuckDetector.getFileRewriteCount(file);
596
- if (ctx.onMeta) {
597
- ctx.onMeta(t("exec.file_rewrite_warning", {
598
- file: file,
599
- count: String(count),
600
- }));
601
- }
602
- }
603
- }
604
- this.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
282
+ this.advancePlanIfStepComplete();
605
283
  }
606
284
  },
607
285
  };
608
286
  }
609
- preserveActive() {
610
- if (!this.tracker)
611
- return;
612
- const plan = this.tracker.getPlan();
613
- if (plan.steps.every((s) => s.status === "done" || s.status === "skipped")) {
614
- this.store.archivePlan(plan);
615
- }
616
- else {
617
- this.store.saveDraft(plan);
618
- }
619
- }
620
287
  checkPlanAlignment(call) {
621
288
  if (!this.tracker)
622
289
  return null;
@@ -633,6 +300,8 @@ export class ExecutionModule {
633
300
  "grep",
634
301
  "file_info",
635
302
  "load_skill",
303
+ "question",
304
+ "approve",
636
305
  ];
637
306
  if (allowedAlways.includes(call.name))
638
307
  return null;
@@ -643,26 +312,7 @@ export class ExecutionModule {
643
312
  const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
644
313
  if (callPaths.length === 0)
645
314
  return null;
646
- // Files that already belong to COMPLETED (done/skipped) steps are
647
- // legitimate rewrite targets — the agent iterates on them while fixing
648
- // LSP/type errors. Blocking those would stop normal bug-fixing.
649
- const plan = this.tracker.getPlan();
650
- const finishedPaths = new Set();
651
- for (const s of plan.steps) {
652
- if (s.status === "done" || s.status === "skipped") {
653
- const ps = s.description
654
- .match(/\b[\w./\\-]+\.[a-z]+/gi)
655
- ?.map((p) => p.toLowerCase()) ?? [];
656
- ps.forEach((p) => finishedPaths.add(p));
657
- }
658
- }
659
- const offPath = callPaths.some((p) => {
660
- if (finishedPaths.has(p) ||
661
- [...finishedPaths].some((s) => s.includes(p))) {
662
- return false;
663
- }
664
- return !stepPaths.some((s) => p.includes(s) || s.includes(p));
665
- });
315
+ const offPath = callPaths.some((p) => !stepPaths.some((s) => p.includes(s) || s.includes(p)));
666
316
  if (!offPath)
667
317
  return null;
668
318
  return t("exec.plan_warning", {
@@ -671,109 +321,17 @@ export class ExecutionModule {
671
321
  tool: call.name,
672
322
  });
673
323
  }
674
- advancePlanIfStepComplete(contextManager, sessionLog) {
324
+ advancePlanIfStepComplete() {
675
325
  const step = this.tracker?.getCurrentStep();
676
326
  if (!step)
677
327
  return;
678
- const stepText = step.description.toLowerCase();
679
328
  const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
680
- // Step gate: verify dependencies when mentioned — check BEFORE file paths
681
- const isDepsStep = stepText.includes("install") ||
682
- stepText.includes("зависим") ||
683
- stepText.includes("init") ||
684
- stepText.includes("инициализац");
685
- if (isDepsStep) {
686
- const lockFiles = [
687
- "package-lock.json",
688
- "bun.lockb",
689
- "yarn.lock",
690
- "pnpm-lock.yaml",
691
- "node_modules",
692
- "go.sum",
693
- "Cargo.lock",
694
- "Pipfile.lock",
695
- "poetry.lock",
696
- "requirements.txt",
697
- ];
698
- const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
699
- if (!hasLockFile) {
700
- if (contextManager) {
701
- const hints = this.depsGateHints.get(step.id) || 0;
702
- this.depsGateHints.set(step.id, hints + 1);
703
- const force = hints >= 1;
704
- const content = force
705
- ? t("exec.step_gate_deps_force", { step: String(step.id) })
706
- : t("exec.step_gate_deps", {
707
- step: String(step.id),
708
- description: step.description,
709
- });
710
- contextManager.addMessage({
711
- role: "user",
712
- content: `<system-summary>${content}</system-summary>`,
713
- });
714
- }
715
- return;
716
- }
717
- }
718
329
  if (stepPaths.length === 0)
719
330
  return;
720
331
  const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
721
- if (!allExist)
722
- return;
723
- // Step gate: verify files have content (not empty)
724
- const emptyFiles = [];
725
- for (const p of stepPaths) {
726
- try {
727
- const content = readFileSync(resolve(this.baseDir, p), "utf-8");
728
- if (content.trim().length < 10) {
729
- emptyFiles.push(p);
730
- }
731
- }
732
- catch {
733
- // If we can't read it, rely on existsSync result
734
- }
332
+ if (allExist) {
333
+ this.tracker?.updateStepStatus(step.id, "done");
334
+ this.tracker?.advance();
735
335
  }
736
- if (emptyFiles.length > 0 && contextManager) {
737
- contextManager.addMessage({
738
- role: "user",
739
- content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
740
- });
741
- return;
742
- }
743
- // All checks passed — advance
744
- this.tracker?.updateStepStatus(step.id, "done");
745
- const hadNext = this.tracker?.advance() ?? false;
746
- if (this.tracker) {
747
- this.store.saveActive(this.tracker.getPlan());
748
- sessionLog?.plan("auto-advance", `Step ${step.id} auto-completed | ${this.tracker.getProgressString()} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
749
- }
750
- if (contextManager) {
751
- const nextStep = hadNext ? this.tracker?.getCurrentStep() : null;
752
- const nextMsg = nextStep
753
- ? t("exec.step_gate_ok", {
754
- step: String(step.id),
755
- nextStep: String(nextStep.id),
756
- nextDesc: nextStep.description,
757
- })
758
- : t("exec.step_gate_last", { step: String(step.id) });
759
- contextManager.addMessage({
760
- role: "user",
761
- content: `<system-summary>${nextMsg}</system-summary>`,
762
- });
763
- }
764
- }
765
- /**
766
- * Extract the original task statement from the first user message in the
767
- * conversation history. Used to validate plan coverage against requirements.
768
- */
769
- getTaskText(ctx) {
770
- const manager = ctx?.contextManager;
771
- if (!manager)
772
- return "";
773
- const history = manager.getActiveHistory();
774
- const firstUser = history.find((m) => m.role === "user");
775
- if (!firstUser)
776
- return "";
777
- return getMessageText(firstUser.content).trim();
778
336
  }
779
337
  }