@deeeed/metamask-harness 0.78.0 → 0.80.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 (215) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +2 -34
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/ensure-runner-deps.sh +2 -2
  21. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  22. package/adapters/shared/open-debug.mjs +2 -2
  23. package/adapters/shared/runner-deps-ready.mjs +1 -1
  24. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  25. package/adapters/terminal/cleanup.mjs +2 -1
  26. package/adapters/terminal/inject.mjs +2 -1
  27. package/adapters/terminal/lib/cdp-client.mjs +1 -1
  28. package/bin/mm-harness +9 -28
  29. package/dist/adapters/checkout-files.js +16 -0
  30. package/dist/adapters/console-capture.js +14 -1
  31. package/dist/adapters/core/actions.js +4 -3
  32. package/dist/adapters/core/overlay.js +1 -1
  33. package/dist/adapters/core/readiness.js +28 -1
  34. package/dist/adapters/core/run.js +3 -3
  35. package/dist/adapters/core/surface.js +22 -5
  36. package/dist/adapters/extension/browser-pid.js +56 -0
  37. package/dist/adapters/extension/console-capture.js +1 -1
  38. package/dist/adapters/extension/doctor-checks.js +1 -1
  39. package/dist/adapters/extension/ensure-ready.js +5 -4
  40. package/dist/adapters/extension/extension-id.js +1 -1
  41. package/dist/adapters/extension/feature-flags.js +1 -1
  42. package/dist/adapters/extension/fixtures.js +1 -2
  43. package/dist/adapters/extension/launch.js +4 -5
  44. package/dist/adapters/extension/launched-browser.js +1 -2
  45. package/dist/adapters/extension/network-observer.js +1 -1
  46. package/dist/adapters/extension/overlay.js +1 -1
  47. package/dist/adapters/extension/product-config.js +1 -1
  48. package/dist/adapters/extension/readiness.js +1 -1
  49. package/dist/adapters/extension/reload.js +1 -1
  50. package/dist/adapters/extension/run.js +24 -10
  51. package/dist/adapters/extension/runtime-decision.js +3 -2
  52. package/dist/adapters/extension/runtime.js +9 -5
  53. package/dist/adapters/extension/surface.js +20 -5
  54. package/dist/adapters/extension/verify.js +2 -2
  55. package/dist/adapters/harness-freshness.js +2 -1
  56. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  57. package/dist/adapters/mobile/app-identity.js +10 -0
  58. package/dist/adapters/mobile/doctor-checks.js +1 -1
  59. package/dist/adapters/mobile/fixtures.js +1 -1
  60. package/dist/adapters/mobile/launch.js +27 -28
  61. package/dist/adapters/mobile/lifecycle.js +1 -1
  62. package/dist/adapters/mobile/metro-env.js +9 -59
  63. package/dist/adapters/mobile/performance-observer.js +1 -1
  64. package/dist/adapters/mobile/prepare-target.js +1 -1
  65. package/dist/adapters/mobile/prepare.js +4 -5
  66. package/dist/adapters/mobile/provision.js +1 -1
  67. package/dist/adapters/mobile/readiness.js +1 -1
  68. package/dist/adapters/mobile/recording-target.js +15 -0
  69. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  70. package/dist/adapters/mobile/reload.js +1 -1
  71. package/dist/adapters/mobile/run.js +9 -8
  72. package/dist/adapters/mobile/runtime-decision.js +5 -7
  73. package/dist/adapters/mobile/source-freshness.js +22 -126
  74. package/dist/adapters/mobile/surface.js +31 -6
  75. package/dist/adapters/mobile/ui.js +16 -5
  76. package/dist/adapters/mobile/verify.js +2 -3
  77. package/dist/adapters/performance/cdp-trace.js +1 -1
  78. package/dist/adapters/performance/device-platform.js +1 -1
  79. package/dist/adapters/registry.js +2 -14
  80. package/dist/adapters/slot-ports.js +1 -1
  81. package/dist/adapters/surface.js +1 -0
  82. package/dist/adapters/terminal/console-capture.js +1 -1
  83. package/dist/adapters/terminal/launch.js +1 -1
  84. package/dist/adapters/terminal/run.js +25 -2
  85. package/dist/adapters/terminal/surface.js +12 -4
  86. package/dist/adapters.js +8 -4
  87. package/dist/advisor-policy.js +1 -1
  88. package/dist/checkout-view.js +1 -2
  89. package/dist/cli.js +28 -15
  90. package/dist/command-contract.js +1 -26
  91. package/dist/commands/call.js +10 -24
  92. package/dist/commands/check.js +3 -4
  93. package/dist/commands/checklist.js +1 -1
  94. package/dist/commands/completion-candidates.js +2 -10
  95. package/dist/commands/config.js +2 -2
  96. package/dist/commands/core-readiness.js +12 -7
  97. package/dist/commands/debug.js +3 -3
  98. package/dist/commands/device-target.js +5 -5
  99. package/dist/commands/doctor.js +2 -14
  100. package/dist/commands/domain.js +2 -2
  101. package/dist/commands/ensure-ready.js +1 -7
  102. package/dist/commands/fixtures.js +5 -6
  103. package/dist/commands/help.js +3 -4
  104. package/dist/commands/launch/device.js +10 -17
  105. package/dist/commands/launch/extension.js +3 -9
  106. package/dist/commands/launch/mobile.js +1 -1
  107. package/dist/commands/list-executables.js +1 -3
  108. package/dist/commands/logs.js +3 -3
  109. package/dist/commands/manifest.js +1 -12
  110. package/dist/commands/mobile-device-view.js +4 -2
  111. package/dist/commands/overlay.js +110 -0
  112. package/dist/commands/pin.js +1 -1
  113. package/dist/commands/pr-body.js +2 -2
  114. package/dist/commands/prepare.js +6 -11
  115. package/dist/commands/provision.js +3 -12
  116. package/dist/commands/recipe-advice.js +1 -2
  117. package/dist/commands/recipe-quality.js +2 -3
  118. package/dist/commands/resolve-extension.js +1 -7
  119. package/dist/commands/review.js +4 -4
  120. package/dist/commands/run-engine.js +12 -51
  121. package/dist/commands/run.js +4 -25
  122. package/dist/commands/runtime-decision.js +1 -6
  123. package/dist/commands/runtime-health.js +1 -7
  124. package/dist/commands/runtime-launch.js +1 -13
  125. package/dist/commands/self-test.js +2 -6
  126. package/dist/commands/status-watch.js +1 -2
  127. package/dist/commands/status.js +2 -5
  128. package/dist/commands/task-init.js +2 -3
  129. package/dist/commands/tutorial.js +2 -2
  130. package/dist/commands/update.js +1 -1
  131. package/dist/completions-cache.js +1 -1
  132. package/dist/console-classifier.js +19 -0
  133. package/dist/discovery.js +1 -1
  134. package/dist/doctor.js +3 -4
  135. package/dist/funding-execution-context.js +15 -1
  136. package/dist/host.js +32 -0
  137. package/dist/index.js +1 -6
  138. package/dist/live-tsx.js +49 -0
  139. package/dist/mm-harness-cli.js +2 -4
  140. package/dist/network-observation.js +4 -4
  141. package/dist/paths.js +48 -78
  142. package/dist/performance-observation.js +2 -2
  143. package/dist/progress.js +1 -1
  144. package/dist/recipe-assistance-context.js +2 -4
  145. package/dist/recipe-security.js +2 -59
  146. package/dist/review/knowledge.js +3 -3
  147. package/dist/runner.js +5 -3
  148. package/dist/runtime-context.js +1 -1
  149. package/dist/task-view.js +3 -4
  150. package/docs/CONTRIBUTING.md +3 -3
  151. package/docs/cli-contract.md +2 -2
  152. package/docs/feature-guard.md +90 -0
  153. package/library/README.md +3 -1
  154. package/library/actions/core/perps/_controller.mjs +153 -130
  155. package/library/actions/core/perps/quote_fees.mjs +58 -0
  156. package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
  157. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  158. package/library/actions/extension/feature_flags/state.mjs +1 -1
  159. package/library/actions/extension/platform/cdp.mjs +2 -8
  160. package/library/actions/harness-exports.mjs +1 -6
  161. package/library/actions/mobile/app/network-control.mjs +6 -8
  162. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  163. package/library/actions/mobile/platform/bridge.mjs +8 -8
  164. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  165. package/library/actions/mobile/platform/native-session.mjs +3 -4
  166. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  167. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  168. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  169. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  170. package/library/actions/shared/console-findings.mjs +1 -1
  171. package/library/actions/terminal/platform/runtime.mjs +2 -1
  172. package/library/manifests/core.action-manifest.json +83 -13
  173. package/package.json +8 -8
  174. package/adapters/extension/lib/slot-title.cjs +0 -292
  175. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  176. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  177. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  178. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  179. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  180. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  181. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  182. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  183. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  184. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  185. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  186. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  187. package/adapters/mobile/launch-metro.cjs +0 -77
  188. package/adapters/mobile/metro-config.cjs +0 -98
  189. package/adapters/mobile/metro-log-generation.cjs +0 -106
  190. package/dist/adapters/mobile/frame-metrics.js +0 -118
  191. package/dist/adapters/mobile/video-recorder.js +0 -289
  192. package/dist/artifact-files.js +0 -92
  193. package/dist/checkout-lock.js +0 -149
  194. package/dist/cli-color.js +0 -98
  195. package/dist/command-journal.js +0 -308
  196. package/dist/commands/last.js +0 -60
  197. package/dist/commands/launch/index.js +0 -109
  198. package/dist/commands/parse-args.js +0 -279
  199. package/dist/commands/reload.js +0 -41
  200. package/dist/commands/run-report.js +0 -241
  201. package/dist/commands/shared.js +0 -323
  202. package/dist/commands/stop.js +0 -60
  203. package/dist/devices.js +0 -100
  204. package/dist/execution-provenance.js +0 -332
  205. package/dist/harness.js +0 -451
  206. package/dist/heal-bounds.js +0 -137
  207. package/dist/json-stream.js +0 -60
  208. package/dist/leaf-invoke.js +0 -19
  209. package/dist/library-provenance.js +0 -33
  210. package/dist/live-adapter-contract.js +0 -380
  211. package/dist/recording-target.js +0 -120
  212. package/dist/run-diagnostics.js +0 -364
  213. package/dist/run-recording.js +0 -481
  214. package/dist/runtime-proof.js +0 -90
  215. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // MetaMask Mobile recovery lines for the bridge error codes that
4
+ // @farmslot/adapter-rn classifies; cdp-bridge.cjs prints one after a coded failure.
5
+ //
6
+ // Every coded failure teaches the escape for the caller's ACTUAL situation — a
7
+ // down/transitioning target is recoverable, so the Next: line names the action
8
+ // that brings it back rather than leaving the operator to guess.
9
+ const TEACHING_BY_ERROR_CODE = {
10
+ NO_TARGET:
11
+ 'Next: bring the app to the foreground with `mm-harness launch <platform>` so a debug target is available.',
12
+ CDP_TIMEOUT:
13
+ 'Next: the app is reloading or busy — retry, or `mm-harness launch <platform>` to foreground it.',
14
+ WS_CLOSED:
15
+ 'Next: the Hermes debug page closed (app reload/backgrounded) — retry once the app settles.',
16
+ METRO_UNREACHABLE:
17
+ 'Next: start Metro with `mm-harness start-metro` (if it is already up, check the slot WATCHER_PORT).',
18
+ };
19
+
20
+ module.exports = { TEACHING_BY_ERROR_CODE };
@@ -77,6 +77,22 @@ rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
77
77
  rm -rf "$HARNESS_DIR/runner/flows"
78
78
  rm -f "$HARNESS_DIR/runner/library.json"
79
79
  cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
80
+ # cdp-bridge.cjs runs from the target checkout, which cannot resolve the runner's
81
+ # packages; vendor the bridge libraries it requires next to it.
82
+ node - "$RUNNER_DIR" "$HARNESS_DIR/bridge-runtime/node_modules/@farmslot" <<'NODE' || exit 1
83
+ const fs = require('node:fs');
84
+ const path = require('node:path');
85
+ const { createRequire } = require('node:module');
86
+ const [runnerDir, dest] = process.argv.slice(2);
87
+ const requireFromRunner = createRequire(path.join(runnerDir, 'package.json'));
88
+ for (const [name, dirs] of [['adapter-rn', ['bridge-runtime']], ['recipe-runner', ['cdp-broker']]]) {
89
+ const packageJson = requireFromRunner.resolve(`@farmslot/${name}/package.json`);
90
+ const target = path.join(dest, name);
91
+ fs.mkdirSync(target, { recursive: true });
92
+ fs.copyFileSync(packageJson, path.join(target, 'package.json'));
93
+ for (const dir of dirs) fs.cpSync(path.join(path.dirname(packageJson), dir), path.join(target, dir), { recursive: true });
94
+ }
95
+ NODE
80
96
  chmod -R u+rwX,go+rX "$HARNESS_DIR"
81
97
 
82
98
  add_git_exclude() {
@@ -2,7 +2,6 @@
2
2
  'use strict';
3
3
 
4
4
  const fs = require('node:fs');
5
- const path = require('node:path');
6
5
  const { spawn } = require('node:child_process');
7
6
 
8
7
  const args = { port: '', out: '', err: '' };
@@ -25,7 +24,7 @@ if (!args.port || !args.out || !args.err) {
25
24
  process.exit(2);
26
25
  }
27
26
 
28
- const forwarder = path.join(__dirname, 'bridge-runtime', 'console-forwarder.cjs');
27
+ const forwarder = require.resolve('@farmslot/adapter-rn/bridge-runtime/console-forwarder.cjs');
29
28
  const errFd = fs.openSync(args.err, 'a');
30
29
  try {
31
30
  const child = spawn(process.execPath, [forwarder, '--port', args.port, '--out', args.out], {
@@ -28,7 +28,7 @@ PLATFORM="ios"
28
28
  TARGET="$PWD"
29
29
  SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
30
30
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
31
- ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
31
+ ADB_BIN="${RECIPE_RN_ADB_PATH:-}"
32
32
  PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
33
33
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
34
34
  UI_ONLY=false
@@ -67,7 +67,7 @@ if [ "$PLATFORM" = "android" ]; then
67
67
  printf 'Next: brew install android-platform-tools\n' >&2
68
68
  exit 1
69
69
  fi
70
- export MM_HARNESS_ADB_PATH="$ADB_BIN"
70
+ export RECIPE_RN_ADB_PATH="$ADB_BIN"
71
71
  fi
72
72
 
73
73
  TARGET="$(cd "$TARGET" && pwd)"
@@ -4,13 +4,13 @@ import { spawnSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { createRequire } from 'node:module';
7
- import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
7
+ import { resolveMobileToolPath } from '@farmslot/adapter-rn';
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
10
  const WebSocket = require('ws');
11
- const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
12
- const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
13
- const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
11
+ const { discoverTarget, rankRuntimeCandidates } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
12
+ const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('@farmslot/recipe-runner/cdp-broker');
13
+ const { createWSClient } = require('@farmslot/adapter-rn/bridge-runtime/lib/ws-client.cjs');
14
14
 
15
15
  async function observeRuntimeReset(port, target) {
16
16
  const socketPath = brokerSocketPath(
@@ -6,7 +6,7 @@ PLATFORM="${PLATFORM:-ios}"
6
6
  TARGET="$PWD"
7
7
  SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
8
8
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
9
- ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
9
+ ADB_BIN="${RECIPE_RN_ADB_PATH:-}"
10
10
  MM_RESET_TEMP_ROOT=""
11
11
  MM_RESET_STAGING_DIR=""
12
12
 
@@ -23,13 +23,14 @@ done
23
23
  TARGET="$(cd "$TARGET" && pwd -P)"
24
24
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
25
  if [ -z "$PORT" ] && command -v node >/dev/null 2>&1; then
26
- PORT="$(APP_ROOT="$TARGET" CONFIG_PATH="$SCRIPT_DIR/bridge-runtime/lib/config.cjs" node -e "process.stdout.write(String(require(process.env.CONFIG_PATH).resolvePort()))")"
26
+ PORT="$(cd "$SCRIPT_DIR" && APP_ROOT="$TARGET" node -e "process.stdout.write(String(require('@farmslot/adapter-rn/bridge-runtime/lib/config.cjs').resolvePort()))")"
27
27
  fi
28
28
  PORT="${PORT:-8081}"
29
29
  # shellcheck disable=SC1091
30
30
  . "$SCRIPT_DIR/../shared/harness-path.sh"
31
31
  LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
32
- FORWARDER="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
32
+ command -v node >/dev/null 2>&1 || exit 0
33
+ FORWARDER="$(cd "$SCRIPT_DIR" && node -p "require.resolve('@farmslot/adapter-rn/bridge-runtime/console-forwarder.cjs')")" || exit 1
33
34
  PID_FILE="$LOG_DIR/console-forwarder.pid"
34
35
  APP_LOG_FILE="$LOG_DIR/app-console.log"
35
36
 
@@ -54,18 +54,23 @@ fi
54
54
  # only ever inspect or signal a Metro listening on $PORT.
55
55
  # shellcheck disable=SC1091
56
56
  . "$SCRIPT_DIR/lib/metro-listener.sh"
57
- HARNESS_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
57
+ # Metro config wrapper, launcher and log helpers ship in @farmslot/adapter-rn.
58
+ ADAPTER_RN_METRO_DIR="$(cd "$SCRIPT_DIR" && node -p "require('node:path').dirname(require.resolve('@farmslot/adapter-rn/metro/metro-config.cjs'))")" || {
59
+ echo "start-metro: @farmslot/adapter-rn is not installed next to the runner; reinstall the runner." >&2
60
+ exit 1
61
+ }
62
+ HARNESS_METRO_CONFIG="$ADAPTER_RN_METRO_DIR/metro-config.cjs"
58
63
  if [ "${EXPO_OVERRIDE_METRO_CONFIG:-}" = "$HARNESS_METRO_CONFIG" ]; then
59
- PRODUCT_METRO_CONFIG="${MM_HARNESS_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
64
+ PRODUCT_METRO_CONFIG="${RECIPE_RN_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
60
65
  else
61
66
  PRODUCT_METRO_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
62
67
  fi
63
- export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
64
- export MM_HARNESS_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
68
+ export RECIPE_RN_METRO_PROJECT_ROOT="$TARGET"
69
+ export RECIPE_RN_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
65
70
  export EXPO_OVERRIDE_METRO_CONFIG="$HARNESS_METRO_CONFIG"
66
- MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} MM_HARNESS_METRO_PROJECT_ROOT MM_HARNESS_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"
67
- if [ -n "${MM_HARNESS_METRO_ENV_FINGERPRINT:-}" ]; then
68
- MOBILE_METRO_REQUIRED_ENV="$MOBILE_METRO_REQUIRED_ENV MM_HARNESS_METRO_ENV_FINGERPRINT"
71
+ MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} RECIPE_RN_METRO_PROJECT_ROOT RECIPE_RN_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"
72
+ if [ -n "${RECIPE_RN_METRO_ENV_FINGERPRINT:-}" ]; then
73
+ MOBILE_METRO_REQUIRED_ENV="$MOBILE_METRO_REQUIRED_ENV RECIPE_RN_METRO_ENV_FINGERPRINT"
69
74
  fi
70
75
 
71
76
  # RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
@@ -75,8 +80,8 @@ mkdir -p "$LOG_DIR"
75
80
  LOG_FILE="$LOG_DIR/metro.log"
76
81
  PID_FILE="$LOG_DIR/metro.pid"
77
82
  METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
78
- METRO_LOG_GENERATION="$SCRIPT_DIR/metro-log-generation.cjs"
79
- METRO_LOG_COALESCER="$SCRIPT_DIR/coalesce-metro-log.cjs"
83
+ METRO_LOG_GENERATION="$ADAPTER_RN_METRO_DIR/metro-log-generation.cjs"
84
+ METRO_LOG_COALESCER="$ADAPTER_RN_METRO_DIR/coalesce-metro-log.cjs"
80
85
 
81
86
  # --- helpers ------------------------------------------------------------------
82
87
 
@@ -106,8 +111,8 @@ write_metro_build_env() {
106
111
  done
107
112
  )" || { rm -f "$temporary"; return 1; }
108
113
  eval "$product_values"
109
- required_MM_HARNESS_METRO_PROJECT_ROOT="$MM_HARNESS_METRO_PROJECT_ROOT"
110
- required_MM_HARNESS_METRO_BASE_CONFIG="$MM_HARNESS_METRO_BASE_CONFIG"
114
+ required_RECIPE_RN_METRO_PROJECT_ROOT="$RECIPE_RN_METRO_PROJECT_ROOT"
115
+ required_RECIPE_RN_METRO_BASE_CONFIG="$RECIPE_RN_METRO_BASE_CONFIG"
111
116
  required_EXPO_OVERRIDE_METRO_CONFIG="$EXPO_OVERRIDE_METRO_CONFIG"
112
117
  [ "$CLEAR" = true ] && clear_value=1
113
118
  {
@@ -322,7 +327,7 @@ else
322
327
  )
323
328
  [ -z "$METRO_WORKERS" ] || launcher_args+=(--workers "$METRO_WORKERS")
324
329
  [ "$CLEAR" = true ] && launcher_args+=(--clear)
325
- node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
330
+ node "$ADAPTER_RN_METRO_DIR/launch-metro.cjs" "${launcher_args[@]}"
326
331
  ) || exit 1
327
332
  STARTED_METRO_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
328
333
 
@@ -14,10 +14,10 @@ const {
14
14
  describeRequested,
15
15
  describeTargets,
16
16
  hasMatchingRoute,
17
- } = require('./bridge-runtime/lib/match-bridge-target.cjs');
17
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/match-bridge-target.cjs');
18
18
  const {
19
19
  rankRuntimeCandidates,
20
- } = require('./bridge-runtime/lib/target-discovery.cjs');
20
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
21
21
 
22
22
  const DEFAULT_MAX_POLLS = 90;
23
23
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # ensure-runner-deps.sh — idempotent install of runner dependencies.
3
3
  #
4
- # Source checkout runs TypeScript via Node/tsx and imports @farmslot/recipe-harness.
4
+ # Source checkout runs TypeScript via Node/tsx and imports @farmslot/recipe-runner.
5
5
  # Cloned runner checkouts under product temp/ often have no node_modules, so use
6
6
  # the runner's pinned Yarn release and immutable lockfile.
7
7
  #
@@ -79,7 +79,7 @@ if [ -L "$RUNNER_DIR/node_modules" ]; then
79
79
  fi
80
80
 
81
81
  if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
82
- if [ -f "$PROTOCOL_ROOT/packages/recipe-harness/package.json" ]; then
82
+ if [ -f "$PROTOCOL_ROOT/packages/recipe-runner/package.json" ]; then
83
83
  FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
84
84
  node "$RUNNER_DIR/scripts/link-local-farmslot.mjs" >&2 || true
85
85
  if deps_ready; then
@@ -8,7 +8,7 @@
8
8
  // the next launch refreshes every slot from it.
9
9
  //
10
10
  // Source files count by size + mtime, so it is cheap enough to run on every launch;
11
- // the only file read is @farmslot/adapter-web's package.json, for its version. No
11
+ // the only files read are the vendored packages' package.json, for their versions. No
12
12
  // Date.now()/random is used (both would break determinism).
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
@@ -25,17 +25,22 @@ export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
25
25
  for (const rel of sourceDirs) {
26
26
  collect(path.join(runnerDir, rel), runnerDir, entries);
27
27
  }
28
- // inject.mjs copies the browser helpers from @farmslot/adapter-web, so its sources
29
- // are overlay inputs too; label them by package so the install location does not count.
30
- if (adapter === 'extension') {
31
- const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve('@farmslot/adapter-web/package.json');
28
+ // The injectors copy package files into the overlay (Extension: @farmslot/adapter-web's
29
+ // browser helpers; Mobile: the bridge libraries cdp-bridge.cjs requires), so those
30
+ // sources are overlay inputs too; label them by package so the install location
31
+ // does not count.
32
+ const vendored = adapter === 'extension'
33
+ ? [['@farmslot/adapter-web', ['src']]]
34
+ : [['@farmslot/adapter-rn', ['bridge-runtime']], ['@farmslot/recipe-runner', ['cdp-broker']]];
35
+ for (const [name, dirs] of vendored) {
36
+ const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve(`${name}/package.json`);
32
37
  const packageRoot = path.dirname(packageJson);
33
38
  const packageEntries = [];
34
- collect(path.join(packageRoot, 'src'), packageRoot, packageEntries);
35
- entries.push(...packageEntries.map((entry) => `@farmslot/adapter-web/${entry}`));
39
+ for (const dir of dirs) collect(path.join(packageRoot, dir), packageRoot, packageEntries);
40
+ entries.push(...packageEntries.map((entry) => `${name}/${entry}`));
36
41
  // npm tarballs carry fixed mtimes, so an upgrade can keep every size and mtime:
37
42
  // the version marks it.
38
- entries.push(`@farmslot/adapter-web@${JSON.parse(fs.readFileSync(packageJson, 'utf8')).version}`);
43
+ entries.push(`${name}@${JSON.parse(fs.readFileSync(packageJson, 'utf8')).version}`);
39
44
  }
40
45
  entries.sort();
41
46
  return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
@@ -15,10 +15,10 @@ import path from 'node:path';
15
15
  const require = createRequire(import.meta.url);
16
16
  const {
17
17
  discoverTarget,
18
- } = require('../mobile/bridge-runtime/lib/target-discovery.cjs');
18
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
19
19
  const {
20
20
  deviceIdFromUrl,
21
- } = require('../mobile/bridge-runtime/lib/cdp-broker.cjs');
21
+ } = require('@farmslot/recipe-runner/cdp-broker');
22
22
 
23
23
  function usage() {
24
24
  console.error(`Usage:
@@ -25,7 +25,7 @@ const acceptsHoistedDependencies = path
25
25
  let dependencyVersionSatisfies;
26
26
  try {
27
27
  const helperPath = requireFromRunner.resolve(
28
- '@farmslot/recipe-harness/runtime/deps-readiness',
28
+ '@farmslot/recipe-runner/runtime/deps-readiness',
29
29
  );
30
30
  ({ dependencyVersionSatisfies } = await import(pathToFileURL(helperPath).href));
31
31
  if (typeof dependencyVersionSatisfies !== 'function') process.exit(1);
@@ -60,12 +60,15 @@ fi
60
60
 
61
61
  # Farmslot slot sync (requires farmslot root and sync-fixtures.sh).
62
62
  if [ -n "$SLOT_ID" ]; then
63
+ # An explicit FARMSLOT_ROOT wins; otherwise the operator checkout's farm script.
63
64
  FARMSLOT_ROOT="${FARMSLOT_ROOT:-}"
64
- for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
65
- [ -d "$candidate/scripts" ] || continue
66
- FARMSLOT_ROOT="$candidate"
67
- break
68
- done
65
+ if [ -z "$FARMSLOT_ROOT" ]; then
66
+ for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
67
+ [ -f "$candidate/scripts/sync-fixtures.sh" ] || continue
68
+ FARMSLOT_ROOT="$candidate"
69
+ break
70
+ done
71
+ fi
69
72
  if [ -n "$FARMSLOT_ROOT" ] && [ -f "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" ]; then
70
73
  if bash "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" --slot "$SLOT_ID" 2>/dev/null; then
71
74
  [ -f "$CANONICAL" ] && exit 0
@@ -11,7 +11,8 @@ import { existsSync, readdirSync, rmSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
- import { recipeHarnessPath, walletFixturePath } from '../../library/actions/harness-exports.mjs';
14
+ import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
15
+ import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
15
16
  import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
16
17
  import { stopConsoleCollector, stopTerminalBrowser } from './lib/processes.mjs';
17
18
 
@@ -13,7 +13,8 @@ import { copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFile
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
 
16
- import { recipeHarnessPath, recipeRuntimeDir, walletFixturePath } from '../../library/actions/harness-exports.mjs';
16
+ import { recipeHarnessPath, recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
17
+ import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
17
18
  import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
18
19
  import { isTerminalCheckout } from './lib/readiness.mjs';
19
20
 
@@ -1,6 +1,6 @@
1
1
  // Minimal browser-level Chrome DevTools Protocol client with flattened target
2
2
  // sessions, for the long-running terminal wallet host. Page-level UI actions use
3
- // @farmslot/recipe-harness/runtime/cdp instead.
3
+ // @farmslot/recipe-runner/runtime/cdp instead.
4
4
 
5
5
  import WebSocket from 'ws';
6
6
 
package/bin/mm-harness CHANGED
@@ -90,24 +90,6 @@ if [ -f "$ENSURE_DEPS" ]; then
90
90
  bash "$ENSURE_DEPS" "$RUNNER_DIR"
91
91
  fi
92
92
 
93
- find_protocol_root() {
94
- local start="$1"
95
- local dir
96
- dir="$(cd "$start" && pwd -P)"
97
- while [ "$dir" != "/" ]; do
98
- if [ -f "$dir/packages/recipe-harness/package.json" ] && [ -f "$dir/packages/protocol/package.json" ]; then
99
- printf '%s\n' "$dir"
100
- return 0
101
- fi
102
- if [ -f "$dir/farmslot/packages/recipe-harness/package.json" ] && [ -f "$dir/farmslot/packages/protocol/package.json" ]; then
103
- printf '%s\n' "$dir/farmslot"
104
- return 0
105
- fi
106
- dir="$(dirname "$dir")"
107
- done
108
- return 1
109
- }
110
-
111
93
  node_can_run_source_typescript() {
112
94
  # Node refuses type-stripping for files under node_modules (ERR_UNSUPPORTED_NODE_
113
95
  # MODULES_TYPE_STRIPPING). When this runner is installed as a dependency, the
@@ -183,20 +165,19 @@ if [ ! -f "$ENTRY_TS" ]; then
183
165
  exit 1
184
166
  fi
185
167
 
186
- TSX_BIN="${TSX_BIN:-$RUNNER_DIR/node_modules/.bin/tsx}"
187
- if [ ! -x "$TSX_BIN" ] && [ -x "$RUNNER_DIR/../../.bin/tsx" ]; then
188
- TSX_BIN="$RUNNER_DIR/../../.bin/tsx"
168
+ # ENTRY_TSX, not TSX_BIN: TSX_BIN is the caller's choice of runner for live
169
+ # adapter scripts too, so this fallback must not overwrite the exported value.
170
+ ENTRY_TSX="${TSX_BIN:-$RUNNER_DIR/node_modules/.bin/tsx}"
171
+ if [ ! -x "$ENTRY_TSX" ] && [ -x "$RUNNER_DIR/../../.bin/tsx" ]; then
172
+ ENTRY_TSX="$RUNNER_DIR/../../.bin/tsx"
189
173
  fi
190
- if [ ! -x "$TSX_BIN" ]; then
174
+ if [ ! -x "$ENTRY_TSX" ]; then
191
175
  if [ -z "${FARMSLOT_ROOT:-}" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
192
176
  FARMSLOT_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
193
177
  fi
194
- if [ -z "${FARMSLOT_ROOT:-}" ]; then
195
- FARMSLOT_ROOT="$(find_protocol_root "$RUNNER_DIR" || find_protocol_root "$PWD" || true)"
196
- fi
197
178
  if [ -n "${FARMSLOT_ROOT:-}" ]; then
198
179
  FARMSLOT_ROOT="$(cd "$FARMSLOT_ROOT" && pwd -P)"
199
- TSX_BIN="$FARMSLOT_ROOT/node_modules/.bin/tsx"
180
+ ENTRY_TSX="$FARMSLOT_ROOT/node_modules/.bin/tsx"
200
181
  fi
201
182
  fi
202
183
 
@@ -204,8 +185,8 @@ if node_can_run_source_typescript; then
204
185
  exec node --no-warnings "$ENTRY_TS" "$@"
205
186
  fi
206
187
 
207
- if [ -x "$TSX_BIN" ]; then
208
- exec "$TSX_BIN" "$ENTRY_TS" "$@"
188
+ if [ -x "$ENTRY_TSX" ]; then
189
+ exec "$ENTRY_TSX" "$ENTRY_TS" "$@"
209
190
  fi
210
191
 
211
192
  echo "mm-harness: source checkout requires dist/mm-harness-cli.js, Node.js TypeScript type stripping, or a local tsx binary." >&2
@@ -0,0 +1,16 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function checkoutHas(target, relative) {
4
+ return fs.existsSync(path.join(target, relative));
5
+ }
6
+ function checkoutHasDir(target, relative) {
7
+ try {
8
+ return fs.statSync(path.join(target, relative)).isDirectory();
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ export {
14
+ checkoutHas,
15
+ checkoutHasDir
16
+ };
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import {
5
5
  collectorMatches,
6
6
  collectorOwnerMarker,
7
+ collectorStatus,
7
8
  isLegacyCollector,
8
9
  isOwnedCollector,
9
10
  isPriorInstallCollector,
@@ -80,6 +81,17 @@ async function stopProcess(pid) {
80
81
  await new Promise((resolve) => setTimeout(resolve, 100));
81
82
  }
82
83
  }
84
+ async function verifyOwnedCollectorControl(capture, control) {
85
+ const status = collectorStatus({
86
+ pidFile: capture.pidFile,
87
+ extensionLog: capture.extensionLog,
88
+ marker: collectorOwnerMarker(path.dirname(capture.pidFile)),
89
+ cdpPort: capture.cdpPort
90
+ });
91
+ if (!status.ok) return { ok: false, detail: status.detail };
92
+ const result = await control();
93
+ return { ok: result.ok, detail: result.ok ? `${status.detail}; ${result.detail}` : result.detail };
94
+ }
83
95
  const ROTATE_LOG_BYTES = 32 * 1024 * 1024;
84
96
  function rotateLargeLog(file) {
85
97
  if (fileSize(file) < ROTATE_LOG_BYTES) return;
@@ -118,5 +130,6 @@ function containsAttachment(from) {
118
130
  export {
119
131
  COLLECTOR_SCRIPT,
120
132
  ROTATE_LOG_BYTES,
121
- ensureConsoleCollector
133
+ ensureConsoleCollector,
134
+ verifyOwnedCollectorControl
122
135
  };
@@ -5,10 +5,11 @@ const coreActions = {
5
5
  "metamask.perps.close_positions",
6
6
  "metamask.perps.start_state",
7
7
  "metamask.perps.read_snapshot",
8
- "metamask.perps.read_account"
8
+ "metamask.perps.read_account",
9
+ "metamask.perps.quote_fees"
9
10
  ],
10
- // read_account is Core's alone, and only its live adapter implements it.
11
- liveOnly: ["metamask.perps.read_account"],
11
+ // read_account and quote_fees are Core's alone, and only its live adapter implements them.
12
+ liveOnly: ["metamask.perps.read_account", "metamask.perps.quote_fees"],
12
13
  tsxLiveScripts: {
13
14
  env: async (projectRoot, tempDir) => (await import("./live-script-env.js")).coreLiveScriptEnv(projectRoot, tempDir)
14
15
  },
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { recipeHarnessPath } from "../../paths.js";
3
+ import { recipeHarnessPath } from "@farmslot/recipe-cli/harness";
4
4
  function isExecutable(file) {
5
5
  try {
6
6
  const stat = fs.statSync(file);
@@ -1,8 +1,35 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { coreDependencyBlock } from "../../commands/core-readiness.js";
4
+ import { resolveLiveTsx } from "../../live-tsx.js";
3
5
  const coreReadiness = {
4
- compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge"
6
+ compatibilityMode: (target) => fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge",
7
+ checks: (target) => [coreControllerActionsCheck(target)]
5
8
  };
9
+ function coreControllerActionsCheck(target) {
10
+ const block = coreDependencyBlock(target);
11
+ if (block) {
12
+ return {
13
+ id: "core-controller-actions",
14
+ status: "fail",
15
+ // A checkout with no dependencies installed is reported, not failed: the
16
+ // runtime check already says it is not ready and how to install. Once
17
+ // dependencies are installed, an action that still cannot run fails doctor.
18
+ required: block.code !== "CORE_DEPS_MISSING",
19
+ message: `metamask.perps.* actions cannot run: ${block.message}`,
20
+ detail: block.code,
21
+ userAction: block.userAction
22
+ };
23
+ }
24
+ const runner = resolveLiveTsx(target);
25
+ return {
26
+ id: "core-controller-actions",
27
+ status: "pass",
28
+ required: true,
29
+ message: "metamask.perps.* actions can run.",
30
+ detail: runner ? `tsx (${runner.source}): ${runner.bin}` : void 0
31
+ };
32
+ }
6
33
  export {
7
34
  coreReadiness
8
35
  };
@@ -1,11 +1,11 @@
1
+ import { isRecord } from "@farmslot/recipe-cli/harness";
1
2
  import { coreDependencyBlock } from "../../commands/core-readiness.js";
2
- import { isRecord } from "../../commands/parse-args.js";
3
3
  const coreRun = {
4
4
  async dependencyBlock(target, { recipe, librarySources, action }) {
5
5
  const usesController = action !== void 0 ? actionUsesCoreController(action) : await recipeUsesCoreController(recipe, librarySources);
6
6
  return usesController ? coreDependencyBlock(target) : null;
7
7
  },
8
- violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing, configure a reserved wallet-fixture.json account and pass its non-empty name; an explicit 0x address is supported only by read-only actions" : void 0
8
+ violationUserAction: (violation) => violation.code === "WALLET_STATE_REQUIRED" ? "for signing, select a wallet-fixture.json account by name or by its address; an address outside the fixture works only for read-only actions" : void 0
9
9
  };
10
10
  async function recipeUsesCoreController(recipe, librarySources) {
11
11
  if (objectUsesCoreController(recipe)) return true;
@@ -13,7 +13,7 @@ async function recipeUsesCoreController(recipe, librarySources) {
13
13
  if (refs.length === 0) return false;
14
14
  const sources = librarySources ?? [];
15
15
  if (sources.length === 0) return false;
16
- const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
16
+ const { loadRecipeLibraries } = await import("@farmslot/recipe-runner");
17
17
  const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
18
18
  const visited = /* @__PURE__ */ new Set();
19
19
  const visit = (ref) => {
@@ -1,18 +1,32 @@
1
+ import { shellQuote } from "@farmslot/recipe-cli/harness";
1
2
  import path from "node:path";
2
- import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
3
- import { shellQuote } from "../../commands/parse-args.js";
3
+ import { depsCheck } from "@farmslot/recipe-runner/runtime/deps-readiness";
4
+ import { defineAdapter } from "@farmslot/adapter-sdk";
4
5
  import { adapterRegistry } from "../registry.js";
5
6
  import { coreActions } from "./actions.js";
6
7
  import { coreHarness, corePrepare, coreRuntimeContext } from "./overlay.js";
7
8
  import { coreReadiness } from "./readiness.js";
8
9
  import { coreRun } from "./run.js";
9
- const coreSurface = {
10
- adapter: "core",
10
+ import { coreDependencyBlock } from "../../commands/core-readiness.js";
11
+ import { checkoutHas, checkoutHasDir } from "../checkout-files.js";
12
+ const coreSurface = defineAdapter({
13
+ id: "core",
14
+ sdkVersion: 1,
11
15
  headless: true,
12
16
  resolveSlotPorts() {
13
17
  },
14
18
  async runtimeStatus(target) {
15
19
  const deps = depsCheck(path.resolve(target));
20
+ const block = deps.status === "current" ? coreDependencyBlock(target) : null;
21
+ if (block) {
22
+ return {
23
+ decision: "blocked",
24
+ reasonCode: block.code,
25
+ reasons: [`Core is headless; controller actions cannot run: ${block.message}`],
26
+ nextAction: block.userAction,
27
+ deps: deps.status
28
+ };
29
+ }
16
30
  const ready = deps.status === "current";
17
31
  return {
18
32
  decision: ready ? "ready" : "install",
@@ -54,6 +68,9 @@ const coreSurface = {
54
68
  appLogSource() {
55
69
  return null;
56
70
  },
71
+ detect: {
72
+ files: (target) => checkoutHasDir(target, "packages/perps-controller") && checkoutHas(target, "yarn.lock")
73
+ },
57
74
  hints: {
58
75
  launch: "mm-harness run <recipe> # run recipes against the headless core",
59
76
  relaunch: "mm-harness verify",
@@ -67,7 +84,7 @@ const coreSurface = {
67
84
  fixtures: { operations: ["init", "sync", "set"] },
68
85
  prepare: corePrepare,
69
86
  run: coreRun
70
- };
87
+ });
71
88
  adapterRegistry.register(coreSurface);
72
89
  export {
73
90
  coreSurface