@deeeed/metamask-harness 0.17.5 → 0.19.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 (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+
8
+ const args = { port: '', out: '', err: '' };
9
+ for (let i = 2; i < process.argv.length; i += 1) {
10
+ const key = process.argv[i];
11
+ if (key === '--port') args.port = process.argv[++i] || '';
12
+ else if (key === '--out') args.out = process.argv[++i] || '';
13
+ else if (key === '--err') args.err = process.argv[++i] || '';
14
+ else if (key === '--help' || key === '-h') {
15
+ process.stdout.write('Usage: launch-console-forwarder.cjs --port <port> --out <app-log> --err <error-log>\n');
16
+ process.exit(0);
17
+ } else {
18
+ process.stderr.write(`launch-console-forwarder: unknown arg: ${key}\n`);
19
+ process.exit(2);
20
+ }
21
+ }
22
+
23
+ if (!args.port || !args.out || !args.err) {
24
+ process.stderr.write('launch-console-forwarder: --port, --out, and --err are required\n');
25
+ process.exit(2);
26
+ }
27
+
28
+ const forwarder = path.join(__dirname, 'bridge-runtime', 'console-forwarder.cjs');
29
+ const errFd = fs.openSync(args.err, 'a');
30
+ try {
31
+ const child = spawn(process.execPath, [forwarder, '--port', args.port, '--out', args.out], {
32
+ detached: true,
33
+ stdio: ['ignore', errFd, errFd],
34
+ });
35
+ child.unref();
36
+ process.stdout.write(`${child.pid}\n`);
37
+ } finally {
38
+ fs.closeSync(errFd);
39
+ }
@@ -13,6 +13,7 @@
13
13
  # --simulator <udid|name|"booted"> iOS simulator (default: IOS_SIMULATOR env, else "booted")
14
14
  # --adb-serial <serial> Android device serial (default: ADB_SERIAL env)
15
15
  # --preflight-mode fast|auto|rebuild-native|clean (default: fast; env MOBILE_PREFLIGHT_MODE)
16
+ # --ui-only iOS only: ensure Simulator.app is visible; do not relaunch the app
16
17
  # Outputs:
17
18
  # Progress on stderr; exit 0 pass; 1 open failed; 2 bad args.
18
19
  #
@@ -25,6 +26,7 @@ SIMULATOR="${IOS_SIMULATOR:-${SIM_UDID:-booted}}"
25
26
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
26
27
  PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
27
28
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
29
+ UI_ONLY=false
28
30
 
29
31
  while [ "$#" -gt 0 ]; do
30
32
  case "$1" in
@@ -34,10 +36,11 @@ while [ "$#" -gt 0 ]; do
34
36
  --adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
35
37
  --preflight-mode) PREFLIGHT_MODE="$2"; shift 2 ;;
36
38
  --port) PORT="$2"; shift 2 ;;
39
+ --ui-only) UI_ONLY=true; shift ;;
37
40
  -h|--help)
38
41
  printf 'Usage: open-device.sh [--platform ios|android] [--target <dir>]\n'
39
42
  printf ' [--simulator <udid|name|booted>] [--adb-serial <serial>]\n'
40
- printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>]\n'
43
+ printf ' [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>] [--ui-only]\n'
41
44
  exit 0
42
45
  ;;
43
46
  *) printf 'open-device: unknown arg: %s\n' "$1" >&2; exit 2 ;;
@@ -48,6 +51,10 @@ case "$PLATFORM" in
48
51
  ios|android) ;;
49
52
  *) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
50
53
  esac
54
+ if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
55
+ printf 'open-device: --ui-only is supported only for ios.\n' >&2
56
+ exit 2
57
+ fi
51
58
 
52
59
  TARGET="$(cd "$TARGET" && pwd)"
53
60
 
@@ -149,19 +156,46 @@ boot_simulator_if_needed() {
149
156
  xcrun simctl bootstatus "$udid" -b >/dev/null
150
157
  }
151
158
 
152
- show_simulator() {
159
+ simulator_ui_running() {
160
+ pgrep -x Simulator >/dev/null 2>&1
161
+ }
162
+
163
+ open_simulator_in_background_if_needed() {
153
164
  local target="$1" udid=""
165
+ [ "${MM_HARNESS_SHOW_SIMULATOR:-1}" != "0" ] || return 0
166
+ simulator_ui_running && return 0
167
+ printf 'Opening Simulator UI in the background\n' >&2
154
168
  if [ "$target" = "booted" ]; then
155
- printf 'Opening Simulator UI\n' >&2
156
- open -a Simulator >/dev/null 2>&1 || true
169
+ open -g -a Simulator >/dev/null 2>&1 || {
170
+ printf 'open-device: failed to open Simulator UI.\n' >&2
171
+ return 1
172
+ }
157
173
  else
158
174
  udid="$(sim_udid_for_target "$target")"
159
175
  if [ -n "$udid" ]; then
160
- open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
176
+ open -g -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || {
177
+ printf 'open-device: failed to open Simulator UI for %s.\n' "$target" >&2
178
+ return 1
179
+ }
161
180
  else
162
- open -a Simulator >/dev/null 2>&1 || true
181
+ open -g -a Simulator >/dev/null 2>&1 || {
182
+ printf 'open-device: failed to open Simulator UI.\n' >&2
183
+ return 1
184
+ }
163
185
  fi
164
186
  fi
187
+ }
188
+
189
+ focus_simulator() {
190
+ local target="$1" udid=""
191
+ if [ "$target" != "booted" ]; then
192
+ udid="$(sim_udid_for_target "$target")"
193
+ fi
194
+ if [ -n "$udid" ]; then
195
+ open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
196
+ else
197
+ open -a Simulator >/dev/null 2>&1 || true
198
+ fi
165
199
  osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
166
200
  }
167
201
 
@@ -220,8 +254,12 @@ if [ "$PLATFORM" = "ios" ]; then
220
254
  DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
221
255
 
222
256
  boot_simulator_if_needed "$SIM_TARGET" || exit 1
257
+ open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
223
258
  if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
224
- show_simulator "$SIM_TARGET"
259
+ focus_simulator "$SIM_TARGET"
260
+ fi
261
+ if $UI_ONLY; then
262
+ exit 0
225
263
  fi
226
264
 
227
265
  LAUNCHED=false
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Ensure one runner-owned CDP console forwarder is alive for this Mobile slot.
3
+ set -uo pipefail
4
+
5
+ TARGET="$PWD"
6
+ PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
7
+ QUIET=false
8
+
9
+ while [ "$#" -gt 0 ]; do
10
+ case "$1" in
11
+ --target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
12
+ --port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
13
+ --quiet) QUIET=true; shift ;;
14
+ -h|--help)
15
+ printf 'Usage: start-console-forwarder.sh [--target <dir>] [--port <port>] [--quiet]\n'
16
+ exit 0
17
+ ;;
18
+ *) printf 'start-console-forwarder: unknown arg: %s\n' "$1" >&2; exit 2 ;;
19
+ esac
20
+ done
21
+
22
+ [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || exit 0
23
+ TARGET="$(cd "$TARGET" && pwd -P)"
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ # shellcheck disable=SC1091
26
+ . "$SCRIPT_DIR/../shared/harness-path.sh"
27
+ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
28
+ FORWARDER="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
29
+ PID_FILE="$LOG_DIR/console-forwarder.pid"
30
+ APP_LOG_FILE="$LOG_DIR/app-console.log"
31
+
32
+ { [ -f "$FORWARDER" ] && command -v node >/dev/null 2>&1; } || exit 0
33
+ mkdir -p "$LOG_DIR"
34
+
35
+ # One debugger owns a React Native page. Reap any collector for this Metro port,
36
+ # including one left by an older globally installed harness.
37
+ while read -r pid cmd; do
38
+ case "$cmd" in
39
+ *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
40
+ kill "$pid" 2>/dev/null || true
41
+ ;;
42
+ esac
43
+ done < <(ps -axo pid=,command= 2>/dev/null || true)
44
+
45
+ if [ -f "$PID_FILE" ]; then
46
+ old_pid="$(cat "$PID_FILE" 2>/dev/null || true)"
47
+ if [ -n "$old_pid" ]; then
48
+ case "$(ps -p "$old_pid" -o command= 2>/dev/null)" in
49
+ *console-forwarder.cjs*) kill "$old_pid" 2>/dev/null || true ;;
50
+ esac
51
+ fi
52
+ rm -f "$PID_FILE"
53
+ fi
54
+
55
+ : > "$APP_LOG_FILE"
56
+ pid="$(node "$SCRIPT_DIR/launch-console-forwarder.cjs" \
57
+ --port "$PORT" \
58
+ --out "$APP_LOG_FILE" \
59
+ --err "$LOG_DIR/console-forwarder.err")" || exit 1
60
+ printf '%s\n' "$pid" > "$PID_FILE"
61
+
62
+ # A successful launch must never claim capture when the collector died at boot.
63
+ sleep 0.1
64
+ if ! kill -0 "$pid" 2>/dev/null; then
65
+ rm -f "$PID_FILE"
66
+ printf 'start-console-forwarder: collector exited during startup; inspect %s\n' \
67
+ "$LOG_DIR/console-forwarder.err" >&2
68
+ exit 1
69
+ fi
70
+ [ "$QUIET" = true ] || printf 'Device console → app-console.log (CDP forwarder pid %s)\n' "$pid" >&2
@@ -77,45 +77,6 @@ default_metro_workers() {
77
77
  # shellcheck disable=SC1091
78
78
  . "$SCRIPT_DIR/lib/tmux-viewer.sh"
79
79
 
80
- # Bridgeless RN gates console→Metro forwarding on console._isPolyfilled, which is
81
- # false with the Hermes native console — device logs (incl. DevLogger) never reach
82
- # metro.log. The forwarder attaches over CDP and appends them back. Kill switch:
83
- # METAMASK_RECIPE_CONSOLE_FORWARD=0.
84
- start_console_forwarder() {
85
- local forwarder="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
86
- local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
87
- [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
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
- local pid cmd
93
- while read -r pid cmd; do
94
- case "$cmd" in
95
- *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
96
- kill "$pid" 2>/dev/null || true
97
- ;;
98
- esac
99
- done < <(ps -axo pid=,command= 2>/dev/null || true)
100
- if [ -f "$fwd_pid_file" ]; then
101
- local old_fwd
102
- old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
103
- # A crash can leave a stale pid file and the OS can recycle the pid for an
104
- # unrelated process — only kill when the command line is really ours.
105
- if [ -n "$old_fwd" ]; then
106
- case "$(ps -p "$old_fwd" -o command= 2>/dev/null)" in
107
- *console-forwarder.cjs*) kill "$old_fwd" 2>/dev/null ;;
108
- esac
109
- fi
110
- rm -f "$fwd_pid_file"
111
- fi
112
- nohup node "$forwarder" --port "$PORT" --out "$LOG_FILE" \
113
- </dev/null >> "$LOG_DIR/console-forwarder.err" 2>&1 &
114
- echo "$!" > "$fwd_pid_file"
115
- disown 2>/dev/null || true
116
- printf 'Device console → metro.log (CDP forwarder pid %s)\n' "$(cat "$fwd_pid_file")" >&2
117
- }
118
-
119
80
  write_metro_runner() {
120
81
  local runner="$1"
121
82
  local clear_flag=""
@@ -179,7 +140,6 @@ if metro_ready; then
179
140
  rm -f "$PID_FILE"
180
141
  elif metro_listener_valid; then
181
142
  printf 'Metro already running and valid on port %s\n' "$PORT" >&2
182
- start_console_forwarder
183
143
  exit 0
184
144
  else
185
145
  stop_metro_listener || exit 1
@@ -269,7 +229,6 @@ if [ "$READY" = true ]; then
269
229
  first_ready_pid="${ready_pids%% *}"
270
230
  [ -z "$first_ready_pid" ] || printf '%s\n' "$first_ready_pid" > "$PID_FILE"
271
231
  printf 'Metro ready on port %s\n' "$PORT" >&2
272
- start_console_forwarder
273
232
  printf ' Next: mm-harness logs (tail Metro)\n' >&2
274
233
  exit 0
275
234
  fi
@@ -261,76 +261,19 @@ check_file "package.json"
261
261
  check_file "app/dev-tools/AgenticService/AgenticService.ts"
262
262
 
263
263
  if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
264
- checks+=("{\"name\":\"NavigationService patch\",\"status\":\"fail\"}")
264
+ checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
265
265
  status="fail"
266
266
  else
267
- checks+=("{\"name\":\"NavigationService patch\",\"status\":\"pass\"}")
267
+ checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"pass\"}")
268
268
  fi
269
269
 
270
270
  if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
271
- checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"fail\"}")
271
+ checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
272
272
  status="fail"
273
273
  else
274
- checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"pass\"}")
274
+ checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"pass\"}")
275
275
  fi
276
276
 
277
- # Drift detection (read-only): compare the in-repo AgenticService/HUD against
278
- # the resolved runner overlay, not a runner-owned copy. The runner is the only
279
- # MetaMask-aware source of truth for Mobile injection.
280
- overlay_drift_check="$(
281
- HARNESS_DIR_FOR_DRIFT="$HARNESS_DIR" TARGET_FOR_DRIFT="$TARGET" node <<'NODE'
282
- const fs = require('fs');
283
- const path = require('path');
284
- const name = 'agentic overlay matches runner (HUD freshness)';
285
- const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
286
- const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'dev-tools', 'AgenticService');
287
- function readTrimmed(file) {
288
- try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
289
- }
290
- let runnerDir = readTrimmed(path.join(harnessDir, 'runner', '.runner-source'));
291
- if (!runnerDir) {
292
- try {
293
- const manifest = JSON.parse(fs.readFileSync(path.join(harnessDir, 'manifest.json'), 'utf8'));
294
- runnerDir = manifest && manifest.source && manifest.source.runnerDir || '';
295
- } catch {}
296
- }
297
- if (!runnerDir) {
298
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
299
- process.exit(0);
300
- }
301
- const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'dev-tools', 'AgenticService');
302
- let checked = 0;
303
- const drifted = [];
304
- const missing = [];
305
- try {
306
- for (const entry of fs.readdirSync(overlayDir)) {
307
- if (!entry.endsWith('.patch')) continue;
308
- if (/\.test\./u.test(entry)) continue;
309
- const base = entry.slice(0, -'.patch'.length);
310
- const targetFile = path.join(targetDir, base);
311
- if (!fs.existsSync(targetFile)) { missing.push(base); continue; }
312
- checked += 1;
313
- if (fs.readFileSync(path.join(overlayDir, entry), 'utf8') !== fs.readFileSync(targetFile, 'utf8')) {
314
- drifted.push(base);
315
- }
316
- }
317
- } catch (error) {
318
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
319
- process.exit(0);
320
- }
321
- if (drifted.length || missing.length) {
322
- const parts = [];
323
- if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
324
- if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
325
- 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' }));
326
- } else if (checked === 0) {
327
- process.stdout.write(JSON.stringify({ name, status: 'pass' }));
328
- } else {
329
- process.stdout.write(JSON.stringify({ name, status: 'pass' }));
330
- }
331
- NODE
332
- )"
333
- checks+=("$overlay_drift_check")
334
277
  run_with_timeout() {
335
278
  local log_path="$1"
336
279
  local timeout_s="$2"
@@ -362,16 +305,14 @@ live_status_ok() {
362
305
  local device_target="$2"
363
306
  cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
364
307
  {
365
- "schema_version": 1,
308
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
366
309
  "title": "Mobile runner status smoke",
367
310
  "description": "Checks that the installed runner can read Mobile app status before live verification.",
368
- "validate": {
369
- "workflow": {
370
- "entry": "status",
371
- "nodes": {
372
- "status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
373
- "done": { "action": "end", "status": "pass" }
374
- }
311
+ "workflow": {
312
+ "entry": "status",
313
+ "nodes": {
314
+ "status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
315
+ "done": { "action": "end", "status": "pass" }
375
316
  }
376
317
  }
377
318
  }
@@ -380,15 +321,27 @@ JSON
380
321
  cd "$TARGET"
381
322
  device_args=()
382
323
  [ -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
324
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
384
325
  ) > "$log_path" 2>&1
385
326
  }
386
327
 
387
328
  ensure_live_runtime() {
388
329
  local device_target="$1"
389
- if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
390
- return 0
391
- fi
330
+ local attempt
331
+ for attempt in 1 2 3 4 5; do
332
+ if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
333
+ return 0
334
+ fi
335
+ # A freshly booted simulator can expose its Metro bridge just before
336
+ # `simctl list devices booted` publishes the device. Bound that launch→verify
337
+ # race instead of reporting a false DEVICE_NOT_FOUND immediately.
338
+ if [ "$attempt" -lt 5 ] && grep -q '"code"[[:space:]]*:[[:space:]]*"DEVICE_NOT_FOUND"' "$ARTIFACTS/logs/app-status-precheck.log"; then
339
+ echo "Waiting for Mobile device discovery (${attempt}/5): ${device_target:-configured target}" >&2
340
+ sleep 1
341
+ continue
342
+ fi
343
+ break
344
+ done
392
345
  if [ "$AUTO_START" = true ]; then
393
346
  cat >&2 <<'EOF'
394
347
  Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.
@@ -434,17 +387,15 @@ if [ "$STATIC_ONLY" = false ]; then
434
387
 
435
388
  cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
436
389
  {
437
- "schema_version": 1,
390
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
438
391
  "title": "Mobile v1 runner live bridge smoke",
439
392
  "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
440
- "validate": {
441
- "workflow": {
442
- "entry": "status",
443
- "nodes": {
444
- "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
445
- "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "required": true, "timeout_ms": 15000, "next": "done" },
446
- "done": { "action": "end", "status": "pass" }
447
- }
393
+ "workflow": {
394
+ "entry": "status",
395
+ "nodes": {
396
+ "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
397
+ "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 15000, "next": "done" },
398
+ "done": { "action": "end", "status": "pass" }
448
399
  }
449
400
  }
450
401
  }
@@ -460,7 +411,7 @@ JSON
460
411
  IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
461
412
  ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
462
413
  ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
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
414
+ "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
464
415
  ) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
465
416
  checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
466
417
  else
@@ -24,10 +24,10 @@ REQUIRE_PLATFORM=""
24
24
 
25
25
  while [ "$#" -gt 0 ]; do
26
26
  case "$1" in
27
- --target) TARGET="$2"; shift 2 ;;
28
- --port) PORT="$2"; shift 2 ;;
29
- --max-polls) MAX_POLLS="$2"; shift 2 ;;
30
- --platform) REQUIRE_PLATFORM="$2"; shift 2 ;;
27
+ --target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
28
+ --port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
29
+ --max-polls) [ "$#" -ge 2 ] || { echo "Missing value for --max-polls" >&2; exit 2; }; MAX_POLLS="$2"; shift 2 ;;
30
+ --platform) [ "$#" -ge 2 ] || { echo "Missing value for --platform" >&2; exit 2; }; REQUIRE_PLATFORM="$2"; shift 2 ;;
31
31
  -h|--help)
32
32
  printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>] [--platform ios|android]\n'
33
33
  exit 0
@@ -36,7 +36,12 @@ while [ "$#" -gt 0 ]; do
36
36
  esac
37
37
  done
38
38
 
39
- TARGET="$(cd "$TARGET" && pwd)"
39
+ case "$MAX_POLLS" in
40
+ ''|*[!0-9]*) echo "wait-for-bridge: --max-polls must be an integer of at least 2." >&2; exit 2 ;;
41
+ esac
42
+ [ "$MAX_POLLS" -ge 2 ] || { echo "wait-for-bridge: --max-polls must be at least 2 for stable readiness." >&2; exit 2; }
43
+
44
+ TARGET="$(cd "$TARGET" && pwd -P)"
40
45
 
41
46
  # The matcher reads the required platform from the environment so the value survives
42
47
  # into the node subprocess that runs the shared matcher against the status output.
@@ -128,15 +133,24 @@ try {
128
133
  ' "$MATCH_LIB" "$STATUS_LOG" 2>/dev/null || printf 'none'
129
134
  }
130
135
 
136
+ READY_STREAK=0
131
137
  for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
132
138
  if bridge_has_route; then
133
- printf 'Mobile bridge ready\n' >&2
134
- exit 0
139
+ READY_STREAK=$((READY_STREAK + 1))
140
+ # The bridge can appear briefly while React Native is still completing its
141
+ # startup navigation/reload. Require two consecutive healthy snapshots so a
142
+ # following command never inherits that transient gap.
143
+ if [ "$READY_STREAK" -ge 2 ]; then
144
+ printf 'Mobile bridge ready\n' >&2
145
+ exit 0
146
+ fi
147
+ else
148
+ READY_STREAK=0
135
149
  fi
136
150
  if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
137
151
  printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
138
152
  fi
139
- sleep 2
153
+ [ "$ATTEMPT" -ge "$MAX_POLLS" ] || sleep 2
140
154
  done
141
155
 
142
156
  cat "$STATUS_LOG" >&2 || true
@@ -12,27 +12,29 @@
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import crypto from 'node:crypto';
15
+ import { pathToFileURL } from 'node:url';
15
16
 
16
- // Trees whose contents are copied into <slot>/harness by inject.mjs. Any change
17
- // here means injected slots are stale.
18
- const SOURCE_DIRS = ['adapters/extension', 'adapters/shared', 'library'];
19
-
20
- export function harnessSourceFingerprint(runnerDir) {
17
+ export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
18
+ if (adapter !== 'extension' && adapter !== 'mobile') {
19
+ throw new Error(`Unsupported harness fingerprint adapter: ${adapter}`);
20
+ }
21
+ const sourceDirs = [`adapters/${adapter}`, 'adapters/shared', 'library'];
21
22
  const entries = [];
22
- for (const rel of SOURCE_DIRS) {
23
+ for (const rel of sourceDirs) {
23
24
  collect(path.join(runnerDir, rel), runnerDir, entries);
24
25
  }
25
26
  entries.sort();
26
27
  return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
27
28
  }
28
29
 
30
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
31
+ process.stdout.write(`${harnessSourceFingerprint(path.resolve(process.argv[2] ?? '.'), process.argv[3])}\n`);
32
+ }
33
+
29
34
  function collect(dir, root, out) {
30
- let dirents;
31
- try {
32
- dirents = fs.readdirSync(dir, { withFileTypes: true });
33
- } catch {
34
- return; // absent tree contributes nothing
35
- }
35
+ // These trees are required runner inputs. Fail closed instead of producing a
36
+ // weaker fingerprint that could make a stale injected harness appear current.
37
+ const dirents = fs.readdirSync(dir, { withFileTypes: true });
36
38
  for (const dirent of dirents) {
37
39
  // Skip hidden noise (.DS_Store, editor swap files) so the fingerprint tracks
38
40
  // only real source and does not thrash re-injects.
@@ -172,10 +172,10 @@ function extensionRuntimeContextPath(repo) {
172
172
  const repoRoot = path.resolve(repo);
173
173
  const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
174
174
  if (envCtx) {
175
- const resolved = path.resolve(envCtx);
176
- if (resolved === repoRoot || resolved.startsWith(`${repoRoot}${path.sep}`)) {
177
- return resolved;
178
- }
175
+ // Farmslot may manage runtime context outside the checkout. Treat the
176
+ // explicit environment override as authoritative; containment is not a
177
+ // security boundary and silently dropping it selects the wrong port.
178
+ return path.resolve(envCtx);
179
179
  }
180
180
  return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
181
181
  }
@@ -198,12 +198,22 @@ export function resolveExtensionRuntimeContext(repo) {
198
198
  }
199
199
  }
200
200
 
201
- // Checkout-scoped extension ports: runtime context (authoritative) pool suffix formula.
202
- export function formatExtensionSlotPorts(repo) {
201
+ // Context and pool describe checkout identity and therefore replace stale
202
+ // inherited env values. The suffix formula is only a fallback and is applied
203
+ // separately by callers so an explicitly supplied env port can still win.
204
+ export function formatExtensionIdentityPorts(repo) {
203
205
  const realRepo = path.resolve(repo);
204
206
  return mergeKvLayers(
205
207
  { source: resolveExtensionRuntimeContext(realRepo), overwrite: true },
206
208
  { source: resolveSlotPortsByRepo(realRepo), overwrite: false },
209
+ );
210
+ }
211
+
212
+ // Checkout-scoped extension ports: runtime context (authoritative) → pool → suffix formula.
213
+ export function formatExtensionSlotPorts(repo) {
214
+ const realRepo = path.resolve(repo);
215
+ return mergeKvLayers(
216
+ { source: formatExtensionIdentityPorts(realRepo), overwrite: true },
207
217
  { source: resolveDefaultExtensionPorts(realRepo), overwrite: false },
208
218
  );
209
219
  }
@@ -285,6 +295,7 @@ export const cliFns = {
285
295
  // Back-compat cli key for one release.
286
296
  resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
287
297
  resolve_default_extension_ports: resolveDefaultExtensionPorts,
298
+ resolve_extension_identity_ports: (repo) => formatExtensionIdentityPorts(repo) || null,
288
299
  resolve_extension_runtime_context: resolveExtensionRuntimeContext,
289
300
  resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
290
301
  resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
@@ -52,12 +52,13 @@ apply_extension_slot_ports() {
52
52
  local repo="${1:-.}" line key val resolved="" ctx_file="" runtime_rel="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
53
53
  repo="$(cd "$repo" && pwd)"
54
54
  ctx_file="$repo/$runtime_rel/agentic-runtime.json"
55
- if [ -f "$ctx_file" ]; then
55
+ if [ -z "${RECIPE_RUNTIME_CONTEXT:-}" ] && [ -f "$ctx_file" ]; then
56
56
  RECIPE_RUNTIME_CONTEXT="$ctx_file"
57
- else
58
- unset RECIPE_RUNTIME_CONTEXT
59
57
  fi
60
- resolved="$(_resolve_ports_cli resolve_extension_slot_ports "$repo" 2>/dev/null)" || return 0
58
+ export RECIPE_RUNTIME_CONTEXT
59
+ # Context/pool are authoritative checkout identity. Formula ports are only
60
+ # fallback values and must not replace explicitly supplied env ports.
61
+ resolved="$(_resolve_ports_cli resolve_extension_identity_ports "$repo" 2>/dev/null)" || resolved=""
61
62
  while IFS= read -r line; do
62
63
  [ -n "$line" ] || continue
63
64
  key="${line%%=*}"
@@ -68,6 +69,17 @@ apply_extension_slot_ports() {
68
69
  SLOT_ID) RECIPE_SLOT_ID="$val" ;;
69
70
  esac
70
71
  done <<< "$resolved"
72
+ resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)" || return 0
73
+ while IFS= read -r line; do
74
+ [ -n "$line" ] || continue
75
+ key="${line%%=*}"
76
+ val="${line#*=}"
77
+ case "$key" in
78
+ CDP_PORT) [ -n "${CDP_PORT:-}" ] || CDP_PORT="$val" ;;
79
+ WATCHER_PORT) [ -n "${WATCHER_PORT:-}" ] || WATCHER_PORT="$val" ;;
80
+ SLOT_ID) [ -n "${RECIPE_SLOT_ID:-}" ] || RECIPE_SLOT_ID="$val" ;;
81
+ esac
82
+ done <<< "$resolved"
71
83
  }
72
84
 
73
85
  apply_resolved_extension_ports() {
@@ -111,4 +123,4 @@ apply_resolved_mobile_ports() {
111
123
  ;;
112
124
  esac
113
125
  done <<< "$resolved"
114
- }
126
+ }