micro-models-agent 0.41.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +19 -9
  3. package/dist/cli/completer.js +37 -36
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +23 -48
  6. package/dist/cli/repl-commands.js +12 -40
  7. package/dist/cli/repl.js +87 -217
  8. package/dist/cli/security-commands.js +7 -5
  9. package/dist/cli/setup.js +26 -8
  10. package/dist/config/config.js +5 -52
  11. package/dist/config/defaults.js +5 -29
  12. package/dist/config/experts.js +1 -1
  13. package/dist/config/index.js +3 -3
  14. package/dist/config/security.js +10 -3
  15. package/dist/core/agent-moe.js +10 -2
  16. package/dist/core/agent.js +82 -273
  17. package/dist/core/bootstrap.js +13 -80
  18. package/dist/core/index.js +2 -2
  19. package/dist/core/prompt-builder.js +2 -23
  20. package/dist/core/session-logger.js +4 -46
  21. package/dist/i18n/en.json +2 -75
  22. package/dist/i18n/ru.json +1 -74
  23. package/dist/index.js +1 -1
  24. package/dist/llm/image-utils.js +5 -4
  25. package/dist/llm/index.js +4 -4
  26. package/dist/llm/model-loader.js +6 -6
  27. package/dist/llm/openai-compat.js +34 -40
  28. package/dist/llm/orchestrator.js +29 -33
  29. package/dist/llm/response.js +9 -9
  30. package/dist/logger/app-logger.js +1 -1
  31. package/dist/logger/index.js +1 -1
  32. package/dist/main.js +648 -183
  33. package/dist/migration/backup.js +13 -13
  34. package/dist/migration/detect.js +11 -11
  35. package/dist/migration/index.js +2 -2
  36. package/dist/modules/browser/actions.js +4 -34
  37. package/dist/modules/browser/bridge-server.mjs +202 -202
  38. package/dist/modules/browser/cookie-store.js +6 -6
  39. package/dist/modules/browser/index.js +5 -7
  40. package/dist/modules/browser/module.js +7 -8
  41. package/dist/modules/browser/session.js +84 -87
  42. package/dist/modules/browser/snapshot.js +58 -92
  43. package/dist/modules/browser/types.js +1 -4
  44. package/dist/modules/certification/cli.js +4 -2
  45. package/dist/modules/certification/fact-checker.js +3 -1
  46. package/dist/modules/certification/loader.js +9 -3
  47. package/dist/modules/certification/runner.js +4 -1
  48. package/dist/modules/context/index.js +1 -1
  49. package/dist/modules/context/manager.js +86 -160
  50. package/dist/modules/execution/auditor.js +25 -177
  51. package/dist/modules/execution/module.js +544 -201
  52. package/dist/modules/execution/moe-executor.js +0 -25
  53. package/dist/modules/execution/plan-store.js +3 -1
  54. package/dist/modules/execution/plan-validator.js +10 -10
  55. package/dist/modules/execution/planner.js +1 -6
  56. package/dist/modules/execution/stuck-detector.js +10 -173
  57. package/dist/modules/execution/verifier.js +42 -86
  58. package/dist/modules/hallucination/confidence.js +1 -8
  59. package/dist/modules/hallucination/detector.js +5 -2
  60. package/dist/modules/hallucination/factual.js +64 -3
  61. package/dist/modules/hallucination/index.js +1 -1
  62. package/dist/modules/hallucination/js-identifiers.js +0 -190
  63. package/dist/modules/hallucination/llm-judge.js +3 -1
  64. package/dist/modules/indexer/cache.js +7 -9
  65. package/dist/modules/indexer/index.js +3 -3
  66. package/dist/modules/indexer/module.js +42 -95
  67. package/dist/modules/indexer/walker.js +17 -17
  68. package/dist/modules/lsp/client.js +31 -74
  69. package/dist/modules/lsp/config.js +33 -87
  70. package/dist/modules/lsp/index.js +3 -3
  71. package/dist/modules/lsp/module.js +21 -185
  72. package/dist/modules/mcp/module.js +6 -2
  73. package/dist/modules/memory/index.js +1 -1
  74. package/dist/modules/memory/module.js +23 -71
  75. package/dist/modules/memory/search.js +9 -11
  76. package/dist/modules/memory/store.js +13 -13
  77. package/dist/modules/pipelines/engine.js +10 -10
  78. package/dist/modules/pipelines/index.js +3 -3
  79. package/dist/modules/pipelines/parser.js +14 -17
  80. package/dist/modules/pipelines/template.js +1 -1
  81. package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
  82. package/dist/modules/plugins/builtin/notify.js +2 -3
  83. package/dist/modules/plugins/index.js +1 -1
  84. package/dist/modules/plugins/loader.js +17 -59
  85. package/dist/modules/plugins/manager.js +17 -73
  86. package/dist/modules/processes/index.js +1 -1
  87. package/dist/modules/processes/registry.js +46 -135
  88. package/dist/modules/registry.js +2 -4
  89. package/dist/modules/security/audit-notifier.js +39 -39
  90. package/dist/modules/security/command-validator.js +8 -2
  91. package/dist/modules/security/data-sanitizer.js +9 -1
  92. package/dist/modules/security/encryption.js +56 -58
  93. package/dist/modules/security/network-validator.js +9 -1
  94. package/dist/modules/security/path-validator.js +3 -1
  95. package/dist/modules/security/security-policies.js +19 -3
  96. package/dist/modules/security/session-encryption.js +1 -1
  97. package/dist/modules/security/session-isolation.js +8 -8
  98. package/dist/modules/session/index.js +3 -3
  99. package/dist/modules/session/module.js +5 -5
  100. package/dist/modules/session/store.js +9 -3
  101. package/dist/modules/skills/module.js +2 -1
  102. package/dist/modules/updater/checker.js +6 -70
  103. package/dist/modules/updater/index.js +1 -2
  104. package/dist/modules/user-profile/compressor.js +2 -2
  105. package/dist/modules/user-profile/index.js +1 -1
  106. package/dist/modules/user-profile/profile.js +9 -9
  107. package/dist/tools/attach-image.js +1 -1
  108. package/dist/tools/bash.js +19 -178
  109. package/dist/tools/browser.js +29 -46
  110. package/dist/tools/executor.js +5 -4
  111. package/dist/tools/file-info.js +12 -13
  112. package/dist/tools/filter-tools.js +2 -9
  113. package/dist/tools/glob-tool.js +11 -11
  114. package/dist/tools/grep-tool.js +3 -1
  115. package/dist/tools/index.js +2 -13
  116. package/dist/tools/list-dir.js +17 -18
  117. package/dist/tools/load-skill.js +3 -1
  118. package/dist/tools/path-utils.js +4 -4
  119. package/dist/tools/pipeline-run.js +25 -25
  120. package/dist/tools/process-kill.js +11 -11
  121. package/dist/tools/process-list.js +22 -20
  122. package/dist/tools/process-log.js +18 -22
  123. package/dist/tools/question.js +3 -1
  124. package/dist/tools/read-file.js +2 -10
  125. package/dist/tools/recall.js +37 -44
  126. package/dist/tools/registry.js +4 -15
  127. package/dist/tools/remember.js +29 -29
  128. package/dist/tools/scope-check.js +9 -9
  129. package/dist/tools/subagent.js +9 -54
  130. package/dist/tools/user-input.js +1 -1
  131. package/dist/tools/web-browse.js +3 -3
  132. package/dist/tools/web-fetch.js +3 -3
  133. package/dist/tools/web-search.js +3 -3
  134. package/dist/tools/write-file.js +3 -1
  135. package/dist/ui/box.js +5 -1
  136. package/dist/ui/index.js +6 -6
  137. package/dist/ui/md-formatter.js +33 -33
  138. package/dist/ui/output.js +5 -5
  139. package/dist/ui/renderer.js +10 -15
  140. package/dist/ui/table.js +1 -1
  141. package/package.json +48 -48
  142. package/dist/cli/plugin-commands.js +0 -36
  143. package/dist/cli/run-result.js +0 -22
  144. package/dist/core/version.js +0 -24
  145. package/dist/modules/artifacts/store.js +0 -61
  146. package/dist/modules/browser/bridge-client.js +0 -199
  147. package/dist/modules/browser/bridge-path.js +0 -10
  148. package/dist/modules/browser/driver.js +0 -136
  149. package/dist/modules/context/chunk-query.js +0 -100
  150. package/dist/modules/context/fact-extractor.js +0 -162
  151. package/dist/modules/context/history.js +0 -15
  152. package/dist/modules/execution/audit-runners.js +0 -152
  153. package/dist/modules/execution/execution-plugin.js +0 -272
  154. package/dist/modules/execution/plan-tool.js +0 -508
  155. package/dist/modules/execution/windows-commands.js +0 -41
  156. package/dist/modules/indexer/project-profile.js +0 -183
  157. package/dist/modules/lsp/check-tool.js +0 -58
  158. package/dist/modules/lsp/command.js +0 -60
  159. package/dist/modules/lsp/probe.js +0 -76
  160. package/dist/modules/lsp/project-root.js +0 -32
  161. package/dist/modules/lsp/startup-check.js +0 -141
  162. package/dist/modules/processes/detect.js +0 -34
  163. package/dist/modules/skills/matcher.js +0 -27
  164. package/dist/modules/updater/module.js +0 -116
  165. package/dist/tools/chunk-query.js +0 -99
  166. package/dist/tools/download-file.js +0 -116
  167. package/dist/tools/enable-tools.js +0 -58
  168. package/dist/tools/hidden-tools-block.js +0 -37
  169. package/dist/ui/line-editor.js +0 -703
  170. package/dist/ui/line-math.js +0 -69
  171. package/dist/ui/plan-view.js +0 -103
@@ -1,15 +1,17 @@
1
1
  import { t } from "../../i18n/index";
2
+ import { PlanCreator } from "./planner";
2
3
  import { PlanTracker } from "./tracker";
3
4
  import { StepVerifier } from "./verifier";
4
5
  import { StuckDetector } from "./stuck-detector";
5
- import { Auditor, findExistingFile } from "./auditor";
6
+ import { Auditor } from "./auditor";
6
7
  import { PlanStore } from "./plan-store";
8
+ import { checkPlanCoverage } from "./plan-coverage";
7
9
  import { getMessageText } from "../../llm/provider";
8
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
9
10
  import { existsSync, readFileSync } from "fs";
10
11
  import { resolve } from "path";
11
- import { createPlanToolDefinitions } from "./plan-tool";
12
- import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, } from "./execution-plugin";
12
+ const STUCK_RECOVERY_COOLDOWN = 5;
13
+ const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
14
+ const FORCE_SKIP_THRESHOLD = 10;
13
15
  export class ExecutionModule {
14
16
  name = "execution";
15
17
  tracker = null;
@@ -18,9 +20,17 @@ export class ExecutionModule {
18
20
  auditor;
19
21
  store;
20
22
  baseDir;
23
+ 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();
21
30
  // Skip the first audit after restoring a plan from disk — gives the user
22
31
  // one chance to change the subject before the audit gate locks in.
23
32
  _auditSkipsRemaining = 0;
33
+ stuckNotified = false;
24
34
  /**
25
35
  * Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
26
36
  * but flushed in onBeforeThink, so they never land between an assistant
@@ -28,41 +38,6 @@ export class ExecutionModule {
28
38
  * buries the warnings the model should react to).
29
39
  */
30
40
  pendingMessages = [];
31
- // Per-command failure counters for forbidden Windows commands (grep/sed/ls/
32
- // find/… invoked through the bash tool). A hard stop after the second
33
- // failure is evidence-based (actual command failures), not task
34
- // classification (rule #10).
35
- forbiddenBashFailures = new Map();
36
- /**
37
- * The execution-plugin's mutable state. Owned HERE and passed BY REFERENCE
38
- * to the plugin factory — the plugin writes deps.state.* directly, so the
39
- * assignments are visible on the module without any write-back.
40
- */
41
- state = {
42
- lastRecoveryIteration: -STUCK_RECOVERY_COOLDOWN,
43
- consecutivePlanWarnings: 0,
44
- lastStepId: -1,
45
- stuckNotified: false,
46
- depsGateHints: new Map(),
47
- };
48
- /**
49
- * Read/write accessor for the tracker, shared with the plan-tool and
50
- * execution-plugin factories. The `plan` tool's abort action clears it
51
- * (current = null) — the setter keeps this.tracker in sync. Object-literal
52
- * getters bind `this` to the literal, so the instance is captured in a
53
- * closure instead.
54
- */
55
- trackerRef = (() => {
56
- const self = this;
57
- return {
58
- get current() {
59
- return self.tracker;
60
- },
61
- set current(v) {
62
- self.tracker = v;
63
- },
64
- };
65
- })();
66
41
  constructor(baseDir, stuckThreshold = 6) {
67
42
  this.baseDir = baseDir;
68
43
  this.verifier = new StepVerifier(baseDir);
@@ -79,16 +54,7 @@ export class ExecutionModule {
79
54
  const plan = this.store.loadActive();
80
55
  if (!plan)
81
56
  return false;
82
- const restored = new PlanTracker(plan);
83
- // A plan that was already complete when saved must not come back as the
84
- // active plan — archive it so it cannot re-anchor the agent (restored
85
- // completed plans caused the "continue step 4" loop on restart).
86
- if (restored.isComplete()) {
87
- this.store.archivePlan(plan);
88
- this.tracker = null;
89
- return false;
90
- }
91
- this.tracker = restored;
57
+ this.tracker = new PlanTracker(plan);
92
58
  // Advance to the first non-done step
93
59
  this.tracker.syncCurrentStep();
94
60
  this._auditSkipsRemaining = 1; // restored plan — one free pass
@@ -116,7 +82,9 @@ export class ExecutionModule {
116
82
  }
117
83
  const plan = this.tracker.getPlan();
118
84
  const audit = await this.auditor.audit(plan);
119
- const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
85
+ const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
86
+ ? [`${s.id}. ${s.description}`]
87
+ : []);
120
88
  const done = plan.steps.filter((s) => s.status === "done").length;
121
89
  const passed = audit.passed && pendingSteps.length === 0;
122
90
  return {
@@ -129,7 +97,7 @@ export class ExecutionModule {
129
97
  };
130
98
  }
131
99
  getSystemPromptBlock() {
132
- if (!this.tracker || this.tracker.isComplete())
100
+ if (!this.tracker)
133
101
  return null;
134
102
  return {
135
103
  content: this.tracker.toPromptBlock(),
@@ -138,52 +106,505 @@ export class ExecutionModule {
138
106
  estimatedTokens: 200,
139
107
  };
140
108
  }
141
- /**
142
- * Short "current plan" line for the compaction summary. Null when no plan
143
- * is active. For 9B models the explicit instruction "continue it, do NOT
144
- * create a new plan" matters — after compaction the plan block is visible
145
- * but the model re-creates the plan anyway (observed: ses_msvuao0h).
146
- *
147
- * When the plan is complete, the summary says the plan is done and the
148
- * model may create a new plan — NOT "continue step 4, do NOT create a
149
- * new plan" which anchored the ses_msxjbnm4 loop.
150
- */
151
- getPlanSummary() {
152
- if (!this.tracker)
153
- return null;
154
- const plan = this.tracker.getPlan();
155
- const done = plan.steps.filter((s) => s.status === "done").length;
156
- const total = plan.steps.length;
157
- if (this.tracker.isComplete()) {
158
- return `[${plan.id}] "${plan.title}" ${done}/${total} done — plan complete. You may create a new plan for new work (plan create), or reply with your final answer.`;
159
- }
160
- const current = this.tracker.getCurrentStepIndex() + 1;
161
- return `[${plan.id}] "${plan.title}" ${done}/${total} done, current: step ${current} — continue it (plan show / re-plan), do NOT create a new plan`;
162
- }
163
109
  getToolDefinitions() {
164
- return createPlanToolDefinitions({
165
- store: this.store,
166
- verifier: this.verifier,
167
- trackerRef: this.trackerRef,
168
- preserveActive: () => this.preserveActive(),
169
- setPlan: (p) => this.setPlan(p),
170
- missingStepDeliverables: (s) => this.missingStepDeliverables(s),
171
- stillExistingDeliverables: (s) => this.stillExistingDeliverables(s),
172
- parseKinds: (a, st) => this.parseKinds(a, st),
173
- getTaskText: (c) => this.getTaskText(c),
174
- baseDir: this.baseDir,
175
- });
110
+ return [
111
+ {
112
+ 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?)',
114
+ parameters: {
115
+ type: "object",
116
+ properties: {
117
+ action: {
118
+ type: "string",
119
+ enum: [
120
+ "create",
121
+ "update",
122
+ "show",
123
+ "abort",
124
+ "list",
125
+ "switch",
126
+ "re-plan",
127
+ ],
128
+ },
129
+ title: { type: "string" },
130
+ steps: { type: "array", items: { type: "string" } },
131
+ step: { type: "number" },
132
+ status: { type: "string", enum: ["done", "failed", "skipped"] },
133
+ note: { type: "string" },
134
+ id: { type: "string", description: "Plan id (for switch action)" },
135
+ },
136
+ required: ["action"],
137
+ },
138
+ handler: async (_ctx, args) => {
139
+ 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
+ if (action === "create") {
214
+ const title = String(args.title || "Task Plan");
215
+ const steps = Array.isArray(args.steps)
216
+ ? args.steps.map(String)
217
+ : [];
218
+ if (steps.length === 0) {
219
+ return { success: false, output: t("plan.no_steps") };
220
+ }
221
+ this.preserveActive();
222
+ const plan = PlanCreator.createPlan(title, steps, this.baseDir);
223
+ this.setPlan(plan);
224
+ 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
+ return {
242
+ success: true,
243
+ output,
244
+ display: displayOut,
245
+ };
246
+ }
247
+ if (action === "show") {
248
+ if (!this.tracker) {
249
+ return { success: false, output: t("plan.no_active") };
250
+ }
251
+ const display = this.tracker.toPromptBlock();
252
+ return {
253
+ success: true,
254
+ output: `${t("plan.show_header")}\n${display}`,
255
+ display,
256
+ };
257
+ }
258
+ if (action === "update" && args.step && this.tracker) {
259
+ this.tracker.updateStepStatus(Number(args.step), args.status || "done");
260
+ if (args.note)
261
+ this.tracker.addNote(Number(args.step), String(args.note));
262
+ this.tracker.syncCurrentStep();
263
+ this.store.saveActive(this.tracker.getPlan());
264
+ const progress = this.tracker.getProgressString();
265
+ 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
+ return {
268
+ success: true,
269
+ output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
270
+ display,
271
+ };
272
+ }
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
+ if (action === "abort") {
293
+ if (this.tracker) {
294
+ this.store.archivePlan(this.tracker.getPlan());
295
+ }
296
+ this.tracker = null;
297
+ this.store.clearActive();
298
+ return { success: true, output: t("plan.aborted") };
299
+ }
300
+ if (!this.tracker) {
301
+ return { success: false, output: t("plan.no_active") };
302
+ }
303
+ return {
304
+ success: false,
305
+ output: t("plan.unknown_action", { action }),
306
+ };
307
+ },
308
+ },
309
+ {
310
+ name: "todo",
311
+ description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
312
+ parameters: {
313
+ type: "object",
314
+ properties: {
315
+ action: { type: "string", enum: ["add", "done", "list"] },
316
+ items: { type: "array", items: { type: "string" } },
317
+ },
318
+ required: ["action"],
319
+ },
320
+ handler: async (_ctx, args) => {
321
+ if (!this.tracker) {
322
+ return { success: false, output: t("plan.no_active") };
323
+ }
324
+ const action = String(args.action);
325
+ const currentStep = this.tracker.getCurrentStep();
326
+ if (!currentStep) {
327
+ return { success: false, output: t("plan.step_not_found") };
328
+ }
329
+ if (action === "add" && Array.isArray(args.items)) {
330
+ const items = args.items.map(String);
331
+ currentStep.note = currentStep.note
332
+ ? `${currentStep.note}; ${items.join(", ")}`
333
+ : items.join(", ");
334
+ return {
335
+ success: true,
336
+ output: t("todo.added", {
337
+ count: String(items.length),
338
+ items: items.join(", "),
339
+ }),
340
+ };
341
+ }
342
+ 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
+ return {
351
+ success: true,
352
+ output: t("plan.step_status", {
353
+ step: String(currentStep.id),
354
+ status: "done",
355
+ }),
356
+ display,
357
+ };
358
+ }
359
+ if (action === "list") {
360
+ const note = currentStep.note || "(no sub-tasks)";
361
+ return {
362
+ success: true,
363
+ output: `Step ${currentStep.id}: ${currentStep.description}\nSub-tasks: ${note}`,
364
+ };
365
+ }
366
+ return {
367
+ success: false,
368
+ output: t("todo.unknown_action", { action }),
369
+ };
370
+ },
371
+ },
372
+ {
373
+ name: "verify",
374
+ description: "Run verification for the current step. Checks files mentioned in the step description.",
375
+ parameters: {
376
+ type: "object",
377
+ properties: {
378
+ step: { type: "number", description: "Step number to verify" },
379
+ },
380
+ },
381
+ handler: async (_ctx, args) => {
382
+ if (!this.tracker)
383
+ return { success: false, output: t("plan.no_active") };
384
+ const step = args.step
385
+ ? this.tracker.getStep(Number(args.step))
386
+ : this.tracker.getCurrentStep();
387
+ if (!step)
388
+ return { success: false, output: t("plan.step_not_found") };
389
+ const result = await this.verifier.verifyStep(step.description);
390
+ return {
391
+ success: result.passed,
392
+ output: result.passed
393
+ ? t("verify.passed")
394
+ : t("verify.failed", {
395
+ details: result.failed.map((f) => f.message).join("; "),
396
+ }),
397
+ };
398
+ },
399
+ },
400
+ ];
176
401
  }
177
402
  getPlugin() {
178
- return createExecutionPlugin({
179
- stuckDetector: this.stuckDetector,
180
- trackerRef: this.trackerRef,
181
- pendingMessages: this.pendingMessages,
182
- forbiddenBashFailures: this.forbiddenBashFailures,
183
- state: this.state,
184
- checkPlanAlignment: (call) => this.checkPlanAlignment(call),
185
- advancePlanIfStepComplete: (cm, sl) => this.advancePlanIfStepComplete(cm, sl),
186
- });
403
+ return {
404
+ name: "execution",
405
+ 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;
420
+ }
421
+ 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
+ }
451
+ }
452
+ const stuckReason = this.stuckDetector.getStuckReason();
453
+ 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
+ }
464
+ }
465
+ if (this.stuckDetector.isStuck() ||
466
+ this.stuckDetector.hasRepetitiveToolCalls()) {
467
+ const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
468
+ if (currentIter - this.lastRecoveryIteration >=
469
+ STUCK_RECOVERY_COOLDOWN) {
470
+ const recovery = this.stuckDetector.getRecoveryMessage();
471
+ 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
+ ctx.contextManager.addMessage({
487
+ role: "user",
488
+ content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
489
+ });
490
+ }
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
+ 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
+ }
521
+ }
522
+ },
523
+ onBeforeTool: (_ctx, call) => {
524
+ const warning = this.checkPlanAlignment(call);
525
+ if (warning) {
526
+ this.pendingMessages.push({
527
+ role: "user",
528
+ content: `<system-summary>${warning}</system-summary>`,
529
+ });
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
+ }
547
+ return true;
548
+ },
549
+ onToolCall: (ctx) => {
550
+ const toolName = ctx?.toolName;
551
+ const args = ctx?.args;
552
+ if (toolName && args) {
553
+ this.stuckDetector.recordToolCall(toolName, args);
554
+ }
555
+ },
556
+ onAfterTool: (ctx, call, result) => {
557
+ 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
+ }
574
+ }
575
+ else {
576
+ 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
+ }
588
+ if (result.success &&
589
+ (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);
605
+ }
606
+ },
607
+ };
187
608
  }
188
609
  preserveActive() {
189
610
  if (!this.tracker)
@@ -215,11 +636,11 @@ export class ExecutionModule {
215
636
  ];
216
637
  if (allowedAlways.includes(call.name))
217
638
  return null;
218
- const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
639
+ const stepPaths = (step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
219
640
  if (stepPaths.length === 0)
220
641
  return null;
221
- const argStr = JSON.stringify(call.arguments);
222
- const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
642
+ const argStr = JSON.stringify(call.arguments).toLowerCase();
643
+ const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
223
644
  if (callPaths.length === 0)
224
645
  return null;
225
646
  // Files that already belong to COMPLETED (done/skipped) steps are
@@ -229,13 +650,15 @@ export class ExecutionModule {
229
650
  const finishedPaths = new Set();
230
651
  for (const s of plan.steps) {
231
652
  if (s.status === "done" || s.status === "skipped") {
232
- extractFileLikeTokens(stripUrls(s.description))
233
- .map((p) => p.toLowerCase())
234
- .forEach((p) => finishedPaths.add(p));
653
+ const ps = s.description
654
+ .match(/\b[\w./\\-]+\.[a-z]+/gi)
655
+ ?.map((p) => p.toLowerCase()) ?? [];
656
+ ps.forEach((p) => finishedPaths.add(p));
235
657
  }
236
658
  }
237
659
  const offPath = callPaths.some((p) => {
238
- if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
660
+ if (finishedPaths.has(p) ||
661
+ [...finishedPaths].some((s) => s.includes(p))) {
239
662
  return false;
240
663
  }
241
664
  return !stepPaths.some((s) => p.includes(s) || s.includes(p));
@@ -253,12 +676,7 @@ export class ExecutionModule {
253
676
  if (!step)
254
677
  return;
255
678
  const stepText = step.description.toLowerCase();
256
- // Shared file-token filter (allow-list of real extensions, extension-only
257
- // segments rejected). A step like "Удалить .js, оставить .tsx/.ts" must
258
- // NOT produce the phantom token "tsx/.ts" — that never exists, making
259
- // allGone trivially true and auto-advancing a delete step while the real
260
- // files to delete still exist (the raw regex path did exactly that).
261
- const stepPaths = extractFileLikeTokens(stripUrls(step.description)) || [];
679
+ const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
262
680
  // Step gate: verify dependencies when mentioned — check BEFORE file paths
263
681
  const isDepsStep = stepText.includes("install") ||
264
682
  stepText.includes("зависим") ||
@@ -280,8 +698,8 @@ export class ExecutionModule {
280
698
  const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
281
699
  if (!hasLockFile) {
282
700
  if (contextManager) {
283
- const hints = this.state.depsGateHints.get(step.id) || 0;
284
- this.state.depsGateHints.set(step.id, hints + 1);
701
+ const hints = this.depsGateHints.get(step.id) || 0;
702
+ this.depsGateHints.set(step.id, hints + 1);
285
703
  const force = hints >= 1;
286
704
  const content = force
287
705
  ? t("exec.step_gate_deps_force", { step: String(step.id) })
@@ -300,32 +718,28 @@ export class ExecutionModule {
300
718
  if (stepPaths.length === 0)
301
719
  return;
302
720
  const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
303
- const allGone = stepPaths.every((p) => !existsSync(resolve(this.baseDir, p)));
304
- const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
305
- if (!satisfied)
721
+ if (!allExist)
306
722
  return;
307
723
  // Step gate: verify files have content (not empty)
308
- if (step.kind !== "delete") {
309
- const emptyFiles = [];
310
- for (const p of stepPaths) {
311
- try {
312
- const content = readFileSync(resolve(this.baseDir, p), "utf-8");
313
- if (content.trim().length < 10) {
314
- emptyFiles.push(p);
315
- }
316
- }
317
- catch {
318
- // If we can't read it, rely on existsSync result
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);
319
730
  }
320
731
  }
321
- if (emptyFiles.length > 0 && contextManager) {
322
- contextManager.addMessage({
323
- role: "user",
324
- content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
325
- });
326
- return;
732
+ catch {
733
+ // If we can't read it, rely on existsSync result
327
734
  }
328
735
  }
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
+ }
329
743
  // All checks passed — advance
330
744
  this.tracker?.updateStepStatus(step.id, "done");
331
745
  const hadNext = this.tracker?.advance() ?? false;
@@ -347,68 +761,6 @@ export class ExecutionModule {
347
761
  content: `<system-summary>${nextMsg}</system-summary>`,
348
762
  });
349
763
  }
350
- // Auto-archive a plan that just became complete (all steps done/skipped).
351
- // Keeps a finished plan from staying active forever and anchoring the agent
352
- // to a completed task ("continue step 4, do NOT create a new plan" — the
353
- // ses_msxjbnm4 loop). The per-step deliverable gate already verified files.
354
- if (this.tracker?.isComplete()) {
355
- const completed = this.tracker.getPlan();
356
- this.store.archivePlan(completed);
357
- this.tracker = null;
358
- sessionLog?.plan("auto-archive", `Plan ${completed.id} complete — archived`);
359
- }
360
- }
361
- /**
362
- * File tokens named in a step description that do not resolve to a real
363
- * file. Uses the same shared token extractor (allow-list of real file
364
- * extensions, URLs stripped) and the auditor's subtree resolver, so
365
- * "bicycles.ts" finds "bicycle-shop/src/data/bicycles.ts" and a hostname in
366
- * the description is never reported as a missing deliverable. Steps with no
367
- * file tokens (pure commands, verification runs) cannot be verified.
368
- */
369
- missingStepDeliverables(step) {
370
- const tokens = extractFileLikeTokens(stripUrls(step.description));
371
- if (tokens.length === 0)
372
- return [];
373
- return tokens.filter((p) => !findExistingFile(this.baseDir, p));
374
- }
375
- /**
376
- * File tokens named in a step description that still resolve to a real
377
- * file. The inverse of missingStepDeliverables — used by the gate for
378
- * delete-kind steps ("done" means the files are gone).
379
- */
380
- stillExistingDeliverables(step) {
381
- const tokens = extractFileLikeTokens(stripUrls(step.description));
382
- if (tokens.length === 0)
383
- return [];
384
- return tokens.filter((p) => findExistingFile(this.baseDir, p));
385
- }
386
- /**
387
- * Validate the optional per-step `kinds` argument for plan create and
388
- * update-with-steps. Returns length-aligned kinds or an i18n error string.
389
- * Absent kinds → null (caller falls back to all-create semantics). Used by
390
- * BOTH branches so a rebuild via update can never silently drop kinds.
391
- */
392
- parseKinds(args, steps) {
393
- if (args.kinds === undefined)
394
- return { kinds: null, error: null };
395
- if (!Array.isArray(args.kinds)) {
396
- return { kinds: null, error: t("plan.kinds_not_array") };
397
- }
398
- const kindsRaw = args.kinds.map(String);
399
- if (kindsRaw.length !== steps.length) {
400
- return { kinds: null, error: t("plan.kinds_mismatch") };
401
- }
402
- const invalid = kindsRaw.filter((k) => k !== "create" && k !== "delete");
403
- if (invalid.length > 0) {
404
- return {
405
- kinds: null,
406
- error: t("plan.kinds_invalid", {
407
- kinds: invalid.join(", "),
408
- }),
409
- };
410
- }
411
- return { kinds: kindsRaw, error: null };
412
764
  }
413
765
  /**
414
766
  * Extract the original task statement from the first user message in the
@@ -419,16 +771,7 @@ export class ExecutionModule {
419
771
  if (!manager)
420
772
  return "";
421
773
  const history = manager.getActiveHistory();
422
- // The first user message may be an injected <system-summary> (compaction
423
- // block) — it is NOT the task. Coverage must compare the plan against the
424
- // model's real instruction (observed: a compaction summary's stale file
425
- // list produced a bogus "plan is missing files" warning).
426
- const firstUser = history.find((m) => {
427
- if (m.role !== "user")
428
- return false;
429
- const text = getMessageText(m.content);
430
- return !text.startsWith("<system-summary>") && !text.includes("[Compressed:");
431
- });
774
+ const firstUser = history.find((m) => m.role === "user");
432
775
  if (!firstUser)
433
776
  return "";
434
777
  return getMessageText(firstUser.content).trim();