@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.15.0 - 2026-07-13
6
+
7
+ ### Changed
8
+
9
+ - Extension `status` and `doctor` now report the webpack dev server as `up` only when a target-owned watcher is alive; historical successful build logs no longer imply liveness after `stop`.
10
+ - Mobile overlay freshness no longer reports `pass` when every checkable runner overlay file is absent from the target; missing or drifted files are classified before the empty-overlay success case.
11
+ - Mobile live verification now compares AgenticService/HUD freshness against the canonical `app/dev-tools/AgenticService` runner and product paths instead of skipping the check through the obsolete `app/core/AgenticService` layout.
12
+ - `check diff` now rejects only newly added ESLint directive comments before running repository tools, without treating prose or string literals containing `eslint-disable` as suppressions; pre-existing suppressions remain untouched.
13
+ - Mobile `run`/`call` now fingerprints bundle-affecting source and environment state against the last successful app reload; default infra healing performs one `app.lifecycle restart` before proof when the loaded source is missing or stale, while `--heal off` stops with the exact recovery command.
14
+ - Mobile `metamask.perps.start_state` now restores the fixture-backed unlocked wallet before navigation, so required post-edit app restarts do not race a locked/initializing runtime.
15
+ - Mobile `ui.wait_for` discovery now exposes its `test_id`, `expected`, text, and timeout schema plus an `expected=absent` example; shared recipe examples use the same supported `expected` field instead of the ignored `visibility` spelling.
16
+ - Mobile fast-preflight dependency failures now present numbered standalone vs orchestrated recovery choices, so interactive agents can relay a portable one-character choice instead of inventing an option from an unnumbered `Next:` hint.
17
+ - `checklist mark` now blocks a `mm-harness check diff` gate until that task's own `artifacts/validation-summary.json` records a real diff-check PASS, preventing stale or failed validation from being marked complete.
18
+ - Changed-file ESLint now respects the repository's configured warning severity instead of failing unchanged warnings via `--max-warnings=0`; real failures tell agents at the harness boundary to use existing patterns/tokens rather than suppressing or hiding violations, weakening configuration, or bypassing `check diff`.
19
+ - Mobile Perps `start_state` now enters the requested Perps route before reading orders or positions, initializing the provider client on a freshly launched wallet instead of timing out with `CLIENT_NOT_INITIALIZED`.
20
+
21
+ - Updated the compatible Farmslot runtime set to protocol 0.8, recipe-harness 0.5, and agent-runtime 0.2, making passive `ui.screen`/`ui.visible` observation support available to adapters that declare those observers without a local Farmslot checkout.
22
+ - Checklist terminal signals are monotonic through `mm-harness`: a late numeric or `start` mark can no longer regress a completed or blocked task back to `running`.
23
+ - Core `check diff` now runs changed package tests through their owning Yarn workspaces instead of passing workspace paths to the root Jest configuration.
24
+
25
+ - Mobile iOS launch no longer opens or activates Simulator.app during normal automation; set `MM_HARNESS_FOCUS_SIMULATOR=1` only when an operator explicitly wants the Simulator UI foregrounded.
26
+
27
+ - Public Extension `stop` now tears down the checkout-owned watcher, Chrome/CDP profile processes, viewer tails/windows, and stale runtime PID/lock markers; explicit `--port` overrides only the watcher port and no longer hides the runtime-context CDP owner.
28
+ - Tmux log viewers now require an explicit `RECIPE_TMUX_SESSION` or checkout `agentic-runtime.json` session; merely running from another tmux session never creates, sweeps, or marker-closes windows there, while stale markers are discarded and file logs remain available.
29
+ - Extension `run`/`call --heal auto` now rebuild or reload before proof when source, `dist/chrome`, and the loaded runtime snapshot are out of sync; healing-off runs stop before execution with the exact `launch --build --verify` recovery command.
30
+ - Extension launch, readiness, reattach, actions, and screenshots preserve the operator's keyboard focus by default while retaining a headed, isolated CDP browser; macOS app launch uses Launch Services in the background, and explicit window focus remains available with `MM_HARNESS_FOCUS_BROWSER=1`.
31
+ - `mm-harness check diff --fix` now repairs only changed files with the checkout's direct ESLint and oxfmt/Prettier binaries, then runs the normal bounded validation, so agents do not need to guess repository lint scripts.
32
+ - Recipe-run infra healing now performs the adapter's runtime recovery before retrying instead of only recording a recovery label and immediately rerunning the recipe.
33
+ - Extension `doctor --fix` now enables the checkout-local Perps build flag when `.metamaskrc` has it disabled, so packaged Perps actions cannot launch against a build that omits their product surface.
34
+ - Extension `ui.screenshot` now falls back from a failed capture-helper window snapshot to bounded Chrome-native capture, then to an explicitly labeled computed-style DOM raster when the headed Chrome screenshot domain is unavailable.
35
+ - Mobile `ui.screenshot` now validates simctl/adb output as PNG evidence before registration and records the native provider, command mode, selected device, and MIME type in artifact provenance.
36
+ - `checklist mark ... complete` now teaches the exact compact quality input, canonical builder command, and coverage artifact when a recipe activates those terminal requirements, instead of forcing agents to reverse-engineer the artifact validator.
37
+
38
+ ### Added
39
+ - Checkout-scoped operation locking prevents concurrent launch, fixture mutation, and recipe execution in one repository while allowing other checkouts/slots to run independently; stale locks from exited processes are reclaimed.
40
+ - `doctor --fix` creates an ignored, checkout-scoped `agentic-runtime.json` when absent, with stable claimed local ports for ordinary standalone clones while preserving explicit or Farmslot-provided slot resources.
41
+ - `fixtures init --from <path>|--dev` initializes the canonical checkout wallet fixture explicitly; `--dev` is a disposable public test wallet and is never suitable for real funds.
42
+
43
+ ### Fixed
44
+ - Missing checkout targets now return the structured usage envelope before lock acquisition in `run` and `doctor`; recovery bounds distinguish a consumed successful repair, adapter helpers preserve explicit runtime/device pins, checklist-only runtime resolution is lazy, and fixture finalization is time-bounded.
45
+ - Checkout contention now uses one bounded JSON contract across `call`, `launch`, `doctor --fix`, `fixtures`, and `run`: exit 4 with `SANDBOX_BUSY` and an actionable lock path.
46
+ - Extension loaded-snapshot checks now use size/mtime comparison instead of full checksums, fall back to an in-process comparison when `rsync` is unavailable, and report comparison failures without forcing a misleading relaunch.
47
+ - Mobile Perps environment repair now publishes `.js.env` atomically without following destination symlinks, and Mobile source preflight returns an actionable `MOBILE_SOURCE_UNAVAILABLE` error for non-Git targets.
48
+ - Mobile no longer translates the Extension-only `visible=true` wait alias into an unsupported enum; `call --arg` accepts quoted JSON strings for numeric-looking string values.
49
+ - Timed streaming leaves now settle as soon as a SIGTERM-terminated direct child exits, reap SIGTERM-ignoring descendants, and forward Ctrl+C/SIGTERM to detached child trees before the harness exits.
50
+ - `doctor --fix` now schema-validates, quarantines, and recreates malformed, semantically invalid, or cross-adapter-contaminated checkout runtime context without touching product data; wallet-fixture recovery commands use `--force` only for invalid or incomplete existing fixtures, while a missing fixture retains the safe unforced initialization command.
51
+ - `logs --source app` now resolves the adapter-owned application console (`extension-console.log` on Extension and the CDP-forwarded `metro.log` on Mobile); an absent console returns `APP_LOG_UNAVAILABLE` instead of falling back to a dev-server log or reporting a usage error.
52
+ - Doctor JSON now retains runtime probe failures, marks every diagnostic check as required or optional, derives its status from required checks, and reports runtime readiness separately.
53
+ - Debug JSON now preserves the leaf target, method, endpoint, and detail; `--no-open` reaches the leaf, and Mobile reports its debug endpoint without invoking it.
54
+ - Checklist diff gates and terminal-signal preservation now share agent-runtime's numeric step semantics, preventing exponent-form step tokens from bypassing a required PASS or regressing a terminal task.
55
+ - Extension Perps config repair and fixture initialization now stage validated, mode-correct regular files before atomic publication, preserve existing config modes, refuse no-force overwrites, do not follow destination symlinks, and reject runtime-ancestor symlinks.
56
+ - Mobile Metro startup now reaps an existing same-port console forwarder even when macOS `ps` pads a short PID, while retaining command-and-port ownership checks.
57
+ - Extension and Core dependency readiness now requires Yarn's node-modules install state when the checkout selects `nodeLinker: node-modules`, preventing a leftover `.yarn/install-state.gz` from reporting removed dependencies as current.
58
+ - Non-tmux Mobile Metro launch now uses a repository-shipped detached Node launcher instead of a same-process-group `nohup` child, so Metro survives the launching terminal/process-group boundary while retaining exact checkout PID, port, env, log, and public-stop ownership.
59
+ - Mobile launch now fingerprints `.js.env`, `.env`, and `.env.local` per runtime directory; a missing or changed successful-bundle baseline forces exactly one cache-cleared Metro restart, while failed starts and mid-launch env changes cannot bless stale transforms.
60
+ - Mobile `fixtures set` now bounds wallet application at 120 seconds, uses short bridge-readiness probes against the resolved slot Metro port, terminates only its owned child process group on timeout, releases the checkout lock, and leaves runtime relaunch to the explicit recovery command.
61
+ - Mobile `doctor --fix` now adds `MM_PERPS_ENABLED=true` and `OVERRIDE_REMOTE_FEATURE_FLAGS=true` to `.js.env` only when each key is absent, while preserving explicit repository or Farmslot values such as `false`; read-only doctor reports both effective local settings.
62
+ - Extension `metamask.perps.start_state` now rejects `page: false` during planning because the action must enter Perps before controller convergence; teardown retains `false` as its skip-navigation option.
63
+ - `run --plan` now rejects `validate.workflow.startState`, which execution ignores, and directs authors to the supported recipe-root `startState` field.
64
+ - `check diff` now uses checkout-configured oxfmt when present, falling back to Prettier, so formatting follows each repository's declared tool.
65
+ - `check diff` no longer passes ESLint 9's `--no-warn-ignored` flag to repositories on ESLint 8, so changed-file lint runs instead of failing during CLI argument parsing.
66
+ - Extension fixture seeding now fails immediately with the visible MetaMask fatal-startup detail instead of waiting through unlock/setup polling when the product cannot initialize.
67
+ - Extension harness fixtures now initialize Perps on testnet; start-state enters the current `#/perps-home` surface before controller convergence, reads the persisted controller network without requiring an initialized Perps request channel, and proves a requested toggle with a bounded poll.
68
+ - Extension `ui.wait_for` now documents its fields and accepts `visible=true|false` as a concise alias for visible/absent assertions.
69
+ - `mm-harness call` now parses JSON-like numbers, booleans, arrays, and objects from its documented `key=value` syntax, so typed action fields do not arrive as invalid strings.
70
+ - Extension launch auto-healing now repairs missing or stale dependencies with the checkout's pinned Node version instead of teaching agents to guess a host tool manager.
71
+ - Extension `metamask.perps.ensure_positions state=open` now preserves the position-selection result across the CDP metadata wrapper instead of treating the wrapped result as an array.
72
+ - Checkout locks are reentrant for nested `mm-harness` subprocesses carrying the owning operation's token, so `launch --verify` cannot block its own runner smoke while unrelated processes remain excluded.
73
+ - `doctor --fix` now reports top-level `status: "fail"` and exits nonzero when any repair remains unresolved, so missing fixtures and other sandbox blockers cannot look ready to agents.
74
+ - Fixture sync now fails with an actionable initialization command when no usable source exists, and Core accepts the same canonical signing fixture consumed by its Perps controller actions.
75
+ - `launch --verify` now performs the selected quick or `--build` launch before verification instead of bypassing launch through the legacy live path; `--build --verify` no longer drops the build request.
76
+ - Extension `verify` and `live` infer the same checkout-scoped CDP port as `status`, `doctor`, and `launch --verify` via the shared `formatExtensionSlotPorts` ladder (runtime context → pool → slot-suffix formula), so standalone verify passes on healthy prepared slots without `--cdp-port`.
77
+
3
78
  ## 0.14.7 - 2026-07-10
4
79
 
5
80
  ### Changed
@@ -232,11 +307,11 @@ The "version people use": every remaining identified fix/improvement batched on
232
307
  `--json` error envelope) instead of throwing.
233
308
  - **`run` auto-detects the adapter** from the target/cwd when `--adapter` is absent
234
309
  (parity with call/doctor); teaches when the repo type cannot be detected.
235
- - **Log-tail windows follow a run-owned session ladder** — `RECIPE_TMUX_SESSION` →
236
- the checkout's `agentic-runtime.json` `session` → the current tmux client — with no
237
- slot-number derivation (the orchestrator names the session; the harness only
238
- populates windows). Stale `metro-<port>` windows whose port has no live listener
239
- are swept on Metro start.
310
+ - **Log-tail windows follow an explicitly owned session ladder** —
311
+ `RECIPE_TMUX_SESSION` → the checkout's `agentic-runtime.json` `session`, with no
312
+ current-session or slot-number inference (the orchestrator names the session; the
313
+ harness only populates windows). Stale `metro-<port>` windows whose port has no
314
+ live listener are swept only inside that resolved session on Metro start.
240
315
  - **Extension slot ports fill independently** — the resolver no longer early-returns
241
316
  once CDP is known, so a context that pins only `cdpPort` still gets `WATCHER_PORT`
242
317
  from the pool/formula.
@@ -398,7 +473,7 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
398
473
  - **Deps baseline recorded the instant a setup leaf succeeds** — `prepareMobile` records the deps fingerprint baseline immediately after any successful `yarn-setup`, unconditionally (previously only a later `record: true` re-decide did, gated on the original decision being `install` and on `RECIPE_UP_INSTALL_ATTEMPTED`). Genuine-drift detection is fingerprint-based and self-sustaining after one good install.
399
474
  - **CocoaPods no longer crashes on non-UTF-8 podspecs under a locale-less env** — pod-triggering spawns (`yarn setup` / native `yarn start:*`) now pin a UTF-8 `LANG`/`LC_ALL` when the caller has none. A gateway/tmux parent env without a locale made Ruby parse podspecs as US-ASCII and die on the first multibyte byte (`Invalid react-native-mmkv.podspec: "\xE2" on US-ASCII`).
400
475
  - **`launch` no longer reports a dead app as ready** — the mobile readiness decision treated a healthy Metro + successful bundle as `ready` without checking the target device, so a dev client that had crashed or was never opened produced `✓ ready` while nothing was running. The decision now probes the device (`xcrun simctl spawn … launchctl list` / `adb shell ps -A`, fail-closed) and returns `launch`/`app-not-running` (relaunch the client) unless the app is actually alive.
401
- - **Metro log-tail window no longer leaks into an unrelated tmux session** — running `start-metro` outside a tmux client fell back to `tmux display-message` (the last-attached session, e.g. the user's own) and created its `metro-<port>` window — with a `tail -F` — there, outliving the run. The viewer now opens a window only in a session that belongs to this run (a named slot session that exists, or the current session when actually inside tmux) and otherwise skips it; Metro still runs detached to the log. Extracted to `adapters/mobile/lib/tmux-viewer.sh` so the rule is unit-tested; contract tests stub tmux hermetically so no suite run can create a real window.
476
+ - **Metro log-tail window no longer leaks into an unrelated tmux session** — running `start-metro` without an explicit session previously fell back to whichever tmux client was current or last attached. The viewer now opens a window only from `RECIPE_TMUX_SESSION` or the checkout runtime context and otherwise skips it; Metro still runs detached to the log. Extracted to `adapters/mobile/lib/tmux-viewer.sh` so the rule is unit-tested; contract tests stub tmux hermetically so no suite run can create a real window.
402
477
 
403
478
  ### Added
404
479
  - **Adapter surface — one seam for per-platform behavior** — `src/adapters/surface.ts` defines `AdapterSurface` + a `getAdapterSurface(adapter)` registry that owns the behavior commands used to hand-roll with `if (adapter === …)` ladders (slot-port resolution, read-only runtime status, dev-server describe/stop, log sources, and platform-phrased Next: hints), with thin `mobile`/`extension`/`core` implementations that delegate to the existing readiness/port plumbing (re-homed to `src/adapters/slot-ports.ts`). `doctor`, `launch`, `stop`, `logs`, `debug`, and `fixtures` now resolve platform behavior through the surface instead of branching. This closes the platform-blind class of bug (a platform handled in one command but forgotten in another). Documented in `docs/ADAPTER-SURFACE.md`; enforces `docs/UX-PRINCIPLES.md` principle 1.
File without changes
File without changes
File without changes
@@ -312,7 +312,7 @@ const resumeWebpack = () => {
312
312
  if (CDP_PORT) args.push('--remote-debugging-port=' + CDP_PORT);
313
313
 
314
314
  const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
315
- execFileSync('open', ['-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
315
+ execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
316
316
  for (let i = 0; i < 60; i++) {
317
317
  try {
318
318
  const version = await new Promise((resolve, reject) => {
@@ -412,7 +412,9 @@ const resumeWebpack = () => {
412
412
  await p.close().catch(() => {});
413
413
  }
414
414
  }
415
- await page.bringToFront().catch(() => {});
415
+ if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
416
+ await page.bringToFront().catch(() => {});
417
+ }
416
418
 
417
419
  fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
418
420
  console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
File without changes
@@ -89,45 +89,84 @@ if (args['stop-only'] !== undefined) {
89
89
  process.exit(0);
90
90
  }
91
91
 
92
+ const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
93
+ const chromeArgs = [
94
+ `--user-data-dir=${args.profile}`,
95
+ '--remote-debugging-address=127.0.0.1',
96
+ `--remote-debugging-port=${cdpPort}`,
97
+ '--no-first-run',
98
+ '--disable-first-run-ui',
99
+ '--disable-default-apps',
100
+ '--disable-popup-blocking',
101
+ '--disable-extensions-file-access-check',
102
+ '--disable-extensions-content-verification',
103
+ '--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
104
+ `--disable-extensions-except=${args['extension-dir']}`,
105
+ `--load-extension=${args['extension-dir']}`,
106
+ // Initial tab: caller's dapp URL when provided, else the extension home. The
107
+ // extension home avoids leaving disposable chrome://newtab/ or extensions
108
+ // manager tabs in the operator's slot browser.
109
+ initialUrl,
110
+ ];
92
111
  const logFd = fs.openSync(args['chrome-log'], 'a');
93
- let child;
112
+ let browserPid;
94
113
  try {
95
- const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
96
- child = spawn(args['chrome-bin'], [
97
- `--user-data-dir=${args.profile}`,
98
- '--remote-debugging-address=127.0.0.1',
99
- `--remote-debugging-port=${cdpPort}`,
100
- '--no-first-run',
101
- '--disable-first-run-ui',
102
- '--disable-default-apps',
103
- '--disable-popup-blocking',
104
- '--disable-extensions-file-access-check',
105
- '--disable-extensions-content-verification',
106
- '--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
107
- `--disable-extensions-except=${args['extension-dir']}`,
108
- `--load-extension=${args['extension-dir']}`,
109
- // Initial tab: caller's dapp URL when provided, else the extension home. The
110
- // extension home avoids leaving disposable chrome://newtab/ or extensions
111
- // manager tabs in the operator's slot browser.
112
- initialUrl,
113
- ], {
114
- detached: true,
115
- env: {
116
- ...process.env,
117
- BUNDLED_DEBUGPY_PATH: undefined,
118
- PYTHONHOME: undefined,
119
- PYTHONPATH: undefined,
120
- DYLD_LIBRARY_PATH: undefined,
121
- DYLD_FALLBACK_LIBRARY_PATH: undefined,
122
- DYLD_INSERT_LIBRARIES: undefined,
123
- },
124
- stdio: ['ignore', logFd, logFd],
125
- });
114
+ const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
115
+ if (application) {
116
+ // Launch Services does not attach Chrome's stdio, so chrome.log may stay empty; CDP ownership is the readiness signal.
117
+ execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
118
+ env: sanitizedChildEnv(),
119
+ stdio: ['ignore', logFd, logFd],
120
+ });
121
+ browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
122
+ if (browserPid === null) {
123
+ throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
124
+ }
125
+ } else {
126
+ const child = spawn(args['chrome-bin'], chromeArgs, {
127
+ detached: true,
128
+ env: sanitizedChildEnv(),
129
+ stdio: ['ignore', logFd, logFd],
130
+ });
131
+ child.unref();
132
+ browserPid = child.pid;
133
+ }
126
134
  } finally {
127
135
  fs.closeSync(logFd);
128
136
  }
129
- child.unref();
130
- fs.writeFileSync(args['chrome-pid'], `${child.pid}\n`);
137
+ fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
138
+
139
+ function sanitizedChildEnv() {
140
+ return {
141
+ ...process.env,
142
+ BUNDLED_DEBUGPY_PATH: undefined,
143
+ PYTHONHOME: undefined,
144
+ PYTHONPATH: undefined,
145
+ DYLD_LIBRARY_PATH: undefined,
146
+ DYLD_FALLBACK_LIBRARY_PATH: undefined,
147
+ DYLD_INSERT_LIBRARIES: undefined,
148
+ };
149
+ }
150
+
151
+ function macApplicationForExecutable(executable) {
152
+ let current = path.resolve(executable);
153
+ while (current !== path.dirname(current)) {
154
+ if (current.endsWith('.app')) return current;
155
+ current = path.dirname(current);
156
+ }
157
+ return null;
158
+ }
159
+
160
+ function waitForOwnedCdpPid(port, profile, extensionDir) {
161
+ for (let i = 0; i < 300; i += 1) {
162
+ const pid = cdpListenerPids(port).find((candidate) =>
163
+ processIsOwnedHarnessBrowser(candidate, profile, extensionDir),
164
+ );
165
+ if (pid !== undefined) return pid;
166
+ spawnSync('sleep', ['0.1']);
167
+ }
168
+ return null;
169
+ }
131
170
 
132
171
  function parseArgs(argv) {
133
172
  const parsed = {};
File without changes
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # Inputs (flags / env):
12
12
  # --target <metamask-extension> (default $PWD)
13
- # --cdp-port <port> (required, numeric-validated)
13
+ # --cdp-port <port> (optional; inferred from checkout context/pool when omitted)
14
14
  # --launch-existing-dist | --start-watch | --prepare-cmd <cmd>
15
15
  # (env RECIPE_HARNESS_EXTENSION_LAUNCH_CMD)
16
16
  # --dist-dir <rel> (default dist/chrome), --chrome-user-data-dir <dir>,
@@ -55,18 +55,11 @@ while [ "$#" -gt 0 ]; do
55
55
  --dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
56
56
  --chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
57
57
  --remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
58
- -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] --cdp-port <port> [--launch-existing-dist|--start-watch|--prepare-cmd <cmd>] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
58
+ -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--prepare-cmd <cmd>] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
59
59
  *) echo "Unknown arg: $1" >&2; exit 2 ;;
60
60
  esac
61
61
  done
62
62
 
63
- [ -n "$CDP_PORT" ] || { echo "Missing --cdp-port for Extension live validation" >&2; exit 2; }
64
- # Reject a non-numeric port before it is interpolated into the Chrome launch
65
- # command and the CDP HTTP probes.
66
- case "$CDP_PORT" in
67
- *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
68
- esac
69
-
70
63
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
71
64
  # shellcheck disable=SC1091
72
65
  for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
@@ -77,6 +70,25 @@ if ! command -v harness_root >/dev/null 2>&1; then
77
70
  echo "mm-harness: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
78
71
  exit 1
79
72
  fi
73
+ TARGET="$(cd "$TARGET" && pwd)"
74
+ # Infer checkout-scoped CDP when omitted (context → pool → formula).
75
+ if [ -z "$CDP_PORT" ]; then
76
+ for _rsp in "$SCRIPT_DIR/lib/resolve-slot-ports.sh" "$SCRIPT_DIR/../shared/resolve-slot-ports.sh"; do
77
+ [ -f "$_rsp" ] && { # shellcheck disable=SC1090
78
+ . "$_rsp"; break; }
79
+ done
80
+ unset _rsp
81
+ if command -v apply_extension_slot_ports >/dev/null 2>&1; then
82
+ apply_extension_slot_ports "$TARGET"
83
+ fi
84
+ fi
85
+ [ -n "$CDP_PORT" ] || { echo "Could not infer extension CDP port for $TARGET; pass --cdp-port or prepare the slot." >&2; exit 2; }
86
+ # Reject a non-numeric port before it is interpolated into the Chrome launch
87
+ # command and the CDP HTTP probes.
88
+ case "$CDP_PORT" in
89
+ *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
90
+ esac
91
+
80
92
  # verify.sh is a recipe-tree feature: co-located in installed copies, under
81
93
  # adapters/extension in a repo checkout.
82
94
  VERIFY_SH=""
@@ -88,7 +100,6 @@ if [ -z "$VERIFY_SH" ]; then
88
100
  echo "mm-harness: extension verify.sh not found next to $SCRIPT_DIR; reinstall the runner." >&2
89
101
  exit 1
90
102
  fi
91
- TARGET="$(cd "$TARGET" && pwd)"
92
103
  RUNTIME_DIR="$(recipe_runtime_dir)"
93
104
  RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
94
105
  CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
File without changes
@@ -299,9 +299,9 @@ async function closePages(context, predicate) {
299
299
  if (!startUrl && (url.startsWith('http://') || url.startsWith('https://'))) return true;
300
300
  return false;
301
301
  });
302
- await home.bringToFront().catch(() => {
303
- // Foreground is an operator affordance; health is verified separately.
304
- });
302
+ if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
303
+ await home.bringToFront().catch(() => {});
304
+ }
305
305
  }
306
306
  if (typeof browser.disconnect === 'function') await browser.disconnect();
307
307
  else await browser.close();
File without changes
File without changes
@@ -100,6 +100,10 @@ json_list() {
100
100
  curl -s "http://127.0.0.1:${CDP_PORT}/json/list"
101
101
  }
102
102
 
103
+ focus_browser() {
104
+ [ "${MM_HARNESS_FOCUS_BROWSER:-}" = "1" ]
105
+ }
106
+
103
107
  find_sidepanel_id() {
104
108
  json_list | python3 -c "import json,sys; d=json.load(sys.stdin); ids=[t.get('id','') for t in d if t.get('type')=='page' and 'sidepanel.html' in t.get('url','')]; print(ids[0] if ids else '')"
105
109
  }
@@ -143,7 +147,7 @@ activate_sandbox_page() {
143
147
  target_id="$(
144
148
  EXT_ID="$ext_id" json_list | python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if ext and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'sidepanel.html' not in t.get('url','')), None) or next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None) or (pages[0] if pages else None); print(target.get('id','') if target else '')"
145
149
  )"
146
- if [ -n "$target_id" ]; then
150
+ if [ -n "$target_id" ] && focus_browser; then
147
151
  curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
148
152
  fi
149
153
  }
@@ -177,7 +181,7 @@ activate_dapp_tab() {
177
181
  target_id="$(
178
182
  json_list | python3 -c "import json,sys; d=json.load(sys.stdin); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None); print(target.get('id','') if target else '')"
179
183
  )"
180
- if [ -n "$target_id" ]; then
184
+ if [ -n "$target_id" ] && focus_browser; then
181
185
  curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
182
186
  fi
183
187
  }
@@ -259,7 +263,9 @@ const { chromium } = require('playwright');
259
263
  });
260
264
  }
261
265
 
262
- await page.bringToFront();
266
+ if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
267
+ await page.bringToFront();
268
+ }
263
269
  const result = await page.evaluate(async () => {
264
270
  const currentWindow = await chrome.windows.getCurrent();
265
271
  const id = '__recipe_open_sidepanel__';
@@ -331,7 +337,7 @@ const { chromium } = require('playwright');
331
337
  const dappPage = context
332
338
  .pages()
333
339
  .find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
334
- if (dappPage) {
340
+ if (dappPage && process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
335
341
  try {
336
342
  await dappPage.bringToFront();
337
343
  } catch {
File without changes
@@ -235,8 +235,8 @@ nohup env -u BUNDLED_DEBUGPY_PATH ${path_prefix:+PATH="${path_prefix}${PATH}"} $
235
235
  started_pid=$!
236
236
  printf '%s\n' "$started_pid" > "$watch_pid_file"
237
237
  # The orchestrator names the session; the harness only populates windows. Resolve
238
- # via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session
239
- # current tmux client) — no slot-number derivation.
238
+ # via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session)
239
+ # no current-session fallback or slot-number derivation.
240
240
  for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
241
241
  # shellcheck disable=SC1091
242
242
  [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
File without changes
@@ -155,6 +155,23 @@ else
155
155
  refresh_extension_id
156
156
  fi
157
157
 
158
+ # Infer checkout-scoped CDP port when caller omitted --cdp-port.
159
+ if [ -z "$CDP_PORT" ]; then
160
+ for _rsp in "$SCRIPT_DIR/lib/resolve-slot-ports.sh" "$SCRIPT_DIR/../../adapters/shared/resolve-slot-ports.sh" "$SCRIPT_DIR/../shared/resolve-slot-ports.sh"; do
161
+ [ -f "$_rsp" ] && { # shellcheck disable=SC1090
162
+ . "$_rsp"; break; }
163
+ done
164
+ unset _rsp
165
+ if command -v apply_extension_slot_ports >/dev/null 2>&1; then
166
+ apply_extension_slot_ports "$TARGET"
167
+ fi
168
+ fi
169
+ if [ -n "$CDP_PORT" ]; then
170
+ case "$CDP_PORT" in
171
+ *[!0-9]*) echo "Invalid inferred --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
172
+ esac
173
+ fi
174
+
158
175
  status="pass"
159
176
  checks=()
160
177
 
@@ -378,8 +395,8 @@ if [ "$STATIC_ONLY" = false ]; then
378
395
  checks+=("{\"name\":\"fixture/profile status\",\"status\":\"$(node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.log(v.status === "READY" ? "pass" : "warn");' "$ARTIFACTS/logs/fixture-status.json")\"}")
379
396
 
380
397
  if [ -z "$CDP_PORT" ]; then
381
- echo "Live extension verify requires --cdp-port. Static checks may pass, but runtime proof is missing." > "$ARTIFACTS/logs/live-missing-cdp.log"
382
- checks+=("{\"name\":\"live runtime CDP port\",\"status\":\"fail\",\"detail\":\"missing --cdp-port\"}")
398
+ echo "Live extension verify could not infer a checkout CDP port. Run from a prepared slot checkout or pass --cdp-port." > "$ARTIFACTS/logs/live-missing-cdp.log"
399
+ checks+=("{\"name\":\"live runtime CDP port\",\"status\":\"fail\",\"detail\":\"missing CDP port (checkout-scoped inference failed)\"}")
383
400
  status="fail"
384
401
  live_mode="missing-cdp"
385
402
  else
@@ -503,6 +503,9 @@ async function generate(args) {
503
503
  data.PreferencesController.autoLockTimeLimit = 0;
504
504
  }
505
505
  data.PerpsController ??= {};
506
+ // Harness-owned fixtures default Perps mutations to testnet. This is both
507
+ // deterministic and prevents an autonomous recipe from initializing mainnet.
508
+ data.PerpsController.isTestnet = true;
506
509
  data.PerpsController.isFirstTimeUser = { mainnet: false, testnet: false };
507
510
  data.PerpsController.hasPlacedFirstOrder = { mainnet: true, testnet: true };
508
511
  patchAccountTracker(
@@ -705,6 +708,15 @@ async function waitForWalletScreen(page) {
705
708
  const unlockSelector = '[data-testid="unlock-password"]';
706
709
  const deadline = Date.now() + 45000;
707
710
  while (Date.now() < deadline) {
711
+ const fatalStartup = await page
712
+ .evaluate(() => {
713
+ const text = document.body?.innerText || '';
714
+ return text.includes('MetaMask had trouble starting') ? text.slice(0, 500) : null;
715
+ })
716
+ .catch(() => null);
717
+ if (fatalStartup) {
718
+ return { state: 'fatal', selector: null, detail: fatalStartup };
719
+ }
708
720
  for (const selector of readySelectors) {
709
721
  if (await locatorVisible(page.locator(selector))) {
710
722
  return { state: 'unlocked', selector };
@@ -762,7 +774,8 @@ async function unlockIfNeeded(page, password) {
762
774
  state = await waitForWalletScreen(page);
763
775
  }
764
776
  if (state.state !== 'unlocked') {
765
- throw new Error(`Wallet did not reach unlocked home screen after fixture seeding (state=${state.state}). Next: mm-harness call ensure_unlocked # then relaunch`);
777
+ const detail = state.detail ? `: ${state.detail.replace(/\s+/gu, ' ').trim()}` : '';
778
+ throw new Error(`Wallet did not reach unlocked home screen after fixture seeding (state=${state.state})${detail}. Next: inspect the product startup error before retrying launch`);
766
779
  }
767
780
  return state;
768
781
  }
@@ -18,6 +18,14 @@
18
18
  "inputs": "--target --port --clear; env WATCHER_PORT, METRO_PORT, MOBILE_METRO_REQUIRED_ENV, METRO_MAX_WORKERS",
19
19
  "outputs": "Metro PID file + log; exit 0 ready / 1 timeout / 2 bad args"
20
20
  },
21
+ {
22
+ "id": "mobile/launch-metro",
23
+ "entry": "adapters/mobile/launch-metro.cjs",
24
+ "kind": "node",
25
+ "purpose": "Launch Metro detached from the invoking process group so the runtime survives command cleanup.",
26
+ "inputs": "--target --port --log --pid-file [--workers] [--clear]",
27
+ "outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
28
+ },
21
29
  {
22
30
  "id": "mobile/stop-metro",
23
31
  "entry": "adapters/mobile/stop-metro.sh",
@@ -310,8 +318,8 @@
310
318
  "id": "lib/tmux-session",
311
319
  "entry": "adapters/shared/tmux-session.sh",
312
320
  "kind": "lib",
313
- "purpose": "resolve_run_tmux_session: the run-owned tmux session a log-tail window belongs in (RECIPE_TMUX_SESSION -> agentic-runtime.json session -> current client; no slot-number derivation).",
314
- "inputs": "resolve_run_tmux_session [runtime-dir]; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
321
+ "purpose": "resolve_run_tmux_session: the explicitly owned tmux session a log-tail window belongs in (RECIPE_TMUX_SESSION -> agentic-runtime.json session; no current-session or slot-number inference).",
322
+ "inputs": "resolve_run_tmux_session [runtime-dir]; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT",
315
323
  "outputs": "resolved session name on stdout (empty when none)"
316
324
  },
317
325
  {
@@ -319,7 +327,7 @@
319
327
  "entry": "adapters/shared/tmux-viewer.sh",
320
328
  "kind": "lib",
321
329
  "purpose": "Run-owned tmux log viewer helpers that open, replace, and clean prefixed viewer windows without slot-name inference.",
322
- "inputs": "tmux_viewer_open/close_marker/sweep_prefix functions; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
330
+ "inputs": "tmux_viewer_open/close_marker/sweep_prefix functions; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT",
323
331
  "outputs": "viewer windows and marker files; no-op when no run-owned session is available"
324
332
  },
325
333
  {
@@ -346,6 +354,14 @@
346
354
  "inputs": "ensure-runner-deps.sh [RUNNER_DIR]; env FARMSLOT_ROOT / METAMASK_RUNNER_PROTOCOL_ROOT optional local link target",
347
355
  "outputs": "installed node_modules in RUNNER_DIR; exit 0 when deps ready"
348
356
  },
357
+ {
358
+ "id": "lib/install-repo-deps",
359
+ "entry": "adapters/shared/install-repo-deps.sh",
360
+ "kind": "bash",
361
+ "purpose": "Install a checkout's pinned dependencies through its repository Node toolchain.",
362
+ "inputs": "--target <checkout>",
363
+ "outputs": "corepack yarn install output; exit 0/1/2"
364
+ },
349
365
  {
350
366
  "id": "lib/resolve-slot-ports",
351
367
  "entry": "adapters/shared/resolve-slot-ports.mjs",
File without changes
@@ -115,15 +115,17 @@ echo "Fixture OK: password + ${ACCOUNT_COUNT} entry(ies), ${EXPECTED_ETH_TOTAL}
115
115
 
116
116
  wait_for_cdp_bridge() {
117
117
  local waited=0
118
- local max="${CDP_READY_TIMEOUT:-90}"
118
+ local max="${CDP_READY_TIMEOUT:-30}"
119
+ local probe_timeout="${CDP_READY_PROBE_TIMEOUT:-1000}"
120
+ local started="$SECONDS"
119
121
  local output=""
120
- while [ "$waited" -lt "$max" ]; do
121
- if output=$($CDP eval "JSON.stringify({ok:true})" 2>&1); then
122
+ while [ "$((SECONDS - started))" -lt "$max" ]; do
123
+ if output=$(CDP_TIMEOUT="$probe_timeout" CDP_DISCOVERY_RETRIES=1 $CDP eval "JSON.stringify({ok:true})" 2>&1); then
122
124
  echo "CDP bridge connected."
123
125
  return 0
124
126
  fi
125
127
  sleep 1
126
- waited=$((waited + 1))
128
+ waited=$((SECONDS - started))
127
129
  [ "$waited" -eq 5 ] && echo "Waiting for CDP bridge..."
128
130
  done
129
131
  echo "ERROR: CDP not reachable after ${max}s"
File without changes
File without changes