@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
package/dist/manifest.js CHANGED
@@ -1,5 +1,10 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
4
+ import {
5
+ actionSourceIntegrity,
6
+ metaMaskActionExecutionCapabilities
7
+ } from "./recipe-security.js";
3
8
  function loadMetaMaskMobileActionManifest() {
4
9
  return asActionManifest(readJson(manifestPath("mobile")));
5
10
  }
@@ -15,6 +20,130 @@ function loadActionManifest(adapter, overridePath) {
15
20
  if (adapter === "core") return loadMetaMaskCoreActionManifest();
16
21
  return loadMetaMaskExtensionActionManifest();
17
22
  }
23
+ async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
24
+ if (overridePath) {
25
+ const manifestPath2 = path.resolve(overridePath);
26
+ const implementationRoot = path.resolve(
27
+ taskActionRoot ?? defaultImplementationRoot(manifestPath2)
28
+ );
29
+ const manifest = withMetaMaskExecutionCapabilities(loadActionManifest(adapter, manifestPath2));
30
+ await validateManifest(manifest);
31
+ const integrity = await actionSourceIntegrity(manifestPath2, "task", implementationRoot);
32
+ return {
33
+ manifest,
34
+ actionSources: describeManifestSources(manifest, {
35
+ name: "task",
36
+ tier: "task",
37
+ manifestPath: manifestPath2,
38
+ implementationRoot,
39
+ trust: "unknown",
40
+ ...integrity
41
+ })
42
+ };
43
+ }
44
+ const canonicalPath = manifestPath(adapter);
45
+ const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }];
46
+ const manifests = [];
47
+ for (const source of sources) {
48
+ const file = path.join(source.root, "manifests", `${adapter}.action-manifest.json`);
49
+ if (!fs.existsSync(file)) continue;
50
+ const manifest = asActionManifest(readJson(file));
51
+ await validateManifest(manifest);
52
+ manifests.push({ source, file, manifest });
53
+ }
54
+ if (!manifests.some((entry) => path.resolve(entry.file) === path.resolve(canonicalPath))) {
55
+ const manifest = loadActionManifest(adapter);
56
+ await validateManifest(manifest);
57
+ manifests.push({ source: { name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }, file: canonicalPath, manifest });
58
+ }
59
+ const canonical = manifests.find((entry) => entry.source.name === "metamask") ?? manifests.at(-1);
60
+ if (!canonical) throw new Error(`No ${adapter} action manifest could be resolved.`);
61
+ const base = cloneManifest(canonical.manifest);
62
+ const official = new Set(stringArray(recordOf(base).supported_official_actions));
63
+ const custom = /* @__PURE__ */ new Map();
64
+ const customMetadata = /* @__PURE__ */ new Map();
65
+ const bindings = /* @__PURE__ */ new Map();
66
+ const canonicalImplementationRoot = path.join(canonical.source.root, "actions");
67
+ const canonicalIntegrity = await actionSourceIntegrity(
68
+ canonical.file,
69
+ "canonical",
70
+ canonicalImplementationRoot
71
+ );
72
+ const actionSources = describeManifestSources(base, {
73
+ name: "metamask",
74
+ tier: "canonical",
75
+ manifestPath: canonical.file,
76
+ implementationRoot: canonicalImplementationRoot,
77
+ trust: "trusted",
78
+ ...canonicalIntegrity
79
+ });
80
+ for (const entry of manifests) {
81
+ const sourceName = entry.source.name ?? path.basename(entry.source.root);
82
+ const tier = sourceName === "metamask" ? "canonical" : sourceName === "personal" ? "personal" : "team";
83
+ const implementationRoot = path.join(entry.source.root, "actions");
84
+ const integrity = await actionSourceIntegrity(entry.file, tier, implementationRoot);
85
+ const sourceInfo = {
86
+ name: sourceName,
87
+ tier,
88
+ manifestPath: entry.file,
89
+ implementationRoot,
90
+ trust: entry.source.provenance?.trust ?? (tier === "canonical" ? "trusted" : "unknown"),
91
+ ...integrity
92
+ };
93
+ const manifestRecord = recordOf(entry.manifest);
94
+ const metadata = recordOf(manifestRecord.action_metadata);
95
+ for (const action of customActions(manifestRecord.custom_actions)) {
96
+ if (official.has(action.name)) {
97
+ throw new Error(`Library action ${action.name} from ${entry.file} conflicts with an official action.`);
98
+ }
99
+ if (custom.has(action.name)) {
100
+ const existing = actionSources.get(action.name);
101
+ if (existing && existing.name !== sourceName) {
102
+ existing.shadows = [...existing.shadows ?? [], sourceName];
103
+ }
104
+ continue;
105
+ }
106
+ custom.set(action.name, action.value);
107
+ customMetadata.set(action.name, metadata[action.name]);
108
+ actionSources.set(action.name, { ...sourceInfo });
109
+ }
110
+ for (const binding of objectArray(manifestRecord.native_bindings)) {
111
+ const action = typeof binding.action === "string" ? binding.action : void 0;
112
+ if (action && !bindings.has(action)) bindings.set(action, binding);
113
+ }
114
+ }
115
+ const baseRecord = recordOf(base);
116
+ baseRecord.custom_actions = [...custom.values()];
117
+ const baseMetadata = recordOf(baseRecord.action_metadata);
118
+ for (const [name, metadata] of customMetadata) {
119
+ if (metadata !== void 0) baseMetadata[name] = metadata;
120
+ }
121
+ baseRecord.action_metadata = baseMetadata;
122
+ baseRecord.native_bindings = [...bindings.values()];
123
+ return { manifest: withMetaMaskExecutionCapabilities(base), actionSources };
124
+ }
125
+ function defaultImplementationRoot(selectedManifestPath) {
126
+ const manifestDirectory = path.dirname(selectedManifestPath);
127
+ return path.basename(manifestDirectory) === "manifests" ? path.join(path.dirname(manifestDirectory), "actions") : path.join(manifestDirectory, "actions");
128
+ }
129
+ function withMetaMaskExecutionCapabilities(manifest) {
130
+ const result = cloneManifest(manifest);
131
+ const record = recordOf(result);
132
+ if (!Array.isArray(record.custom_actions)) return result;
133
+ record.custom_actions = record.custom_actions.map((entry) => {
134
+ const name = typeof entry === "string" ? entry : recordOf(entry).name;
135
+ if (typeof name !== "string") return entry;
136
+ const capabilities = metaMaskActionExecutionCapabilities(name);
137
+ if (capabilities.length === 0) return entry;
138
+ const declaredValue = typeof entry === "string" ? void 0 : recordOf(entry).execution_capabilities;
139
+ const declared = Array.isArray(declaredValue) ? declaredValue.filter(
140
+ (capability) => typeof capability === "string"
141
+ ) : [];
142
+ const executionCapabilities = [.../* @__PURE__ */ new Set([...declared, ...capabilities])];
143
+ return typeof entry === "string" ? { name, execution_capabilities: executionCapabilities } : { ...recordOf(entry), execution_capabilities: executionCapabilities };
144
+ });
145
+ return result;
146
+ }
18
147
  async function validateManifest(manifest) {
19
148
  const { validateRecipeActionManifestDocument } = await importRecipeProtocol();
20
149
  const result = validateRecipeActionManifestDocument(manifest);
@@ -28,10 +157,41 @@ async function validateManifest(manifest) {
28
157
  function asActionManifest(value) {
29
158
  return value;
30
159
  }
160
+ function cloneManifest(value) {
161
+ return JSON.parse(JSON.stringify(value));
162
+ }
163
+ function recordOf(value) {
164
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
165
+ }
166
+ function stringArray(value) {
167
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
168
+ }
169
+ function objectArray(value) {
170
+ return Array.isArray(value) ? value.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
171
+ }
172
+ function customActions(value) {
173
+ if (!Array.isArray(value)) return [];
174
+ return value.flatMap((entry) => {
175
+ if (typeof entry === "string") return [{ name: entry, value: entry }];
176
+ const record = recordOf(entry);
177
+ return typeof record.name === "string" ? [{ name: record.name, value: entry }] : [];
178
+ });
179
+ }
180
+ function describeManifestSources(manifest, customSource) {
181
+ const record = recordOf(manifest);
182
+ const sources = /* @__PURE__ */ new Map();
183
+ for (const name of stringArray(record.supported_official_actions)) {
184
+ sources.set(name, { name: "official", tier: "official", manifestPath: customSource.manifestPath });
185
+ }
186
+ for (const action of customActions(record.custom_actions)) sources.set(action.name, { ...customSource });
187
+ return sources;
188
+ }
31
189
  export {
32
190
  loadActionManifest,
33
191
  loadMetaMaskCoreActionManifest,
34
192
  loadMetaMaskExtensionActionManifest,
35
193
  loadMetaMaskMobileActionManifest,
36
- validateManifest
194
+ resolveActionManifest,
195
+ validateManifest,
196
+ withMetaMaskExecutionCapabilities
37
197
  };
@@ -4,10 +4,17 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { color } from "./cli-color.js";
7
+ import { withCommandJournal } from "./command-journal.js";
8
+ import { JsonStreamWriter } from "./json-stream.js";
7
9
  import { handleUpdate, maybeNudge } from "./commands/update.js";
8
10
  import { handleCallHelp } from "./commands/call.js";
9
11
  import { getAdapterSurface } from "./adapters/surface.js";
10
12
  import { detectAdapter } from "./harness.js";
13
+ import {
14
+ PUBLIC_COMMAND_CONTRACTS,
15
+ publicCommandNames,
16
+ validatePublicInvocation
17
+ } from "./command-contract.js";
11
18
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
19
  globalThis.__MM_HARNESS_WRAPPER__ = true;
13
20
  const { main: recipeMain } = await import("./cli.js");
@@ -69,13 +76,16 @@ Example:
69
76
  },
70
77
  {
71
78
  name: "actions",
72
- summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
79
+ summary: "Discover typed single operations and their fields.",
73
80
  example: "mm-harness actions --adapter mobile",
74
- helpText: `mm-harness actions [flags]
81
+ helpText: `mm-harness actions [query] [flags]
75
82
 
76
- List the action vocabulary + field schemas for the checkout adapter.
83
+ Discover typed single operations and their field schemas for this checkout.
77
84
 
85
+ query Search names, categories, fields, and descriptions (typo-tolerant)
78
86
  --action <name> Describe one action; fuzzy-resolves like call (short or full name)
87
+ --categories List compact action categories and counts
88
+ --category <name> List only one category (for example ui, wallet, or perps)
79
89
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
80
90
  --target <path> Checkout path (default: cwd)
81
91
  --raw Dump raw action registry JSON
@@ -83,6 +93,9 @@ Example:
83
93
 
84
94
  Example:
85
95
  mm-harness actions --adapter mobile
96
+ mm-harness actions positions --adapter mobile
97
+ mm-harness actions --adapter mobile --categories --json
98
+ mm-harness actions --adapter mobile --category ui --json
86
99
  mm-harness actions --adapter mobile --action assert_orders
87
100
  mm-harness actions --adapter extension --raw`
88
101
  },
@@ -121,7 +134,7 @@ Example:
121
134
  Actions differ per adapter \u2014 list this checkout's with:
122
135
  mm-harness actions.
123
136
 
124
- --list List everything invocable for the adapter (actions + flows); no <action> needed
137
+ --list List actions accepted by call for this adapter; no <action> needed
125
138
  --arg k=v Action field value (repeatable; equivalent to key=value shorthand)
126
139
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
127
140
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
@@ -137,46 +150,61 @@ Example (real actions; run mm-harness actions for this checkout's full set):
137
150
  mm-harness call navigate page=perps --adapter mobile
138
151
  mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)`
139
152
  },
140
- {
141
- name: "flows",
142
- summary: "Browse the reusable flow library (bare = list; `flows promote` publishes a flow up a tier). Resolves across libraries \u2014 personal > team > canonical, highest tier wins.",
143
- example: "mm-harness flows",
144
- helpText: `mm-harness flows [flags]
145
-
146
- Browse the reusable flow library. Flows resolve across libraries by precedence
147
- (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
148
- Flow resolution is adapter-global, so there is no --adapter flag here.
149
-
150
- --library <name=path> Add/override a library source (repeatable)
151
- --target <path> Checkout path (default: cwd)
152
- --json Machine-readable output
153
-
154
- Example:
155
- mm-harness flows`
156
- },
157
153
  {
158
154
  name: "run",
159
- summary: "Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.",
155
+ summary: "Execute a complete proof and write its evidence.",
160
156
  example: "mm-harness run recipe.json",
161
- helpText: `mm-harness run <recipe.json> [flags]
157
+ helpText: `mm-harness run <recipe|recipe.json> [key=value ...] [flags]
162
158
 
163
- Validate + run a recipe and write evidence (summary / trace / artifacts).
159
+ Validate and execute a complete proof, then write summary, trace, diagnostics,
160
+ and artifacts. Inside a checkout, the adapter, target, ports, and artifact
161
+ directory are normally detected automatically.
164
162
 
165
- --list List everything invocable for the adapter (actions + flows); no <recipe> needed
163
+ Discovery:
164
+ --list List complete recipes accepted by run for this adapter; no <recipe> needed
165
+ --describe Show one recipe's parameters, composed actions/recipes, and runnable command
166
166
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
167
+
168
+ Common overrides:
167
169
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
168
170
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
171
+ --platform <ios|android> Mobile platform preference; use --adapter for the product adapter
169
172
  --target <path> Checkout path (default: cwd)
170
- --artifacts-dir <dir> Where to write evidence (required unless --plan)
171
- --action-manifest <path> Override the action manifest
173
+ --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
172
174
  --library <name=path> Add/override a recipe-library source (repeatable)
173
175
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
174
176
  --json Machine-readable output
177
+ --json-stream Line-flushed JSONL progress + terminal event
175
178
  --record-video=full-run Record a video of the run
176
179
 
180
+ Advanced integrations:
181
+ runtime: --cdp-port, --watcher-port/--metro-port, --runtime-dir, --slot,
182
+ --validation-runtime-dir, --launch-existing-dist
183
+ managed trust boundary (not needed for normal runs; Farmslot supplies it):
184
+ --source-trust, --source-kind, --source-name, --source-digest,
185
+ --approve-plan (binds approval to the exact reviewed plan)
186
+ schema: --action-manifest
187
+
177
188
  Example:
178
- mm-harness run recipe.json --plan --adapter mobile
179
- mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
189
+ mm-harness run wallet.smoke --describe
190
+ mm-harness run perps.clean-market-testnet market=BTC --plan
191
+ mm-harness run recipe.json market=ETH --adapter extension`
192
+ },
193
+ {
194
+ name: "last",
195
+ summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
196
+ example: "mm-harness last --json",
197
+ helpText: `mm-harness last [flags]
198
+
199
+ Read the atomic per-checkout resumability journal. Discovery commands do not
200
+ replace it, and an interrupted process remains recorded as verdict=running.
201
+
202
+ --target <path> Checkout path (default: cwd)
203
+ --runtime-dir <dir> Runtime dir containing last-command.json
204
+ --json Machine-readable envelope
205
+
206
+ Example:
207
+ mm-harness last --json`
180
208
  },
181
209
  {
182
210
  name: "doctor",
@@ -375,6 +403,7 @@ Example:
375
403
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
376
404
  --target <path> Checkout path (default: cwd)
377
405
  --json Machine-readable summary (recovered[] / mutations[] / phase)
406
+ --json-stream Line-flushed JSONL progress + terminal event
378
407
 
379
408
  Example:
380
409
  mm-harness launch ios
@@ -393,8 +422,8 @@ Example:
393
422
  --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
394
423
  --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
395
424
  --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
396
- --source <label> Log source per adapter \u2014 mobile: metro|app (default metro);
397
- extension: webpack|watcher|rebuild|app (default webpack).
425
+ --source <label> Log source per adapter \u2014 mobile: metro|app;
426
+ extension: extension|dapp|webpack|watcher|rebuild.
398
427
  Core is headless (teaching error).
399
428
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
400
429
  --target <path> Checkout path (default: cwd)
@@ -448,9 +477,10 @@ Example:
448
477
  name: "fixtures",
449
478
  summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
450
479
  example: "mm-harness fixtures set",
451
- helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
480
+ helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
452
481
 
453
482
  Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
483
+ With no action, show the current fixture status and safe next command.
454
484
  init Create it from --from <path>, or explicitly choose --dev for a
455
485
  disposable public test wallet that must never hold real funds.
456
486
  sync Refresh the wallet fixture files on the target.
@@ -473,6 +503,7 @@ Example:
473
503
  --cdp-port <port> finalize: CDP port of the running extension
474
504
  --extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
475
505
  --extension-id-file <path> finalize: optional file to read/write the resolved extension id
506
+ --action-manifest <path> Extension set: override the wallet action manifest
476
507
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
477
508
  --target <path> Checkout path (default: cwd)
478
509
  --device <udid|serial|name> Mobile only: target this device for sync/set
@@ -487,21 +518,28 @@ Example:
487
518
  mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
488
519
  }
489
520
  ];
490
- const RETIRED = [
491
- {
492
- name: "live",
493
- message: `mm-harness live is retired (exit 2).
494
-
495
- Replacement: mm-harness launch --verify (install overlay \u2192 launch \u2192 CDP/bridge poll \u2192 smoke verify).`
496
- },
497
- {
498
- name: "manifest",
499
- message: `mm-harness manifest is retired (exit 2).
500
-
501
- Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
502
- identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
521
+ const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
522
+ REAL.map((command) => [command.name, command.example])
523
+ );
524
+ function assertPublicContractMatchesSurface() {
525
+ const registered = REAL.map((command) => command.name).sort();
526
+ const contracted = publicCommandNames().sort();
527
+ if (registered.join("\n") !== contracted.join("\n")) {
528
+ throw new Error(
529
+ `public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
530
+ );
503
531
  }
504
- ];
532
+ for (const command of REAL) {
533
+ const registeredAliases = [...command.aliases ?? []].sort();
534
+ const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
535
+ if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
536
+ throw new Error(
537
+ `public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
538
+ );
539
+ }
540
+ }
541
+ }
542
+ assertPublicContractMatchesSurface();
505
543
  const HELP_GROUPS = [
506
544
  {
507
545
  title: "DAILY LOOP",
@@ -510,13 +548,13 @@ const HELP_GROUPS = [
510
548
  },
511
549
  {
512
550
  title: "DISCOVER",
513
- blurb: "compose recipes from the action vocabulary + flow library (--json is the agent-primary form)",
514
- commands: ["actions", "call", "flows"]
551
+ blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
552
+ commands: ["actions", "call"]
515
553
  },
516
554
  {
517
555
  title: "PROVE",
518
556
  blurb: "run recipes and inspect readiness",
519
- commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
557
+ commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
520
558
  },
521
559
  {
522
560
  title: "RUNTIME OVERLAY",
@@ -604,7 +642,7 @@ function groupedHelp() {
604
642
  lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
605
643
  lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
606
644
  lines.push("");
607
- lines.push("See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).");
645
+ lines.push("See README.md for the workflow and docs/RECIPES.md for proof authoring.");
608
646
  return `${lines.join("\n")}
609
647
  `;
610
648
  }
@@ -616,11 +654,6 @@ async function delegate(argv) {
616
654
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
617
655
  }
618
656
  }
619
- function translateActionsRaw(argv) {
620
- const rest = argv.slice(1).filter((arg) => arg !== "--raw");
621
- const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
622
- return ["manifest", ...withJson];
623
- }
624
657
  const pkgVersion = (() => {
625
658
  try {
626
659
  return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
@@ -638,8 +671,7 @@ for (const command of REAL) {
638
671
  if (command.name === "update") {
639
672
  process.exit(await handleUpdate(rawArgv.slice(1)));
640
673
  }
641
- const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
642
- process.exit(await delegate(argv));
674
+ process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
643
675
  });
644
676
  }
645
677
  const HIDDEN = [
@@ -661,24 +693,9 @@ program.command("completions").description("Install/print bundled shell tab-comp
661
693
  const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
662
694
  process.exit(result.status ?? 1);
663
695
  });
664
- for (const retired of RETIRED) {
665
- program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
666
- if (retired.name === "live") {
667
- const adapterIdx = rawArgv.indexOf("--adapter");
668
- const aliasIdx = rawArgv.indexOf("-a");
669
- const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
670
- if (adapterVal === "mobile" || adapterVal === "android") {
671
- process.exit(await delegate(rawArgv));
672
- return;
673
- }
674
- }
675
- console.error(retired.message);
676
- process.exit(2);
677
- });
678
- }
679
696
  const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
680
697
  if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
681
- setImmediate(() => void maybeNudge());
698
+ maybeNudge();
682
699
  }
683
700
  function hasPassthroughHelp(argv) {
684
701
  const divider = argv.indexOf("--");
@@ -692,10 +709,66 @@ function isCallActionHelp(argv) {
692
709
  const scope = divider === -1 ? argv : argv.slice(0, divider);
693
710
  return scope.includes("--help") || scope.includes("-h");
694
711
  }
712
+ function jsonRequestedBeforePassthrough(argv) {
713
+ const divider = argv.indexOf("--");
714
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
715
+ return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
716
+ }
717
+ function jsonStreamRequestedBeforePassthrough(argv) {
718
+ const divider = argv.indexOf("--");
719
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
720
+ return scope.includes("--json-stream");
721
+ }
722
+ function emitUsageError(error, json, jsonStream) {
723
+ if (jsonStream) {
724
+ const stream = new JsonStreamWriter(error.command, true);
725
+ stream.error({ code: error.code, message: error.message, userAction: error.userAction });
726
+ stream.complete("fail", 2);
727
+ } else if (json) {
728
+ process.stdout.write(
729
+ `${JSON.stringify(
730
+ {
731
+ schemaVersion: 1,
732
+ command: error.command,
733
+ status: "fail",
734
+ error: {
735
+ code: error.code,
736
+ message: error.message,
737
+ userAction: error.userAction
738
+ },
739
+ exitCode: 2
740
+ },
741
+ null,
742
+ 2
743
+ )}
744
+ `
745
+ );
746
+ } else {
747
+ const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
748
+ process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
749
+ Next: ${error.userAction}
750
+ `);
751
+ }
752
+ process.exit(2);
753
+ }
695
754
  if (rawArgv.length === 0) {
696
755
  process.stdout.write(groupedHelp());
697
756
  process.exit(0);
698
757
  }
758
+ const preflightBypass = /* @__PURE__ */ new Set([
759
+ ...HIDDEN,
760
+ "completions"
761
+ ]);
762
+ if (!preflightBypass.has(rawArgv[0] ?? "")) {
763
+ const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
764
+ if (usageError) {
765
+ emitUsageError(
766
+ usageError,
767
+ jsonRequestedBeforePassthrough(rawArgv),
768
+ jsonStreamRequestedBeforePassthrough(rawArgv)
769
+ );
770
+ }
771
+ }
699
772
  if (hasPassthroughHelp(rawArgv)) {
700
773
  process.exit(await delegate(rawArgv));
701
774
  }
package/dist/paths.js CHANGED
@@ -47,10 +47,7 @@ function recipeWatchLogCandidates() {
47
47
  }
48
48
  function resolveLocalProtocolRoot() {
49
49
  const candidates = [
50
- // Orchestrator-neutral name is primary; FARMSLOT_ROOT stays a back-compat alias
51
- // for one release (the installer injects METAMASK_RUNNER_PROTOCOL_ROOT).
52
50
  process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
53
- process.env.FARMSLOT_ROOT,
54
51
  readConfiguredProtocolRoot(),
55
52
  findProtocolRoot(runnerDir),
56
53
  findProtocolRoot(process.cwd())
@@ -62,7 +59,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
62
59
  const root = resolveLocalProtocolRoot();
63
60
  if (!root) {
64
61
  throw new Error(
65
- `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT (or the legacy FARMSLOT_ROOT), or create .farmslot-root for this dev-only path.`
62
+ `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
66
63
  );
67
64
  }
68
65
  return root;
@@ -152,7 +149,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
152
149
  const root = resolveLocalProtocolRoot();
153
150
  if (!root) {
154
151
  throw new Error(
155
- `${packageName} is not installed. Install @farmslot/* packages normally, or set FARMSLOT_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
152
+ `${packageName} is not installed. Install @farmslot/* packages normally, or set METAMASK_RUNNER_PROTOCOL_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
156
153
  );
157
154
  }
158
155
  return import(pathToFileURL(path.join(root, localSourceEntry)).href);