micro-models-agent 0.50.0 → 0.50.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 (2) hide show
  1. package/dist/main.js +132 -40
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1991,7 +1991,6 @@ var init_security = __esm(() => {
1991
1991
  "python",
1992
1992
  "python3",
1993
1993
  "node",
1994
- "bun",
1995
1994
  "perl",
1996
1995
  "ruby",
1997
1996
  "del",
@@ -2013,8 +2012,6 @@ var init_security = __esm(() => {
2013
2012
  "--delete",
2014
2013
  "--remove",
2015
2014
  "--dangerous",
2016
- "--yes",
2017
- "-y",
2018
2015
  "--no-confirm"
2019
2016
  ],
2020
2017
  dangerousOperators: [">", ">>", "2>", "2>>", ";", "&&", "||", "|", "`", "$("]
@@ -2539,6 +2536,8 @@ Command: {command}`,
2539
2536
  {lines}`,
2540
2537
  "proc.none": "No background processes running.",
2541
2538
  "proc.not_found": "Process not found: {id}",
2539
+ "proc.missing_id": "Provide the process id: process_kill id=proc_xxx (the id is returned by bash when a command goes to the background, or by process_list).",
2540
+ "proc.missing_id_list": "No background processes are currently running.",
2542
2541
  "proc.killed": "Process {id} (PID {pid}) killed.",
2543
2542
  "proc.kill_failed": "Failed to kill process {id}",
2544
2543
  "proc.list_header": "Background processes",
@@ -2595,6 +2594,8 @@ Fix the error and re-edit the file (a clean write clears the failure), or mark t
2595
2594
  "plan.deleted": "Plan {id} deleted permanently.",
2596
2595
  "plan.purged": "Deleted {count} plan(s). Plan storage is empty.",
2597
2596
  "plan.no_deliverables": "note: step {step} names no files — its completion cannot be auto-verified; run an explicit check (e.g. build/tests) before your final answer.",
2597
+ "plan.bulk_update_rejected": 'Bulk steps[] rewrite via action=update is not supported: it would discard all step statuses. Use "re-plan" (keeps completed steps, replaces the rest) or mark steps individually: plan update step=N status=done|failed|skipped.',
2598
+ "plan.readonly": 'Plan {id} is {status} (read-only) — it cannot be updated. Use "plan switch" to activate it, or create a new plan for new work.',
2598
2599
  "plan.list_legend": "[*] active · [ ] draft · [-] archived",
2599
2600
  "exec.plan_nudge": "You made {count} file-changing tool call(s) without a plan. For tasks that create or modify files, or span multiple steps, create a plan first (plan create) with concrete steps (exact filenames, commands, deliverables), then continue.",
2600
2601
  "todo.added": "Added {count} todo(s): {items}",
@@ -2941,6 +2942,7 @@ Apply a matching solution from these results. If none is relevant — do NOT rep
2941
2942
  "exec.error_search_failed": 'Web search returned nothing for "{query}".',
2942
2943
  "exec.error_search_no_query": "Error output is not meaningful — skipping the web search.",
2943
2944
  "exec.npm_exec_hint": '"could not determine executable to run" — no "bin" for that package/script. Use "npm run <script>" (script must exist in package.json) or "bunx <pkg>" for a package that declares a bin.',
2945
+ "exec.hidden_tool_hint": '"{tool}" is not a shell command — it is an MMA tool that is currently hidden. Call the enable_tools tool with tags ["shell"] to unlock it; it becomes available on the next iteration.',
2944
2946
  "exec.task_reminder": "Task: {task}. Continue making progress — do not repeat failed actions.",
2945
2947
  "hall.max_retries_exhausted": "Model returned empty or insufficient responses after multiple retries",
2946
2948
  "hall.short_response": "Response too short or empty",
@@ -3248,6 +3250,8 @@ var init_ru = __esm(() => {
3248
3250
  {lines}`,
3249
3251
  "proc.none": "Фоновых процессов нет.",
3250
3252
  "proc.not_found": "Процесс не найден: {id}",
3253
+ "proc.missing_id": "Укажите id процесса: process_kill id=proc_xxx (id возвращается bash при уходе команды в фон или выводится через process_list).",
3254
+ "proc.missing_id_list": "Сейчас фоновых процессов нет.",
3251
3255
  "proc.killed": "Процесс {id} (PID {pid}) остановлен.",
3252
3256
  "proc.kill_failed": "Не удалось остановить процесс {id}",
3253
3257
  "proc.list_header": "Фоновые процессы",
@@ -3304,6 +3308,8 @@ var init_ru = __esm(() => {
3304
3308
  "plan.deleted": "План {id} удалён окончательно.",
3305
3309
  "plan.purged": "Удалено планов: {count}. Хранилище планов пусто.",
3306
3310
  "plan.no_deliverables": "примечание: шаг {step} не называет файлов — его выполнение нельзя проверить автоматически; перед финальным ответом явно проверь результат (например, запусти сборку/тесты).",
3311
+ "plan.bulk_update_rejected": 'Массовая перезапись steps[] через action=update не поддерживается: она бы стёрла статусы всех шагов. Используй "re-plan" (сохраняет завершённые шаги, заменяет остальные) или отмечай шаги по одному: plan update step=N status=done|failed|skipped.',
3312
+ "plan.readonly": 'План {id} имеет статус {status} (только чтение) — обновить его нельзя. Используй "plan switch", чтобы активировать его, или создай новый план для новой задачи.',
3307
3313
  "plan.list_legend": "[*] активный · [ ] черновик · [-] архив",
3308
3314
  "exec.plan_nudge": "Ты сделал(а) {count} вызов(ов), изменяющих файлы, без плана. Для задач, создающих/изменяющих файлы или состоящих из нескольких шагов, сначала создай план (plan create) с конкретными шагами (точные имена файлов, команды, результаты), а потом продолжай.",
3309
3315
  "todo.added": "Добавлено {count} задач: {items}",
@@ -3650,6 +3656,7 @@ var init_ru = __esm(() => {
3650
3656
  "exec.error_search_failed": 'Поиск в интернете для "{query}" ничего не дал.',
3651
3657
  "exec.error_search_no_query": "Текст ошибки незначимый — поиск в интернете пропущен.",
3652
3658
  "exec.npm_exec_hint": '"could not determine executable to run" — у пакета/скрипта нет "bin". Используй "npm run <script>" (скрипт должен быть в package.json) или "bunx <pkg>" для пакета с объявленным bin.',
3659
+ "exec.hidden_tool_hint": '"{tool}" — не команда оболочки, это инструмент MMA, который сейчас скрыт. Вызови тул enable_tools с tags ["shell"], чтобы включить его; он станет доступен на следующей итерации.',
3653
3660
  "exec.task_reminder": "Задача: {task}. Продолжай работу — не повторяй неудачные действия.",
3654
3661
  "hall.max_retries_exhausted": "Модель вернула пустой или недостаточный ответ после нескольких попыток",
3655
3662
  "hall.short_response": "Слишком короткий или пустой ответ",
@@ -4200,22 +4207,6 @@ function normalizeLspServerArgs(config) {
4200
4207
  }
4201
4208
  }
4202
4209
  }
4203
- function normalizeStaleSecurityConfig(config) {
4204
- const sec = config.security;
4205
- if (!sec || sec.enabled !== false)
4206
- return;
4207
- const bash = sec.bash;
4208
- if (!bash || bash.enabled !== false || bash.blockDangerousFlags !== false)
4209
- return;
4210
- const LEGACY_OPERATORS = [">", ">>", "2>", "2>>", "`"];
4211
- if (JSON.stringify(bash.dangerousOperators ?? []) !== JSON.stringify(LEGACY_OPERATORS))
4212
- return;
4213
- const defaultCmds = new Set(DEFAULT_SECURITY_CONFIG.bash.blacklist);
4214
- const blacklist = Array.isArray(bash.blacklist) ? bash.blacklist : [];
4215
- if (blacklist.some((cmd) => !defaultCmds.has(cmd)))
4216
- return;
4217
- config.security = JSON.parse(JSON.stringify(DEFAULT_SECURITY_CONFIG));
4218
- }
4219
4210
  function loadJSON(path) {
4220
4211
  try {
4221
4212
  if (existsSync4(path)) {
@@ -4289,7 +4280,6 @@ function loadConfig(options) {
4289
4280
  config.security.contentScan.dangerousPatterns = restoreDangerousPatterns(config.security.contentScan.dangerousPatterns, DEFAULT_SECURITY_CONFIG.contentScan.dangerousPatterns);
4290
4281
  }
4291
4282
  normalizeLspServerArgs(config);
4292
- normalizeStaleSecurityConfig(config);
4293
4283
  config = applyEnvVars(config);
4294
4284
  try {
4295
4285
  const encryptor = new ConfigEncryptor;
@@ -8628,7 +8618,7 @@ Hint: ${t("exec.npm_exec_hint")}`;
8628
8618
  }
8629
8619
  return output;
8630
8620
  }
8631
- var BASH_GRACE_MS = 5000, SPAWN_SETTLE_MS = 100, BG_OUTPUT_PREVIEW_LINES = 15, bashGraceMs, FAILING_FIRST_WORDS, HARD_BLOCK_THRESHOLD = 3, UNIX_TO_WIN_HINTS, UNIX_TO_WIN_TRANSLATE, NEVER_TOOL_CALLS, CLI_FILE_RUN_RE, NPM_EXEC_RE, bashTool;
8621
+ var BASH_GRACE_MS = 5000, SPAWN_SETTLE_MS = 100, BG_OUTPUT_PREVIEW_LINES = 15, bashGraceMs, FAILING_FIRST_WORDS, HARD_BLOCK_THRESHOLD = 3, UNIX_TO_WIN_HINTS, UNIX_TO_WIN_TRANSLATE, NEVER_TOOL_CALLS, CLI_FILE_RUN_RE, NPM_EXEC_RE, HIDDEN_PROCESS_TOOLS, bashTool;
8632
8622
  var init_bash = __esm(() => {
8633
8623
  init_command_validator();
8634
8624
  init_audit_log();
@@ -8694,6 +8684,7 @@ var init_bash = __esm(() => {
8694
8684
  ]);
8695
8685
  CLI_FILE_RUN_RE = /\b(bun|node|deno|python|python3|tsx|ts-node|php|ruby|go\s+run)\S*\s+(run\s+)?["']?[\w./\\-]+\.(ts|js|tsx|jsx|mjs|cjs|py)\b/;
8696
8686
  NPM_EXEC_RE = /could not determine executable to run/i;
8687
+ HIDDEN_PROCESS_TOOLS = ["process_list", "process_log", "process_kill"];
8697
8688
  bashTool = {
8698
8689
  name: "bash",
8699
8690
  icon: "\uD83D\uDCBB",
@@ -8728,6 +8719,13 @@ ${redirected.output}`
8728
8719
  };
8729
8720
  }
8730
8721
  const command = adaptCommandForWindows(originalCommand);
8722
+ const baseCmd = originalCommand.trim().split(/\s+/)[0];
8723
+ if (HIDDEN_PROCESS_TOOLS.includes(baseCmd) && ctx.toolExecutor?.hasTool?.(baseCmd)) {
8724
+ return {
8725
+ success: false,
8726
+ output: t("exec.hidden_tool_hint", { tool: baseCmd })
8727
+ };
8728
+ }
8731
8729
  if (platform2() === "win32") {
8732
8730
  const echoWrite = extractEchoFileWrite(originalCommand);
8733
8731
  if (echoWrite) {
@@ -8930,6 +8928,14 @@ var init_process_log = __esm(() => {
8930
8928
  required: ["id"]
8931
8929
  },
8932
8930
  handler: async (ctx, args) => {
8931
+ if (args.id === undefined || String(args.id).trim() === "") {
8932
+ const known = processRegistry.list();
8933
+ const ids = known.map((p) => p.id);
8934
+ const listPart = ids.length ? `
8935
+ ${t("proc.list_header")}: ${ids.join(", ")}` : `
8936
+ ${t("proc.missing_id_list")}`;
8937
+ return { success: false, output: `${t("proc.missing_id")}${listPart}` };
8938
+ }
8933
8939
  const id = String(args.id);
8934
8940
  const entry = processRegistry.get(id);
8935
8941
  if (!entry) {
@@ -8971,6 +8977,14 @@ var init_process_kill = __esm(() => {
8971
8977
  required: ["id"]
8972
8978
  },
8973
8979
  handler: async (ctx, args) => {
8980
+ if (args.id === undefined || String(args.id).trim() === "") {
8981
+ const known = processRegistry.list();
8982
+ const ids = known.map((p) => p.id);
8983
+ const listPart = ids.length ? `
8984
+ ${t("proc.list_header")}: ${ids.join(", ")}` : `
8985
+ ${t("proc.missing_id_list")}`;
8986
+ return { success: false, output: `${t("proc.missing_id")}${listPart}` };
8987
+ }
8974
8988
  const id = String(args.id);
8975
8989
  const entry = processRegistry.get(id);
8976
8990
  if (!entry) {
@@ -10580,6 +10594,12 @@ class StuckDetector {
10580
10594
  this.consecutiveFailures = 0;
10581
10595
  this.lastFailedTool = "";
10582
10596
  this.escalationCount = 0;
10597
+ for (const [tool, count] of this.toolErrors) {
10598
+ if (count <= 1)
10599
+ this.toolErrors.delete(tool);
10600
+ else
10601
+ this.toolErrors.set(tool, count - 1);
10602
+ }
10583
10603
  if (this.iterationsOnCurrentStep > 0) {
10584
10604
  this.iterationsOnCurrentStep = Math.max(0, this.iterationsOnCurrentStep - 1);
10585
10605
  }
@@ -10770,6 +10790,20 @@ class StuckDetector {
10770
10790
  count: this.repetitionThreshold
10771
10791
  });
10772
10792
  }
10793
+ getWarningKey() {
10794
+ if (this.isStuck())
10795
+ return "stuck";
10796
+ const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
10797
+ if (errorTool)
10798
+ return `tool-errors:${errorTool[0]}`;
10799
+ if (this.hasConsecutiveFailures())
10800
+ return "consecutive";
10801
+ if (this.hasRepetitiveToolCalls())
10802
+ return "repetitive";
10803
+ if (this.hasReadOnlyLoop())
10804
+ return "read-only";
10805
+ return "";
10806
+ }
10773
10807
  getStuckReason() {
10774
10808
  if (this.isStuck()) {
10775
10809
  return t("exec.stuck", {
@@ -18034,6 +18068,16 @@ import { spawn as spawn6, execSync } from "child_process";
18034
18068
  import { existsSync as existsSync32, readFileSync as readFileSync17 } from "fs";
18035
18069
  import { resolve as resolve18, extname as extname4, join as join26 } from "path";
18036
18070
  import { platform as platform5 } from "os";
18071
+ function lintCacheKey(baseDir, lintScript) {
18072
+ return `${baseDir}::${lintScript}`;
18073
+ }
18074
+ function isMissingBinaryError(err, combinedOutput) {
18075
+ if (err.status === 127)
18076
+ return true;
18077
+ if (err.code === "ENOENT")
18078
+ return true;
18079
+ return MISSING_BINARY_RE.test(combinedOutput);
18080
+ }
18037
18081
  function contentHash(content) {
18038
18082
  let h = 5381;
18039
18083
  for (let i = 0;i < content.length; i++) {
@@ -18143,23 +18187,35 @@ class LintOnWritePlugin {
18143
18187
  return null;
18144
18188
  }
18145
18189
  async runProjectLint(ctx, result, signal) {
18190
+ let lintScript;
18146
18191
  try {
18147
18192
  const packageJsonPath = join26(ctx.baseDir, "package.json");
18148
18193
  if (!existsSync32(packageJsonPath)) {
18149
18194
  return;
18150
18195
  }
18151
18196
  const packageJson = JSON.parse(readFileSync17(packageJsonPath, "utf-8"));
18152
- const lintScript = packageJson.scripts?.lint;
18197
+ lintScript = packageJson.scripts?.lint;
18153
18198
  if (!lintScript) {
18154
18199
  return;
18155
18200
  }
18201
+ if (missingLintBinaries.has(lintCacheKey(ctx.baseDir, lintScript))) {
18202
+ ctx.logger.debug(`Lint skipped (linter for "${lintScript}" not installed)`);
18203
+ return;
18204
+ }
18156
18205
  ctx.logger.debug(`Running lint: ${lintScript}`);
18157
18206
  await runAsync(lintScript, ctx.baseDir, 30000, signal);
18158
18207
  ctx.logger.debug("Lint passed");
18159
18208
  } catch (err) {
18160
18209
  const stderr = (err.stderr?.toString?.() || "").trim();
18161
18210
  const stdout = (err.stdout?.toString?.() || "").trim();
18162
- const detail = (stderr || stdout || err.message).split(`
18211
+ const combined = `${stderr}
18212
+ ${stdout}`;
18213
+ if (lintScript && isMissingBinaryError(err, combined)) {
18214
+ missingLintBinaries.add(lintCacheKey(ctx.baseDir, lintScript));
18215
+ ctx.logger.debug(`Lint binary missing — skipping project lint for this session`);
18216
+ return;
18217
+ }
18218
+ const detail = combined.trim().split(`
18163
18219
  `).filter(Boolean).slice(-5).join(`
18164
18220
  `);
18165
18221
  ctx.logger.warn(`Lint failed: ${err.message}`);
@@ -18264,9 +18320,11 @@ function runAsync(command, cwd, timeoutMs, signal) {
18264
18320
  });
18265
18321
  });
18266
18322
  }
18267
- var TYPE_CHECK_DEBOUNCE_MS = 2000, syntaxCache, _winDecoder, plugin;
18323
+ var TYPE_CHECK_DEBOUNCE_MS = 2000, missingLintBinaries, MISSING_BINARY_RE, syntaxCache, _winDecoder, plugin;
18268
18324
  var init_lint_on_write = __esm(() => {
18269
18325
  init_project_root();
18326
+ missingLintBinaries = new Set;
18327
+ MISSING_BINARY_RE = /command not found|not recognized|не является внутренней|не распознан|не є внутрішньою|".+?" не является/i;
18270
18328
  syntaxCache = new Map;
18271
18329
  plugin = new LintOnWritePlugin;
18272
18330
  });
@@ -18294,6 +18352,13 @@ function generatePlanId() {
18294
18352
  }
18295
18353
  return `plan_${id}`;
18296
18354
  }
18355
+ function getPlanProgress(plan) {
18356
+ const done = plan.steps.filter((s) => s.status === "done").length;
18357
+ const skipped = plan.steps.filter((s) => s.status === "skipped").length;
18358
+ const total = plan.steps.length;
18359
+ const settled = done + skipped;
18360
+ return { done, skipped, total, settled, terminal: total > 0 && settled === total };
18361
+ }
18297
18362
 
18298
18363
  class PlanCreator {
18299
18364
  static createPlan(title, stepDescriptions, baseDir, kinds) {
@@ -18334,9 +18399,9 @@ class PlanCreator {
18334
18399
  }
18335
18400
  static toPromptBlock(plan, currentStepIndex) {
18336
18401
  const date = plan.createdAt.slice(0, 10);
18337
- const doneCount = plan.steps.filter((s) => s.status === "done").length;
18338
- const terminal = plan.steps.every((s) => s.status === "done" || s.status === "skipped");
18339
- const progress = terminal ? `${doneCount}/${plan.steps.length} done, complete` : `${doneCount}/${plan.steps.length} done, current: step ${currentStepIndex + 1}`;
18402
+ const p = getPlanProgress(plan);
18403
+ const skippedPart = p.skipped > 0 ? `, ${p.skipped} skipped` : "";
18404
+ const progress = p.terminal ? `${p.settled}/${p.total} done${skippedPart}, complete` : `${p.settled}/${p.total} done${skippedPart}, current: step ${currentStepIndex + 1}`;
18340
18405
  const lines = [
18341
18406
  `[${plan.id}] ${plan.title}`,
18342
18407
  `Dir: ${plan.baseDir}`,
@@ -18402,13 +18467,15 @@ class PlanTracker {
18402
18467
  return this.plan.steps.every((s) => s.status === "done" || s.status === "skipped");
18403
18468
  }
18404
18469
  getProgressString() {
18405
- const done = this.plan.steps.filter((s) => s.status === "done").length;
18406
- const total = this.plan.steps.length;
18407
- const pct = total > 0 ? Math.round(done / total * 100) : 0;
18470
+ const p = getPlanProgress(this.plan);
18471
+ const total = p.total;
18472
+ const settled = p.settled;
18473
+ const pct = total > 0 ? Math.round(settled / total * 100) : 0;
18408
18474
  const barWidth = 10;
18409
- const filled = Math.round(done / total * barWidth);
18475
+ const filled = Math.round(settled / total * barWidth);
18410
18476
  const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
18411
- return `[${this.plan.id}] ${this.plan.title} ${done}/${total} ${bar} ${pct}%`;
18477
+ const skippedPart = p.skipped > 0 ? ` (${p.skipped} skipped)` : "";
18478
+ return `[${this.plan.id}] ${this.plan.title} ${settled}/${total}${skippedPart} ${bar} ${pct}%`;
18412
18479
  }
18413
18480
  toPromptBlock() {
18414
18481
  return PlanCreator.toPromptBlock(this.plan, this.currentStepIndex);
@@ -18786,13 +18853,20 @@ function createExecutionPlugin(deps) {
18786
18853
  }
18787
18854
  const stuckReason = deps.stuckDetector.getStuckReason();
18788
18855
  if (stuckReason) {
18789
- const logIt = deps.stuckDetector.isStuck() ? !deps.state.stuckNotified : true;
18856
+ const isStuck = deps.stuckDetector.isStuck();
18857
+ const warnKey = deps.stuckDetector.getWarningKey();
18858
+ const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
18859
+ const logIt = isStuck ? !deps.state.stuckNotified : warnKey !== deps.state.lastStuckWarnKey || iter - deps.state.lastStuckWarnIter >= STUCK_WARN_REPEAT_EVERY;
18790
18860
  if (logIt) {
18791
18861
  ctx.logger?.warn(stuckReason);
18792
18862
  ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
18793
- if (deps.stuckDetector.isStuck())
18863
+ deps.state.lastStuckWarnKey = warnKey;
18864
+ deps.state.lastStuckWarnIter = iter;
18865
+ if (isStuck)
18794
18866
  deps.state.stuckNotified = true;
18795
18867
  }
18868
+ } else {
18869
+ deps.state.lastStuckWarnKey = "";
18796
18870
  }
18797
18871
  if (deps.stuckDetector.isStuck() || deps.stuckDetector.hasRepetitiveToolCalls() || deps.stuckDetector.hasReadOnlyLoop()) {
18798
18872
  const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
@@ -18973,7 +19047,7 @@ Last compile error: ${first[1]}`;
18973
19047
  }
18974
19048
  };
18975
19049
  }
18976
- var STUCK_RECOVERY_COOLDOWN = 5, MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3, FORCE_SKIP_THRESHOLD = 10, PLAN_NUDGE_THRESHOLD = 2;
19050
+ var STUCK_RECOVERY_COOLDOWN = 5, MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3, FORCE_SKIP_THRESHOLD = 10, STUCK_WARN_REPEAT_EVERY = 5, PLAN_NUDGE_THRESHOLD = 2;
18977
19051
  var init_execution_plugin = __esm(() => {
18978
19052
  init_i18n();
18979
19053
  init_bash();
@@ -19228,12 +19302,12 @@ ${display}`,
19228
19302
  deps.store.archivePlan(plan);
19229
19303
  deps.recordCompleted(plan);
19230
19304
  deps.trackerRef.current = null;
19231
- const done = plan.steps.filter((s) => s.status === "done").length;
19305
+ const p = getPlanProgress(plan);
19232
19306
  return {
19233
19307
  success: true,
19234
19308
  output: t("plan.completed_archived", {
19235
19309
  id: plan.id,
19236
- done: String(done),
19310
+ done: String(p.settled),
19237
19311
  total: String(plan.steps.length)
19238
19312
  })
19239
19313
  };
@@ -19327,13 +19401,13 @@ ${t("plan.no_deliverables", { step: String(stepId) })}` : "";
19327
19401
  deps.store.archivePlan(plan);
19328
19402
  deps.recordCompleted(plan);
19329
19403
  deps.trackerRef.current = null;
19330
- const done = plan.steps.filter((s) => s.status === "done").length;
19404
+ const p = getPlanProgress(plan);
19331
19405
  return {
19332
19406
  success: true,
19333
19407
  output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}
19334
19408
  ${t("plan.completed_archived", {
19335
19409
  id: plan.id,
19336
- done: String(done),
19410
+ done: String(p.settled),
19337
19411
  total: String(plan.steps.length)
19338
19412
  })}${vacuousNote}`
19339
19413
  };
@@ -19348,8 +19422,15 @@ ${progress}${vacuousNote}`,
19348
19422
  display
19349
19423
  };
19350
19424
  }
19351
- if (action === "update" && Array.isArray(args.steps) && args.steps.length > 0 && deps.trackerRef.current) {
19425
+ if (action === "update" && Array.isArray(args.steps) && args.steps.length > 0 && !args.step) {
19352
19426
  const tracker = deps.trackerRef.current;
19427
+ if (!tracker) {
19428
+ return { success: false, output: t("plan.no_active") };
19429
+ }
19430
+ const progressed = tracker.getPlan().steps.some((s) => s.status !== "pending");
19431
+ if (progressed) {
19432
+ return { success: false, output: t("plan.bulk_update_rejected") };
19433
+ }
19353
19434
  const title = String(args.title || tracker.getPlan().title);
19354
19435
  const steps = args.steps.map(String);
19355
19436
  const parsed = deps.parseKinds(args, steps);
@@ -19436,6 +19517,15 @@ ${progress}${vacuousNote}`,
19436
19517
  };
19437
19518
  }
19438
19519
  if (!deps.trackerRef.current) {
19520
+ if (args.id) {
19521
+ const found = deps.store.find(String(args.id));
19522
+ if (found && found.status !== "active") {
19523
+ return {
19524
+ success: false,
19525
+ output: t("plan.readonly", { id: found.plan.id, status: found.status })
19526
+ };
19527
+ }
19528
+ }
19439
19529
  return { success: false, output: t("plan.no_active") };
19440
19530
  }
19441
19531
  return {
@@ -19610,6 +19700,8 @@ class ExecutionModule {
19610
19700
  consecutivePlanWarnings: 0,
19611
19701
  lastStepId: -1,
19612
19702
  stuckNotified: false,
19703
+ lastStuckWarnKey: "",
19704
+ lastStuckWarnIter: -STUCK_WARN_REPEAT_EVERY,
19613
19705
  depsGateHints: new Map,
19614
19706
  mutationsWithoutPlan: 0,
19615
19707
  planNudgeSent: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {