@deeeed/metamask-harness 0.84.0 → 0.86.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 (75) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +5 -3
  3. package/adapters/extension/console-tail.mjs +11 -7
  4. package/adapters/extension/ensure-browser.sh +17 -18
  5. package/adapters/extension/refresh-build.sh +1 -1
  6. package/adapters/extension/start-watch.sh +17 -8
  7. package/adapters/manifest.json +1 -1
  8. package/adapters/mobile/reload-app.mjs +2 -1
  9. package/adapters/mobile/start-console-forwarder.sh +8 -0
  10. package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
  11. package/dist/adapters/console-capture.js +3 -1
  12. package/dist/adapters/extension/actions.js +5 -0
  13. package/dist/adapters/extension/console-capture.js +18 -0
  14. package/dist/adapters/extension/fixtures.js +2 -0
  15. package/dist/adapters/extension/overlay.js +5 -4
  16. package/dist/adapters/extension/runtime.js +2 -0
  17. package/dist/adapters/mobile/launch.js +19 -2
  18. package/dist/adapters/mobile/provision.js +4 -2
  19. package/dist/adapters/mobile/run.js +3 -2
  20. package/dist/adapters/mobile/verify.js +15 -7
  21. package/dist/adapters/web-dapp/actions.js +7 -5
  22. package/dist/adapters.js +5 -3
  23. package/dist/command-contract.js +32 -20
  24. package/dist/commands/check.js +5 -7
  25. package/dist/commands/debug.js +2 -2
  26. package/dist/commands/doctor.js +2 -3
  27. package/dist/commands/domain.js +4 -3
  28. package/dist/commands/fixtures.js +3 -3
  29. package/dist/commands/help.js +6 -5
  30. package/dist/commands/launch/extension.js +9 -7
  31. package/dist/commands/logs.js +2 -2
  32. package/dist/commands/prepare.js +4 -4
  33. package/dist/commands/provision.js +2 -2
  34. package/dist/commands/review.js +8 -10
  35. package/dist/commands/runtime-launch.js +2 -0
  36. package/dist/commands/status-probe.js +8 -1
  37. package/dist/commands/status.js +4 -2
  38. package/dist/commands/task-init.js +2 -3
  39. package/dist/console-classifier.js +3 -2
  40. package/dist/doctor.js +6 -2
  41. package/dist/mm-harness-cli.js +19 -6
  42. package/dist/paths.js +9 -4
  43. package/dist/recipe-security.js +1 -1
  44. package/dist/review/knowledge.js +16 -4
  45. package/dist/runner.js +18 -3
  46. package/docs/CONTRIBUTING.md +11 -0
  47. package/docs/QA.md +1 -0
  48. package/docs/RECIPES.md +18 -2
  49. package/docs/cli-contract.md +8 -6
  50. package/library/actions/core/perps/_controller.mjs +2 -2
  51. package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
  52. package/library/actions/extension/platform/cdp.mjs +72 -2
  53. package/library/actions/extension/ui/navigate.mjs +3 -1
  54. package/library/actions/extension/wallet/select_account.mjs +17 -17
  55. package/library/actions/extension/wallet/state.mjs +75 -21
  56. package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
  57. package/library/actions/mobile/wallet/select_account.mjs +34 -18
  58. package/library/actions/shared/console-assert.mjs +174 -0
  59. package/library/actions/shared/console-collector.mjs +10 -3
  60. package/library/actions/shared/console-findings.mjs +108 -7
  61. package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
  62. package/library/actions/web-dapp/platform/hud.mjs +2 -9
  63. package/library/actions/web-dapp/platform/page.mjs +62 -16
  64. package/library/actions/web-dapp/platform/runtime.mjs +1 -1
  65. package/library/diagnostics/console-allowlist.json +13 -0
  66. package/library/manifests/core.action-manifest.json +6 -6
  67. package/library/manifests/extension.action-manifest.json +37 -1
  68. package/library/manifests/mobile.action-manifest.json +40 -4
  69. package/library/manifests/web-dapp.action-manifest.json +1 -1
  70. package/library/recipes/core/health/check.recipe.json +40 -0
  71. package/library/recipes/extension/health/check.recipe.json +71 -0
  72. package/library/recipes/mobile/health/check.recipe.json +71 -0
  73. package/package.json +9 -9
  74. package/scripts/completions.sh +2 -2
  75. package/site/assets/help-recipes.json +3 -2
@@ -4,7 +4,7 @@ import crypto from "node:crypto";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { runnerDir } from "../../paths.js";
7
- const USAGE = "Usage: mm-harness verify --adapter mobile -- [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]";
7
+ const USAGE = "Usage: mm-harness verify --adapter mobile -- [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--watcher-port <port>] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]";
8
8
  const ENV_FILES = [".js.env", ".env", ".env.local"];
9
9
  const PREFLIGHT_MODES = ["fast", "auto", "default", "rebuild-native", "clean"];
10
10
  const AUTO_START_TRUE = ["1", "true", "TRUE", "True", "yes", "YES", "Yes", "on", "ON", "On"];
@@ -36,6 +36,7 @@ function parseMobileVerifyArgs(argv, env = process.env) {
36
36
  let platform = env.RECIPE_HARNESS_PLATFORM || env.PLATFORM || "ios";
37
37
  let preflightMode = env.RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE || "fast";
38
38
  let autoStart = env.RECIPE_HARNESS_MOBILE_AUTO_START ?? "false";
39
+ let watcherPort;
39
40
  for (let i = 0; i < argv.length; i += 1) {
40
41
  const arg = argv[i];
41
42
  const needsValue = () => {
@@ -65,6 +66,12 @@ function parseMobileVerifyArgs(argv, env = process.env) {
65
66
  platform = value;
66
67
  break;
67
68
  }
69
+ case "--watcher-port": {
70
+ const value = needsValue();
71
+ if (value === void 0) return { kind: "error", message: "Missing value for --watcher-port", exitCode: 2 };
72
+ watcherPort = value;
73
+ break;
74
+ }
68
75
  case "--preflight-mode": {
69
76
  const value = needsValue();
70
77
  if (value === void 0) return { kind: "error", message: "Missing value for --preflight-mode", exitCode: 2 };
@@ -101,7 +108,8 @@ function parseMobileVerifyArgs(argv, env = process.env) {
101
108
  staticOnly,
102
109
  platform,
103
110
  preflightMode,
104
- autoStart: AUTO_START_TRUE.includes(autoStart)
111
+ autoStart: AUTO_START_TRUE.includes(autoStart),
112
+ ...watcherPort === void 0 ? {} : { watcherPort }
105
113
  }
106
114
  };
107
115
  }
@@ -141,8 +149,8 @@ function buildMobileVerifySummary(input) {
141
149
  generatedAt: input.generatedAt
142
150
  };
143
151
  }
144
- function resolveWatcherPort(target, env = process.env) {
145
- const explicit = env.WATCHER_PORT || "";
152
+ function resolveWatcherPort(target, env = process.env, flag) {
153
+ const explicit = flag || env.RECIPE_WATCHER_PORT || env.WATCHER_PORT || "";
146
154
  if (explicit) return explicit;
147
155
  for (const file of ENV_FILES) {
148
156
  const full = path.join(target, file);
@@ -299,7 +307,7 @@ async function runMobileVerify(argv, io = defaultIo) {
299
307
  io.err(parsed.message);
300
308
  return parsed.exitCode;
301
309
  }
302
- const { staticOnly, platform, preflightMode, autoStart } = parsed.options;
310
+ const { staticOnly, platform, preflightMode, autoStart, watcherPort } = parsed.options;
303
311
  const target = path.resolve(parsed.options.target);
304
312
  if (!fs.existsSync(target)) {
305
313
  io.err(`Refusing mobile verify: target checkout does not exist: ${target}`);
@@ -344,9 +352,9 @@ async function runMobileVerify(argv, io = defaultIo) {
344
352
  let fixtureStatus = null;
345
353
  let portHolder = null;
346
354
  if (!staticOnly) {
347
- const port = resolveWatcherPort(target);
355
+ const port = resolveWatcherPort(target, process.env, watcherPort);
348
356
  if (!isNumericWatcherPort(port)) {
349
- io.err(`Refusing mobile verify: resolved watcher port is not numeric: '${port}' (check WATCHER_PORT)`);
357
+ io.err(`Refusing mobile verify: resolved watcher port is not numeric: '${port}' (check --watcher-port, RECIPE_WATCHER_PORT or WATCHER_PORT)`);
350
358
  return 2;
351
359
  }
352
360
  let deviceTarget = mobileDeviceTarget(target, platform, process.env);
@@ -9,7 +9,7 @@ const webDappActions = {
9
9
  "app.performance_assert"
10
10
  ],
11
11
  cdpTarget: { transport: "chrome-cdp", probePath: "json/version" },
12
- ui: (harness, { hudPolicy }) => ({
12
+ ui: (harness) => ({
13
13
  base: harness.createCdpWebUiTransport({
14
14
  async withPage(input, callback) {
15
15
  const { withAppPage } = await import("../../../library/actions/web-dapp/platform/page.mjs");
@@ -30,14 +30,16 @@ const webDappActions = {
30
30
  async execute(action, node, context, standard) {
31
31
  if (action === "app.hud") {
32
32
  const { showAppHud } = await import("../../../library/actions/web-dapp/platform/hud.mjs");
33
- return showAppHud(
34
- { action, node, context: { nodeId: context.nodeId, projectRoot: context.projectRoot } },
35
- { hudPolicy }
36
- );
33
+ return showAppHud({ action, node, context: { nodeId: context.nodeId, projectRoot: context.projectRoot } });
37
34
  }
38
35
  return standard();
39
36
  }
40
37
  }),
38
+ // Deletes hud.json (the wallet host redraws from it) and the HUD on the app tabs.
39
+ async clearHud(projectRoot) {
40
+ const { showAppHud } = await import("../../../library/actions/web-dapp/platform/hud.mjs");
41
+ await showAppHud({ node: { clear: true }, context: { nodeId: "hud-clear", projectRoot } });
42
+ },
41
43
  // ensure_positions and ensure_orders with an open or present state place an order.
42
44
  inputFindings: (nodeId, node) => perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: false })
43
45
  };
package/dist/adapters.js CHANGED
@@ -90,7 +90,9 @@ const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
90
90
  "app.network_capture",
91
91
  "app.network_assert",
92
92
  "app.performance_capture",
93
- "app.performance_assert"
93
+ "app.performance_assert",
94
+ // Proves a capture of the running app; a static result would pass on no evidence.
95
+ "metamask.app.assert_no_console_errors"
94
96
  ]);
95
97
  function requiresLiveAdapter(platform, action) {
96
98
  return LIVE_ONLY_ACTIONS.has(action) || Boolean(getAdapterSurface(platform).actions.liveOnly?.includes(action)) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_FEATURE_FLAG_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
@@ -290,7 +292,7 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
290
292
  )
291
293
  );
292
294
  }
293
- function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters, { hudPolicy = "auto" } = {}) {
295
+ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
294
296
  const ui = getAdapterSurface(platform).actions.ui;
295
297
  if (!ui) {
296
298
  return {
@@ -301,7 +303,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters, { hu
301
303
  }
302
304
  };
303
305
  }
304
- const { base, execute } = ui(harness, { hudPolicy });
306
+ const { base, execute } = ui(harness, { hudPolicy: "show" });
305
307
  async function standard(action, node, context) {
306
308
  if (action === "ui.navigate") {
307
309
  const live = await runLiveFirst(
@@ -19,6 +19,12 @@ const adapterIds = (tokens) => adapterChoices(optionValues(tokens, "--library"),
19
19
  env: operatorLibraryEnv()
20
20
  });
21
21
  const ADAPTER = { "--adapter": value(adapterIds) };
22
+ function selectableAdapters(libraries = []) {
23
+ return adapterIds(libraries.flatMap((entry) => ["--library", entry]));
24
+ }
25
+ function withAdapterList(text, libraries = []) {
26
+ return text.replaceAll("{adapters}", selectableAdapters(libraries).join("|"));
27
+ }
22
28
  const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
23
29
  const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value((tokens) => [...adapterIds(tokens), "ios", "android"]) };
24
30
  const DEVICE = { "--device": value() };
@@ -88,7 +94,7 @@ const PUBLIC_COMMANDS = [
88
94
 
89
95
  Loads the complete recipe guide shipped with this harness.
90
96
 
91
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
97
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
92
98
  --target <path> Checkout path (default: cwd)
93
99
  --json Load the same guide as structured agent context
94
100
 
@@ -289,6 +295,7 @@ Example:
289
295
  Home dashboard for the current checkout \u2014 the detected adapter, the next command
290
296
  to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
291
297
 
298
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
292
299
  --target <path> Checkout path (default: cwd)
293
300
  --json Machine-readable envelope { adapter, target, view, devices[], next }
294
301
  --fast Skip all live-state probes (instant output, safe for scripts)
@@ -334,7 +341,7 @@ Example:
334
341
  mm-harness status --fast
335
342
  mm-harness status --watch`,
336
343
  contract: {
337
- options: options(HELP, JSON, TARGET, DEVICE, {
344
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
338
345
  "--fast": bool(),
339
346
  "--all-devices": bool(),
340
347
  "--watch": bool(),
@@ -429,7 +436,7 @@ Example:
429
436
  refuses with the numbered choice when that is ambiguous.
430
437
 
431
438
  --target <path> Checkout path (default: cwd)
432
- --platform <mobile|extension|core|terminal> Force the platform (auto-detected inside a checkout)
439
+ --platform <{adapters}> Force the platform (auto-detected inside a checkout)
433
440
  --mobile-platform <ios|android> Mobile target when status cannot decide it
434
441
  --clear-metro Mobile only: clear Metro's transform cache in the launch step
435
442
  --artifacts-dir <dir> Where sandbox.json and the per-step reports land
@@ -633,7 +640,7 @@ Materialize:
633
640
  --matrix Compare action availability across Mobile, Extension, Core, and Terminal
634
641
  --categories List compact action categories and counts
635
642
  --category <name> List only one category (for example ui, wallet, or perps)
636
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
643
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
637
644
  --target <path> Checkout path (default: cwd)
638
645
  --raw Dump raw action registry JSON
639
646
  --json Machine-readable output
@@ -674,7 +681,7 @@ Example:
674
681
  core headless \u2014 no dev server to stop (teaching error)
675
682
 
676
683
  --port <port> Metro/webpack port override; Extension CDP remains checkout-context scoped
677
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
684
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
678
685
  --target <path> Checkout path (default: cwd)
679
686
  --json Machine-readable output
680
687
 
@@ -707,19 +714,21 @@ Example:
707
714
  --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
708
715
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
709
716
  --cdp-port <port> Extension only: bind the action to this live browser runtime.
710
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
717
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
711
718
  --target <path> Checkout path (default: cwd)
712
719
  --artifacts-dir <dir> Where to write evidence (default: <task>/artifacts/calls/<action>-<id>
713
720
  inside a task, else temp/recipe/calls/<action>-<id>)
714
721
  --action-manifest <path> Override the action manifest
715
722
  --library <name=path> Add/override a recipe-library source (repeatable)
716
723
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
724
+ --hud <show|hide> Recipe HUD: the call's intent= drawn over the app, headless included (default: show)
717
725
  --json Machine-readable output
718
726
 
719
727
  Example (real actions; run mm-harness actions for this checkout's full set):
720
728
  mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
721
729
  mm-harness call navigate page=perps --adapter mobile
722
730
  mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)
731
+ mm-harness call ui.screenshot intent="Show the order filled" # intent= is the HUD text
723
732
 
724
733
  Remote feature flags (mobile applies at runtime; extension pins at launch):
725
734
  mm-harness call metamask.feature_flags.read --adapter mobile
@@ -731,7 +740,8 @@ Remote feature flags (mobile applies at runtime; extension pins at launch):
731
740
  "--list": bool(),
732
741
  "--domain": value(),
733
742
  "--source": value(),
734
- "--sort": value(["domain", "name", "library"])
743
+ "--sort": value(["domain", "name", "library"]),
744
+ "--hud": value(["show", "hide"])
735
745
  }),
736
746
  positionals: [{ label: "action" }],
737
747
  minimumPositionals: 0,
@@ -765,7 +775,7 @@ Discovery:
765
775
 
766
776
  Common overrides:
767
777
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
768
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
778
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
769
779
  --platform <ios|android> Mobile platform preference; use --adapter for the product adapter
770
780
  --target <path> Checkout path (default: cwd)
771
781
  --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
@@ -778,7 +788,7 @@ Common overrides:
778
788
  --json Machine-readable output
779
789
  --json-stream Line-flushed JSONL progress + terminal event
780
790
  --record-video=full-run Record a video of the run
781
- --hud <auto|show|hide> Recipe HUD policy (auto shows normal Mobile recipes and a headful terminal browser; hide is explicit)
791
+ --hud <show|hide> Recipe HUD: each node's intent drawn over the app on every adapter, headless included (default: show)
782
792
 
783
793
  Advanced integrations:
784
794
  runtime: --cdp-port, --watcher-port, --runtime-dir, --slot,
@@ -801,7 +811,7 @@ Example:
801
811
  "--plan": bool(),
802
812
  "--proof": bool(),
803
813
  "--describe": bool(),
804
- "--hud": value(["auto", "show", "hide"])
814
+ "--hud": value(["show", "hide"])
805
815
  }),
806
816
  positionals: [{ label: "recipe" }],
807
817
  minimumPositionals: 1,
@@ -850,7 +860,7 @@ Example:
850
860
  --cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
851
861
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
852
862
  --all-devices Mobile only: show every connected device instead of the slot-scoped target
853
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
863
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
854
864
  --target <path> Checkout path (default: cwd)
855
865
  --runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
856
866
  --json Machine-readable output (incompatible with --print-ready)
@@ -882,7 +892,7 @@ Example:
882
892
  when available. Does not launch an app or run a recipe.
883
893
 
884
894
  --target <path> Checkout path (default: cwd)
885
- --adapter <mobile|extension|core|terminal> Adapter label (auto-detected when possible)
895
+ --adapter <{adapters}> Adapter label (auto-detected when possible)
886
896
  --base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
887
897
  --profile <fast|full> Validation depth (default: fast)
888
898
  --fix Fix changed files, then validate them
@@ -929,11 +939,11 @@ Example:
929
939
  Optional advice:
930
940
  advisor-status --json
931
941
  Resolve saved settings and overrides without reading tasks or calling a model.
932
- plan --task <task.json> --adapter <mobile|extension|core|terminal> --advisor typesafe
942
+ plan --task <task.json> --adapter <{adapters}> --advisor typesafe
933
943
  Assess each acceptance criterion before a recipe exists. Task JSON contains
934
944
  acceptanceCriteria: ["criterion", "..."] (1\u201320 entries), with optional title/description.
935
945
  Returns known candidate refs for reuse/composition, or an explicit discovery gap.
936
- advise --recipe <recipe.json> --task <TASK.md> --adapter <mobile|extension|core|terminal>
946
+ advise --recipe <recipe.json> --task <TASK.md> --adapter <{adapters}>
937
947
  --advisor <provider|off> Override saved policy for this task (currently: typesafe)
938
948
  --advisor-model <model> Override the provider default model or alias
939
949
  --advisor-stages <csv> Select plan,draft,evidence (saved/default: plan)
@@ -983,7 +993,7 @@ Example:
983
993
  provisioning path only: artifact cache + simulator create + simctl install.
984
994
  JavaScript dependencies and Metro remain dispatch-time launch concerns.
985
995
 
986
- --adapter <mobile|extension|core|terminal> Target adapter (mobile supported; extension/core/terminal teach)
996
+ --adapter <{adapters}> Target adapter (mobile supported; the others teach)
987
997
  --target <path> Slot checkout path (default: cwd)
988
998
  --platform <ios> Platform (default ios)
989
999
  --simulator <name|udid> Override agentic-runtime.json simulator (alias: --device)
@@ -1027,7 +1037,7 @@ Example:
1027
1037
  artifact cache + simulator create + simctl install. JavaScript dependencies
1028
1038
  and Metro remain dispatch-time launch concerns.
1029
1039
 
1030
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1040
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1031
1041
  --target <path> Checkout path (default: cwd)
1032
1042
  --runway Thin mobile Runway artifact install instead of overlay install
1033
1043
  --platform <ios> Runway platform (default ios)
@@ -1062,7 +1072,7 @@ Example:
1062
1072
  Extension live checks infer the checkout CDP port from runtime context,
1063
1073
  pool, or slot suffix when --cdp-port is omitted.
1064
1074
 
1065
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1075
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1066
1076
  --target <path> Checkout path (default: cwd)
1067
1077
  --json Machine-readable output
1068
1078
 
@@ -1082,7 +1092,7 @@ Example:
1082
1092
 
1083
1093
  Remove the installed overlay and restore the checkout.
1084
1094
 
1085
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1095
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1086
1096
  --target <path> Checkout path (default: cwd)
1087
1097
 
1088
1098
  Example:
@@ -1235,7 +1245,7 @@ Example:
1235
1245
  --source <label> Log source per adapter \u2014 mobile: metro|app;
1236
1246
  extension: extension|dapp|webpack|watcher|rebuild.
1237
1247
  Core is headless (teaching error).
1238
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1248
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1239
1249
  --target <path> Checkout path (default: cwd)
1240
1250
  --json Machine-readable output
1241
1251
 
@@ -1458,5 +1468,7 @@ export {
1458
1468
  HIDDEN_COMMANDS,
1459
1469
  PUBLIC_COMMANDS,
1460
1470
  REPLACED_OPTIONS,
1461
- explainMissingAdapterLibrary
1471
+ explainMissingAdapterLibrary,
1472
+ selectableAdapters,
1473
+ withAdapterList
1462
1474
  };
@@ -1,8 +1,7 @@
1
- import { detectAdapter, EXIT, usageOut, optionFlag, optionString, parseArgs, shellQuote } from "@farmslot/recipe-cli/harness";
1
+ import { EXIT, contextAdapter, usageOut, optionFlag, optionString, parseArgs, shellQuote } from "@farmslot/recipe-cli/harness";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import "../adapters/surface.js";
6
5
  const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter <adapter>] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
7
6
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
8
7
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
@@ -20,12 +19,11 @@ async function handleCheck(argv) {
20
19
  return usageOut(json, "check", message, USAGE);
21
20
  }
22
21
  const target = path.resolve(optionString(options, "target") ?? process.cwd());
23
- const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
24
- const detected = explicitAdapter ?? detectAdapter(target);
25
- if (detected === "ios" || detected === "android") {
26
- return usageOut(json, "check", `unsupported adapter '${detected}'`, USAGE);
22
+ const platform = optionString(options, "adapter") ? void 0 : optionString(options, "platform");
23
+ if (platform === "ios" || platform === "android") {
24
+ return usageOut(json, "check", `unsupported adapter '${platform}'`, USAGE);
27
25
  }
28
- const adapter = detected;
26
+ const adapter = contextAdapter(target);
29
27
  const profileInput = optionString(options, "profile");
30
28
  if (profileInput && profileInput !== "fast" && profileInput !== "full") {
31
29
  return usageOut(json, "check", "--profile must be fast or full", USAGE);
@@ -1,4 +1,4 @@
1
- import { adapterDetectNext, EXIT, flag, parseFlags, resolveFlagsAdapter, spawnScript, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
1
+ import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnScript, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
2
2
  import path from "node:path";
3
3
  import { runnerDir } from "../paths.js";
4
4
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -7,7 +7,7 @@ async function handleDebug(argv) {
7
7
  const { options } = parseFlags(argv, DEBUG_BOOLEANS);
8
8
  const json = flag(options, "json");
9
9
  const target = targetOf(options);
10
- const adapter = resolveFlagsAdapter(options, target);
10
+ const adapter = contextAdapter(target);
11
11
  if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
12
12
  const surface = getAdapterSurface(adapter);
13
13
  if (surface.headless) return usageOut(json, "debug", `${adapter} is headless; there is no debug console.`, surface.hints.relaunch);
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { featureFlagReport } from "../feature-flags.js";
5
- import { adapterPluginChecks, color, acquireCheckoutLock, detectAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, targetPath } from "@farmslot/recipe-cli/harness";
5
+ import { adapterPluginChecks, color, acquireCheckoutLock, contextAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, targetPath } from "@farmslot/recipe-cli/harness";
6
6
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
7
7
  import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
8
8
  import { assertAdapter, runnerDir } from "../paths.js";
@@ -33,8 +33,7 @@ async function handleDoctor({ options }) {
33
33
  const expectLive = optionFlag(options, "expectLive") || printReady;
34
34
  const allDevices = optionFlag(options, "allDevices");
35
35
  const platformOption = optionString(options, "platform");
36
- const explicitAdapter = optionString(options, "adapter") ?? (platformOption === "ios" || platformOption === "android" ? "mobile" : platformOption);
37
- const adapter = explicitAdapter ?? detectAdapter(target);
36
+ const adapter = contextAdapter(target);
38
37
  if (!adapter) {
39
38
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
40
39
  }
@@ -4,7 +4,8 @@ import {
4
4
  classifyDomain,
5
5
  configPath,
6
6
  discoverLibraries,
7
- readConfig
7
+ readConfig,
8
+ REVIEW_ADAPTERS
8
9
  } from "../review/knowledge.js";
9
10
  import { adapterFromOptions } from "./review.js";
10
11
  async function handleDomain(argv) {
@@ -12,14 +13,14 @@ async function handleDomain(argv) {
12
13
  const target = targetPath(parsed.options);
13
14
  const json = optionFlag(parsed.options, "json");
14
15
  const declared = optionString(parsed.options, "domain") ?? process.env.MM_HARNESS_DOMAIN;
15
- const detected = adapterFromOptions(parsed.options, target);
16
+ const detected = adapterFromOptions();
16
17
  const adapter = detected === "unscoped" ? void 0 : detected;
17
18
  if (declared) {
18
19
  emit(json, { domain: declared, source: "declared", adapter: adapter ?? null, target });
19
20
  return 0;
20
21
  }
21
22
  if (!adapter) {
22
- throw usageError("No MetaMask checkout detected: pass --adapter <mobile|extension|core|terminal> or --domain <name>.");
23
+ throw usageError(`No MetaMask checkout detected: pass --adapter <${REVIEW_ADAPTERS.join("|")}> or --domain <name>.`);
23
24
  }
24
25
  const config = readConfig(configPath());
25
26
  const libraries = discoverLibraries({ target, config });
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { runnerDir, walletFixturePath } from "../paths.js";
5
5
  import { getAdapterSurface } from "../adapters/surface.js";
6
6
  import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
7
- import { acquireCheckoutLock, adapterDetectNext, checkoutBusyOut, EXIT, flag, parseFlags, resolveFlagsAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
7
+ import { acquireCheckoutLock, adapterDetectNext, checkoutBusyOut, EXIT, flag, parseFlags, contextAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
8
8
  import { applyDeviceTargeting } from "./device-target.js";
9
9
  import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec } from "../feature-flags.js";
10
10
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
@@ -38,7 +38,7 @@ function fixturesStatus(options) {
38
38
  );
39
39
  }
40
40
  const target = targetOf(options);
41
- const adapter = resolveFlagsAdapter(options, target);
41
+ const adapter = contextAdapter(target);
42
42
  if (!adapter) {
43
43
  return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
44
44
  }
@@ -114,7 +114,7 @@ async function handleFixturesLocked(argv) {
114
114
  );
115
115
  }
116
116
  const target = targetOf(options);
117
- const adapter = resolveFlagsAdapter(options, target);
117
+ const adapter = contextAdapter(target);
118
118
  if (!adapter) {
119
119
  return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
120
120
  }
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { color, detectAdapter, optionString, parseArgs, targetPath } from "@farmslot/recipe-cli/harness";
3
+ import { color, contextAdapter, parseArgs, targetPath } from "@farmslot/recipe-cli/harness";
4
+ import { selectableAdapters } from "../command-contract.js";
4
5
  import { handleHelpReview } from "./review.js";
5
6
  import "../adapters/surface.js";
6
- const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core|terminal>";
7
+ const unscopedNext = () => `cd into a MetaMask checkout or pass --adapter <${selectableAdapters().join("|")}>`;
7
8
  function validHelpLine(value) {
8
9
  return typeof value === "string" || Boolean(value && typeof value.text === "string" && Array.isArray(value.adapters) && value.adapters.length > 0 && value.adapters.every((adapter) => ["mobile", "extension", "core", "web-dapp"].includes(adapter)));
9
10
  }
@@ -35,7 +36,7 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
35
36
  harnessVersion,
36
37
  recipeProtocolVersion: topic.recipeProtocolVersion,
37
38
  adapter,
38
- ...adapter === "unscoped" ? { next: UNSCOPED_NEXT } : {},
39
+ ...adapter === "unscoped" ? { next: unscopedNext() } : {},
39
40
  target,
40
41
  topic: topic.topic,
41
42
  title: topic.title,
@@ -58,7 +59,7 @@ function renderRecipeHelp(topic, harnessVersion, adapter) {
58
59
  paint("warn", topic.scopeNotice),
59
60
  `Visual tutorial: ${paint("cmd", "mm-harness tutorial")}`
60
61
  ];
61
- if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`));
62
+ if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${unscopedNext()}.`));
62
63
  for (const [index, section] of scopedSections(topic, adapter).entries()) {
63
64
  lines.push("", paint("label", `${index + 1}. ${section.title}`), ` ${section.instruction}`);
64
65
  for (const detail of section.details) lines.push(` - ${detail}`);
@@ -72,7 +73,7 @@ function handleHelp(argv, context) {
72
73
  const parsed = parseArgs(argv);
73
74
  if (parsed.positional[0] === "review") return handleHelpReview(argv, context.harnessVersion);
74
75
  const target = targetPath(parsed.options);
75
- const adapter = optionString(parsed.options, "adapter") ?? detectAdapter(target) ?? "unscoped";
76
+ const adapter = contextAdapter(target) ?? "unscoped";
76
77
  const topic = loadRecipeHelp(context.packageRoot);
77
78
  const payload = recipeHelpPayload(topic, context.harnessVersion, adapter, target);
78
79
  if (parsed.options.json === true) {
@@ -24,6 +24,7 @@ import {
24
24
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
25
25
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
26
26
  import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
27
+ import { recordExtensionConsoleLaunch } from "../../adapters/extension/console-capture.js";
27
28
  import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
28
29
  import { sameFeatureFlags } from "../../feature-flags.js";
29
30
  const { CdpSession } = await importRecipeHarnessRuntimeCdp();
@@ -108,6 +109,14 @@ Next: ${block.userAction}`
108
109
  }
109
110
  await ensureHarnessFresh(target, "extension");
110
111
  restoreRequestedPorts();
112
+ if (!reusable && artifactLaunchable && remoteFlags) {
113
+ return {
114
+ status: EXIT.infra,
115
+ output: `REMOTE_FLAG_NOT_PINNABLE: the remote feature-flag pin '${remoteFlags.spec}' (from --remote-flag and the wallet fixture's remoteFeatureFlags) cannot be applied to a verified release artifact; the artifact's manifest is signed evidence and is never rewritten.
116
+ Next: drop --remote-flag and remove remoteFeatureFlags from the wallet fixture to validate the artifact as shipped, or validate the variant on a source checkout launched without the artifact.`
117
+ };
118
+ }
119
+ recordExtensionConsoleLaunch(target);
111
120
  if (wantWatch) {
112
121
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
113
122
  const watchArgs = ["--target", target, "--runner-bin", path.join(runnerDir, "bin/mm-harness")];
@@ -128,13 +137,6 @@ Next: ${block.userAction}`
128
137
  return extensionReattach(target, displayMode);
129
138
  }
130
139
  if (artifactLaunchable) {
131
- if (remoteFlags) {
132
- return {
133
- status: EXIT.infra,
134
- output: `REMOTE_FLAG_NOT_PINNABLE: the remote feature-flag pin '${remoteFlags.spec}' (from --remote-flag and the wallet fixture's remoteFeatureFlags) cannot be applied to a verified release artifact; the artifact's manifest is signed evidence and is never rewritten.
135
- Next: drop --remote-flag and remove remoteFeatureFlags from the wallet fixture to validate the artifact as shipped, or validate the variant on a source checkout launched without the artifact.`
136
- };
137
- }
138
140
  return extensionLaunchReleaseArtifact(target, artifact);
139
141
  }
140
142
  return extensionLaunchDevelopment(target, remoteFlags);
@@ -1,4 +1,4 @@
1
- import { adapterDetectNext, EXIT, flag, parseFlags, resolveFlagsAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
1
+ import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { runnerDir } from "../paths.js";
@@ -8,7 +8,7 @@ async function handleLogs(argv) {
8
8
  const { options } = parseFlags(argv, LOGS_BOOLEANS);
9
9
  const json = flag(options, "json");
10
10
  const target = targetOf(options);
11
- const adapter = resolveFlagsAdapter(options, target);
11
+ const adapter = contextAdapter(target);
12
12
  if (!adapter) {
13
13
  return usageOut(json, "logs", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
14
14
  }
@@ -1,4 +1,4 @@
1
- import { acquireCheckoutLock, trackCheckoutChild, recordCommandStage, color, stripAnsi, harnessExecutable, recipeRuntimeDir, detectAdapter, optionFlag, optionString, parseArgs, targetPath, usageError, EXIT } from "@farmslot/recipe-cli/harness";
1
+ import { acquireCheckoutLock, trackCheckoutChild, recordCommandStage, color, stripAnsi, harnessExecutable, recipeRuntimeDir, contextAdapter, optionFlag, optionString, parseArgs, targetPath, usageError, EXIT } from "@farmslot/recipe-cli/harness";
2
2
  import { spawn } from "node:child_process";
3
3
  import { OperationOutputTail } from "@farmslot/recipe-runner/runtime/operation";
4
4
  import fs from "node:fs";
@@ -265,8 +265,8 @@ function mobilePlatformOption(value) {
265
265
  Next: ${USAGE}`
266
266
  );
267
267
  }
268
- function resolvePlatform(requested, target) {
269
- const platform = requested ?? detectAdapter(target);
268
+ function resolvePlatform(target) {
269
+ const platform = contextAdapter(target);
270
270
  const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
271
271
  if (surface?.prepare) {
272
272
  return surface;
@@ -296,7 +296,7 @@ async function handlePrepareLocked(argv) {
296
296
  const { options } = parseArgs(argv);
297
297
  const json = optionFlag(options, "json");
298
298
  const target = targetPath(options);
299
- const surface = resolvePlatform(optionString(options, "platform"), target);
299
+ const surface = resolvePlatform(target);
300
300
  const platform = surface.id;
301
301
  const artifactsDir = path.resolve(
302
302
  optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
@@ -1,11 +1,11 @@
1
- import { detectAdapter, adapterDetectNext, usageOut, applyWatcherPortOption, optionFlag, optionString, shellQuote, shellQuoteArg, targetPath } from "@farmslot/recipe-cli/harness";
1
+ import { contextAdapter, adapterDetectNext, usageOut, applyWatcherPortOption, optionFlag, optionString, shellQuote, shellQuoteArg, targetPath } from "@farmslot/recipe-cli/harness";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
4
  async function handleProvision({ positional, options, rawArgv }) {
5
5
  applyWatcherPortOption(options);
6
6
  const json = optionFlag(options, "json");
7
7
  const target = targetPath(options);
8
- const adapter = optionString(options, "adapter") ?? detectAdapter(target);
8
+ const adapter = contextAdapter(target);
9
9
  if (!adapter) {
10
10
  return usageOut(json, "provision", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
11
11
  }
@@ -1,4 +1,4 @@
1
- import { detectAdapter, optionFlag, optionString, parseArgs, targetPath, usageError } from "@farmslot/recipe-cli/harness";
1
+ import { harnessContext, optionFlag, optionString, parseArgs, targetPath, usageError } from "@farmslot/recipe-cli/harness";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { runnerDir } from "../paths.js";
@@ -15,7 +15,6 @@ import {
15
15
  resolveReference,
16
16
  defaultBaseRef
17
17
  } from "../review/knowledge.js";
18
- import "../adapters/surface.js";
19
18
  const EXIT_OK = 0;
20
19
  let cachedHarnessVersion;
21
20
  function harnessVersion() {
@@ -25,18 +24,17 @@ function harnessVersion() {
25
24
  }
26
25
  return cachedHarnessVersion;
27
26
  }
28
- function adapterFromOptions(options, target) {
29
- const raw = optionString(options, "adapter") ?? optionString(options, "platform");
30
- if (raw !== void 0) {
31
- if (raw !== "mobile" && raw !== "extension" && raw !== "core" && raw !== "terminal") throw usageError(`--adapter must be mobile, extension, core, or terminal (got ${raw}).`);
32
- return raw;
27
+ function adapterFromOptions() {
28
+ const adapter = harnessContext()?.adapter;
29
+ const scope = REVIEW_ADAPTERS.find((id) => id === adapter?.value);
30
+ if (adapter?.source === "flag" && !scope) {
31
+ throw usageError(`--adapter must be one of ${REVIEW_ADAPTERS.join(", ")} (got ${adapter.value}).`);
33
32
  }
34
- const detected = detectAdapter(target);
35
- return REVIEW_ADAPTERS.find((adapter) => adapter === detected) ?? "unscoped";
33
+ return scope ?? "unscoped";
36
34
  }
37
35
  function resolveReviewContext(options, { fetch = true } = {}) {
38
36
  const target = targetPath(options);
39
- const adapter = adapterFromOptions(options, target);
37
+ const adapter = adapterFromOptions();
40
38
  const domain = optionString(options, "domain") ?? process.env.MM_HARNESS_DOMAIN;
41
39
  const config = readConfig(configPath());
42
40
  const context = { target, adapter, ...domain ? { domain } : {} };