@deeeed/metamask-harness 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +4 -7
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/inject.mjs +1 -4
  5. package/adapters/extension/verify.sh +1 -1
  6. package/adapters/mobile/inject.sh +5 -4
  7. package/adapters/mobile/verify.sh +15 -19
  8. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  9. package/dist/adapters/harness-freshness.js +49 -0
  10. package/dist/cli.js +0 -4
  11. package/dist/command-contract.js +51 -40
  12. package/dist/command-journal.js +50 -12
  13. package/dist/commands/call.js +95 -20
  14. package/dist/commands/check.js +1 -1
  15. package/dist/commands/completion-candidates.js +7 -19
  16. package/dist/commands/fixtures.js +50 -1
  17. package/dist/commands/last.js +9 -1
  18. package/dist/commands/launch/extension.js +2 -2
  19. package/dist/commands/launch/mobile.js +2 -0
  20. package/dist/commands/list-executables.js +56 -20
  21. package/dist/commands/manifest.js +28 -12
  22. package/dist/commands/parse-args.js +55 -3
  23. package/dist/commands/provision.js +0 -1
  24. package/dist/commands/run-engine.js +514 -336
  25. package/dist/commands/run.js +25 -37
  26. package/dist/commands/shared.js +1 -1
  27. package/dist/heal-bounds.js +5 -0
  28. package/dist/mm-harness-cli.js +22 -30
  29. package/dist/run-diagnostics.js +1 -1
  30. package/dist/run-recording.js +1 -1
  31. package/dist/runner.js +24 -2
  32. package/docs/CONTRIBUTING.md +2 -3
  33. package/docs/QA.md +0 -1
  34. package/docs/RECIPES.md +52 -91
  35. package/library/README.md +5 -5
  36. package/library/actions/core/perps/_controller.mjs +33 -1
  37. package/library/actions/core/perps/assert_orders.mjs +6 -7
  38. package/library/actions/core/perps/assert_positions.mjs +6 -7
  39. package/library/actions/core/perps/close_orders.mjs +2 -0
  40. package/library/actions/core/perps/close_positions.mjs +2 -0
  41. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  42. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  43. package/library/actions/core/perps/place_order.mjs +7 -3
  44. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  45. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  46. package/library/actions/extension/perps/perps.mjs +43 -14
  47. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  48. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  49. package/library/actions/mobile/perps/perps.mjs +40 -12
  50. package/library/library.json +1 -1
  51. package/library/manifests/core.action-manifest.json +1170 -413
  52. package/library/manifests/extension.action-manifest.json +1495 -641
  53. package/library/manifests/mobile.action-manifest.json +1704 -769
  54. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  55. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  56. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  57. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  58. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  59. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  60. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  61. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  62. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  63. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  64. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  65. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  66. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  67. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  68. package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
  69. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
  70. package/library/recipes/runner/smoke.core.recipe.json +18 -20
  71. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  72. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  73. package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
  74. package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
  75. package/package.json +3 -3
  76. package/scripts/completions.sh +1 -4
  77. package/dist/adapters/extension/harness-freshness.js +0 -39
  78. package/dist/commands/flows.js +0 -91
  79. package/library/flows/perps.flows.json +0 -64
@@ -10,6 +10,7 @@ import {
10
10
  resolveAdapter,
11
11
  runtimeOptionsFromCli,
12
12
  isRecord,
13
+ parseRecipeParamAssignments,
13
14
  shellQuote,
14
15
  targetPath,
15
16
  usageError
@@ -33,6 +34,7 @@ import { acquireCheckoutLock } from "../checkout-lock.js";
33
34
  import { JsonStreamWriter } from "../json-stream.js";
34
35
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
35
36
  import { recipeTrustFailure } from "../recipe-security.js";
37
+ import { recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
36
38
  async function handleRun(parsed) {
37
39
  const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
38
40
  const restoreStdout = stream.isolateStdout();
@@ -104,7 +106,11 @@ async function handleRunInner({ positional, options }, stream) {
104
106
  }
105
107
  const targetRecipe = positional[0];
106
108
  if (!targetRecipe) throw usageError("run requires <recipe.json>.");
109
+ const paramAssignments = positional.slice(1);
107
110
  if (optionFlag(options, "describe")) {
111
+ if (paramAssignments.length > 0) {
112
+ throw usageError("run --describe does not accept parameter values; inspect the declaration first.");
113
+ }
108
114
  if (stream.enabled) {
109
115
  const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
110
116
  stream.error({
@@ -116,7 +122,8 @@ async function handleRunInner({ positional, options }, stream) {
116
122
  }
117
123
  return handleDescribeRecipe(targetRecipe, options);
118
124
  }
119
- if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options, stream);
125
+ const params = parseRecipeParamAssignments(paramAssignments);
126
+ if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, params, options, stream);
120
127
  const { adapter, target } = resolveAdapter(options);
121
128
  const json = optionFlag(options, "json");
122
129
  const jsonOutput = json && !stream.enabled;
@@ -139,7 +146,7 @@ async function handleRunInner({ positional, options }, stream) {
139
146
  }
140
147
  if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
141
148
  stream.phase("validate");
142
- const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
149
+ const validated = await validateRunRecipeStatic(targetRecipe, adapter, options, params);
143
150
  if (validated.usageError) {
144
151
  const userAction = runUsageRecovery(
145
152
  validated.usageError.code,
@@ -160,7 +167,7 @@ async function handleRunInner({ positional, options }, stream) {
160
167
  if (validated.errorCount > 0) {
161
168
  return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
162
169
  }
163
- const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
170
+ const depsBlock = adapter === "core" && await recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
164
171
  if (depsBlock) {
165
172
  return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
166
173
  }
@@ -179,9 +186,11 @@ async function handleRunInner({ positional, options }, stream) {
179
186
  "set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
180
187
  );
181
188
  }
189
+ recordCommandEvidence(artifactsDir);
182
190
  const librarySources = validated.librarySources;
183
191
  const runtimeOptions = {
184
192
  ...runtimeOptionsFromCli(options),
193
+ params,
185
194
  ...librarySources ? { librarySources } : {},
186
195
  stdoutIsMachineContract: machine,
187
196
  onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
@@ -372,19 +381,22 @@ function formatPreviewLine(line, out) {
372
381
  const rest = match[2];
373
382
  return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
374
383
  }
375
- function recipeUsesCoreController(recipe, librarySources) {
384
+ async function recipeUsesCoreController(recipe, librarySources) {
376
385
  if (objectUsesCoreController(recipe)) return true;
377
386
  const refs = collectCallRefs(recipe);
378
387
  if (refs.length === 0) return false;
379
- const flows = loadFlowCatalogs(librarySources ?? []);
388
+ const sources = librarySources ?? [];
389
+ if (sources.length === 0) return false;
390
+ const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
391
+ const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
380
392
  const visited = /* @__PURE__ */ new Set();
381
393
  const visit = (ref) => {
382
394
  if (visited.has(ref)) return false;
383
395
  visited.add(ref);
384
- const flow = flows.get(ref);
385
- if (!flow) return false;
386
- if (objectUsesCoreController(flow)) return true;
387
- return collectCallRefs(flow).some(visit);
396
+ const dependency = recipes.get(ref);
397
+ if (!dependency) return false;
398
+ if (objectUsesCoreController(dependency.document)) return true;
399
+ return collectCallRefs(dependency.document).some(visit);
388
400
  };
389
401
  return refs.some(visit);
390
402
  }
@@ -408,41 +420,16 @@ function collectCallRefs(value) {
408
420
  visit(value);
409
421
  return [...new Set(refs)];
410
422
  }
411
- function loadFlowCatalogs(librarySources) {
412
- const flows = /* @__PURE__ */ new Map();
413
- for (const source of librarySources) {
414
- const flowsDir = path.join(source.root, "flows");
415
- let entries;
416
- try {
417
- entries = fs.readdirSync(flowsDir);
418
- } catch {
419
- continue;
420
- }
421
- for (const entry of entries) {
422
- if (!entry.endsWith(".json")) continue;
423
- try {
424
- const parsed = JSON.parse(fs.readFileSync(path.join(flowsDir, entry), "utf8"));
425
- if (!isRecord(parsed) || !isRecord(parsed.flows)) continue;
426
- for (const [id, flow] of Object.entries(parsed.flows)) {
427
- if (!flows.has(id)) flows.set(id, flow);
428
- }
429
- } catch {
430
- continue;
431
- }
432
- }
433
- }
434
- return flows;
435
- }
436
423
  function actionUsesCoreController(action) {
437
424
  return typeof action === "string" && action.startsWith("metamask.perps.");
438
425
  }
439
- async function handleRunPlan(recipeArg, options, stream) {
426
+ async function handleRunPlan(recipeArg, params, options, stream) {
440
427
  const json = optionFlag(options, "json");
441
428
  const jsonOutput = json && !stream.enabled;
442
429
  const { adapter, target } = resolveAdapter(options);
443
430
  stream.phase("resolve", { adapter, target, recipe: recipeArg });
444
431
  stream.phase("validate");
445
- const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
432
+ const validated = await validateRunRecipeStatic(recipeArg, adapter, options, params);
446
433
  if (validated.usageError) {
447
434
  const userAction = runUsageRecovery(
448
435
  validated.usageError.code,
@@ -460,7 +447,7 @@ async function handleRunPlan(recipeArg, options, stream) {
460
447
  userAction
461
448
  );
462
449
  }
463
- const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
450
+ const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid, effectiveParams } = validated;
464
451
  const status = errorCount === 0 ? "pass" : "fail";
465
452
  const nodeCount = countRecipeNodes(recipe);
466
453
  const artifactsDir = optionString(options, "artifactsDir");
@@ -519,6 +506,7 @@ async function handleRunPlan(recipeArg, options, stream) {
519
506
  status,
520
507
  adapter,
521
508
  recipe: recipeFile,
509
+ params: redactStructuredValue(effectiveParams),
522
510
  findings,
523
511
  plan
524
512
  };
@@ -53,7 +53,7 @@ function flag(options, key) {
53
53
  return options[key] === true;
54
54
  }
55
55
  function targetOf(options) {
56
- return path.resolve(str(options, "target") ?? str(options, "projectRoot") ?? process.cwd());
56
+ return path.resolve(str(options, "target") ?? process.cwd());
57
57
  }
58
58
  const ADAPTER_TOKENS = ["mobile", "extension", "core"];
59
59
  function resolveAdapter(options, target, hint) {
@@ -7,6 +7,10 @@ import { EXIT } from "./commands/shared.js";
7
7
  function newHealState() {
8
8
  return { recovered: [], mutations: [], attemptedRecoveries: [] };
9
9
  }
10
+ function conciseFailureForHuman(output) {
11
+ const errors = output.split(/\r?\n/u).map((line) => /^(?:[A-Za-z]*Error):\s*(.+)$/u.exec(line.trim())?.[1]).filter((line) => Boolean(line));
12
+ return errors.at(-1) ?? output.trim();
13
+ }
10
14
  function parseHeal(options, fallback) {
11
15
  const value = options.heal;
12
16
  if (value === void 0) return fallback;
@@ -113,6 +117,7 @@ export {
113
117
  RECOVERY_CODE,
114
118
  checkHealBounds,
115
119
  classifyFailure,
120
+ conciseFailureForHuman,
116
121
  ensureOverlay,
117
122
  newHealState,
118
123
  parseHeal,
@@ -150,54 +150,45 @@ Example (real actions; run mm-harness actions for this checkout's full set):
150
150
  mm-harness call navigate page=perps --adapter mobile
151
151
  mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
152
152
  },
153
- {
154
- name: "flows",
155
- summary: "Browse reusable parameterized action sequences.",
156
- example: "mm-harness flows",
157
- helpText: `mm-harness flows [list|describe <ref>|promote] [flags]
158
-
159
- Browse reusable parameterized action sequences. Flows resolve by precedence
160
- (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
161
- Flow resolution is adapter-global, so there is no --adapter flag here.
162
-
163
- --library <name=path> Add/override a library source (repeatable)
164
- --target <path> Checkout path (default: cwd)
165
- --json Machine-readable output
166
-
167
- Example:
168
- mm-harness flows
169
- mm-harness flows describe perps.clean_market_testnet
170
- mm-harness flows describe perps.clean_market_testnet --json`
171
- },
172
153
  {
173
154
  name: "run",
174
155
  summary: "Execute a complete proof and write its evidence.",
175
156
  example: "mm-harness run recipe.json",
176
- helpText: `mm-harness run <recipe.json> [flags]
157
+ helpText: `mm-harness run <recipe|recipe.json> [key=value ...] [flags]
177
158
 
178
159
  Validate and execute a complete proof, then write summary, trace, diagnostics,
179
- and artifacts. A recipe may use actions directly or call reusable flows.
160
+ and artifacts. Inside a checkout, the adapter, target, ports, and artifact
161
+ directory are normally detected automatically.
180
162
 
163
+ Discovery:
181
164
  --list List complete recipes accepted by run for this adapter; no <recipe> needed
182
- --describe Show one recipe's metadata, declared inputs, composed actions/flows, and next command
165
+ --describe Show one recipe's parameters, composed actions/recipes, and runnable command
183
166
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
167
+
168
+ Common overrides:
184
169
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
185
- --cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
186
- --launch-existing-dist Extension: reuse/launch the existing dist without rebuilding
187
170
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
171
+ --platform <ios|android> Mobile platform preference; use --adapter for the product adapter
188
172
  --target <path> Checkout path (default: cwd)
189
173
  --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
190
- --action-manifest <path> Override the action manifest
191
174
  --library <name=path> Add/override a recipe-library source (repeatable)
192
175
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
193
176
  --json Machine-readable output
194
177
  --json-stream Line-flushed JSONL progress + terminal event
195
178
  --record-video=full-run Record a video of the run
196
179
 
180
+ Advanced integrations:
181
+ runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
182
+ --validation-runtime-dir, --launch-existing-dist
183
+ managed trust boundary (not needed for normal runs; Farmslot supplies it):
184
+ --source-trust, --source-kind, --source-name, --source-digest,
185
+ --approve-plan (binds approval to the exact reviewed plan)
186
+ schema: --action-manifest
187
+
197
188
  Example:
198
189
  mm-harness run wallet.smoke --describe
199
- mm-harness run recipe.json --plan --adapter mobile
200
- mm-harness run recipe.json --adapter extension`
190
+ mm-harness run perps.clean-market-testnet market=BTC --plan
191
+ mm-harness run recipe.json market=ETH --adapter extension`
201
192
  },
202
193
  {
203
194
  name: "last",
@@ -486,9 +477,10 @@ Example:
486
477
  name: "fixtures",
487
478
  summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
488
479
  example: "mm-harness fixtures set",
489
- helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
480
+ helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
490
481
 
491
482
  Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
483
+ With no action, show the current fixture status and safe next command.
492
484
  init Create it from --from <path>, or explicitly choose --dev for a
493
485
  disposable public test wallet that must never hold real funds.
494
486
  sync Refresh the wallet fixture files on the target.
@@ -556,8 +548,8 @@ const HELP_GROUPS = [
556
548
  },
557
549
  {
558
550
  title: "DISCOVER",
559
- blurb: "compose recipes from the action vocabulary + flow library (--json is the agent-primary form)",
560
- commands: ["actions", "call", "flows"]
551
+ blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
552
+ commands: ["actions", "call"]
561
553
  },
562
554
  {
563
555
  title: "PROVE",
@@ -180,7 +180,7 @@ function runMobileIssueCommand(projectRoot, command) {
180
180
  }
181
181
  }
182
182
  function redactPreview(value) {
183
- return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
183
+ return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(api[-_]?key|password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization|vault)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
184
184
  }
185
185
  function dedupeFindings(findings) {
186
186
  const byFingerprint = /* @__PURE__ */ new Map();
@@ -10,7 +10,7 @@ async function startRecipeRecording(adapter, projectRoot, artifactsDir, options)
10
10
  if (!options.record) return void 0;
11
11
  if (process.platform !== "darwin") {
12
12
  console.error(
13
- "WARN: --record uses capture-helper and is currently supported only on macOS; continuing without video."
13
+ "WARN: --record-video uses capture-helper and is currently supported only on macOS; continuing without video."
14
14
  );
15
15
  return void 0;
16
16
  }
package/dist/runner.js CHANGED
@@ -69,7 +69,10 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
69
69
  (entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
70
70
  );
71
71
  const autoHudDisabled = options.autoHud === false || process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
72
- const logger = options.quietStdout ? new console.Console(process.stderr) : console;
72
+ const logger = deduplicateLibraryResolutionLogs(
73
+ options.quietStdout ? new console.Console(process.stderr) : console,
74
+ options.suppressLibraryResolutionLogs
75
+ );
73
76
  return createRecipeRunner({
74
77
  actionManifest,
75
78
  adapters: withActionProgress(
@@ -101,6 +104,25 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
101
104
  }
102
105
  });
103
106
  }
107
+ function deduplicateLibraryResolutionLogs(logger, suppress = false) {
108
+ const seen = /* @__PURE__ */ new Set();
109
+ return {
110
+ info(message) {
111
+ if (message.startsWith("Recipe libraries:")) {
112
+ if (suppress) return;
113
+ if (seen.has(message)) return;
114
+ seen.add(message);
115
+ }
116
+ logger.info(message);
117
+ },
118
+ warn(message) {
119
+ logger.warn(message);
120
+ },
121
+ error(message) {
122
+ logger.error(message);
123
+ }
124
+ };
125
+ }
104
126
  async function prepareLiveAdapterImplementations(platform, actions, sources, trustTaskActions = false) {
105
127
  const prepared = /* @__PURE__ */ new Map();
106
128
  if (!sources) return prepared;
@@ -165,7 +187,7 @@ function withActionProgress(adapters, onActionEvent) {
165
187
  try {
166
188
  const result = await entry.execute(node, context);
167
189
  if (reportProgress) {
168
- onActionEvent({ ...event, status: result.status === "fail" ? "failed" : "passed" });
190
+ onActionEvent({ ...event, status: "passed" });
169
191
  }
170
192
  return result;
171
193
  } catch (error) {
@@ -11,7 +11,7 @@ bin/mm-harness
11
11
  -> @farmslot/recipe-harness generic execution, UI transports, evidence
12
12
  -> @farmslot/protocol schemas
13
13
  -> adapters/ focused host/browser/device leaves
14
- -> library/ MetaMask actions, flows, recipes
14
+ -> library/ MetaMask actions and recipes
15
15
  -> MetaMask checkout product under test
16
16
  ```
17
17
 
@@ -37,8 +37,7 @@ task-local.
37
37
  | `adapters/<platform>/` | focused shell/Node host operations |
38
38
  | `adapters/<platform>/inject*` | assemble the checkout overlay from shipped adapter/library files |
39
39
  | `library/actions/` | executable MetaMask action adapters |
40
- | `library/flows/` | reusable parameterized behavior |
41
- | `library/recipes/` | reusable proofs |
40
+ | `library/recipes/` | reusable parameterized behavior and proofs |
42
41
  | `library/manifests/` | declared capability surface |
43
42
  | `scripts/` | build/validation tooling and shipped completion/human-QA helpers |
44
43
 
package/docs/QA.md CHANGED
@@ -29,7 +29,6 @@ mm-harness doctor
29
29
  mm-harness doctor --json
30
30
  mm-harness status
31
31
  mm-harness actions
32
- mm-harness flows
33
32
  mm-harness run --list
34
33
  mm-harness run runner.smoke --describe
35
34
  mm-harness run runner.smoke --plan
package/docs/RECIPES.md CHANGED
@@ -2,98 +2,84 @@
2
2
 
3
3
  Use the smallest durable layer that proves the task:
4
4
 
5
- 1. bundled actions;
6
- 2. a bundled or team flow;
7
- 3. a task-local recipe;
8
- 4. a trusted task-local action or direct controller/CDP operation when no
9
- stable action exists.
5
+ 1. call an existing action;
6
+ 2. compose existing recipes;
7
+ 3. write a task-local recipe;
8
+ 4. use a verified controller or CDP path when no stable action exists.
10
9
 
11
- Search before writing code:
10
+ Discover before authoring:
12
11
 
13
12
  ```bash
14
13
  mm-harness actions <intent>
15
14
  mm-harness actions --action <name>
16
- mm-harness flows
17
- mm-harness flows describe <ref> --json
18
15
  mm-harness run --list
19
16
  mm-harness run <name> --describe
20
17
  ```
21
18
 
22
- Discovery reports the winning source, fields, examples, and shadowed sources.
23
- Use `--json` when an agent needs exact metadata.
19
+ Discovery reports parameters, selected adapter variant, nested recipes, source,
20
+ and shadows. Use `--json` for exact metadata.
24
21
 
25
- ## Compose a proof
22
+ ## Author and run
26
23
 
27
- An action performs one operation. A flow packages reusable product setup or
28
- behavior. A recipe owns the task's acceptance criteria and evidence.
29
-
30
- Minimal recipe:
24
+ An action performs one typed operation. A recipe owns a reusable graph or a
25
+ task proof and may call other recipes.
31
26
 
32
27
  ```json
33
28
  {
34
- "schema_version": 1,
35
- "title": "Wallet readiness proof",
29
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
30
+ "title": "Wallet readiness",
36
31
  "description": "Proves the running wallet is reachable and unlocked.",
37
- "validate": {
38
- "workflow": {
39
- "entry": "unlock",
40
- "nodes": {
41
- "unlock": {
42
- "action": "metamask.wallet.ensure_unlocked",
43
- "intent": "Ensure the fixture-backed wallet is unlocked",
44
- "next": "done"
45
- },
46
- "done": { "action": "end", "status": "pass" }
47
- }
32
+ "paramsSchema": {
33
+ "type": "object",
34
+ "properties": {
35
+ "account": { "type": "string", "default": "Account 1" }
36
+ },
37
+ "additionalProperties": false
38
+ },
39
+ "workflow": {
40
+ "entry": "unlock",
41
+ "nodes": {
42
+ "unlock": {
43
+ "action": "metamask.wallet.ensure_unlocked",
44
+ "intent": "Make the fixture-backed wallet ready for use",
45
+ "next": "done"
46
+ },
47
+ "done": { "action": "end", "status": "pass" }
48
48
  }
49
49
  }
50
50
  }
51
51
  ```
52
52
 
53
- Validate without side effects, then run:
53
+ Defaults are applied before validation. Root values use `key=value`; a nested
54
+ recipe call uses `action: "call"`, `ref`, and `params`.
54
55
 
55
56
  ```bash
56
- mm-harness run recipe.json --plan
57
- mm-harness run recipe.json
58
- ```
59
-
60
- To reuse an existing Extension build on a specific CDP runtime:
61
-
62
- ```bash
63
- mm-harness run recipe.json --cdp-port 9500 --launch-existing-dist
57
+ mm-harness run recipe.json account="Account 2" --plan
58
+ mm-harness run recipe.json account="Account 2"
64
59
  ```
65
60
 
66
61
  Rules:
67
62
 
68
- - Use real product paths; never mutate DOM, Redux, controller, or persisted
69
- state to fabricate proof.
70
- - Setup may use supported APIs, but a read/assert or visible UI postcondition
71
- must prove the result.
72
- - Use `ui.*` for human-visible behavior. Keep ticket copy, styling, and layout
73
- claims in the task recipe and screenshots.
74
- - Parameterize repeated behavior instead of multiplying action names.
75
- - Every `ensure_*` flow must prove a postcondition.
63
+ - Validate with `--plan` before side effects.
64
+ - Use real product paths; never mutate hidden state to fabricate proof.
65
+ - Prove preparation with an independent read/assert or visible UI postcondition.
66
+ - Keep ticket-specific claims in task-local recipes.
67
+ - Parameterize repeated behavior instead of multiplying names.
76
68
  - Keep secrets out of recipes, libraries, and evidence.
77
69
 
78
- The Recipe Protocol v1 schema is authoritative:
70
+ The protocol is authoritative:
79
71
  <https://farmslot.io/docs/reference/recipe-protocol-v1>.
80
72
 
81
- ## Share reusable behavior
82
-
83
- A library may contain manifests, action implementations, flows, and complete
84
- recipes:
73
+ ## Share a library
85
74
 
86
75
  ```text
87
76
  team-recipes/
88
77
  library.json
89
78
  manifests/extension.action-manifest.json
90
79
  actions/extension/wallet/ensure_ready.mjs
91
- flows/wallet.flows.json
92
80
  recipes/onboarding/smoke.extension.recipe.json
93
81
  ```
94
82
 
95
- Minimal `library.json`:
96
-
97
83
  ```json
98
84
  {
99
85
  "kind": "recipe-library",
@@ -103,59 +89,34 @@ Minimal `library.json`:
103
89
  }
104
90
  ```
105
91
 
106
- Enable it:
107
-
108
92
  ```bash
109
93
  export RECIPE_LIBRARY_PATH="wallet=$HOME/shared-library/wallet-team"
110
- mm-harness flows
111
94
  mm-harness run --list
112
95
  mm-harness run onboarding.smoke --describe
113
96
  mm-harness run onboarding.smoke --plan
114
97
  ```
115
98
 
116
- Use `--library wallet=/path/to/library` for one command. Without an explicit
117
- library, the personal library is `$FARMSLOT_HOME/recipe-library` (default
118
- `~/.farmslot/recipe-library`). Resolution is task/local configuration first,
119
- then bundled MetaMask; runs snapshot resolved definitions so shadowing is
120
- reviewable.
121
-
122
- Promote a repeated flow only after it has a useful description, typed params,
123
- and successful proof:
124
-
125
- ```bash
126
- mm-harness flows promote --from recipe.json --flow wallet.ensure_ready --run artifacts
127
- ```
99
+ Use `--library wallet=/path/to/library` for one command. Resolution follows the
100
+ explicit library order, then bundled MetaMask. Adapter-specific variants are
101
+ selected deterministically. Every run records the root recipe, exact resolved
102
+ dependency documents, their digests, call edges, selected sources, and shadows.
128
103
 
129
104
  ## Direct product access
130
105
 
131
- The bundled catalog is intentionally small. A task may use a real controller or
132
- CDP path when that is clearer than adding a permanent wrapper.
133
-
134
- Before doing so, validate the path in current product source and record:
135
-
136
- - the controller/API and supported parameters;
137
- - whether it reads or mutates state;
138
- - required preconditions;
139
- - the independent postcondition;
140
- - trace redaction.
141
-
142
- Do not infer route or controller names from memory. A direct mutation still
143
- requires a separate read/assert or visible UI proof. If the operation repeats
144
- across tasks, move it to a reviewed team library; promote it into the bundled
145
- catalog only when it is broadly reusable and stable across supported products.
146
-
147
- ## Cross-platform expectations
148
-
149
- Mobile and Extension use the same wallet and Perps vocabulary where both expose
150
- the behavior. Core is headless: it reports fixture-backed controller inputs and
151
- does not pretend to have a selected UI account or app route.
106
+ The bundled library is intentionally small. A task may use a verified controller
107
+ or CDP path when that is clearer than adding a permanent wrapper. Confirm the
108
+ current source path, mutation risk, preconditions, independent postcondition,
109
+ and redaction. Do not infer route or controller names from memory. Repeated team
110
+ behavior belongs in a reviewed team recipe; only broadly reusable stable
111
+ capabilities belong in the bundled library.
152
112
 
153
- Inspect the current capability instead of assuming parity:
113
+ Mobile and Extension share vocabulary where the product supports it. Core is
114
+ headless and must not fabricate UI state. Inspect the current surface:
154
115
 
155
116
  ```bash
156
117
  mm-harness actions --category wallet --json
157
118
  mm-harness actions --category perps --json
158
- mm-harness actions --action metamask.wallet.list_accounts --json
119
+ mm-harness run --list --json
159
120
  ```
160
121
 
161
122
  See [Security](SECURITY.md) before enabling custom executable actions.
package/library/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Recipe library
2
2
 
3
3
  - An **action** performs one typed operation.
4
- - A **flow** packages a reusable parameterized sequence of actions.
5
- - A **recipe** is a complete proof that may call actions and flows.
4
+ - A **recipe** is a parameterized, composable graph that may call actions and
5
+ other recipes.
6
6
 
7
7
  - `actions/<adapter>/<group>/<name>.mjs` — executable action modules.
8
8
  **Importing an action module RUNS it**: each ends with `runAdapter(main)` at top
@@ -13,6 +13,6 @@
13
13
  - Actions load harness helpers ONLY through `actions/harness-exports.mjs` (the
14
14
  dist-preferring bridge) — never deep-import `src/` (published installs have no
15
15
  src/, and the entrypoint would close an import cycle through adapters.ts).
16
- - `recipes/` — runnable smoke/validation recipes (`mm-harness run <file>`).
17
- - Flow resolution order is personal > team > canonical; shadowing is recorded in
18
- summary.json (never silent).
16
+ - `recipes/` — runnable recipes (`mm-harness run <name-or-file>`).
17
+ - Recipe resolution is explicit library order, then bundled MetaMask; selected
18
+ variants and shadows are recorded in run evidence.