@deeeed/metamask-harness 0.17.5 → 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 (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
@@ -1,20 +1,21 @@
1
1
  import fs from "node:fs";
2
- import { mkdtemp } from "node:fs/promises";
3
- import os from "node:os";
2
+ import { createHash } from "node:crypto";
4
3
  import path from "node:path";
5
- import { loadActionManifest } from "../manifest.js";
4
+ import { resolveActionManifest } from "../manifest.js";
6
5
  import { importRecipeProtocol } from "../paths.js";
7
- import { recipeRunning } from "../heal-bounds.js";
6
+ import { conciseFailureForHuman, recipeRunning } from "../heal-bounds.js";
8
7
  import { color } from "../cli-color.js";
9
- import { EXIT } from "./shared.js";
10
- import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
8
+ import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
9
+ import { actionExampleCommand, describeManifestActions, fuzzyResolveActions } from "./manifest.js";
11
10
  import {
12
11
  parseArgs,
13
12
  isRecord,
14
13
  optionFlag,
15
14
  optionString,
15
+ optionStrings,
16
16
  resolveAdapter,
17
17
  runtimeOptionsFromCli,
18
+ shellQuote,
18
19
  usageError
19
20
  } from "./parse-args.js";
20
21
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -27,11 +28,16 @@ import {
27
28
  prepareHeal,
28
29
  recoverRunInfra,
29
30
  resolveMetaMaskLibrarySources,
31
+ preflightRecipe,
30
32
  runRecipe,
31
33
  synthesizeOneNodeRecipe,
32
34
  validateRecipeAdapterAware
33
35
  } from "./run-engine.js";
34
36
  import { acquireCheckoutLock } from "../checkout-lock.js";
37
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
38
+ import { recipeTrustFailure } from "../recipe-security.js";
39
+ import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
40
+ import { closest } from "../command-contract.js";
35
41
  async function handleCall(argv) {
36
42
  if (argv.includes("--list")) {
37
43
  const { options: options2 } = parseArgs(argv, "call");
@@ -50,7 +56,8 @@ async function handleCall(argv) {
50
56
  let discovery = "mm-harness actions";
51
57
  try {
52
58
  const { adapter: adapter2 } = resolveAdapter(options);
53
- const manifest2 = loadActionManifest(adapter2, optionString(options, "actionManifest"));
59
+ const librarySources2 = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
60
+ const { manifest: manifest2 } = await resolveActionManifest(adapter2, optionString(options, "actionManifest"), librarySources2);
54
61
  const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
55
62
  const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
56
63
  example = `mm-harness call ${exampleAction} --adapter ${adapter2}`;
@@ -59,47 +66,70 @@ async function handleCall(argv) {
59
66
  }
60
67
  const message = `call requires <action>. Example: ${example}`;
61
68
  const userAction = `${example} # see the vocabulary: ${discovery}`;
62
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", error: { code: "USAGE", message, userAction } }, null, 2));
69
+ if (json) console.log(JSON.stringify({
70
+ schemaVersion: 1,
71
+ command: "call",
72
+ status: "fail",
73
+ error: { code: "CLI_MISSING_POSITIONAL", message, userAction },
74
+ exitCode: EXIT.usage
75
+ }, null, 2));
63
76
  else console.error(`${message}
64
77
  See the vocabulary: ${discovery}`);
65
78
  return EXIT.usage;
66
79
  }
67
80
  const { adapter, target } = resolveAdapter(options);
81
+ if (!fs.existsSync(target)) {
82
+ return usageOut(
83
+ json,
84
+ "call",
85
+ `target does not exist: ${target}`,
86
+ "pass --target <metamask-checkout> pointing to an existing checkout"
87
+ );
88
+ }
68
89
  getAdapterSurface(adapter).resolveSlotPorts(target);
69
90
  const dtResult = applyDeviceTargeting("call", adapter, options, { gate: true, rerun: "" });
70
91
  if ("code" in dtResult) {
71
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
72
- else console.error(`\u2717 call: ${dtResult.message}`);
92
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
93
+ else console.error(`\u2717 call: ${dtResult.message}
94
+ Next: ${dtResult.userAction}`);
73
95
  return EXIT.usage;
74
96
  }
75
97
  if (recipeRunning(target)) {
76
98
  const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
99
+ const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
77
100
  if (json) {
78
- console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
101
+ console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
79
102
  } else {
80
- console.error(`\u2717 mm-harness call: ${msg}`);
103
+ console.error(`\u2717 mm-harness call: ${msg}
104
+ Next: ${userAction}`);
81
105
  }
82
106
  return EXIT.bounded;
83
107
  }
84
108
  const actionManifestOverride = optionString(options, "actionManifest");
85
- const manifest = loadActionManifest(adapter, actionManifestOverride);
109
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
110
+ const { manifest, actionSources } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
86
111
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
87
112
  const names = getRecipeActionManifestActionNames(manifest);
88
113
  const resolution = resolveActionName(shortName, names);
89
114
  if (resolution.status === "unknown") {
90
- const message = `\u2717 call: unknown action "${shortName}" for the ${adapter} adapter.
91
- See the vocabulary: mm-harness actions --adapter ${adapter} --json`;
92
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message } }, null, 2));
93
- else console.error(message);
115
+ const message = `unknown action "${shortName}" for the ${adapter} adapter.`;
116
+ const userAction = `mm-harness actions --adapter ${adapter} --json`;
117
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
118
+ else console.error(`\u2717 call: ${message}
119
+ Next: ${userAction}`);
94
120
  return EXIT.usage;
95
121
  }
96
122
  if (resolution.status === "ambiguous") {
97
- const message = `\u2717 call: "${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
98
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates } }, null, 2));
99
- else console.error(message);
123
+ const message = `"${shortName}" is ambiguous: ${resolution.candidates.join(", ")} \u2014 use the full name.`;
124
+ const userAction = `mm-harness actions --action ${resolution.candidates[0]} --adapter ${adapter} --json`;
125
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, error: { code: "ACTION_AMBIGUOUS", message, candidates: resolution.candidates, userAction } }, null, 2));
126
+ else console.error(`\u2717 call: ${message}
127
+ Next: ${userAction}`);
100
128
  return EXIT.usage;
101
129
  }
102
130
  const resolvedAction = resolution.resolved;
131
+ const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
132
+ const defaultsUsed = actionDefaultsUsed(describedAction, args);
103
133
  const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
104
134
  if (depsBlock) {
105
135
  if (json) {
@@ -111,21 +141,91 @@ async function handleCall(argv) {
111
141
  return EXIT.usage;
112
142
  }
113
143
  const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
114
- const validation = await validateRecipeAdapterAware(recipe, manifest);
144
+ const validation = await validateRecipeAdapterAware(adapter, recipe, manifest, librarySources);
115
145
  if (validation.status === "invalid") {
116
- const message = `\u2717 call ${resolvedAction}: recipe validation failed`;
117
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args, findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message } }, null, 2));
146
+ const message = `call ${resolvedAction}: recipe validation failed`;
147
+ const parameterHelp = parameterValidationHelp(describedAction, validation.findings, args);
148
+ const userAction = describedAction ? actionExampleCommand(
149
+ describedAction,
150
+ adapter,
151
+ target,
152
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
153
+ ) ?? `mm-harness actions --action ${resolvedAction} --adapter ${adapter}` : `mm-harness actions --action ${resolvedAction} --adapter ${adapter}`;
154
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, ...parameterHelp.length > 0 ? { parameterHelp } : {}, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
118
155
  else {
119
- console.error(message);
120
- for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
156
+ console.error(`\u2717 ${message}`);
157
+ const taught = new Set(parameterHelp.map((entry) => `${entry.issue}:${entry.name}`));
158
+ for (const entry of parameterHelp) console.error(renderParameterValidationHelp(entry));
159
+ for (const finding of validation.findings) {
160
+ const name = finding.path.split(".").at(-1);
161
+ const issue = parameterIssue(finding.code);
162
+ if (issue && name && taught.has(`${issue}:${name}`)) continue;
163
+ console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
164
+ }
165
+ console.error(` Next: ${userAction}`);
166
+ }
167
+ return EXIT.validation;
168
+ }
169
+ const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
170
+ recordCommandEvidence(artifactsDir);
171
+ const requestedRuntimeOptions = runtimeOptionsFromCli(options);
172
+ const callRuntimeOptions = {
173
+ ...requestedRuntimeOptions,
174
+ ...librarySources ? { librarySources } : {},
175
+ autoHud: false,
176
+ suppressLibraryResolutionLogs: true,
177
+ stdoutIsMachineContract: json,
178
+ source: requestedRuntimeOptions.source ?? {
179
+ kind: "operator",
180
+ trust: "trusted",
181
+ name: "mm-harness call"
182
+ }
183
+ };
184
+ let preflightedExecution;
185
+ try {
186
+ preflightedExecution = await preflightRecipe(
187
+ adapter,
188
+ recipe,
189
+ artifactsDir,
190
+ target,
191
+ actionManifestOverride,
192
+ callRuntimeOptions
193
+ );
194
+ } catch (error) {
195
+ const trustFailure = recipeTrustFailure(error);
196
+ if (!trustFailure) throw error;
197
+ const planDigest = trustFailure.details?.recipeDigest;
198
+ if (planDigest) {
199
+ trustFailure.userAction = `review the plan, then rerun this call with --artifacts-dir ${shellQuote(artifactsDir)} --approve-plan ${shellQuote(planDigest)}; managed callers must keep the same artifact directory and execution environment, then set FARMSLOT_RECIPE_APPROVE_PLAN=${shellQuote(planDigest)}`;
200
+ }
201
+ if (json) {
202
+ console.log(JSON.stringify({
203
+ schemaVersion: 1,
204
+ command: "call",
205
+ status: "fail",
206
+ error: trustFailure,
207
+ exitCode: EXIT.validation
208
+ }, null, 2));
209
+ } else {
210
+ console.error(`\u2717 mm-harness call: ${trustFailure.message}`);
211
+ const blocked = trustFailure.details?.blocked;
212
+ if (blocked?.length) {
213
+ console.error(" Restricted plan nodes:");
214
+ for (const node of blocked.slice(0, 10)) {
215
+ const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
216
+ console.error(
217
+ ` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
218
+ );
219
+ }
220
+ if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
221
+ }
222
+ console.error(` Next: ${trustFailure.userAction}`);
121
223
  }
122
224
  return EXIT.validation;
123
225
  }
124
226
  const lock = acquireCheckoutLock(target, "call");
125
227
  if ("message" in lock) {
126
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", status: "fail", recoverable: false, error: { code: "SANDBOX_BUSY", message: lock.message } }, null, 2));
127
- else console.error(`\u2717 mm-harness call: ${lock.message}`);
128
- return EXIT.bounded;
228
+ return checkoutBusyOut(json, "call", lock.message, lock.path);
129
229
  }
130
230
  try {
131
231
  const prepared = await prepareHeal(adapter, target, options, json, {
@@ -133,27 +233,41 @@ async function handleCall(argv) {
133
233
  });
134
234
  if (typeof prepared === "number") return prepared;
135
235
  const { state, heal } = prepared;
136
- const scratch = await mkdtemp(path.join(os.tmpdir(), "mm-harness-call-"));
137
- const recipeFile = path.join(scratch, "call.recipe.json");
138
- fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
139
- `);
140
- const artifactsDir = optionString(options, "artifactsDir") ?? path.join(scratch, "artifacts");
141
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
142
- const callRuntimeOptions = {
143
- ...runtimeOptionsFromCli(options),
144
- ...librarySources ? { librarySources } : {},
145
- stdoutIsMachineContract: json
146
- };
147
236
  const { result, violation } = await executeWithHealBounds(
148
- () => runRecipe(adapter, recipeFile, artifactsDir, target, actionManifestOverride, callRuntimeOptions),
237
+ () => {
238
+ const execution = preflightedExecution;
239
+ preflightedExecution = void 0;
240
+ return runRecipe(
241
+ adapter,
242
+ recipe,
243
+ artifactsDir,
244
+ target,
245
+ actionManifestOverride,
246
+ callRuntimeOptions,
247
+ execution
248
+ );
249
+ },
149
250
  adapter,
150
251
  target,
151
252
  heal,
152
253
  state,
153
254
  () => recoverRunInfra(adapter, target, json)
154
255
  );
155
- if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
256
+ if (violation !== null) {
257
+ const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
258
+ const example = describedAction ? actionExampleCommand(
259
+ describedAction,
260
+ adapter,
261
+ target,
262
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
263
+ ) : void 0;
264
+ const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
265
+ return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
266
+ }
156
267
  const callOutput = readCallOutput(result.tracePath);
268
+ const safeArgs = redactCallValue(args);
269
+ const safeCallOutput = redactCallValue(callOutput);
270
+ const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
157
271
  if (json) {
158
272
  console.log(
159
273
  JSON.stringify(
@@ -163,30 +277,43 @@ async function handleCall(argv) {
163
277
  adapter,
164
278
  action: shortName,
165
279
  resolvedAction,
166
- args,
280
+ args: safeArgs,
281
+ ...Object.keys(defaultsUsed).length > 0 ? { defaultsUsed } : {},
167
282
  status: result.status,
168
283
  summaryPath: result.summaryPath,
169
284
  tracePath: result.tracePath,
170
285
  artifactManifestPath: result.artifactManifestPath,
171
- ...callOutput !== void 0 ? { output: callOutput } : {},
286
+ ...result.diagnosticsPath ? { diagnosticsPath: result.diagnosticsPath } : {},
287
+ ...result.sideFindings ? { sideFindings: result.sideFindings } : {},
288
+ ...callOutput !== void 0 ? { output: safeCallOutput } : {},
172
289
  recovered: state.recovered,
173
290
  mutations: state.mutations,
174
- exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime
291
+ exitCode: result.status === "pass" ? EXIT.ok : EXIT.runtime,
292
+ ...result.status === "fail" ? { error: { code: "ACTION_EXECUTION_FAILED", message: `${resolvedAction} failed; inspect the persisted result and evidence paths`, userAction: failureUserAction } } : {}
175
293
  },
176
294
  null,
177
295
  2
178
296
  )
179
297
  );
180
298
  } else {
181
- const rendered = callOutput !== void 0 ? `
182
- Result:
183
- ${formatCallOutput(callOutput)}` : "";
299
+ const renderedInputs = Object.keys(args).length > 0 ? formatCallOutput(safeArgs) : "";
184
300
  const out = (style, text) => color(style, text, { stream: process.stdout });
301
+ const diagnostics = formatRunDiagnosticsForHuman(
302
+ readRunDiagnosticsDocument(result.diagnosticsPath),
303
+ adapter
304
+ );
305
+ const renderedDiagnostics = `${out("label", "Diagnostics:")}
306
+ ${diagnostics.map((line) => ` ${line}`).join("\n")}
307
+ `;
185
308
  console.log(
186
- `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
309
+ `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${renderedInputs ? `
310
+ ${out("label", "Inputs:")}
311
+ ${renderedInputs}` : ""}${Object.keys(defaultsUsed).length > 0 ? `
312
+ ${renderDefaultsUsed(defaultsUsed, process.stdout)}` : ""}${callOutput !== void 0 ? `
187
313
  ${out("label", "Result:")}
188
- ${formatCallOutput(callOutput)}` : ""}
189
- ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
314
+ ${formatCallOutput(safeCallOutput)}` : ""}
315
+ ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
316
+ Next: ${failureUserAction}` : "")
190
317
  );
191
318
  }
192
319
  return result.status === "pass" ? EXIT.ok : EXIT.runtime;
@@ -194,6 +321,59 @@ ${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
194
321
  lock.release();
195
322
  }
196
323
  }
324
+ function parameterValidationHelp(action, findings, args) {
325
+ const schema = isRecord(action?.schema) ? action.schema : {};
326
+ const properties = isRecord(schema.properties) ? schema.properties : {};
327
+ return findings.flatMap((finding) => {
328
+ const issue = parameterIssue(finding.code);
329
+ if (!issue) return [];
330
+ const name = finding.path.split(".").at(-1);
331
+ const property = name && isRecord(properties[name]) ? properties[name] : void 0;
332
+ if (!name || !property) return [];
333
+ const validValues = Array.isArray(property.enum) ? property.enum : void 0;
334
+ const received = Object.hasOwn(args, name) ? args[name] : void 0;
335
+ const suggestion = issue === "invalid" && received !== void 0 && validValues ? closest(String(received), validValues.map(String)) : void 0;
336
+ return [{
337
+ issue,
338
+ name,
339
+ ...typeof property.type === "string" ? { type: property.type } : {},
340
+ ...validValues ? { validValues } : {},
341
+ ...typeof property.description === "string" ? { description: property.description } : {},
342
+ ...issue === "invalid" && received !== void 0 ? { received } : {},
343
+ ...suggestion ? { suggestion } : {}
344
+ }];
345
+ });
346
+ }
347
+ function parameterIssue(code) {
348
+ if (code === "recipe.missing_param") return "missing";
349
+ if (code === "recipe.invalid_param_value_enum") return "invalid";
350
+ return void 0;
351
+ }
352
+ function renderParameterValidationHelp(parameter) {
353
+ const out = (style, text) => color(style, text, { stream: process.stderr });
354
+ const heading = parameter.issue === "missing" ? `${out("cmd", parameter.name)}${parameter.type ? ` (${parameter.type}, required)` : " (required)"}` : `${out("cmd", parameter.name)} received ${out("err", JSON.stringify(parameter.received) ?? String(parameter.received))}`;
355
+ const values = parameter.validValues?.length ? ` Valid values: ${parameter.validValues.map((value) => out("accent", String(value))).join(", ")}.` : "";
356
+ const suggestion = parameter.suggestion ? ` Did you mean ${out("accent", JSON.stringify(parameter.suggestion))}?` : "";
357
+ const description = parameter.description ? ` ${parameter.description}` : "";
358
+ return ` ${heading}.${values}${suggestion}${description}`;
359
+ }
360
+ function actionDefaultsUsed(action, args) {
361
+ const schema = isRecord(action?.schema) ? action.schema : {};
362
+ const properties = isRecord(schema.properties) ? schema.properties : {};
363
+ return Object.fromEntries(
364
+ Object.entries(properties).filter(([name, entry]) => !Object.hasOwn(args, name) && isRecord(entry) && Object.hasOwn(entry, "default")).map(([name, entry]) => [name, entry.default])
365
+ );
366
+ }
367
+ function renderDefaultsUsed(defaults, stream) {
368
+ const out = (style, text) => color(style, text, { stream });
369
+ const values = Object.entries(defaults).map(([name, value]) => `${out("cmd", name)}=${out("accent", JSON.stringify(value) ?? String(value))}`).join(", ");
370
+ return `${out("label", "Defaults used:")} ${values}`;
371
+ }
372
+ function defaultCallArtifactsDir(target, action, args) {
373
+ const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
374
+ const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
375
+ return path.join(target, "temp", "recipe", "calls", `${actionStem}-${digest}`);
376
+ }
197
377
  function readCallOutput(tracePath) {
198
378
  try {
199
379
  const trace = JSON.parse(fs.readFileSync(tracePath, "utf8"));
@@ -209,20 +389,27 @@ function formatCallOutput(value) {
209
389
  if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
210
390
  return JSON.stringify(value, null, 2);
211
391
  }
392
+ function redactCallValue(value, key = "") {
393
+ return key && isSensitiveKey(key) ? "<redacted>" : redactStructuredValue(value);
394
+ }
212
395
  async function handleCallHelp(argv, genericHelp) {
213
396
  const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
214
397
  const { options } = parseArgs(rest, "call");
215
398
  let adapter;
216
399
  let manifest;
400
+ let actionSources = /* @__PURE__ */ new Map();
217
401
  try {
218
402
  ({ adapter } = resolveAdapter(options));
219
- manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
403
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
404
+ const resolution = await resolveActionManifest(adapter, optionString(options, "actionManifest"), librarySources);
405
+ manifest = resolution.manifest;
406
+ actionSources = resolution.actionSources;
220
407
  } catch {
221
408
  process.stdout.write(`${genericHelp}
222
409
  `);
223
410
  return EXIT.ok;
224
411
  }
225
- const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest), shortName) : [];
412
+ const matches = shortName ? fuzzyResolveActions(describeManifestActions(manifest, actionSources), shortName) : [];
226
413
  if (matches.length === 0) {
227
414
  process.stdout.write(`${genericHelp}
228
415
  `);
@@ -250,7 +437,8 @@ function renderCallActionHelp(entry) {
250
437
  );
251
438
  const lines = [`mm-harness call ${entry.name} [key=value ...] [--arg k=v ...] [flags]`, ""];
252
439
  if (entry.description) lines.push(` ${entry.description}`, "");
253
- const names = Object.keys(properties).sort();
440
+ lines.push(` Source: ${entry.source}${entry.sourceManifest ? ` (${entry.sourceManifest})` : ""}`, "");
441
+ const names = Object.keys(properties).filter((name) => name !== "action" && name !== "next").sort();
254
442
  if (names.length === 0) {
255
443
  lines.push(" Fields: (none)");
256
444
  } else {
@@ -262,7 +450,8 @@ function renderCallActionHelp(entry) {
262
450
  const req = required.has(name) ? " (required)" : "";
263
451
  const desc = typeof prop.description === "string" ? ` \u2014 ${prop.description}` : "";
264
452
  const enumVals = Array.isArray(prop.enum) ? ` [one of: ${prop.enum.join(", ")}]` : "";
265
- lines.push(` ${name.padEnd(width)} ${type}${req}${desc}${enumVals}`);
453
+ const defaultValue = Object.hasOwn(prop, "default") ? ` [default: ${JSON.stringify(prop.default)}]` : "";
454
+ lines.push(` ${name.padEnd(width)} ${type}${req}${defaultValue}${desc}${enumVals}`);
266
455
  }
267
456
  }
268
457
  const examples = renderCallExamples(short, entry.examples);
@@ -320,7 +509,7 @@ function parseCallValue(value) {
320
509
  const number = Number(value);
321
510
  if (Number.isFinite(number)) return number;
322
511
  }
323
- if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
512
+ if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}") || value.startsWith('"') && value.endsWith('"')) {
324
513
  try {
325
514
  return JSON.parse(value);
326
515
  } catch {
@@ -340,6 +529,7 @@ function isForwardedOptionValue(argv, index) {
340
529
  }
341
530
  const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
342
531
  "action-manifest",
532
+ "approve-plan",
343
533
  "adapter",
344
534
  "artifacts-dir",
345
535
  "cdp-port",
@@ -348,8 +538,11 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
348
538
  "library",
349
539
  "metro-port",
350
540
  "platform",
351
- "project-root",
352
541
  "slot",
542
+ "source-digest",
543
+ "source-kind",
544
+ "source-name",
545
+ "source-trust",
353
546
  "target",
354
547
  "validation-runtime-dir",
355
548
  "watcher-port"
@@ -373,6 +566,8 @@ function pickCallExampleAction(names) {
373
566
  return names[0] ?? "command";
374
567
  }
375
568
  export {
569
+ defaultCallArtifactsDir,
376
570
  handleCall,
377
- handleCallHelp
571
+ handleCallHelp,
572
+ redactCallValue
378
573
  };
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { detectAdapter } from "../harness.js";
5
5
  import { EXIT, usageOut } from "./shared.js";
6
- import { optionFlag, optionString, parseArgs } from "./parse-args.js";
6
+ import { optionFlag, optionString, parseArgs, shellQuote } from "./parse-args.js";
7
7
  const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
8
8
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
9
9
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
@@ -18,7 +18,7 @@ async function handleCheck(argv) {
18
18
  const message = action ? `unknown action '${action}'` : "missing action";
19
19
  return usageOut(json, "check", message, USAGE);
20
20
  }
21
- const target = path.resolve(optionString(options, "target") ?? optionString(options, "projectRoot") ?? process.cwd());
21
+ const target = path.resolve(optionString(options, "target") ?? process.cwd());
22
22
  const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
23
23
  const detected = explicitAdapter ?? detectAdapter(target);
24
24
  if (detected && !["mobile", "extension", "core"].includes(detected)) {
@@ -111,6 +111,7 @@ async function handleCheck(argv) {
111
111
  const status = [...fixes, ...checks].some((check) => check.status === "fail") ? "fail" : "pass";
112
112
  const exitCode = status === "pass" ? EXIT.ok : EXIT.validation;
113
113
  const guidance = policyFailed ? ["Remove the newly added eslint-disable directive(s). Fix the underlying violation with existing named styles, project tokens, or another established source pattern; then rerun mm-harness check diff [--fix]. Other checks were skipped until this policy failure is removed."] : checks.some((check) => check.id === "eslint" && check.status === "fail") ? ["Fix the reported violations in changed source using existing project patterns and tokens; do not suppress rules, hide violations behind indirection, weaken configuration, or bypass check diff. Then rerun mm-harness check diff [--fix]."] : [];
114
+ const failureUserAction = guidance[0] ?? `inspect ${path.join(artifactsDir, "validation-summary.json")}, fix the failing check, then retry: mm-harness check diff --target ${shellQuote(target)} --base ${shellQuote(baseRef)} --profile ${profile} --json`;
114
115
  const summary = {
115
116
  schemaVersion: 1,
116
117
  command: "check",
@@ -129,6 +130,7 @@ async function handleCheck(argv) {
129
130
  fixes,
130
131
  checks,
131
132
  guidance,
133
+ ...status === "fail" ? { error: { code: "CHECK_DIFF_FAILED", message: "one or more bounded checks failed", userAction: failureUserAction } } : {},
132
134
  artifacts: {
133
135
  dir: artifactsDir,
134
136
  summaryJson: path.join(artifactsDir, "validation-summary.json"),
@@ -518,10 +520,14 @@ function renderHuman(summary) {
518
520
  if (check.status === "fail" && check.logPath) console.log(` log: ${check.logPath}`);
519
521
  }
520
522
  for (const item of summary.guidance) console.log(` Next: ${item}`);
523
+ if (summary.guidance.length === 0 && summary.status === "fail" && summary.error?.userAction) {
524
+ console.log(` Next: ${summary.error.userAction}`);
525
+ }
521
526
  console.log(`Artifacts: ${summary.artifacts.summaryJson}`);
522
527
  }
523
528
  function failEnvelope(input) {
524
529
  const exitCode = EXIT.usage;
530
+ const userAction = USAGE;
525
531
  const body = {
526
532
  schemaVersion: 1,
527
533
  command: "check",
@@ -534,11 +540,11 @@ function failEnvelope(input) {
534
540
  baseRef: input.baseRef,
535
541
  baseSource: "unknown",
536
542
  artifacts: { dir: input.artifactsDir },
537
- error: { code: input.code, message: input.message }
543
+ error: { code: input.code, message: input.message, userAction }
538
544
  };
539
545
  if (input.json) console.log(JSON.stringify(body, null, 2));
540
546
  else console.error(`\u2717 check diff: ${input.message}
541
- Next: ${USAGE}`);
547
+ Next: ${userAction}`);
542
548
  return exitCode;
543
549
  }
544
550
  export {
@@ -1,25 +1,26 @@
1
- import { loadActionManifest } from "../manifest.js";
1
+ import { resolveActionManifest } from "../manifest.js";
2
2
  import {
3
3
  invalidateCompletionCache,
4
4
  readFreshCandidates,
5
5
  writeCompletionCandidates
6
6
  } from "../completions-cache.js";
7
- import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
7
+ import { importRecipeProtocol } from "../paths.js";
8
8
  import { EXIT } from "./shared.js";
9
9
  import {
10
10
  parseArgs,
11
11
  optionFlag,
12
12
  optionString,
13
+ optionStrings,
13
14
  resolveAdapter,
14
15
  targetPath
15
16
  } from "./parse-args.js";
16
- import { resolveMetaMaskLibrarySources } from "./run-engine.js";
17
+ import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
17
18
  async function handleCompletionCandidates(argv) {
18
19
  const kind = argv[0];
19
20
  const { options } = parseArgs(argv.slice(1), "completion-candidates");
20
21
  const json = optionFlag(options, "json");
21
- if (kind !== "actions" && kind !== "flows") {
22
- console.error("completion-candidates requires <actions|flows>.");
22
+ if (kind !== "actions" && kind !== "recipes") {
23
+ console.error("completion-candidates requires <actions|recipes>.");
23
24
  return EXIT.usage;
24
25
  }
25
26
  const target = targetPath(options);
@@ -27,25 +28,19 @@ async function handleCompletionCandidates(argv) {
27
28
  let candidates;
28
29
  if (kind === "actions") {
29
30
  adapter = resolveAdapter(options).adapter;
30
- candidates = readFreshCandidates(target, "actions");
31
- if (!candidates) {
32
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
33
- const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
34
- candidates = getRecipeActionManifestActionNames(manifest);
35
- writeCompletionCandidates(target, "actions", candidates);
36
- }
31
+ const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
32
+ const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
33
+ const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
34
+ candidates = getRecipeActionManifestActionNames(manifest);
35
+ writeCompletionCandidates(target, "actions", candidates);
37
36
  } else {
38
- candidates = readFreshCandidates(target, "flows");
37
+ adapter = resolveAdapter(options).adapter;
38
+ const cacheKey = `recipes:${adapter}`;
39
+ candidates = readFreshCandidates(target, cacheKey);
39
40
  if (!candidates) {
40
- const sources = await resolveMetaMaskLibrarySources(void 0).catch(() => void 0);
41
- if (!sources) {
42
- candidates = [];
43
- } else {
44
- const harness = await importRecipeHarness();
45
- const resolution = await harness.loadRecipeLibraries(sources);
46
- candidates = [...resolution.flows.keys()].sort();
47
- writeCompletionCandidates(target, "flows", candidates);
48
- }
41
+ const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library")).catch(() => void 0);
42
+ candidates = sources ? (await listRunnableRecipes(adapter, sources)).map((recipe) => recipe.name) : [];
43
+ writeCompletionCandidates(target, cacheKey, candidates);
49
44
  }
50
45
  }
51
46
  if (json) console.log(JSON.stringify({ schemaVersion: 1, kind, adapter, candidates }, null, 2));