@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
package/dist/paths.js CHANGED
@@ -1,43 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { recipeRuntimeDir, recipeRuntimePath } from "@farmslot/recipe-cli/harness";
4
5
  import { adapterRegistry } from "./adapters/registry.js";
5
6
  const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
- const pathDefaults = readPathDefaults();
7
- const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
8
- const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
9
7
  const PREPARE_PROGRESS_ARTIFACT = "prepare/progress.json";
10
- function recipeRuntimeDir() {
11
- return validateRelativeRecipePath("RECIPE_RUNTIME_DIR", process.env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
12
- }
13
- function recipeHarnessRoot() {
14
- return validateRelativeRecipePath("RECIPE_HARNESS_ROOT", process.env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
15
- }
16
- function harnessExecutable() {
17
- return path.resolve(process.env.MM_HARNESS_EXECUTABLE ?? path.join(runnerDir, "bin/mm-harness"));
18
- }
19
- function readPathDefaults() {
20
- const defaultsPath = path.join(runnerDir, "adapters/shared/path-defaults.json");
21
- const parsed = JSON.parse(fs.readFileSync(defaultsPath, "utf8"));
22
- return {
23
- recipeRuntimeDir: validateRelativeRecipePath("recipeRuntimeDir", parsed.recipeRuntimeDir || ""),
24
- recipeHarnessRoot: validateRelativeRecipePath("recipeHarnessRoot", parsed.recipeHarnessRoot || "")
25
- };
26
- }
27
- function validateRelativeRecipePath(name, value) {
28
- if (!value || path.isAbsolute(value)) throw new Error(`${name} must be a non-empty relative path: ${value}`);
29
- if (!/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${name} contains unsupported characters: ${value}`);
30
- for (const part of value.split("/")) {
31
- if (!part || part === "." || part === "..") throw new Error(`${name} contains unsafe path component: ${value}`);
32
- }
33
- return value;
34
- }
35
- function recipeRuntimePath(projectRoot, ...segments) {
36
- return path.join(projectRoot, recipeRuntimeDir(), ...segments);
37
- }
38
- function recipeHarnessPath(projectRoot, ...segments) {
39
- return path.join(projectRoot, recipeHarnessRoot(), ...segments);
40
- }
41
8
  function walletFixturePath(projectRoot) {
42
9
  return recipeRuntimePath(projectRoot, "wallet-fixture.json");
43
10
  }
@@ -51,42 +18,51 @@ function recipeWatchLogCandidates() {
51
18
  ];
52
19
  }
53
20
  function resolveLocalProtocolRoot() {
54
- const candidates = [
55
- process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
56
- readConfiguredProtocolRoot(),
57
- findProtocolRoot(runnerDir),
58
- findProtocolRoot(process.cwd())
59
- ].filter(Boolean);
60
- const root = candidates[0];
21
+ const root = process.env.METAMASK_RUNNER_PROTOCOL_ROOT || readConfiguredProtocolRoot();
61
22
  return root ? path.resolve(root) : void 0;
62
23
  }
63
- function resolveRequiredLocalProtocolRoot(reason) {
64
- const root = resolveLocalProtocolRoot();
65
- if (!root) {
66
- throw new Error(
67
- `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
68
- );
24
+ function resolveFarmslotPoolDir(starts = [runnerDir, process.cwd()]) {
25
+ const configured = resolveLocalProtocolRoot();
26
+ if (configured) return path.join(configured, "pool");
27
+ for (const start of starts) {
28
+ const pool = findPoolDir(start);
29
+ if (pool) return pool;
69
30
  }
70
- return root;
31
+ return void 0;
71
32
  }
33
+ const LIBRARY_ACTION_NAMESPACE = "metamask";
72
34
  function readConfiguredProtocolRoot() {
73
35
  const configPath = path.join(runnerDir, ".farmslot-root");
74
36
  if (!fs.existsSync(configPath)) return void 0;
75
37
  const value = fs.readFileSync(configPath, "utf8").trim();
76
38
  return value || void 0;
77
39
  }
78
- function findProtocolRoot(start) {
40
+ function findPoolDir(start) {
79
41
  let dir = path.resolve(start);
80
42
  while (dir !== path.dirname(dir)) {
81
- if (isProtocolRoot(dir)) return dir;
82
- const sibling = path.join(dir, "farmslot");
83
- if (isProtocolRoot(sibling)) return sibling;
43
+ for (const root of [dir, path.join(dir, "farmslot")]) {
44
+ const pool = path.join(root, "pool");
45
+ if (holdsSlotPool(pool)) return pool;
46
+ }
84
47
  dir = path.dirname(dir);
85
48
  }
86
49
  return void 0;
87
50
  }
88
- function isProtocolRoot(candidate) {
89
- return fs.existsSync(path.join(candidate, "packages/recipe-harness/package.json")) && fs.existsSync(path.join(candidate, "packages/protocol/package.json"));
51
+ function holdsSlotPool(candidate) {
52
+ let names;
53
+ try {
54
+ names = fs.readdirSync(candidate);
55
+ } catch {
56
+ return false;
57
+ }
58
+ return names.some((name) => {
59
+ if (!name.endsWith(".json") || name.includes(".bak")) return false;
60
+ try {
61
+ return Array.isArray(JSON.parse(fs.readFileSync(path.join(candidate, name), "utf8"))?.slots);
62
+ } catch {
63
+ return false;
64
+ }
65
+ });
90
66
  }
91
67
  function assertAdapter(adapter) {
92
68
  const ids = adapterRegistry.list();
@@ -110,44 +86,44 @@ function readJson(file) {
110
86
  }
111
87
  async function importRecipeHarness() {
112
88
  return importProtocolPackage(
113
- "@farmslot/recipe-harness",
114
- "packages/recipe-harness/src/index.ts"
89
+ "@farmslot/recipe-runner",
90
+ "packages/recipe-runner/src/index.ts"
115
91
  );
116
92
  }
117
93
  async function importRecipeHarnessRuntimeCdp() {
118
94
  return importProtocolPackage(
119
- "@farmslot/recipe-harness/runtime/cdp",
120
- "packages/recipe-harness/src/runtime/cdp.ts"
95
+ "@farmslot/recipe-runner/runtime/cdp",
96
+ "packages/recipe-runner/src/runtime/cdp.ts"
121
97
  );
122
98
  }
123
99
  async function importRecipeHarnessAppLifecycle() {
124
100
  return importProtocolPackage(
125
- "@farmslot/recipe-harness/adapters/app-lifecycle",
126
- "packages/recipe-harness/src/adapters/app-lifecycle.ts"
101
+ "@farmslot/recipe-runner/adapters/app-lifecycle",
102
+ "packages/recipe-runner/src/adapters/app-lifecycle.ts"
127
103
  );
128
104
  }
129
105
  async function importRecipeHarnessRuntimeBrowserExtension() {
130
106
  return importProtocolPackage(
131
- "@farmslot/recipe-harness/runtime/browser-extension",
132
- "packages/recipe-harness/src/runtime/browser-extension.ts"
107
+ "@farmslot/recipe-runner/runtime/browser-extension",
108
+ "packages/recipe-runner/src/runtime/browser-extension.ts"
133
109
  );
134
110
  }
135
111
  async function importRecipeHarnessRuntimeReactNativeBridge() {
136
112
  return importProtocolPackage(
137
- "@farmslot/recipe-harness/runtime/react-native-bridge",
138
- "packages/recipe-harness/src/runtime/react-native-bridge.ts"
113
+ "@farmslot/recipe-runner/runtime/react-native-bridge",
114
+ "packages/recipe-runner/src/runtime/react-native-bridge.ts"
139
115
  );
140
116
  }
141
117
  async function importMetaMaskNativeUiProvider() {
142
118
  return importProtocolPackage(
143
- "@farmslot/expo-recipe",
144
- "packages/expo-recipe/src/index.ts"
119
+ "@farmslot/adapter-rn",
120
+ "packages/adapter-rn/src/index.ts"
145
121
  );
146
122
  }
147
- async function importRecipeHarnessCli() {
123
+ async function importRecipeRunnerCli() {
148
124
  return importProtocolPackage(
149
- "@farmslot/recipe-harness/cli",
150
- "packages/recipe-harness/src/cli/index.ts"
125
+ "@farmslot/recipe-runner/cli",
126
+ "packages/recipe-runner/src/cli/index.ts"
151
127
  );
152
128
  }
153
129
  async function importRecipeProtocol() {
@@ -176,30 +152,24 @@ function isMissingPackageError(error, packageName) {
176
152
  return (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED") && error.message.includes(packageName);
177
153
  }
178
154
  export {
179
- DEFAULT_RECIPE_HARNESS_ROOT,
180
- DEFAULT_RECIPE_RUNTIME_DIR,
155
+ LIBRARY_ACTION_NAMESPACE,
181
156
  PREPARE_PROGRESS_ARTIFACT,
182
157
  assertAdapter,
183
158
  extensionIdPath,
184
- harnessExecutable,
185
159
  importMetaMaskNativeUiProvider,
186
160
  importRecipeHarness,
187
161
  importRecipeHarnessAppLifecycle,
188
- importRecipeHarnessCli,
189
162
  importRecipeHarnessRuntimeBrowserExtension,
190
163
  importRecipeHarnessRuntimeCdp,
191
164
  importRecipeHarnessRuntimeReactNativeBridge,
192
165
  importRecipeProtocol,
166
+ importRecipeRunnerCli,
193
167
  manifestPath,
194
168
  readJson,
195
- recipeHarnessPath,
196
- recipeHarnessRoot,
197
169
  recipePath,
198
- recipeRuntimeDir,
199
- recipeRuntimePath,
200
170
  recipeWatchLogCandidates,
171
+ resolveFarmslotPoolDir,
201
172
  resolveLocalProtocolRoot,
202
- resolveRequiredLocalProtocolRoot,
203
173
  runnerDir,
204
174
  walletFixturePath
205
175
  };
@@ -4,13 +4,13 @@ import { gzip } from "node:zlib";
4
4
  import { getAdapterSurface } from "./adapters/surface.js";
5
5
  import { createExtensionPerformanceBackend } from "./adapters/extension/performance-observer.js";
6
6
  import { TRACE_KINDS } from "./adapters/performance/cdp-trace.js";
7
- import { summarizeFrames } from "./adapters/mobile/frame-metrics.js";
7
+ import { summarizeFrames } from "@farmslot/adapter-rn";
8
8
  import { summarizeJavaScriptTasks } from "./adapters/performance/js-task-metrics.js";
9
9
  import {
10
10
  indexArtifactManifest,
11
11
  readContainedJsonArtifact,
12
12
  writeContainedArtifact
13
- } from "./artifact-files.js";
13
+ } from "@farmslot/recipe-cli/harness";
14
14
  const sessions = /* @__PURE__ */ new Map();
15
15
  const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1e3;
16
16
  const MAX_DURATION_MS = 60 * 60 * 1e3;
package/dist/progress.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { parseArgs } from "node:util";
3
- import { color } from "./cli-color.js";
3
+ import { color } from "@farmslot/recipe-cli/harness";
4
4
  function shouldEmitJson(explicit) {
5
5
  if (explicit) return true;
6
6
  const env = process.env.RECIPE_PROGRESS_JSON;
@@ -1,13 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
- import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } from "@farmslot/recipe-harness";
4
- import { isSensitiveKey } from "./command-journal.js";
3
+ import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } from "@farmslot/recipe-runner";
4
+ import { isSensitiveKey, CliError, isRecord, optionStrings, EXIT } from "@farmslot/recipe-cli/harness";
5
5
  import { resolveActionManifest } from "./manifest.js";
6
6
  import { describeManifestActions } from "./commands/manifest.js";
7
7
  import { recipeComposition } from "@farmslot/recipe-cli";
8
8
  import { resolveMetaMaskLibrarySources, validateRunRecipeStatic } from "./commands/run-engine.js";
9
- import { CliError, isRecord, optionStrings } from "./commands/parse-args.js";
10
- import { EXIT } from "./commands/shared.js";
11
9
  import { MAX_ADVICE_STATE_BYTES } from "./recipe-assistance-rubric.js";
12
10
  function digest(value) {
13
11
  return createHash("sha256").update(value).digest("hex");
@@ -18,6 +18,7 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
18
18
  "metamask.perps.read_positions",
19
19
  "metamask.perps.read_orders",
20
20
  "metamask.perps.read_account",
21
+ "metamask.perps.quote_fees",
21
22
  "metamask.perps.read_snapshot",
22
23
  "metamask.perps.assert_visible_consistency",
23
24
  "metamask.perps.assert_positions",
@@ -175,65 +176,7 @@ function isPathWithin(root, candidate) {
175
176
  function isMissing(error) {
176
177
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
177
178
  }
178
- function explicitRecipeTrustOptions(values) {
179
- const { sourceTrust, sourceKind, sourceName, sourceDigest, approvalDigest } = values;
180
- const source = sourceTrust || sourceKind || sourceName || sourceDigest ? {
181
- trust: sourceTrust ?? "unknown",
182
- kind: sourceKind ?? "recipe-file",
183
- ...sourceName ? { name: sourceName } : {},
184
- ...sourceDigest ? { digest: sourceDigest } : {}
185
- } : void 0;
186
- return {
187
- ...source ? { source } : {},
188
- ...approvalDigest ? { approval: { planDigest: approvalDigest } } : {}
189
- };
190
- }
191
- function recipeTrustFailure(error) {
192
- if (!error || typeof error !== "object") return void 0;
193
- const value = error;
194
- if (typeof value.code !== "string" || !value.code.startsWith("RECIPE_") || typeof value.message !== "string" || typeof value.userAction !== "string") return void 0;
195
- const failure = value.failure && typeof value.failure === "object" ? value.failure : void 0;
196
- const blocked = Array.isArray(failure?.blocked) ? failure.blocked.flatMap((entry) => {
197
- if (!entry || typeof entry !== "object") return [];
198
- const node = entry;
199
- if (typeof node.nodeId !== "string" || typeof node.action !== "string" || !Array.isArray(node.capabilities)) return [];
200
- const origin = node.origin && typeof node.origin === "object" ? node.origin : void 0;
201
- const adapterOrigin = node.adapterOrigin && typeof node.adapterOrigin === "object" ? node.adapterOrigin : void 0;
202
- const implementation = adapterOrigin ? {
203
- ...typeof adapterOrigin.kind === "string" ? { kind: adapterOrigin.kind } : {},
204
- ...typeof adapterOrigin.trust === "string" ? { trust: adapterOrigin.trust } : {},
205
- ...typeof adapterOrigin.digest === "string" ? { digest: adapterOrigin.digest } : {}
206
- } : void 0;
207
- return [{
208
- nodeId: node.nodeId,
209
- action: node.action,
210
- capabilities: node.capabilities.filter(
211
- (item) => typeof item === "string"
212
- ),
213
- source: typeof origin?.kind === "string" ? origin.kind : "unknown",
214
- ...implementation && Object.keys(implementation).length > 0 ? { implementation } : {}
215
- }];
216
- }) : void 0;
217
- const recipeDigest = typeof failure?.recipeDigest === "string" ? failure.recipeDigest : void 0;
218
- const userAction = recipeDigest && (value.code === "RECIPE_TRUST_REQUIRED" || value.code === "RECIPE_APPROVAL_MISMATCH") && !value.userAction.includes("execution environment") ? `${value.userAction}; rerun with the same project root, artifact directory, and execution environment` : value.userAction;
219
- return {
220
- code: value.code,
221
- message: value.message,
222
- userAction,
223
- ...failure ? {
224
- details: {
225
- // Farmslot reports the approved execution plan digest as
226
- // recipeDigest in failures; approval input names it planDigest.
227
- ...recipeDigest ? { recipeDigest } : {},
228
- ...typeof failure.trust === "string" ? { trust: failure.trust } : {},
229
- ...blocked?.length ? { blocked } : {}
230
- }
231
- } : {}
232
- };
233
- }
234
179
  export {
235
180
  actionSourceIntegrity,
236
- explicitRecipeTrustOptions,
237
- metaMaskActionExecutionCapabilities,
238
- recipeTrustFailure
181
+ metaMaskActionExecutionCapabilities
239
182
  };
@@ -1,9 +1,9 @@
1
+ import { detectAdapter, usageError } from "@farmslot/recipe-cli/harness";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
- import { detectAdapter } from "../harness.js";
6
- import { usageError } from "../commands/parse-args.js";
6
+ import "../adapters/surface.js";
7
7
  const REVIEW_ADAPTERS = ["mobile", "extension", "core", "terminal"];
8
8
  const ADVISOR_STAGES = ["plan", "draft", "evidence"];
9
9
  function parseAdvisorSettings(value) {
@@ -221,7 +221,7 @@ async function hydrateLibraryPathEnv(context) {
221
221
  if (discovered.length === 0) return;
222
222
  const entries = discovered.map((library) => `${library.name}=${library.root}`);
223
223
  if (!existing) {
224
- const { personalRecipeLibraryRoot } = await import("@farmslot/recipe-harness");
224
+ const { personalRecipeLibraryRoot } = await import("@farmslot/recipe-runner");
225
225
  const personal = personalRecipeLibraryRoot(env);
226
226
  if (isDir(personal)) entries.push(`personal=${personal}`);
227
227
  }
package/dist/runner.js CHANGED
@@ -10,10 +10,11 @@ import {
10
10
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
11
11
  import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./recipe-security.js";
12
12
  import {
13
+ createRecordingTargetProvider,
13
14
  prepareLiveAdapterScript
14
- } from "./live-adapter-contract.js";
15
- import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
15
+ } from "@farmslot/recipe-cli/harness";
16
16
  import {
17
+ LIBRARY_ACTION_NAMESPACE,
17
18
  importMetaMaskNativeUiProvider,
18
19
  importRecipeHarness,
19
20
  importRecipeHarnessAppLifecycle,
@@ -109,7 +110,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
109
110
  ),
110
111
  logger,
111
112
  recording: {
112
- targetProvider: createMetaMaskRecordingTargetProvider(adapter),
113
+ targetProvider: createRecordingTargetProvider(adapter),
113
114
  ...videoRecorder ? { videoRecorder } : {}
114
115
  },
115
116
  runner: runnerProvenance(),
@@ -204,6 +205,7 @@ async function prepareLiveAdapterImplementations(platform, actions, sources, tru
204
205
  const executable = await prepareLiveAdapterScript({
205
206
  platform,
206
207
  action,
208
+ namespace: LIBRARY_ACTION_NAMESPACE,
207
209
  implementationRoot: source.implementationRoot,
208
210
  allowRuntimeImports: source.tier === "canonical" || source.trust === "trusted" || source.tier === "task" && trustTaskActions
209
211
  });
@@ -1,10 +1,10 @@
1
+ import { recipeRuntimeDir } from "@farmslot/recipe-cli/harness";
1
2
  import { createHash } from "node:crypto";
2
3
  import { execFileSync } from "node:child_process";
3
4
  import fs from "node:fs";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
  import { getAdapterSurface } from "./adapters/surface.js";
7
- import { recipeRuntimeDir } from "./paths.js";
8
8
  function ensureRuntimeContext(target, adapter) {
9
9
  const repoRoot = fs.realpathSync(target);
10
10
  const runtimeDir = recipeRuntimeDir();
package/dist/task-view.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readOperations, processIdentity } from "@farmslot/recipe-harness/runtime/operation";
1
+ import { readOperations, processIdentity } from "@farmslot/recipe-runner/runtime/operation";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
@@ -22,9 +22,8 @@ import {
22
22
  subtaskPaths,
23
23
  WORKER_SIGNAL_FILE
24
24
  } from "@farmslot/protocol";
25
- import { readCommandJournal } from "./command-journal.js";
26
- import { resolveRuntimeContextPath } from "./harness.js";
27
- import { PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "./paths.js";
25
+ import { readCommandJournal, recipeRuntimeDir, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
26
+ import { PREPARE_PROGRESS_ARTIFACT } from "./paths.js";
28
27
  import { parseLibraryPathEnv } from "./review/knowledge.js";
29
28
  const CHECKLIST_LABEL_LIMIT = 70;
30
29
  const TASK_SILENT_AFTER_MS = 10 * 60 * 1e3;
@@ -13,7 +13,7 @@ bin/mm-harness
13
13
  -> @farmslot/agent-runtime checklist state
14
14
  -> @farmslot/handoff scrubbed learning capture/share
15
15
  -> @farmslot/recipe-cli recipe discovery: libraries, runnable checks, composition
16
- -> @farmslot/recipe-harness generic execution, UI transports, evidence
16
+ -> @farmslot/recipe-runner generic execution, UI transports, evidence
17
17
  -> @farmslot/protocol schemas
18
18
  -> adapters/ focused host/browser/device leaves
19
19
  -> library/ MetaMask actions and recipes
@@ -26,7 +26,7 @@ bin/mm-harness
26
26
  | `@farmslot/agent-runtime` | task-local checklist state and terminal contracts |
27
27
  | `@farmslot/handoff` | learning assembly, scrubbing, validation, approval, and publication |
28
28
  | `@farmslot/recipe-cli` | recipe discovery: library resolution, runnable checks, composition |
29
- | `@farmslot/recipe-harness` | generic execution, recovery, traces, artifacts, `ui.*` |
29
+ | `@farmslot/recipe-runner` | generic execution, recovery, traces, artifacts, `ui.*` |
30
30
  | `mm-harness` | MetaMask runtime control, diagnostics, durable domain actions |
31
31
  | skills/checklists | task workflow and proof expectations |
32
32
  | product checkout | app behavior and explicit debug hooks |
@@ -180,7 +180,7 @@ test. Update `CHANGELOG.md` under `Unreleased` for user-visible behavior.
180
180
 
181
181
  When upgrading the protocol or runtime, align their transitive consumers before
182
182
  pinning them here. Farmslot's `workspace:*` dependencies become exact versions
183
- when packed, so `agent-runtime` and `expo-recipe` can retain older copies until
183
+ when packed, so `agent-runtime` and `adapter-rn` can retain older copies until
184
184
  republished. Verify the complete install with
185
185
  `bash tests/contract/packaging-dist-install.test.sh` before releasing the harness.
186
186
 
@@ -109,11 +109,11 @@ Every family checks its own golden directory: a golden no case writes fails the
109
109
  Each case runs under a safety cap, `GOLDENS_CASE_TIMEOUT` (default 600 s). A case that hits it reports `TIMEOUT <case>` and fails without being compared or written, so `--update` can never accept a truncated capture.
110
110
 
111
111
  Each family is a normal contract test (`tests/contract/goldens-<family>.test.sh`). `gd_init` (`tests/contract/goldens/lib.sh`) builds a sandbox on `ct_init`, and every case runs under `env -i`:
112
- - `gd_init` first unsets every inherited `MM_HARNESS_*`, `RECIPE_*`, `METAMASK_*` and `FARMSLOT_*` variable, plus device and port pins (`IOS_SIMULATOR`, `ADB_SERIAL`, `WATCHER_PORT`, `METRO_PORT`, `CDP_PORT`, `IDB_PATH`, `ANDROID_HOME`, …). A developer's `MM_HARNESS_BIN` or `MM_HARNESS_IDB_PATH` export therefore cannot redirect a case;
112
+ - `gd_init` first unsets every inherited `MM_HARNESS_*`, `RECIPE_*`, `METAMASK_*` and `FARMSLOT_*` variable, plus device and port pins (`IOS_SIMULATOR`, `ADB_SERIAL`, `WATCHER_PORT`, `METRO_PORT`, `CDP_PORT`, `IDB_PATH`, `ANDROID_HOME`, …). A developer's `MM_HARNESS_BIN` or `RECIPE_RN_IDB_PATH` export therefore cannot redirect a case;
113
113
  - the environment is an allowlist: sandbox `HOME`, caches, config, `FARMSLOT_HOME`, `TZ=UTC`, no update probe, and only the variables the family sets itself;
114
114
  - every interpreter and tool the goldens launch is resolved to its real binary before `HOME` moves into the sandbox: `node` (`process.execPath`), `git` (`git --exec-path`), `bash` (`$BASH`) and `timeout`. The goldens need no Python. A tool that resolves to a script, the shape of an asdf/mise/volta/pyenv shim, fails the family up front with a message naming it. Each is checked to run with a sandboxed `HOME`;
115
115
  - `PATH` is pinned to the case stubs, the sandbox stubs, those real binaries, and `/usr/bin:/bin:/usr/sbin:/sbin`. Host tools (`idb`, `capture-helper`, a developer's Chrome) cannot change a golden;
116
- - device-tool discovery is pinned: `MM_HARNESS_IDB_PATH` and `MM_HARNESS_ADB_PATH` point at sandbox stubs, which discovery checks before any absolute Homebrew or pipx path;
116
+ - device-tool discovery is pinned: `RECIPE_RN_IDB_PATH` and `RECIPE_RN_ADB_PATH` point at sandbox stubs, which discovery checks before any absolute Homebrew or pipx path;
117
117
  - localhost probes cannot reach a real server. `gd_reserve_ports` runs a Node port holder (`tests/contract/goldens/port-holder.mjs`) that holds ephemeral ports on `127.0.0.1` and `::1` and resets every connection it accepts, so Metro `/status` and CDP probes never get a response and nothing real can take the port mid-run. Each family exports the farm's slot port variables (`RECIPE_CDP_PORT`, `WATCHER_PORT`, `METRO_PORT`) set to those ports, so commands without an explicit port use them too. Goldens show them as `<PORT:NAME>`, rewritten only in port positions (`*port` keys, `:<port>`, `port <n>`/`PORT=<n>`, or a string that is exactly the port), so an unrelated equal number such as a 60000 ms timeout is never rewritten;
118
118
  - `tmux`, `xcrun` and `adb` are stubbed (`ct_init`, plus one booted simulator and one Android device for Mobile);
119
119
  - forbidden stubs record any call to `open`, `osascript`, `lsappinfo`, `screencapture`, `yarn`, `npx`, `npm`, `corepack`, `gh` and Chrome/Chromium, and the family fails if one is called. A browser stub answers only `--version`, and `npm` only `root -g` (install-source detection), with a sandbox path;
@@ -0,0 +1,90 @@
1
+ # Feature-loss guard
2
+
3
+ While the platform phases move mm-harness code into Farmslot packages, nothing the frozen 0.76.0 release could do may get lost. Unit tests don't prove that. The guard has three parts, all run against real builds:
4
+
5
+ | Part | Script | Proves | When |
6
+ |---|---|---|---|
7
+ | Inventory | `scripts/feature-inventory.mjs --check` | every 0.76.0 capability still exists, or is mapped in `tests/feature-inventory/changes.json` | every PR review, every release |
8
+ | Live differential | `scripts/differential.mjs` | the same commands on the same real slots behave the same on 0.76.0 and on the PR build | every migration PR (report attached to the PR) |
9
+ | Release smoke | `scripts/release-smoke.mjs` | the fixed real recipes pass on the canary Mac, recorded, and match the previous release | every release |
10
+
11
+ The baseline is `@deeeed/metamask-harness@0.76.0` (da656404, before phase 2). The repo declares no dependency on it: `scripts/feature-guard/builds.mjs` installs it on first use, on each host, into `~/.cache/mm-harness-baseline/0.76.0` with npm's `--before` set just after 0.76.0's publish time, so every host resolves the dependency tree that release shipped with (the differential report prints each host's tree digest); it is never upgraded. The candidate is `npm pack` of the checkout, installed in `~/.cache/mm-harness-builds/<sha>`, so both sides are installed packages in sibling directories. Commands run as argv arrays, locally without a shell and remotely through one fixed ssh launcher with quoted arguments.
12
+
13
+ ## 1. Inventory
14
+
15
+ ```bash
16
+ node scripts/feature-inventory.mjs --check # this checkout, packed and installed, vs tests/feature-inventory/baseline-0.76.0.json
17
+ node scripts/feature-inventory.mjs --check --bin <installed mm-harness> # a published build
18
+ ```
19
+
20
+ The inventory asks the build itself, so it stays valid while code moves between packages:
21
+
22
+ - **CLI**: a public command refuses `<cmd> --feature-inventory-probe` as an unknown option, a removed one is refused as an unknown command. A flag exists when `<cmd> <flags…> --feature-inventory-canary --help` is refused only at the trailing canary (a lost flag is refused first; the canary proves the validator walked every flag before it, and `--help` keeps an accepted probe from running anything). An option value exists when `<cmd> <flag>=<value> --feature-inventory-canary --help` is refused only at the canary. Positional choices come from the "Valid values" an invalid positional is refused with. Candidates to probe are the union of the build's own command contract and the baseline's grammar, so moving `command-contract.ts` cannot hide a lost flag.
23
+ - **Oracle check**: every probe reads presence from a refusal, so the run first proves the refusals are seen (a bogus command, flag and option value must each be refused, `--help` must succeed), and any probe result that is neither an acceptance nor a known refusal stops the run. A build that does not start, or a CLI whose wording changed (the contract moving into `@farmslot/recipe-cli`), fails loudly; update the patterns in `feature-inventory.mjs` in that PR.
24
+ - **Hidden commands** (`self-test`, `ensure-ready`, `runtime-*`, `resolve-extension`, `completion-candidates`, `completions`) bypass the preflight validator. They are probed with a flag that has no value, which their own parsers reject before doing any work; a different result stops the run. If that parse ever changed, the probe would reach the command, inside the sandbox.
25
+ - **Per adapter** (the `--adapter` choices): action ids, their schema fields and enum values, and observers (`actions --raw --json`); call short names (`call --list --json`); bundled recipes and their parameters (`run --list --json`); doctor check ids (`doctor --json`) against an empty target, plus a real checkout per `--checkout <adapter>=<dir>`.
26
+ - **Environment variables**: a source scan of the package and every `@farmslot` package it depends on (a direct JS read, a name passed to a helper that reads env by parameter or listed next to an `env[<variable>]` access, or a shell expansion the script never assigns), the env inputs `adapters/manifest.json` documents, and every unset variable a Node process looked up while the probes ran (including a read-only `help review` per adapter) (`scripts/feature-guard/env-trace.mjs`, which sees names built at runtime). This is a floor: a name that is still present somewhere does not prove the same code path reads it; the differential covers behaviour.
27
+ - **Package surface**: bins, exports, the package entry's export names, `files` entries that exist in the package, `adapters/manifest.json` features, every shipped script and module path under `adapters/` and `library/`, and the export subpaths and bins of the `@farmslot` dependencies (where moved code lands).
28
+
29
+ Probes run in `scripts/feature-guard/sandbox.mjs`: a sandbox HOME and config, no inherited recipe libraries, inert device and process listings, and stubs that fail the inventory if a probe reaches a browser, launcher, package manager or `gh`.
30
+
31
+ `--check` fails on a baseline key that is missing from the build unless `changes.json` covers it. Doctor contexts the run did not record (no `--checkout`) are reported as skipped. The baseline file is regenerated only when the probe logic changes, and always from the frozen 0.76.0 bin:
32
+
33
+ ```bash
34
+ node scripts/feature-inventory.mjs --bin ~/.cache/mm-harness-baseline/0.76.0/node_modules/.bin/mm-harness \
35
+ --out tests/feature-inventory/baseline-0.76.0.json
36
+ ```
37
+
38
+ ### `changes.json`: consolidation is welcome
39
+
40
+ The guard protects capabilities, not names. Merging or simplifying actions, commands or flags is fine when the capability survives. Each entry has an `id`, a `reason` and a `pr`:
41
+
42
+ - `consolidated`: `from` (old keys, `*` allowed) → `to` (keys that must exist in the build), the `argumentMapping`, and a `differential` the differential's consolidated lane runs:
43
+ - `{ "kind": "cli", "where": "sandbox" | "slot", "adapter": …, "old": { "argv", "env" }, "new": { "argv", "env" } }`: the old invocation on 0.76.0 vs the mapped one on the PR build; equivalent normalized outcome passes.
44
+ - `{ "kind": "env", …same…, "names": [[old, new], …] }`: as `cli`, and the baseline must read each old variable and the build the new one (env trace; leave them unset).
45
+ - `{ "kind": "module", "old": [paths or specifiers], "new": [specifiers], "renamed": { old: new }, "names": [old names] }`: each new home exports every name the old module did (or, with `names`, the listed names when only part of the old module moved).
46
+ - `{ "kind": "script", "old": { "script", "args" }, "new": { "script", "args" } }`: a moved CLI script, run on both sides.
47
+ - `dropped`: `keys` proven dead, with `evidence` (no caller in farms, skills, recipe libraries, docs).
48
+
49
+ No shims: the PR that consolidates renames every caller (farm hooks, skills, recipes, docs) in the same change.
50
+
51
+ ## 2. Live differential
52
+
53
+ ```bash
54
+ node scripts/differential.mjs \
55
+ --slot core=~/dev/metamask/core-6 \
56
+ --slot terminal=~/dev/metamask/va-mmcx-terminal-1 --slot-var terminal:cdpPort=9541 --slot-var terminal:appPort=9341 \
57
+ --slot-env terminal:TERMINAL_EXTENSION_CHECKOUT=<extension checkout with a dist> \
58
+ --slot-env terminal:RECIPE_WALLET_FIXTURE=<farm wallet fixture> \
59
+ --slot-env terminal:RECIPE_LIBRARY_PATH=perps=<dir>:terminal=<dir> \
60
+ --slot extension=macpro.local:/Users/deeeed/dev/metamask/metamask-extension-1 \
61
+ --slot-var extension:cdpPort=7661 --slot-var extension:watcherPort=9011 --slot-var extension:slot=macpro-mme-1 \
62
+ --slot mobile=macpro.local:/Users/deeeed/dev/metamask/metamask-mobile-5 \
63
+ --slot-var mobile:watcherPort=8065 --slot-var mobile:device=mm-5
64
+ ```
65
+
66
+ Lanes (`--lanes`, default all):
67
+
68
+ - `help`: `--help`, `--version` and `<cmd> --help` for every baseline command, sandboxed.
69
+ - `slots`: per `--slot`, discovery (`actions --raw`, `call --list`, `run --list`, `doctor --json`) and the adapter's cases in `tests/differential/matrix.json`: the farm hook argv the goldens freeze, run on the real slot, plus real runs and calls (`live`), including a probe that requires the live transport (`cdp.target`), not only checkout metadata. The prelude gives both sides the same start; the whole baseline sequence runs before the candidate's. A slot on another host runs over ssh, with both builds installed there.
70
+ - `consolidated`: the `changes.json` differentials.
71
+ - `goldens`: the contract golden families run against each build from a copy of `tests/`, compared golden by golden.
72
+
73
+ Captures are normalized (paths, versions, pids, timestamps, durations, epoch numbers, encrypted vaults) and compared per facet: exit code, stdout (JSON paths or text), stderr when stdout is empty, artifact files and tree. `volatile` paths in the matrix (product console warnings that arrive on the product's own timing) are masked but their keys still compared. Every remaining difference must match an entry in `tests/differential/intended.json` (`case` glob, optional `facet`, `paths` or `text`, `reason`, `pr`). The report lands in `evidence/differential/<sha>/report.md`, with `results.json` and every raw capture beside it; attach the report to the PR. Verdicts: PASS (exit 0), FAIL (exit 1: an unexplained difference or a lane error), INCOMPLETE (exit 3: a case was skipped, e.g. a consolidated proof without its `--slot`).
74
+
75
+ What the normaliser deliberately masks, and so cannot catch: any semver under a key ending in `version` other than the harness's own, numbers under keys ending in `Ms`/`Seconds`/`duration`/`elapsed` (so a changed timeout value), epoch-millisecond numbers, and the `volatile` paths (a new product warning during a live run). Each varies run to run on real slots; the goldens freeze the static values.
76
+
77
+ Machine rules: on macwork, headless only and no focus stealing (`TERMINAL_HEADLESS=1` is in the matrix). Headful runs and the Extension wallet popup fail while macwork's screen is locked (`Page.captureScreenshot` timeout). Mobile and the Extension run on free macpro or mini slots, never mini-mm-3. The differential stops and cleans the slot runtime it used; reinstall the farm's overlay afterwards if the slot is handed back to the farm.
78
+
79
+ ## 3. Release smoke
80
+
81
+ ```bash
82
+ node scripts/release-smoke.mjs --version 0.79.0 <the same --slot flags> # a published release -> ~/dev/metamask/mm-harness-smoke/0.79.0
83
+ node scripts/release-smoke.mjs <the same --slot flags> # this checkout, packed -> .../0.79.0+<package digest>, compared with 0.79.0
84
+ ```
85
+
86
+ `tests/release-smoke/*.json` is the fixed set: Mobile Perps smoke, Extension Perps smoke, Terminal wallet connect (`terminal.perps.ensure-session`), and a core `call`. All are read-only; the canary slots must hold the farm's testnet wallet fixture (the Terminal enforces testnet itself). Each runs its prelude (install or fixture sync, launch, wallet fixture), the recipe with `--record-video=full-run` (the Terminal adapter has no video; its trace is the record), then its after steps (stop). Runs are kept per build on the host in `~/dev/metamask/mm-harness-smoke/<version>` (published) , `<version>+<package digest>` (a packed checkout; commits that only touch repo tooling are the same build) or `<version>+local` (`--bin`), with the full artifact directory (summary, trace, video) per smoke; an `--only` run updates its smokes and keeps the others recorded for the same packed or published build (a `--bin` build keeps nothing: it can change in place). The run compares with the published run at the highest version not above its own. Verdicts: FAIL (exit 1) on a non-pass status, a missing trace or expected video, or a regression (status, or the node/action sequence of a passing run); INCOMPLETE (exit 3) when a smoke has not run for this build or has nothing to compare with (`--no-compare` marks the first recorded run).
87
+
88
+ ## CI
89
+
90
+ Not wired yet. The inventory `--check` needs no slot and can run in CI on the packed build; the differential and the smoke set need real slots and stay in the review and release flow. The workflow change is proposed separately.
package/library/README.md CHANGED
@@ -10,7 +10,9 @@
10
10
  That is the engine contract (the recipe engine spawns/imports them with the
11
11
  input env set) — a bare `import` outside the engine throws
12
12
  "Missing live adapter input path", which is expected, not a bug.
13
- - Actions load harness helpers ONLY through `actions/harness-exports.mjs` (the
13
+ - Actions import the generic harness helpers (runtime paths, recording
14
+ snapshots) directly from `@farmslot/recipe-cli/harness`, and load the
15
+ MetaMask helpers ONLY through `actions/harness-exports.mjs` (the
14
16
  dist-preferring bridge) — never deep-import `src/` (published installs have no
15
17
  src/, and the entrypoint would close an import cycle through adapters.ts).
16
18
  - `recipes/` — runnable recipes (`mm-harness run <name-or-file>`).