@deeeed/metamask-harness 0.19.0 → 0.20.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 (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/adapters/extension/ensure-browser.sh +6 -4
  3. package/adapters/extension/launch-browser.cjs +6 -1
  4. package/adapters/extension/lib/chrome-args.cjs +11 -1
  5. package/adapters/extension/start-watch.sh +1 -1
  6. package/adapters/extension/verify.sh +17 -0
  7. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +33 -0
  8. package/adapters/mobile/open-device.sh +12 -1
  9. package/adapters/mobile/wait-for-bridge.sh +1 -1
  10. package/dist/adapters/extension/ensure-ready.js +7 -2
  11. package/dist/adapters/extension/runtime-decision.js +4 -6
  12. package/dist/adapters/extension/runtime.js +114 -17
  13. package/dist/adapters/mobile/prepare.js +40 -6
  14. package/dist/adapters.js +6 -1
  15. package/dist/command-contract.js +58 -5
  16. package/dist/commands/call.js +6 -3
  17. package/dist/commands/check.js +9 -2
  18. package/dist/commands/checklist.js +117 -14
  19. package/dist/commands/launch/extension.js +8 -3
  20. package/dist/commands/launch/index.js +38 -25
  21. package/dist/commands/launch/mobile.js +2 -2
  22. package/dist/commands/manifest.js +72 -5
  23. package/dist/commands/run-engine.js +7 -2
  24. package/dist/heal-bounds.js +1 -1
  25. package/dist/live-adapter-contract.js +15 -2
  26. package/dist/metamask-action-validation.js +45 -0
  27. package/dist/mm-harness-cli.js +7 -4
  28. package/docs/CONTRIBUTING.md +8 -0
  29. package/docs/RECIPES.md +15 -0
  30. package/library/actions/core/perps/assert_orders.mjs +16 -5
  31. package/library/actions/core/perps/assert_positions.mjs +16 -5
  32. package/library/actions/extension/perps/perps.mjs +111 -19
  33. package/library/actions/extension/platform/cdp.mjs +8 -3
  34. package/library/actions/extension/ui/navigate.mjs +239 -16
  35. package/library/actions/mobile/perps/perps.mjs +108 -10
  36. package/library/actions/mobile/ui/navigate.mjs +1 -1
  37. package/library/manifests/core.action-manifest.json +100 -11
  38. package/library/manifests/extension.action-manifest.json +80 -12
  39. package/library/manifests/mobile.action-manifest.json +85 -10
  40. package/library/recipes/perps/lifecycle.recipe.json +3 -9
  41. package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
  42. package/package.json +5 -4
  43. package/scripts/completions.sh +2 -2
@@ -58,8 +58,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
58
58
  })
59
59
  },
60
60
  checklist: {
61
- usage: "mm-harness checklist mark <task-dir> <step> [options]",
62
- options: options(HELP, {
61
+ usage: "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]",
62
+ options: options(HELP, JSON, {
63
63
  "--mark-last": bool(),
64
64
  "--already-fixed": bool(),
65
65
  "--no-self-review": bool(),
@@ -67,10 +67,14 @@ const PUBLIC_COMMAND_CONTRACTS = {
67
67
  "--skip-checklist": bool(),
68
68
  "--reason": value(),
69
69
  "--checklist": value(),
70
- "--signal": value()
70
+ "--signal": value(),
71
+ "--share": bool(),
72
+ "--config": value(),
73
+ "--destination": value(),
74
+ "--metadata": value()
71
75
  }),
72
76
  positionals: [
73
- { label: "action", choices: ["mark"] },
77
+ { label: "action", choices: ["mark", "closeout"] },
74
78
  { label: "task-dir" },
75
79
  {
76
80
  label: "step",
@@ -82,7 +86,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
82
86
  validDescription: "start|complete|no-change|blocked|a positive numeric step"
83
87
  }
84
88
  ],
85
- minimumPositionals: 3
89
+ minimumPositionals: 2
86
90
  },
87
91
  actions: {
88
92
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
@@ -307,6 +311,55 @@ function validatePublicInvocation(argv, examples = {}) {
307
311
  }
308
312
  }
309
313
  if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
314
+ if (name === "checklist") {
315
+ const action = positionals[0];
316
+ const markOnly = [
317
+ "--mark-last",
318
+ "--already-fixed",
319
+ "--no-self-review",
320
+ "--skip-learnings",
321
+ "--skip-checklist",
322
+ "--reason",
323
+ "--checklist",
324
+ "--signal"
325
+ ];
326
+ const closeoutOnly = [
327
+ "--json",
328
+ "--share",
329
+ "--config",
330
+ "--destination",
331
+ "--metadata"
332
+ ];
333
+ const invalidOption = (action === "mark" ? closeoutOnly : markOnly).find(
334
+ (option) => seenOptions.has(option)
335
+ );
336
+ if (invalidOption && (action === "mark" || action === "closeout")) {
337
+ return usageFailure(
338
+ "CLI_UNKNOWN_OPTION",
339
+ name,
340
+ `${invalidOption} is not valid with checklist ${action}.`,
341
+ contract,
342
+ examples
343
+ );
344
+ }
345
+ if (action === "mark" && positionals.length < 3) {
346
+ return missingPositionalFailure(
347
+ name,
348
+ "checklist mark requires <step>.",
349
+ contract,
350
+ examples
351
+ );
352
+ }
353
+ if (action === "closeout" && positionals.length > 2) {
354
+ return usageFailure(
355
+ "CLI_EXCESS_POSITIONAL",
356
+ name,
357
+ `unexpected positional '${positionals[2]}'; checklist closeout accepts only <task-dir>.`,
358
+ contract,
359
+ examples
360
+ );
361
+ }
362
+ }
310
363
  if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
311
364
  const missing = contract.positionals?.[0]?.label ?? "argument";
312
365
  return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
@@ -149,7 +149,8 @@ async function handleCall(argv) {
149
149
  describedAction,
150
150
  adapter,
151
151
  target,
152
- process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
152
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
153
+ args
153
154
  ) ?? `mm-harness actions --action ${resolvedAction} --adapter ${adapter}` : `mm-harness actions --action ${resolvedAction} --adapter ${adapter}`;
154
155
  if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, ...parameterHelp.length > 0 ? { parameterHelp } : {}, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
155
156
  else {
@@ -259,7 +260,8 @@ async function handleCall(argv) {
259
260
  describedAction,
260
261
  adapter,
261
262
  target,
262
- process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
263
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness",
264
+ args
263
265
  ) : void 0;
264
266
  const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
265
267
  return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
@@ -331,12 +333,13 @@ function parameterValidationHelp(action, findings, args) {
331
333
  const property = name && isRecord(properties[name]) ? properties[name] : void 0;
332
334
  if (!name || !property) return [];
333
335
  const validValues = Array.isArray(property.enum) ? property.enum : void 0;
336
+ const type = typeof property.type === "string" ? property.type : Array.isArray(property.type) ? property.type.filter((value) => typeof value === "string").join("|") : void 0;
334
337
  const received = Object.hasOwn(args, name) ? args[name] : void 0;
335
338
  const suggestion = issue === "invalid" && received !== void 0 && validValues ? closest(String(received), validValues.map(String)) : void 0;
336
339
  return [{
337
340
  issue,
338
341
  name,
339
- ...typeof property.type === "string" ? { type: property.type } : {},
342
+ ...type ? { type } : {},
340
343
  ...validValues ? { validValues } : {},
341
344
  ...typeof property.description === "string" ? { description: property.description } : {},
342
345
  ...issue === "invalid" && received !== void 0 ? { received } : {},
@@ -61,6 +61,7 @@ async function handleCheck(argv) {
61
61
  });
62
62
  const policyFailed = suppressionCheck.status === "fail";
63
63
  const fixes = [];
64
+ const eslintConfigArgs = resolveEslintConfigArgs(packageJson);
64
65
  if (fix && !policyFailed) {
65
66
  fixes.push(
66
67
  runToolCheck({
@@ -70,7 +71,7 @@ async function handleCheck(argv) {
70
71
  artifactsDir,
71
72
  files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
72
73
  command: executable(target, "eslint"),
73
- args: ["--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
74
+ args: [...eslintConfigArgs, "--fix", "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
74
75
  })
75
76
  );
76
77
  fixes.push(runFormatterFix({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
@@ -85,7 +86,7 @@ async function handleCheck(argv) {
85
86
  artifactsDir,
86
87
  files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file)),
87
88
  command: executable(target, "eslint"),
88
- args: ["--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
89
+ args: [...eslintConfigArgs, "--cache", "--cache-location", path.join(artifactsDir, "eslintcache")]
89
90
  })
90
91
  );
91
92
  checks.push(runFormatterCheck({ target, artifactsDir, files: existingChangedFiles.filter((file) => FORMAT_EXT_RE.test(file)) }));
@@ -294,6 +295,12 @@ function executable(target, name) {
294
295
  const local = path.join(target, "node_modules", ".bin", name);
295
296
  return fs.existsSync(local) ? local : null;
296
297
  }
298
+ function resolveEslintConfigArgs(packageJson) {
299
+ const lintScript = packageJson?.scripts?.["lint:eslint"];
300
+ if (typeof lintScript !== "string") return [];
301
+ const match = lintScript.match(/(?:^|\s)(?:-c|--config)\s+(['"]?)([^\s'"]+)\1/u);
302
+ return match?.[2] ? ["--config", match[2]] : [];
303
+ }
297
304
  function runToolCheck(input) {
298
305
  if (input.files.length === 0) {
299
306
  return { id: input.id, label: input.label, status: "skip", reason: "no changed matching files" };
@@ -7,7 +7,96 @@ const require2 = createRequire(import.meta.url);
7
7
  const MARK_SCRIPT = require2.resolve(
8
8
  "@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
9
9
  );
10
- const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
10
+ const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
11
+ const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
12
+ "--reason",
13
+ "--checklist",
14
+ "--signal",
15
+ "--config",
16
+ "--destination",
17
+ "--metadata"
18
+ ]);
19
+ function parseChecklistArgv(argv) {
20
+ const positionals = [];
21
+ for (let index = 0; index < argv.length; index += 1) {
22
+ const arg = argv[index] ?? "";
23
+ if (arg.startsWith("-")) {
24
+ const option = arg.split("=", 1)[0] ?? arg;
25
+ if (CHECKLIST_VALUE_OPTIONS.has(option) && !arg.includes("=")) index += 1;
26
+ continue;
27
+ }
28
+ positionals.push({ index, value: arg });
29
+ }
30
+ const consumed = new Set(positionals.slice(0, 2).map(({ index }) => index));
31
+ return {
32
+ action: positionals[0]?.value,
33
+ taskDir: positionals[1]?.value,
34
+ step: positionals[2]?.value,
35
+ forwarded: argv.filter((_, index) => !consumed.has(index))
36
+ };
37
+ }
38
+ function handoffCommand() {
39
+ if (process.env.HANDOFF_BIN) {
40
+ try {
41
+ fs.accessSync(process.env.HANDOFF_BIN, fs.constants.X_OK);
42
+ return { command: process.env.HANDOFF_BIN, prefix: [] };
43
+ } catch {
44
+ return { command: process.execPath, prefix: [process.env.HANDOFF_BIN] };
45
+ }
46
+ }
47
+ return {
48
+ command: process.execPath,
49
+ prefix: [require2.resolve("@farmslot/handoff/cli")]
50
+ };
51
+ }
52
+ function closeoutArgs(taskDir, args) {
53
+ const normalized = args.flatMap((arg) => {
54
+ const option = ["--config", "--destination", "--metadata"].find(
55
+ (name) => arg.startsWith(`${name}=`)
56
+ );
57
+ return option ? [option, arg.slice(option.length + 1)] : [arg];
58
+ });
59
+ if (process.env.METAMASK_AGENTIC_LEARNINGS_DIR && !normalized.includes("--destination")) {
60
+ return [
61
+ "closeout",
62
+ taskDir,
63
+ ...normalized,
64
+ "--destination",
65
+ process.env.METAMASK_AGENTIC_LEARNINGS_DIR
66
+ ];
67
+ }
68
+ return ["closeout", taskDir, ...normalized];
69
+ }
70
+ function runCloseout(taskDir, args, stdio) {
71
+ try {
72
+ const { command, prefix } = handoffCommand();
73
+ return spawnSync(command, [...prefix, ...closeoutArgs(taskDir, args)], {
74
+ encoding: "utf8",
75
+ stdio
76
+ });
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ return {
80
+ status: 1,
81
+ stdout: "",
82
+ stderr: message,
83
+ error: new Error(message)
84
+ };
85
+ }
86
+ }
87
+ function stageLearningPackage(taskDir, step, markArgs) {
88
+ if (!["complete", "no-change"].includes(step ?? "") || markArgs.includes("--skip-learnings") || !fs.existsSync(path.join(taskDir, "inputs", "handoff.json"))) {
89
+ return;
90
+ }
91
+ try {
92
+ const result = runCloseout(taskDir, ["--json"], "pipe");
93
+ if (result.status === 0) return;
94
+ } catch {
95
+ }
96
+ console.error(
97
+ `Warning: learning capture could not be staged; rerun: mm-harness checklist closeout ${taskDir}; task verdict is unchanged.`
98
+ );
99
+ }
11
100
  function terminalSignalStatus(taskDir) {
12
101
  try {
13
102
  const target = JSON.parse(
@@ -20,8 +109,7 @@ function terminalSignalStatus(taskDir) {
20
109
  return null;
21
110
  }
22
111
  }
23
- function preserveTerminalSignal(taskDir, markArgs) {
24
- const step = markArgs[0];
112
+ function preserveTerminalSignal(taskDir, step) {
25
113
  if (step !== "start" && numericMarkStep(step) === null) return false;
26
114
  const status = terminalSignalStatus(taskDir);
27
115
  if (!status) return false;
@@ -60,8 +148,7 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
60
148
  }
61
149
  return false;
62
150
  }
63
- function checkDiffGateReady(taskDir, markArgs) {
64
- const step = markArgs[0];
151
+ function checkDiffGateReady(taskDir, step) {
65
152
  const stepNumber = numericMarkStep(step);
66
153
  if (stepNumber === null) return true;
67
154
  let checklist = "";
@@ -92,8 +179,8 @@ function checkDiffGateReady(taskDir, markArgs) {
92
179
  console.error("Then rerun the same checklist mark command.");
93
180
  return false;
94
181
  }
95
- function teachRecipeCompletion(taskDir, markArgs) {
96
- if (markArgs[0] !== "complete") return;
182
+ function teachRecipeCompletion(taskDir, step) {
183
+ if (step !== "complete") return;
97
184
  const artifactsDir = path.join(taskDir, "artifacts");
98
185
  if (!fs.existsSync(path.join(artifactsDir, "recipe.json"))) return;
99
186
  const qualityPath = path.join(artifactsDir, "recipe-quality.json");
@@ -114,25 +201,41 @@ function teachRecipeCompletion(taskDir, markArgs) {
114
201
  console.error("Then rerun the same checklist complete command.");
115
202
  }
116
203
  async function handleChecklist(argv) {
117
- const [action, taskDir, ...markArgs] = argv;
118
- if (action !== "mark" || !taskDir || markArgs.length === 0) {
204
+ const { action, taskDir, step, forwarded } = parseChecklistArgv(argv);
205
+ if (!taskDir) {
119
206
  console.error(`usage: ${USAGE}`);
120
207
  return EXIT.usage;
121
208
  }
122
209
  const resolvedTaskDir = path.resolve(taskDir);
123
- if (preserveTerminalSignal(resolvedTaskDir, markArgs)) return EXIT.ok;
124
- if (!checkDiffGateReady(resolvedTaskDir, markArgs)) return EXIT.runtime;
125
- teachRecipeCompletion(resolvedTaskDir, markArgs);
210
+ if (action === "closeout") {
211
+ const result2 = runCloseout(resolvedTaskDir, forwarded, "inherit");
212
+ if (result2.error) {
213
+ console.error(`mm-harness checklist closeout: ${result2.error.message}`);
214
+ return EXIT.runtime;
215
+ }
216
+ return result2.status ?? EXIT.runtime;
217
+ }
218
+ if (action !== "mark" || !step) {
219
+ console.error(`usage: ${USAGE}`);
220
+ return EXIT.usage;
221
+ }
222
+ if (preserveTerminalSignal(resolvedTaskDir, step)) return EXIT.ok;
223
+ if (!checkDiffGateReady(resolvedTaskDir, step)) return EXIT.runtime;
224
+ teachRecipeCompletion(resolvedTaskDir, step);
126
225
  const result = spawnSync(
127
226
  process.execPath,
128
- [MARK_SCRIPT, resolvedTaskDir, ...markArgs],
227
+ [MARK_SCRIPT, resolvedTaskDir, ...forwarded],
129
228
  { stdio: "inherit" }
130
229
  );
131
230
  if (result.error) {
132
231
  console.error(`mm-harness checklist mark: ${result.error.message}`);
133
232
  return EXIT.runtime;
134
233
  }
135
- return result.status ?? EXIT.runtime;
234
+ const status = result.status ?? EXIT.runtime;
235
+ if (status === EXIT.ok) {
236
+ stageLearningPackage(resolvedTaskDir, step, forwarded);
237
+ }
238
+ return status;
136
239
  }
137
240
  export {
138
241
  handleChecklist
@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
2
2
  import http from "node:http";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
5
+ import {
6
+ depsCheck,
7
+ recordDepsBaseline
8
+ } from "@farmslot/recipe-harness/runtime/deps-readiness";
6
9
  import { colorHumanMessage } from "../../cli-color.js";
7
10
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
8
11
  import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
@@ -27,9 +30,11 @@ function extensionDepsBlock(target) {
27
30
  }
28
31
  return null;
29
32
  }
30
- function installExtensionDeps(target) {
33
+ async function installExtensionDeps(target) {
31
34
  const installScript = path.join(runnerDir, "adapters/shared/install-repo-deps.sh");
32
- return spawnScriptStreaming(installScript, ["--target", target], target);
35
+ const result = await spawnScriptStreaming(installScript, ["--target", target], target);
36
+ if (result.status === 0) recordDepsBaseline(target);
37
+ return result;
33
38
  }
34
39
  async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
35
40
  if (process.env.CHROME_USER_DATA_DIR) {
@@ -196,28 +196,28 @@ async function handleLaunchLocked(argv, stream) {
196
196
  exitCode: EXIT.infra
197
197
  });
198
198
  }
199
- const depsBlock = extensionDepsBlock(target);
200
- if (depsBlock) {
201
- if (heal === "off") {
202
- return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
203
- }
204
- const recoveryCode2 = "deps.installed";
205
- state.attemptedRecoveries.push(recoveryCode2);
206
- stream.phase("recover");
207
- const repaired = await installExtensionDeps(target);
208
- if (repaired.status !== 0 || extensionDepsBlock(target)) {
209
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
210
- code: "DEPENDENCY_INSTALL_FAILED",
211
- message: depsBlock.message,
212
- recoverable: false,
213
- userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
214
- originalError: repaired.output,
215
- exitCode: EXIT.infra
216
- });
217
- }
218
- state.recovered.push(recoveryCode2);
219
- state.mutations.push({ type: "dependencies", action: "installed", path: target });
199
+ }
200
+ const depsBlock = extensionDepsBlock(target);
201
+ if (depsBlock) {
202
+ if (heal === "off") {
203
+ return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
220
204
  }
205
+ const recoveryCode2 = "deps.installed";
206
+ state.attemptedRecoveries.push(recoveryCode2);
207
+ stream.phase("recover");
208
+ const repaired = await installExtensionDeps(target);
209
+ if (repaired.status !== 0 || extensionDepsBlock(target)) {
210
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
211
+ code: "DEPENDENCY_INSTALL_FAILED",
212
+ message: depsBlock.message,
213
+ recoverable: false,
214
+ userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
215
+ originalError: repaired.output,
216
+ exitCode: EXIT.infra
217
+ });
218
+ }
219
+ state.recovered.push(recoveryCode2);
220
+ state.mutations.push({ type: "dependencies", action: "installed", path: target });
221
221
  }
222
222
  }
223
223
  stream.phase("launch");
@@ -290,10 +290,20 @@ async function handleLaunchLocked(argv, stream) {
290
290
  originalError: bound.originalError
291
291
  });
292
292
  }
293
- const recoveryCode = RECOVERY_CODE[adapter];
293
+ const restartMobileApp = adapter === "mobile" && mobileBridgeTargetMissing(attempt.output);
294
+ const recoveryCode = restartMobileApp ? "mobile.app-restarted" : RECOVERY_CODE[adapter];
294
295
  state.attemptedRecoveries.push(recoveryCode);
295
296
  stream.phase("recover");
296
- attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
297
+ attempt = await executeComposition(
298
+ adapter,
299
+ mobileTarget,
300
+ tier,
301
+ wantWatch,
302
+ target,
303
+ machine,
304
+ displayMode,
305
+ restartMobileApp
306
+ );
297
307
  if (attempt.status === 0) {
298
308
  state.recovered.push(recoveryCode);
299
309
  return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
@@ -321,6 +331,9 @@ function extensionProductConfigMissing(output) {
321
331
  function mobileProvisioningBlocked(output) {
322
332
  return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
323
333
  }
334
+ function mobileBridgeTargetMissing(output) {
335
+ return /no bridge target matched/iu.test(output);
336
+ }
324
337
  function mobileProvisionCommand(target, mobileTarget) {
325
338
  const platform = mobileTarget === "android" ? "android" : "ios";
326
339
  const device = platform === "ios" ? process.env.IOS_SIMULATOR || process.env.SIM_UDID : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
@@ -406,9 +419,9 @@ function nativeInputsChanged(target, adapter) {
406
419
  return false;
407
420
  }
408
421
  }
409
- async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen") {
422
+ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen", restartMobileApp = false) {
410
423
  if (adapter === "mobile") {
411
- return launchMobile(target, mobileTarget, tier, json);
424
+ return launchMobile(target, mobileTarget, tier, json, restartMobileApp);
412
425
  }
413
426
  return launchExtension(target, tier, wantWatch, displayMode);
414
427
  }
@@ -1,11 +1,11 @@
1
1
  import { prepareMobile } from "../../adapters/mobile/prepare.js";
2
2
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
3
- async function launchMobile(target, mobileTarget, tier, json) {
3
+ async function launchMobile(target, mobileTarget, tier, json, restartApp = false) {
4
4
  await ensureHarnessFresh(target, "mobile");
5
5
  const platform = mobileTarget ?? "ios";
6
6
  const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
7
7
  const preflightMode = tier === "build" ? "auto" : "fast";
8
- return prepareMobile(target, { platform, json, watcherPort, preflightMode });
8
+ return prepareMobile(target, { platform, json, watcherPort, preflightMode, restartApp });
9
9
  }
10
10
  export {
11
11
  launchMobile
@@ -10,10 +10,12 @@ import {
10
10
  shellQuoteArg
11
11
  } from "./parse-args.js";
12
12
  import { resolveMetaMaskLibrarySources } from "./run-engine.js";
13
+ import { isSensitiveKey, redactStructuredValue } from "../command-journal.js";
13
14
  import {
14
15
  officialRecipeActionCapabilities
15
16
  } from "@farmslot/protocol";
16
17
  import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
18
+ import { closest } from "../command-contract.js";
17
19
  async function handleActions({ options, positional }) {
18
20
  const { adapter, target } = resolveAdapter(options);
19
21
  const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
@@ -174,7 +176,7 @@ function renderHumanActionCatalog(actions, options) {
174
176
  return lines.join("\n");
175
177
  }
176
178
  function renderHumanActionExample(entry, adapter, target, executable) {
177
- const node = authoredExampleNode(entry.examples);
179
+ const node = authoredExampleNode(entry.examples, void 0, entry.schema);
178
180
  if (!node) return void 0;
179
181
  const out = (style, text) => color(style, text, { stream: process.stdout });
180
182
  const command = actionExampleCommand(entry, adapter, target, executable);
@@ -187,12 +189,12 @@ function renderHumanActionExample(entry, adapter, target, executable) {
187
189
  ];
188
190
  return lines.join("\n");
189
191
  }
190
- function actionExampleCommand(entry, adapter, target, executable) {
191
- const node = authoredExampleNode(entry.examples);
192
+ function actionExampleCommand(entry, adapter, target, executable, preferredValues) {
193
+ const node = authoredExampleNode(entry.examples, preferredValues, entry.schema);
192
194
  if (!node) return void 0;
193
195
  const commandName = executable.endsWith("/mm-harness") ? "mm-harness" : executable;
194
196
  const args = entry.fields.flatMap(
195
- (field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${actionCallValue(node[field])}`)] : []
197
+ (field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${safeActionCallValue(field, node[field])}`)] : []
196
198
  );
197
199
  return [
198
200
  shellQuoteArg(commandName),
@@ -205,16 +207,81 @@ function actionExampleCommand(entry, adapter, target, executable) {
205
207
  shellQuoteArg(target)
206
208
  ].join(" ");
207
209
  }
208
- function authoredExampleNode(examples) {
210
+ function authoredExampleNode(examples, preferredValues, schema) {
209
211
  if (!Array.isArray(examples)) return void 0;
212
+ if (preferredValues && Object.keys(preferredValues).length > 0) {
213
+ const normalizedValues = normalizePreferredValues(preferredValues, schema);
214
+ const nodes = examples.map((example) => isRecord(example) && isRecord(example.node) ? example.node : void 0).filter((node) => node !== void 0);
215
+ if (nodes.length === 0) return void 0;
216
+ const ranked = nodes.map((node) => ({
217
+ node,
218
+ completeness: Object.keys(node).filter((name) => !["action", "intent", "next"].includes(name)).length,
219
+ score: Object.entries(normalizedValues).reduce((score, [name, value]) => {
220
+ if (!Object.hasOwn(node, name)) return score;
221
+ return score + (sameActionValue(name, node[name], value) ? 3 : -1);
222
+ }, 0)
223
+ })).sort(
224
+ (left, right) => right.score - left.score || right.completeness - left.completeness
225
+ );
226
+ return { ...ranked[0].node, ...normalizedValues };
227
+ }
210
228
  for (const example of examples) {
211
229
  if (isRecord(example) && isRecord(example.node)) return example.node;
212
230
  }
213
231
  return void 0;
214
232
  }
233
+ function normalizePreferredValues(preferredValues, schema) {
234
+ const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : {};
235
+ return Object.fromEntries(
236
+ Object.entries(preferredValues).flatMap(([name, value]) => {
237
+ const property = isRecord(properties[name]) ? properties[name] : void 0;
238
+ if (property && !matchesSchemaType(property.type, value)) return [];
239
+ const values = property && Array.isArray(property.enum) ? property.enum : void 0;
240
+ if (!values) {
241
+ return [[name, value]];
242
+ }
243
+ const compatibleValue = values.find(
244
+ (candidate) => sameActionValue(name, candidate, value)
245
+ );
246
+ if (compatibleValue !== void 0) return [[name, compatibleValue]];
247
+ const strings = values.filter((candidate) => typeof candidate === "string");
248
+ const suggestion = typeof value === "string" ? closest(value, strings) : void 0;
249
+ return suggestion === void 0 ? [] : [[name, suggestion]];
250
+ })
251
+ );
252
+ }
253
+ function matchesSchemaType(type, value) {
254
+ const types = Array.isArray(type) ? type : [type];
255
+ if (types.every((candidate) => typeof candidate !== "string")) return true;
256
+ return types.some((candidate) => {
257
+ if (candidate === "null") return value === null;
258
+ if (candidate === "array") return Array.isArray(value);
259
+ if (candidate === "object") return isRecord(value);
260
+ if (candidate === "integer") return typeof value === "number" && Number.isInteger(value);
261
+ return typeof value === candidate;
262
+ });
263
+ }
264
+ function sameActionValue(name, left, right) {
265
+ if (name === "state") {
266
+ const canonicalState = (value) => {
267
+ if (value === "present") return "open";
268
+ if (value === "absent") return "none";
269
+ return value;
270
+ };
271
+ left = canonicalState(left);
272
+ right = canonicalState(right);
273
+ }
274
+ const scalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
275
+ if (scalar(left) && scalar(right)) return String(left) === String(right);
276
+ return JSON.stringify(left) === JSON.stringify(right);
277
+ }
215
278
  function actionCallValue(value) {
216
279
  return typeof value === "string" ? value : JSON.stringify(value);
217
280
  }
281
+ function safeActionCallValue(field, value) {
282
+ if (isSensitiveKey(field)) return `<${field}>`;
283
+ return actionCallValue(redactStructuredValue(value));
284
+ }
218
285
  function humanFieldNames(entry) {
219
286
  const schema = isRecord(entry.schema) ? entry.schema : void 0;
220
287
  const properties = schema && isRecord(schema.properties) ? schema.properties : void 0;
@@ -20,6 +20,7 @@ import {
20
20
  } from "../paths.js";
21
21
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
+ import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
23
24
  import {
24
25
  beginRunDiagnostics,
25
26
  finishRunDiagnostics
@@ -276,7 +277,10 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
276
277
  manifest,
277
278
  validationOptions
278
279
  );
279
- const findings = [...withManifest.findings];
280
+ const findings = [
281
+ ...withManifest.findings,
282
+ ...validateMetaMaskActionInputs(recipe)
283
+ ];
280
284
  if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
281
285
  try {
282
286
  const digest = protocol.digestRecipeDocument(recipe);
@@ -304,7 +308,8 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
304
308
  dependency.document,
305
309
  manifest,
306
310
  validationOptions
307
- ).findings
311
+ ).findings,
312
+ ...validateMetaMaskActionInputs(dependency.document)
308
313
  );
309
314
  }
310
315
  } catch (error) {
@@ -62,7 +62,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
62
62
  }
63
63
  function classifyFailure(output) {
64
64
  if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
65
- if (/metro|cdp|chrome|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
65
+ if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
66
66
  return "infra";
67
67
  }
68
68
  return "app";