@deeeed/metamask-harness 0.68.0 → 0.69.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.69.0 - 2026-09-30
6
+
7
+ ### Changed
8
+
9
+ - Group and color recipe/action listings by domain, show library origins and paths separately, and support domain/source filters and domain/name/library sorting. Keep recipe parameter details in `--describe` and `--json`.
10
+
11
+ ### Fixed
12
+
13
+ - Accept repeated `task init --acceptance` flags at the public CLI boundary so Recipe Cook can record authored criteria in the task document and structured handoff.
14
+ - Report Mobile launch dependency blockers once with the dependency repair command; preserve the readiness cause instead of labelling it app logic or adding a conflicting doctor hint.
15
+
5
16
  ## 0.68.0 - 2026-09-29
6
17
 
7
18
  ### Changed
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readiness";
4
4
  import { resolveAndExportMobileToolPaths } from "../../../library/actions/mobile/platform/tool-paths.mjs";
5
5
  import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
6
+ import { shellQuote } from "../../commands/parse-args.js";
6
7
  import { runnerDir } from "../../paths.js";
7
8
  import {
8
9
  decideMobileReadiness,
@@ -51,12 +52,14 @@ async function prepareMobile(target, opts = {}) {
51
52
  });
52
53
  if (report.decision === "blocked") {
53
54
  const reasons = report.reasons.join(" ");
54
- const next = report.userAction ?? "fix the bundle error in app code before retrying recipe up.";
55
+ const next = report.userAction ?? `mm-harness logs --target ${shellQuote(target)} --full`;
55
56
  const msg = `mobile prepare blocked: ${reasons}
56
57
  Next: ${next}`;
57
- if (!json) process.stderr.write(`${msg}
58
- `);
59
- return { status: EXIT.runtime, output: msg };
58
+ return {
59
+ status: EXIT.runtime,
60
+ output: msg,
61
+ readinessBlock: { reasonCode: report.reasonCode, message: reasons, userAction: next }
62
+ };
60
63
  }
61
64
  const iosAccessibilityChanged = platform === "ios" && report.decision !== "unknown" && enableIosAccessibility(json);
62
65
  const rebuildNative = preflightMode === "rebuild-native" || preflightMode === "clean";
@@ -12,6 +12,7 @@ import {
12
12
  } from "@farmslot/recipe-harness/runtime/log-analysis";
13
13
  import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
14
14
  import { color } from "../../cli-color.js";
15
+ import { shellQuote } from "../../commands/parse-args.js";
15
16
  import { recipeRuntimePath } from "../../paths.js";
16
17
  import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
17
18
  import { mobileProductMarkers } from "./deps-markers.js";
@@ -149,7 +150,7 @@ const launchActions = (target, clearMetro = false) => {
149
150
  return actions;
150
151
  };
151
152
  function depsInstallCommand(resolved) {
152
- return fs.existsSync(path.join(resolved, "node_modules")) ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
153
+ return fs.existsSync(path.join(resolved, "node_modules")) ? "yarn setup:expo --no-build-ios --no-build-android" : "yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android";
153
154
  }
154
155
  async function decideMobileReadiness(target, options = {}) {
155
156
  const resolved = path.resolve(target);
@@ -157,18 +158,15 @@ async function decideMobileReadiness(target, options = {}) {
157
158
  const fast = options.preflightMode === "fast";
158
159
  const report = await computeMobileReadiness(resolved, options, fast);
159
160
  if (fast && report.decision === "install") {
160
- const standalone = depsInstallCommand(resolved);
161
161
  return {
162
162
  ...report,
163
163
  decision: "blocked",
164
164
  reasonCode: "deps-not-ready",
165
165
  reasons: [
166
- "Fast preflight found dependencies not ready and does not install them (the orchestrator deps phase owns installation).",
167
- ...report.reasons
166
+ ...report.reasons,
167
+ "Quick launch does not install dependencies."
168
168
  ],
169
- userAction: `1. Standalone checkout \u2014 run ${standalone} in the checkout.
170
- 2. Orchestrated slot \u2014 run its deps/prepare phase.
171
- After the selected action succeeds, re-run \`mm-harness launch <platform>\`.`,
169
+ userAction: `cd ${shellQuote(resolved)} && ${depsInstallCommand(resolved)}`,
172
170
  actions: []
173
171
  };
174
172
  }
@@ -189,9 +187,9 @@ async function decideMobileReadiness(target, options = {}) {
189
187
  )
190
188
  };
191
189
  }
192
- function writeDepsNote(resolved) {
190
+ function writeDepsNote() {
193
191
  const stream = process.stderr;
194
- const body = `package.json/yarn.lock are newer than the node_modules install markers and no install baseline is recorded yet, so the installed node_modules are used as they are. Pulling commits refreshes those timestamps without changing dependencies, and a required-dependency presence check still runs, so this on its own is not evidence that anything drifted. Act only if you know dependencies changed: run ${depsInstallCommand(resolved)} once \u2014 that refreshes the markers, and \`mm-harness prepare\` records a baseline when it runs the install, so this note stops.`;
192
+ const body = "No install baseline is recorded. Required package checks passed; timestamps alone do not establish dependency drift. Reusing the installed dependencies.";
195
193
  stream.write(
196
194
  `
197
195
  ${color("warn", "mm-harness note (deps):", { stream })} ${color("dim", body, { stream })}
@@ -206,7 +204,6 @@ async function computeMobileReadiness(resolved, options, fast) {
206
204
  let deps = rawDeps;
207
205
  if (rawDeps.status === "stale" && !rawDeps.hasBaseline) {
208
206
  deps = { installed: rawDeps.installed, status: "current", hasBaseline: false };
209
- writeDepsNote(resolved);
210
207
  }
211
208
  if (deps.status === "current") {
212
209
  const absent = missingRequiredDeps(resolved);
@@ -325,6 +322,7 @@ async function computeMobileReadiness(resolved, options, fast) {
325
322
  actions: installActions(resolved)
326
323
  };
327
324
  }
325
+ if (rawDeps.status === "stale" && !rawDeps.hasBaseline) writeDepsNote();
328
326
  if (metro.status === "down") {
329
327
  return {
330
328
  schemaVersion: 1,
@@ -168,6 +168,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
168
168
  "--title": value(),
169
169
  "--task-text": value(),
170
170
  "--task-file": value(),
171
+ "--acceptance": value(),
171
172
  "--ticket": value(),
172
173
  "--source-ref": value(),
173
174
  "--surface": value(),
@@ -241,6 +242,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
241
242
  call: {
242
243
  options: options(HELP, JSON, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
243
244
  "--list": bool(),
245
+ "--domain": value(),
246
+ "--source": value(),
247
+ "--sort": value(["domain", "name", "library"]),
244
248
  "--arg": value()
245
249
  }),
246
250
  positionals: [{ label: "action" }],
@@ -253,6 +257,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
253
257
  run: {
254
258
  options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, RUN_FUNDING, {
255
259
  "--list": bool(),
260
+ "--domain": value(),
261
+ "--source": value(),
262
+ "--sort": value(["domain", "name", "library"]),
256
263
  "--plan": bool(),
257
264
  "--proof": bool(),
258
265
  "--describe": bool(),
@@ -65,6 +65,9 @@ async function handleCall(argv) {
65
65
  }
66
66
  const { action: shortName, args, rest } = parseCallArgs(argv);
67
67
  const { options } = parseArgs(rest, "call");
68
+ if (["domain", "source", "sort"].some((name) => optionString(options, name) !== void 0)) {
69
+ throw usageError("--domain, --source, and --sort require call --list.");
70
+ }
68
71
  const json = optionFlag(options, "json");
69
72
  if (!shortName) {
70
73
  let example = "mm-harness call <action>";
@@ -320,6 +320,10 @@ async function handleLaunchLocked(argv, stream) {
320
320
  if (attempt.status === 0) {
321
321
  return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
322
322
  }
323
+ const readinessFailure = mobileReadinessFailure(attempt);
324
+ if (readinessFailure) {
325
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, readinessFailure);
326
+ }
323
327
  if (adapter === "extension" && attempt.output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
324
328
  return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
325
329
  code: "REMOTE_FLAG_NOT_PINNABLE",
@@ -503,6 +507,10 @@ async function handleLaunchLocked(argv, stream) {
503
507
  state.recovered.push(recoveryCode);
504
508
  return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
505
509
  }
510
+ const retryReadinessFailure = mobileReadinessFailure(attempt);
511
+ if (retryReadinessFailure) {
512
+ return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, retryReadinessFailure);
513
+ }
506
514
  return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
507
515
  code: "SAME_RECOVERY_TWICE",
508
516
  message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
@@ -511,6 +519,17 @@ async function handleLaunchLocked(argv, stream) {
511
519
  originalError: attempt.output.trim() || void 0
512
520
  });
513
521
  }
522
+ function mobileReadinessFailure(result) {
523
+ const block = result.readinessBlock;
524
+ if (!block) return void 0;
525
+ return {
526
+ code: block.reasonCode === "deps-not-ready" ? "MOBILE_DEPENDENCIES_NOT_READY" : "MOBILE_READINESS_BLOCKED",
527
+ message: block.message,
528
+ userAction: block.userAction,
529
+ recoverable: false,
530
+ exitCode: result.status
531
+ };
532
+ }
514
533
  function extensionQuickReattachFailed(output) {
515
534
  return /\[reattach\]|extension quick reattach|mm-harness launch --build/u.test(output);
516
535
  }
@@ -5,13 +5,14 @@ import { importRecipeProtocol } from "../paths.js";
5
5
  import { color } from "../cli-color.js";
6
6
  import { EXIT } from "./shared.js";
7
7
  import { optionFlag, optionString, optionStrings, resolveAdapter, shellQuoteArg } from "./parse-args.js";
8
- import { describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
8
+ import { actionDomain, describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
9
9
  import {
10
10
  describeRunnableRecipe,
11
11
  listRunnableRecipes,
12
12
  resolveMetaMaskLibrarySources
13
13
  } from "./run-engine.js";
14
14
  const finalSegment = (name) => name.split(".").pop() ?? name;
15
+ const recipeDomain = (name) => name.includes(".") ? name.split(".")[0].replaceAll("_", "-") : "general";
15
16
  function catalogDescription(description) {
16
17
  if (!description) return "";
17
18
  const firstSentence = description.match(/^.*?[.!?](?:\s|$)/u)?.[0]?.trim() ?? description;
@@ -20,23 +21,64 @@ function catalogDescription(description) {
20
21
  async function handleListExecutables(command, options) {
21
22
  const json = optionFlag(options, "json");
22
23
  const { adapter } = resolveAdapter(options);
24
+ const domainFilter = optionString(options, "domain")?.replaceAll("_", "-");
25
+ const sourceFilter = optionString(options, "source");
26
+ const sort = optionString(options, "sort") ?? (json ? "name" : "domain");
27
+ const groupKey = (entry, domain) => sort === "library" ? entry.source : sort === "domain" ? domain : "";
28
+ const select = (available2, domainOf) => available2.filter((entry) => (!domainFilter || domainOf(entry) === domainFilter) && (!sourceFilter || entry.source === sourceFilter)).sort((left, right) => groupKey(left, domainOf(left)).localeCompare(groupKey(right, domainOf(right))) || left.name.localeCompare(right.name));
29
+ const out = (style, text) => color(style, text, { stream: process.stdout });
30
+ const stylesFor = (domains) => {
31
+ const palette = ["label", "accent", "ok", "warn"];
32
+ return new Map([...new Set(domains)].sort().map((domain, index) => [domain, palette[index % palette.length]]));
33
+ };
23
34
  const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
35
+ const libraryLines = () => {
36
+ const lines = ["Libraries loaded:"];
37
+ for (const source of librarySources ?? []) {
38
+ const name = source.name ?? path.basename(source.root);
39
+ const kind = source.provenance?.kind === "bundled" ? "bundled" : "linked";
40
+ lines.push(` ${out("bold", name)} ${kind} \xB7 ${source.root}`);
41
+ }
42
+ if (domainFilter || sourceFilter) {
43
+ lines.push(`Filter: ${[domainFilter && `domain=${domainFilter}`, sourceFilter && `source=${sourceFilter}`].filter(Boolean).join(" ")}`);
44
+ }
45
+ return lines;
46
+ };
24
47
  if (command === "run") {
25
- const recipes = await listRunnableRecipes(adapter, librarySources);
48
+ const available2 = await listRunnableRecipes(adapter, librarySources);
49
+ const recipes = select(available2, (recipe) => recipeDomain(recipe.name));
26
50
  if (json) {
27
51
  console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, recipes }, null, 2));
28
52
  return EXIT.ok;
29
53
  }
30
- const out = (style, text) => color(style, text, { stream: process.stdout });
54
+ const domainStyles = stylesFor(available2.map((recipe) => recipeDomain(recipe.name)));
31
55
  console.log(out("bold", `runnable recipes (${adapter})`));
56
+ for (const line of libraryLines()) console.log(line);
32
57
  console.log(out("comment", "Inspect: mm-harness run <recipe> --describe"));
58
+ if (recipes.length === 0) {
59
+ console.log("No runnable recipes match.");
60
+ console.log(`Available domains: ${[...domainStyles.keys()].join(", ") || "none"}`);
61
+ console.log(`Available sources: ${[...new Set(available2.map((recipe) => recipe.source))].join(", ") || "none"}`);
62
+ }
63
+ const groups = /* @__PURE__ */ new Map();
33
64
  for (const recipe of recipes) {
34
- const shadowed = recipe.shadows.length > 0 ? ` shadows=${recipe.shadows.join(",")}` : "";
35
- const variant = recipe.variant ? ` variant=${recipe.variant}` : " variant=all";
36
- const parameters = recipe.parameters.length > 0 ? ` params=${recipe.parameters.map((parameter) => parameter.name).join(",")}` : "";
37
- const summary = catalogDescription(recipe.description);
38
- const description = summary ? ` \u2014 ${summary}` : "";
39
- console.log(` ${out("cmd", recipe.name)} ${out("dim", `[${recipe.source}]${variant}${shadowed}${parameters}`)}${description}`);
65
+ const group = groupKey(recipe, recipeDomain(recipe.name));
66
+ groups.set(group, [...groups.get(group) ?? [], recipe]);
67
+ }
68
+ for (const [group, entries] of groups) {
69
+ if (group) {
70
+ const label = sort === "library" ? `Library: ${group}` : group;
71
+ console.log(`
72
+ ${out(sort === "domain" ? domainStyles.get(group) : "bold", label)} ${out("dim", `(${entries.length})`)}`);
73
+ }
74
+ for (const recipe of entries) {
75
+ const style = domainStyles.get(recipeDomain(recipe.name));
76
+ const shadowed = recipe.shadows.length > 0 ? ` shadows=${recipe.shadows.join(",")}` : "";
77
+ const variant = recipe.variant ? ` variant=${recipe.variant}` : " variant=all";
78
+ const summary = catalogDescription(recipe.description);
79
+ console.log(` ${out(style, recipe.name)} ${out("dim", `library=${recipe.source}${variant}${shadowed}`)}`);
80
+ if (summary) console.log(` ${summary}`);
81
+ }
40
82
  }
41
83
  return EXIT.ok;
42
84
  }
@@ -47,9 +89,10 @@ async function handleListExecutables(command, options) {
47
89
  );
48
90
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
49
91
  const protocolNames = new Set(getRecipeActionManifestActionNames(manifest));
50
- const described = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
92
+ const available = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
93
+ const described = select(available, actionDomain);
51
94
  const shortCounts = /* @__PURE__ */ new Map();
52
- for (const entry of described) {
95
+ for (const entry of available) {
53
96
  const short = finalSegment(entry.name);
54
97
  shortCounts.set(short, (shortCounts.get(short) ?? 0) + 1);
55
98
  }
@@ -73,9 +116,17 @@ async function handleListExecutables(command, options) {
73
116
  console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
74
117
  return EXIT.ok;
75
118
  }
119
+ const contextLines = libraryLines();
120
+ if (described.length === 0) {
121
+ contextLines.push(`Available domains: ${[...new Set(available.map(actionDomain))].sort().join(", ") || "none"}`);
122
+ contextLines.push(`Available sources: ${[...new Set(available.map((entry) => entry.source))].sort().join(", ") || "none"}`);
123
+ }
76
124
  console.log(renderHumanActionCatalog(described, {
77
125
  title: `invocable actions (${adapter})`,
78
126
  guidance: "Use: mm-harness call <name> \xB7 short names shown first where unambiguous",
127
+ sort,
128
+ domainStyles: stylesFor(available.map(actionDomain)),
129
+ contextLines,
79
130
  displayName: (entry) => {
80
131
  const short = shortByName.get(entry.name);
81
132
  return short ? `${short} (${entry.name})` : `${entry.name} (full name only \u2014 ambiguous short)`;
@@ -345,16 +345,21 @@ function summarizeDescribedAction(entry) {
345
345
  const { schema: _schema, examples: _examples, ...summary } = entry;
346
346
  return summary;
347
347
  }
348
+ function actionDomain(entry) {
349
+ return entry.category.replaceAll("_", "-");
350
+ }
348
351
  function renderHumanActionCatalog(actions, options) {
349
352
  const out = (style, text) => color(style, text, { stream: process.stdout });
350
353
  const groups = /* @__PURE__ */ new Map();
351
354
  for (const entry of actions) {
352
- const group = actionDomainGroup(entry);
355
+ const group = options.sort === "name" ? "" : options.sort === "library" ? `Library: ${entry.source}` : options.sort === "domain" ? actionDomain(entry) : actionDomainGroup(entry);
353
356
  groups.set(group, [...groups.get(group) ?? [], entry]);
354
357
  }
355
- const lines = [out("bold", options.title), out("comment", options.guidance)];
358
+ const lines = [out("bold", options.title), ...options.contextLines ?? [], out("comment", options.guidance)];
359
+ if (actions.length === 0) lines.push("No callable actions match.");
356
360
  for (const [group, entries] of groups) {
357
- lines.push("", `${out(group === "official" ? "label" : "accent", group)} ${out("dim", `(${entries.length})`)}`);
361
+ const groupStyle = options.sort === "domain" ? options.domainStyles?.get(group) ?? "accent" : group === "official" ? "label" : "accent";
362
+ if (group) lines.push("", `${out(groupStyle, group)} ${out("dim", `(${entries.length})`)}`);
358
363
  for (const entry of entries) {
359
364
  const fieldNames = humanFieldNames(entry);
360
365
  const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
@@ -362,7 +367,8 @@ function renderHumanActionCatalog(actions, options) {
362
367
  const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
363
368
  const source = ` ${out("dim", `[${entry.source}]`)}`;
364
369
  const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
365
- lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${cases}${risk}${description}`);
370
+ const nameStyle = options.domainStyles?.get(actionDomain(entry)) ?? "cmd";
371
+ lines.push(` ${out(nameStyle, options.displayName?.(entry) ?? entry.name)}${source}${fields}${cases}${risk}${description}`);
366
372
  }
367
373
  }
368
374
  return lines.join("\n");
@@ -585,6 +591,7 @@ function levenshtein(left, right) {
585
591
  }
586
592
  export {
587
593
  ACTION_MATRIX_ADAPTERS,
594
+ actionDomain,
588
595
  actionExampleCommand,
589
596
  actionLibraryContextArgs,
590
597
  describeManifestActions,
@@ -147,6 +147,9 @@ async function handleRun(parsed) {
147
147
  }
148
148
  }
149
149
  async function handleRunInner({ positional, options }, stream) {
150
+ if (!optionFlag(options, "list") && ["domain", "source", "sort"].some((name) => optionString(options, name) !== void 0)) {
151
+ throw usageError("--domain, --source, and --sort require run --list.");
152
+ }
150
153
  if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
151
154
  const message = "--describe cannot be combined with --list or --plan.";
152
155
  const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
@@ -298,6 +298,7 @@ Example:
298
298
  Task:
299
299
  --title <t> Task title (required)
300
300
  --task-text <s> | --task-file <p> Task description, inline or from a file
301
+ --acceptance <criterion> Authored acceptance criterion (repeatable, preserved in order)
301
302
  --ticket <key> Ticket key
302
303
  --source-ref <url> Where the task came from
303
304
 
@@ -430,6 +431,9 @@ Example:
430
431
  mm-harness actions.
431
432
 
432
433
  --list List actions accepted by call for this adapter; no <action> needed
434
+ --domain <name> With --list: filter by action domain, e.g. perps or swap-bridge
435
+ --source <name> With --list: filter by origin, e.g. metamask, perps, or official
436
+ --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
433
437
  --arg k=v Action field value (repeatable; equivalent to key=value shorthand)
434
438
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
435
439
  --cdp-port <port> Extension only: bind the action to this live browser runtime.
@@ -464,6 +468,9 @@ Remote feature flags (mobile applies at runtime; extension pins at launch):
464
468
 
465
469
  Discovery:
466
470
  --list List complete recipes accepted by run for this adapter; no <recipe> needed
471
+ --domain <name> With --list: filter by recipe domain, e.g. perps or swap-bridge
472
+ --source <name> With --list: filter by library origin, e.g. metamask or perps
473
+ --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
467
474
  --describe Show one recipe's parameters, composed actions/recipes, and runnable command
468
475
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
469
476
  --proof Preflight behavioral proof bindings before planning/execution; runtime evidence is still required.
package/docs/RECIPES.md CHANGED
@@ -542,12 +542,19 @@ selected deterministically from `recipes/<adapter>/<domain>/`. Identical
542
542
  cross-adapter graphs live once in `recipes/shared/<domain>/`; an adapter-specific
543
543
  variant takes precedence. Both resolve as `<domain>.<recipe>`, without a platform
544
544
  or `shared` prefix. Keep domains below `core`, `extension`, `mobile`, or `shared`.
545
- Legacy
546
- `*.<adapter>.recipe.json` paths remain readable during migration, but new and
547
- migrated libraries use the adapter-first layout. A file cannot declare its
545
+
546
+ Legacy `*.<adapter>.recipe.json` paths remain readable during migration, but new
547
+ and migrated libraries use the adapter-first layout. A file cannot declare its
548
548
  adapter through both forms. Every run records the root recipe, exact resolved
549
549
  dependency documents, their digests, call edges, selected sources, and shadows.
550
550
 
551
+ `mm-harness run --list` and `mm-harness call --list` group recipes/actions by domain
552
+ and show their library origins separately.
553
+ Use `--domain swap-bridge` or `--source perps` to filter, and `--sort name|domain|library`
554
+ to change the order. These options also apply to `--json` output, which keeps name
555
+ ordering unless `--sort` is supplied. Domain filters accept hyphens or underscores,
556
+ so `--domain swap-bridge` includes `metamask.swap_bridge.*` actions.
557
+
551
558
  ## Direct product access
552
559
 
553
560
  The bundled library is intentionally small. A task may use a verified controller
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.68.0",
3
+ "version": "0.69.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -44,8 +44,8 @@ _mmh_flags_for() {
44
44
  execution-template) printf '%s' "--dir --domain-dir --project-worker --project-name --package-templates --package-id --flow --run-mode --platform --domain --id --provenance --include-shadowed --no-include-shadowed --title --force --json" ;;
45
45
  recipe-quality) printf '%s' "--input --output --recipe --task --evidence --params --adapter --library --advisor --advisor-model --advisor-stages --json" ;;
46
46
  actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
47
- call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
48
- run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
47
+ call) printf '%s' "--list --domain --source --sort --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
48
+ run) printf '%s' "--list --domain --source --sort --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
49
49
  doctor) printf '%s' "--adapter --target --fix --json" ;;
50
50
  install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
51
51
  *) printf '%s' "" ;;