@deeeed/metamask-harness 0.13.0 → 0.14.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 (79) hide show
  1. package/CHANGELOG.md +17 -4
  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 +0 -0
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +0 -0
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +0 -0
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +0 -0
  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 +0 -0
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +0 -0
  17. package/adapters/extension/verify.sh +4 -4
  18. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  19. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  20. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  21. package/adapters/mobile/cleanup.sh +0 -0
  22. package/adapters/mobile/inject.sh +0 -0
  23. package/adapters/mobile/lib/metro-listener.sh +0 -0
  24. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  25. package/adapters/mobile/open-device.sh +0 -0
  26. package/adapters/mobile/prewarm-bundle.sh +0 -0
  27. package/adapters/mobile/start-metro.sh +15 -1
  28. package/adapters/mobile/stop-metro.sh +0 -0
  29. package/adapters/mobile/verify.sh +50 -13
  30. package/adapters/mobile/wait-for-bridge.sh +0 -0
  31. package/adapters/mobile/yarn-setup.sh +0 -0
  32. package/adapters/shared/activate-repo-node.sh +0 -0
  33. package/adapters/shared/activate-repo-ruby.sh +0 -0
  34. package/adapters/shared/cli-ux.sh +0 -0
  35. package/adapters/shared/ensure-runner-deps.sh +0 -0
  36. package/adapters/shared/harness-path.sh +0 -0
  37. package/adapters/shared/hash-helpers.sh +0 -0
  38. package/adapters/shared/json-field.sh +0 -0
  39. package/adapters/shared/log-tui.mjs +7 -3
  40. package/adapters/shared/open-log-window.sh +0 -0
  41. package/adapters/shared/reap-checkout-metros.sh +0 -0
  42. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  43. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  44. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  45. package/adapters/shared/resolve-slot-ports.sh +0 -0
  46. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  47. package/adapters/shared/tmux-session.sh +0 -0
  48. package/dist/cli-commands.js +5 -4
  49. package/dist/commands/call.js +61 -5
  50. package/dist/commands/device-target.js +10 -3
  51. package/dist/commands/fixtures.js +2 -1
  52. package/dist/commands/run-engine.js +62 -3
  53. package/dist/commands/run.js +2 -0
  54. package/dist/commands/self-test.js +1 -1
  55. package/dist/commands/status.js +19 -4
  56. package/dist/mm-harness-cli.js +7 -5
  57. package/dist/recipe-files.js +14 -0
  58. package/docs/architecture.md +2 -2
  59. package/docs/live-adapter-contract.md +1 -1
  60. package/docs/recipe-libraries.md +21 -18
  61. package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
  62. package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
  63. package/library/recipes/perps/smoke.core.recipe.json +39 -0
  64. package/library/recipes/perps/smoke.extension.recipe.json +51 -0
  65. package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
  66. package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
  67. package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
  68. package/package.json +2 -2
  69. package/scripts/completions.sh +0 -0
  70. package/scripts/install-completions.sh +0 -0
  71. /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
  72. /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
  73. /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
  74. /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
  75. /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
  76. /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
  77. /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
  78. /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
  79. /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 - 2026-07-08
4
+
5
+ ### Added
6
+ - `mm-harness call` accepts action fields as direct `key=value` shorthand in addition to repeated `--arg k=v`, so commands like `mm-harness call ui.navigate page=perps` now match the examples agents naturally write.
7
+ - Packaged recipes are grouped by domain under `library/recipes/<domain>/` and still resolve by dotted names (`perps.smoke`, `runner.smoke`) with legacy aliases for the former flat names.
8
+ - Packaged `perps.smoke` recipes for mobile, extension, and core provide an explicit non-mutating Perps domain smoke. The existing bare `smoke` alias remains a runner/transport smoke for compatibility.
9
+
10
+ ### Fixed
11
+ - `status` now hydrates slot runtime context before device enumeration, so a Farmslot checkout pinned to `IOS_SIMULATOR=mm-1` reports only the selected slot simulator instead of every booted simulator/device on the machine. Live status also updates `Next:` to `mm-harness logs` when the selected bridge is reachable.
12
+ - The tmux Metro window opened by `launch ios|android` now streams the Metro output it writes to `metro.log` instead of opening a blank server window.
13
+ - `log-tui watch` now matches success/failure patterns only against log content emitted after the watcher starts, so stale markers already present in long-lived Metro/webpack logs cannot false-pass readiness checks.
14
+ - `start-metro` now reaps any existing mobile console-forwarder attached to the same Metro port before spawning this slot's forwarder, even when the old process was launched from another checkout or package install path.
15
+
3
16
  ## 0.13.0 - 2026-07-08
4
17
 
5
18
  ### Added
@@ -29,7 +42,7 @@
29
42
  ### Added
30
43
  - Mobile recipes can now use the standard outer `app.lifecycle` action for deterministic performance start states without rebuilding (Android background/foreground/terminate/restart, iOS simulator launch/terminate/restart through the shared Farmslot lifecycle adapter).
31
44
  - `fixtures set --device <serial|udid|name>` now pins mobile fixture sync to the requested device using the same targeting rules as `run`, `call`, and `doctor`.
32
- - New lifecycle-controlled measured flows in the packaged library: `app-lifecycle-android-smoke`, `perps-performance-warm-start`, `perps-performance-background-resume`, `perps-performance-cold-start`; performance recipes now keep lifecycle/unlock in `setup[]`/`startState` so measured nodes carry only CUF timings.
45
+ - New lifecycle-controlled measured flows in the packaged library: `app.lifecycle.android-smoke`, `perps.performance.warm-start`, `perps.performance.background-resume`, `perps.performance.cold-start`; performance recipes now keep lifecycle/unlock in `setup[]`/`startState` so measured nodes carry only CUF timings.
33
46
 
34
47
  ### Changed
35
48
  - `@farmslot/recipe-harness` dependency raised to `^0.4.0` (ships the `app.lifecycle` adapter — the `FARMSLOT_ROOT` local-source fallback is no longer needed at runtime).
@@ -46,9 +59,9 @@
46
59
  ## 0.10.0 - 2026-07-07
47
60
 
48
61
  ### Added
49
- - **Canonical `perps-performance` measured flow** (`library/recipes/perps-performance.mobile.recipe.json`): unlock → open the Perps market list → read live state → open a market detail — one node per user-visible step with stable node names, so the per-node `duration`s in `trace.json` are the timings you monitor. Run pinned: `mm-harness run perps-performance --device <serial> --heal off`. Device-proven end-to-end on a physical Pixel.
62
+ - **Canonical `perps.performance` measured flow** (`library/recipes/perps/performance.mobile.recipe.json`): unlock → open the Perps market list → read live state → open a market detail — one node per user-visible step with stable node names, so the per-node `duration`s in `trace.json` are the timings you monitor. Run pinned: `mm-harness run perps.performance --device <serial> --heal off`. Device-proven end-to-end on a physical Pixel.
50
63
  - **`run <name>` resolves personal/team recipe libraries.** Previously only the packaged library was probed by name; custom recipes ran by path. Sources resolve in library-precedence order (personal > team shadow the packaged canonical — a same-named personal recipe wins), path-shaped args never probe libraries, and a miss teaches which sources were searched. Zero-flag default: `$FARMSLOT_HOME/recipe-library`.
51
- - **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps-performance-recipe` contract test, so the doc cannot drift from reality.
64
+ - **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps.performance-recipe` contract test, so the doc cannot drift from reality.
52
65
  - **`call` declared on mobile and extension manifests** (was core-only) with self-discovery metadata — personal `flows/` segments are now usable from mobile/extension recipes. Honest limitation documented: flows carry no adapter dimension in the protocol yet, so a cross-adapter `call` fails at live-run rather than plan time.
53
66
 
54
67
  ### Fixed
@@ -71,7 +84,7 @@
71
84
  ## 0.8.0 - 2026-07-07
72
85
 
73
86
  ### Added
74
- - **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `smoke.mobile.recipe.json`), then adapterless (`run perps-lifecycle`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
87
+ - **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `library/recipes/runner/smoke.mobile.recipe.json`), then adapterless (`run perps.lifecycle` → `library/recipes/perps/lifecycle.recipe.json`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
75
88
  - **`--device <udid|serial|name>` — first-class mobile device targeting on `run`, `call`, and `doctor`.** Resolves against connected devices (adb + booted simulators; exact id first, then exact name with an ambiguity teaching error) and sets the same env `launch --device` does. Mobile `run`/`call` **without** `--device` while more than one targetable device is connected (across android + ios; android state `device`, iOS `Booted`) fail fast listing the devices and the `--device` hint, so a recipe never lands on the wrong device silently. `run --plan`/`--list` are exempt; extension/core teach. Errors honor `--json` with structured envelopes.
76
89
  - **`status` gains `devices[]`** (additive): `{platform, id, name, state, selected}` for connected android devices and booted iOS simulators on mobile checkouts — the `yarn a:status` successor; `selected` reflects the current env pinning.
77
90
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -88,7 +88,7 @@ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
88
88
  # --out (optional): a task-local recipes dir. Resolve it safely within the target
89
89
  # (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
90
90
  # `live --out <dir>` does not silently fall back to the installed default.
91
- SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/smoke.extension.recipe.json"
91
+ SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/runner/smoke.extension.recipe.json"
92
92
  if [ -n "$OUT" ]; then
93
93
  # Fail fast on a task-local --out that does not contain the requested recipe.
94
94
  # Silently falling back to the installed default would validate a different
@@ -98,11 +98,11 @@ if [ -n "$OUT" ]; then
98
98
  echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
99
99
  exit 2
100
100
  fi
101
- if [ ! -f "$OUT_ABS/smoke.extension.recipe.json" ]; then
102
- echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
101
+ if [ ! -f "$OUT_ABS/runner/smoke.extension.recipe.json" ]; then
102
+ echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no runner/smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
103
103
  exit 2
104
104
  fi
105
- SMOKE_RECIPE="$OUT_ABS/smoke.extension.recipe.json"
105
+ SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
106
106
  fi
107
107
  ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
108
108
  mkdir -p "$ARTIFACTS/logs"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -86,6 +86,20 @@ start_console_forwarder() {
86
86
  local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
87
87
  [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
88
88
  { [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
89
+ # Only one debugger can own a React Native page. Reap any forwarder already
90
+ # attached to this Metro port, even if it was launched from another checkout or
91
+ # package install path and therefore does not own this runtime dir's pid file.
92
+ while IFS= read -r line; do
93
+ local pid cmd
94
+ pid="${line%% *}"
95
+ cmd="${line#* }"
96
+ [ "$pid" != "$line" ] || continue
97
+ case "$cmd" in
98
+ *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
99
+ kill "$pid" 2>/dev/null || true
100
+ ;;
101
+ esac
102
+ done < <(ps -axo pid=,command= 2>/dev/null || true)
89
103
  if [ -f "$fwd_pid_file" ]; then
90
104
  local old_fwd
91
105
  old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
@@ -126,7 +140,7 @@ set +a
126
140
  export EXPO_NO_TYPESCRIPT_SETUP=1
127
141
  export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
128
142
  export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
129
- exec yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag >> "$(printf '%q' "$LOG_FILE")" 2>&1
143
+ yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
130
144
  EOF
131
145
  chmod +x "$runner"
132
146
  }
File without changes
@@ -216,6 +216,34 @@ process.stdout.write(value);
216
216
  NODE
217
217
  }
218
218
 
219
+ runtime_context_value() {
220
+ TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
221
+ const fs = require('fs');
222
+ const path = require('path');
223
+ const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
224
+ const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
225
+ const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
226
+ const file = path.join(target, runtimeDir, 'agentic-runtime.json');
227
+ let data = {};
228
+ try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
229
+ for (const name of names) {
230
+ const value = data && data[name];
231
+ if (value !== undefined && value !== null && value !== '') {
232
+ process.stdout.write(String(value));
233
+ process.exit(0);
234
+ }
235
+ }
236
+ NODE
237
+ }
238
+
239
+ mobile_device_target() {
240
+ if [ "$PLATFORM" = "android" ]; then
241
+ printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
242
+ return
243
+ fi
244
+ printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
245
+ }
246
+
219
247
  check_file() {
220
248
  local rel="$1"
221
249
  if [ -e "$TARGET/$rel" ]; then
@@ -331,6 +359,7 @@ run_with_timeout() {
331
359
 
332
360
  live_status_ok() {
333
361
  local log_path="$1"
362
+ local device_target="$2"
334
363
  cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
335
364
  {
336
365
  "schema_version": 1,
@@ -349,12 +378,15 @@ live_status_ok() {
349
378
  JSON
350
379
  (
351
380
  cd "$TARGET"
352
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
381
+ device_args=()
382
+ [ -n "$device_target" ] && device_args=(--device "$device_target")
383
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
353
384
  ) > "$log_path" 2>&1
354
385
  }
355
386
 
356
387
  ensure_live_runtime() {
357
- if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log"; then
388
+ local device_target="$1"
389
+ if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
358
390
  return 0
359
391
  fi
360
392
  if [ "$AUTO_START" = true ]; then
@@ -366,7 +398,17 @@ EOF
366
398
  }
367
399
 
368
400
  if [ "$STATIC_ONLY" = false ]; then
369
- if ensure_live_runtime; then
401
+ device_target="$(mobile_device_target)"
402
+ if [ -z "$device_target" ]; then
403
+ if [ "$PLATFORM" = "android" ]; then
404
+ device_target="$(jsenv_value ADB_SERIAL)"
405
+ [ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
406
+ else
407
+ device_target="$(jsenv_value IOS_SIMULATOR)"
408
+ fi
409
+ fi
410
+
411
+ if ensure_live_runtime "$device_target"; then
370
412
  checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
371
413
  else
372
414
  checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
@@ -394,20 +436,13 @@ if [ "$STATIC_ONLY" = false ]; then
394
436
  {
395
437
  "schema_version": 1,
396
438
  "title": "Mobile v1 runner live bridge smoke",
397
- "description": "Verifies the installed MetaMask runner can drive the React Native debug bridge through Recipe v1 actions.",
439
+ "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
398
440
  "validate": {
399
441
  "workflow": {
400
442
  "entry": "status",
401
443
  "nodes": {
402
444
  "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
403
- "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "wallet-setup" },
404
- "wallet-setup": { "action": "metamask.wallet.setup", "intent": "Prepare the wallet fixture for the live bridge smoke", "timeout_ms": 45000, "next": "wallet-unlock" },
405
- "wallet-unlock": { "action": "metamask.wallet.ensure_unlocked", "intent": "Unlock the wallet through the manifest-declared action", "timeout_ms": 45000, "next": "wallet-read" },
406
- "wallet-read": { "action": "metamask.wallet.read_state", "intent": "Read wallet state before navigating to the wallet view", "timeout_ms": 45000, "next": "navigate-wallet" },
407
- "navigate-wallet": { "action": "ui.navigate", "intent": "Open the wallet view through the UI navigation action", "route": "WalletView", "timeout_ms": 45000, "next": "wait-wallet" },
408
- "wait-wallet": { "action": "ui.wait_for", "intent": "Wait until the wallet screen is present", "test_id": "wallet-screen", "expected": "present", "timeout_ms": 45000, "next": "hud-smoke" },
409
- "hud-smoke": { "action": "app.hud", "status": "running", "intent": "Show the Mobile live bridge smoke HUD", "progress": { "current": 1, "total": 1 }, "timeout_ms": 45000, "next": "screenshot" },
410
- "screenshot": { "action": "ui.screenshot", "intent": "Capture proof that the wallet screen is visible", "path": "screenshots/mobile-v1-live-smoke.png", "timeout_ms": 45000, "next": "done" },
445
+ "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "done" },
411
446
  "done": { "action": "end", "status": "pass" }
412
447
  }
413
448
  }
@@ -417,13 +452,15 @@ JSON
417
452
 
418
453
  ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
419
454
  adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
455
+ device_args=()
456
+ [ -n "$device_target" ] && device_args=(--device "$device_target")
420
457
  if (
421
458
  cd "$TARGET"
422
459
  METAMASK_RECIPE_AUTO_HUD=0 \
423
460
  IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
424
461
  ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
425
462
  ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
426
- "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
463
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --project-root "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
427
464
  ) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
428
465
  checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
429
466
  else
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -222,13 +222,17 @@ async function watchLog(options) {
222
222
  const chunk = readNewContent(logPath, offset);
223
223
  offset = chunk.offset;
224
224
  const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
225
+ // Match readiness only against bytes read after this watch started. This
226
+ // assumes line-oriented log writes; a marker split across partial writes
227
+ // waits for a later complete marker instead of stale-passing.
228
+ const matchText = chunk.text;
225
229
  const events = extractEvents(allText, eventsLimit);
226
230
 
227
- if (matchesPattern(allText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
231
+ if (matchesPattern(matchText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
228
232
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
229
233
  return 1;
230
234
  }
231
- if (matchesPattern(allText, successPattern)) {
235
+ if (matchesPattern(matchText, successPattern)) {
232
236
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
233
237
  if (mode !== 'quiet') {
234
238
  const elapsed = Math.round((Date.now() - started) / 1000);
@@ -357,4 +361,4 @@ async function main() {
357
361
  main().catch((error) => {
358
362
  console.error(error instanceof Error ? error.message : String(error));
359
363
  process.exit(1);
360
- });
364
+ });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { listRecipeFiles } from "./recipe-files.js";
4
5
  const SPEC = {
5
6
  shim: [
6
7
  { name: "sync", desc: "Copy fixture, write slot context, install harness", flags: ["--dry-run", "--no-install"] },
@@ -16,7 +17,7 @@ const SPEC = {
16
17
  { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
17
18
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
18
19
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port", "--device"] },
19
- { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
20
+ { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
20
21
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
21
22
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
22
23
  { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
@@ -124,9 +125,9 @@ function listRunRecipes(adapter, root, cwd = process.cwd()) {
124
125
  for (const resolveDir of RUN_RECIPE_DIRS) {
125
126
  const dir = resolveDir(adapter, root);
126
127
  if (!fs.existsSync(dir)) continue;
127
- for (const name of fs.readdirSync(dir)) {
128
- if (!name.endsWith(".recipe.json") || byBase.has(name)) continue;
129
- byBase.set(name, path.join(dir, name));
128
+ for (const rel of listRecipeFiles(dir)) {
129
+ if (byBase.has(rel)) continue;
130
+ byBase.set(rel, path.join(dir, rel));
130
131
  }
131
132
  }
132
133
  return [...byBase.values()].map((abs) => path.relative(cwd, abs)).sort();
@@ -16,6 +16,7 @@ import {
16
16
  runtimeOptionsFromCli,
17
17
  usageError
18
18
  } from "./parse-args.js";
19
+ import { getAdapterSurface } from "../adapters/surface.js";
19
20
  import { handleListExecutables } from "./list-executables.js";
20
21
  import { applyDeviceTargeting } from "./device-target.js";
21
22
  import {
@@ -33,7 +34,7 @@ async function handleCall(argv) {
33
34
  return handleListExecutables("call", options2);
34
35
  }
35
36
  if (argv.length > 0 && argv[0].startsWith("--")) {
36
- const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
37
+ const message = "call requires <action> first: mm-harness call <action> [key=value ...] [--arg k=v ...] [flags]";
37
38
  console.error(message);
38
39
  return EXIT.usage;
39
40
  }
@@ -60,6 +61,7 @@ async function handleCall(argv) {
60
61
  return EXIT.usage;
61
62
  }
62
63
  const { adapter, target } = resolveAdapter(options);
64
+ getAdapterSurface(adapter).resolveSlotPorts(target);
63
65
  const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
64
66
  if ("code" in dtResult) {
65
67
  if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
@@ -127,6 +129,7 @@ async function handleCall(argv) {
127
129
  state
128
130
  );
129
131
  if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
132
+ const callOutput = readCallOutput(result.tracePath);
130
133
  if (json) {
131
134
  console.log(
132
135
  JSON.stringify(
@@ -141,6 +144,7 @@ async function handleCall(argv) {
141
144
  summaryPath: result.summaryPath,
142
145
  tracePath: result.tracePath,
143
146
  artifactManifestPath: result.artifactManifestPath,
147
+ ...callOutput !== void 0 ? { output: callOutput } : {},
144
148
  recovered: state.recovered,
145
149
  mutations: state.mutations,
146
150
  exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
@@ -150,11 +154,29 @@ async function handleCall(argv) {
150
154
  )
151
155
  );
152
156
  } else {
153
- console.log(`call ${resolvedAction}: ${result.status}
157
+ const rendered = callOutput !== void 0 ? `
158
+ Result:
159
+ ${formatCallOutput(callOutput)}` : "";
160
+ console.log(`call ${resolvedAction}: ${result.status}${rendered}
154
161
  Artifacts: ${result.artifactManifestPath}`);
155
162
  }
156
163
  return result.status === "pass" ? EXIT.ok : EXIT.runtime;
157
164
  }
165
+ function readCallOutput(tracePath) {
166
+ try {
167
+ const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
168
+ const entries = Array.isArray(trace) ? trace : isRecord(trace) && Array.isArray(trace.entries) ? trace.entries : [];
169
+ const entry = entries.find((item) => isRecord(item) && item.nodeId === "call");
170
+ return isRecord(entry) && Object.prototype.hasOwnProperty.call(entry, "output") ? entry.output : void 0;
171
+ } catch {
172
+ return void 0;
173
+ }
174
+ }
175
+ function formatCallOutput(value) {
176
+ if (typeof value === "string") return value;
177
+ if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
178
+ return JSON.stringify(value, null, 2);
179
+ }
158
180
  async function handleCallHelp(argv, genericHelp) {
159
181
  const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
160
182
  const { options } = parseArgs(rest, "call");
@@ -194,13 +216,13 @@ function renderCallActionHelp(entry) {
194
216
  const required = new Set(
195
217
  Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === "string") : []
196
218
  );
197
- const lines = [`mm-harness call ${entry.name} [--arg k=v ...] [flags]`, ""];
219
+ const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
198
220
  if (entry.description) lines.push(` ${entry.description}`, "");
199
221
  const names = Object.keys(properties).sort();
200
222
  if (names.length === 0) {
201
223
  lines.push(" Fields: (none)");
202
224
  } else {
203
- lines.push(" Fields (pass with --arg <name>=<value>):");
225
+ lines.push(" Fields (pass as <name>=<value> or --arg <name>=<value>):");
204
226
  const width = Math.max(...names.map((name) => name.length));
205
227
  for (const name of names) {
206
228
  const prop = isRecord(properties[name]) ? properties[name] : {};
@@ -226,7 +248,7 @@ function renderCallExamples(short, examples) {
226
248
  for (const example of examples.slice(0, 2)) {
227
249
  const node = isRecord(example) && isRecord(example.node) ? example.node : void 0;
228
250
  if (!node) continue;
229
- const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) => `--arg ${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
251
+ const tokens = Object.entries(node).filter(([key]) => key !== "action" && key !== "intent").map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
230
252
  out.push(`mm-harness call ${short} ${tokens.join(" ")}`.trim());
231
253
  }
232
254
  return out;
@@ -249,10 +271,44 @@ function parseCallArgs(argv) {
249
271
  action = arg;
250
272
  continue;
251
273
  }
274
+ if (action !== void 0 && !isForwardedOptionValue(argv, i) && isArgPair(arg)) {
275
+ const eq = arg.indexOf("=");
276
+ args[arg.slice(0, eq)] = arg.slice(eq + 1);
277
+ continue;
278
+ }
252
279
  rest.push(arg);
253
280
  }
254
281
  return { action, args, rest };
255
282
  }
283
+ function isArgPair(value) {
284
+ const eq = value.indexOf("=");
285
+ return eq > 0;
286
+ }
287
+ function isForwardedOptionValue(argv, index) {
288
+ if (index === 0) return false;
289
+ const previous = argv[index - 1];
290
+ if (!previous.startsWith("--") || previous.includes("=")) return false;
291
+ return VALUE_TAKING_CALL_FLAGS.has(normalizeFlagName(previous));
292
+ }
293
+ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
294
+ "action-manifest",
295
+ "adapter",
296
+ "artifacts-dir",
297
+ "cdp-port",
298
+ "device",
299
+ "heal",
300
+ "library",
301
+ "metro-port",
302
+ "platform",
303
+ "project-root",
304
+ "slot",
305
+ "target",
306
+ "validation-runtime-dir",
307
+ "watcher-port"
308
+ ]);
309
+ function normalizeFlagName(flag) {
310
+ return flag.replace(/^--/u, "");
311
+ }
256
312
  function resolveActionName(shortName, names) {
257
313
  if (names.includes(shortName)) return { status: "ok", resolved: shortName, candidates: [shortName] };
258
314
  const finalSegment = (name) => name.split(".").pop() ?? name;
@@ -111,9 +111,16 @@ function applyDeviceTargeting(command, adapter, options, opts) {
111
111
  ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
112
112
  };
113
113
  }
114
- if (!opts.gate) return { ok: true };
115
114
  const connected = listConnectedDevices();
116
115
  const targetable = connected.filter(isTargetable);
116
+ const selectedTargetable = targetable.filter(deviceSelected);
117
+ if (selectedTargetable.length === 1) {
118
+ const selected = selectedTargetable[0];
119
+ if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
120
+ else setIosDeviceEnv(selected.id, selected.name);
121
+ return { ok: true };
122
+ }
123
+ if (!opts.gate) return { ok: true };
117
124
  if (targetable.length > 1) {
118
125
  return {
119
126
  ok: false,
@@ -121,8 +128,8 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
121
128
  message: `${targetable.length} mobile devices available \u2014 ${command} needs exactly one target.
122
129
  Connected devices:
123
130
  ${formatConnectedDevices(connected)}
124
- Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
125
- --device ${d.id}`).join("")}`
131
+ ` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
132
+ --device ${d.id}`).join("")}`)
126
133
  };
127
134
  }
128
135
  return { ok: true };
@@ -39,13 +39,14 @@ async function handleFixtures(argv, deps) {
39
39
  if (!adapter) {
40
40
  return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
41
41
  }
42
+ const surface = getAdapterSurface(adapter);
43
+ surface.resolveSlotPorts(target);
42
44
  const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
43
45
  if ("code" in dtResult) {
44
46
  return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
45
47
  }
46
48
  if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
47
49
  if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
48
- const surface = getAdapterSurface(adapter);
49
50
  if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
50
51
  const canonicalFixture = walletFixturePath(target);
51
52
  const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;