@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +81 -6
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +4 -2
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +73 -34
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +21 -10
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +3 -3
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +10 -4
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +2 -2
  17. package/adapters/extension/stop-viewers.sh +0 -0
  18. package/adapters/extension/verify.sh +19 -2
  19. package/adapters/extension/wallet-fixture-state.cjs +14 -1
  20. package/adapters/manifest.json +19 -3
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  22. package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
  23. package/adapters/mobile/cleanup.sh +0 -0
  24. package/adapters/mobile/inject.sh +0 -0
  25. package/adapters/mobile/launch-metro.cjs +74 -0
  26. package/adapters/mobile/lib/metro-listener.sh +0 -0
  27. package/adapters/mobile/lib/tmux-viewer.sh +4 -4
  28. package/adapters/mobile/open-device.sh +3 -1
  29. package/adapters/mobile/prewarm-bundle.sh +0 -0
  30. package/adapters/mobile/start-metro.sh +12 -18
  31. package/adapters/mobile/stop-metro.sh +1 -0
  32. package/adapters/mobile/verify.sh +5 -5
  33. package/adapters/mobile/wait-for-bridge.sh +0 -0
  34. package/adapters/mobile/yarn-setup.sh +0 -0
  35. package/adapters/shared/activate-repo-node.sh +0 -0
  36. package/adapters/shared/activate-repo-ruby.sh +0 -0
  37. package/adapters/shared/cli-ux.sh +0 -0
  38. package/adapters/shared/ensure-runner-deps.sh +0 -0
  39. package/adapters/shared/harness-path.sh +0 -0
  40. package/adapters/shared/hash-helpers.sh +0 -0
  41. package/adapters/shared/install-repo-deps.sh +29 -0
  42. package/adapters/shared/json-field.sh +0 -0
  43. package/adapters/shared/open-debug.mjs +35 -4
  44. package/adapters/shared/open-log-window.sh +1 -1
  45. package/adapters/shared/reap-checkout-metros.sh +0 -0
  46. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  47. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  48. package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
  49. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  50. package/adapters/shared/resolve-slot-ports.sh +21 -17
  51. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  52. package/adapters/shared/tmux-session.sh +0 -5
  53. package/adapters/shared/tmux-viewer.sh +7 -2
  54. package/dist/adapters/core/surface.js +3 -0
  55. package/dist/adapters/extension/ensure-ready.js +0 -7
  56. package/dist/adapters/extension/runtime-decision.js +93 -3
  57. package/dist/adapters/extension/surface.js +17 -6
  58. package/dist/adapters/mobile/metro-env.js +67 -0
  59. package/dist/adapters/mobile/perps-env.js +101 -0
  60. package/dist/adapters/mobile/prepare.js +32 -14
  61. package/dist/adapters/mobile/runtime-decision.js +21 -2
  62. package/dist/adapters/mobile/source-freshness.js +116 -0
  63. package/dist/adapters/mobile/surface.js +3 -0
  64. package/dist/adapters/resolve-slot-ports.js +4 -0
  65. package/dist/adapters/slot-ports.js +131 -49
  66. package/dist/adapters.js +7 -2
  67. package/dist/checkout-lock.js +72 -0
  68. package/dist/cli.js +2 -0
  69. package/dist/commands/call.js +91 -54
  70. package/dist/commands/check.js +266 -46
  71. package/dist/commands/checklist.js +136 -0
  72. package/dist/commands/debug.js +21 -2
  73. package/dist/commands/doctor.js +187 -12
  74. package/dist/commands/fixtures.js +177 -42
  75. package/dist/commands/launch/extension.js +6 -1
  76. package/dist/commands/launch/index.js +54 -22
  77. package/dist/commands/logs.js +24 -4
  78. package/dist/commands/run-engine.js +223 -7
  79. package/dist/commands/run.js +67 -50
  80. package/dist/commands/shared.js +86 -5
  81. package/dist/commands/stop.js +1 -2
  82. package/dist/doctor.js +39 -17
  83. package/dist/harness.js +5 -2
  84. package/dist/heal-bounds.js +1 -1
  85. package/dist/mm-harness-cli.js +37 -9
  86. package/dist/runner.js +35 -5
  87. package/dist/runtime-context.js +228 -0
  88. package/docs/CLI-SPEC.md +15 -11
  89. package/docs/MENTAL-MODEL.md +6 -6
  90. package/library/actions/extension/perps/perps.mjs +29 -8
  91. package/library/actions/extension/platform/cdp.mjs +128 -28
  92. package/library/actions/extension/ui/navigate.mjs +1 -1
  93. package/library/actions/mobile/perps/perps.mjs +13 -1
  94. package/library/actions/mobile/platform/bridge.mjs +302 -29
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
  96. package/library/manifests/extension.action-manifest.json +32 -12
  97. package/library/manifests/mobile.action-manifest.json +25 -3
  98. package/package.json +5 -5
  99. package/scripts/completions.sh +7 -4
  100. package/scripts/install-completions.sh +0 -0
@@ -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 checks = [];
55
- checks.push(
56
- runToolCheck({
57
- id: "eslint",
58
- label: "Changed-file ESLint",
59
- target,
60
- artifactsDir,
61
- files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
62
- command: executable(target, "eslint"),
63
- args: ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache"), "--max-warnings=0", "--no-warn-ignored"]
64
- })
65
- );
66
- checks.push(
67
- runToolCheck({
68
- id: "prettier",
69
- label: "Changed-file Prettier",
70
- target,
71
- artifactsDir,
72
- files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)),
73
- command: executable(target, "prettier"),
74
- args: ["--check"]
75
- })
76
- );
77
- checks.push(
78
- runToolCheck({
79
- id: "jest",
80
- label: "Changed test files",
81
- target,
82
- artifactsDir,
83
- files: existingChangedFiles.filter((file) => TEST_FILE_RE.test(file)),
84
- command: executable(target, "jest"),
85
- args: ["--no-coverage"]
86
- })
87
- );
88
- if (profile === "full") {
89
- checks.push(runTypecheck({ target, artifactsDir, packageJson }));
90
- } else {
91
- checks.push({
92
- id: "typecheck",
93
- label: "TypeScript project check",
94
- status: "skip",
95
- reason: "profile=fast; run with --profile full for repo-wide typecheck"
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
+ };
@@ -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
- { schemaVersion: 1, command: "debug", adapter, mode: worker ? "worker" : devMenu ? "dev-menu" : "default", exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
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
  };