@deeeed/metamask-harness 0.17.3 → 0.17.5

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 (114) hide show
  1. package/CHANGELOG.md +6 -96
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -51
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -49
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -105
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -35
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -4,17 +4,10 @@ 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";
9
7
  import { handleUpdate, maybeNudge } from "./commands/update.js";
10
8
  import { handleCallHelp } from "./commands/call.js";
11
9
  import { getAdapterSurface } from "./adapters/surface.js";
12
10
  import { detectAdapter } from "./harness.js";
13
- import {
14
- PUBLIC_COMMAND_CONTRACTS,
15
- publicCommandNames,
16
- validatePublicInvocation
17
- } from "./command-contract.js";
18
11
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19
12
  globalThis.__MM_HARNESS_WRAPPER__ = true;
20
13
  const { main: recipeMain } = await import("./cli.js");
@@ -76,16 +69,13 @@ Example:
76
69
  },
77
70
  {
78
71
  name: "actions",
79
- summary: "Discover typed single operations and their fields.",
72
+ summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
80
73
  example: "mm-harness actions --adapter mobile",
81
- helpText: `mm-harness actions [query] [flags]
74
+ helpText: `mm-harness actions [flags]
82
75
 
83
- Discover typed single operations and their field schemas for this checkout.
76
+ List the action vocabulary + field schemas for the checkout adapter.
84
77
 
85
- query Search names, categories, fields, and descriptions (typo-tolerant)
86
78
  --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)
89
79
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
90
80
  --target <path> Checkout path (default: cwd)
91
81
  --raw Dump raw action registry JSON
@@ -93,9 +83,6 @@ Example:
93
83
 
94
84
  Example:
95
85
  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
99
86
  mm-harness actions --adapter mobile --action assert_orders
100
87
  mm-harness actions --adapter extension --raw`
101
88
  },
@@ -152,11 +139,11 @@ Example (real actions; run mm-harness actions for this checkout's full set):
152
139
  },
153
140
  {
154
141
  name: "flows",
155
- summary: "Browse reusable parameterized action sequences.",
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.",
156
143
  example: "mm-harness flows",
157
144
  helpText: `mm-harness flows [flags]
158
145
 
159
- Browse reusable parameterized action sequences. Flows resolve by precedence
146
+ Browse the reusable flow library. Flows resolve across libraries by precedence
160
147
  (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
161
148
  Flow resolution is adapter-global, so there is no --adapter flag here.
162
149
 
@@ -169,12 +156,11 @@ Example:
169
156
  },
170
157
  {
171
158
  name: "run",
172
- summary: "Execute a complete proof and write its evidence.",
159
+ summary: "Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.",
173
160
  example: "mm-harness run recipe.json",
174
161
  helpText: `mm-harness run <recipe.json> [flags]
175
162
 
176
- Validate and execute a complete proof, then write summary, trace, diagnostics,
177
- and artifacts. A recipe may use actions directly or call reusable flows.
163
+ Validate + run a recipe and write evidence (summary / trace / artifacts).
178
164
 
179
165
  --list List everything invocable for the adapter (actions + flows); no <recipe> needed
180
166
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
@@ -186,29 +172,12 @@ Example:
186
172
  --library <name=path> Add/override a recipe-library source (repeatable)
187
173
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
188
174
  --json Machine-readable output
189
- --json-stream Line-flushed JSONL progress + terminal event
190
175
  --record-video=full-run Record a video of the run
191
176
 
192
177
  Example:
193
178
  mm-harness run recipe.json --plan --adapter mobile
194
179
  mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
195
180
  },
196
- {
197
- name: "last",
198
- summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
199
- example: "mm-harness last --json",
200
- helpText: `mm-harness last [flags]
201
-
202
- Read the atomic per-checkout resumability journal. Discovery commands do not
203
- replace it, and an interrupted process remains recorded as verdict=running.
204
-
205
- --target <path> Checkout path (default: cwd)
206
- --runtime-dir <dir> Runtime dir containing last-command.json
207
- --json Machine-readable envelope
208
-
209
- Example:
210
- mm-harness last --json`
211
- },
212
181
  {
213
182
  name: "doctor",
214
183
  summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
@@ -406,7 +375,6 @@ Example:
406
375
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
407
376
  --target <path> Checkout path (default: cwd)
408
377
  --json Machine-readable summary (recovered[] / mutations[] / phase)
409
- --json-stream Line-flushed JSONL progress + terminal event
410
378
 
411
379
  Example:
412
380
  mm-harness launch ios
@@ -425,8 +393,8 @@ Example:
425
393
  --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
426
394
  --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
427
395
  --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
428
- --source <label> Log source per adapter \u2014 mobile: metro|app;
429
- extension: extension|dapp|webpack|watcher|rebuild.
396
+ --source <label> Log source per adapter \u2014 mobile: metro|app (default metro);
397
+ extension: webpack|watcher|rebuild|app (default webpack).
430
398
  Core is headless (teaching error).
431
399
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
432
400
  --target <path> Checkout path (default: cwd)
@@ -505,7 +473,6 @@ Example:
505
473
  --cdp-port <port> finalize: CDP port of the running extension
506
474
  --extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
507
475
  --extension-id-file <path> finalize: optional file to read/write the resolved extension id
508
- --action-manifest <path> Extension set: override the wallet action manifest
509
476
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
510
477
  --target <path> Checkout path (default: cwd)
511
478
  --device <udid|serial|name> Mobile only: target this device for sync/set
@@ -520,28 +487,21 @@ Example:
520
487
  mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
521
488
  }
522
489
  ];
523
- const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
524
- REAL.map((command) => [command.name, command.example])
525
- );
526
- function assertPublicContractMatchesSurface() {
527
- const registered = REAL.map((command) => command.name).sort();
528
- const contracted = publicCommandNames().sort();
529
- if (registered.join("\n") !== contracted.join("\n")) {
530
- throw new Error(
531
- `public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
532
- );
533
- }
534
- for (const command of REAL) {
535
- const registeredAliases = [...command.aliases ?? []].sort();
536
- const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
537
- if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
538
- throw new Error(
539
- `public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
540
- );
541
- }
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.`
542
503
  }
543
- }
544
- assertPublicContractMatchesSurface();
504
+ ];
545
505
  const HELP_GROUPS = [
546
506
  {
547
507
  title: "DAILY LOOP",
@@ -556,7 +516,7 @@ const HELP_GROUPS = [
556
516
  {
557
517
  title: "PROVE",
558
518
  blurb: "run recipes and inspect readiness",
559
- commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
519
+ commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
560
520
  },
561
521
  {
562
522
  title: "RUNTIME OVERLAY",
@@ -644,7 +604,7 @@ function groupedHelp() {
644
604
  lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
645
605
  lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
646
606
  lines.push("");
647
- lines.push("See README.md for the workflow and docs/CHEATSHEET.md for examples.");
607
+ lines.push("See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).");
648
608
  return `${lines.join("\n")}
649
609
  `;
650
610
  }
@@ -656,6 +616,11 @@ async function delegate(argv) {
656
616
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
657
617
  }
658
618
  }
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
+ }
659
624
  const pkgVersion = (() => {
660
625
  try {
661
626
  return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
@@ -673,7 +638,8 @@ for (const command of REAL) {
673
638
  if (command.name === "update") {
674
639
  process.exit(await handleUpdate(rawArgv.slice(1)));
675
640
  }
676
- process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
641
+ const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
642
+ process.exit(await delegate(argv));
677
643
  });
678
644
  }
679
645
  const HIDDEN = [
@@ -695,9 +661,24 @@ program.command("completions").description("Install/print bundled shell tab-comp
695
661
  const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
696
662
  process.exit(result.status ?? 1);
697
663
  });
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
+ }
698
679
  const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
699
680
  if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
700
- maybeNudge();
681
+ setImmediate(() => void maybeNudge());
701
682
  }
702
683
  function hasPassthroughHelp(argv) {
703
684
  const divider = argv.indexOf("--");
@@ -711,66 +692,10 @@ function isCallActionHelp(argv) {
711
692
  const scope = divider === -1 ? argv : argv.slice(0, divider);
712
693
  return scope.includes("--help") || scope.includes("-h");
713
694
  }
714
- function jsonRequestedBeforePassthrough(argv) {
715
- const divider = argv.indexOf("--");
716
- const scope = divider === -1 ? argv : argv.slice(0, divider);
717
- return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
718
- }
719
- function jsonStreamRequestedBeforePassthrough(argv) {
720
- const divider = argv.indexOf("--");
721
- const scope = divider === -1 ? argv : argv.slice(0, divider);
722
- return scope.includes("--json-stream");
723
- }
724
- function emitUsageError(error, json, jsonStream) {
725
- if (jsonStream) {
726
- const stream = new JsonStreamWriter(error.command, true);
727
- stream.error({ code: error.code, message: error.message, userAction: error.userAction });
728
- stream.complete("fail", 2);
729
- } else if (json) {
730
- process.stdout.write(
731
- `${JSON.stringify(
732
- {
733
- schemaVersion: 1,
734
- command: error.command,
735
- status: "fail",
736
- error: {
737
- code: error.code,
738
- message: error.message,
739
- userAction: error.userAction
740
- },
741
- exitCode: 2
742
- },
743
- null,
744
- 2
745
- )}
746
- `
747
- );
748
- } else {
749
- const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
750
- process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
751
- Next: ${error.userAction}
752
- `);
753
- }
754
- process.exit(2);
755
- }
756
695
  if (rawArgv.length === 0) {
757
696
  process.stdout.write(groupedHelp());
758
697
  process.exit(0);
759
698
  }
760
- const preflightBypass = /* @__PURE__ */ new Set([
761
- ...HIDDEN,
762
- "completions"
763
- ]);
764
- if (!preflightBypass.has(rawArgv[0] ?? "")) {
765
- const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
766
- if (usageError) {
767
- emitUsageError(
768
- usageError,
769
- jsonRequestedBeforePassthrough(rawArgv),
770
- jsonStreamRequestedBeforePassthrough(rawArgv)
771
- );
772
- }
773
- }
774
699
  if (hasPassthroughHelp(rawArgv)) {
775
700
  process.exit(await delegate(rawArgv));
776
701
  }
package/dist/paths.js CHANGED
@@ -47,7 +47,10 @@ 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).
50
52
  process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
53
+ process.env.FARMSLOT_ROOT,
51
54
  readConfiguredProtocolRoot(),
52
55
  findProtocolRoot(runnerDir),
53
56
  findProtocolRoot(process.cwd())
@@ -59,7 +62,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
59
62
  const root = resolveLocalProtocolRoot();
60
63
  if (!root) {
61
64
  throw new Error(
62
- `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
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.`
63
66
  );
64
67
  }
65
68
  return root;
@@ -149,7 +152,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
149
152
  const root = resolveLocalProtocolRoot();
150
153
  if (!root) {
151
154
  throw new Error(
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.`
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.`
153
156
  );
154
157
  }
155
158
  return import(pathToFileURL(path.join(root, localSourceEntry)).href);
package/dist/runner.js CHANGED
@@ -58,7 +58,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
58
58
  const logger = options.quietStdout ? new console.Console(process.stderr) : console;
59
59
  return createRecipeRunner({
60
60
  actionManifest,
61
- adapters: withActionProgress([...core, ...ui, ...lifecycle, ...custom], options.onActionEvent),
61
+ adapters: [...core, ...ui, ...lifecycle, ...custom],
62
62
  logger,
63
63
  recording: {
64
64
  targetProvider: createMetaMaskRecordingTargetProvider(adapter)
@@ -76,36 +76,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
76
76
  }
77
77
  });
78
78
  }
79
- function withActionProgress(adapters, onActionEvent) {
80
- if (!onActionEvent) return adapters;
81
- return adapters.map((entry) => ({
82
- ...entry,
83
- async execute(node, context) {
84
- const event = { nodeId: context.nodeId, action: entry.action };
85
- const reportProgress = !isAutomaticHudProgress(entry.action, node, context.nodeId);
86
- if (reportProgress) onActionEvent({ ...event, status: "running" });
87
- try {
88
- const result = await entry.execute(node, context);
89
- if (reportProgress) {
90
- onActionEvent({ ...event, status: result.status === "fail" ? "failed" : "passed" });
91
- }
92
- return result;
93
- } catch (error) {
94
- if (reportProgress) onActionEvent({ ...event, status: "failed" });
95
- throw error;
96
- }
97
- }
98
- }));
99
- }
100
- function isAutomaticHudProgress(action, node, nodeId) {
101
- if (action !== "app.hud" || node === null || typeof node !== "object" || Array.isArray(node)) {
102
- return false;
103
- }
104
- const payload = node;
105
- if (typeof payload.action_name === "string") return true;
106
- const progress = payload.progress;
107
- return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
108
- }
109
79
  function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
110
80
  if (adapter !== "mobile") return lifecycle;
111
81
  return lifecycle.map((entry) => ({
@@ -113,11 +83,7 @@ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
113
83
  async execute(node, context) {
114
84
  const command = node.command ?? node.event ?? node.state;
115
85
  const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
116
- const source = reloadsSource ? mobileSourceFingerprint(context.projectRoot) : void 0;
117
- if (source?.status === "unavailable") {
118
- throw new Error(`${source.error.message} Next: ${source.error.userAction}`);
119
- }
120
- const fingerprint = source?.fingerprint;
86
+ const fingerprint = reloadsSource ? mobileSourceFingerprint(context.projectRoot) : void 0;
121
87
  const result = await entry.execute(node, context);
122
88
  if (fingerprint !== void 0 && !recordMobileSourceBaseline(context.projectRoot, fingerprint)) {
123
89
  throw new Error(
@@ -150,6 +116,5 @@ export {
150
116
  createMetaMaskExtensionRunner,
151
117
  createMetaMaskMobileRunner,
152
118
  createMetaMaskRunner,
153
- isAutomaticHudProgress,
154
119
  mobileSourceAwareLifecycleAdapters
155
120
  };
@@ -53,32 +53,6 @@ function readExistingContext(contextPath, repoRoot, runtimeDir, adapter) {
53
53
  return { context, existed: true };
54
54
  }
55
55
  function validExistingContext(context, repoRoot, runtimeDir, adapter) {
56
- const allowedFields = /* @__PURE__ */ new Set([
57
- "schemaVersion",
58
- "repoRoot",
59
- "project",
60
- "slotId",
61
- "machine",
62
- "runtimeOwner",
63
- "strict",
64
- "platform",
65
- "runtimeDir",
66
- "gitDirty",
67
- "simulator",
68
- "adbSerial",
69
- "distDir",
70
- "extensionId",
71
- "gitHead",
72
- "gitBranch",
73
- "gitCommit",
74
- "createdAt",
75
- "session",
76
- "cdpPort",
77
- "watcherPort",
78
- "metroPort",
79
- "devServerPort"
80
- ]);
81
- if (Object.keys(context).some((field) => !allowedFields.has(field))) return false;
82
56
  if (context.schemaVersion !== void 0 && context.schemaVersion !== 1) return false;
83
57
  if (context.repoRoot !== void 0 && (typeof context.repoRoot !== "string" || path.resolve(context.repoRoot) !== repoRoot)) return false;
84
58
  if (context.platform !== void 0 && context.platform !== adapter) return false;
@@ -108,6 +82,14 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
108
82
  const value = context[field];
109
83
  if (value !== void 0 && (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535)) return false;
110
84
  }
85
+ if (context.runtimeStart !== void 0) {
86
+ if (!context.runtimeStart || typeof context.runtimeStart !== "object" || Array.isArray(context.runtimeStart)) return false;
87
+ const runtimeStart = context.runtimeStart;
88
+ if (runtimeStart.approved !== void 0 && typeof runtimeStart.approved !== "boolean") return false;
89
+ for (const field of ["command", "readyUrl"]) {
90
+ if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
91
+ }
92
+ }
111
93
  const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "adbSerial"] : ["cdpPort", "extensionId"];
112
94
  if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
113
95
  return true;
@@ -1,26 +1,119 @@
1
- # Adapter surface
1
+ # The adapter surface — one seam for per-platform behavior
2
2
 
3
- `src/adapters/surface.ts` is the typed seam for Mobile, Extension, and Core.
4
- Commands ask it for platform behavior instead of re-deriving platform rules.
3
+ `mm-harness` runs against three platforms (mobile | extension | core). The
4
+ danger is that every command re-derives "what does this platform do?" with its
5
+ own `if (adapter === 'mobile') … else if (adapter === 'extension') …` ladder.
6
+ When it does, a platform gets forgotten in one command but not another — the
7
+ class of bug where `doctor` reported a live runtime section for mobile only,
8
+ `stop` refused every non-mobile adapter, and `logs` knew a single extension log
9
+ file instead of all three. Those are *platform-blind* commands.
5
10
 
6
- Each adapter implements:
11
+ The **adapter surface** is the fix: one interface per platform, resolved through
12
+ a registry, that owns the behavior a command would otherwise branch on.
7
13
 
8
- - runtime readiness;
9
- - slot-owned ports;
10
- - dev-server lifecycle;
11
- - log sources;
12
- - launch/recovery hints;
13
- - headless capability.
14
+ ## The interface
14
15
 
15
- Implementations live in `src/adapters/{mobile,extension,core}/surface.ts`.
16
- Platform-specific shell and Node operations live under `adapters/<platform>/`.
16
+ `src/adapters/surface.ts` defines `AdapterSurface` and the registry
17
+ `getAdapterSurface(adapter)`. Each platform ships one implementation:
17
18
 
18
- When adding behavior:
19
+ - `src/adapters/mobile/surface.ts`
20
+ - `src/adapters/extension/surface.ts`
21
+ - `src/adapters/core/surface.ts`
19
22
 
20
- 1. Extend `AdapterSurface` only when every platform needs the concept.
21
- 2. Implement all three adapters; Core should explicitly report headless/N/A.
22
- 3. Call the surface from the command instead of adding adapter conditionals.
23
- 4. Add a registry-completeness contract and a real affected-platform check.
23
+ ```ts
24
+ interface AdapterSurface {
25
+ readonly adapter: MetaMaskRecipeAdapter;
26
+ readonly headless: boolean; // core runs no app/dev server
27
+ resolveSlotPorts(target: string): void; // context > pool > formula (no-op for core)
28
+ runtimeStatus(target: string): Promise<AdapterRuntimeStatus>; // read-only readiness for doctor
29
+ devServer: {
30
+ describe(): string; // "Metro" | "webpack watcher" | "no dev server (headless)"
31
+ stop(target: string): AdapterDevServerStop; // idempotent, slot-scoped; headless = teach
32
+ };
33
+ logSources(target: string): AdapterLogSource[]; // ordered candidate log files (empty for core)
34
+ hints: { launch: string; relaunch: string }; // platform-phrased Next: hints
35
+ }
36
+ ```
24
37
 
25
- Mechanisms unique to one command may remain platform-specific. Shared readiness,
26
- ports, logs, lifecycle, and recovery language belong on the surface.
38
+ `runtimeStatus` returns a normalized shape so `doctor` renders one line the same
39
+ way for every platform:
40
+
41
+ ```ts
42
+ interface AdapterRuntimeStatus {
43
+ decision: string;
44
+ reasonCode?: string;
45
+ reasons: string[];
46
+ deps?: string;
47
+ devServer?: { label: string; status: string }; // absent for headless core
48
+ }
49
+ ```
50
+
51
+ The implementations are thin: they delegate to the readiness/port plumbing that
52
+ already existed (`decideExtensionReadiness`, `mobileRuntimeStatus`, the slot-port
53
+ resolvers and the webpack-watcher stop, all re-homed to
54
+ `src/adapters/slot-ports.ts`). The surface is an organizing seam, not a rewrite.
55
+
56
+ ## The rule
57
+
58
+ **A command never branches on adapter for behavior the surface owns.**
59
+
60
+ - Do not write `if (adapter === 'core')` — ask `surface.headless`.
61
+ - Do not write `adapter === 'mobile' ? metroPorts() : extensionPorts()` — call
62
+ `surface.resolveSlotPorts(target)`.
63
+ - Do not print `adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness
64
+ launch'` — use `surface.hints.launch`.
65
+
66
+ A new platform behavior is added by **extending the surface** (a new member on
67
+ the interface plus its three implementations), never by adding another branch to
68
+ a command. TypeScript then makes it impossible to ship a platform that forgot the
69
+ new member, because each implementation is annotated `: AdapterSurface`.
70
+
71
+ What the surface deliberately does **not** own: a command may still branch on
72
+ adapter for a *mechanism* that is genuinely platform-specific and not part of the
73
+ interface — e.g. `fixtures set` uses a shell arm on mobile and the engine path on
74
+ extension. Those branches select a mechanism; they never re-derive readiness,
75
+ ports, log locations, dev-server lifecycle, or Next: phrasing, which are the
76
+ surface's responsibility.
77
+
78
+ ## Migration status
79
+
80
+ Every command that used to branch on adapter now resolves through the surface:
81
+
82
+ | command | surface-backed | uses |
83
+ | --- | --- | --- |
84
+ | `doctor` | yes | `resolveSlotPorts` + `runtimeStatus` (mobile/extension/core runtime section) |
85
+ | `launch` | yes | `resolveSlotPorts` |
86
+ | `stop` | yes | `resolveSlotPorts` + `devServer.stop` (+ headless teaching) |
87
+ | `logs` | yes | `logSources` + `hints.launch` + `headless` |
88
+ | `debug` | yes | `headless` + `hints.relaunch` (core teaching); flag semantics stay per-command |
89
+ | `fixtures` | yes | `headless` + `hints` (retry/launch); the set *mechanism* stays per-platform |
90
+ | `run` / `call` | n/a | engine path; core-headless is handled by the heal contract, not the surface |
91
+ | `flows` | no (by design) | recipe-library flows are adapter-global; not a platform-owned behavior |
92
+ | `completion-candidates` | partial | `actions` scope to the detected checkout adapter; `flows` are adapter-global |
93
+
94
+ `flows` is intentionally not surface-backed: a recipe-library flow can compose
95
+ actions across platforms, so flows are adapter-global by design rather than a
96
+ platform-blind gap. `completion-candidates actions` already resolves the adapter
97
+ from the checkout context (cwd/`--target`); only the adapter-global `flows`
98
+ candidates are unscoped, matching the flows model.
99
+
100
+ ## How this composes with the UX principles
101
+
102
+ `docs/UX-PRINCIPLES.md` principle 1 (*Context-aware by default* — "output is
103
+ scoped to the platform… help, flag lists, and completion candidates shrink to
104
+ what applies here") states the intent. The adapter surface is its **enforcement
105
+ mechanism**: a command that resolves platform behavior through the surface is
106
+ context-aware by construction, and one that hand-rolls an adapter ladder is the
107
+ exact failure principle 1 warns against. When adding a command or output path,
108
+ satisfy principle 1 by going through the surface.
109
+
110
+ ## Adding a platform behavior — checklist
111
+
112
+ 1. Add the member to `AdapterSurface` in `src/adapters/surface.ts`.
113
+ 2. Implement it in all three `src/adapters/*/surface.ts` (TypeScript will not
114
+ compile until you do).
115
+ 3. Have the command call `getAdapterSurface(adapter).<member>` instead of
116
+ branching.
117
+ 4. Cover the closed blind spot with a contract test (see
118
+ `tests/contract/adapter-surface.test.sh`), and keep the registry-completeness
119
+ assertion green.