micro-models-agent 0.63.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +7 -4
  41. package/dist/i18n/ru.json +7 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1606 -800
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/driver.js +46 -4
  59. package/dist/modules/certification/cli.js +85 -42
  60. package/dist/modules/certification/loader.js +15 -1
  61. package/dist/modules/certification/manifest.js +126 -15
  62. package/dist/modules/certification/runner.js +4 -26
  63. package/dist/modules/certification/scenarios.js +184 -5
  64. package/dist/modules/certification/syntax-scenarios.js +51 -0
  65. package/dist/modules/context/chunk-query.js +25 -5
  66. package/dist/modules/context/fact-extractor.js +6 -2
  67. package/dist/modules/context/manager.js +23 -7
  68. package/dist/modules/execution/audit-runners.js +7 -1
  69. package/dist/modules/execution/auditor.js +3 -3
  70. package/dist/modules/execution/execution-plugin.js +22 -15
  71. package/dist/modules/execution/input-from.js +46 -0
  72. package/dist/modules/execution/module.js +107 -18
  73. package/dist/modules/execution/moe-executor.js +166 -54
  74. package/dist/modules/execution/plan-actions.js +524 -0
  75. package/dist/modules/execution/plan-steps.js +23 -0
  76. package/dist/modules/execution/plan-store.js +15 -3
  77. package/dist/modules/execution/plan-tool.js +6 -488
  78. package/dist/modules/execution/plan-validator.js +24 -0
  79. package/dist/modules/execution/stuck-detector.js +3 -18
  80. package/dist/modules/execution/tracker.js +14 -5
  81. package/dist/modules/execution/transient-error.js +30 -0
  82. package/dist/modules/execution/verifier.js +94 -7
  83. package/dist/modules/execution/windows-commands.js +11 -0
  84. package/dist/modules/hallucination/confidence.js +36 -23
  85. package/dist/modules/hallucination/consistency.js +3 -0
  86. package/dist/modules/hallucination/detector.js +8 -3
  87. package/dist/modules/hallucination/factual.js +26 -7
  88. package/dist/modules/hallucination/llm-judge.js +12 -2
  89. package/dist/modules/indexer/map-command.js +35 -0
  90. package/dist/modules/indexer/map-select.js +87 -0
  91. package/dist/modules/indexer/module.js +34 -22
  92. package/dist/modules/indexer/symbols.js +189 -0
  93. package/dist/modules/indexer/walker.js +96 -42
  94. package/dist/modules/lsp/check-tool.js +2 -1
  95. package/dist/modules/lsp/client.js +49 -32
  96. package/dist/modules/lsp/config.js +55 -2
  97. package/dist/modules/lsp/module.js +38 -5
  98. package/dist/modules/lsp/probe.js +4 -3
  99. package/dist/modules/lsp/project-root.js +41 -1
  100. package/dist/modules/lsp/startup-check.js +12 -4
  101. package/dist/modules/mcp/client.js +153 -104
  102. package/dist/modules/mcp/module.js +165 -41
  103. package/dist/modules/memory/module.js +4 -3
  104. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  105. package/dist/modules/plugins/manager.js +47 -84
  106. package/dist/modules/pricing/index.js +17 -7
  107. package/dist/modules/pricing/prices.js +30 -12
  108. package/dist/modules/processes/index.js +1 -0
  109. package/dist/modules/processes/kill-tree.js +56 -0
  110. package/dist/modules/processes/registry.js +2 -54
  111. package/dist/modules/providers/cache.js +23 -0
  112. package/dist/modules/providers/factory.js +28 -0
  113. package/dist/modules/providers/fallback.js +7 -5
  114. package/dist/modules/providers/health.js +2 -1
  115. package/dist/modules/providers/index.js +1 -0
  116. package/dist/modules/providers/manager.js +17 -2
  117. package/dist/modules/providers/presets.js +79 -6
  118. package/dist/modules/reasoning/policy.js +40 -0
  119. package/dist/modules/reasoning/probe.js +111 -0
  120. package/dist/modules/security/audit-notifier.js +42 -27
  121. package/dist/modules/security/command-validator.js +25 -20
  122. package/dist/modules/security/encryption.js +6 -12
  123. package/dist/modules/security/network-validator.js +76 -5
  124. package/dist/modules/security/path-validator.js +77 -34
  125. package/dist/modules/security/rate-limiter.js +11 -0
  126. package/dist/modules/security/security-policies.js +1 -1
  127. package/dist/modules/security/session-encryption.js +13 -2
  128. package/dist/modules/security/session-isolation.js +2 -9
  129. package/dist/modules/session/manager.js +11 -0
  130. package/dist/modules/session/module.js +11 -3
  131. package/dist/modules/session/store.js +41 -5
  132. package/dist/modules/skills/loader.js +7 -1
  133. package/dist/modules/skills/module.js +2 -1
  134. package/dist/modules/updater/changelog-reader.js +94 -0
  135. package/dist/modules/updater/dev-detect.js +17 -0
  136. package/dist/modules/updater/index.js +1 -0
  137. package/dist/modules/updater/module.js +14 -3
  138. package/dist/output/bus.js +32 -0
  139. package/dist/output/channel.js +233 -0
  140. package/dist/output/format.js +14 -0
  141. package/dist/output/index.js +7 -0
  142. package/dist/output/json-sink.js +22 -0
  143. package/dist/output/machine.js +8 -0
  144. package/dist/output/session-sink.js +27 -0
  145. package/dist/output/types.js +1 -0
  146. package/dist/tools/approve.js +6 -2
  147. package/dist/tools/attach-image.js +11 -11
  148. package/dist/tools/auto-fixer.js +198 -0
  149. package/dist/tools/bash.js +142 -89
  150. package/dist/tools/chunk-query.js +10 -6
  151. package/dist/tools/download-file.js +1 -1
  152. package/dist/tools/edit-file.js +20 -2
  153. package/dist/tools/executor.js +54 -9
  154. package/dist/tools/glob-tool.js +7 -0
  155. package/dist/tools/grep-tool.js +15 -1
  156. package/dist/tools/index.js +3 -1
  157. package/dist/tools/list-dir.js +3 -1
  158. package/dist/tools/load-skill.js +2 -1
  159. package/dist/tools/mcp-call.js +1 -1
  160. package/dist/tools/move-file.js +5 -4
  161. package/dist/tools/path-utils.js +7 -0
  162. package/dist/tools/pipeline-run.js +1 -1
  163. package/dist/tools/prompt-io.js +28 -0
  164. package/dist/tools/question.js +12 -12
  165. package/dist/tools/scope-request.js +91 -0
  166. package/dist/tools/session-info.js +44 -0
  167. package/dist/tools/set-thinking.js +71 -0
  168. package/dist/tools/subagent.js +50 -9
  169. package/dist/tools/syntax-validator.js +177 -0
  170. package/dist/tools/user-input.js +16 -9
  171. package/dist/tools/write-file.js +17 -1
  172. package/dist/ui/diff.js +10 -0
  173. package/dist/ui/line-editor.js +179 -26
  174. package/dist/ui/line-math.js +20 -3
  175. package/dist/ui/md-formatter.js +100 -10
  176. package/dist/ui/output.js +5 -4
  177. package/dist/ui/plan-view.js +2 -7
  178. package/dist/ui/renderer.js +89 -85
  179. package/dist/ui/spinner.js +14 -4
  180. package/dist/utils/error.js +4 -0
  181. package/dist/utils/index.js +4 -0
  182. package/dist/utils/retry.js +17 -0
  183. package/dist/utils/sleep.js +23 -0
  184. package/dist/utils/truncate.js +9 -0
  185. package/package.json +1 -1
@@ -5,11 +5,14 @@ import { DEFAULTS } from "./defaults";
5
5
  import { DEFAULT_SECURITY_CONFIG } from "./security";
6
6
  import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
7
7
  import { t } from "../i18n/index";
8
+ import { deepMerge, regexReplacer, regexReviver } from "./utils";
9
+ export { deepMerge };
8
10
  import { MigrationDetector } from "../migration/detect";
9
11
  import { BackupManager } from "../migration/backup";
10
12
  import { validateExpertConfig } from "./experts";
11
13
  import { ConfigEncryptor } from "../modules/security/encryption";
12
14
  import { loadDomainFiles, saveDomainFiles } from "./domains";
15
+ import { defaultOutputBus } from "../output";
13
16
  // ── Centralized path constants ──────────────────────────────────────────
14
17
  export const CONFIG_DIR = join(homedir(), ".mma");
15
18
  export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
@@ -42,50 +45,6 @@ function restoreDangerousPatterns(patterns, defaults) {
42
45
  return fallback[i] || fallback[0] || p;
43
46
  });
44
47
  }
45
- function deepMerge(target, source) {
46
- const result = { ...target };
47
- for (const key of Object.keys(source)) {
48
- const srcVal = source[key];
49
- const tgtVal = target[key];
50
- if (srcVal !== null &&
51
- srcVal !== undefined &&
52
- typeof srcVal === "object" &&
53
- !Array.isArray(srcVal) &&
54
- typeof tgtVal === "object" &&
55
- !Array.isArray(tgtVal)) {
56
- result[key] = deepMerge(tgtVal, srcVal);
57
- }
58
- else if (srcVal !== undefined) {
59
- result[key] = srcVal;
60
- }
61
- }
62
- return result;
63
- }
64
- /**
65
- * JSON replacer that serializes RegExp objects as {__regex, source, flags}
66
- * so they survive JSON.stringify/parse round-trips.
67
- */
68
- function regexReplacer(_key, value) {
69
- if (value instanceof RegExp) {
70
- return { __regex: true, source: value.source, flags: value.flags };
71
- }
72
- return value;
73
- }
74
- /**
75
- * JSON reviver that restores RegExp objects serialized by regexReplacer.
76
- */
77
- function regexReviver(_key, value) {
78
- if (value && typeof value === "object" && value.__regex === true) {
79
- const { source, flags } = value;
80
- try {
81
- return new RegExp(source, flags);
82
- }
83
- catch {
84
- return value;
85
- }
86
- }
87
- return value;
88
- }
89
48
  /**
90
49
  * Legacy npx `args` for LSP servers that predate the fixed invocation.
91
50
  */
@@ -117,50 +76,133 @@ function normalizeLspServerArgs(config) {
117
76
  }
118
77
  }
119
78
  function loadJSON(path) {
79
+ if (!existsSync(path))
80
+ return null;
120
81
  try {
121
- if (existsSync(path)) {
122
- return JSON.parse(readFileSync(path, "utf-8"), regexReviver);
123
- }
82
+ return JSON.parse(readFileSync(path, "utf-8"), regexReviver);
124
83
  }
125
84
  catch {
126
- /* ignore malformed files */
85
+ // Never silent: a typo'd config must not quietly wipe the user's settings.
86
+ defaultOutputBus.log("warn", "config", t("config.parse_failed", { path }));
127
87
  }
128
88
  return null;
129
89
  }
90
+ /**
91
+ * Apply MMA_* env overrides. Returns the overridden keys with their PRE-ENV
92
+ * values so `saveConfig` can avoid persisting session-scoped env values (C4).
93
+ */
130
94
  function applyEnvVars(config) {
131
95
  const env = process.env;
96
+ const overrides = [];
132
97
  const result = { ...config };
133
- if (env.MMA_MODEL)
98
+ const record = (key, envValue) => {
99
+ const original = result[key];
100
+ if (original !== envValue)
101
+ overrides.push({ key, envValue, originalValue: original });
102
+ };
103
+ if (env.MMA_MODEL) {
104
+ record("model", env.MMA_MODEL);
134
105
  result.model = env.MMA_MODEL;
135
- if (env.MMA_PROVIDER_TYPE)
106
+ }
107
+ const originalProvider = result.provider;
108
+ // normalizeEntries prefers entries[] and ignores the flat provider fields, so
109
+ // a flat-only env override is a silent no-op on multi-provider configs (the
110
+ // certification harness depends on these vars working). Resolve the active
111
+ // entry and patch it too.
112
+ const activeEntryIndex = (() => {
113
+ const entries = result.provider.entries;
114
+ if (!entries || entries.length === 0)
115
+ return -1;
116
+ const active = result.provider.active;
117
+ if (!active)
118
+ return 0;
119
+ const i = entries.findIndex((e) => e.label === active || e.type === active);
120
+ return i >= 0 ? i : 0;
121
+ })();
122
+ const patchActiveEntry = (patch) => {
123
+ if (activeEntryIndex < 0)
124
+ return;
125
+ const entries = result.provider.entries;
126
+ const next = [...entries];
127
+ next[activeEntryIndex] = { ...next[activeEntryIndex], ...patch };
128
+ result.provider = { ...result.provider, entries: next };
129
+ };
130
+ if (env.MMA_PROVIDER_TYPE) {
136
131
  result.provider = { ...result.provider, type: env.MMA_PROVIDER_TYPE };
137
- if (env.MMA_PROVIDER_BASEURL)
132
+ patchActiveEntry({ type: env.MMA_PROVIDER_TYPE });
133
+ }
134
+ if (env.MMA_PROVIDER_BASEURL) {
138
135
  result.provider = { ...result.provider, baseUrl: env.MMA_PROVIDER_BASEURL };
139
- if (env.MMA_PROVIDER_APIKEY)
136
+ patchActiveEntry({ baseUrl: env.MMA_PROVIDER_BASEURL });
137
+ }
138
+ if (env.MMA_PROVIDER_APIKEY) {
140
139
  result.provider = { ...result.provider, apiKey: env.MMA_PROVIDER_APIKEY };
141
- if (env.MMA_LOG_LEVEL)
140
+ patchActiveEntry({ apiKey: env.MMA_PROVIDER_APIKEY });
141
+ }
142
+ if (env.MMA_PROVIDER_TYPE || env.MMA_PROVIDER_BASEURL || env.MMA_PROVIDER_APIKEY) {
143
+ // Record once with the FINAL env-mutated provider object so the identity
144
+ // check in stripEnvOverrides matches what the loaded config holds.
145
+ overrides.push({ key: "provider", envValue: result.provider, originalValue: originalProvider });
146
+ }
147
+ if (env.MMA_LOG_LEVEL) {
148
+ record("logLevel", env.MMA_LOG_LEVEL);
142
149
  result.logLevel = env.MMA_LOG_LEVEL;
143
- if (env.MMA_LOCALE)
150
+ }
151
+ if (env.MMA_LOCALE) {
152
+ record("locale", env.MMA_LOCALE);
144
153
  result.locale = env.MMA_LOCALE;
154
+ }
145
155
  if (env.MMA_CONTEXT_WINDOW) {
146
156
  const v = parseInt(env.MMA_CONTEXT_WINDOW, 10);
147
- if (!isNaN(v))
157
+ if (!isNaN(v)) {
158
+ record("contextWindow", v);
148
159
  result.contextWindow = v;
160
+ }
149
161
  }
150
162
  if (env.MMA_MAX_TOOL_ITERATIONS) {
151
163
  const v = parseInt(env.MMA_MAX_TOOL_ITERATIONS, 10);
152
- if (!isNaN(v))
164
+ if (!isNaN(v)) {
165
+ record("maxToolIterations", v);
153
166
  result.maxToolIterations = v;
167
+ }
154
168
  }
155
169
  if (env.MMA_STUCK_THRESHOLD) {
156
170
  const v = parseInt(env.MMA_STUCK_THRESHOLD, 10);
157
- if (!isNaN(v))
171
+ if (!isNaN(v)) {
172
+ record("stuckThreshold", v);
158
173
  result.stuckThreshold = v;
174
+ }
175
+ }
176
+ if (env.MMA_AUTO_PLAN) {
177
+ const v = env.MMA_AUTO_PLAN === "true";
178
+ record("autoPlan", v);
179
+ result.autoPlan = v;
180
+ }
181
+ if (env.MMA_MOE_ENABLED) {
182
+ const next = { ...result.moe, enabled: env.MMA_MOE_ENABLED === "true" };
183
+ record("moe", next);
184
+ result.moe = next;
185
+ }
186
+ return { config: result, overrides };
187
+ }
188
+ /** Pre-env values for env-touched keys, keyed by the loaded config object. */
189
+ const envOverrideMap = new WeakMap();
190
+ /**
191
+ * Restore pre-env values before persisting — session-scoped env overrides
192
+ * (e.g. the cert harness baseUrl) must NOT leak into domain files. An explicit
193
+ * later change (setProvider/reconfigure replacing provider, /model use) shows
194
+ * up as a different object/value and is kept as-is.
195
+ */
196
+ function stripEnvOverrides(config) {
197
+ const overrides = envOverrideMap.get(config);
198
+ if (!overrides || overrides.length === 0)
199
+ return config;
200
+ const result = { ...config };
201
+ for (const o of overrides) {
202
+ if (result[o.key] === o.envValue) {
203
+ result[o.key] = o.originalValue;
204
+ }
159
205
  }
160
- if (env.MMA_AUTO_PLAN)
161
- result.autoPlan = env.MMA_AUTO_PLAN === "true";
162
- if (env.MMA_MOE_ENABLED)
163
- result.moe = { ...result.moe, enabled: env.MMA_MOE_ENABLED === "true" };
164
206
  return result;
165
207
  }
166
208
  /**
@@ -184,15 +226,24 @@ export function loadConfig(options) {
184
226
  const detector = new MigrationDetector(options.configDir);
185
227
  if (detector.needsMigration()) {
186
228
  const backup = new BackupManager(options.configDir);
229
+ // Full-directory backup (documented behavior) + config.json snapshot
230
+ // before we touch anything.
231
+ backup.backupAll();
187
232
  backup.backupConfig();
188
233
  const summary = backup.getBackupSummary();
189
- console.log(t("migration.detected", { summary }));
234
+ defaultOutputBus.log("info", "config", t("migration.detected", { summary }));
190
235
  try {
191
236
  unlinkSync(globalPath);
192
237
  }
193
- catch { }
238
+ catch (e) {
239
+ // Best-effort removal: loadConfig falls back to defaults either way,
240
+ // but a leftover v1 file would re-trigger migration on every startup.
241
+ defaultOutputBus.log("error", "config", `[config] failed to remove legacy config.json: ${e instanceof Error ? e.message : String(e)}`);
242
+ }
194
243
  }
195
- let config = { ...DEFAULTS };
244
+ // Deep clone: DEFAULTS holds singleton objects (security, lsp, tools…)
245
+ // that must never be mutated by runtime config changes.
246
+ let config = structuredClone(DEFAULTS);
196
247
  let legacyDetected = false;
197
248
  // Step 1: legacy config.json (fallback)
198
249
  const globalData = loadJSON(globalPath);
@@ -221,7 +272,8 @@ export function loadConfig(options) {
221
272
  }
222
273
  normalizeLspServerArgs(config);
223
274
  // Step 6: env vars
224
- config = applyEnvVars(config);
275
+ const { config: envConfig, overrides } = applyEnvVars(config);
276
+ config = envConfig;
225
277
  // Step 7: decrypt
226
278
  try {
227
279
  const encryptor = new ConfigEncryptor();
@@ -229,10 +281,18 @@ export function loadConfig(options) {
229
281
  for (const key of Object.keys(decrypted)) {
230
282
  config[key] = decrypted[key];
231
283
  }
284
+ // Decrypt rebuilds the object tree (new identities). Re-point the provider
285
+ // override at the post-decrypt provider so the save-time identity check in
286
+ // stripEnvOverrides still matches the loaded config.
287
+ const providerOverride = overrides.find((o) => o.key === "provider");
288
+ if (providerOverride)
289
+ providerOverride.envValue = config.provider;
232
290
  }
233
291
  catch (e) {
234
- console.warn(t("config.decryption_warning", { error: e.message }));
292
+ defaultOutputBus.log("warn", "config", t("config.decryption_warning", { error: e.message }));
235
293
  }
294
+ if (overrides.length > 0)
295
+ envOverrideMap.set(config, overrides);
236
296
  return { config, legacyDetected };
237
297
  }
238
298
  export function validateConfig(config, allToolTags) {
@@ -255,8 +315,10 @@ export function validateConfig(config, allToolTags) {
255
315
  * @param configDir Optional config directory (e.g. ~/.mma). When provided, writes domain files.
256
316
  */
257
317
  export function saveConfig(config, configPath, configDir) {
318
+ // Session-scoped MMA_* env overrides must not be persisted (C4).
319
+ const toWrite = stripEnvOverrides(config);
258
320
  if (configDir) {
259
- saveDomainFiles(config, configDir);
321
+ saveDomainFiles(toWrite, configDir);
260
322
  return;
261
323
  }
262
324
  // Legacy single-file write (backward compat for tests / edge cases)
@@ -265,12 +327,12 @@ export function saveConfig(config, configPath, configDir) {
265
327
  try {
266
328
  const encryptor = new ConfigEncryptor();
267
329
  const encryptedConfig = encryptor.encrypt({
268
- ...config,
330
+ ...toWrite,
269
331
  });
270
332
  writeFileSync(configPath, JSON.stringify(encryptedConfig, regexReplacer, 2), "utf-8");
271
333
  }
272
334
  catch (e) {
273
- console.warn(t("config.encryption_warning", { error: e.message }));
274
- writeFileSync(configPath, JSON.stringify(config, regexReplacer, 2), "utf-8");
335
+ defaultOutputBus.log("warn", "config", t("config.encryption_warning", { error: e.message }));
336
+ writeFileSync(configPath, JSON.stringify(toWrite, regexReplacer, 2), "utf-8");
275
337
  }
276
338
  }
@@ -1,8 +1,10 @@
1
1
  import { DEFAULT_SECURITY_CONFIG } from "./security";
2
2
  import { DEFAULT_LSP_CONFIG } from "../modules/lsp/config";
3
+ /** Single source of truth for the default model (experts inherit it, C8). */
4
+ const DEFAULT_MODEL = "qwen/qwen3.5-9b";
3
5
  export const DEFAULTS = {
4
6
  version: "2.0.0",
5
- model: "qwen/qwen3.5-9b",
7
+ model: DEFAULT_MODEL,
6
8
  provider: {
7
9
  type: "openai-compat",
8
10
  baseUrl: "http://localhost:1234/v1",
@@ -14,31 +16,42 @@ export const DEFAULTS = {
14
16
  },
15
17
  moe: {
16
18
  enabled: false,
19
+ maxReplanCycles: 3,
17
20
  },
18
21
  tools: {
22
+ // TODO(2026-08-29): revisit tool-set visibility. enableOnDemand:false
23
+ // restores the full ~28-tool set (legacy behavior) — safe for small
24
+ // models but costs schema tokens per call. The on-demand narrowing
25
+ // (defaultTags + enable_tools) kept the footprint small but made the 9B
26
+ // model call hidden tools as shell commands / drift to MCP. Decide the
27
+ // right trade-off for small models and pick one mode.
19
28
  defaultTags: ["file", "code"],
20
- enableOnDemand: true,
29
+ enableOnDemand: false,
21
30
  },
22
31
  experts: {
23
32
  code: {
24
- model: "qwen/qwen3.5-9b",
33
+ model: DEFAULT_MODEL,
25
34
  tool_tags: ["file", "code", "shell"],
26
35
  max_attempts: 3,
36
+ description: "file operations, code editing, shell commands",
27
37
  },
28
38
  research: {
29
- model: "qwen/qwen3.5-9b",
39
+ model: DEFAULT_MODEL,
30
40
  tool_tags: ["research"],
31
41
  max_attempts: 3,
42
+ description: "web search, web fetch, web browse, history search",
32
43
  },
33
44
  browser: {
34
- model: "qwen/qwen3.5-9b",
45
+ model: DEFAULT_MODEL,
35
46
  tool_tags: ["browser", "vision"],
36
47
  max_attempts: 3,
48
+ description: "browser automation, screenshots",
37
49
  },
38
50
  vision: {
39
- model: "qwen/qwen3.5-9b",
51
+ model: DEFAULT_MODEL,
40
52
  tool_tags: ["browser", "vision", "file"],
41
53
  max_attempts: 2,
54
+ description: "browser + file reading for visual tasks",
42
55
  },
43
56
  },
44
57
  contextWindow: 32768,
@@ -47,6 +60,9 @@ export const DEFAULTS = {
47
60
  responseReserve: 0.15,
48
61
  compactionThreshold: 0.75,
49
62
  },
63
+ instructions: {
64
+ summarize: true,
65
+ },
50
66
  modelLoad: {
51
67
  autoLoad: false,
52
68
  flashAttention: true,
@@ -62,7 +78,7 @@ export const DEFAULTS = {
62
78
  maxToolIterations: 1000,
63
79
  stuckThreshold: 6,
64
80
  autoPlan: true,
65
- showReasoning: false,
81
+ showReasoning: true,
66
82
  logLevel: "info",
67
83
  locale: "en",
68
84
  session: {
@@ -98,10 +114,9 @@ export const DEFAULTS = {
98
114
  },
99
115
  ui: {
100
116
  spinner: true,
101
- toolStyle: "inline",
102
- toolComments: true,
103
117
  showContextStats: false,
104
118
  showCompaction: true,
119
+ verbose: false,
105
120
  },
106
121
  mcpServers: {
107
122
  context7: {
@@ -124,6 +139,11 @@ export const DEFAULTS = {
124
139
  skills: {
125
140
  budget: 0.15,
126
141
  },
142
+ hallucination: {
143
+ judge: {
144
+ enabled: false,
145
+ },
146
+ },
127
147
  subagent: {
128
148
  resultMode: "text",
129
149
  maxSummaryChars: 2000,
@@ -134,8 +154,14 @@ export const DEFAULTS = {
134
154
  lsp: DEFAULT_LSP_CONFIG,
135
155
  updater: {
136
156
  enabled: true,
137
- checkOnStart: true,
138
- autoInstall: true,
157
+ checkOnStart: false,
158
+ autoInstall: false,
139
159
  intervalMs: 0,
140
160
  },
161
+ reasoning: {
162
+ mode: "auto",
163
+ min: "low",
164
+ max: "high",
165
+ overrideCooldown: 5,
166
+ },
141
167
  };
@@ -1,5 +1,8 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
2
2
  import { join } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { deepMerge, regexReplacer, regexReviver } from "./utils";
5
+ import { defaultOutputBus } from "../output";
3
6
  /**
4
7
  * Mapping: domain name → AppConfig keys that belong to it.
5
8
  * Each domain becomes one JSON file in ~/.mma/config/<domain>.json.
@@ -10,6 +13,7 @@ export const CONFIG_DOMAINS = {
10
13
  "model",
11
14
  "contextWindow",
12
15
  "contextBudget",
16
+ "instructions",
13
17
  "modelLoad",
14
18
  "maxToolIterations",
15
19
  "stuckThreshold",
@@ -17,6 +21,8 @@ export const CONFIG_DOMAINS = {
17
21
  "showReasoning",
18
22
  "logLevel",
19
23
  "locale",
24
+ "reasoning",
25
+ "hallucination",
20
26
  ],
21
27
  provider: ["provider", "orchestrator", "retry", "pricing"],
22
28
  moe: ["moe", "experts"],
@@ -31,55 +37,6 @@ export const CONFIG_DOMAINS = {
31
37
  skills: ["skills"],
32
38
  updater: ["updater"],
33
39
  };
34
- /**
35
- * JSON replacer that serializes RegExp objects as {__regex, source, flags}
36
- * so they survive JSON.stringify/parse round-trips.
37
- */
38
- function regexReplacer(_key, value) {
39
- if (value instanceof RegExp) {
40
- return { __regex: true, source: value.source, flags: value.flags };
41
- }
42
- return value;
43
- }
44
- /**
45
- * JSON reviver that restores RegExp objects serialized by regexReplacer.
46
- */
47
- function regexReviver(_key, value) {
48
- if (value &&
49
- typeof value === "object" &&
50
- value.__regex === true) {
51
- const { source, flags } = value;
52
- try {
53
- return new RegExp(source, flags);
54
- }
55
- catch {
56
- return value;
57
- }
58
- }
59
- return value;
60
- }
61
- /**
62
- * Deep merge two objects. Arrays and scalars in source replace target.
63
- */
64
- function deepMerge(target, source) {
65
- const result = { ...target };
66
- for (const key of Object.keys(source)) {
67
- const srcVal = source[key];
68
- const tgtVal = target[key];
69
- if (srcVal !== null &&
70
- srcVal !== undefined &&
71
- typeof srcVal === "object" &&
72
- !Array.isArray(srcVal) &&
73
- typeof tgtVal === "object" &&
74
- !Array.isArray(tgtVal)) {
75
- result[key] = deepMerge(tgtVal, srcVal);
76
- }
77
- else if (srcVal !== undefined) {
78
- result[key] = srcVal;
79
- }
80
- }
81
- return result;
82
- }
83
40
  /**
84
41
  * Extract a domain subset from a full AppConfig.
85
42
  */
@@ -138,7 +95,9 @@ export function loadDomainFiles(configDir) {
138
95
  anyLoaded = true;
139
96
  }
140
97
  catch {
141
- // Skip malformed domain files
98
+ // Never silent: surface the offending path instead of silently ignoring
99
+ // a domain the user believes is active.
100
+ defaultOutputBus.log("warn", "config", t("config.parse_failed", { path: filePath }));
142
101
  }
143
102
  }
144
103
  return anyLoaded ? result : null;
@@ -0,0 +1,56 @@
1
+ // src/config/utils.ts
2
+ // Shared deep-merge + RegExp-safe JSON helpers. Previously duplicated across
3
+ // config.ts and domains.ts, where the two copies had drifted (config.ts
4
+ // assigned RegExp by reference; domains.ts spread it into {} and destroyed it).
5
+ export function isPlainObject(v) {
6
+ if (typeof v !== "object" || v === null)
7
+ return false;
8
+ const proto = Object.getPrototypeOf(v);
9
+ return proto === Object.prototype || proto === null;
10
+ }
11
+ /**
12
+ * Deep merge two objects. Arrays, scalars, and non-plain values (RegExp, Date)
13
+ * in source replace target by reference — spreading a RegExp would silently
14
+ * destroy it to {}.
15
+ */
16
+ export function deepMerge(target, source) {
17
+ const result = { ...target };
18
+ for (const key of Object.keys(source)) {
19
+ const srcVal = source[key];
20
+ const tgtVal = target[key];
21
+ if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
22
+ result[key] = deepMerge(tgtVal, srcVal);
23
+ }
24
+ else if (srcVal !== undefined) {
25
+ result[key] = srcVal;
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ /**
31
+ * JSON replacer that serializes RegExp objects as {__regex, source, flags}
32
+ * so they survive JSON.stringify/parse round-trips.
33
+ */
34
+ export function regexReplacer(_key, value) {
35
+ if (value instanceof RegExp) {
36
+ return { __regex: true, source: value.source, flags: value.flags };
37
+ }
38
+ return value;
39
+ }
40
+ /**
41
+ * JSON reviver that restores RegExp objects serialized by regexReplacer.
42
+ */
43
+ export function regexReviver(_key, value) {
44
+ if (value &&
45
+ typeof value === "object" &&
46
+ value.__regex === true) {
47
+ const { source, flags } = value;
48
+ try {
49
+ return new RegExp(source, flags);
50
+ }
51
+ catch {
52
+ return value;
53
+ }
54
+ }
55
+ return value;
56
+ }
@@ -0,0 +1,49 @@
1
+ import { t } from "../../i18n/index";
2
+ import { MAX_AUDIT_RETRIES } from "./constants";
3
+ /**
4
+ * Финальный аудит-вентиль: проверяет, что текстовый ответ действительно
5
+ * выполнил задачу (при активном плане/скрипте), прежде чем принять его.
6
+ *
7
+ * Отклонение переспрашивает модель через <system-summary> и взводит
8
+ * suppressRepetitionRetry — повторный ответ на выполненную задачу
9
+ * естественно "повторяющийся" и не должен жечь бюджет халлюцинаций.
10
+ * Исчерпание повторов (MAX_AUDIT_RETRIES или упор в лимит итераций)
11
+ * переводит цикл в состояние auditFailed → success:false.
12
+ */
13
+ export class AuditGate {
14
+ finalAudit;
15
+ constructor(finalAudit) {
16
+ this.finalAudit = finalAudit;
17
+ }
18
+ /** Возвращает null, если аудит не настроен (ответ принимается безусловно). */
19
+ async evaluate(deps) {
20
+ if (!this.finalAudit)
21
+ return null;
22
+ const audit = await this.finalAudit();
23
+ if (!audit || audit.passed)
24
+ return { action: "pass" };
25
+ const { state, logger, contextManager } = deps;
26
+ state.iterAuditRejection = true;
27
+ logger.warn(`Final audit incomplete: ${audit.summary}`);
28
+ const steps = audit.pendingSteps.slice(0, 5).join("; ") || "—";
29
+ contextManager.addMessage({
30
+ role: "user",
31
+ content: `<system-summary>${t("exec.audit_incomplete", {
32
+ summary: audit.summary,
33
+ steps,
34
+ })}</system-summary>`,
35
+ });
36
+ deps.slog.logAudit(audit.summary, deps.iteration);
37
+ state.auditRetries++;
38
+ state.lastAuditSummary = audit.summary;
39
+ if (state.auditRetries >= MAX_AUDIT_RETRIES || deps.iteration >= deps.maxIterations - 1) {
40
+ logger.warn(`Final audit still incomplete after ${state.auditRetries} retries — reporting failure`);
41
+ state.auditFailed = true;
42
+ return { action: "exhausted", summary: audit.summary };
43
+ }
44
+ // Следующий текстовый ответ — принудительный повтор уже выполненной
45
+ // задачи: разрешаем одну повторность без халлюцинационного повтора.
46
+ state.suppressRepetitionRetry = true;
47
+ return { action: "reject" };
48
+ }
49
+ }