micro-models-agent 2.13.1 → 2.15.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.15.0] - 2026-10-01
10
+
11
+ ### Added
12
+
13
+ - **Web UI: stale-translations badge** (`src/modules/webui/i18n-stale.ts`, `i18n-api.ts`, `host-state.ts`, `core/host.ts`, `assets/index.html`, `js/dom.js`, `js/settings.js`, `app.js`, `styles.css`): `GET /api/i18n` and the state snapshot carry `i18nStale` (dict files on disk newer than the server process); the topbar shows a yellow badge with a reload action instead of silently rendering raw keys. New keys `web.i18n.stale`/`web.i18n.reload` (en+ru). Tests: `tests/webui-i18n.test.ts`, `tests/webui-usage.test.ts`, `tests/webui-assets.test.ts`.
14
+
15
+ ### Fixed
16
+
17
+ - **Plan auto-advance no longer completes modify-steps on stale files** (`src/modules/execution/module.ts`): `advancePlanIfStepComplete` and final-audit `autoCloseEvidenceSteps` now require `provablyStale()` to be false — every named file must be created/modified after `plan.createdAt` (fail-open on bad dates and unreadable mtimes, delete-kind unchanged). Previously a step naming a long-existing file auto-completed before a line of code was written (observed: 6 plan steps auto-closed while the agent was still on step 1). Tests: `tests/execution-plan-deliverables.test.ts`.
18
+ - **Critic evidence includes git-tracked changes** (`src/modules/critic/evidence.ts`): `buildEvidencePack` unions tool-arg file lists with `git status --porcelain` (staged + unstaged + untracked, capped, fail-open outside repos), so edits made outside file tools (helper scripts via bash, heredocs) no longer produce false `concern` verdicts about missing changes. Tests: `tests/critic-evidence.test.ts`.
19
+ - **Repeated-failure memory writes are diagnosed, model misses skipped** (`src/core/agent/tool-batch.ts`, `loop-state.ts`): `diagnoseRepeatedFailures()` records distinct error headlines with signature-based cause/solution (edit_file anchor miss → CRLF/stale-read hypothesis) and skips global-memory writes for model errors (invented test paths, exit-code-only grep misses). Previously boilerplate causes and the model's own wrong guesses landed in `errors.md`. Tests: `tests/tool-failure-diagnosis.test.ts`.
20
+ - **Factual check resolves path suffixes** (`src/modules/hallucination/factual.ts`): `webui/state.ts` now matches `src/modules/webui/state.ts` via the cached basename index (no new FS walks), fixing false "mentioned files not found" warnings on short paths. Tests: `tests/hallucination-factual.test.ts`.
21
+ - **`edit_file` tolerates LF anchors in CRLF files** (`src/tools/edit-file.ts`): `spliceNormalized()` matches on normalized line endings and splices the original, keeping the file's ending style (no more "string not found" loops on Windows checkouts). Tests: `tests/edit-file.test.ts`.
22
+ - **Web usage re-render covers the context breakdown** (`src/modules/webui/assets/js/feed.js`): `h.contextBreakdown` added to the `renderUsage` signature so part shifts at constant used/limit repaint the segmented bar and legend. Guard: `tests/webui-assets.test.ts`.
23
+ - **Terminal context-budget part words are localized** (`src/cli/repl.ts`, `src/i18n/en.json`, `ru.json`): the startup banner showed untranslated latin `sys/res/hist` in both locales; now `repl.ctx_part_system/reserve/history` (`system/reserve/history` + `система/резерв/история`). Web legend (usage parts) intentionally unchanged. Tests: `tests/i18n.test.ts`.
24
+ - **Verification hygiene is model-facing** (`src/core/system-prompt.ts`, `src/tools/browser.ts`): verification `mma web` servers must run isolated (`MMA_CONFIG_DIR`), never against the user's live server; without vision capabilities the browser description steers to snapshot text and DOM/API assertions. Tests: `tests/core/bootstrap.test.ts`, `tests/tools/browser.test.ts`.
25
+
26
+ ## [2.14.0] - 2026-10-01
27
+
28
+ ### Changed
29
+
30
+ - **Web UI**: file actions now render as quiet terminal-style lines instead of boxed cards (`src/modules/webui/assets/js/feed.js`, `styles.css`). Each file tool shows `◇ <label> · <path-link> [copy-icon] <status>` in the tool-category color — no more Open/Download/Path pill buttons (path click opens the viewer, copy is an icon-only hover action). Consecutive file rows collapse into an expandable `│ N files` group with a chevron toggle. File-type badges are gone (format detection stays silent for highlighting only; draw.io hint is verbose-only). Dead strings `web.card.open/download/copy_path/download_title` removed, count via new `web.card.files_*` (en+ru). Tests: `tests/webui-assets.test.ts`, `tests/webui-smoke.test.ts`, new `tests/e2e/web-filecard.e2e-test.ts`.
31
+
9
32
  ## [2.13.1] - 2026-09-30
10
33
 
11
34
  ### Fixed
@@ -15,7 +15,7 @@ loadCommands,
15
15
  } from "./js/tabs.js";
16
16
  import {
17
17
  renderSelectors, renderSettings, loadConfig, loadModels, loadProviders,
18
- loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus,
18
+ loadMemory, loadSkills, loadPlugins, applyTheme, currentTheme, applyFontSize, setConnStatus, setStaleBar,
19
19
  } from "./js/settings.js";
20
20
  import {
21
21
  initRail, setPlan as railPlan, railEvent, applyRailI18n,
@@ -88,6 +88,7 @@ function renderState(snapshot, busy) {
88
88
  sidebarSessionChanged(sessionId);
89
89
  renderWorkspace(S.state.host);
90
90
  renderSessionsDropdown();
91
+ setStaleBar(!!(snapshot && snapshot.i18nStale));
91
92
  }
92
93
 
93
94
  async function refresh() {
@@ -259,6 +260,7 @@ function applyStaticI18n() {
259
260
  applySidebarI18n();
260
261
  if (msgs.querySelector(".empty")) showEmptyFeed();
261
262
  if (connStatus && !connStatus.hidden) setConnStatus(true);
263
+ setStaleBar(undefined, true);
262
264
  }
263
265
 
264
266
  async function boot() {
@@ -29,6 +29,7 @@
29
29
  <button class="icon-btn" id="toggleSidebar" title="sidebar" aria-label="sidebar" aria-expanded="false">▤</button>
30
30
  <button class="icon-btn" id="openSettings" title="settings" aria-label="settings" aria-haspopup="dialog" aria-expanded="false">⚙</button>
31
31
  <span class="conn" id="connStatus" role="status" aria-live="polite" hidden></span>
32
+ <span class="conn stale" id="staleBar" role="status" hidden></span>
32
33
  </div>
33
34
 
34
35
  <div class="bar-sub" id="sessBar">
@@ -14,6 +14,7 @@ export const attachStrip = document.getElementById("attachStrip");
14
14
  export const attachErr = document.getElementById("attachErr");
15
15
  export const imgPicker = document.getElementById("imgPicker");
16
16
  export const connStatus = document.getElementById("connStatus");
17
+ export const staleBar = document.getElementById("staleBar");
17
18
  export const liveEl = document.getElementById("live");
18
19
  export const sendBtn = document.getElementById("sendBtn");
19
20
 
@@ -444,56 +444,91 @@ function safeUrl(url) {
444
444
  return null;
445
445
  }
446
446
 
447
- function cardFor(name, args, sid) {
447
+ function maybeGroupFilerow(feed, row) {
448
+ const prev = row.previousElementSibling;
449
+ const group = prev && prev.classList && prev.classList.contains("filegroup") ? prev : null;
450
+ const prevRow = prev && prev.classList && prev.classList.contains("filerow") ? prev : null;
451
+ if (group && group.dataset.open !== "1") {
452
+ group.querySelector(".items").appendChild(row);
453
+ if (row.dataset.fail === "1") group.dataset.fail = "1";
454
+ const n = group.querySelectorAll(".items > .filerow").length;
455
+ group.querySelector(".count").textContent = pluralCount("web.card.files", n);
456
+ group.querySelector(".ok").textContent = group.dataset.fail === "1" ? " ✗" : " ✓";
457
+ return group;
458
+ }
459
+ if (prevRow) {
460
+ const wrap = el("div", "filegroup");
461
+ const head = el("button", "head");
462
+ head.setAttribute("aria-expanded", "false");
463
+ head.appendChild(el("span", "g", "│ "));
464
+ head.appendChild(el("span", "count", pluralCount("web.card.files", 2)));
465
+ const fail = prevRow.dataset.fail === "1" || row.dataset.fail === "1";
466
+ if (fail) wrap.dataset.fail = "1";
467
+ head.appendChild(el("span", "ok", fail ? " ✗" : " ✓"));
468
+ const chev = icon("chevron-down");
469
+ head.appendChild(chev);
470
+ const items = el("div", "items hidden");
471
+ feed.replaceChild(wrap, prevRow);
472
+ items.appendChild(prevRow);
473
+ items.appendChild(row);
474
+ wrap.appendChild(head);
475
+ wrap.appendChild(items);
476
+ head.addEventListener("click", () => {
477
+ const open = wrap.dataset.open === "1";
478
+ wrap.dataset.open = open ? "" : "1";
479
+ head.setAttribute("aria-expanded", open ? "false" : "true");
480
+ items.classList.toggle("hidden", open);
481
+ });
482
+ return wrap;
483
+ }
484
+ return null;
485
+ }
486
+
487
+ function cardFor(name, args, sid, ok) {
448
488
  const feed = feedForEvent(sid);
449
489
  if (!feed) return null;
450
490
  if (FILE_TOOLS.indexOf(name) !== -1) {
451
491
  const path = args.path || args.file || args.dir;
452
492
  if (!path) return null;
453
- const card = el("div", "filecard");
454
- const top = el("div", "fc-top");
455
- top.appendChild(el("span", "g", "◇"));
456
- const nameEl = el("span", "name", path);
457
- nameEl.title = path;
458
- top.appendChild(nameEl);
459
- card.appendChild(top);
460
- const bottom = el("div", "fc-bottom");
461
- bottom.appendChild(el("span", "lab", labelOf(name)));
462
- const actions = el("span", "fc-actions");
463
- const baseName = String(path.split(/[\\/]/).pop() || path);
493
+ const row = el("div", "filerow " + categoryOf(name));
494
+ row.appendChild(el("span", "g", "◇"));
495
+ row.appendChild(el("span", "lab", labelOf(name)));
496
+ row.appendChild(el("span", "sep", " · "));
464
497
  if (OPENABLE_TOOLS.indexOf(name) !== -1) {
465
- const openLink = el("a", "fc-open", t("web.card.open"));
498
+ const openLink = el("a", "path", String(path).split(/[\\/]/).pop() || path);
466
499
  openLink.href = "/api/file?path=" + encodeURIComponent(path);
467
500
  openLink.target = "_blank";
468
501
  openLink.rel = "noopener noreferrer";
469
- openLink.title = t("web.card.open_title");
470
- actions.appendChild(openLink);
471
- const dlLink = el("a", "fc-open", t("web.card.download"));
472
- dlLink.href = "/api/file?path=" + encodeURIComponent(path) + "&download=1";
473
- dlLink.title = t("web.card.download_title");
474
- dlLink.setAttribute("download", baseName);
475
- actions.appendChild(dlLink);
502
+ openLink.title = t("web.card.open_title") + " · " + path;
503
+ row.appendChild(openLink);
504
+ const tail = String(path).split(/[\\/]/).slice(0, -1).join("/");
505
+ if (tail) row.appendChild(el("span", "tail", " · " + tail));
506
+ } else {
507
+ const s = el("span", "tail", path);
508
+ s.title = path;
509
+ row.appendChild(s);
476
510
  }
477
- const copyBtn = el("button", "fc-open", t("web.card.copy_path"));
511
+ const copyBtn = el("button", "copy-btn");
478
512
  copyBtn.type = "button";
479
513
  copyBtn.title = t("web.card.copy_title");
514
+ copyBtn.setAttribute("aria-label", t("web.card.copy_title"));
515
+ copyBtn.appendChild(icon("copy"));
480
516
  copyBtn.addEventListener("click", () => {
481
517
  copyText(path).then((ok) => {
482
- copyBtn.textContent = ok ? "✓ " + t("web.code.copied") : "⚠ " + t("web.code.copy_failed");
483
- window.setTimeout(() => {
484
- copyBtn.textContent = t("web.card.copy_path");
485
- }, 1400);
518
+ copyBtn.replaceChildren(icon(ok ? "check" : "copy"));
519
+ if (!ok) { const st = row.querySelector(".st"); if (st) st.textContent = " ⚠"; }
520
+ window.setTimeout(() => copyBtn.replaceChildren(icon("copy")), 1400);
486
521
  });
487
522
  });
488
- actions.appendChild(copyBtn);
489
- bottom.appendChild(actions);
490
- card.appendChild(bottom);
491
- if (/\.drawio(\.xml)?$/i.test(path)) {
492
- const hint = el("div", "fc-hint", t("web.card.drawio_hint"));
493
- card.appendChild(hint);
523
+ row.appendChild(copyBtn);
524
+ row.appendChild(el("span", "st", ok === false ? " ✗" : ok === true ? " ✓" : ""));
525
+ if (ok === false) row.dataset.fail = "1";
526
+ if (/\.drawio(\.xml)?$/i.test(path) && verbosity() === "verbose") {
527
+ row.appendChild(el("span", "detail", "↳ " + t("web.card.drawio_hint")));
494
528
  }
495
- appendTo(feed, card);
496
- return card;
529
+ appendTo(feed, row);
530
+ try { maybeGroupFilerow(feed, row); } catch (err) { /* fail-open: plain row stays */ }
531
+ return row;
497
532
  }
498
533
  if (LINK_TOOLS.indexOf(name) !== -1 && args.url) {
499
534
  const line = el("div", "linkline");
@@ -543,13 +578,37 @@ export function onToolEnd(data) {
543
578
  S.toolNodes.delete(data.toolCallId);
544
579
  const args = S.pendingArgs.get(data.toolCallId) || {};
545
580
  S.pendingArgs.delete(data.toolCallId);
581
+ // File tools render as a single filerow (viewer link + copy): the `.act`
582
+ // line from `onToolStart` is replaced, otherwise the feed shows the same
583
+ // action twice (act + filerow). Without a path `cardFor` renders nothing,
584
+ // so keep the generic act line in that case.
585
+ const filePath = args.path || args.file || args.dir;
586
+ if (FILE_TOOLS.indexOf(data.name) !== -1 && filePath) {
587
+ if (line && line.parentNode) line.remove();
588
+ if (verbosity() === "quiet") {
589
+ if (data.success) {
590
+ const card = cardFor(data.name, args, data.sessionId, data.success);
591
+ if (card) S.runNodes.push(card);
592
+ S.runDuration += typeof data.durationMs === "number" ? data.durationMs : 0;
593
+ } else {
594
+ const card = cardFor(data.name, args, data.sessionId, data.success);
595
+ const cause = firstOutputLine(data.output);
596
+ if (cause) insertAfter(card || line, el("div", "detail", "↳ " + cause));
597
+ if (card) S.runNodes.push(card);
598
+ }
599
+ return;
600
+ }
601
+ const card = cardFor(data.name, args, data.sessionId, data.success);
602
+ renderToolOutput(card || line, data.output);
603
+ return;
604
+ }
546
605
  if (verbosity() === "quiet") {
547
606
  // Quiet hides tool bodies. Track successful nodes so a run of read-only
548
607
  // actions can fold at the next segment; a failure keeps its line and adds
549
608
  // a single `↳` cause instead of the whole model-facing body.
550
609
  if (data.success) {
551
610
  if (line) S.runNodes.push(line);
552
- const card = cardFor(data.name, args, data.sessionId);
611
+ const card = cardFor(data.name, args, data.sessionId, data.success);
553
612
  if (card) S.runNodes.push(card);
554
613
  S.runDuration += typeof data.durationMs === "number" ? data.durationMs : 0;
555
614
  } else {
@@ -558,7 +617,7 @@ export function onToolEnd(data) {
558
617
  }
559
618
  return;
560
619
  }
561
- if (data.success) cardFor(data.name, args, data.sessionId);
620
+ if (data.success) cardFor(data.name, args, data.sessionId, data.success);
562
621
  renderToolOutput(line, data.output);
563
622
  }
564
623
 
@@ -980,6 +1039,58 @@ function usageRow(label, value, cls) {
980
1039
  return row;
981
1040
  }
982
1041
 
1042
+ // Segmented context bar: system · tools · history · reserve · free, widths
1043
+ // relative to the full context window so the parts always sum to one bar.
1044
+ function contextSegBar(h, used, limit) {
1045
+ const bar = el("div", "bar segbar");
1046
+ const bd = h.contextBreakdown;
1047
+ if (!bd || typeof bd.window !== "number" || bd.window <= 0) {
1048
+ // Legacy fallback (pre-breakdown payloads): single used/limit fill.
1049
+ const fill = el("i");
1050
+ fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
1051
+ bar.appendChild(fill);
1052
+ return bar;
1053
+ }
1054
+ const denom = Math.max(bd.window, bd.used + bd.reserve);
1055
+ const parts = [
1056
+ ["seg-system", bd.system],
1057
+ ["seg-tools", bd.tools],
1058
+ ["seg-history", bd.history],
1059
+ ["seg-reserve", bd.reserve],
1060
+ ["seg-free", bd.free],
1061
+ ];
1062
+ for (const [cls, n] of parts) {
1063
+ if (!n || n <= 0) continue;
1064
+ const seg = el("i", cls);
1065
+ seg.style.width = (n / denom) * 100 + "%";
1066
+ bar.appendChild(seg);
1067
+ }
1068
+ return bar;
1069
+ }
1070
+
1071
+ // Dim legend under the segmented bar: one dot+label+value per context part.
1072
+ function contextLegend(h) {
1073
+ const box = el("div", "seglegend");
1074
+ const bd = h.contextBreakdown;
1075
+ if (!bd) return box;
1076
+ const items = [
1077
+ ["seg-system", "web.usage.part_system", bd.system],
1078
+ ["seg-tools", "web.usage.part_tools", bd.tools],
1079
+ ["seg-history", "web.usage.part_history", bd.history],
1080
+ ["seg-reserve", "web.usage.part_reserve", bd.reserve],
1081
+ ["seg-free", "web.usage.part_free", bd.free],
1082
+ ];
1083
+ for (const [cls, labelKey, n] of items) {
1084
+ if (!n || n <= 0) continue;
1085
+ const item = el("span", "segl-item");
1086
+ item.appendChild(el("i", "segl-dot " + cls));
1087
+ item.appendChild(el("span", "segl-label", t(labelKey)));
1088
+ item.appendChild(el("b", "", fmtTokens(n)));
1089
+ box.appendChild(item);
1090
+ }
1091
+ return box;
1092
+ }
1093
+
983
1094
  // Session row with a copy button next to the id (icon-only, ✓ on success).
984
1095
  function usageSessionRow(sessionId) {
985
1096
  const row = el("div", "kv");
@@ -1010,7 +1121,7 @@ export function renderUsage(h) {
1010
1121
  const cfg = S.state.config || {};
1011
1122
  const sig = JSON.stringify([
1012
1123
  h.sessionId, h.model, h.contextUsed, h.contextLimit, h.cacheHitRate, h.costUsd,
1013
- h.compactionCount, h.contextQuality, h.contextProbe, cfg.model, cfg.contextWindow,
1124
+ h.compactionCount, h.contextQuality, h.contextProbe, h.contextBreakdown, cfg.model, cfg.contextWindow,
1014
1125
  I18N.locale,
1015
1126
  ]);
1016
1127
  if (sig === S.lastUsageSig) return;
@@ -1023,11 +1134,8 @@ export function renderUsage(h) {
1023
1134
  const limit = typeof h.contextLimit === "number" ? h.contextLimit : cfg.contextWindow || null;
1024
1135
  usageEl.appendChild(usageRow(t("web.usage.context"), used != null && limit ? fmtTokens(used) + " / " + fmtTokens(limit) : "—"));
1025
1136
  if (used != null && limit) {
1026
- const bar = el("div", "bar");
1027
- const fill = el("i");
1028
- fill.style.width = Math.min(100, Math.round((used / limit) * 100)) + "%";
1029
- bar.appendChild(fill);
1030
- usageEl.appendChild(bar);
1137
+ usageEl.appendChild(contextSegBar(h, used, limit));
1138
+ usageEl.appendChild(contextLegend(h));
1031
1139
  }
1032
1140
  const hit = typeof h.cacheHitRate === "number" ? h.cacheHitRate : null;
1033
1141
  usageEl.appendChild(usageRow(t("web.usage.cache"), hit != null ? Math.round(hit * 100) + "%" : "—", hit != null ? "" : "dim"));
@@ -3,7 +3,7 @@
3
3
  // share from tabs; confirm/toast from dialog. Never imports app (the locale
4
4
  // refresh reaches the app shell through the store hooks registry).
5
5
  import {
6
- el, icon, link, inlineTextNodes, connStatus, makeToggle,
6
+ el, icon, link, inlineTextNodes, connStatus, staleBar, makeToggle,
7
7
  } from "./dom.js";
8
8
  import { api } from "./api.js";
9
9
  import { t, loadI18n } from "./i18n.js";
@@ -324,6 +324,26 @@ export function setConnStatus(disconnected) {
324
324
  connStatus.textContent = disconnected ? "⋯ " + t("web.conn.reconnecting") : "";
325
325
  }
326
326
 
327
+ // Stale-translations banner: dict files on disk are newer than the running
328
+ // server process, so the chrome may show raw keys. Rebuilt only on S.state
329
+ // change (not on every 2s poll) to keep the reload-button focus stable;
330
+ // `force` re-renders the label after a locale switch.
331
+ let lastStale = null;
332
+ export function setStaleBar(stale, force) {
333
+ if (!staleBar) return;
334
+ // undefined = перерисовать последнее состояние (смена локали).
335
+ const on = stale === undefined ? !!lastStale : !!stale;
336
+ if (!force && on === lastStale) return;
337
+ lastStale = on;
338
+ staleBar.hidden = !on;
339
+ staleBar.textContent = "";
340
+ if (!on) return;
341
+ staleBar.appendChild(el("span", "", "⚠ " + t("web.i18n.stale") + " "));
342
+ staleBar.appendChild(link(t("web.i18n.reload"), "", () => location.reload()));
343
+ }
344
+ return lastStale;
345
+ }
346
+
327
347
  // Panel → named sections lives in `settings-controls.js` (PANEL_SECTIONS).
328
348
  // Custom panels (model/ui/browser…) render a bespoke body first and then their
329
349
  // sections; purely generic panels (general/context/…) render sections only.
@@ -54,6 +54,12 @@
54
54
  --checker-border: #2e2e2e;
55
55
  --ph-light: #eaeef2;
56
56
  --track: #262626;
57
+ /* Context-breakdown segments (system/tools/history/reserve/free). */
58
+ --seg-system: #a78bfa;
59
+ --seg-tools: #38bdf8;
60
+ --seg-history: #34d399;
61
+ --seg-reserve: #fbbf24;
62
+ --seg-free: #3f3f46;
57
63
  --plan-line: #2e2e2e;
58
64
  --plan-bar-fg: #a3a3a3;
59
65
  --sec-h: #a3a3a3;
@@ -124,6 +130,12 @@ body.light {
124
130
  --checker-border: #d0d7de;
125
131
  --ph-light: #eaeef2;
126
132
  --track: #eaeef2;
133
+ /* Context-breakdown segments (system/tools/history/reserve/free). */
134
+ --seg-system: #7c3aed;
135
+ --seg-tools: #0284c7;
136
+ --seg-history: #059669;
137
+ --seg-reserve: #d97706;
138
+ --seg-free: #d0d7de;
127
139
  --plan-line: #d0d7de;
128
140
  --plan-bar-fg: #57606a;
129
141
  --sec-h: #57606a;
@@ -167,6 +179,8 @@ body[data-font="large"] { font-size: 17px; }
167
179
  #openSettings { margin-left: auto; margin-right: 6px; }
168
180
  #toggleSidebar { display: none; }
169
181
  .conn { color: var(--dim-a); font-size: 11.5px; white-space: nowrap; }
182
+ .conn.stale { color: var(--yellow); }
183
+ .conn.stale .linkbtn { font-size: 11.5px; }
170
184
  .tabstrip {
171
185
  display: flex; gap: 4px; align-items: center; overflow-x: auto;
172
186
  flex: 1; min-width: 0;
@@ -377,31 +391,23 @@ body.light .codeblock { border: 1px solid var(--border); }
377
391
  }
378
392
  body.light .imgcard .ph { background: var(--ph-light); }
379
393
  .imgcard .cap { padding: 6px 10px; color: var(--dim-a); font-size: 12px; }
380
- .filecard {
381
- margin-left: 26px; padding: 8px 12px; border-radius: 6px;
382
- background: var(--surface); border: 1px solid var(--border-2); color: var(--muted);
383
- display: flex; flex-direction: column; gap: 6px; align-items: stretch;
384
- font-size: 12.5px; max-width: 480px; min-width: 0;
385
- }
386
- .filecard .fc-top { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
387
- .filecard .g { color: var(--cyan); flex: 0 0 auto; }
388
- .filecard .name {
389
- color: var(--fg); min-width: 0; overflow-wrap: anywhere; word-break: break-word;
390
- }
391
- .filecard .fc-bottom {
392
- display: flex; gap: 10px; align-items: center; justify-content: space-between;
393
- }
394
- .filecard .lab { color: var(--dim-a); min-width: 0; overflow-wrap: anywhere; }
395
- .filecard .fc-open {
396
- flex: 0 0 auto; font: inherit; font-size: 12px; line-height: 1.4;
397
- color: var(--accent); background: transparent; border: 1px solid var(--border-2);
398
- border-radius: 4px; padding: 2px 10px; cursor: pointer; text-decoration: none;
399
- }
400
- .filecard .fc-open:hover { border-color: var(--accent); text-decoration: none; }
401
- .filecard .fc-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
402
- .filecard button.fc-open { font: inherit; }
403
- .filecard .fc-actions { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: 0 0 auto; }
404
- .filecard .fc-hint { color: var(--dim-a); font-size: 12px; }
394
+ .filerow { margin-left: 26px; display: flex; gap: 8px; align-items: baseline; min-width: 0; font-size: 12.5px; color: var(--muted); }
395
+ .filerow .g { flex: 0 0 auto; }
396
+ .filerow.recon .g, .filerow.recon .lab { color: var(--cyan); }
397
+ .filerow.mutate .g, .filerow.mutate .lab { color: var(--magenta); }
398
+ .filerow .sep { color: var(--dim-b); }
399
+ .filerow a.path { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; word-break: break-word; }
400
+ .filerow a.path:hover { text-decoration: underline; }
401
+ .filerow .tail { color: var(--dim-a); overflow-wrap: anywhere; word-break: break-word; }
402
+ .filerow .copy-btn { background: transparent; border: 0; padding: 2px; color: var(--dim-a); cursor: pointer; opacity: 0; }
403
+ .filerow:hover .copy-btn, .filerow:focus-within .copy-btn { opacity: 1; }
404
+ @media (pointer: coarse) { .filerow .copy-btn { opacity: 1; } }
405
+ .filerow .copy-btn:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
406
+ .filerow .copy-btn .svg-icon { width: 14px; height: 14px; }
407
+ .filegroup { margin-left: 26px; font-size: 12.5px; color: var(--muted); }
408
+ .filegroup .head { background: transparent; border: 0; color: inherit; font: inherit; cursor: pointer; display: flex; gap: 6px; align-items: baseline; padding: 0; }
409
+ .filegroup .items.hidden { display: none; }
410
+ .filegroup .items > .filerow { margin-left: 0; }
405
411
  .linkline { margin-left: 26px; min-width: 0; overflow-wrap: anywhere; }
406
412
  .linkline a { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; word-break: break-word; }
407
413
  .linkline a:hover { text-decoration: underline; }
@@ -613,7 +619,7 @@ body.light .sel > button { border: 1px solid var(--border); }
613
619
 
614
620
  /* new rows rise in softly (a bulk history render animates once as a whole —
615
621
  one shared fade, still cheap: opacity/transform only) */
616
- .feed > :is(.u, .md, .act, .think, .q, .filecard, .planblock, .imgcard, .linkline,
622
+ .feed > :is(.u, .md, .act, .think, .q, .filerow, .filegroup, .planblock, .imgcard, .linkline,
617
623
  .toolout, .metric, .detail, .metaln, .warnline, .errline, .cmdout, .sysline,
618
624
  .startbanner, .grouped, .msg-actions, .busy) {
619
625
  animation: rise .18s ease-out;
@@ -640,7 +646,7 @@ body.light .sel > button { border: 1px solid var(--border); }
640
646
  .overlay.open .modal { animation: rise .18s ease-out; }
641
647
 
642
648
  /* hover/active blends */
643
- .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filecard, .icon-btn,
649
+ .sel > button, .menu .mi, .stab, .q .qopt, .q .qsend, .filerow, .filegroup, .icon-btn,
644
650
  .btn, .linkbtn, .chip, .cb-copy, .act-btn, .copy-btn, .composer .attach-btn,
645
651
  .composer .send-btn, .dd-menu .si, .dd-menu .act, .cmdk-item, .toggle {
646
652
  transition: background-color .12s, color .12s, border-color .12s;
@@ -849,6 +855,26 @@ body.light .kv .wn { color: var(--yellow); }
849
855
  .bar { height: 4px; background: var(--track); border-radius: 2px; margin-top: 6px; overflow: hidden; }
850
856
  .bar i { display: block; height: 100%; width: 0; background: var(--green-fill); transition: width .2s; }
851
857
 
858
+ /* Segmented context bar: flex row so widths add up to the full window. */
859
+ .bar.segbar { display: flex; }
860
+ .bar.segbar i { width: auto; flex: 0 0 auto; transition: width .25s; }
861
+ .bar.segbar .seg-system { background: var(--seg-system); }
862
+ .bar.segbar .seg-tools { background: var(--seg-tools); }
863
+ .bar.segbar .seg-history { background: var(--seg-history); }
864
+ .bar.segbar .seg-reserve { background: var(--seg-reserve); }
865
+ .bar.segbar .seg-free { background: var(--seg-free); }
866
+ /* Legend of context parts (dim dot + label + tokens): rows, no boxes. */
867
+ .seglegend { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 5px; }
868
+ .segl-item { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--dim-b); }
869
+ .segl-dot { width: 7px; height: 7px; border-radius: 2px; flex: 0 0 auto; }
870
+ .segl-dot.seg-system { background: var(--seg-system); }
871
+ .segl-dot.seg-tools { background: var(--seg-tools); }
872
+ .segl-dot.seg-history { background: var(--seg-history); }
873
+ .segl-dot.seg-reserve { background: var(--seg-reserve); }
874
+ .segl-dot.seg-free { background: var(--seg-free); }
875
+ .segl-item b { color: var(--muted); font-weight: 600; }
876
+ .segl-label { color: var(--dim-b); }
877
+
852
878
  /* ---- settings modal + nested dialogs (provider, confirm) ---- */
853
879
  .overlay {
854
880
  display: none; position: fixed; inset: 0; z-index: 50;
package/dist/i18n/en.json CHANGED
@@ -592,6 +592,9 @@
592
592
  "repl.model": "Model:",
593
593
  "repl.provider": "Provider:",
594
594
  "repl.context": "Context:",
595
+ "repl.ctx_part_system": "system",
596
+ "repl.ctx_part_reserve": "reserve",
597
+ "repl.ctx_part_history": "history",
595
598
  "repl.sysprompt_label": "System prompt:",
596
599
  "repl.sysprompt_size": "{used} / {budget} tokens",
597
600
  "repl.sysprompt_desc": "System prompt size and blocks (use --full for the text)",
@@ -1052,6 +1055,8 @@
1052
1055
  "web.sessbar.prev": "previous session",
1053
1056
  "web.sessbar.next": "next session",
1054
1057
  "web.conn.reconnecting": "reconnecting…",
1058
+ "web.i18n.stale": "translations updated — restart the server",
1059
+ "web.i18n.reload": "reload page",
1055
1060
  "web.sidebar.plan": "plan",
1056
1061
  "web.sidebar.session": "session",
1057
1062
  "web.sidebar.usage": "usage",
@@ -1118,6 +1123,11 @@
1118
1123
  "web.usage.cache": "cache",
1119
1124
  "web.usage.cost": "cost",
1120
1125
  "web.usage.compactions": "compactions",
1126
+ "web.usage.part_system": "instructions",
1127
+ "web.usage.part_tools": "tools",
1128
+ "web.usage.part_history": "dialogue",
1129
+ "web.usage.part_reserve": "response reserve",
1130
+ "web.usage.part_free": "free",
1121
1131
  "web.usage.quality": "quality",
1122
1132
  "web.usage.probe": "probe",
1123
1133
  "web.usage.probe_loaded": "{actual} loaded",
@@ -1500,13 +1510,12 @@
1500
1510
  "web.code.copy_failed": "copy failed",
1501
1511
  "web.msg.copy": "copy",
1502
1512
  "web.msg.copy_md": "copy as markdown",
1503
- "web.card.open": "open ↗",
1504
- "web.card.download": "download ⤓",
1505
- "web.card.copy_path": "copy path",
1506
1513
  "web.card.open_title": "Preview in a browser tab",
1507
- "web.card.download_title": "Download the file",
1508
1514
  "web.card.copy_title": "Copy the workspace path",
1509
1515
  "web.card.drawio_hint": "Draw.io diagram is XML — preview shows code; open it in draw.io / VS Code to see the scheme.",
1516
+ "web.card.files_one": "{count} file",
1517
+ "web.card.files_few": "{count} files",
1518
+ "web.card.files_many": "{count} files",
1510
1519
  "web.card.screenshot": "screenshot",
1511
1520
  "web.card.attached_image": "attached image",
1512
1521
  "web.card.browser_viewport": "browser · viewport",
package/dist/i18n/ru.json CHANGED
@@ -589,6 +589,9 @@
589
589
  "repl.model": "Модель:",
590
590
  "repl.provider": "Провайдер:",
591
591
  "repl.context": "Контекст:",
592
+ "repl.ctx_part_system": "система",
593
+ "repl.ctx_part_reserve": "резерв",
594
+ "repl.ctx_part_history": "история",
592
595
  "repl.sysprompt_label": "Системный промпт:",
593
596
  "repl.sysprompt_size": "{used} / {budget} токенов",
594
597
  "repl.sysprompt_desc": "Размер системного промпта и блоки (текст — через --full)",
@@ -1055,6 +1058,8 @@
1055
1058
  "web.sessbar.prev": "предыдущая сессия",
1056
1059
  "web.sessbar.next": "следующая сессия",
1057
1060
  "web.conn.reconnecting": "переподключение…",
1061
+ "web.i18n.stale": "переводы обновлены — перезапустите сервер",
1062
+ "web.i18n.reload": "обновить страницу",
1058
1063
  "web.sidebar.plan": "план",
1059
1064
  "web.sidebar.session": "сессия",
1060
1065
  "web.sidebar.usage": "использование",
@@ -1121,6 +1126,11 @@
1121
1126
  "web.usage.cache": "кеш",
1122
1127
  "web.usage.cost": "стоимость",
1123
1128
  "web.usage.compactions": "сжатия",
1129
+ "web.usage.part_system": "инструкции",
1130
+ "web.usage.part_tools": "инструменты",
1131
+ "web.usage.part_history": "диалог",
1132
+ "web.usage.part_reserve": "резерв ответа",
1133
+ "web.usage.part_free": "свободно",
1124
1134
  "web.usage.quality": "качество",
1125
1135
  "web.usage.probe": "проб",
1126
1136
  "web.usage.probe_loaded": "загружено {actual}",
@@ -1476,13 +1486,12 @@
1476
1486
  "web.code.copy_failed": "не скопировано",
1477
1487
  "web.msg.copy": "копировать",
1478
1488
  "web.msg.copy_md": "копировать как markdown",
1479
- "web.card.open": "открыть ↗",
1480
- "web.card.download": "скачать ⤓",
1481
- "web.card.copy_path": "путь",
1482
1489
  "web.card.open_title": "Предпросмотр во вкладке браузера",
1483
- "web.card.download_title": "Скачать файл",
1484
1490
  "web.card.copy_title": "Скопировать путь в рабочей папке",
1485
1491
  "web.card.drawio_hint": "Диаграмма draw.io — это XML: предпросмотр покажет код, схему смотри в draw.io / VS Code.",
1492
+ "web.card.files_one": "{count} файл",
1493
+ "web.card.files_few": "{count} файла",
1494
+ "web.card.files_many": "{count} файлов",
1486
1495
  "web.card.screenshot": "снимок экрана",
1487
1496
  "web.card.attached_image": "прикреплённое изображение",
1488
1497
  "web.card.browser_viewport": "браузер · область просмотра",
package/dist/main.js CHANGED
@@ -3073,6 +3073,9 @@ Available commands:`,
3073
3073
  "repl.model": "Model:",
3074
3074
  "repl.provider": "Provider:",
3075
3075
  "repl.context": "Context:",
3076
+ "repl.ctx_part_system": "system",
3077
+ "repl.ctx_part_reserve": "reserve",
3078
+ "repl.ctx_part_history": "history",
3076
3079
  "repl.sysprompt_label": "System prompt:",
3077
3080
  "repl.sysprompt_size": "{used} / {budget} tokens",
3078
3081
  "repl.sysprompt_desc": "System prompt size and blocks (use --full for the text)",
@@ -3577,6 +3580,8 @@ Updated: {updatedAt}`,
3577
3580
  "web.sessbar.prev": "previous session",
3578
3581
  "web.sessbar.next": "next session",
3579
3582
  "web.conn.reconnecting": "reconnecting…",
3583
+ "web.i18n.stale": "translations updated — restart the server",
3584
+ "web.i18n.reload": "reload page",
3580
3585
  "web.sidebar.plan": "plan",
3581
3586
  "web.sidebar.session": "session",
3582
3587
  "web.sidebar.usage": "usage",
@@ -3643,6 +3648,11 @@ Updated: {updatedAt}`,
3643
3648
  "web.usage.cache": "cache",
3644
3649
  "web.usage.cost": "cost",
3645
3650
  "web.usage.compactions": "compactions",
3651
+ "web.usage.part_system": "instructions",
3652
+ "web.usage.part_tools": "tools",
3653
+ "web.usage.part_history": "dialogue",
3654
+ "web.usage.part_reserve": "response reserve",
3655
+ "web.usage.part_free": "free",
3646
3656
  "web.usage.quality": "quality",
3647
3657
  "web.usage.probe": "probe",
3648
3658
  "web.usage.probe_loaded": "{actual} loaded",
@@ -4025,13 +4035,12 @@ Updated: {updatedAt}`,
4025
4035
  "web.code.copy_failed": "copy failed",
4026
4036
  "web.msg.copy": "copy",
4027
4037
  "web.msg.copy_md": "copy as markdown",
4028
- "web.card.open": "open ↗",
4029
- "web.card.download": "download ⤓",
4030
- "web.card.copy_path": "copy path",
4031
4038
  "web.card.open_title": "Preview in a browser tab",
4032
- "web.card.download_title": "Download the file",
4033
4039
  "web.card.copy_title": "Copy the workspace path",
4034
4040
  "web.card.drawio_hint": "Draw.io diagram is XML — preview shows code; open it in draw.io / VS Code to see the scheme.",
4041
+ "web.card.files_one": "{count} file",
4042
+ "web.card.files_few": "{count} files",
4043
+ "web.card.files_many": "{count} files",
4035
4044
  "web.card.screenshot": "screenshot",
4036
4045
  "web.card.attached_image": "attached image",
4037
4046
  "web.card.browser_viewport": "browser · viewport",
@@ -4973,6 +4982,9 @@ var init_ru = __esm(() => {
4973
4982
  "repl.model": "Модель:",
4974
4983
  "repl.provider": "Провайдер:",
4975
4984
  "repl.context": "Контекст:",
4985
+ "repl.ctx_part_system": "система",
4986
+ "repl.ctx_part_reserve": "резерв",
4987
+ "repl.ctx_part_history": "история",
4976
4988
  "repl.sysprompt_label": "Системный промпт:",
4977
4989
  "repl.sysprompt_size": "{used} / {budget} токенов",
4978
4990
  "repl.sysprompt_desc": "Размер системного промпта и блоки (текст — через --full)",
@@ -5484,6 +5496,8 @@ var init_ru = __esm(() => {
5484
5496
  "web.sessbar.prev": "предыдущая сессия",
5485
5497
  "web.sessbar.next": "следующая сессия",
5486
5498
  "web.conn.reconnecting": "переподключение…",
5499
+ "web.i18n.stale": "переводы обновлены — перезапустите сервер",
5500
+ "web.i18n.reload": "обновить страницу",
5487
5501
  "web.sidebar.plan": "план",
5488
5502
  "web.sidebar.session": "сессия",
5489
5503
  "web.sidebar.usage": "использование",
@@ -5550,6 +5564,11 @@ var init_ru = __esm(() => {
5550
5564
  "web.usage.cache": "кеш",
5551
5565
  "web.usage.cost": "стоимость",
5552
5566
  "web.usage.compactions": "сжатия",
5567
+ "web.usage.part_system": "инструкции",
5568
+ "web.usage.part_tools": "инструменты",
5569
+ "web.usage.part_history": "диалог",
5570
+ "web.usage.part_reserve": "резерв ответа",
5571
+ "web.usage.part_free": "свободно",
5553
5572
  "web.usage.quality": "качество",
5554
5573
  "web.usage.probe": "проб",
5555
5574
  "web.usage.probe_loaded": "загружено {actual}",
@@ -5905,13 +5924,12 @@ var init_ru = __esm(() => {
5905
5924
  "web.code.copy_failed": "не скопировано",
5906
5925
  "web.msg.copy": "копировать",
5907
5926
  "web.msg.copy_md": "копировать как markdown",
5908
- "web.card.open": "открыть ↗",
5909
- "web.card.download": "скачать ⤓",
5910
- "web.card.copy_path": "путь",
5911
5927
  "web.card.open_title": "Предпросмотр во вкладке браузера",
5912
- "web.card.download_title": "Скачать файл",
5913
5928
  "web.card.copy_title": "Скопировать путь в рабочей папке",
5914
5929
  "web.card.drawio_hint": "Диаграмма draw.io — это XML: предпросмотр покажет код, схему смотри в draw.io / VS Code.",
5930
+ "web.card.files_one": "{count} файл",
5931
+ "web.card.files_few": "{count} файла",
5932
+ "web.card.files_many": "{count} файлов",
5915
5933
  "web.card.screenshot": "снимок экрана",
5916
5934
  "web.card.attached_image": "прикреплённое изображение",
5917
5935
  "web.card.browser_viewport": "браузер · область просмотра",
@@ -11095,7 +11113,7 @@ function buildSystemInfo(config, baseDir, profileCompressed) {
11095
11113
  if (config.autoPlan) {
11096
11114
  lines.push(`PLAN (MANDATORY): file creation, package installs, or multi-action tasks MUST create a "plan" BEFORE starting.`);
11097
11115
  }
11098
- lines.push(`Runtime verification: when a bug is described in terms of runtime behavior`, `(what happens when the app runs), reproduce it —`, `run the app / build / browser tool — confirm the fix against actual runtime output`, `before calling "plan_step step=N status=done".`, `Check existing servers first (process_list/process_log): a stale dev server`, `runs the old pipeline and keeps failing after the fix.`, `Restart/kill it (process_kill) and verify the exact endpoint/port reported —`, `a fresh port does NOT prove the user's error is gone.`);
11116
+ lines.push(`Runtime verification: when a bug is described in terms of runtime behavior`, `(what happens when the app runs), reproduce it —`, `run the app / build / browser tool — confirm the fix against actual runtime output`, `before calling "plan_step step=N status=done".`, `Check existing servers first (process_list/process_log): a stale dev server`, `runs the old pipeline and keeps failing after the fix.`, `Restart/kill it (process_kill) and verify the exact endpoint/port reported —`, `a fresh port does NOT prove the user's error is gone.`, `Verification "mma web" servers run isolated (MMA_CONFIG_DIR=<empty tmp dir>) —`, `never verify against the user's live server/sessions/state.`);
11099
11117
  const sec = config.security;
11100
11118
  if (sec?.enabled) {
11101
11119
  const secLines = [
@@ -11908,6 +11926,18 @@ var init_walker = __esm(() => {
11908
11926
  // src/modules/indexer/cache.ts
11909
11927
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync18, mkdirSync as mkdirSync11, rmSync } from "fs";
11910
11928
  import { join as join20 } from "path";
11929
+ function isIndexedFile(v) {
11930
+ if (typeof v !== "object" || v === null)
11931
+ return false;
11932
+ const f = v;
11933
+ return typeof f.path === "string" && typeof f.language === "string" && Array.isArray(f.exports) && typeof f.size === "number";
11934
+ }
11935
+ function isIndexResult(v) {
11936
+ if (typeof v !== "object" || v === null)
11937
+ return false;
11938
+ const r = v;
11939
+ return Array.isArray(r.files) && r.files.every(isIndexedFile) && typeof r.totalSize === "number";
11940
+ }
11911
11941
 
11912
11942
  class IndexCache {
11913
11943
  cachePath;
@@ -11927,7 +11957,7 @@ class IndexCache {
11927
11957
  return null;
11928
11958
  try {
11929
11959
  const parsed = JSON.parse(readFileSync14(this.cachePath, "utf-8"));
11930
- if (parsed.version !== CACHE_VERSION || !parsed.result)
11960
+ if (parsed.version !== CACHE_VERSION || !isIndexResult(parsed.result))
11931
11961
  return null;
11932
11962
  if (this.root !== undefined && parsed.root !== this.root)
11933
11963
  return null;
@@ -14394,6 +14424,40 @@ var init_write_file = __esm(() => {
14394
14424
  // src/tools/edit-file.ts
14395
14425
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync13 } from "fs";
14396
14426
  import { resolve as resolve6 } from "path";
14427
+ function spliceNormalized(content, oldStr, newStr) {
14428
+ const normOld = oldStr.replace(/\r\n/g, `
14429
+ `);
14430
+ const norm = [];
14431
+ const map = [];
14432
+ for (let i = 0;i < content.length; i++) {
14433
+ if (content[i] === "\r" && content[i + 1] === `
14434
+ `) {
14435
+ norm.push(`
14436
+ `);
14437
+ map.push(i);
14438
+ i++;
14439
+ } else {
14440
+ norm.push(content[i]);
14441
+ map.push(i);
14442
+ }
14443
+ }
14444
+ const normContent = norm.join("");
14445
+ const idx = normContent.indexOf(normOld);
14446
+ if (idx === -1 || normOld.length === 0)
14447
+ return null;
14448
+ const startOrig = map[idx];
14449
+ const endNorm = idx + normOld.length;
14450
+ const endOrig = endNorm < map.length ? map[endNorm] : content.length;
14451
+ let insert = newStr;
14452
+ if (content.includes(`\r
14453
+ `) && !newStr.includes(`\r
14454
+ `) && newStr.includes(`
14455
+ `)) {
14456
+ insert = newStr.replace(/\n/g, `\r
14457
+ `);
14458
+ }
14459
+ return content.slice(0, startOrig) + insert + content.slice(endOrig);
14460
+ }
14397
14461
  var editFileTool;
14398
14462
  var init_edit_file = __esm(() => {
14399
14463
  init_i18n();
@@ -14478,13 +14542,19 @@ var init_edit_file = __esm(() => {
14478
14542
  output: t("file.read_failed", { path, error: errMsg(err) })
14479
14543
  };
14480
14544
  }
14481
- if (!content.includes(oldStr)) {
14482
- return {
14483
- success: false,
14484
- output: t("file.string_not_found", { str: oldStr, path })
14485
- };
14545
+ let updated;
14546
+ if (content.includes(oldStr)) {
14547
+ updated = content.replace(oldStr, () => newStr);
14548
+ } else {
14549
+ const spliced = spliceNormalized(content, oldStr, newStr);
14550
+ if (!spliced) {
14551
+ return {
14552
+ success: false,
14553
+ output: t("file.string_not_found", { str: oldStr, path })
14554
+ };
14555
+ }
14556
+ updated = spliced;
14486
14557
  }
14487
- const updated = content.replace(oldStr, () => newStr);
14488
14558
  if (securityConfig?.enabled && securityConfig?.contentScan?.enabled) {
14489
14559
  const scanResult = scanContent(updated, path, securityConfig.contentScan);
14490
14560
  if (!scanResult.allowed) {
@@ -21450,6 +21520,27 @@ function queueToolImages(sink, images) {
21450
21520
  }
21451
21521
 
21452
21522
  // src/core/agent/tool-batch.ts
21523
+ function diagnoseRepeatedFailures(tool, headlines) {
21524
+ const genericCause = "Repeated failures suggest a systemic problem (config, environment, or a broken tool), not a one-off";
21525
+ const genericSolution = "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool";
21526
+ const modelMiss = tool === "bash" ? [/did not match any test files?/i, /no such file/i, /cannot find (the )?path/i] : [];
21527
+ if (headlines.length > 0 && headlines.every((h) => /^\(exit code \d+\)$/.test(h.trim()) || modelMiss.some((re) => re.test(h)))) {
21528
+ return {
21529
+ skip: true,
21530
+ cause: "Model named a wrong target (path/filter); the tool itself works",
21531
+ solution: "Re-check the target path or filter instead of blaming the tool"
21532
+ };
21533
+ }
21534
+ if (tool === "edit_file" && headlines.some((h) => /string not found|не найдена/i.test(h))) {
21535
+ return {
21536
+ skip: false,
21537
+ cause: "Anchor mismatch between the model anchor and file content — often CRLF vs LF line endings or a stale read, not a broken tool",
21538
+ solution: "Re-read the exact block with read_file and retry with a short unique single-line anchor"
21539
+ };
21540
+ }
21541
+ return { skip: false, cause: genericCause, solution: genericSolution };
21542
+ }
21543
+
21453
21544
  class ToolBatchExecutor {
21454
21545
  deps;
21455
21546
  constructor(deps) {
@@ -21609,15 +21700,22 @@ ${result.diff}
21609
21700
  this.deps.compactionService.compactAfterTool(state);
21610
21701
  if (!result.success && !result.blocked && !infoTools.has(call.name)) {
21611
21702
  const key = call.name;
21612
- const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "" };
21703
+ const prev = state.toolFailureCounts.get(key) ?? { count: 0, error: "", samples: [] };
21613
21704
  prev.count++;
21614
- prev.error = String(result.output ?? "").slice(0, 200);
21705
+ const errText = String(result.output ?? "");
21706
+ prev.error = errText.slice(0, 200);
21707
+ const headline = errText.split(`
21708
+ `)[0].slice(0, 160).trim();
21709
+ if (headline && !prev.samples.includes(headline) && prev.samples.length < 3) {
21710
+ prev.samples.push(headline);
21711
+ }
21615
21712
  state.toolFailureCounts.set(key, prev);
21616
21713
  if (prev.count >= MIN_REPEATED_TOOL_FAILURES && !state.memoryRuleRecorded.has(key)) {
21617
21714
  state.memoryRuleRecorded.add(key);
21618
21715
  const memStore = this.deps.memoryStore;
21619
- if (memStore) {
21620
- memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.error})`, `Repeated ${key} failures suggest a systemic problem (config, environment, or a broken tool), not a one-off`, "Check the error message, verify the tool's dependencies are installed/configured, and consider a different tool");
21716
+ const diagnosis = diagnoseRepeatedFailures(key, prev.samples);
21717
+ if (memStore && !diagnosis.skip) {
21718
+ memStore.appendRule("errors", `Tool ${key} failed ${prev.count}x (${prev.samples.join(" | ") || prev.error})`, diagnosis.cause, diagnosis.solution);
21621
21719
  logger3.diagnostic("warn", `Recorded repeated ${key} failures to memory (${prev.count}x)`);
21622
21720
  }
21623
21721
  }
@@ -22449,6 +22547,7 @@ var init_provider = __esm(() => {
22449
22547
 
22450
22548
  // src/modules/critic/evidence.ts
22451
22549
  import { readFileSync as readFileSync26 } from "fs";
22550
+ import { execFileSync } from "node:child_process";
22452
22551
  import { resolve as resolve15 } from "path";
22453
22552
  function isWorkMutation(name) {
22454
22553
  return MUTATING_TOOLS.has(name) || name === "bash";
@@ -22466,6 +22565,10 @@ function buildEvidencePack(input) {
22466
22565
  if (cmd)
22467
22566
  commands.push(cmd);
22468
22567
  }
22568
+ if (input.baseDir) {
22569
+ for (const f of gitChangedFiles(input.baseDir))
22570
+ changedFiles.add(f);
22571
+ }
22469
22572
  return {
22470
22573
  task: input.task,
22471
22574
  planSummary: input.planSummary,
@@ -22490,6 +22593,45 @@ function changedFileOf(ev) {
22490
22593
  return;
22491
22594
  return typeof ev.args?.path === "string" ? ev.args.path : undefined;
22492
22595
  }
22596
+ function parsePorcelainLine(line) {
22597
+ if (line.length < 4)
22598
+ return null;
22599
+ let p = line.slice(3).trim();
22600
+ const arrow = p.indexOf(" -> ");
22601
+ if (arrow !== -1)
22602
+ p = p.slice(arrow + 4).trim();
22603
+ if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) {
22604
+ try {
22605
+ p = JSON.parse(p);
22606
+ } catch {
22607
+ p = p.slice(1, -1);
22608
+ }
22609
+ }
22610
+ return p || null;
22611
+ }
22612
+ function gitChangedFiles(baseDir) {
22613
+ let out;
22614
+ try {
22615
+ out = execFileSync("git", ["status", "--porcelain"], {
22616
+ cwd: resolve15(baseDir),
22617
+ encoding: "utf-8",
22618
+ stdio: ["ignore", "pipe", "ignore"],
22619
+ timeout: 1e4
22620
+ });
22621
+ } catch {
22622
+ return [];
22623
+ }
22624
+ const files = [];
22625
+ for (const line of out.split(`
22626
+ `)) {
22627
+ const p = parsePorcelainLine(line);
22628
+ if (p)
22629
+ files.push(p);
22630
+ if (files.length >= MAX_GIT_FILES)
22631
+ break;
22632
+ }
22633
+ return files;
22634
+ }
22493
22635
  function commandOf(ev) {
22494
22636
  if (ev.tool !== "bash" || typeof ev.args?.command !== "string")
22495
22637
  return;
@@ -22513,7 +22655,7 @@ function readExcerpts(baseDir, paths) {
22513
22655
  }
22514
22656
  return out;
22515
22657
  }
22516
- var MUTATING_TOOLS, MAX_DIGEST_LINES = 40, MAX_DIGEST_OUTPUT_CHARS = 300, MAX_EXCERPT_FILES = 5, MAX_EXCERPT_CHARS = 1200, MAX_ANSWER_CHARS = 2000;
22658
+ var MUTATING_TOOLS, MAX_DIGEST_LINES = 40, MAX_DIGEST_OUTPUT_CHARS = 300, MAX_EXCERPT_FILES = 5, MAX_EXCERPT_CHARS = 1200, MAX_ANSWER_CHARS = 2000, MAX_GIT_FILES = 20;
22517
22659
  var init_evidence = __esm(() => {
22518
22660
  init_tool_output();
22519
22661
  init_context_log();
@@ -23650,7 +23792,8 @@ class Agent {
23650
23792
  costBreakdown: this.costTracker.breakdown(),
23651
23793
  cache: cacheStats,
23652
23794
  compactionCount: contextManager.getCompactionCount(),
23653
- contextQuality: contextManager.getQuality()
23795
+ contextQuality: contextManager.getQuality(),
23796
+ contextBreakdown: contextManager.getBreakdown?.()
23654
23797
  };
23655
23798
  }
23656
23799
  return {
@@ -23668,6 +23811,7 @@ class Agent {
23668
23811
  cache: cacheStats,
23669
23812
  compactionCount: contextManager.getCompactionCount(),
23670
23813
  contextQuality: contextManager.getQuality(),
23814
+ contextBreakdown: contextManager.getBreakdown?.(),
23671
23815
  llmDurationMs: state.totalLlmDuration
23672
23816
  };
23673
23817
  }
@@ -24280,6 +24424,28 @@ ${lines.join(`
24280
24424
  summary: this.compactedBlock ?? ""
24281
24425
  };
24282
24426
  }
24427
+ systemTokens() {
24428
+ const sys = this.messages.find((m) => m.role === "system");
24429
+ return sys ? this.estimateMessageTokensCached(sys) : 0;
24430
+ }
24431
+ getBreakdown() {
24432
+ const used = this.getEstimatedTokens();
24433
+ const system = this.systemTokens();
24434
+ const tools = this.toolTokens;
24435
+ const history = Math.max(0, used - system - tools);
24436
+ const b = this.budget;
24437
+ return {
24438
+ system,
24439
+ tools,
24440
+ history,
24441
+ used,
24442
+ systemBudget: b.systemPrompt,
24443
+ historyBudget: b.history,
24444
+ reserve: b.responseReserve,
24445
+ free: Math.max(0, this.contextWindow - used - b.responseReserve),
24446
+ window: this.contextWindow
24447
+ };
24448
+ }
24283
24449
  getSnapshot() {
24284
24450
  return {
24285
24451
  window: this.contextWindow,
@@ -24289,7 +24455,8 @@ ${lines.join(`
24289
24455
  messageCount: this.messages.length,
24290
24456
  quality: this.getQuality(),
24291
24457
  compactionCount: this.compactionCount,
24292
- iterationsSinceCompaction: this.iterationsSinceCompaction
24458
+ iterationsSinceCompaction: this.iterationsSinceCompaction,
24459
+ breakdown: this.getBreakdown()
24293
24460
  };
24294
24461
  }
24295
24462
  updateSystemPrompt(content) {
@@ -24494,7 +24661,33 @@ var init_factual = __esm(() => {
24494
24661
  if (!fp.includes("/") && !fp.includes("\\")) {
24495
24662
  return this.bareNameExists(fp);
24496
24663
  }
24497
- return false;
24664
+ return this.suffixExists(fp);
24665
+ }
24666
+ suffixExists(fp) {
24667
+ const norm = fp.replace(/\\/g, "/");
24668
+ const tail = `/${norm}`;
24669
+ const base = norm.split("/").pop() ?? norm;
24670
+ const fwd = (p) => p.replace(/\\/g, "/");
24671
+ for (const known of this.knownFiles) {
24672
+ if (fwd(known).endsWith(tail))
24673
+ return true;
24674
+ }
24675
+ const checkIndex = () => {
24676
+ for (const full of this.getNameIndex().get(base) ?? []) {
24677
+ if (fwd(full).endsWith(tail))
24678
+ return true;
24679
+ }
24680
+ return false;
24681
+ };
24682
+ if (checkIndex())
24683
+ return true;
24684
+ const now = Date.now();
24685
+ if (now - this.lastRescan < FactualCheck.RESCAN_COOLDOWN_MS)
24686
+ return false;
24687
+ this.lastRescan = now;
24688
+ this.nameCache = null;
24689
+ this.nameCacheTime = 0;
24690
+ return checkIndex();
24498
24691
  }
24499
24692
  dotfileVariants(fp) {
24500
24693
  const variants = [fp];
@@ -28741,6 +28934,7 @@ function createBrowserTool() {
28741
28934
  "Do NOT use it for search queries: search engines (Google, Yandex, Bing) block headless browsers with CAPTCHA/429/redirect pages — use the web_search tool for searching.",
28742
28935
  "Each snapshot returns: a numbered list of interactive elements, the visible page text (Content section), browser console messages, and network errors.",
28743
28936
  "Use the Content section to understand what the page says; use element numbers as targets for click/type.",
28937
+ "Without vision capabilities screenshots return only a text note — verify visuals through snapshot text, and for local web UI prefer DOM/API assertions (fetch /api/* endpoints, grep built assets) over driving the browser.",
28744
28938
  "Actions: open (url), click (target), type (target, text), scroll (direction), back, forward, screenshot, snapshot, close, wait (ms)."
28745
28939
  ].join(" "),
28746
28940
  parameters: {
@@ -32383,11 +32577,26 @@ var init_plan_tool = __esm(() => {
32383
32577
  });
32384
32578
 
32385
32579
  // src/modules/execution/module.ts
32386
- import { existsSync as existsSync51, readFileSync as readFileSync37 } from "fs";
32580
+ import { existsSync as existsSync51, readFileSync as readFileSync37, statSync as statSync11 } from "fs";
32387
32581
  import { resolve as resolve22 } from "path";
32388
32582
  function planPathToken(p) {
32389
32583
  return toForwardSlash(p).toLowerCase();
32390
32584
  }
32585
+ function provablyStale(resolved, sinceMs) {
32586
+ if (Number.isNaN(sinceMs))
32587
+ return false;
32588
+ for (const r of resolved) {
32589
+ if (!r)
32590
+ return false;
32591
+ try {
32592
+ if (statSync11(r).mtimeMs > sinceMs)
32593
+ return false;
32594
+ } catch {
32595
+ return false;
32596
+ }
32597
+ }
32598
+ return true;
32599
+ }
32391
32600
 
32392
32601
  class ExecutionModule {
32393
32602
  name = "execution";
@@ -32631,6 +32840,8 @@ class ExecutionModule {
32631
32840
  });
32632
32841
  if (empty)
32633
32842
  continue;
32843
+ if (provablyStale(resolved, Date.parse(plan.createdAt)))
32844
+ continue;
32634
32845
  }
32635
32846
  step.status = "done";
32636
32847
  step.note = `auto-closed by final audit: ${step.kind === "delete" ? "all named files removed" : "all named files exist with content"} (${tokens.join(", ")})`;
@@ -32858,6 +33069,11 @@ class ExecutionModule {
32858
33069
  const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
32859
33070
  if (!satisfied)
32860
33071
  return;
33072
+ if (step.kind !== "delete") {
33073
+ const plan = this.tracker?.getPlan();
33074
+ if (plan && provablyStale(resolved, Date.parse(plan.createdAt)))
33075
+ return;
33076
+ }
32861
33077
  if (step.kind !== "delete") {
32862
33078
  const emptyFiles = [];
32863
33079
  for (const r of resolved) {
@@ -44557,7 +44773,7 @@ var init_loader3 = __esm(() => {
44557
44773
  });
44558
44774
 
44559
44775
  // src/modules/certification/fact-checker.ts
44560
- import { existsSync as existsSync56, readFileSync as readFileSync40, statSync as statSync11 } from "fs";
44776
+ import { existsSync as existsSync56, readFileSync as readFileSync40, statSync as statSync12 } from "fs";
44561
44777
  import { join as join50 } from "path";
44562
44778
  function checkSandbox(sandboxDir, checks, exitCode, output) {
44563
44779
  const failures = [];
@@ -44603,14 +44819,14 @@ function runCheck2(sandboxDir, check, exitCode, output) {
44603
44819
  }
44604
44820
  function isFile(p) {
44605
44821
  try {
44606
- return existsSync56(p) && statSync11(p).isFile();
44822
+ return existsSync56(p) && statSync12(p).isFile();
44607
44823
  } catch {
44608
44824
  return false;
44609
44825
  }
44610
44826
  }
44611
44827
  function isDir(p) {
44612
44828
  try {
44613
- return existsSync56(p) && statSync11(p).isDirectory();
44829
+ return existsSync56(p) && statSync12(p).isDirectory();
44614
44830
  } catch {
44615
44831
  return false;
44616
44832
  }
@@ -49232,7 +49448,7 @@ import { homedir as homedir8 } from "os";
49232
49448
  // src/cli/completer.ts
49233
49449
  init_registry2();
49234
49450
  init_i18n();
49235
- import { readdirSync as readdirSync19, statSync as statSync12 } from "fs";
49451
+ import { readdirSync as readdirSync19, statSync as statSync13 } from "fs";
49236
49452
  import { join as join54 } from "path";
49237
49453
  function buildSlashCompletions(commands) {
49238
49454
  return Array.from(commands.entries()).map(([name, cmd]) => ({
@@ -49306,7 +49522,7 @@ class FilePathProvider {
49306
49522
  const matches = [];
49307
49523
  for (const entry of entries) {
49308
49524
  const entryPath = join54(fullPath, entry);
49309
- const stat2 = statSync12(entryPath);
49525
+ const stat2 = statSync13(entryPath);
49310
49526
  const completion = prefix + entry + (stat2.isDirectory() ? "/" : "");
49311
49527
  if (completion.startsWith(ctx.partial)) {
49312
49528
  matches.push(completion);
@@ -51310,7 +51526,7 @@ ${divider()}`);
51310
51526
  row(t("repl.model"), pc.white(this.config.model));
51311
51527
  const endpoint = activeProviderEndpoint(this.config.provider);
51312
51528
  row(t("repl.provider"), `${endpoint.type} → ${pc.dim(endpoint.baseUrl)}`);
51313
- row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(sys:${sysBudget} res:${resBudget} hist:${histBudget})`)}`);
51529
+ row(t("repl.context"), `${pc.white(String(ctx))} ${pc.dim(`(${t("repl.ctx_part_system")}:${sysBudget} ${t("repl.ctx_part_reserve")}:${resBudget} ${t("repl.ctx_part_history")}:${histBudget})`)}`);
51314
51530
  const si = this.agent.getSystemPromptInfo();
51315
51531
  row(t("repl.sysprompt_label"), pc.dim(t("repl.sysprompt_size", { used: si.tokenCount, budget: sysBudget })));
51316
51532
  if (this.skillsModule) {
@@ -51534,7 +51750,7 @@ init_output();
51534
51750
  init_i18n();
51535
51751
  init_colors();
51536
51752
  import { existsSync as existsSync63 } from "node:fs";
51537
- import { join as join60 } from "node:path";
51753
+ import { join as join61 } from "node:path";
51538
51754
 
51539
51755
  // src/modules/webui/module.ts
51540
51756
  init_config2();
@@ -52750,10 +52966,28 @@ function handleModelsGet(res, s) {
52750
52966
 
52751
52967
  // src/modules/webui/i18n-api.ts
52752
52968
  init_i18n();
52969
+
52970
+ // src/modules/webui/i18n-stale.ts
52971
+ import { statSync as statSync14 } from "fs";
52972
+ import { join as join60 } from "path";
52973
+ var serverStartedAt = Date.now();
52974
+ var dictFiles = ["en.json", "ru.json"].map((f) => join60(import.meta.dirname, "..", "..", "i18n", f));
52975
+ function isI18nStale() {
52976
+ for (const p of dictFiles) {
52977
+ try {
52978
+ if (statSync14(p).mtimeMs > serverStartedAt)
52979
+ return true;
52980
+ } catch {}
52981
+ }
52982
+ return false;
52983
+ }
52984
+
52985
+ // src/modules/webui/i18n-api.ts
52753
52986
  function handleI18nGet(res, s) {
52754
52987
  sendJson(res, 200, {
52755
52988
  locale: s.config?.locale ?? "en",
52756
- dicts: getFlatDicts()
52989
+ dicts: getFlatDicts(),
52990
+ i18nStale: isI18nStale()
52757
52991
  });
52758
52992
  }
52759
52993
 
@@ -54574,6 +54808,8 @@ function captureUsage(result, fallbackLimit) {
54574
54808
  }
54575
54809
  if (typeof result.contextQuality === "number")
54576
54810
  usage.contextQuality = result.contextQuality;
54811
+ if (result.contextBreakdown)
54812
+ usage.contextBreakdown = result.contextBreakdown;
54577
54813
  return usage;
54578
54814
  }
54579
54815
  function planEventFor(toolName, execModule) {
@@ -54596,10 +54832,12 @@ function buildHostState(deps, busy) {
54596
54832
  compactionCount: deps.usage?.compactionCount,
54597
54833
  cacheHitRate: deps.usage?.cacheHitRate,
54598
54834
  contextQuality: deps.usage?.contextQuality,
54835
+ contextBreakdown: deps.usage?.contextBreakdown,
54599
54836
  contextProbe: deps.contextProbe,
54600
54837
  workspace: deps.workspace,
54601
54838
  instructions: deps.instructions,
54602
54839
  projectMap: deps.projectMap ?? undefined,
54840
+ i18nStale: isI18nStale(),
54603
54841
  plan: planSnapshotFromModule(adaptActivePlan(deps.execModule?.getActivePlan()))
54604
54842
  };
54605
54843
  }
@@ -54801,7 +55039,7 @@ async function runWeb(opts) {
54801
55039
  process.env[MMA_ENV.WEBUI_OPEN] = "0";
54802
55040
  const mmaDir = globalMmaDir();
54803
55041
  const { hasDomainFiles: hasDomainFiles2 } = await Promise.resolve().then(() => (init_domains(), exports_domains));
54804
- const virgin = !existsSync63(join60(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55042
+ const virgin = !existsSync63(join61(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
54805
55043
  const {
54806
55044
  agent,
54807
55045
  config,
@@ -54923,7 +55161,7 @@ init_config2();
54923
55161
  init_i18n();
54924
55162
  init_colors();
54925
55163
  import { existsSync as existsSync64, readFileSync as readFileSync45 } from "fs";
54926
- import { join as join62, dirname as dirname30 } from "path";
55164
+ import { join as join63, dirname as dirname30 } from "path";
54927
55165
  import { fileURLToPath as fileURLToPath7 } from "url";
54928
55166
 
54929
55167
  // src/modules/updater/index.ts
@@ -55045,7 +55283,7 @@ init_i18n();
55045
55283
  init_utils();
55046
55284
  init_paths();
55047
55285
  import { appendFileSync as appendFileSync8, mkdirSync as mkdirSync30 } from "fs";
55048
- import { join as join61 } from "path";
55286
+ import { join as join62 } from "path";
55049
55287
  import { homedir as homedir9 } from "os";
55050
55288
  var CRASH_LOG_DIR = globalLogsDir();
55051
55289
  var CRASH_LOG_FILE = "crash.jsonl";
@@ -55063,7 +55301,7 @@ function formatCrashEntry(type2, err) {
55063
55301
  function writeCrashEntry(dir, entry) {
55064
55302
  try {
55065
55303
  mkdirSync30(dir, { recursive: true });
55066
- appendFileSync8(join61(dir, CRASH_LOG_FILE), `${JSON.stringify(entry)}
55304
+ appendFileSync8(join62(dir, CRASH_LOG_FILE), `${JSON.stringify(entry)}
55067
55305
  `, "utf-8");
55068
55306
  } catch {}
55069
55307
  }
@@ -55134,7 +55372,7 @@ function resolveEarlyLocale() {
55134
55372
  if (env3)
55135
55373
  return env3;
55136
55374
  const dir = globalMmaDir();
55137
- for (const p of [join62(dir, "config", "core.json"), join62(dir, "config.json")]) {
55375
+ for (const p of [join63(dir, "config", "core.json"), join63(dir, "config.json")]) {
55138
55376
  try {
55139
55377
  const parsed = JSON.parse(readFileSync45(p, "utf-8"));
55140
55378
  if (typeof parsed.locale === "string")
@@ -55234,7 +55472,7 @@ async function main() {
55234
55472
  const prompt = program2.args.join(" ");
55235
55473
  const mmaDir = globalMmaDir();
55236
55474
  const { hasDomainFiles: hasDomainFiles2 } = await Promise.resolve().then(() => (init_domains(), exports_domains));
55237
- const virgin = !existsSync64(join62(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55475
+ const virgin = !existsSync64(join63(mmaDir, "config.json")) && !hasDomainFiles2(mmaDir);
55238
55476
  const { agent, config, baseDir, legacyDetected, contextProbe } = await bootstrap(undefined, projectDir, noAgentsMd, exitOnComplete, reasoningLevel, contextLog);
55239
55477
  if (verboseOpt)
55240
55478
  config.ui.verbosity = "verbose";
@@ -55325,7 +55563,7 @@ async function main() {
55325
55563
  if (onboardingStart === "init")
55326
55564
  process.env[MMA_ENV.START_INIT] = "1";
55327
55565
  const mmaDir = globalMmaDir();
55328
- const legacyConfigPath = join62(mmaDir, "config.json");
55566
+ const legacyConfigPath = join63(mmaDir, "config.json");
55329
55567
  let hasAnyConfig = existsSync64(legacyConfigPath);
55330
55568
  if (!hasAnyConfig) {
55331
55569
  try {
@@ -55333,7 +55571,7 @@ async function main() {
55333
55571
  hasAnyConfig = hasDomainFiles2(mmaDir);
55334
55572
  } catch {}
55335
55573
  }
55336
- const projectConfigPath = projectDir ? join62(projectDir, ".mmrc") : join62(process.cwd(), ".mmrc");
55574
+ const projectConfigPath = projectDir ? join63(projectDir, ".mmrc") : join63(process.cwd(), ".mmrc");
55337
55575
  if (!hasAnyConfig && !exitOnComplete && readEnv("POST_SETUP") !== "1") {
55338
55576
  getDefaultChannel().writeLine(`
55339
55577
  ${t("cli.first_run")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "2.13.1",
3
+ "version": "2.15.0",
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": {