micro-models-agent 0.41.0 → 0.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +9 -19
  3. package/dist/cli/completer.js +36 -37
  4. package/dist/cli/index.js +2 -2
  5. package/dist/cli/main.js +48 -23
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +40 -12
  8. package/dist/cli/repl.js +217 -87
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +5 -7
  11. package/dist/cli/setup.js +8 -26
  12. package/dist/config/config.js +52 -5
  13. package/dist/config/defaults.js +29 -5
  14. package/dist/config/experts.js +1 -1
  15. package/dist/config/index.js +3 -3
  16. package/dist/config/security.js +3 -10
  17. package/dist/core/agent-moe.js +2 -10
  18. package/dist/core/agent.js +273 -82
  19. package/dist/core/bootstrap.js +80 -13
  20. package/dist/core/index.js +2 -2
  21. package/dist/core/prompt-builder.js +23 -2
  22. package/dist/core/session-logger.js +46 -4
  23. package/dist/core/version.js +24 -0
  24. package/dist/i18n/en.json +75 -2
  25. package/dist/i18n/ru.json +74 -1
  26. package/dist/index.js +1 -1
  27. package/dist/llm/image-utils.js +4 -5
  28. package/dist/llm/index.js +4 -4
  29. package/dist/llm/model-loader.js +6 -6
  30. package/dist/llm/openai-compat.js +40 -34
  31. package/dist/llm/orchestrator.js +33 -29
  32. package/dist/llm/response.js +9 -9
  33. package/dist/logger/app-logger.js +1 -1
  34. package/dist/logger/index.js +1 -1
  35. package/dist/main.js +1111 -954
  36. package/dist/migration/backup.js +13 -13
  37. package/dist/migration/detect.js +11 -11
  38. package/dist/migration/index.js +2 -2
  39. package/dist/modules/artifacts/store.js +61 -0
  40. package/dist/modules/browser/actions.js +34 -4
  41. package/dist/modules/browser/bridge-client.js +199 -0
  42. package/dist/modules/browser/bridge-path.js +10 -0
  43. package/dist/modules/browser/bridge-server.mjs +202 -202
  44. package/dist/modules/browser/cookie-store.js +6 -6
  45. package/dist/modules/browser/driver.js +136 -0
  46. package/dist/modules/browser/index.js +7 -5
  47. package/dist/modules/browser/module.js +8 -7
  48. package/dist/modules/browser/session.js +87 -84
  49. package/dist/modules/browser/snapshot.js +92 -58
  50. package/dist/modules/browser/types.js +4 -1
  51. package/dist/modules/certification/cli.js +2 -4
  52. package/dist/modules/certification/fact-checker.js +1 -3
  53. package/dist/modules/certification/loader.js +3 -9
  54. package/dist/modules/certification/runner.js +1 -4
  55. package/dist/modules/context/chunk-query.js +100 -0
  56. package/dist/modules/context/fact-extractor.js +162 -0
  57. package/dist/modules/context/history.js +15 -0
  58. package/dist/modules/context/index.js +1 -1
  59. package/dist/modules/context/manager.js +160 -86
  60. package/dist/modules/execution/audit-runners.js +152 -0
  61. package/dist/modules/execution/auditor.js +177 -25
  62. package/dist/modules/execution/execution-plugin.js +272 -0
  63. package/dist/modules/execution/module.js +201 -544
  64. package/dist/modules/execution/moe-executor.js +25 -0
  65. package/dist/modules/execution/plan-store.js +1 -3
  66. package/dist/modules/execution/plan-tool.js +508 -0
  67. package/dist/modules/execution/plan-validator.js +10 -10
  68. package/dist/modules/execution/planner.js +6 -1
  69. package/dist/modules/execution/stuck-detector.js +173 -10
  70. package/dist/modules/execution/verifier.js +86 -42
  71. package/dist/modules/execution/windows-commands.js +41 -0
  72. package/dist/modules/hallucination/confidence.js +8 -1
  73. package/dist/modules/hallucination/detector.js +2 -5
  74. package/dist/modules/hallucination/factual.js +3 -64
  75. package/dist/modules/hallucination/index.js +1 -1
  76. package/dist/modules/hallucination/js-identifiers.js +190 -0
  77. package/dist/modules/hallucination/llm-judge.js +1 -3
  78. package/dist/modules/indexer/cache.js +9 -7
  79. package/dist/modules/indexer/index.js +3 -3
  80. package/dist/modules/indexer/module.js +95 -42
  81. package/dist/modules/indexer/project-profile.js +183 -0
  82. package/dist/modules/indexer/walker.js +17 -17
  83. package/dist/modules/lsp/check-tool.js +58 -0
  84. package/dist/modules/lsp/client.js +74 -31
  85. package/dist/modules/lsp/command.js +60 -0
  86. package/dist/modules/lsp/config.js +87 -33
  87. package/dist/modules/lsp/index.js +3 -3
  88. package/dist/modules/lsp/module.js +185 -21
  89. package/dist/modules/lsp/probe.js +76 -0
  90. package/dist/modules/lsp/project-root.js +32 -0
  91. package/dist/modules/lsp/startup-check.js +141 -0
  92. package/dist/modules/mcp/module.js +2 -6
  93. package/dist/modules/memory/index.js +1 -1
  94. package/dist/modules/memory/module.js +71 -23
  95. package/dist/modules/memory/search.js +11 -9
  96. package/dist/modules/memory/store.js +13 -13
  97. package/dist/modules/pipelines/engine.js +10 -10
  98. package/dist/modules/pipelines/index.js +3 -3
  99. package/dist/modules/pipelines/parser.js +17 -14
  100. package/dist/modules/pipelines/template.js +1 -1
  101. package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
  102. package/dist/modules/plugins/builtin/notify.js +3 -2
  103. package/dist/modules/plugins/index.js +1 -1
  104. package/dist/modules/plugins/loader.js +59 -17
  105. package/dist/modules/plugins/manager.js +73 -17
  106. package/dist/modules/processes/detect.js +34 -0
  107. package/dist/modules/processes/index.js +1 -1
  108. package/dist/modules/processes/registry.js +135 -46
  109. package/dist/modules/registry.js +4 -2
  110. package/dist/modules/security/audit-notifier.js +39 -39
  111. package/dist/modules/security/command-validator.js +2 -8
  112. package/dist/modules/security/data-sanitizer.js +1 -9
  113. package/dist/modules/security/encryption.js +58 -56
  114. package/dist/modules/security/network-validator.js +1 -9
  115. package/dist/modules/security/path-validator.js +1 -3
  116. package/dist/modules/security/security-policies.js +3 -19
  117. package/dist/modules/security/session-encryption.js +1 -1
  118. package/dist/modules/security/session-isolation.js +8 -8
  119. package/dist/modules/session/index.js +3 -3
  120. package/dist/modules/session/module.js +5 -5
  121. package/dist/modules/session/store.js +3 -9
  122. package/dist/modules/skills/matcher.js +27 -0
  123. package/dist/modules/skills/module.js +1 -2
  124. package/dist/modules/updater/checker.js +70 -6
  125. package/dist/modules/updater/index.js +2 -1
  126. package/dist/modules/updater/module.js +116 -0
  127. package/dist/modules/user-profile/compressor.js +2 -2
  128. package/dist/modules/user-profile/index.js +1 -1
  129. package/dist/modules/user-profile/profile.js +9 -9
  130. package/dist/tools/attach-image.js +1 -1
  131. package/dist/tools/bash.js +178 -19
  132. package/dist/tools/browser.js +46 -29
  133. package/dist/tools/chunk-query.js +99 -0
  134. package/dist/tools/download-file.js +116 -0
  135. package/dist/tools/enable-tools.js +58 -0
  136. package/dist/tools/executor.js +4 -5
  137. package/dist/tools/file-info.js +13 -12
  138. package/dist/tools/filter-tools.js +9 -2
  139. package/dist/tools/glob-tool.js +11 -11
  140. package/dist/tools/grep-tool.js +1 -3
  141. package/dist/tools/hidden-tools-block.js +37 -0
  142. package/dist/tools/index.js +13 -2
  143. package/dist/tools/list-dir.js +18 -17
  144. package/dist/tools/load-skill.js +1 -3
  145. package/dist/tools/path-utils.js +4 -4
  146. package/dist/tools/pipeline-run.js +25 -25
  147. package/dist/tools/process-kill.js +11 -11
  148. package/dist/tools/process-list.js +20 -22
  149. package/dist/tools/process-log.js +22 -18
  150. package/dist/tools/question.js +1 -3
  151. package/dist/tools/read-file.js +10 -2
  152. package/dist/tools/recall.js +44 -37
  153. package/dist/tools/registry.js +15 -4
  154. package/dist/tools/remember.js +29 -29
  155. package/dist/tools/scope-check.js +9 -9
  156. package/dist/tools/subagent.js +54 -9
  157. package/dist/tools/user-input.js +1 -1
  158. package/dist/tools/web-browse.js +3 -3
  159. package/dist/tools/web-fetch.js +3 -3
  160. package/dist/tools/web-search.js +3 -3
  161. package/dist/tools/write-file.js +1 -3
  162. package/dist/ui/box.js +1 -5
  163. package/dist/ui/index.js +6 -6
  164. package/dist/ui/line-editor.js +703 -0
  165. package/dist/ui/line-math.js +69 -0
  166. package/dist/ui/md-formatter.js +33 -33
  167. package/dist/ui/output.js +5 -5
  168. package/dist/ui/plan-view.js +103 -0
  169. package/dist/ui/renderer.js +15 -10
  170. package/dist/ui/table.js +1 -1
  171. package/package.json +48 -48
@@ -1,17 +1,15 @@
1
1
  import { t } from "../../i18n/index";
2
- import { PlanCreator } from "./planner";
3
2
  import { PlanTracker } from "./tracker";
4
3
  import { StepVerifier } from "./verifier";
5
4
  import { StuckDetector } from "./stuck-detector";
6
- import { Auditor } from "./auditor";
5
+ import { Auditor, findExistingFile } from "./auditor";
7
6
  import { PlanStore } from "./plan-store";
8
- import { checkPlanCoverage } from "./plan-coverage";
9
7
  import { getMessageText } from "../../llm/provider";
8
+ import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
10
9
  import { existsSync, readFileSync } from "fs";
11
10
  import { resolve } from "path";
12
- const STUCK_RECOVERY_COOLDOWN = 5;
13
- const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
14
- const FORCE_SKIP_THRESHOLD = 10;
11
+ import { createPlanToolDefinitions } from "./plan-tool";
12
+ import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, } from "./execution-plugin";
15
13
  export class ExecutionModule {
16
14
  name = "execution";
17
15
  tracker = null;
@@ -20,17 +18,9 @@ export class ExecutionModule {
20
18
  auditor;
21
19
  store;
22
20
  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();
30
21
  // Skip the first audit after restoring a plan from disk — gives the user
31
22
  // one chance to change the subject before the audit gate locks in.
32
23
  _auditSkipsRemaining = 0;
33
- stuckNotified = false;
34
24
  /**
35
25
  * Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
36
26
  * but flushed in onBeforeThink, so they never land between an assistant
@@ -38,6 +28,41 @@ export class ExecutionModule {
38
28
  * buries the warnings the model should react to).
39
29
  */
40
30
  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
+ })();
41
66
  constructor(baseDir, stuckThreshold = 6) {
42
67
  this.baseDir = baseDir;
43
68
  this.verifier = new StepVerifier(baseDir);
@@ -54,7 +79,16 @@ export class ExecutionModule {
54
79
  const plan = this.store.loadActive();
55
80
  if (!plan)
56
81
  return false;
57
- this.tracker = new PlanTracker(plan);
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;
58
92
  // Advance to the first non-done step
59
93
  this.tracker.syncCurrentStep();
60
94
  this._auditSkipsRemaining = 1; // restored plan — one free pass
@@ -82,9 +116,7 @@ export class ExecutionModule {
82
116
  }
83
117
  const plan = this.tracker.getPlan();
84
118
  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
- : []);
119
+ const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
88
120
  const done = plan.steps.filter((s) => s.status === "done").length;
89
121
  const passed = audit.passed && pendingSteps.length === 0;
90
122
  return {
@@ -97,7 +129,7 @@ export class ExecutionModule {
97
129
  };
98
130
  }
99
131
  getSystemPromptBlock() {
100
- if (!this.tracker)
132
+ if (!this.tracker || this.tracker.isComplete())
101
133
  return null;
102
134
  return {
103
135
  content: this.tracker.toPromptBlock(),
@@ -106,505 +138,52 @@ export class ExecutionModule {
106
138
  estimatedTokens: 200,
107
139
  };
108
140
  }
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
+ }
109
163
  getToolDefinitions() {
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
- ];
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
+ });
401
176
  }
402
177
  getPlugin() {
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
- };
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
+ });
608
187
  }
609
188
  preserveActive() {
610
189
  if (!this.tracker)
@@ -636,11 +215,11 @@ export class ExecutionModule {
636
215
  ];
637
216
  if (allowedAlways.includes(call.name))
638
217
  return null;
639
- const stepPaths = (step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
218
+ const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
640
219
  if (stepPaths.length === 0)
641
220
  return null;
642
- const argStr = JSON.stringify(call.arguments).toLowerCase();
643
- const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
221
+ const argStr = JSON.stringify(call.arguments);
222
+ const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
644
223
  if (callPaths.length === 0)
645
224
  return null;
646
225
  // Files that already belong to COMPLETED (done/skipped) steps are
@@ -650,15 +229,13 @@ export class ExecutionModule {
650
229
  const finishedPaths = new Set();
651
230
  for (const s of plan.steps) {
652
231
  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));
232
+ extractFileLikeTokens(stripUrls(s.description))
233
+ .map((p) => p.toLowerCase())
234
+ .forEach((p) => finishedPaths.add(p));
657
235
  }
658
236
  }
659
237
  const offPath = callPaths.some((p) => {
660
- if (finishedPaths.has(p) ||
661
- [...finishedPaths].some((s) => s.includes(p))) {
238
+ if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
662
239
  return false;
663
240
  }
664
241
  return !stepPaths.some((s) => p.includes(s) || s.includes(p));
@@ -676,7 +253,12 @@ export class ExecutionModule {
676
253
  if (!step)
677
254
  return;
678
255
  const stepText = step.description.toLowerCase();
679
- const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
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)) || [];
680
262
  // Step gate: verify dependencies when mentioned — check BEFORE file paths
681
263
  const isDepsStep = stepText.includes("install") ||
682
264
  stepText.includes("зависим") ||
@@ -698,8 +280,8 @@ export class ExecutionModule {
698
280
  const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
699
281
  if (!hasLockFile) {
700
282
  if (contextManager) {
701
- const hints = this.depsGateHints.get(step.id) || 0;
702
- this.depsGateHints.set(step.id, hints + 1);
283
+ const hints = this.state.depsGateHints.get(step.id) || 0;
284
+ this.state.depsGateHints.set(step.id, hints + 1);
703
285
  const force = hints >= 1;
704
286
  const content = force
705
287
  ? t("exec.step_gate_deps_force", { step: String(step.id) })
@@ -718,28 +300,32 @@ export class ExecutionModule {
718
300
  if (stepPaths.length === 0)
719
301
  return;
720
302
  const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
721
- if (!allExist)
303
+ const allGone = stepPaths.every((p) => !existsSync(resolve(this.baseDir, p)));
304
+ const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
305
+ if (!satisfied)
722
306
  return;
723
307
  // 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);
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
730
319
  }
731
320
  }
732
- catch {
733
- // If we can't read it, rely on existsSync result
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;
734
327
  }
735
328
  }
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
329
  // All checks passed — advance
744
330
  this.tracker?.updateStepStatus(step.id, "done");
745
331
  const hadNext = this.tracker?.advance() ?? false;
@@ -761,6 +347,68 @@ export class ExecutionModule {
761
347
  content: `<system-summary>${nextMsg}</system-summary>`,
762
348
  });
763
349
  }
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 };
764
412
  }
765
413
  /**
766
414
  * Extract the original task statement from the first user message in the
@@ -771,7 +419,16 @@ export class ExecutionModule {
771
419
  if (!manager)
772
420
  return "";
773
421
  const history = manager.getActiveHistory();
774
- const firstUser = history.find((m) => m.role === "user");
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
+ });
775
432
  if (!firstUser)
776
433
  return "";
777
434
  return getMessageText(firstUser.content).trim();