@deeeed/metamask-harness 0.17.5 → 0.18.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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
@@ -9,8 +9,9 @@ import {
9
9
  recipeRunning,
10
10
  checkHealBounds
11
11
  } from "../heal-bounds.js";
12
- import { loadActionManifest, validateManifest } from "../manifest.js";
12
+ import { resolveActionManifest, validateManifest } from "../manifest.js";
13
13
  import { getAdapterSurface } from "../adapters/surface.js";
14
+ import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
14
15
  import {
15
16
  importRecipeHarness,
16
17
  importRecipeProtocol,
@@ -20,56 +21,51 @@ import {
20
21
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
21
22
  import { listRecipeFiles } from "../recipe-files.js";
22
23
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
24
+ import {
25
+ beginRunDiagnostics,
26
+ finishRunDiagnostics
27
+ } from "../run-diagnostics.js";
23
28
  import { EXIT, spawnScriptStreaming } from "./shared.js";
24
29
  import {
25
30
  actionManifestPathOption,
26
31
  optionString,
27
32
  isRecord,
28
33
  shellQuoteArg,
34
+ targetPath,
29
35
  usageError
30
36
  } from "./parse-args.js";
31
- async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
32
- const previousCdpPort = process.env.CDP_PORT;
33
- const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
34
- const previousWatcherPort = process.env.WATCHER_PORT;
35
- const previousMetroPort = process.env.METRO_PORT;
36
- const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
37
- getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
38
- if (runtimeOptions.cdpPort) {
39
- process.env.CDP_PORT = runtimeOptions.cdpPort;
40
- process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
41
- }
42
- if (runtimeOptions.watcherPort) {
43
- process.env.WATCHER_PORT = runtimeOptions.watcherPort;
44
- process.env.METRO_PORT = runtimeOptions.watcherPort;
45
- }
46
- if (runtimeOptions.launchExistingDist) {
47
- process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
37
+ function requireRecipeTrustResolver(harness) {
38
+ if (typeof harness.resolveRecipeTrustInput !== "function") {
39
+ throw usageError(
40
+ "Recipe trust enforcement requires a newer @farmslot/recipe-harness than the installed package. Next: mm-harness update"
41
+ );
48
42
  }
43
+ return harness.resolveRecipeTrustInput;
44
+ }
45
+ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
46
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
47
+ adapter,
48
+ projectRoot,
49
+ runtimeOptions
50
+ );
49
51
  try {
52
+ const execution = preflightedExecution ?? await resolveRecipeExecution(
53
+ adapter,
54
+ recipe,
55
+ artifactsDir,
56
+ projectRoot,
57
+ actionManifestPath,
58
+ runtimeOptions
59
+ );
60
+ const { runner, runRequest, absoluteArtifactsDir, useFramedExtensionRecording } = execution;
61
+ await runner.preflight(runRequest);
50
62
  await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
51
- const absoluteArtifactsDir = path.resolve(artifactsDir);
52
- const recordVideo = runtimeOptions.recordVideo ?? false;
53
- const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
63
+ const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
54
64
  const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
55
65
  record: true,
56
66
  cdpPort: runtimeOptions.cdpPort
57
67
  }) : void 0;
58
68
  try {
59
- const manifest = loadActionManifest(adapter, actionManifestPath);
60
- await validateManifest(manifest);
61
- const { createMetaMaskRunner } = await import("../runner.js");
62
- const runner = await createMetaMaskRunner(adapter, manifest, {
63
- quietStdout: runtimeOptions.stdoutIsMachineContract === true
64
- });
65
- const runRequest = {
66
- recipePath: path.resolve(recipe),
67
- artifactsDir: absoluteArtifactsDir,
68
- projectRoot,
69
- env: recipeRunEnv(adapter, runtimeOptions),
70
- recordVideo: useFramedExtensionRecording ? false : recordVideo,
71
- ...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
72
- };
73
69
  let result;
74
70
  try {
75
71
  result = await runner.run(runRequest);
@@ -80,17 +76,141 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
80
76
  }
81
77
  }
82
78
  await stopRecipeRecording(recording, result);
83
- return result;
79
+ return finishRunDiagnostics(diagnosticBaseline, result);
84
80
  } finally {
85
81
  await stopRecipeRecording(recording);
86
82
  }
87
83
  } finally {
84
+ restoreRuntimeEnvironment();
85
+ }
86
+ }
87
+ async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
88
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
89
+ adapter,
90
+ projectRoot,
91
+ runtimeOptions
92
+ );
93
+ try {
94
+ const execution = await resolveRecipeExecution(
95
+ adapter,
96
+ recipe,
97
+ artifactsDir,
98
+ projectRoot,
99
+ actionManifestPath,
100
+ runtimeOptions
101
+ );
102
+ await execution.runner.preflight(execution.runRequest);
103
+ return execution;
104
+ } finally {
105
+ restoreRuntimeEnvironment();
106
+ }
107
+ }
108
+ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
109
+ const absoluteArtifactsDir = path.resolve(artifactsDir);
110
+ const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
111
+ const recordVideo = runtimeOptions.recordVideo ?? false;
112
+ const resolveRecipeTrustInput = requireRecipeTrustResolver(await importRecipeHarness());
113
+ const trust = resolveRecipeTrustInput({
114
+ sourceTrust: runtimeOptions.source?.trust,
115
+ sourceKind: runtimeOptions.source?.kind,
116
+ sourceName: runtimeOptions.source?.name,
117
+ sourceDigest: runtimeOptions.source?.digest,
118
+ approvalDigest: runtimeOptions.approval?.planDigest
119
+ });
120
+ const librarySources = executionLibrarySources(
121
+ runtimeOptions.librarySources,
122
+ trust.source?.trust ?? "trusted"
123
+ );
124
+ const { manifest, actionSources } = await resolveActionManifest(
125
+ adapter,
126
+ actionManifestPath,
127
+ librarySources,
128
+ process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR
129
+ );
130
+ const { createMetaMaskRunner } = await import("../runner.js");
131
+ const runner = await createMetaMaskRunner(adapter, manifest, {
132
+ quietStdout: runtimeOptions.stdoutIsMachineContract === true,
133
+ autoHud: trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
134
+ onActionEvent: runtimeOptions.onActionEvent,
135
+ actionSources,
136
+ // A direct engineer invocation is the explicit trust boundary for a
137
+ // task-local adapter. Inherited gateway provenance remains authoritative,
138
+ // so an untrusted worker cannot promote itself with CLI flags.
139
+ trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
140
+ });
141
+ const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
142
+ return {
143
+ runner,
144
+ absoluteArtifactsDir,
145
+ useFramedExtensionRecording,
146
+ runRequest: {
147
+ ...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
148
+ artifactsDir: absoluteArtifactsDir,
149
+ projectRoot,
150
+ inheritProcessEnv: false,
151
+ env: {
152
+ ...recipeProcessEnvironment(),
153
+ ...recipeRunEnv(adapter, runtimeOptions),
154
+ METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
155
+ },
156
+ recordVideo: useFramedExtensionRecording ? false : recordVideo,
157
+ ...trust,
158
+ ...librarySources ? { librarySources } : {}
159
+ }
160
+ };
161
+ }
162
+ function recipeProcessEnvironment() {
163
+ const env = { ...process.env };
164
+ delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
165
+ return env;
166
+ }
167
+ function executionLibrarySources(sources, invocationTrust) {
168
+ return sources?.map((source) => {
169
+ if (source.name === "metamask" || source.provenance?.trust === "trusted") return source;
170
+ return {
171
+ ...source,
172
+ provenance: {
173
+ ...source.provenance,
174
+ kind: source.provenance?.kind ?? "library",
175
+ trust: invocationTrust === "trusted" ? "trusted" : "unknown",
176
+ name: source.provenance?.name ?? source.name ?? path.basename(source.root)
177
+ }
178
+ };
179
+ });
180
+ }
181
+ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
182
+ const previousCdpPort = process.env.CDP_PORT;
183
+ const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
184
+ const previousWatcherPort = process.env.WATCHER_PORT;
185
+ const previousMetroPort = process.env.METRO_PORT;
186
+ const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
187
+ getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
188
+ if (runtimeOptions.cdpPort) {
189
+ process.env.CDP_PORT = runtimeOptions.cdpPort;
190
+ process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
191
+ }
192
+ if (runtimeOptions.watcherPort) {
193
+ process.env.WATCHER_PORT = runtimeOptions.watcherPort;
194
+ process.env.METRO_PORT = runtimeOptions.watcherPort;
195
+ }
196
+ if (runtimeOptions.launchExistingDist) {
197
+ process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
198
+ }
199
+ return () => {
88
200
  restoreEnv("CDP_PORT", previousCdpPort);
89
201
  restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
90
202
  restoreEnv("WATCHER_PORT", previousWatcherPort);
91
203
  restoreEnv("METRO_PORT", previousMetroPort);
92
204
  restoreEnv("METAMASK_RECIPE_EXTENSION_AUTOLAUNCH", previousExtensionAutolaunch);
205
+ };
206
+ }
207
+ function actionSourceMap(sources) {
208
+ const roots = {};
209
+ for (const [action, source] of sources) {
210
+ if (source.tier === "official") continue;
211
+ roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(path.dirname(path.dirname(source.manifestPath)), "actions"));
93
212
  }
213
+ return JSON.stringify(roots);
94
214
  }
95
215
  function recipeRunEnv(adapter, runtimeOptions = {}) {
96
216
  const base = {
@@ -162,12 +282,16 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
162
282
  const file = path.join(source.root, "recipes", candidate);
163
283
  if (isRecipeFile(file)) return { recipeFile: file };
164
284
  }
285
+ const listedFile = recipeFileByPublicName(path.join(source.root, "recipes"), recipeArg, adapter);
286
+ if (listedFile) return { recipeFile: listedFile };
165
287
  }
166
288
  } else {
167
289
  for (const candidate of candidates) {
168
290
  const file = recipePath(candidate);
169
291
  if (isRecipeFile(file)) return { recipeFile: file };
170
292
  }
293
+ const listedFile = recipeFileByPublicName(recipePath(""), recipeArg, adapter);
294
+ if (listedFile) return { recipeFile: listedFile };
171
295
  }
172
296
  }
173
297
  const packagedNames = libraryRecipeNames(adapter);
@@ -176,6 +300,155 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
176
300
  const suffix = packagedNames.length > 0 ? ` Library recipes for ${adapter}: ${packagedNames.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
177
301
  return { notFound: notFoundCore + suffix };
178
302
  }
303
+ function readRecipeMetadata(file) {
304
+ try {
305
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
306
+ return {
307
+ ...typeof parsed.title === "string" && parsed.title.trim() ? { title: parsed.title.trim() } : {},
308
+ ...typeof parsed.description === "string" && parsed.description.trim() ? { description: parsed.description.trim() } : {}
309
+ };
310
+ } catch {
311
+ return {};
312
+ }
313
+ }
314
+ function recipeNameFromFile(file, adapter) {
315
+ if (!file.endsWith(".recipe.json")) return void 0;
316
+ let name = file.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".");
317
+ const scope = name.split(".").pop() ?? "";
318
+ if (["mobile", "extension", "core"].includes(scope) && scope !== adapter) return void 0;
319
+ const scoped = scope === adapter;
320
+ if (scoped) name = name.slice(0, -(adapter.length + 1));
321
+ return { name, scoped };
322
+ }
323
+ function recipeFileByPublicName(recipesRoot, recipeName, adapter) {
324
+ let match;
325
+ let files;
326
+ try {
327
+ files = listRecipeFiles(recipesRoot);
328
+ } catch {
329
+ return void 0;
330
+ }
331
+ for (const file of files) {
332
+ const candidate = recipeNameFromFile(file, adapter);
333
+ if (!candidate || candidate.name !== recipeName) continue;
334
+ if (!match || candidate.scoped && !match.scoped) match = { file, scoped: candidate.scoped };
335
+ }
336
+ return match ? path.join(recipesRoot, match.file) : void 0;
337
+ }
338
+ function listRunnableRecipes(adapter, librarySources) {
339
+ const sources = librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: recipePath("") }];
340
+ const resolved = /* @__PURE__ */ new Map();
341
+ for (const source of sources) {
342
+ const recipesRoot = librarySources && librarySources.length > 0 ? path.join(source.root, "recipes") : source.root;
343
+ let files;
344
+ try {
345
+ files = listRecipeFiles(recipesRoot);
346
+ } catch {
347
+ continue;
348
+ }
349
+ const sourceName = source.name ?? path.basename(source.root);
350
+ const withinSource = /* @__PURE__ */ new Map();
351
+ for (const file of files) {
352
+ const candidate = recipeNameFromFile(file, adapter);
353
+ if (!candidate) continue;
354
+ const existing = withinSource.get(candidate.name);
355
+ if (!existing || candidate.scoped && !existing.scoped) {
356
+ withinSource.set(candidate.name, { file, scoped: candidate.scoped });
357
+ }
358
+ }
359
+ for (const [name, candidate] of withinSource) {
360
+ const winner = resolved.get(name);
361
+ if (winner) winner.shadows.push(sourceName);
362
+ else resolved.set(name, {
363
+ name,
364
+ source: sourceName,
365
+ file: path.join("recipes", candidate.file),
366
+ shadows: [],
367
+ ...readRecipeMetadata(path.join(recipesRoot, candidate.file))
368
+ });
369
+ }
370
+ }
371
+ return [...resolved.values()].sort((left, right) => left.name.localeCompare(right.name));
372
+ }
373
+ function describeRunnableRecipe(recipeArg, adapter, librarySources) {
374
+ const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
375
+ if ("notFound" in resolved) return resolved;
376
+ let document;
377
+ try {
378
+ document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
379
+ } catch (error) {
380
+ return {
381
+ unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
382
+ };
383
+ }
384
+ if (!isRecord(document)) {
385
+ return { unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.` };
386
+ }
387
+ const listed = listRunnableRecipes(adapter, librarySources).find((candidate) => candidate.name === recipeArg);
388
+ const composition = recipeComposition(document);
389
+ return {
390
+ recipe: {
391
+ name: listed?.name ?? recipeArg,
392
+ source: listed?.source ?? "file",
393
+ file: listed?.file ?? resolved.recipeFile,
394
+ shadows: listed?.shadows ?? [],
395
+ ...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
396
+ ...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {},
397
+ adapter,
398
+ path: resolved.recipeFile,
399
+ ...Object.prototype.hasOwnProperty.call(document, "schema_version") ? { schemaVersion: document.schema_version } : {},
400
+ inputs: isRecord(document.inputs) ? redactRecipeInputs(document.inputs) : {},
401
+ inputOverridesSupported: false,
402
+ actions: composition.actions,
403
+ actionsComplete: composition.actionsComplete,
404
+ flows: composition.flows
405
+ }
406
+ };
407
+ }
408
+ function recipeComposition(recipe) {
409
+ const actions = /* @__PURE__ */ new Set();
410
+ const flows = /* @__PURE__ */ new Set();
411
+ const visitNode = (value) => {
412
+ if (!isRecord(value) || typeof value.action !== "string") return;
413
+ actions.add(value.action);
414
+ if (value.action === "call" && typeof value.ref === "string") flows.add(value.ref);
415
+ };
416
+ const visitWorkflow = (value) => {
417
+ if (!isRecord(value)) return;
418
+ for (const phase of ["setup", "teardown"]) {
419
+ if (Array.isArray(value[phase])) value[phase].forEach(visitNode);
420
+ }
421
+ if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
422
+ };
423
+ visitNode(recipe.startState);
424
+ if (isRecord(recipe.validate)) visitWorkflow(recipe.validate.workflow);
425
+ const inlineFlows = isRecord(recipe.flows) ? recipe.flows : {};
426
+ const visitedFlows = /* @__PURE__ */ new Set();
427
+ let actionsComplete = true;
428
+ for (const ref of flows) {
429
+ if (visitedFlows.has(ref)) continue;
430
+ visitedFlows.add(ref);
431
+ const flow = inlineFlows[ref];
432
+ if (flow !== void 0) {
433
+ visitWorkflow(isRecord(flow) && isRecord(flow.workflow) ? flow.workflow : flow);
434
+ } else {
435
+ actionsComplete = false;
436
+ }
437
+ }
438
+ return {
439
+ actions: [...actions].sort(),
440
+ actionsComplete,
441
+ flows: [...flows].sort()
442
+ };
443
+ }
444
+ function redactRecipeInputs(inputs) {
445
+ return Object.fromEntries(
446
+ Object.entries(inputs).map(([key, value]) => [
447
+ key,
448
+ /(?:password|passphrase|secret|token|credential|mnemonic|seed|srp|vault|api[_-]?key|private[_-]?key)/iu.test(key) ? "<redacted>" : isRecord(value) ? redactRecipeInputs(value) : Array.isArray(value) ? value.map((entry) => isRecord(entry) ? redactRecipeInputs(entry) : entry) : value
449
+ ])
450
+ );
451
+ }
179
452
  function libraryRecipeNames(adapter) {
180
453
  let entries;
181
454
  try {
@@ -183,20 +456,11 @@ function libraryRecipeNames(adapter) {
183
456
  } catch {
184
457
  return [];
185
458
  }
186
- const scopes = ["mobile", "extension", "core"];
187
- const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
188
- const scope = n.split(".").pop() ?? "";
189
- return !scopes.includes(scope) || scope === adapter;
190
- }).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
459
+ const names = entries.map((file) => recipeNameFromFile(file, adapter)?.name).filter((name) => Boolean(name));
191
460
  return [...new Set(names)].sort();
192
461
  }
193
462
  function recipeNameCandidates(recipeArg, adapter) {
194
- const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
195
- const candidates = [];
196
- for (const name of names) {
197
- candidates.push(...singleRecipeNameCandidates(name, adapter));
198
- }
199
- return [...new Set(candidates)];
463
+ return singleRecipeNameCandidates(recipeArg, adapter);
200
464
  }
201
465
  function singleRecipeNameCandidates(name, adapter) {
202
466
  if (name.endsWith(".recipe.json")) {
@@ -222,32 +486,6 @@ function domainPathForRecipeName(name) {
222
486
  function uniqueStrings(values) {
223
487
  return [...new Set(values)];
224
488
  }
225
- const LEGACY_RECIPE_ALIASES = {
226
- "action-validation": ["runner.action-validation"],
227
- "action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
228
- "action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
229
- "app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
230
- "app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
231
- "order-lifecycle": ["perps.order-lifecycle"],
232
- "order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
233
- "perps-lifecycle": ["perps.lifecycle"],
234
- "perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
235
- "perps-performance": ["perps.performance"],
236
- "perps-performance-background-resume": ["perps.performance.background-resume"],
237
- "perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
238
- "perps-performance-cold-start": ["perps.performance.cold-start"],
239
- "perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
240
- "perps-performance-warm-start": ["perps.performance.warm-start"],
241
- "perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
242
- "perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
243
- "read-markets": ["perps.read-markets"],
244
- "read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
245
- "smoke": ["runner.smoke"],
246
- "smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
247
- "smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
248
- "trading-lifecycle": ["perps.trading-lifecycle"],
249
- "trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
250
- };
251
489
  async function validateRunRecipeStatic(recipeArg, adapter, options) {
252
490
  const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
253
491
  const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
@@ -283,7 +521,11 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
283
521
  };
284
522
  }
285
523
  const findings = [];
286
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
524
+ const { manifest } = await resolveActionManifest(
525
+ adapter,
526
+ optionString(options, "actionManifest"),
527
+ librarySources
528
+ );
287
529
  let manifestOk = true;
288
530
  try {
289
531
  await validateManifest(manifest);
@@ -333,7 +575,12 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
333
575
  async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
334
576
  if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
335
577
  try {
336
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
578
+ const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
579
+ const { manifest } = await resolveActionManifest(
580
+ adapter,
581
+ optionString(options, "actionManifest"),
582
+ librarySources
583
+ );
337
584
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
338
585
  const actions = getRecipeActionManifestActionNames(manifest);
339
586
  const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
@@ -341,6 +588,7 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
341
588
  const device = optionString(options, "device");
342
589
  const actionManifest = optionString(options, "actionManifest");
343
590
  const parts = ["mm-harness", "call", matches[0]];
591
+ parts.push("--adapter", adapter, "--target", shellQuoteArg(targetPath(options)));
344
592
  if (device) parts.push("--device", shellQuoteArg(device));
345
593
  if (actionManifest) parts.push("--action-manifest", shellQuoteArg(actionManifest));
346
594
  return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
@@ -353,16 +601,33 @@ async function resolveMetaMaskLibrarySources(libraryEntry) {
353
601
  if (typeof harness.resolveRecipeLibrarySources !== "function") {
354
602
  if (libraryEntry) {
355
603
  throw usageError(
356
- "--library requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support / resolveRecipeLibrarySources). The pinned 0.3.0 lacks it, and npm 0.3.2 still does not export it \u2014 recipe-library support is PENDING PUBLISH from farmslot."
604
+ "--library requires a current @farmslot/recipe-harness installation that exports recipe-library support. Next: mm-harness update"
357
605
  );
358
606
  }
359
607
  return void 0;
360
608
  }
609
+ const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
361
610
  const sources = await harness.resolveRecipeLibrarySources(
362
- libraryEntry ? { cliEntries: [libraryEntry] } : void 0
611
+ cliEntries.length > 0 ? { cliEntries } : void 0
363
612
  );
364
- sources.push({ name: "metamask", root: path.join(runnerDir, "library") });
365
- return sources;
613
+ const canonicalRoot = path.resolve(runnerDir, "library");
614
+ const configuredCanonical = sources.find((source) => source.name === "metamask");
615
+ if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
616
+ throw usageError(
617
+ `Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
618
+ );
619
+ }
620
+ if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
621
+ sources.push({ name: "metamask", root: canonicalRoot });
622
+ }
623
+ return sources.map((source) => ({
624
+ ...source,
625
+ provenance: source.provenance ?? {
626
+ kind: source.name === "metamask" ? "bundled" : "library",
627
+ trust: source.name === "metamask" ? "trusted" : "unknown",
628
+ name: source.name ?? path.basename(source.root)
629
+ }
630
+ }));
366
631
  }
367
632
  function synthesizeOneNodeRecipe(action, args) {
368
633
  return {
@@ -373,7 +638,12 @@ function synthesizeOneNodeRecipe(action, args) {
373
638
  workflow: {
374
639
  entry: "call",
375
640
  nodes: {
376
- call: { action, ...args, next: "done", intent: `Call ${action} in isolation` },
641
+ call: {
642
+ ...args,
643
+ action,
644
+ next: "done",
645
+ intent: typeof args.intent === "string" ? args.intent : `Call ${action} in isolation`
646
+ },
377
647
  done: { action: "end", status: "pass" }
378
648
  }
379
649
  }
@@ -381,7 +651,8 @@ function synthesizeOneNodeRecipe(action, args) {
381
651
  };
382
652
  }
383
653
  async function runOneNode(adapter, action, args, target, actionManifest) {
384
- const manifest = loadActionManifest(adapter, actionManifest);
654
+ const librarySources = await resolveMetaMaskLibrarySources(void 0);
655
+ const { manifest } = await resolveActionManifest(adapter, actionManifest, librarySources);
385
656
  const recipe = synthesizeOneNodeRecipe(action, args);
386
657
  const validation = await validateRecipeAdapterAware(recipe, manifest);
387
658
  if (validation.status === "invalid") return { status: "fail" };
@@ -390,7 +661,7 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
390
661
  fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
391
662
  `);
392
663
  const result = await runRecipe(adapter, recipeFile, path.join(scratch, "artifacts"), target, actionManifest, {
393
- librarySources: await resolveMetaMaskLibrarySources(void 0),
664
+ librarySources,
394
665
  stdoutIsMachineContract: true
395
666
  });
396
667
  return { status: result.status === "pass" ? "pass" : "fail" };
@@ -398,10 +669,12 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
398
669
  async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
399
670
  if (recipeRunning(target)) {
400
671
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
672
+ const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
401
673
  if (json) {
402
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
674
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
403
675
  } else {
404
- console.error(`\u2717 mm-harness: ${msg}`);
676
+ console.error(`\u2717 mm-harness: ${msg}
677
+ Next: ${userAction}`);
405
678
  }
406
679
  return EXIT.bounded;
407
680
  }
@@ -414,21 +687,25 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
414
687
  return EXIT.usage;
415
688
  }
416
689
  const state = newHealState();
690
+ prepareOptions.onPhase?.("install");
417
691
  const ensured = await ensureOverlay(adapter, target, heal, state, json);
418
692
  if (!ensured.ok) {
693
+ const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
419
694
  if (json) {
420
695
  console.log(
421
696
  JSON.stringify(
422
- { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
697
+ { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error, userAction } },
423
698
  null,
424
699
  2
425
700
  )
426
701
  );
427
702
  } else {
428
- console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}`);
703
+ console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
704
+ Next: ${userAction}`);
429
705
  }
430
706
  return EXIT.infra;
431
707
  }
708
+ prepareOptions.onPhase?.("healthcheck");
432
709
  if (adapter === "extension") {
433
710
  const previousCdpPort = process.env.CDP_PORT;
434
711
  const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
@@ -441,7 +718,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
441
718
  }
442
719
  if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
443
720
  try {
444
- const current = await ensureExtensionProofRuntime(target, heal, state, json);
721
+ const current = await ensureExtensionProofRuntime(target, heal, state, json, {
722
+ onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
723
+ });
445
724
  if (current !== null) return current;
446
725
  } finally {
447
726
  restoreEnv("CDP_PORT", previousCdpPort);
@@ -450,7 +729,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
450
729
  }
451
730
  }
452
731
  if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
453
- const current = await ensureMobileProofRuntime(target, heal, state, json);
732
+ const current = await ensureMobileProofRuntime(target, heal, state, json, {
733
+ onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
734
+ });
454
735
  if (current !== null) return current;
455
736
  }
456
737
  return { state, heal };
@@ -460,6 +741,20 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
460
741
  const check = deps.check ?? (() => source.mobileSourceCheck(target));
461
742
  const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
462
743
  const initial = check();
744
+ if (initial.status === "unavailable") {
745
+ if (json) {
746
+ console.log(JSON.stringify({
747
+ schemaVersion: 1,
748
+ status: "fail",
749
+ recoverable: false,
750
+ error: initial.error
751
+ }, null, 2));
752
+ } else {
753
+ console.error(`\u2717 ${initial.error.message}`);
754
+ console.error(` Next: ${initial.error.userAction}`);
755
+ }
756
+ return EXIT.usage;
757
+ }
463
758
  if (initial.status === "current") return null;
464
759
  const targetArg = JSON.stringify(path.resolve(target));
465
760
  const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
@@ -481,6 +776,7 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
481
776
  }
482
777
  const recoveryCode = "mobile.source_reloaded";
483
778
  state.attemptedRecoveries.push(recoveryCode);
779
+ deps.onRecovery?.(recoveryCode);
484
780
  if (!json) console.error(`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`);
485
781
  const restart = deps.restart ?? (async () => {
486
782
  const result2 = await runOneNode(
@@ -521,6 +817,12 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
521
817
  state.mutations.push({ type: "runtime", action: "restarted", reasonCode: `source-${initial.status}` });
522
818
  return null;
523
819
  }
820
+ function resolveRunMobilePlatform() {
821
+ if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
822
+ return process.env.PLATFORM;
823
+ }
824
+ return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
825
+ }
524
826
  async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
525
827
  const decide = deps.decide ?? (async () => {
526
828
  const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
@@ -532,6 +834,22 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
532
834
  });
533
835
  const initial = await decide();
534
836
  if (initial.decision === "ready") return null;
837
+ if (initial.reasonCode === "runtime-dist-check-failed") {
838
+ const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
839
+ const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
840
+ if (json) {
841
+ console.log(JSON.stringify({
842
+ schemaVersion: 1,
843
+ status: "fail",
844
+ recoverable: false,
845
+ error: { code: "EXTENSION_RUNTIME_CHECK_FAILED", message, userAction: userAction2 }
846
+ }, null, 2));
847
+ } else {
848
+ console.error(`\u2717 ${message}`);
849
+ console.error(` Next: ${userAction2}`);
850
+ }
851
+ return EXIT.infra;
852
+ }
535
853
  if (!["dist-stale", "dist-missing", "runtime-dist-stale", "runtime-dist-missing"].includes(initial.reasonCode)) {
536
854
  return null;
537
855
  }
@@ -556,20 +874,33 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
556
874
  return EXIT.infra;
557
875
  }
558
876
  const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
877
+ const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
878
+ if (productConfigBlock) {
879
+ const message = productConfigBlock.message;
880
+ const userAction2 = productConfigBlock.userAction;
881
+ if (json) {
882
+ console.log(JSON.stringify({
883
+ schemaVersion: 1,
884
+ status: "fail",
885
+ recoverable: false,
886
+ mutations: state.mutations,
887
+ error: { code: "EXTENSION_PRODUCT_CONFIG_REQUIRED", message, userAction: userAction2 }
888
+ }, null, 2));
889
+ } else {
890
+ console.error(`\u2717 ${message}`);
891
+ console.error(` Next: ${userAction2}`);
892
+ }
893
+ return EXIT.infra;
894
+ }
559
895
  state.attemptedRecoveries.push(recoveryCode);
896
+ deps.onRecovery?.(recoveryCode);
560
897
  if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
561
898
  const launch = deps.launch ?? (async (requestedTier) => {
562
899
  const { launchExtension } = await import("./launch/extension.js");
563
900
  return launchExtension(target, requestedTier, false);
564
901
  });
565
902
  const result = await launch(tier);
566
- let verified = result.status === 0 ? await decide() : initial;
567
- if (result.status === 0 && verified.decision !== "ready") {
568
- for (let attempt = 0; attempt < 8 && verified.decision !== "ready"; attempt += 1) {
569
- await new Promise((resolve) => setTimeout(resolve, 250));
570
- verified = await decide();
571
- }
572
- }
903
+ const verified = result.status === 0 ? await decide() : initial;
573
904
  if (result.status !== 0 || verified.decision !== "ready") {
574
905
  const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
575
906
  if (json) {
@@ -603,17 +934,11 @@ async function recoverRunInfra(adapter, target, json) {
603
934
  }
604
935
  if (adapter === "mobile") {
605
936
  const { launchMobile } = await import("./launch/mobile.js");
606
- const platform = resolveRunMobilePlatform();
937
+ const platform = process.env.PLATFORM === "android" ? "android" : "ios";
607
938
  return launchMobile(target, platform, "quick", json);
608
939
  }
609
940
  return { status: 0, output: "headless run retry" };
610
941
  }
611
- function resolveRunMobilePlatform() {
612
- if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
613
- return process.env.PLATFORM;
614
- }
615
- return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
616
- }
617
942
  function readRunFailureText(result) {
618
943
  try {
619
944
  const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
@@ -623,7 +948,7 @@ function readRunFailureText(result) {
623
948
  return "";
624
949
  }
625
950
  }
626
- async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
951
+ async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
627
952
  let result = await exec();
628
953
  for (; ; ) {
629
954
  if (result.status === "pass") return { result, violation: null };
@@ -632,6 +957,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
632
957
  if (heal === "off") return { result, violation: null };
633
958
  const recoveryCode = RUN_RECOVERY_CODE[adapter];
634
959
  state.attemptedRecoveries.push(recoveryCode);
960
+ onRecovery(recoveryCode);
635
961
  await recover();
636
962
  result = await exec();
637
963
  if (result.status === "pass") {
@@ -641,7 +967,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
641
967
  }
642
968
  }
643
969
  function emitHealViolation(json, command, result, violation, state, adapter) {
644
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction;
970
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
645
971
  if (json) {
646
972
  console.log(
647
973
  JSON.stringify(
@@ -661,7 +987,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
661
987
  code: violation.code,
662
988
  message: violation.message,
663
989
  retryable: false,
664
- userAction: userAction ?? null,
990
+ userAction,
665
991
  originalError: violation.originalError ?? null
666
992
  }
667
993
  },
@@ -688,12 +1014,16 @@ function countRecipeNodes(recipe) {
688
1014
  }
689
1015
  export {
690
1016
  countRecipeNodes,
1017
+ describeRunnableRecipe,
691
1018
  emitHealViolation,
692
1019
  ensureExtensionProofRuntime,
693
1020
  ensureMobileProofRuntime,
694
1021
  executeWithHealBounds,
1022
+ listRunnableRecipes,
1023
+ preflightRecipe,
695
1024
  prepareHeal,
696
1025
  recoverRunInfra,
1026
+ requireRecipeTrustResolver,
697
1027
  resolveMetaMaskLibrarySources,
698
1028
  resolveRunRecipeArg,
699
1029
  runOneNode,