@deeeed/metamask-harness 0.14.7 → 0.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 +81 -6
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +4 -2
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +73 -34
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +21 -10
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +10 -4
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/stop-viewers.sh +0 -0
- package/adapters/extension/verify.sh +19 -2
- package/adapters/extension/wallet-fixture-state.cjs +14 -1
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/launch-metro.cjs +74 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +4 -4
- package/adapters/mobile/open-device.sh +3 -1
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +12 -18
- package/adapters/mobile/stop-metro.sh +1 -0
- package/adapters/mobile/verify.sh +5 -5
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +29 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +35 -4
- package/adapters/shared/open-log-window.sh +1 -1
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +21 -17
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -5
- package/adapters/shared/tmux-viewer.sh +7 -2
- package/dist/adapters/core/surface.js +3 -0
- package/dist/adapters/extension/ensure-ready.js +0 -7
- package/dist/adapters/extension/runtime-decision.js +93 -3
- package/dist/adapters/extension/surface.js +17 -6
- package/dist/adapters/mobile/metro-env.js +67 -0
- package/dist/adapters/mobile/perps-env.js +101 -0
- package/dist/adapters/mobile/prepare.js +32 -14
- package/dist/adapters/mobile/runtime-decision.js +21 -2
- package/dist/adapters/mobile/source-freshness.js +116 -0
- package/dist/adapters/mobile/surface.js +3 -0
- package/dist/adapters/resolve-slot-ports.js +4 -0
- package/dist/adapters/slot-ports.js +131 -49
- package/dist/adapters.js +7 -2
- package/dist/checkout-lock.js +72 -0
- package/dist/cli.js +2 -0
- package/dist/commands/call.js +91 -54
- package/dist/commands/check.js +266 -46
- package/dist/commands/checklist.js +136 -0
- package/dist/commands/debug.js +21 -2
- package/dist/commands/doctor.js +187 -12
- package/dist/commands/fixtures.js +177 -42
- package/dist/commands/launch/extension.js +6 -1
- package/dist/commands/launch/index.js +54 -22
- package/dist/commands/logs.js +24 -4
- package/dist/commands/run-engine.js +223 -7
- package/dist/commands/run.js +67 -50
- package/dist/commands/shared.js +86 -5
- package/dist/commands/stop.js +1 -2
- package/dist/doctor.js +39 -17
- package/dist/harness.js +5 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/mm-harness-cli.js +37 -9
- package/dist/runner.js +35 -5
- package/dist/runtime-context.js +228 -0
- package/docs/CLI-SPEC.md +15 -11
- package/docs/MENTAL-MODEL.md +6 -6
- package/library/actions/extension/perps/perps.mjs +29 -8
- package/library/actions/extension/platform/cdp.mjs +128 -28
- package/library/actions/extension/ui/navigate.mjs +1 -1
- package/library/actions/mobile/perps/perps.mjs +13 -1
- package/library/actions/mobile/platform/bridge.mjs +302 -29
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
- package/library/manifests/extension.action-manifest.json +32 -12
- package/library/manifests/mobile.action-manifest.json +25 -3
- package/package.json +5 -5
- package/scripts/completions.sh +7 -4
- package/scripts/install-completions.sh +0 -0
package/dist/commands/check.js
CHANGED
|
@@ -4,14 +4,16 @@ import path from "node:path";
|
|
|
4
4
|
import { detectAdapter } from "../harness.js";
|
|
5
5
|
import { EXIT, usageOut } from "./shared.js";
|
|
6
6
|
import { optionFlag, optionString, parseArgs } from "./parse-args.js";
|
|
7
|
-
const USAGE = "mm-harness check diff [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
|
|
7
|
+
const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
|
|
8
8
|
const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
|
|
9
9
|
const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
|
|
10
10
|
const TEST_FILE_RE = /(?:^|[./_-])(?:test|spec)\.(?:c|m)?jsx?$|(?:^|[./_-])(?:test|spec)\.tsx?$/u;
|
|
11
|
+
const ESLINT_SUPPRESSION_RE = /^\s*eslint-disable(?:-line|-next-line)?\b/u;
|
|
11
12
|
async function handleCheck(argv) {
|
|
12
13
|
const { positional, options } = parseArgs(argv, "check");
|
|
13
14
|
const action = positional[0];
|
|
14
15
|
const json = optionFlag(options, "json");
|
|
16
|
+
const fix = optionFlag(options, "fix");
|
|
15
17
|
if (action !== "diff") {
|
|
16
18
|
const message = action ? `unknown action '${action}'` : "missing action";
|
|
17
19
|
return usageOut(json, "check", message, USAGE);
|
|
@@ -51,52 +53,64 @@ async function handleCheck(argv) {
|
|
|
51
53
|
const mergeBase = resolveMergeBase(target, baseRef);
|
|
52
54
|
const changedFiles = listChangedFiles(target, mergeBase, artifactsDir);
|
|
53
55
|
const existingChangedFiles = changedFiles.filter((file) => fs.existsSync(path.join(target, file)));
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
checks.push(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
56
|
+
const suppressionCheck = checkNewEslintSuppressions({
|
|
57
|
+
target,
|
|
58
|
+
artifactsDir,
|
|
59
|
+
mergeBase,
|
|
60
|
+
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file))
|
|
61
|
+
});
|
|
62
|
+
const policyFailed = suppressionCheck.status === "fail";
|
|
63
|
+
const fixes = [];
|
|
64
|
+
if (fix && !policyFailed) {
|
|
65
|
+
fixes.push(
|
|
66
|
+
runToolCheck({
|
|
67
|
+
id: "eslint-fix",
|
|
68
|
+
label: "Changed-file ESLint fix",
|
|
69
|
+
target,
|
|
70
|
+
artifactsDir,
|
|
71
|
+
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
72
|
+
command: executable(target, "eslint"),
|
|
73
|
+
args: ["--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
fixes.push(runFormatterFix({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
77
|
+
}
|
|
78
|
+
const checks = [suppressionCheck];
|
|
79
|
+
if (!policyFailed) {
|
|
80
|
+
checks.push(
|
|
81
|
+
runToolCheck({
|
|
82
|
+
id: "eslint",
|
|
83
|
+
label: "Changed-file ESLint",
|
|
84
|
+
target,
|
|
85
|
+
artifactsDir,
|
|
86
|
+
files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
|
|
87
|
+
command: executable(target, "eslint"),
|
|
88
|
+
args: ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
checks.push(runFormatterCheck({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
|
|
92
|
+
checks.push(
|
|
93
|
+
...runJestChecks({
|
|
94
|
+
adapter,
|
|
95
|
+
target,
|
|
96
|
+
artifactsDir,
|
|
97
|
+
files: existingChangedFiles.filter((file) => TEST_FILE_RE.test(file))
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
if (profile === "full") {
|
|
101
|
+
checks.push(runTypecheck({ target, artifactsDir, packageJson }));
|
|
102
|
+
} else {
|
|
103
|
+
checks.push({
|
|
104
|
+
id: "typecheck",
|
|
105
|
+
label: "TypeScript project check",
|
|
106
|
+
status: "skip",
|
|
107
|
+
reason: "profile=fast; run with --profile full for repo-wide typecheck"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
97
110
|
}
|
|
98
|
-
const status = checks.some((check) => check.status === "fail") ? "fail" : "pass";
|
|
111
|
+
const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
|
|
99
112
|
const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
|
|
113
|
+
const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
|
|
100
114
|
const summary = {
|
|
101
115
|
schemaVersion: 1,
|
|
102
116
|
command: "check",
|
|
@@ -106,12 +120,15 @@ async function handleCheck(argv) {
|
|
|
106
120
|
adapter: adapter ?? null,
|
|
107
121
|
target,
|
|
108
122
|
profile,
|
|
123
|
+
fix,
|
|
109
124
|
baseRef,
|
|
110
125
|
baseSource: base.source,
|
|
111
126
|
baseLabel: base.label ?? null,
|
|
112
127
|
mergeBase,
|
|
113
128
|
changedFiles,
|
|
129
|
+
fixes,
|
|
114
130
|
checks,
|
|
131
|
+
guidance,
|
|
115
132
|
artifacts: {
|
|
116
133
|
dir: artifactsDir,
|
|
117
134
|
summaryJson: path.join(artifactsDir, "validation-summary.json"),
|
|
@@ -184,6 +201,93 @@ function isUnderArtifactsDir(target, artifactsDir, file) {
|
|
|
184
201
|
function normalizeRelativePath(value) {
|
|
185
202
|
return value.split(path.sep).join("/");
|
|
186
203
|
}
|
|
204
|
+
function checkNewEslintSuppressions(input) {
|
|
205
|
+
const id = "policy-suppressions";
|
|
206
|
+
const label = "New ESLint suppression policy";
|
|
207
|
+
if (input.files.length === 0) {
|
|
208
|
+
return { id, label, status: "skip", reason: "no changed JS/TS files" };
|
|
209
|
+
}
|
|
210
|
+
const untracked = new Set(
|
|
211
|
+
runCommand("git", ["ls-files", "--others", "--exclude-standard"], input.target).stdout.split("\n").map((file) => normalizeRelativePath(file.trim())).filter(Boolean)
|
|
212
|
+
);
|
|
213
|
+
const findings = [];
|
|
214
|
+
const trackedFiles = input.files.filter((file) => !untracked.has(normalizeRelativePath(file)));
|
|
215
|
+
if (trackedFiles.length > 0) {
|
|
216
|
+
const base = input.mergeBase ?? "HEAD";
|
|
217
|
+
const diff = runCommand(
|
|
218
|
+
"git",
|
|
219
|
+
["diff", "--unified=0", "--no-color", "--no-ext-diff", base, "--", ...trackedFiles],
|
|
220
|
+
input.target
|
|
221
|
+
).stdout;
|
|
222
|
+
findings.push(...findAddedSuppressions(diff));
|
|
223
|
+
}
|
|
224
|
+
for (const file of input.files.filter((candidate) => untracked.has(normalizeRelativePath(candidate)))) {
|
|
225
|
+
const lines = fs.readFileSync(path.join(input.target, file), "utf8").split("\n");
|
|
226
|
+
lines.forEach((line, index) => {
|
|
227
|
+
if (hasEslintSuppressionDirective(line)) findings.push(`${normalizeRelativePath(file)}:${index + 1}: ${line.trim()}`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const logPath = path.join(input.artifactsDir, `${id}.log`);
|
|
231
|
+
fs.writeFileSync(logPath, findings.length > 0 ? `${findings.join("\n")}
|
|
232
|
+
` : "No newly added eslint-disable directives.\n");
|
|
233
|
+
return findings.length > 0 ? {
|
|
234
|
+
id,
|
|
235
|
+
label,
|
|
236
|
+
status: "fail",
|
|
237
|
+
logPath,
|
|
238
|
+
reason: `${findings.length} newly added eslint-disable directive(s)`
|
|
239
|
+
} : { id, label, status: "pass", logPath };
|
|
240
|
+
}
|
|
241
|
+
function findAddedSuppressions(diff) {
|
|
242
|
+
const findings = [];
|
|
243
|
+
let file = "";
|
|
244
|
+
let newLine = 0;
|
|
245
|
+
for (const line of diff.split("\n")) {
|
|
246
|
+
if (line.startsWith("+++ ")) {
|
|
247
|
+
file = normalizeRelativePath(line.slice(4).replace(/^b\//u, ""));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/u.exec(line);
|
|
251
|
+
if (hunk) {
|
|
252
|
+
newLine = Number(hunk[1]);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
256
|
+
const content = line.slice(1);
|
|
257
|
+
if (hasEslintSuppressionDirective(content)) findings.push(`${file}:${newLine}: ${content.trim()}`);
|
|
258
|
+
newLine += 1;
|
|
259
|
+
} else if (!line.startsWith("-") && !line.startsWith("\\")) {
|
|
260
|
+
newLine += 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
}
|
|
265
|
+
function hasEslintSuppressionDirective(line) {
|
|
266
|
+
let quote = "";
|
|
267
|
+
let escaped = false;
|
|
268
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
269
|
+
const char = line[index];
|
|
270
|
+
if (quote) {
|
|
271
|
+
if (escaped) escaped = false;
|
|
272
|
+
else if (char === "\\") escaped = true;
|
|
273
|
+
else if (char === quote) quote = "";
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
277
|
+
quote = char;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (char !== "/") continue;
|
|
281
|
+
const next = line[index + 1];
|
|
282
|
+
if (next === "/") return ESLINT_SUPPRESSION_RE.test(line.slice(index + 2));
|
|
283
|
+
if (next !== "*") continue;
|
|
284
|
+
if (ESLINT_SUPPRESSION_RE.test(line.slice(index + 2))) return true;
|
|
285
|
+
const end = line.indexOf("*/", index + 2);
|
|
286
|
+
if (end === -1) return false;
|
|
287
|
+
index = end + 1;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
187
291
|
function executable(target, name) {
|
|
188
292
|
const local = path.join(target, "node_modules", ".bin", name);
|
|
189
293
|
return fs.existsSync(local) ? local : null;
|
|
@@ -197,6 +301,118 @@ function runToolCheck(input) {
|
|
|
197
301
|
}
|
|
198
302
|
return runCheckCommand(input.id, input.label, input.target, input.artifactsDir, input.command, [...input.args, ...input.files]);
|
|
199
303
|
}
|
|
304
|
+
function runJestChecks(input) {
|
|
305
|
+
if (input.adapter !== "core" || input.files.length === 0) {
|
|
306
|
+
return [
|
|
307
|
+
runToolCheck({
|
|
308
|
+
id: "jest",
|
|
309
|
+
label: "Changed test files",
|
|
310
|
+
target: input.target,
|
|
311
|
+
artifactsDir: input.artifactsDir,
|
|
312
|
+
files: input.files,
|
|
313
|
+
command: executable(input.target, "jest"),
|
|
314
|
+
args: ["--no-coverage"]
|
|
315
|
+
})
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
const groups = /* @__PURE__ */ new Map();
|
|
319
|
+
const rootFiles = [];
|
|
320
|
+
for (const file of input.files) {
|
|
321
|
+
const workspace = resolveWorkspace(input.target, file);
|
|
322
|
+
if (!workspace) {
|
|
323
|
+
rootFiles.push(file);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const key = workspace.root;
|
|
327
|
+
const group = groups.get(key) ?? { ...workspace, files: [] };
|
|
328
|
+
group.files.push(normalizeRelativePath(path.relative(workspace.root, path.join(input.target, file))));
|
|
329
|
+
groups.set(key, group);
|
|
330
|
+
}
|
|
331
|
+
const resultCount = groups.size + (rootFiles.length > 0 ? 1 : 0);
|
|
332
|
+
const results = [];
|
|
333
|
+
for (const group of [...groups.values()].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
334
|
+
const id = resultCount === 1 ? "jest" : `jest-${safeId(group.name)}`;
|
|
335
|
+
results.push(
|
|
336
|
+
runCheckCommand(
|
|
337
|
+
id,
|
|
338
|
+
`Changed test files (${group.name})`,
|
|
339
|
+
input.target,
|
|
340
|
+
input.artifactsDir,
|
|
341
|
+
"yarn",
|
|
342
|
+
["workspace", group.name, "run", "jest", "--no-coverage", ...group.files.sort()]
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (rootFiles.length > 0) {
|
|
347
|
+
results.push(
|
|
348
|
+
runToolCheck({
|
|
349
|
+
id: resultCount === 1 ? "jest" : "jest-root",
|
|
350
|
+
label: "Changed root test files",
|
|
351
|
+
target: input.target,
|
|
352
|
+
artifactsDir: input.artifactsDir,
|
|
353
|
+
files: rootFiles,
|
|
354
|
+
command: executable(input.target, "jest"),
|
|
355
|
+
args: ["--no-coverage"]
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
function resolveWorkspace(target, file) {
|
|
362
|
+
const root = path.resolve(target);
|
|
363
|
+
let current = path.dirname(path.resolve(root, file));
|
|
364
|
+
while (current !== root && current.startsWith(`${root}${path.sep}`)) {
|
|
365
|
+
const packageJson = readPackageJson(current);
|
|
366
|
+
if (typeof packageJson?.name === "string" && packageJson.name.trim()) {
|
|
367
|
+
return { name: packageJson.name, root: current };
|
|
368
|
+
}
|
|
369
|
+
current = path.dirname(current);
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function safeId(value) {
|
|
374
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
375
|
+
}
|
|
376
|
+
function runFormatterCheck(input) {
|
|
377
|
+
const oxfmtConfig = ["oxfmt.config.ts", "oxfmt.config.mts", "oxfmt.config.cts", "oxfmt.config.js", "oxfmt.config.mjs", "oxfmt.config.cjs", ".oxfmtrc.json"].find((candidate) => fs.existsSync(path.join(input.target, candidate)));
|
|
378
|
+
const oxfmt = oxfmtConfig ? executable(input.target, "oxfmt") : null;
|
|
379
|
+
if (oxfmt && oxfmtConfig) {
|
|
380
|
+
return runToolCheck({
|
|
381
|
+
id: "oxfmt",
|
|
382
|
+
label: "Changed-file oxfmt",
|
|
383
|
+
...input,
|
|
384
|
+
command: oxfmt,
|
|
385
|
+
args: ["--config", oxfmtConfig, "--check"]
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return runToolCheck({
|
|
389
|
+
id: "prettier",
|
|
390
|
+
label: "Changed-file Prettier",
|
|
391
|
+
...input,
|
|
392
|
+
command: executable(input.target, "prettier"),
|
|
393
|
+
args: ["--check"]
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function runFormatterFix(input) {
|
|
397
|
+
const oxfmtConfig = ["oxfmt.config.ts", "oxfmt.config.mts", "oxfmt.config.cts", "oxfmt.config.js", "oxfmt.config.mjs", "oxfmt.config.cjs", ".oxfmtrc.json"].find((candidate) => fs.existsSync(path.join(input.target, candidate)));
|
|
398
|
+
const oxfmt = oxfmtConfig ? executable(input.target, "oxfmt") : null;
|
|
399
|
+
if (oxfmt && oxfmtConfig) {
|
|
400
|
+
return runToolCheck({
|
|
401
|
+
id: "oxfmt-fix",
|
|
402
|
+
label: "Changed-file oxfmt fix",
|
|
403
|
+
...input,
|
|
404
|
+
command: oxfmt,
|
|
405
|
+
args: ["--config", oxfmtConfig, "--write"]
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return runToolCheck({
|
|
409
|
+
id: "prettier-fix",
|
|
410
|
+
label: "Changed-file Prettier fix",
|
|
411
|
+
...input,
|
|
412
|
+
command: executable(input.target, "prettier"),
|
|
413
|
+
args: ["--write"]
|
|
414
|
+
});
|
|
415
|
+
}
|
|
200
416
|
function runTypecheck(input) {
|
|
201
417
|
const scripts = input.packageJson?.scripts && typeof input.packageJson.scripts === "object" ? input.packageJson.scripts : {};
|
|
202
418
|
if (typeof scripts["lint:tsc"] === "string") {
|
|
@@ -270,13 +486,16 @@ function renderMarkdown(summary) {
|
|
|
270
486
|
"",
|
|
271
487
|
`Verdict: ${summary.status}`,
|
|
272
488
|
`Profile: ${summary.profile}`,
|
|
489
|
+
`Fix: ${summary.fix ? "yes" : "no"}`,
|
|
273
490
|
`Base: ${summary.baseRef || "(none)"} (${summary.baseSource || "unknown"}${summary.baseLabel ? `: ${summary.baseLabel}` : ""})`,
|
|
274
491
|
`Changed files: ${summary.changedFiles.length}`,
|
|
275
492
|
"",
|
|
493
|
+
...summary.fixes.length > 0 ? ["## Fixes", "", ...summary.fixes.map((fix) => `- ${fix.status.toUpperCase()} ${fix.id}${fix.reason ? ` \u2014 ${fix.reason}` : ""}${fix.logPath ? ` (${fix.logPath})` : ""}`), ""] : [],
|
|
494
|
+
...summary.guidance.length > 0 ? ["## Next action", "", ...summary.guidance.map((item) => `- ${item}`), ""] : [],
|
|
276
495
|
"## Checks",
|
|
277
496
|
""
|
|
278
497
|
];
|
|
279
|
-
for (const check of summary.checks) {
|
|
498
|
+
for (const check of [...summary.fixes, ...summary.checks]) {
|
|
280
499
|
lines.push(`- ${check.status.toUpperCase()} ${check.id}${check.reason ? ` \u2014 ${check.reason}` : ""}${check.logPath ? ` (${check.logPath})` : ""}`);
|
|
281
500
|
}
|
|
282
501
|
lines.push("", "## Changed Files", "");
|
|
@@ -298,6 +517,7 @@ function renderHuman(summary) {
|
|
|
298
517
|
console.log(` ${prefix} ${check.id}: ${check.status}${check.reason ? ` (${check.reason})` : ""}`);
|
|
299
518
|
if (check.status === "fail" && check.logPath) console.log(` log: ${check.logPath}`);
|
|
300
519
|
}
|
|
520
|
+
for (const item of summary.guidance) console.log(` Next: ${item}`);
|
|
301
521
|
console.log(`Artifacts: ${summary.artifacts.summaryJson}`);
|
|
302
522
|
}
|
|
303
523
|
function failEnvelope(input) {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { EXIT } from "./shared.js";
|
|
6
|
+
const require2 = createRequire(import.meta.url);
|
|
7
|
+
const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
|
|
8
|
+
function terminalSignalStatus(taskDir) {
|
|
9
|
+
try {
|
|
10
|
+
const target = JSON.parse(
|
|
11
|
+
fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
|
|
12
|
+
);
|
|
13
|
+
const signalPath = path.resolve(taskDir, target.signal ?? "SIGNAL.json");
|
|
14
|
+
const signal = JSON.parse(fs.readFileSync(signalPath, "utf8"));
|
|
15
|
+
return signal.status === "complete" || signal.status === "blocked" ? signal.status : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function preserveTerminalSignal(taskDir, markArgs) {
|
|
21
|
+
const step = markArgs[0];
|
|
22
|
+
if (step !== "start" && numericMarkStep(step) === null) return false;
|
|
23
|
+
const status = terminalSignalStatus(taskDir);
|
|
24
|
+
if (!status) return false;
|
|
25
|
+
console.log(
|
|
26
|
+
`signal already terminal: status=${status}; ignoring non-terminal mark ${step}`
|
|
27
|
+
);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function numericMarkStep(step) {
|
|
31
|
+
const stepNumber = Number(step);
|
|
32
|
+
return Number.isInteger(stepNumber) && stepNumber >= 1 ? stepNumber : null;
|
|
33
|
+
}
|
|
34
|
+
function checklistPath(taskDir) {
|
|
35
|
+
try {
|
|
36
|
+
const target = JSON.parse(
|
|
37
|
+
fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
|
|
38
|
+
);
|
|
39
|
+
return path.resolve(taskDir, target.checklist ?? "CHECKLIST.md");
|
|
40
|
+
} catch {
|
|
41
|
+
return path.join(taskDir, "CHECKLIST.md");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
45
|
+
let inFence = false;
|
|
46
|
+
let seen = 0;
|
|
47
|
+
for (const line of checklist.split(/\n/u)) {
|
|
48
|
+
if (/^\s*```/u.test(line)) {
|
|
49
|
+
inFence = !inFence;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (inFence) continue;
|
|
53
|
+
const match = line.match(/^\s*[-*]\s+\[(?: |x|X)\]\s+(.*)$/u);
|
|
54
|
+
if (!match) continue;
|
|
55
|
+
seen += 1;
|
|
56
|
+
if (seen === stepNumber) return /mm-harness check diff/u.test(match[1] ?? "");
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function checkDiffGateReady(taskDir, markArgs) {
|
|
61
|
+
const step = markArgs[0];
|
|
62
|
+
const stepNumber = numericMarkStep(step);
|
|
63
|
+
if (stepNumber === null) return true;
|
|
64
|
+
let checklist = "";
|
|
65
|
+
try {
|
|
66
|
+
checklist = fs.readFileSync(checklistPath(taskDir), "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (!checklistStepRequiresDiffPass(checklist, stepNumber)) return true;
|
|
71
|
+
const summaryPath = path.join(
|
|
72
|
+
taskDir,
|
|
73
|
+
"artifacts",
|
|
74
|
+
"validation-summary.json"
|
|
75
|
+
);
|
|
76
|
+
try {
|
|
77
|
+
const summary = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
|
|
78
|
+
if (summary.command === "check" && summary.action === "diff" && summary.status === "pass") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
console.error(
|
|
84
|
+
`check-diff gate ${step} requires a task-local PASS at ${summaryPath}`
|
|
85
|
+
);
|
|
86
|
+
console.error(
|
|
87
|
+
`Run: mm-harness check diff [--fix] --artifacts-dir ${path.dirname(summaryPath)}`
|
|
88
|
+
);
|
|
89
|
+
console.error("Then rerun the same checklist mark command.");
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function teachRecipeCompletion(taskDir, markArgs) {
|
|
93
|
+
if (markArgs[0] !== "complete") return;
|
|
94
|
+
const artifactsDir = path.join(taskDir, "artifacts");
|
|
95
|
+
if (!fs.existsSync(path.join(artifactsDir, "recipe.json"))) return;
|
|
96
|
+
const qualityPath = path.join(artifactsDir, "recipe-quality.json");
|
|
97
|
+
const coveragePath = path.join(artifactsDir, "recipe-coverage.md");
|
|
98
|
+
const missingQuality = !fs.existsSync(qualityPath);
|
|
99
|
+
const missingCoverage = !fs.existsSync(coveragePath);
|
|
100
|
+
if (!missingQuality && !missingCoverage) return;
|
|
101
|
+
console.error("Recipe completion needs two closure artifacts; derive them from the real recipe run and evidence (do not stub them):");
|
|
102
|
+
if (missingCoverage) {
|
|
103
|
+
console.error(` - ${coveragePath}: map each proof target to its recipe node and evidence, or record a reasoned N/A.`);
|
|
104
|
+
}
|
|
105
|
+
if (missingQuality) {
|
|
106
|
+
const compactPath = path.join(artifactsDir, "recipe-quality-input.json");
|
|
107
|
+
console.error(` - ${qualityPath}: write ${compactPath} as:`);
|
|
108
|
+
console.error(' {"verdict":"pass|warn|fail","reasons":["evidence-backed reason"]}');
|
|
109
|
+
console.error(` then run: mm-harness recipe-quality build --input ${compactPath} --output ${qualityPath}`);
|
|
110
|
+
}
|
|
111
|
+
console.error("Then rerun the same checklist complete command.");
|
|
112
|
+
}
|
|
113
|
+
async function handleChecklist(argv) {
|
|
114
|
+
const [action, taskDir, ...markArgs] = argv;
|
|
115
|
+
if (action !== "mark" || !taskDir || markArgs.length === 0) {
|
|
116
|
+
console.error(`usage: ${USAGE}`);
|
|
117
|
+
return EXIT.usage;
|
|
118
|
+
}
|
|
119
|
+
const resolvedTaskDir = path.resolve(taskDir);
|
|
120
|
+
if (preserveTerminalSignal(resolvedTaskDir, markArgs)) return EXIT.ok;
|
|
121
|
+
if (!checkDiffGateReady(resolvedTaskDir, markArgs)) return EXIT.runtime;
|
|
122
|
+
teachRecipeCompletion(resolvedTaskDir, markArgs);
|
|
123
|
+
const result = spawnSync(
|
|
124
|
+
process.execPath,
|
|
125
|
+
[require2.resolve("@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"), resolvedTaskDir, ...markArgs],
|
|
126
|
+
{ stdio: "inherit" }
|
|
127
|
+
);
|
|
128
|
+
if (result.error) {
|
|
129
|
+
console.error(`mm-harness checklist mark: ${result.error.message}`);
|
|
130
|
+
return EXIT.runtime;
|
|
131
|
+
}
|
|
132
|
+
return result.status ?? EXIT.runtime;
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
handleChecklist
|
|
136
|
+
};
|
package/dist/commands/debug.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { runnerDir } from "../paths.js";
|
|
3
3
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
4
|
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
|
|
5
|
-
const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json"]);
|
|
5
|
+
const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json", "noOpen"]);
|
|
6
6
|
async function handleDebug(argv) {
|
|
7
7
|
const { options } = parseFlags(argv, DEBUG_BOOLEANS);
|
|
8
8
|
const json = flag(options, "json");
|
|
@@ -14,6 +14,7 @@ async function handleDebug(argv) {
|
|
|
14
14
|
surface.resolveSlotPorts(target);
|
|
15
15
|
const worker = flag(options, "worker");
|
|
16
16
|
const devMenu = flag(options, "devMenu");
|
|
17
|
+
const noOpen = flag(options, "noOpen");
|
|
17
18
|
if (adapter === "mobile" && worker) {
|
|
18
19
|
return usageOut(
|
|
19
20
|
json,
|
|
@@ -40,11 +41,21 @@ async function handleDebug(argv) {
|
|
|
40
41
|
debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
|
|
41
42
|
}
|
|
42
43
|
if (json) debugArgs.push("--json");
|
|
44
|
+
if (noOpen) debugArgs.push("--no-open");
|
|
43
45
|
const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
|
|
44
46
|
if (json) {
|
|
47
|
+
const leaf = parseDebugLeaf(result.output);
|
|
45
48
|
console.log(
|
|
46
49
|
JSON.stringify(
|
|
47
|
-
{
|
|
50
|
+
{
|
|
51
|
+
...leaf,
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
command: "debug",
|
|
54
|
+
adapter,
|
|
55
|
+
mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
|
|
56
|
+
opened: leaf.opened === true,
|
|
57
|
+
exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
|
|
58
|
+
},
|
|
48
59
|
null,
|
|
49
60
|
2
|
|
50
61
|
)
|
|
@@ -52,6 +63,14 @@ async function handleDebug(argv) {
|
|
|
52
63
|
}
|
|
53
64
|
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
54
65
|
}
|
|
66
|
+
function parseDebugLeaf(output) {
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(output.trim());
|
|
69
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
return output.trim() ? { detail: output.trim() } : {};
|
|
73
|
+
}
|
|
55
74
|
export {
|
|
56
75
|
handleDebug
|
|
57
76
|
};
|