@deeeed/metamask-harness 0.5.0 → 0.6.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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
package/CHANGELOG.md CHANGED
@@ -1,13 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## 0.5.0 - 2026-07-05
3
+ ## 0.6.0 - 2026-07-06
4
4
 
5
- Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
5
+ Minor release: CLI hardening (decomposed, single-parser, dist-only) plus a
6
+ fresh-slot validation wave — every fix proven live on untouched -2 slots and
7
+ independently cross-reviewed.
8
+
9
+ ### Changed
10
+ - **CLI decomposition + dist-only publish** — `src/cli.ts` is parse+dispatch only;
11
+ handlers under `src/commands/<verb>.ts`; one commander parser; the npm tarball
12
+ ships compiled `dist/` without `src/` and `tsx` is a devDependency (plain-node,
13
+ smaller, faster). Source checkouts keep the tsx dev path (`MM_HARNESS_BIN`).
14
+ - `verify` auto-ensures its own overlay (no refusal on a fresh checkout); every
15
+ engine execution resolves the slot's ports/device first (call/run/fixtures get
16
+ the same slot isolation launch has).
6
17
 
18
+ ### Added
19
+ - **`stop` reaps leaked Metro bundlers by checkout** (port-agnostic, path-
20
+ boundary matched), `doctor` surfaces any orphan and teaches the reap.
21
+ - **`launch --url <dapp>`** (extension) opens the dapp in the main tab beside the
22
+ MetaMask sidepanel; `--stop-only` profile release before seed; stable per-checkout
23
+ Chrome profile + runtime-dist; post-open tab cleanup.
24
+ - DX: run-mode + stale-`dist` warning in `bin`; overlay-delegation notice; docs/CODE-MAP.md.
25
+
26
+ ### Fixed
27
+ - `launch` targets the slot's own simulator (never the `booted` alias) and boots it;
28
+ slot-port resolution layers rungs so a partial rung never nulls the port; provision
29
+ resolves the simulator from the pool and boots before install.
30
+ - `call` works from published installs (dist bridge, no type-strip crash);
31
+ `ensure_unlocked` fails teaching `fixtures set` on an un-onboarded wallet instead
32
+ of vacuously passing (mobile + extension); `provision --json` usage errors emit the
33
+ standard envelope; deps-not-ready teaches a command that works on a bare checkout.
34
+ ## 0.5.1 - 2026-07-05
35
+
36
+ Fresh-install hotfixes found in live validation of 0.5.0.
37
+
38
+ ### Fixed
39
+ - **First launch on a provisioned slot targets the slot's own simulator** — the mobile resolution ladder now reads the `runway-provision.json` baseline (simulator name/udid, watcher port, slot id) when `agentic-runtime.json` doesn't exist yet, and boots that simulator. Previously a provisioned-but-unprepared slot degraded to the simctl `booted` alias, missed the installed dev client, and guessed the wrong Metro port.
40
+ - **`call` works from published installs** — library actions load harness helpers through a dist-preferring bridge instead of importing `src/index.ts` (node refuses to type-strip `.ts` under `node_modules`). The bridge is deliberately narrow (paths + run-recording) to avoid an import cycle through `adapters.ts`.
41
+ - **deps-not-ready guidance matches the caller's state** — on a checkout without node_modules the taught command now leads with `yarn install --immutable` (yarn cannot run scripts at all on a bare checkout), and always ends with the launch re-run.
42
+
43
+ ## Unreleased
44
+
45
+ ### Fixed
46
+ - **`provision --json` adapter-detection failures emit the standard error envelope** — usage errors now match `doctor`/`launch` (envelope on stdout with `error.userAction`; stderr stays human-only). Previously `--json` callers got empty stdout.
47
+
48
+ ### Changed
49
+ - **CLI decomposition** — `src/cli.ts` is parse+dispatch only; handlers live under `src/commands/<verb>.ts`; `provision` routes through `src/commands/provision.ts` (not overlay harness dispatch); launch composition splits under `src/commands/launch/`; commander owns per-command `--help` (no hand-rolled interception).
50
+ - **Published tarball is dist-only** — `files` no longer ships `src/`; `tsx` is a devDependency; `bin/mm-harness` and overlay leaves resolve `dist/*.js` (teaching error if a published install lacks `dist/`). Library actions keep importing `library/actions/harness-exports.mjs` (dist-preferring narrow bridge from 0.5.1); package exports remain for external consumers (`dist/index.js` published; `src/index.ts` when `--conditions=development` and dist is absent).
51
+ - **resolve-farmslot-ports is TypeScript-first** — pool/context/formula port resolution lives in self-contained `adapters/shared/resolve-farmslot-ports-core.mjs` (works from runner and injected overlay copies); `src/adapters/resolve-farmslot-ports.ts` re-exports it for `slot-ports.ts`. `resolve-farmslot-ports.mjs` is the node leaf; `resolve-farmslot-ports.sh` remains a thin bash-compat wrapper for sourced callers.
52
+
53
+ ## 0.5.0 - 2026-07-05
54
+ Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
7
55
  ### Added
8
56
  - **`mm-harness provision runway`** — provisions a mobile slot with exactly: resolve the latest Runway (expo-dev-build) artifact, download it ONCE into a shared machine cache (`~/.cache/metamask-harness/runway/<runId>/`, sha256+size integrity, corrupt entries re-downloaded, never a slot failure), create the simulator if missing, install the dev client on it. Zero yarn/deps/Metro — those stay dispatch-time (lazy, presence-authoritative readiness unchanged). `install --runway` is the overlay-path equivalent (covered by its own contract test). Baseline recorded so doctor reports provisioned-but-deps-pending as a normal state.
9
57
  - **Compiled `dist/` for published installs** — the npm tarball ships esbuild-compiled JS; `npm i -g` installs run the dist entry (~0.21s cold vs ~0.37s tsx). Source checkouts without `dist/` keep the tsx dev path; `bin/mm-harness` prefers dist when present. Packaging contract test (pack → global install → dist entry runs).
10
-
11
58
  ### Changed
12
59
  - Runway provisioning callers (farm installer, mobile-farm `runway` prepare profile) delegate to the harness; the profile no longer runs a deps phase and a contract test enforces that.
13
60
 
@@ -43,17 +90,7 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
43
90
  - **Colored grouped help with DEV OVERRIDE banner and SLOT line**; `mmdev-harness` completions; outcome-stating launch summaries with a `[dev]` marker.
44
91
  - **docs/UX-PRINCIPLES.md** — the CLI interaction contract (human surface rules; `--json` envelope purity is exempt and inviolable).
45
92
 
46
- ## Unreleased
47
-
48
- ### Added
49
- - Add `mm-harness provision` for thin mobile Runway artifact provisioning with a shared integrity-checked cache and simulator install path.
50
- - Add `mm-harness provision runway ios --resolve-only` to return Runway run/revision/artifact metadata without touching simulator, cache, download, install, or baseline state.
51
- - **Dist publish path** — `npm run build` compiles `src/` to `dist/`; `npm pack` / global install runs the compiled CLI entry (`dist/mm-harness-cli.js`) while a source checkout without `dist/` keeps the tsx+src dev path. Contracts: `packaging-dist-install`, `packaging-dev-checkout`.
52
-
53
93
  ### Fixed
54
- - `mm-harness provision runway ios` now routes through the same Runway provisioning surface as `install --runway`, so overlay dispatch accepts the compatibility command instead of rejecting it as an unsupported harness action.
55
- - Runway provisioning now accepts farm context flags (`--slot`, `--watcher-port`, `--runtime-dir`), records slot/watcher/runtime-dir baselines correctly, ignores stale provision baselines unless the app is still installed on the simulator, and emits rerunnable `Next:` commands from the actual invocation.
56
- - Runway iOS artifact extraction now handles the live nested zip layout and app bundle names other than `MetaMask.app` while keeping the cached app directory integrity hash/size contract.
57
94
  - **Mobile preflight no longer re-runs `yarn setup` on every launch** — the deps readiness decision escalated an mtime hint to `stale` and ran the FULL `yarn setup` (submodules + inpage bridge + jetify + pods) inside the launch phase on every invocation in orchestrated (farmslot) contexts. `depsCheck` reports `stale` for a no-baseline checkout whenever a manifest is newer than the install markers; the orchestrator's git phase refreshes tracked-file mtimes on every sync while its deps phase leaves `node_modules` untouched, so "manifest newer than markers" is normal and not proof of drift — yet it duplicated the orchestrator's deps work and turned a fast preflight into 15+ minutes (the behavior that also looked like a hang before 0.3.8's streaming). **Presence is now authoritative:** without a recorded baseline an mtime-only `stale` is trusted as `current` (with a one-line stderr warning), so a churned-but-present checkout launches with zero `yarn` in both tiers. Genuine drift is still caught by the recorded-baseline fingerprint. `mm-harness launch` (quick tier / no `--build`) also dropped `preflightMode` before deciding, so the fast contract never took effect; it is threaded through now.
58
95
  - **Fast tier never installs; genuine gaps teach the orchestrator** — in the fast tier (quick launch / orchestrated), deps that genuinely need work (absent markers, an absent required package, a fingerprint drift, or Metro unable to resolve a module) return a teaching block naming the orchestrator deps/prepare phase (plus the standalone `yarn setup:expo` / `launch --build` remedies) instead of an implicit setup — deps are the orchestrator's contract. The standalone/full tier still installs.
59
96
  - **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.
@@ -139,7 +176,7 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
139
176
  - Core run teaches the correct next step when no account is set.
140
177
  - Resolved CodeQL alerts (insecure temp dir, file-access-to-http).
141
178
 
142
- ## Unreleased
179
+ ## 0.2.0 - 2026-07-03
143
180
 
144
181
  ### Added
145
182
 
@@ -150,12 +187,6 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
150
187
  ### Changed
151
188
 
152
189
  - **Teaching errors are type-enforced**: `usageOut` now requires a `userAction` escape argument, so a teaching error without a reachable next step no longer compiles. The repo-type detection failure teaches BOTH escapes (`cd into a checkout / pass --target <path>` and `--adapter <mobile|extension|core>`) consistently across `doctor`, `launch`, `verify`, and the overlay path.
153
- - **Repository reorganized into `src/`, `adapters/`, `library/` domains**: all TypeScript now lives under `src/` (shared CLI/core plus typed per-adapter logic in `src/adapters/<adapter>/`); everything that executes against or is injected into the app runtime lives under `adapters/` (`adapters/<adapter>/` per-app glue, `adapters/shared/` cross-adapter OS-glue, `adapters/porcelain/` composed entrypoints, `adapters/*/overlay/` the only injected payload); `library/` holds composable vocabulary only. The `runner/` and `orchestration/` trees no longer exist. The surface doctor moved to `scripts/adapter-surface-doctor.mjs` with its index at `adapters/manifest.json`. Placement rules (R1–R11) are documented in `AGENTS.md`. This is a path-only reorganization; runtime behavior is unchanged.
154
-
155
- ## 0.2.0 - 2026-07-03
156
-
157
- ### Changed
158
-
159
190
  - **Docs realigned to the shipped single-bin surface**: `README.md`, `docs/harness-cli.md` (reduced to a pointer at `CLI-SPEC.md`), `docs/recipe-libraries.md`, `docs/extension-runtime-commands.md`, `docs/package-boundaries.md`, and `docs/live-adapter-contract.md` no longer teach the retired `metamask-recipe`/`mm-recipe`/`mme-recipe` names or "planned/stub" daily-loop claims — every command example is `mm-harness`. (`docs/MENTAL-MODEL.md` and `docs/CLI-SPEC.md` retain the old names by design: they are the before→after migration contract.)
160
191
  - **Published package trimmed to runtime needs**: the `files` field drops dev-only helpers (`scripts/check.mjs`, `scripts/quality/`, `scripts/link-local-farmslot.mjs`, `scripts/validate-action-e2e-artifacts.mjs`) and repo-dev docs (`AGENTS.md`, `CLAUDE.md`), keeping `bin`, `runner`, `orchestration`, `library`, the runtime completion scripts, `docs`, `README.md`, and `CHANGELOG.md`. The `files` field now also negates `scripts/README.md` (dev-only) so the published tarball no longer ships it alongside the two runtime completion scripts.
161
192
  - **Comment-hygiene guard extended**: `scripts/quality/comment-hygiene.mjs` now also fails on presentation vocabulary (`verb`/`daily`) in comments and object-property keys (previously only filenames and declarations), enforced with word boundaries so `verbatim`/`verbose` and user-facing help titles like `DAILY LOOP` stay exempt. Source comments and the `verb: 'rebuild'` progress field were reworded/renamed to `command`.
@@ -88,6 +88,7 @@ install_v1_runner_assets() {
88
88
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
89
89
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
90
90
  fi
91
+ printf 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ "$MM_HARNESS_BIN" != %s ]; then echo "mm-harness overlay: %s: %s (reinstall to repoint: mm-harness install)" >&2; fi\n' "$runner_exec_q" 'delegating to the runner pinned at install time' "$runner_exec_q"
91
92
  printf 'exec %s "$@"\n' "$runner_exec_q"
92
93
  } > "$HARNESS_DIR/runner/bin/mm-harness"
93
94
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
@@ -18,7 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { spawnSync } from 'node:child_process';
20
20
  import process from 'node:process';
21
- import { recipeHarnessRoot } from '../../src/paths.ts';
21
+ import { recipeHarnessRoot } from '../shared/recipe-harness-root.mjs';
22
22
 
23
23
  function usage() { console.error('Usage: cleanup-extension-harness.mjs [--target <metamask-extension>]'); }
24
24
  let target = process.cwd();
@@ -23,7 +23,7 @@ import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import { spawnSync } from 'node:child_process';
25
25
  import process from 'node:process';
26
- import { recipeHarnessRoot } from '../../src/paths.ts';
26
+ import { recipeHarnessRoot } from '../shared/recipe-harness-root.mjs';
27
27
 
28
28
  function usage() {
29
29
  console.error('Usage: inject-extension-harness.mjs [--target <metamask-extension>] [--no-git-exclude]');
@@ -72,6 +72,7 @@ const delegate = [
72
72
  '#!/usr/bin/env bash',
73
73
  'set -euo pipefail',
74
74
  protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
75
+ `if [ -n "\${MM_HARNESS_BIN:-}" ] && [ "\$MM_HARNESS_BIN" != ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} ]; then echo "mm-harness overlay: delegating to the runner pinned at install time: ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} (reinstall to repoint: mm-harness install)" >&2; fi`,
75
76
  `exec ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} "$@"`,
76
77
  ].filter(Boolean).join('\n') + '\n';
77
78
  fs.writeFileSync(path.join(harnessDir, 'runner/bin/mm-harness'), delegate, { mode: 0o755 });
@@ -108,6 +109,8 @@ copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join
108
109
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
109
110
  copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
110
111
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
112
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
113
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
111
114
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
112
115
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
113
116
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
@@ -39,9 +39,13 @@ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
39
39
  for (const key of ['chrome-bin', 'profile', 'extension-dir', 'chrome-log', 'chrome-pid']) {
40
40
  if (!args[key]) throw new Error(`Missing --${key}`);
41
41
  }
42
- if (!fs.existsSync(args['chrome-bin'])) throw new Error(`Chrome binary not found: ${args['chrome-bin']}`);
43
- if (!fs.existsSync(path.join(args['extension-dir'], 'manifest.json'))) {
44
- throw new Error(`Extension dist manifest not found: ${path.join(args['extension-dir'], 'manifest.json')}`);
42
+ // --stop-only releases the profile before the dist snapshot exists on a first
43
+ // run, so launch-input validation applies only to a real launch.
44
+ if (args['stop-only'] === undefined) {
45
+ if (!fs.existsSync(args['chrome-bin'])) throw new Error(`Chrome binary not found: ${args['chrome-bin']}`);
46
+ if (!fs.existsSync(path.join(args['extension-dir'], 'manifest.json'))) {
47
+ throw new Error(`Extension dist manifest not found: ${path.join(args['extension-dir'], 'manifest.json')}`);
48
+ }
45
49
  }
46
50
 
47
51
  // Isolation guard: a standalone launch must use a per-checkout profile, never a
@@ -72,6 +76,14 @@ if (previousPid !== null && processLoadsProfile(previousPid, args.profile)) owne
72
76
  terminatePids([...ownedPids]);
73
77
  removeProfileSingletonLocks(args.profile);
74
78
 
79
+ // --stop-only: release the profile (owned Chrome terminated, singleton locks
80
+ // cleared) without launching. The prefill step needs the profile's LevelDB
81
+ // unlocked BEFORE it writes; a prior run's Chrome would hold the lock and the
82
+ // write fails with "Database is not open".
83
+ if (args['stop-only'] !== undefined) {
84
+ process.exit(0);
85
+ }
86
+
75
87
  const logFd = fs.openSync(args['chrome-log'], 'a');
76
88
  let child;
77
89
  try {
@@ -88,7 +100,9 @@ try {
88
100
  '--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
89
101
  `--disable-extensions-except=${args['extension-dir']}`,
90
102
  `--load-extension=${args['extension-dir']}`,
91
- 'chrome://extensions/',
103
+ // Initial tab: the caller's dapp URL when provided (sidepanel demos pair the
104
+ // extension sidepanel with a dapp in the main tab), else the extensions page.
105
+ args['start-url'] || 'chrome://extensions/',
92
106
  ], {
93
107
  detached: true,
94
108
  env: {
@@ -50,6 +50,7 @@ while [ "$#" -gt 0 ]; do
50
50
  --artifacts-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACTS="$2"; shift 2 ;;
51
51
  --prepare-cmd) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; PREPARE_CMD="$2"; shift 2 ;;
52
52
  --launch-existing-dist) LAUNCH_EXISTING_DIST=true; shift ;;
53
+ --start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
53
54
  --start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
54
55
  --dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
55
56
  --chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
@@ -114,8 +115,15 @@ fi
114
115
 
115
116
  if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
116
117
  DIST_ABS="$TARGET/$DIST_DIR"
117
- RUNTIME_DIST_ABS="$ARTIFACTS/runtime-dist"
118
- PROFILE_ABS="${CHROME_USER_DATA_DIR:-$ARTIFACTS/chrome-profile}"
118
+ # Stable beside the profile: the reused profile registers the unpacked
119
+ # extension by PATH; a per-run snapshot path gets pruned and the next launch
120
+ # loads nothing ("No EIP-6963 Provider Detected").
121
+ RUNTIME_DIST_ABS="$TARGET/temp/recipe/runtime/runtime-dist"
122
+ # Stable per-checkout profile: successive runs recognize their own Chrome
123
+ # (ownership guard matches on --user-data-dir), take it over cleanly, and the
124
+ # seeded wallet persists across relaunches. A per-run profile made every
125
+ # relaunch "foreign" and let live/ pruning delete profiles still in use.
126
+ PROFILE_ABS="${CHROME_USER_DATA_DIR:-$TARGET/temp/recipe/runtime/chrome-profile}"
119
127
  FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
120
128
  FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
121
129
  # Wallet fixture resolution chain + provenance live in seed-fixture.sh.
@@ -233,6 +241,7 @@ NODE
233
241
  if $START_WATCH; then
234
242
  prepare_parts+=("bash ${quoted_start_watch} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
235
243
  fi
244
+ prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
236
245
  prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
237
246
  # Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
238
247
  # manifest._flags wins over the fetched ClientConfigApi value. No-op unless
@@ -249,6 +258,9 @@ NODE
249
258
  prepare_parts+=("bash ${quoted_seed_fixture} prefill --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --profile ${quoted_profile} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file}")
250
259
  fi
251
260
  chrome_launch_cmd="node ${quoted_chrome_launcher} --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}"
261
+ if [ -n "${START_URL:-}" ]; then
262
+ chrome_launch_cmd="$chrome_launch_cmd --start-url $(printf '%q' "$START_URL")"
263
+ fi
252
264
  prepare_parts+=("$chrome_launch_cmd")
253
265
  prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
254
266
  if [ -n "$WALLET_FIXTURE_ABS" ]; then
@@ -97,7 +97,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
97
97
  exit 1
98
98
  }
99
99
  else
100
- echo "[refresh] activate-repo-node.sh missing from harness; run: recipe sync" >&2
100
+ echo "[refresh] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
101
101
  exit 1
102
102
  fi
103
103
 
@@ -186,5 +186,5 @@ kill "$BUILD_PID" 2>/dev/null || true
186
186
  wait "$BUILD_PID" 2>/dev/null || true
187
187
  trap - EXIT
188
188
 
189
- echo "[refresh] Build refreshed and frozen — run: recipe reopen"
189
+ echo "[refresh] Build refreshed and frozen — run: mm-harness launch"
190
190
  echo "[refresh] Log kept at: $LOG"
@@ -98,10 +98,12 @@ resolve_ext_id() {
98
98
  printf '%s\n' "$EXT_ID"
99
99
  return
100
100
  fi
101
- if [ -f "$AGENT_DIR/extension.id" ]; then
102
- tr -d '[:space:]' < "$AGENT_DIR/extension.id"
103
- return
104
- fi
101
+ for idf in "$AGENT_DIR/extension.id" "$REPO/temp/recipe/runtime/extension.id"; do
102
+ if [ -f "$idf" ]; then
103
+ tr -d '[:space:]' < "$idf"
104
+ return
105
+ fi
106
+ done
105
107
  json_list | python3 -c "import json,re,sys; d=json.load(sys.stdin); ids=[]; [ids.extend(re.findall(r'^chrome-extension://([^/]+)/', t.get('url',''))) for t in d]; print(ids[0] if ids else '')"
106
108
  }
107
109
 
@@ -239,15 +241,34 @@ const { chromium } = require('playwright');
239
241
  document.getElementById('__recipe_open_sidepanel__')?.remove();
240
242
  });
241
243
 
242
- if (createdPage) {
244
+ // The wallet now lives in the sidepanel: leftover extension home tabs would
245
+ // show it twice and steal the foreground from the dapp tab. Close every
246
+ // non-sidepanel extension page (not just one this helper created), then
247
+ // bring the first http(s) tab back to front.
248
+ for (const candidate of context.pages()) {
249
+ if (
250
+ candidate.url().startsWith(`chrome-extension://${extId}/`) &&
251
+ !candidate.url().includes('/sidepanel.html')
252
+ ) {
253
+ try {
254
+ await candidate.close();
255
+ } catch (error) {
256
+ console.warn(
257
+ `[sidepanel] extension page close failed after successful open: ${
258
+ error && error.message ? error.message : error
259
+ }`,
260
+ );
261
+ }
262
+ }
263
+ }
264
+ const dappPage = context
265
+ .pages()
266
+ .find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
267
+ if (dappPage) {
243
268
  try {
244
- await page.close();
245
- } catch (error) {
246
- console.warn(
247
- `[sidepanel] helper page close failed after successful open: ${
248
- error && error.message ? error.message : error
249
- }`,
250
- );
269
+ await dappPage.bringToFront();
270
+ } catch {
271
+ // focus is cosmetic; the panel is already open
251
272
  }
252
273
  }
253
274
  await browser.close();
@@ -82,7 +82,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
82
82
  exit 1
83
83
  }
84
84
  else
85
- echo "[recipe-harness] activate-repo-node.sh missing from harness; run: recipe sync" >&2
85
+ echo "[recipe-harness] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
86
86
  exit 1
87
87
  fi
88
88
  if [ -z "$WATCHER_PORT" ]; then
@@ -233,7 +233,7 @@ for _lib in "$SCRIPT_DIR/lib" "$SCRIPT_DIR/../shared"; do
233
233
  done
234
234
  unset _lib
235
235
  echo "[recipe-harness] Starting yarn start (watcher-port=${WATCHER_PORT:-default})"
236
- echo "[recipe-harness] compact build view - full log: $watch_log (recipe logs --full)"
236
+ echo "[recipe-harness] compact build view - full log: $watch_log (mm-harness logs --full)"
237
237
  started_pid=""
238
238
  # Launch the watcher detached writing to the log; the tmux window is a read-only tail viewer.
239
239
  path_prefix="${REPO_NODE_PATH_PREFIX:-}"
@@ -668,8 +668,8 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
668
668
  const page = await context.newPage();
669
669
  try {
670
670
  await page.goto(`chrome-extension://${candidate.id}/home.html`, {
671
- waitUntil: 'load',
672
- timeout: 10000,
671
+ waitUntil: 'domcontentloaded',
672
+ timeout: 30000,
673
673
  });
674
674
  if (page.url().startsWith('chrome-error://')) {
675
675
  throw new Error('candidate resolved to chrome-error page');
@@ -726,34 +726,48 @@ async function waitForWalletScreen(page) {
726
726
  return { state: 'unknown', selector: null };
727
727
  }
728
728
 
729
+ async function attemptUnlock(page, password) {
730
+ // Selector ladder: current testids first, then the generic selectors the CDP
731
+ // unlock action uses (proven against the same build). A freshly seeded vault
732
+ // can also still be initializing, so a single attempt is not conclusive —
733
+ // the caller retries.
734
+ const filled = await page.evaluate((pw) => {
735
+ const input =
736
+ document.querySelector('[data-testid="unlock-password"]') ??
737
+ document.querySelector('input[type="password"]');
738
+ if (!input) return false;
739
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
740
+ setter.call(input, pw);
741
+ input.dispatchEvent(new Event('input', { bubbles: true }));
742
+ input.dispatchEvent(new Event('change', { bubbles: true }));
743
+ return true;
744
+ }, password);
745
+ if (!filled) return false;
746
+ return page.evaluate(() => {
747
+ const button =
748
+ document.querySelector('[data-testid="unlock-submit"]') ??
749
+ document.querySelector('button[type="submit"]') ??
750
+ Array.from(document.querySelectorAll('button')).find((candidate) => /unlock/iu.test(candidate.innerText || candidate.textContent || ''));
751
+ if (!button) return false;
752
+ button.click();
753
+ return true;
754
+ });
755
+ }
756
+
729
757
  async function unlockIfNeeded(page, password) {
730
758
  let state = await waitForWalletScreen(page);
731
- if (state.state === 'locked') {
732
- await page.fill('[data-testid="unlock-password"]', password);
733
- try {
734
- await page.locator('[data-testid="unlock-submit"]').first().click({ timeout: 15000 });
735
- } catch (error) {
736
- const message = error && error.message ? error.message : String(error);
737
- if (!message.includes('Timeout')) throw error;
738
- const clicked = await page.evaluate(() => {
739
- const button = document.querySelector('[data-testid="unlock-submit"]');
740
- if (!button) return false;
741
- button.click();
742
- return true;
743
- });
744
- if (!clicked) throw new Error(`Unlock submit timed out and DOM fallback could not find the button: ${message}`);
745
- }
746
- const deadline = Date.now() + 45000;
747
- while (Date.now() < deadline) {
748
- state = await waitForWalletScreen(page);
749
- if (state.state === 'unlocked') {
750
- break;
751
- }
759
+ const deadline = Date.now() + 90000;
760
+ while (state.state !== 'unlocked' && state.state !== 'onboarding' && Date.now() < deadline) {
761
+ if (state.state === 'locked') {
762
+ await attemptUnlock(page, password);
763
+ await page.waitForTimeout(1500);
764
+ } else {
752
765
  await page.waitForTimeout(750);
753
766
  }
767
+ state = await waitForWalletScreen(page);
754
768
  }
755
769
  if (state.state !== 'unlocked') {
756
- throw new Error(`Wallet did not reach unlocked home screen after fixture seeding (state=${state.state})`);
770
+ throw new Error(`Wallet did not reach unlocked home screen after fixture seeding (state=${state.state}). Next: mm-harness call ensure_unlocked # then relaunch`);
757
771
  }
758
772
  return state;
759
773
  }
@@ -308,12 +308,41 @@
308
308
  },
309
309
  {
310
310
  "id": "lib/resolve-farmslot-ports",
311
- "entry": "adapters/shared/resolve-farmslot-ports.sh",
312
- "kind": "lib",
311
+ "entry": "adapters/shared/resolve-farmslot-ports.mjs",
312
+ "kind": "node",
313
313
  "purpose": "Resolve CDP + watcher ports from farmslot pool JSON by repo path, else 6660+N / 9010+N.",
314
- "inputs": "resolve_extension_runtime_ports <repo>",
314
+ "inputs": "resolve-farmslot-ports.mjs resolve_extension_runtime_ports <repo>",
315
315
  "outputs": "CDP_PORT / WATCHER_PORT / SLOT_ID lines on stdout"
316
316
  },
317
+ {
318
+ "id": "lib/resolve-farmslot-ports-core",
319
+ "entry": "adapters/shared/resolve-farmslot-ports-core.mjs",
320
+ "kind": "lib",
321
+ "purpose": "Self-contained pool/context/formula port resolution imported by the node leaf and TS surface.",
322
+ "inputs": "ESM import from resolve-farmslot-ports.mjs or resolve-farmslot-ports.ts",
323
+ "outputs": "resolveFarmslotPortsByRepo and related functions"
324
+ },
325
+ {
326
+ "id": "lib/recipe-harness-root",
327
+ "entry": "adapters/shared/recipe-harness-root.mjs",
328
+ "kind": "lib",
329
+ "purpose": "Resolve RECIPE_HARNESS_ROOT for overlay node leaves without importing src/paths.ts.",
330
+ "inputs": "ESM import; env RECIPE_HARNESS_ROOT optional override",
331
+ "outputs": "recipeHarnessRoot() relative path string"
332
+ },
333
+ {
334
+ "id": "lib/resolve-farmslot-ports-sh",
335
+ "entry": "adapters/shared/resolve-farmslot-ports.sh",
336
+ "kind": "lib",
337
+ "purpose": "Bash-compat sourced wrapper over resolve-farmslot-ports.mjs for stop-metro and legacy callers.",
338
+ "inputs": "source + resolve_* / apply_resolved_* function calls",
339
+ "outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
340
+ },
341
+ {
342
+ "id": "lib/reap-checkout-metros",
343
+ "entry": "adapters/shared/reap-checkout-metros.sh",
344
+ "kind": "lib"
345
+ },
317
346
  {
318
347
  "id": "lib/sync-wallet-fixture",
319
348
  "entry": "adapters/shared/sync-wallet-fixture.sh",
@@ -187,6 +187,7 @@ install_v1_runner_assets() {
187
187
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
188
188
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
189
189
  fi
190
+ printf 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ "$MM_HARNESS_BIN" != %s ]; then echo "mm-harness overlay: %s: %s (reinstall to repoint: mm-harness install)" >&2; fi\n' "$runner_exec_q" 'delegating to the runner pinned at install time' "$runner_exec_q"
190
191
  printf 'exec %s "$@"\n' "$runner_exec_q"
191
192
  } > "$HARNESS_DIR/runner/bin/mm-harness"
192
193
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
@@ -102,7 +102,7 @@ CLEAR_SUFFIX=""
102
102
 
103
103
  printf 'Starting Metro on port %s (workers=%s%s)\n' \
104
104
  "$PORT" "$METRO_WORKERS" "${CLEAR:+, clear}" >&2
105
- printf '(Full log: %s — recipe logs | recipe logs --full)\n' "$LOG_FILE" >&2
105
+ printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
106
106
 
107
107
  # Metro runs detached, writing to the log. The tmux window is a read-only tail.
108
108
  (
@@ -54,8 +54,23 @@ if [ -n "$pids" ]; then
54
54
  else
55
55
  printf 'Metro not running on port %s — nothing to stop\n' "$PORT" >&2
56
56
  fi
57
+
58
+ # The stop decision is made; everything below is best-effort cleanup (pid file,
59
+ # leaked-bundler sweep, tmux window). None of it may fail the command — the
60
+ # idempotent-stop contract is already satisfied. Drop set -e for the tail so an
61
+ # incidental non-zero (platform-dependent) can never turn success into exit 1.
62
+ set +e
57
63
  rm -f "$PID_FILE"
58
64
 
65
+ # Port-scoped stop above misses bundlers a prior launch left on a DIFFERENT port
66
+ # (port drift / missing pid file). Sweep every Metro bound to this checkout so
67
+ # stop fully cleans up — the leak that stacked bundlers across relaunches.
68
+ # Guarded: the reap lib may be absent in a minimal install; skip rather than abort.
69
+ if [ -f "$SCRIPT_DIR/../shared/reap-checkout-metros.sh" ]; then
70
+ . "$SCRIPT_DIR/../shared/reap-checkout-metros.sh"
71
+ reap_checkout_metros "$TARGET" || true
72
+ fi
73
+
59
74
  # Close the read-only log-tail window start-metro opened, if it is still there.
60
75
  if [ -f "$TMUX_FILE" ] && command -v tmux >/dev/null 2>&1; then
61
76
  win="$(cat "$TMUX_FILE" 2>/dev/null || true)"
@@ -64,3 +79,8 @@ if [ -f "$TMUX_FILE" ] && command -v tmux >/dev/null 2>&1; then
64
79
  fi
65
80
  fi
66
81
  rm -f "$TMUX_FILE"
82
+
83
+ # Reaching here = cleanup done (nothing needed stopping, or it stopped and
84
+ # windows/pids were cleared). The idempotent-stop contract is success; do not
85
+ # leak an incidental non-zero from the last command on any platform.
86
+ exit 0
@@ -296,7 +296,7 @@ if (checked === 0) {
296
296
  const parts = [];
297
297
  if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
298
298
  if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
299
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or recipe sync to refresh the in-repo HUD/AgenticService from the runner' }));
299
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
300
300
  } else {
301
301
  process.stdout.write(JSON.stringify({ name, status: 'pass' }));
302
302
  }
@@ -134,7 +134,7 @@ activate_repo_node() {
134
134
  require_repo_node() {
135
135
  local target="${1:-.}"
136
136
  if ! command -v activate_repo_node >/dev/null 2>&1; then
137
- echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (recipe sync)" >&2
137
+ echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (mm-harness install)" >&2
138
138
  return 1
139
139
  fi
140
140
  activate_repo_node "$target" || {