@deeeed/metamask-harness 0.17.5 → 0.18.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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
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)
@@ -139,11 +152,11 @@ Example (real actions; run mm-harness actions for this checkout's full set):
139
152
  },
140
153
  {
141
154
  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.",
155
+ summary: "Browse reusable parameterized action sequences.",
143
156
  example: "mm-harness flows",
144
- helpText: `mm-harness flows [flags]
157
+ helpText: `mm-harness flows [list|describe <ref>|promote] [flags]
145
158
 
146
- Browse the reusable flow library. Flows resolve across libraries by precedence
159
+ Browse reusable parameterized action sequences. Flows resolve by precedence
147
160
  (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
148
161
  Flow resolution is adapter-global, so there is no --adapter flag here.
149
162
 
@@ -152,31 +165,55 @@ Example (real actions; run mm-harness actions for this checkout's full set):
152
165
  --json Machine-readable output
153
166
 
154
167
  Example:
155
- mm-harness flows`
168
+ mm-harness flows
169
+ mm-harness flows describe perps.clean_market_testnet
170
+ mm-harness flows describe perps.clean_market_testnet --json`
156
171
  },
157
172
  {
158
173
  name: "run",
159
- summary: "Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.",
174
+ summary: "Execute a complete proof and write its evidence.",
160
175
  example: "mm-harness run recipe.json",
161
176
  helpText: `mm-harness run <recipe.json> [flags]
162
177
 
163
- Validate + run a recipe and write evidence (summary / trace / artifacts).
178
+ Validate and execute a complete proof, then write summary, trace, diagnostics,
179
+ and artifacts. A recipe may use actions directly or call reusable flows.
164
180
 
165
- --list List everything invocable for the adapter (actions + flows); no <recipe> needed
181
+ --list List complete recipes accepted by run for this adapter; no <recipe> needed
182
+ --describe Show one recipe's metadata, declared inputs, composed actions/flows, and next command
166
183
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
167
184
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). Without it, >1 connected mobile device fails fast and lists them.
185
+ --cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
186
+ --launch-existing-dist Extension: reuse/launch the existing dist without rebuilding
168
187
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
169
188
  --target <path> Checkout path (default: cwd)
170
- --artifacts-dir <dir> Where to write evidence (required unless --plan)
189
+ --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
171
190
  --action-manifest <path> Override the action manifest
172
191
  --library <name=path> Add/override a recipe-library source (repeatable)
173
192
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
174
193
  --json Machine-readable output
194
+ --json-stream Line-flushed JSONL progress + terminal event
175
195
  --record-video=full-run Record a video of the run
176
196
 
177
197
  Example:
198
+ mm-harness run wallet.smoke --describe
178
199
  mm-harness run recipe.json --plan --adapter mobile
179
- mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
200
+ mm-harness run recipe.json --adapter extension`
201
+ },
202
+ {
203
+ name: "last",
204
+ summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
205
+ example: "mm-harness last --json",
206
+ helpText: `mm-harness last [flags]
207
+
208
+ Read the atomic per-checkout resumability journal. Discovery commands do not
209
+ replace it, and an interrupted process remains recorded as verdict=running.
210
+
211
+ --target <path> Checkout path (default: cwd)
212
+ --runtime-dir <dir> Runtime dir containing last-command.json
213
+ --json Machine-readable envelope
214
+
215
+ Example:
216
+ mm-harness last --json`
180
217
  },
181
218
  {
182
219
  name: "doctor",
@@ -375,6 +412,7 @@ Example:
375
412
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
376
413
  --target <path> Checkout path (default: cwd)
377
414
  --json Machine-readable summary (recovered[] / mutations[] / phase)
415
+ --json-stream Line-flushed JSONL progress + terminal event
378
416
 
379
417
  Example:
380
418
  mm-harness launch ios
@@ -393,8 +431,8 @@ Example:
393
431
  --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
394
432
  --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
395
433
  --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).
434
+ --source <label> Log source per adapter \u2014 mobile: metro|app;
435
+ extension: extension|dapp|webpack|watcher|rebuild.
398
436
  Core is headless (teaching error).
399
437
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
400
438
  --target <path> Checkout path (default: cwd)
@@ -473,6 +511,7 @@ Example:
473
511
  --cdp-port <port> finalize: CDP port of the running extension
474
512
  --extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
475
513
  --extension-id-file <path> finalize: optional file to read/write the resolved extension id
514
+ --action-manifest <path> Extension set: override the wallet action manifest
476
515
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
477
516
  --target <path> Checkout path (default: cwd)
478
517
  --device <udid|serial|name> Mobile only: target this device for sync/set
@@ -487,21 +526,28 @@ Example:
487
526
  mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
488
527
  }
489
528
  ];
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.`
529
+ const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
530
+ REAL.map((command) => [command.name, command.example])
531
+ );
532
+ function assertPublicContractMatchesSurface() {
533
+ const registered = REAL.map((command) => command.name).sort();
534
+ const contracted = publicCommandNames().sort();
535
+ if (registered.join("\n") !== contracted.join("\n")) {
536
+ throw new Error(
537
+ `public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
538
+ );
503
539
  }
504
- ];
540
+ for (const command of REAL) {
541
+ const registeredAliases = [...command.aliases ?? []].sort();
542
+ const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
543
+ if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
544
+ throw new Error(
545
+ `public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
546
+ );
547
+ }
548
+ }
549
+ }
550
+ assertPublicContractMatchesSurface();
505
551
  const HELP_GROUPS = [
506
552
  {
507
553
  title: "DAILY LOOP",
@@ -516,7 +562,7 @@ const HELP_GROUPS = [
516
562
  {
517
563
  title: "PROVE",
518
564
  blurb: "run recipes and inspect readiness",
519
- commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
565
+ commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
520
566
  },
521
567
  {
522
568
  title: "RUNTIME OVERLAY",
@@ -604,7 +650,7 @@ function groupedHelp() {
604
650
  lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
605
651
  lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
606
652
  lines.push("");
607
- lines.push("See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).");
653
+ lines.push("See README.md for the workflow and docs/RECIPES.md for proof authoring.");
608
654
  return `${lines.join("\n")}
609
655
  `;
610
656
  }
@@ -616,11 +662,6 @@ async function delegate(argv) {
616
662
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
617
663
  }
618
664
  }
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
665
  const pkgVersion = (() => {
625
666
  try {
626
667
  return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
@@ -638,8 +679,7 @@ for (const command of REAL) {
638
679
  if (command.name === "update") {
639
680
  process.exit(await handleUpdate(rawArgv.slice(1)));
640
681
  }
641
- const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
642
- process.exit(await delegate(argv));
682
+ process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
643
683
  });
644
684
  }
645
685
  const HIDDEN = [
@@ -661,24 +701,9 @@ program.command("completions").description("Install/print bundled shell tab-comp
661
701
  const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
662
702
  process.exit(result.status ?? 1);
663
703
  });
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
704
  const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
680
705
  if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
681
- setImmediate(() => void maybeNudge());
706
+ maybeNudge();
682
707
  }
683
708
  function hasPassthroughHelp(argv) {
684
709
  const divider = argv.indexOf("--");
@@ -692,10 +717,66 @@ function isCallActionHelp(argv) {
692
717
  const scope = divider === -1 ? argv : argv.slice(0, divider);
693
718
  return scope.includes("--help") || scope.includes("-h");
694
719
  }
720
+ function jsonRequestedBeforePassthrough(argv) {
721
+ const divider = argv.indexOf("--");
722
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
723
+ return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
724
+ }
725
+ function jsonStreamRequestedBeforePassthrough(argv) {
726
+ const divider = argv.indexOf("--");
727
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
728
+ return scope.includes("--json-stream");
729
+ }
730
+ function emitUsageError(error, json, jsonStream) {
731
+ if (jsonStream) {
732
+ const stream = new JsonStreamWriter(error.command, true);
733
+ stream.error({ code: error.code, message: error.message, userAction: error.userAction });
734
+ stream.complete("fail", 2);
735
+ } else if (json) {
736
+ process.stdout.write(
737
+ `${JSON.stringify(
738
+ {
739
+ schemaVersion: 1,
740
+ command: error.command,
741
+ status: "fail",
742
+ error: {
743
+ code: error.code,
744
+ message: error.message,
745
+ userAction: error.userAction
746
+ },
747
+ exitCode: 2
748
+ },
749
+ null,
750
+ 2
751
+ )}
752
+ `
753
+ );
754
+ } else {
755
+ const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
756
+ process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
757
+ Next: ${error.userAction}
758
+ `);
759
+ }
760
+ process.exit(2);
761
+ }
695
762
  if (rawArgv.length === 0) {
696
763
  process.stdout.write(groupedHelp());
697
764
  process.exit(0);
698
765
  }
766
+ const preflightBypass = /* @__PURE__ */ new Set([
767
+ ...HIDDEN,
768
+ "completions"
769
+ ]);
770
+ if (!preflightBypass.has(rawArgv[0] ?? "")) {
771
+ const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
772
+ if (usageError) {
773
+ emitUsageError(
774
+ usageError,
775
+ jsonRequestedBeforePassthrough(rawArgv),
776
+ jsonStreamRequestedBeforePassthrough(rawArgv)
777
+ );
778
+ }
779
+ }
699
780
  if (hasPassthroughHelp(rawArgv)) {
700
781
  process.exit(await delegate(rawArgv));
701
782
  }
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);