@deeeed/metamask-harness 0.16.0 → 0.17.1

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 (119) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +90 -137
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/dist/adapters/extension/console-capture.js +84 -0
  61. package/dist/adapters/extension/runtime-decision.js +14 -2
  62. package/dist/adapters/extension/surface.js +3 -2
  63. package/dist/adapters/mobile/prepare.js +22 -7
  64. package/dist/adapters/mobile/surface.js +1 -1
  65. package/dist/adapters/resolve-slot-ports.js +0 -2
  66. package/dist/adapters/slot-ports.js +8 -0
  67. package/dist/cli.js +5 -11
  68. package/dist/command-contract.js +441 -0
  69. package/dist/command-journal.js +225 -0
  70. package/dist/commands/call.js +43 -20
  71. package/dist/commands/check.js +9 -3
  72. package/dist/commands/debug.js +38 -31
  73. package/dist/commands/device-target.js +27 -12
  74. package/dist/commands/doctor.js +19 -6
  75. package/dist/commands/fixtures.js +19 -11
  76. package/dist/commands/last.js +52 -0
  77. package/dist/commands/launch/index.js +166 -59
  78. package/dist/commands/logs.js +8 -6
  79. package/dist/commands/manifest.js +114 -29
  80. package/dist/commands/parse-args.js +1 -0
  81. package/dist/commands/provision.js +10 -3
  82. package/dist/commands/run-engine.js +27 -42
  83. package/dist/commands/run-report.js +1 -1
  84. package/dist/commands/run.js +243 -41
  85. package/dist/commands/shared.js +11 -1
  86. package/dist/commands/status-probe.js +1 -1
  87. package/dist/commands/status.js +1 -1
  88. package/dist/commands/stop.js +7 -2
  89. package/dist/doctor.js +1 -4
  90. package/dist/harness.js +19 -125
  91. package/dist/json-stream.js +57 -0
  92. package/dist/live-adapter-contract.js +6 -20
  93. package/dist/mm-harness-cli.js +111 -41
  94. package/dist/paths.js +2 -5
  95. package/dist/run-diagnostics.js +35 -40
  96. package/dist/runner.js +32 -1
  97. package/dist/runtime-context.js +26 -8
  98. package/docs/CHEATSHEET.md +39 -52
  99. package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
  100. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
  101. package/docs/CODE-MAP.md +1 -1
  102. package/docs/UX-PRINCIPLES.md +2 -0
  103. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  104. package/docs/architecture.md +65 -375
  105. package/docs/live-adapter-contract.md +3 -1
  106. package/library/actions/core/perps/_controller.mjs +10 -55
  107. package/library/manifests/mobile.action-manifest.json +1 -1
  108. package/package.json +3 -2
  109. package/scripts/completions.sh +0 -0
  110. package/scripts/install-completions.sh +0 -0
  111. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  112. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  113. package/adapters/mobile/overlay/compat/README.md +0 -28
  114. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  115. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  116. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  117. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  118. package/docs/CLI-SPEC.md +0 -1102
  119. package/docs/MENTAL-MODEL.md +0 -295
@@ -19,9 +19,10 @@ async function handleLogs(argv) {
19
19
  const logSources = surface.logSources(target);
20
20
  const appLogSource = surface.appLogSource(target);
21
21
  const sourceLabels = logSources.map((entry) => entry.label);
22
- const defaultSource = sourceLabels[0];
22
+ const appSourceLabel = appLogSource?.label ?? "app";
23
+ const defaultSource = appLogSource && appSourceLabel !== "app" ? appSourceLabel : sourceLabels[0];
23
24
  const source = str(options, "source") ?? defaultSource;
24
- const validSources = [...sourceLabels, ...appLogSource ? ["app"] : []];
25
+ const validSources = [...sourceLabels, ...appLogSource ? [appSourceLabel] : [], ...appLogSource && appSourceLabel !== "app" ? ["app"] : []];
25
26
  if (!validSources.includes(source)) {
26
27
  return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
27
28
  }
@@ -32,11 +33,12 @@ async function handleLogs(argv) {
32
33
  }
33
34
  process.env.RECIPE_LOG_EVENTS = events;
34
35
  }
35
- const requested = source === "app" ? appLogSource : logSources.find((entry) => entry.label === source);
36
- const ordered = source === "app" ? requested ? [requested] : [] : requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
36
+ const isAppSource = source === "app" || source === appSourceLabel;
37
+ const requested = isAppSource ? appLogSource : logSources.find((entry) => entry.label === source);
38
+ const ordered = isAppSource ? requested ? [requested] : [] : requested ? [requested] : logSources;
37
39
  const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
38
40
  if (!logFile) {
39
- if (source === "app" && appLogSource) {
41
+ if (isAppSource && appLogSource) {
40
42
  const message = `application console unavailable for this checkout (${path.basename(appLogSource.path)} is absent).`;
41
43
  const userAction = surface.hints.launch;
42
44
  if (json) {
@@ -55,7 +57,7 @@ async function handleLogs(argv) {
55
57
  }
56
58
  return EXIT.runtime;
57
59
  }
58
- const names = source === "app" && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
60
+ const names = isAppSource && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
59
61
  return usageOut(
60
62
  json,
61
63
  "logs",
@@ -1,37 +1,29 @@
1
1
  import { loadActionManifest, validateManifest } from "../manifest.js";
2
2
  import { EXIT } from "./shared.js";
3
- import {
4
- actionManifestPathOption,
5
- isRecord,
6
- optionFlag,
7
- optionString,
8
- resolveAdapter
9
- } from "./parse-args.js";
10
- async function handleManifest({ options }) {
11
- const { adapter } = resolveAdapter(options);
12
- const actionManifestPath = actionManifestPathOption(options, adapter);
13
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
14
- await validateManifest(manifest);
15
- if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
16
- else console.log(actionManifestPath);
17
- return 0;
18
- }
19
- async function handleActions({ options }) {
3
+ import { isRecord, optionFlag, optionString, resolveAdapter } from "./parse-args.js";
4
+ async function handleActions({ options, positional }) {
20
5
  const { adapter } = resolveAdapter(options);
21
6
  const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
22
7
  await validateManifest(manifest);
8
+ if (optionFlag(options, "raw")) {
9
+ console.log(JSON.stringify(manifest, null, 2));
10
+ return EXIT.ok;
11
+ }
23
12
  const json = optionFlag(options, "json");
24
13
  const action = optionString(options, "action");
14
+ const query = positional[0]?.trim();
25
15
  const category = optionString(options, "category")?.toLowerCase();
26
16
  const categoriesOnly = optionFlag(options, "categories");
27
17
  const all = describeManifestActions(manifest);
28
18
  const categories = summarizeActionCategories(all);
29
19
  if (categoriesOnly && (action || category)) {
30
20
  const message = "--categories cannot be combined with --action or --category.";
21
+ const userAction = `mm-harness actions --adapter ${adapter} --categories`;
31
22
  if (json) {
32
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message } }, null, 2));
23
+ console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message, userAction } }, null, 2));
33
24
  } else {
34
- console.error(`\u2717 mm-harness actions: ${message}`);
25
+ console.error(`\u2717 mm-harness actions: ${message}
26
+ Next: ${userAction}`);
35
27
  }
36
28
  return EXIT.usage;
37
29
  }
@@ -52,10 +44,10 @@ async function handleActions({ options }) {
52
44
  }
53
45
  return EXIT.usage;
54
46
  }
55
- const actions = action ? fuzzyResolveActions(categoryActions, action) : categoryActions;
47
+ const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
56
48
  if (action && actions.length === 0) {
57
49
  const message = `no action matches "${action}" for the ${adapter} adapter.`;
58
- const userAction = `mm-harness actions --adapter ${adapter} # list the vocabulary`;
50
+ const userAction = `mm-harness actions --adapter ${adapter}`;
59
51
  if (json) {
60
52
  console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, category, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
61
53
  } else {
@@ -64,24 +56,70 @@ async function handleActions({ options }) {
64
56
  }
65
57
  return EXIT.usage;
66
58
  }
59
+ if (query && actions.length === 0) {
60
+ const message = `no action matches search "${query}" for the ${adapter} adapter.`;
61
+ const userAction = `mm-harness actions --adapter ${adapter} --categories`;
62
+ if (json) {
63
+ console.log(JSON.stringify({
64
+ schemaVersion: 1,
65
+ command: "actions",
66
+ adapter,
67
+ query,
68
+ category,
69
+ error: { code: "ACTION_SEARCH_EMPTY", message, userAction }
70
+ }, null, 2));
71
+ } else {
72
+ console.error(`\u2717 mm-harness actions: ${message}
73
+ Next: ${userAction}`);
74
+ }
75
+ return EXIT.usage;
76
+ }
77
+ const relatedActions = action && actions.length === 1 ? findRelatedActions(all, actions[0]) : void 0;
67
78
  if (json) {
68
- console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, category, actions }, null, 2));
79
+ console.log(JSON.stringify({
80
+ schemaVersion: 1,
81
+ command: "actions",
82
+ adapter,
83
+ ...query ? { query } : {},
84
+ category,
85
+ actions,
86
+ ...relatedActions ? { relatedActions } : {}
87
+ }, null, 2));
69
88
  } else {
70
89
  for (const entry of actions) {
71
90
  const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
72
91
  console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
73
92
  }
93
+ if (relatedActions) console.log(`Related: ${relatedActions.join(", ")}`);
74
94
  }
75
95
  return 0;
76
96
  }
77
97
  function fuzzyResolveActions(entries, query) {
78
98
  const exactFull = entries.filter((e) => e.name === query);
79
99
  if (exactFull.length > 0) return exactFull;
80
- const finalSegment = (name) => name.split(".").pop() ?? name;
81
- const exactSegment = entries.filter((e) => finalSegment(e.name) === query);
100
+ const finalSegment2 = (name) => name.split(".").pop() ?? name;
101
+ const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
82
102
  if (exactSegment.length > 0) return exactSegment;
83
- return entries.filter((e) => finalSegment(e.name).includes(query));
103
+ return entries.filter((e) => finalSegment2(e.name).includes(query));
84
104
  }
105
+ function searchActions(entries, query) {
106
+ const terms = searchTerms(query);
107
+ if (terms.length === 0) return [];
108
+ return entries.map((entry) => {
109
+ const scores = terms.map((term) => actionSearchScore(entry, term));
110
+ return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
111
+ }).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
112
+ }
113
+ function findRelatedActions(entries, selected, limit = 5) {
114
+ const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
115
+ return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
116
+ const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
117
+ const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
118
+ const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
119
+ return { name: entry.name, score };
120
+ }).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
121
+ }
122
+ const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
85
123
  function summarizeActionCategories(actions) {
86
124
  const counts = /* @__PURE__ */ new Map();
87
125
  for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
@@ -114,22 +152,69 @@ function describeManifestAction(name, kind, metadata) {
114
152
  return {
115
153
  name,
116
154
  kind,
117
- category: actionCategory(name),
155
+ category: actionCategory(name, record.category),
118
156
  description: typeof record.description === "string" ? record.description : "",
119
157
  fields: properties,
120
158
  schema,
121
159
  examples: record.examples
122
160
  };
123
161
  }
124
- function actionCategory(name) {
162
+ function actionCategory(name, configured) {
163
+ if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
125
164
  const segments = name.split(".");
126
165
  if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
127
- return segments[0] || "other";
166
+ if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
167
+ if (segments[0] === "ui") return "ui";
168
+ if (name.startsWith("assert_")) return "assertion";
169
+ if (name === "watch_logs" || name === "index_artifacts") return "evidence";
170
+ if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
171
+ return segments.length > 1 ? segments[0] || "utility" : "utility";
172
+ }
173
+ function actionSearchScore(entry, term) {
174
+ const name = entry.name.toLowerCase();
175
+ const segment = finalSegment(name);
176
+ const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
177
+ const fields = entry.fields.map((field) => field.toLowerCase());
178
+ const description = entry.description.toLowerCase();
179
+ if (name === term) return 120;
180
+ if (segment === term) return 110;
181
+ if (entry.category === term) return 100;
182
+ if (fields.includes(term)) return 90;
183
+ if (nameTerms.includes(term)) return 80;
184
+ if (name.includes(term)) return 70;
185
+ if (fields.some((field) => field.includes(term))) return 60;
186
+ if (description.includes(term)) return 50;
187
+ if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
188
+ return 0;
189
+ }
190
+ function fuzzyTermMatch(term, candidate) {
191
+ if (term.length < 4 || candidate.length < 4) return false;
192
+ const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
193
+ return levenshtein(term, candidate) <= threshold;
194
+ }
195
+ function searchTerms(value) {
196
+ return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
197
+ }
198
+ function finalSegment(name) {
199
+ return name.split(".").pop() ?? name;
200
+ }
201
+ function levenshtein(left, right) {
202
+ const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
203
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
204
+ const current = [leftIndex];
205
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
206
+ const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
207
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
208
+ }
209
+ for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
210
+ }
211
+ return prior[right.length] ?? right.length;
128
212
  }
129
213
  export {
130
214
  describeManifestActions,
215
+ findRelatedActions,
131
216
  fuzzyResolveActions,
132
217
  handleActions,
133
- handleManifest,
218
+ searchActions,
134
219
  summarizeActionCategories
135
220
  };
@@ -19,6 +19,7 @@ function parseArgs(argv, command) {
19
19
  const options = {};
20
20
  const booleanOptions = /* @__PURE__ */ new Set([
21
21
  "json",
22
+ "jsonStream",
22
23
  "launchExistingDist",
23
24
  "startWatch",
24
25
  "record",
@@ -1,7 +1,7 @@
1
1
  import { detectAdapter } from "../harness.js";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
- import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
4
+ import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
5
5
  import {
6
6
  applyWatcherPortOption,
7
7
  optionFlag,
@@ -25,9 +25,13 @@ async function handleProvision({ positional, options, rawArgv }) {
25
25
  }
26
26
  const surface = getAdapterSurface(adapter);
27
27
  const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
28
+ const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
29
+ if (adapter === "mobile") {
30
+ writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
31
+ }
28
32
  const result = await surface.runwayProvision.run(target, {
29
33
  json,
30
- platform: optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios",
34
+ platform,
31
35
  branch: optionString(options, "branch"),
32
36
  defaultBranch: optionString(options, "defaultBranch"),
33
37
  run: optionString(options, "run"),
@@ -42,8 +46,10 @@ async function handleProvision({ positional, options, rawArgv }) {
42
46
  resolveOnly: optionFlag(options, "resolveOnly"),
43
47
  rerunCommand
44
48
  });
49
+ const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` : void 0;
50
+ const output = next ? { ...result, next } : result;
45
51
  if (json) {
46
- console.log(JSON.stringify(result, null, 2));
52
+ console.log(JSON.stringify(output, null, 2));
47
53
  } else if (result.status === "pass") {
48
54
  const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
49
55
  const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
@@ -54,6 +60,7 @@ async function handleProvision({ positional, options, rawArgv }) {
54
60
  const action = result.skipped ? "already provisioned" : "provisioned";
55
61
  console.error(`\u2713 ${action} ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "unknown"}`);
56
62
  }
63
+ if (next) console.error(` Next: ${next}`);
57
64
  } else {
58
65
  console.error(`\u2717 mm-harness provision: ${result.error?.message ?? "provision failed"}
59
66
  Next: ${result.error?.userAction ?? rerunCommand}`);
@@ -31,6 +31,7 @@ import {
31
31
  optionString,
32
32
  isRecord,
33
33
  shellQuoteArg,
34
+ targetPath,
34
35
  usageError
35
36
  } from "./parse-args.js";
36
37
  async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
@@ -66,7 +67,8 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
66
67
  await validateManifest(manifest);
67
68
  const { createMetaMaskRunner } = await import("../runner.js");
68
69
  const runner = await createMetaMaskRunner(adapter, manifest, {
69
- quietStdout: runtimeOptions.stdoutIsMachineContract === true
70
+ quietStdout: runtimeOptions.stdoutIsMachineContract === true,
71
+ onActionEvent: runtimeOptions.onActionEvent
70
72
  });
71
73
  const runRequest = {
72
74
  recipePath: path.resolve(recipe),
@@ -198,12 +200,7 @@ function libraryRecipeNames(adapter) {
198
200
  return [...new Set(names)].sort();
199
201
  }
200
202
  function recipeNameCandidates(recipeArg, adapter) {
201
- const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
202
- const candidates = [];
203
- for (const name of names) {
204
- candidates.push(...singleRecipeNameCandidates(name, adapter));
205
- }
206
- return [...new Set(candidates)];
203
+ return singleRecipeNameCandidates(recipeArg, adapter);
207
204
  }
208
205
  function singleRecipeNameCandidates(name, adapter) {
209
206
  if (name.endsWith(".recipe.json")) {
@@ -229,32 +226,6 @@ function domainPathForRecipeName(name) {
229
226
  function uniqueStrings(values) {
230
227
  return [...new Set(values)];
231
228
  }
232
- const LEGACY_RECIPE_ALIASES = {
233
- "action-validation": ["runner.action-validation"],
234
- "action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
235
- "action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
236
- "app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
237
- "app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
238
- "order-lifecycle": ["perps.order-lifecycle"],
239
- "order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
240
- "perps-lifecycle": ["perps.lifecycle"],
241
- "perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
242
- "perps-performance": ["perps.performance"],
243
- "perps-performance-background-resume": ["perps.performance.background-resume"],
244
- "perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
245
- "perps-performance-cold-start": ["perps.performance.cold-start"],
246
- "perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
247
- "perps-performance-warm-start": ["perps.performance.warm-start"],
248
- "perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
249
- "perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
250
- "read-markets": ["perps.read-markets"],
251
- "read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
252
- "smoke": ["runner.smoke"],
253
- "smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
254
- "smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
255
- "trading-lifecycle": ["perps.trading-lifecycle"],
256
- "trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
257
- };
258
229
  async function validateRunRecipeStatic(recipeArg, adapter, options) {
259
230
  const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
260
231
  const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
@@ -348,6 +319,7 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
348
319
  const device = optionString(options, "device");
349
320
  const actionManifest = optionString(options, "actionManifest");
350
321
  const parts = ["mm-harness", "call", matches[0]];
322
+ parts.push("--adapter", adapter, "--target", shellQuoteArg(targetPath(options)));
351
323
  if (device) parts.push("--device", shellQuoteArg(device));
352
324
  if (actionManifest) parts.push("--action-manifest", shellQuoteArg(actionManifest));
353
325
  return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
@@ -405,10 +377,12 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
405
377
  async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
406
378
  if (recipeRunning(target)) {
407
379
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
380
+ const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
408
381
  if (json) {
409
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
382
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
410
383
  } else {
411
- console.error(`\u2717 mm-harness: ${msg}`);
384
+ console.error(`\u2717 mm-harness: ${msg}
385
+ Next: ${userAction}`);
412
386
  }
413
387
  return EXIT.bounded;
414
388
  }
@@ -421,21 +395,25 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
421
395
  return EXIT.usage;
422
396
  }
423
397
  const state = newHealState();
398
+ prepareOptions.onPhase?.("install");
424
399
  const ensured = await ensureOverlay(adapter, target, heal, state, json);
425
400
  if (!ensured.ok) {
401
+ const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
426
402
  if (json) {
427
403
  console.log(
428
404
  JSON.stringify(
429
- { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
405
+ { schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error, userAction } },
430
406
  null,
431
407
  2
432
408
  )
433
409
  );
434
410
  } else {
435
- console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}`);
411
+ console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
412
+ Next: ${userAction}`);
436
413
  }
437
414
  return EXIT.infra;
438
415
  }
416
+ prepareOptions.onPhase?.("healthcheck");
439
417
  if (adapter === "extension") {
440
418
  const previousCdpPort = process.env.CDP_PORT;
441
419
  const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
@@ -448,7 +426,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
448
426
  }
449
427
  if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
450
428
  try {
451
- const current = await ensureExtensionProofRuntime(target, heal, state, json);
429
+ const current = await ensureExtensionProofRuntime(target, heal, state, json, {
430
+ onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
431
+ });
452
432
  if (current !== null) return current;
453
433
  } finally {
454
434
  restoreEnv("CDP_PORT", previousCdpPort);
@@ -457,7 +437,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
457
437
  }
458
438
  }
459
439
  if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
460
- const current = await ensureMobileProofRuntime(target, heal, state, json);
440
+ const current = await ensureMobileProofRuntime(target, heal, state, json, {
441
+ onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
442
+ });
461
443
  if (current !== null) return current;
462
444
  }
463
445
  return { state, heal };
@@ -502,6 +484,7 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
502
484
  }
503
485
  const recoveryCode = "mobile.source_reloaded";
504
486
  state.attemptedRecoveries.push(recoveryCode);
487
+ deps.onRecovery?.(recoveryCode);
505
488
  if (!json) console.error(`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`);
506
489
  const restart = deps.restart ?? (async () => {
507
490
  const result2 = await runOneNode(
@@ -588,6 +571,7 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
588
571
  }
589
572
  const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
590
573
  state.attemptedRecoveries.push(recoveryCode);
574
+ deps.onRecovery?.(recoveryCode);
591
575
  if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
592
576
  const launch = deps.launch ?? (async (requestedTier) => {
593
577
  const { launchExtension } = await import("./launch/extension.js");
@@ -642,7 +626,7 @@ function readRunFailureText(result) {
642
626
  return "";
643
627
  }
644
628
  }
645
- async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
629
+ async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
646
630
  let result = await exec();
647
631
  for (; ; ) {
648
632
  if (result.status === "pass") return { result, violation: null };
@@ -651,6 +635,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
651
635
  if (heal === "off") return { result, violation: null };
652
636
  const recoveryCode = RUN_RECOVERY_CODE[adapter];
653
637
  state.attemptedRecoveries.push(recoveryCode);
638
+ onRecovery(recoveryCode);
654
639
  await recover();
655
640
  result = await exec();
656
641
  if (result.status === "pass") {
@@ -660,7 +645,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
660
645
  }
661
646
  }
662
647
  function emitHealViolation(json, command, result, violation, state, adapter) {
663
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction;
648
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
664
649
  if (json) {
665
650
  console.log(
666
651
  JSON.stringify(
@@ -680,7 +665,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
680
665
  code: violation.code,
681
666
  message: violation.message,
682
667
  retryable: false,
683
- userAction: userAction ?? null,
668
+ userAction,
684
669
  originalError: violation.originalError ?? null
685
670
  }
686
671
  },
@@ -34,7 +34,7 @@ function renderRunReport(summary, entries) {
34
34
  lines.push(
35
35
  "",
36
36
  "## Side findings",
37
- `- REVIEW ${sideFindingTotal} distinct application warning/error event(s); see diagnostics.json (non-blocking)`
37
+ `- REVIEW ${sideFindingTotal} distinct application warning/error event(s) (non-blocking; expanded below and stored in diagnostics.json)`
38
38
  );
39
39
  }
40
40
  lines.push("", "## Steps");