micro-models-agent 0.51.1 → 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -186,6 +186,10 @@ export class BrowserSession {
|
|
|
186
186
|
async open(url) {
|
|
187
187
|
if (!url)
|
|
188
188
|
return { success: false, output: t("browser.url_required") };
|
|
189
|
+
// Security: block dangerous schemes (file:, chrome:, about:, data:).
|
|
190
|
+
if (/^(file|chrome|about|data|javascript|view-source):/i.test(url)) {
|
|
191
|
+
return { success: false, output: `[SECURITY BLOCKED] scheme not allowed: ${url}` };
|
|
192
|
+
}
|
|
189
193
|
if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file://")) {
|
|
190
194
|
const isLocalhost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i.test(url);
|
|
191
195
|
url = isLocalhost ? "http://" + url : "https://" + url;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { rmSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
1
|
+
import { rmSync, writeFileSync } from "fs";
|
|
3
2
|
import { join, dirname } from "path";
|
|
4
3
|
import { fileURLToPath } from "url";
|
|
5
4
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -7,10 +6,9 @@ import { t } from "../../i18n/index";
|
|
|
7
6
|
import { pc } from "../../ui/colors";
|
|
8
7
|
import { loadScenarios, filterByTags } from "./loader";
|
|
9
8
|
import { runScenario, findMmaRoot } from "./runner";
|
|
10
|
-
import { readManifest, upsertCertification, removeCertification } from "./manifest";
|
|
9
|
+
import { readManifest, upsertCertification, removeCertification, isFullyPassed } from "./manifest";
|
|
11
10
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
11
|
const MMA_ROOT = findMmaRoot(HERE);
|
|
13
|
-
const USER_SCENARIO_DIR = join(homedir(), ".mma", "certification", "scenarios");
|
|
14
12
|
function readVersion() {
|
|
15
13
|
const candidates = [join(MMA_ROOT, "package.json")];
|
|
16
14
|
for (const p of candidates) {
|
|
@@ -35,12 +33,7 @@ export function parseTags(s) {
|
|
|
35
33
|
}
|
|
36
34
|
export async function certify(opts) {
|
|
37
35
|
const providerUrl = opts.providerUrl || opts.config.provider.baseUrl;
|
|
38
|
-
|
|
39
|
-
console.error(pc.red(t("cli.cert_security_required")));
|
|
40
|
-
process.exitCode = 1;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const { scenarios, errors } = loadScenarios(USER_SCENARIO_DIR);
|
|
36
|
+
const { scenarios, errors } = loadScenarios(join(opts.projectDir, ".mma", "certification", "scenarios"));
|
|
44
37
|
for (const e of errors)
|
|
45
38
|
console.error(pc.yellow(` ${e}`));
|
|
46
39
|
const selected = filterByTags(scenarios, opts.tags);
|
|
@@ -49,7 +42,7 @@ export async function certify(opts) {
|
|
|
49
42
|
process.exitCode = 1;
|
|
50
43
|
return;
|
|
51
44
|
}
|
|
52
|
-
const manifest = readManifest();
|
|
45
|
+
const manifest = readManifest(opts.projectDir);
|
|
53
46
|
const existing = manifest.certifications.find((e) => e.model === opts.name && e.providerUrl === providerUrl);
|
|
54
47
|
if (existing && !opts.force) {
|
|
55
48
|
console.error(pc.yellow(t("cli.cert_exists", { model: opts.name })));
|
|
@@ -84,6 +77,7 @@ export async function certify(opts) {
|
|
|
84
77
|
sandboxBase,
|
|
85
78
|
defaultReps: opts.reps,
|
|
86
79
|
defaultThreshold: 2,
|
|
80
|
+
baseConfig: opts.config,
|
|
87
81
|
onRep: (id, rep, reps, passed, failures) => {
|
|
88
82
|
const word = passed ? pc.green(t("cli.cert_rep_pass")) : pc.red(t("cli.cert_rep_fail"));
|
|
89
83
|
console.log(`[${idx}/${total}] ${id} (${rep}/${reps})... ${word}`);
|
|
@@ -110,7 +104,7 @@ export async function certify(opts) {
|
|
|
110
104
|
suite,
|
|
111
105
|
results,
|
|
112
106
|
};
|
|
113
|
-
upsertCertification(entry);
|
|
107
|
+
upsertCertification(entry, opts.projectDir);
|
|
114
108
|
console.log(t("cli.cert_done", {
|
|
115
109
|
passed: String(suite.passed),
|
|
116
110
|
failed: String(suite.failed),
|
|
@@ -118,9 +112,17 @@ export async function certify(opts) {
|
|
|
118
112
|
total: String(suite.total),
|
|
119
113
|
}));
|
|
120
114
|
printResults(results);
|
|
115
|
+
if (isFullyPassed(entry)) {
|
|
116
|
+
console.log(pc.green(`\n✔ ${opts.name} is certified`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(pc.yellow(`\n⚠ ${opts.name} is NOT certified — all scenarios must pass`));
|
|
120
|
+
}
|
|
121
|
+
// Always write report file
|
|
122
|
+
writeReport(entry, opts.projectDir);
|
|
121
123
|
}
|
|
122
|
-
export async function certStatus(name, config) {
|
|
123
|
-
const m = readManifest();
|
|
124
|
+
export async function certStatus(name, config, projectDir) {
|
|
125
|
+
const m = readManifest(projectDir);
|
|
124
126
|
const providerUrl = config.provider.baseUrl;
|
|
125
127
|
const entry = m.certifications.find((e) => e.model === name && e.providerUrl === providerUrl);
|
|
126
128
|
if (!entry) {
|
|
@@ -130,20 +132,25 @@ export async function certStatus(name, config) {
|
|
|
130
132
|
console.log(`${t("cli.cert_provider_col")}: ${entry.providerUrl}`);
|
|
131
133
|
console.log(`${t("cli.cert_version_col")}: ${entry.mmaVersion} ${t("cli.cert_date_col")}: ${entry.certifiedAt.slice(0, 10)}`);
|
|
132
134
|
console.log(`${t("cli.cert_suite_col")}: ${entry.suite.passed} pass / ${entry.suite.failed} fail / ${entry.suite.skipped} skipped`);
|
|
135
|
+
const status = isFullyPassed(entry)
|
|
136
|
+
? pc.green("✔ certified")
|
|
137
|
+
: pc.yellow("⚠ NOT certified");
|
|
138
|
+
console.log(`Status: ${status}`);
|
|
133
139
|
printResults(entry.results);
|
|
134
140
|
}
|
|
135
|
-
export async function certList() {
|
|
136
|
-
const m = readManifest();
|
|
141
|
+
export async function certList(projectDir) {
|
|
142
|
+
const m = readManifest(projectDir);
|
|
137
143
|
if (m.certifications.length === 0) {
|
|
138
144
|
console.log(t("cli.cert_empty"));
|
|
139
145
|
return;
|
|
140
146
|
}
|
|
141
147
|
for (const e of m.certifications) {
|
|
142
|
-
|
|
148
|
+
const mark = isFullyPassed(e) ? pc.green("✔") : pc.red("✘");
|
|
149
|
+
console.log(` ${mark} ${e.model} ${pc.dim(e.providerUrl)} ${e.mmaVersion} ${e.certifiedAt.slice(0, 10)} ${e.suite.passed}/${e.suite.total} pass`);
|
|
143
150
|
}
|
|
144
151
|
}
|
|
145
|
-
export async function uncertify(name, config) {
|
|
146
|
-
const removed = removeCertification(name, config.provider.baseUrl);
|
|
152
|
+
export async function uncertify(name, config, projectDir) {
|
|
153
|
+
const removed = removeCertification(name, config.provider.baseUrl, projectDir);
|
|
147
154
|
if (removed)
|
|
148
155
|
console.log(t("cli.cert_uncertified", { model: name }));
|
|
149
156
|
else
|
|
@@ -170,5 +177,37 @@ function printResults(results) {
|
|
|
170
177
|
console.log(` ${icon} ${r.id} ${detail}`);
|
|
171
178
|
if (r.error)
|
|
172
179
|
console.log(` ${pc.dim(r.error)}`);
|
|
180
|
+
if (r.diagnostics)
|
|
181
|
+
console.log(r.diagnostics);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function writeReport(entry, projectDir) {
|
|
185
|
+
const reportDir = join(projectDir, "certification");
|
|
186
|
+
const ts = entry.certifiedAt.replace(/[:.]/g, "-").slice(0, 19);
|
|
187
|
+
const filename = `report-${entry.model.replace(/[/\\:]/g, "_")}-${ts}.json`;
|
|
188
|
+
const reportPath = join(reportDir, filename);
|
|
189
|
+
const report = {
|
|
190
|
+
model: entry.model,
|
|
191
|
+
providerUrl: entry.providerUrl,
|
|
192
|
+
mmaVersion: entry.mmaVersion,
|
|
193
|
+
certifiedAt: entry.certifiedAt,
|
|
194
|
+
certified: isFullyPassed(entry),
|
|
195
|
+
suite: entry.suite,
|
|
196
|
+
results: entry.results.map((r) => ({
|
|
197
|
+
id: r.id,
|
|
198
|
+
title: r.title,
|
|
199
|
+
status: r.status,
|
|
200
|
+
passed: r.passed,
|
|
201
|
+
of: r.of,
|
|
202
|
+
error: r.error,
|
|
203
|
+
diagnostics: r.diagnostics,
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
208
|
+
console.log(pc.dim(`\nReport: ${reportPath}`));
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
console.error(pc.yellow(` Failed to write report: ${e.message}`));
|
|
173
212
|
}
|
|
174
213
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { BUILTIN_SCENARIOS } from "./scenarios";
|
|
5
|
-
const TAGS = ["core", "security", "image", "network", "browser"];
|
|
5
|
+
const TAGS = ["core", "security", "image", "network", "browser", "moe"];
|
|
6
6
|
const CHECK_TYPES = [
|
|
7
7
|
"fileExists",
|
|
8
8
|
"fileNotExists",
|
|
@@ -95,6 +95,7 @@ function normalizeScenario(data, file) {
|
|
|
95
95
|
fixtures: Array.isArray(d.fixtures) ? d.fixtures : undefined,
|
|
96
96
|
checks: Array.isArray(d.checks) ? d.checks : [],
|
|
97
97
|
skipReason: typeof d.skipReason === "string" ? d.skipReason : undefined,
|
|
98
|
+
config: typeof d.config === "object" && d.config !== null ? d.config : undefined,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
export function filterByTags(scenarios, tags) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join } from "path";
|
|
4
|
-
export
|
|
5
|
-
|
|
3
|
+
export function manifestPath(projectDir) {
|
|
4
|
+
return join(projectDir, "certification", "certifications.json");
|
|
5
|
+
}
|
|
6
|
+
export function readManifest(projectDir) {
|
|
7
|
+
const path = manifestPath(projectDir);
|
|
6
8
|
try {
|
|
7
9
|
if (existsSync(path)) {
|
|
8
10
|
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -14,26 +16,27 @@ export function readManifest(path = MANIFEST_PATH) {
|
|
|
14
16
|
}
|
|
15
17
|
return { version: 1, certifications: [] };
|
|
16
18
|
}
|
|
17
|
-
export function saveManifest(m,
|
|
18
|
-
|
|
19
|
+
export function saveManifest(m, projectDir) {
|
|
20
|
+
const path = manifestPath(projectDir);
|
|
21
|
+
mkdirSync(join(projectDir, "certification"), { recursive: true });
|
|
19
22
|
writeFileSync(path, JSON.stringify(m, null, 2), "utf-8");
|
|
20
23
|
}
|
|
21
|
-
export function upsertCertification(entry,
|
|
22
|
-
const m = readManifest(
|
|
24
|
+
export function upsertCertification(entry, projectDir) {
|
|
25
|
+
const m = readManifest(projectDir);
|
|
23
26
|
const idx = m.certifications.findIndex((e) => e.model === entry.model && e.providerUrl === entry.providerUrl);
|
|
24
27
|
if (idx >= 0)
|
|
25
28
|
m.certifications[idx] = entry;
|
|
26
29
|
else
|
|
27
30
|
m.certifications.push(entry);
|
|
28
|
-
saveManifest(m,
|
|
31
|
+
saveManifest(m, projectDir);
|
|
29
32
|
return m;
|
|
30
33
|
}
|
|
31
|
-
export function removeCertification(model, providerUrl,
|
|
32
|
-
const m = readManifest(
|
|
34
|
+
export function removeCertification(model, providerUrl, projectDir) {
|
|
35
|
+
const m = readManifest(projectDir);
|
|
33
36
|
const before = m.certifications.length;
|
|
34
37
|
m.certifications = m.certifications.filter((e) => !(e.model === model && e.providerUrl === providerUrl));
|
|
35
38
|
if (m.certifications.length !== before) {
|
|
36
|
-
saveManifest(m,
|
|
39
|
+
saveManifest(m, projectDir);
|
|
37
40
|
return true;
|
|
38
41
|
}
|
|
39
42
|
return false;
|
|
@@ -41,10 +44,15 @@ export function removeCertification(model, providerUrl, path = MANIFEST_PATH) {
|
|
|
41
44
|
export function isStale(entry, currentVersion) {
|
|
42
45
|
return entry.mmaVersion !== currentVersion;
|
|
43
46
|
}
|
|
44
|
-
export function
|
|
45
|
-
|
|
47
|
+
export function isFullyPassed(entry) {
|
|
48
|
+
return entry.suite.failed === 0 && entry.suite.passed > 0;
|
|
49
|
+
}
|
|
50
|
+
export function getCertMark(model, providerUrl, currentVersion, projectDir) {
|
|
51
|
+
const m = readManifest(projectDir);
|
|
46
52
|
const entry = m.certifications.find((e) => e.model === model && e.providerUrl === providerUrl);
|
|
47
53
|
if (!entry)
|
|
48
54
|
return "none";
|
|
49
|
-
|
|
55
|
+
if (isStale(entry, currentVersion))
|
|
56
|
+
return "stale";
|
|
57
|
+
return isFullyPassed(entry) ? "certified" : "none";
|
|
50
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync, cpSync } from "fs";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, cpSync, writeFileSync, readdirSync, readFileSync } from "fs";
|
|
3
3
|
import { platform } from "os";
|
|
4
|
-
import { join, resolve, dirname } from "path";
|
|
4
|
+
import { join, resolve, dirname, relative } from "path";
|
|
5
5
|
import { checkSandbox } from "./fact-checker";
|
|
6
6
|
export async function runScenario(scenario, opts) {
|
|
7
7
|
if (scenario.mode === "skip") {
|
|
@@ -16,10 +16,11 @@ export async function runScenario(scenario, opts) {
|
|
|
16
16
|
const reps = scenario.reps ?? opts.defaultReps;
|
|
17
17
|
const threshold = Math.min(scenario.passThreshold ?? opts.defaultThreshold, reps);
|
|
18
18
|
const runner = opts.runner ?? defaultRunner;
|
|
19
|
-
const timeoutMs = opts.timeoutMs ??
|
|
19
|
+
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
20
20
|
const entryPoint = resolveMmaEntry(opts.mmaRoot);
|
|
21
21
|
let passed = 0;
|
|
22
22
|
let firstError;
|
|
23
|
+
let lastFailedSandbox;
|
|
23
24
|
for (let i = 1; i <= reps; i++) {
|
|
24
25
|
const sandbox = join(opts.sandboxBase, `run-${scenario.id}-${i}`);
|
|
25
26
|
let failures = [];
|
|
@@ -43,6 +44,14 @@ export async function runScenario(scenario, opts) {
|
|
|
43
44
|
};
|
|
44
45
|
if (opts.providerKey)
|
|
45
46
|
env.MMA_PROVIDER_APIKEY = opts.providerKey;
|
|
47
|
+
// Write cert-specific config if scenario declares overrides
|
|
48
|
+
if (scenario.config && opts.baseConfig) {
|
|
49
|
+
const merged = deepMergeAny(opts.baseConfig, scenario.config);
|
|
50
|
+
const certConfigDir = join(sandbox, ".mma");
|
|
51
|
+
mkdirSync(certConfigDir, { recursive: true });
|
|
52
|
+
writeFileSync(join(certConfigDir, "config.json"), JSON.stringify(merged, null, 2), "utf-8");
|
|
53
|
+
env.MMA_CONFIG_DIR = certConfigDir;
|
|
54
|
+
}
|
|
46
55
|
const res = await runner(env, opts.mmaRoot, args, timeoutMs);
|
|
47
56
|
output = `${res.stdout}\n${res.stderr}`;
|
|
48
57
|
exitCode = res.code ?? -1;
|
|
@@ -64,11 +73,17 @@ export async function runScenario(scenario, opts) {
|
|
|
64
73
|
const pass = failures.length === 0;
|
|
65
74
|
if (pass)
|
|
66
75
|
passed++;
|
|
67
|
-
else
|
|
68
|
-
|
|
76
|
+
else {
|
|
77
|
+
if (!firstError)
|
|
78
|
+
firstError = failures.join("; ");
|
|
79
|
+
lastFailedSandbox = sandbox;
|
|
80
|
+
}
|
|
69
81
|
opts.onRep?.(scenario.id, i, reps, pass, failures);
|
|
70
82
|
}
|
|
71
83
|
const status = passed >= threshold ? "pass" : "fail";
|
|
84
|
+
const diagnostics = status === "fail" && lastFailedSandbox
|
|
85
|
+
? collectDiagnostics(lastFailedSandbox)
|
|
86
|
+
: undefined;
|
|
72
87
|
return {
|
|
73
88
|
id: scenario.id,
|
|
74
89
|
title: scenario.title,
|
|
@@ -76,6 +91,7 @@ export async function runScenario(scenario, opts) {
|
|
|
76
91
|
passed,
|
|
77
92
|
of: reps,
|
|
78
93
|
error: status === "pass" ? undefined : firstError,
|
|
94
|
+
diagnostics,
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
function prepareSandbox(sandbox, scenario, mmaRoot) {
|
|
@@ -91,6 +107,57 @@ function prepareSandbox(sandbox, scenario, mmaRoot) {
|
|
|
91
107
|
cpSync(src, dest);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
110
|
+
/** Collect diagnostic info from a failed sandbox for actionable feedback. */
|
|
111
|
+
export function collectDiagnostics(sandbox) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
// List user-created files (skip .mma internals)
|
|
114
|
+
const files = listFiles(sandbox, sandbox);
|
|
115
|
+
if (files.length > 0) {
|
|
116
|
+
lines.push(` Files created: ${files.join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(" Files created: (none)");
|
|
120
|
+
}
|
|
121
|
+
// Read active plan if present
|
|
122
|
+
const planPath = join(sandbox, ".mma", "plans", "active.json");
|
|
123
|
+
if (existsSync(planPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const plan = JSON.parse(readFileSync(planPath, "utf-8"));
|
|
126
|
+
const steps = plan.steps ?? [];
|
|
127
|
+
const done = steps.filter((s) => s.status === "done").length;
|
|
128
|
+
const pending = steps.filter((s) => s.status === "pending" || s.status === "in_progress");
|
|
129
|
+
lines.push(` Plan: ${done}/${steps.length} steps done`);
|
|
130
|
+
if (pending.length > 0) {
|
|
131
|
+
lines.push(` Pending: ${pending.map((s) => `#${s.id} ${s.description}`).join("; ")}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* corrupt plan — skip */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
function listFiles(dir, root) {
|
|
141
|
+
const result = [];
|
|
142
|
+
try {
|
|
143
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
144
|
+
if (entry.name === ".mma")
|
|
145
|
+
continue;
|
|
146
|
+
const abs = join(dir, entry.name);
|
|
147
|
+
const rel = relative(root, abs).replace(/\\/g, "/");
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
result.push(...listFiles(abs, root));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
result.push(rel);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* ignore */
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
94
161
|
export function resolveMmaEntry(mmaRoot) {
|
|
95
162
|
const dev = join(mmaRoot, "src", "cli", "main.ts");
|
|
96
163
|
if (existsSync(dev))
|
|
@@ -157,3 +224,22 @@ function killTree(child) {
|
|
|
157
224
|
}
|
|
158
225
|
}
|
|
159
226
|
}
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
function deepMergeAny(target, source) {
|
|
229
|
+
if (!source || typeof source !== "object")
|
|
230
|
+
return source;
|
|
231
|
+
if (!target || typeof target !== "object")
|
|
232
|
+
return source;
|
|
233
|
+
const result = { ...target };
|
|
234
|
+
for (const key of Object.keys(source)) {
|
|
235
|
+
const sv = source[key];
|
|
236
|
+
const tv = result[key];
|
|
237
|
+
if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
|
|
238
|
+
result[key] = deepMergeAny(tv, sv);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
result[key] = sv;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|