@deeeed/metamask-harness 0.4.0 → 0.5.1

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 (74) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/adapters/extension/inject.mjs +2 -0
  3. package/adapters/extension/refresh-build.sh +2 -2
  4. package/adapters/extension/start-watch.sh +2 -2
  5. package/adapters/manifest.json +19 -3
  6. package/adapters/mobile/start-metro.sh +1 -1
  7. package/adapters/mobile/verify.sh +1 -1
  8. package/adapters/shared/activate-repo-node.sh +1 -1
  9. package/adapters/shared/cli-ux.sh +24 -23
  10. package/adapters/shared/log-tui.mjs +4 -4
  11. package/adapters/shared/open-debug.mjs +1 -1
  12. package/adapters/shared/resolve-farmslot-ports-core.mjs +196 -0
  13. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  14. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  15. package/dist/adapters/core/surface.js +53 -0
  16. package/dist/adapters/extension/ensure-ready.js +109 -0
  17. package/dist/adapters/extension/extension-id.js +62 -0
  18. package/dist/adapters/extension/runtime-decision.js +305 -0
  19. package/dist/adapters/extension/runtime.js +324 -0
  20. package/dist/adapters/extension/surface.js +69 -0
  21. package/dist/adapters/mobile/deps-markers.js +22 -0
  22. package/dist/adapters/mobile/prepare.js +146 -0
  23. package/dist/adapters/mobile/provision.js +465 -0
  24. package/dist/adapters/mobile/runtime-decision.js +317 -0
  25. package/dist/adapters/mobile/surface.js +54 -0
  26. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  27. package/dist/adapters/slot-ports.js +140 -0
  28. package/dist/adapters/surface.js +14 -0
  29. package/dist/adapters.js +485 -0
  30. package/dist/cli-color.js +79 -0
  31. package/dist/cli-commands.js +224 -0
  32. package/dist/cli-version.js +111 -0
  33. package/dist/cli.js +1571 -0
  34. package/dist/commands/debug.js +56 -0
  35. package/dist/commands/fixtures.js +153 -0
  36. package/dist/commands/launch.js +325 -0
  37. package/dist/commands/logs.js +73 -0
  38. package/dist/commands/shared.js +157 -0
  39. package/dist/commands/update.js +243 -0
  40. package/dist/completions-cache.js +53 -0
  41. package/dist/doctor.js +169 -0
  42. package/dist/harness.js +627 -0
  43. package/dist/heal-bounds.js +120 -0
  44. package/dist/index.js +25 -0
  45. package/dist/leaf-invoke.js +19 -0
  46. package/dist/live-adapter-contract.js +240 -0
  47. package/dist/manifest.js +37 -0
  48. package/dist/mm-harness-cli.js +521 -0
  49. package/dist/paths.js +179 -0
  50. package/dist/progress.js +94 -0
  51. package/dist/recording-target.js +133 -0
  52. package/dist/run-recording.js +271 -0
  53. package/dist/runner.js +88 -0
  54. package/dist/types.js +0 -0
  55. package/docs/CLI-SPEC.md +26 -3
  56. package/library/actions/core/perps/_controller.mjs +1 -1
  57. package/library/actions/extension/platform/cdp.mjs +1 -1
  58. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  59. package/library/actions/harness-exports.mjs +27 -0
  60. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  61. package/library/actions/mobile/wallet/setup.mjs +1 -1
  62. package/package.json +5 -1
  63. package/src/adapters/core/surface.ts +15 -0
  64. package/src/adapters/extension/surface.ts +20 -3
  65. package/src/adapters/mobile/provision.ts +594 -0
  66. package/src/adapters/mobile/runtime-decision.ts +8 -1
  67. package/src/adapters/mobile/surface.ts +16 -4
  68. package/src/adapters/resolve-farmslot-ports.ts +13 -0
  69. package/src/adapters/slot-ports.ts +18 -25
  70. package/src/adapters/surface.ts +35 -0
  71. package/src/cli-commands.ts +1 -1
  72. package/src/cli.ts +149 -6
  73. package/src/harness.ts +140 -3
  74. package/src/mm-harness-cli.ts +52 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-07-05
4
+
5
+ Fresh-install hotfixes found in live validation of 0.5.0.
6
+
7
+ ### Fixed
8
+ - **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.
9
+ - **`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`.
10
+ - **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.
11
+
12
+ ## Unreleased
13
+
14
+ ### Changed
15
+ - **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.
16
+
17
+ ## 0.5.0 - 2026-07-05
18
+ Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
19
+ ### Added
20
+ - **`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.
21
+ - **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).
22
+ ### Changed
23
+ - 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.
24
+
3
25
  ## 0.4.0 - 2026-07-04
4
26
 
5
27
  Minor release: the adapter-surface architecture plus the day's feature wave (0.3.6-0.3.9 shipped fixes as they were validated live; this cut marks the surface additions honestly).
@@ -34,7 +56,18 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
34
56
 
35
57
  ## Unreleased
36
58
 
59
+ ### Added
60
+ - Add `mm-harness provision` for thin mobile Runway artifact provisioning with a shared integrity-checked cache and simulator install path.
61
+ - Add `mm-harness provision runway ios --resolve-only` to return Runway run/revision/artifact metadata without touching simulator, cache, download, install, or baseline state.
62
+ - **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`.
63
+
64
+ ### Changed
65
+ - **Leaf stderr hints use mm-harness verbs** — Metro/webpack compact-log prefixes, log-tail hints, and speed-ladder copy now point at `mm-harness logs`, `mm-harness doctor`, `mm-harness launch`, and `mm-harness install` instead of retired `recipe *` commands. Contract: `leaf-branding-retired`.
66
+
37
67
  ### Fixed
68
+ - `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.
69
+ - 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.
70
+ - 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.
38
71
  - **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.
39
72
  - **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.
40
73
  - **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.
@@ -108,6 +108,8 @@ copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join
108
108
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
109
109
  copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
110
110
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
111
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
112
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
111
113
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
112
114
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
113
115
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
@@ -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"
@@ -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:-}"
@@ -308,12 +308,28 @@
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/resolve-farmslot-ports-sh",
327
+ "entry": "adapters/shared/resolve-farmslot-ports.sh",
328
+ "kind": "lib",
329
+ "purpose": "Bash-compat sourced wrapper over resolve-farmslot-ports.mjs for stop-metro and legacy callers.",
330
+ "inputs": "source + resolve_* / apply_resolved_* function calls",
331
+ "outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
332
+ },
317
333
  {
318
334
  "id": "lib/sync-wallet-fixture",
319
335
  "entry": "adapters/shared/sync-wallet-fixture.sh",
@@ -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
  (
@@ -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" || {
@@ -67,7 +67,7 @@ recipe_instant_hint() {
67
67
  local message="$1"
68
68
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && return 0
69
69
  [ -t 2 ] || return 0
70
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
70
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
71
71
  printf '%s: %s\n' \
72
72
  "$(recipe_color label "$prefix")" \
73
73
  "$(recipe_color dim "$message")" >&2
@@ -92,7 +92,7 @@ recipe_done() {
92
92
  recipe_failure_playbook() {
93
93
  local verb="$1" reason="$2"
94
94
  shift 2
95
- local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-recipe}"
95
+ local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
96
96
  while [ "$#" -gt 0 ]; do
97
97
  case "$1" in
98
98
  --log) log_path="$2"; shift 2 ;;
@@ -106,31 +106,32 @@ recipe_failure_playbook() {
106
106
  "$(recipe_color cmd "$verb")" \
107
107
  "$(recipe_color err failed)" \
108
108
  "$(recipe_color warn "$reason")" >&2
109
- printf '%s\n' "$(recipe_color label next:)" >&2
109
+ printf '%s\n' "$(recipe_color label "next:")" >&2
110
110
  case "$verb" in
111
111
  rebuild|up|watch)
112
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
113
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
114
- printf ' %s\n' "$(recipe_color cmd "$prefix doctor")" >&2
115
- printf ' %s\n' "$(recipe_color cmd "$prefix sync")" >&2
112
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
113
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
114
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
115
+ printf ' %s\n' "$(recipe_color cmd "mm-harness install")" >&2
116
116
  ;;
117
117
  refresh)
118
- printf ' %s\n' "$(recipe_color cmd "$prefix watch")" >&2
119
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
120
- printf ' %s\n' "$(recipe_color cmd "$prefix rebuild")" >&2
118
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
119
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
120
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch --build")" >&2
121
121
  ;;
122
122
  metro|ios|android|setup-wallet)
123
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
124
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
125
- printf ' %s\n' "$(recipe_color cmd "$prefix up")" >&2
123
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
124
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
125
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
126
126
  ;;
127
127
  *)
128
- printf ' %s\n' "$(recipe_color cmd "$prefix status")" >&2
129
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
128
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
129
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
130
130
  ;;
131
131
  esac
132
132
  [ -n "$log_path" ] && printf '%s\n' "$(recipe_color path "log: $log_path")" >&2
133
133
  [ -n "$artifact" ] && printf '%s\n' "$(recipe_color path "artifact: $artifact")" >&2
134
+ return 0
134
135
  }
135
136
 
136
137
  recipe_check_harness_stale() {
@@ -152,12 +153,12 @@ recipe_check_harness_stale() {
152
153
 
153
154
  recipe_warn_harness_stale() {
154
155
  local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
155
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
156
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
156
157
  if recipe_check_harness_stale "$adapter" "$root" "$harness_root" "$runner_root"; then
157
158
  printf '%s: %s - run: %s\n' \
158
159
  "$(recipe_color label "$prefix")" \
159
160
  "$(recipe_color warn 'harness scripts are stale')" \
160
- "$(recipe_color cmd "$prefix sync")" >&2
161
+ "$(recipe_color cmd "mm-harness install")" >&2
161
162
  return 1
162
163
  fi
163
164
  return 0
@@ -190,7 +191,7 @@ recipe_try_farmslot_fixture_sync() {
190
191
  recipe_sync_wallet_fixture() {
191
192
  local root="$1" runtime_dir="$2" cdp_port="${3:-}" seed_script="${4:-}" farmslot_slot="${5:-}"
192
193
  local canonical="$root/$runtime_dir/wallet-fixture.json"
193
- local candidate resolved prefix="${RECIPE_CLI_PREFIX:-recipe}"
194
+ local candidate resolved prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
194
195
 
195
196
  mkdir -p "$root/$runtime_dir"
196
197
  if [ -f "$canonical" ]; then
@@ -239,10 +240,10 @@ recipe_sync_wallet_fixture() {
239
240
  recipe_speed_ladder() {
240
241
  cat >&2 <<'EOF'
241
242
  Speed ladder:
242
- FAST (seconds): recipe watch -> recipe refresh
243
- SLOW (minutes): recipe rebuild
244
- INSPECT: recipe status | recipe ports | recipe logs | recipe logs --full | recipe debug
245
- PROOF: recipe run <recipe.json>
246
- SYNC CONTEXT: recipe sync
243
+ FAST (seconds): mm-harness launch (extension quick relaunch)
244
+ SLOW (minutes): mm-harness launch --build
245
+ INSPECT: mm-harness doctor | mm-harness logs | mm-harness logs --full | mm-harness debug
246
+ PROOF: mm-harness run <recipe.json>
247
+ SYNC CONTEXT: mm-harness fixtures sync
247
248
  EOF
248
249
  }
@@ -153,9 +153,9 @@ function readNewContent(file, offset) {
153
153
 
154
154
  function renderHeader(label, logPath, title) {
155
155
  return [
156
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: recipe logs --full', { stream: process.stderr })}`,
156
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: mm-harness logs --full', { stream: process.stderr })}`,
159
159
  '',
160
160
  ];
161
161
  }
@@ -232,7 +232,7 @@ async function watchLog(options) {
232
232
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
233
233
  if (mode !== 'quiet') {
234
234
  const elapsed = Math.round((Date.now() - started) / 1000);
235
- process.stderr.write(`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
235
+ process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
236
236
  }
237
237
  return 0;
238
238
  }
@@ -221,7 +221,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
221
221
  error: 'cdp-unreachable',
222
222
  inspectUrl,
223
223
  configureHint,
224
- hint: `Start the extension runtime first (recipe reopen). Then open chrome://inspect and add ${configureHint}.`,
224
+ hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
225
225
  detail: error instanceof Error ? error.message : String(error),
226
226
  };
227
227
  }
@@ -0,0 +1,196 @@
1
+ // Farmslot pool + slot-suffix port resolution — pure JS for runner and overlay leaves.
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const coreDir = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ function pathDefault(key) {
11
+ for (const candidate of [
12
+ path.join(coreDir, 'path-defaults.json'),
13
+ path.join(coreDir, '../../adapters/shared/path-defaults.json'),
14
+ ]) {
15
+ if (!fs.existsSync(candidate)) continue;
16
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
17
+ if (value) return value;
18
+ }
19
+ throw new Error(`Missing path default: ${key}`);
20
+ }
21
+
22
+ function recipeRuntimeDir() {
23
+ return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
24
+ }
25
+
26
+ export function formatKvLines(kv) {
27
+ const lines = [];
28
+ if (kv.CDP_PORT !== undefined) lines.push(`CDP_PORT=${kv.CDP_PORT}`);
29
+ if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
30
+ if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
31
+ if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
32
+ return lines.length ? `${lines.join('\n')}\n` : '';
33
+ }
34
+
35
+ export function realRepoPath(repo) {
36
+ if (!repo) return null;
37
+ try {
38
+ return fs.realpathSync(repo);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function inferSlotSuffix(repo) {
45
+ const base = path.basename(repo);
46
+ const m = /-(\d+)$/u.exec(base);
47
+ return m ? m[1] : null;
48
+ }
49
+
50
+ function scoreSlot(slot, slotSuffix) {
51
+ const resources = slot.resources ?? {};
52
+ const cdp = resources.browser?.cdp_port;
53
+ const port = resources['dev-server']?.port;
54
+ const session = slot.session ?? '';
55
+ const slotId = slot.id ?? '';
56
+ let score = 0;
57
+ if (cdp !== undefined) score += 100;
58
+ if (port !== undefined) score += 10;
59
+ if (slotSuffix) {
60
+ if (session === `mme-${slotSuffix}`) score += 50;
61
+ if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
62
+ }
63
+ if (!slotId.toLowerCase().includes('demo')) score += 5;
64
+ return [score, cdp, port, slotId];
65
+ }
66
+
67
+ export function resolveFarmslotPortsByRepo(repo) {
68
+ const realRepo = realRepoPath(repo);
69
+ if (!realRepo) return null;
70
+
71
+ const slotSuffix = inferSlotSuffix(realRepo);
72
+ const roots = [];
73
+ if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
74
+ roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
75
+
76
+ for (const root of roots) {
77
+ if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
78
+ const poolDir = path.join(root, 'pool');
79
+ let best = null;
80
+
81
+ for (const name of fs.readdirSync(poolDir).sort()) {
82
+ if (!name.endsWith('.json') || name.includes('.bak.')) continue;
83
+ let data;
84
+ try {
85
+ data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
86
+ } catch {
87
+ continue;
88
+ }
89
+ for (const slot of data.slots ?? []) {
90
+ const slotRepo = slot.repo ?? '';
91
+ if (!slotRepo) continue;
92
+ let slotReal;
93
+ try {
94
+ slotReal = fs.realpathSync(slotRepo);
95
+ } catch {
96
+ continue;
97
+ }
98
+ if (slotReal !== realRepo) continue;
99
+ const scored = scoreSlot(slot, slotSuffix);
100
+ if (!best || scored[0] > best[0]) best = scored;
101
+ }
102
+ }
103
+
104
+ if (best) {
105
+ const [, cdp, port, slotId] = best;
106
+ return formatKvLines({
107
+ ...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
108
+ ...(port !== undefined ? { WATCHER_PORT: port } : {}),
109
+ ...(slotId ? { SLOT_ID: slotId } : {}),
110
+ });
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ export function resolveDefaultExtensionPorts(repo) {
117
+ const n = inferSlotSuffix(repo);
118
+ if (!n) return null;
119
+ return formatKvLines({
120
+ CDP_PORT: 6660 + Number(n),
121
+ WATCHER_PORT: 9010 + Number(n),
122
+ SLOT_ID: `local-extension-${n}`,
123
+ });
124
+ }
125
+
126
+ export function resolveExtensionRuntimePorts(repo) {
127
+ return resolveFarmslotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
128
+ }
129
+
130
+ export function resolveMobileSlotDefaults(repo) {
131
+ const n = inferSlotSuffix(repo);
132
+ if (!n) return null;
133
+ return formatKvLines({
134
+ WATCHER_PORT: 8060 + Number(n),
135
+ IOS_SIMULATOR: `mm-${n}`,
136
+ SLOT_ID: `local-mobile-${n}`,
137
+ });
138
+ }
139
+
140
+ export function resolveMobileRuntimeContext(repo) {
141
+ const ctxPath = path.join(repo, recipeRuntimeDir(), 'agentic-runtime.json');
142
+ if (fs.existsSync(ctxPath)) {
143
+ try {
144
+ const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
145
+ if (c.simulator || c.metroPort) {
146
+ return formatKvLines({
147
+ ...(c.metroPort !== undefined ? { WATCHER_PORT: c.metroPort } : {}),
148
+ ...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
149
+ ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
150
+ });
151
+ }
152
+ } catch {
153
+ /* unreadable context falls through to the provision baseline */
154
+ }
155
+ }
156
+ return resolveMobileProvisionBaseline(repo);
157
+ }
158
+
159
+ // A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
160
+ // provision baseline records the same authoritative simulator/port identity —
161
+ // without it, a fresh slot's first launch degrades to the simctl `booted`
162
+ // alias and misses the installed dev client entirely.
163
+ function resolveMobileProvisionBaseline(repo) {
164
+ const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
165
+ if (!fs.existsSync(basePath)) return null;
166
+ try {
167
+ const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
168
+ const simulator = c.simulator?.name || c.simulator?.udid;
169
+ if (!simulator && c.watcherPort === undefined) return null;
170
+ return formatKvLines({
171
+ ...(c.watcherPort !== undefined ? { WATCHER_PORT: c.watcherPort } : {}),
172
+ ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
173
+ ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
174
+ });
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ export function resolveMobileRuntimePorts(repo) {
181
+ return (
182
+ resolveMobileRuntimeContext(repo)
183
+ ?? resolveFarmslotPortsByRepo(repo)
184
+ ?? resolveMobileSlotDefaults(repo)
185
+ ?? ''
186
+ );
187
+ }
188
+
189
+ export const cliFns = {
190
+ resolve_farmslot_ports_by_repo: resolveFarmslotPortsByRepo,
191
+ resolve_default_extension_ports: resolveDefaultExtensionPorts,
192
+ resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
193
+ resolve_mobile_runtime_context: resolveMobileRuntimeContext,
194
+ resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
195
+ resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
196
+ };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Node leaf for port resolution — self-contained for runner and injected overlay copies.
3
+
4
+ import { cliFns } from './resolve-farmslot-ports-core.mjs';
5
+
6
+ const [, , fn, repo] = process.argv;
7
+ if (!fn || fn === '-h' || fn === '--help') {
8
+ process.stderr.write('Usage: resolve-farmslot-ports.mjs <fn> <repo>\n');
9
+ process.exit(fn ? 0 : 2);
10
+ }
11
+
12
+ const handler = cliFns[fn];
13
+ if (!handler) {
14
+ process.stderr.write(`resolve-farmslot-ports: unknown fn: ${fn}\n`);
15
+ process.exit(2);
16
+ }
17
+
18
+ const out = handler(repo ?? '.');
19
+ if (!out?.trim()) process.exit(1);
20
+ process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);