@deeeed/metamask-harness 0.17.2 → 0.17.4

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 (81) hide show
  1. package/CHANGELOG.md +35 -8
  2. package/README.md +12 -6
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +5 -1
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +0 -0
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch.sh +0 -0
  11. package/adapters/extension/live.sh +0 -0
  12. package/adapters/extension/readiness.mjs +0 -0
  13. package/adapters/extension/reattach.sh +0 -0
  14. package/adapters/extension/refresh-build.sh +0 -0
  15. package/adapters/extension/seed-fixture.sh +0 -0
  16. package/adapters/extension/sidepanel-toggle.sh +0 -0
  17. package/adapters/extension/snapshot-dist.sh +0 -0
  18. package/adapters/extension/stamp-runtime-title.cjs +18 -9
  19. package/adapters/extension/start-watch.sh +1 -0
  20. package/adapters/extension/stop-viewers.sh +0 -0
  21. package/adapters/extension/sync-webpack-dist.cjs +9 -7
  22. package/adapters/extension/verify.sh +0 -0
  23. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  24. package/adapters/manifest.json +9 -1
  25. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  26. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  27. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  28. package/adapters/mobile/cleanup.sh +0 -0
  29. package/adapters/mobile/inject.sh +13 -11
  30. package/adapters/mobile/launch-console-forwarder.cjs +0 -0
  31. package/adapters/mobile/lib/metro-listener.sh +0 -0
  32. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  33. package/adapters/mobile/open-device.sh +0 -0
  34. package/adapters/mobile/prewarm-bundle.sh +0 -0
  35. package/adapters/mobile/start-console-forwarder.sh +3 -3
  36. package/adapters/mobile/start-metro.sh +0 -0
  37. package/adapters/mobile/stop-metro.sh +0 -0
  38. package/adapters/mobile/verify.sh +0 -0
  39. package/adapters/mobile/wait-for-bridge.sh +22 -8
  40. package/adapters/mobile/yarn-setup.sh +0 -0
  41. package/adapters/shared/activate-repo-node.sh +0 -0
  42. package/adapters/shared/activate-repo-ruby.sh +0 -0
  43. package/adapters/shared/cli-ux.sh +0 -0
  44. package/adapters/shared/ensure-runner-deps.sh +0 -0
  45. package/adapters/shared/harness-path.sh +0 -0
  46. package/adapters/shared/hash-helpers.sh +0 -0
  47. package/adapters/shared/install-repo-deps.sh +0 -0
  48. package/adapters/shared/json-field.sh +0 -0
  49. package/adapters/shared/open-debug.mjs +7 -1
  50. package/adapters/shared/open-log-window.sh +0 -0
  51. package/adapters/shared/reap-checkout-metros.sh +0 -0
  52. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  53. package/adapters/shared/resolve-slot-ports.sh +0 -0
  54. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  55. package/adapters/shared/tmux-session.sh +0 -0
  56. package/adapters/shared/tmux-viewer.sh +0 -0
  57. package/adapters/shared/update-check-worker.mjs +11 -5
  58. package/dist/adapters/extension/console-capture.js +10 -1
  59. package/dist/adapters/mobile/prepare.js +5 -5
  60. package/dist/adapters/slot-ports.js +8 -0
  61. package/dist/cli.js +1 -1
  62. package/dist/commands/doctor.js +33 -8
  63. package/dist/commands/run-engine.js +2 -4
  64. package/dist/commands/update.js +2 -1
  65. package/dist/harness.js +24 -9
  66. package/dist/mm-harness-cli.js +7 -6
  67. package/dist/run-diagnostics.js +1 -6
  68. package/docs/ADAPTER-SURFACE.md +19 -112
  69. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +16 -0
  70. package/docs/UX-PRINCIPLES.md +1 -1
  71. package/docs/VIDEO-DEMO-VALIDATION.md +56 -44
  72. package/docs/recipe-libraries.md +45 -275
  73. package/library/README.md +4 -0
  74. package/library/manifests/core.action-manifest.json +7 -7
  75. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  76. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  77. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  78. package/package.json +4 -2
  79. package/scripts/completions.sh +0 -0
  80. package/scripts/install-completions.sh +0 -0
  81. package/scripts/validate-human-outcomes.mjs +169 -0
@@ -140,7 +140,7 @@ async function handleDoctor({ options }) {
140
140
  requiredChecks
141
141
  };
142
142
  const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
143
- const capture = captureHelperHealth();
143
+ const capture = captureHealth(adapter);
144
144
  const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
145
145
  const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
146
146
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
@@ -192,13 +192,7 @@ async function handleDoctor({ options }) {
192
192
  );
193
193
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
194
194
  }
195
- if (capture) {
196
- console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
197
- if (capture.status !== "pass") {
198
- if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
199
- console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
200
- }
201
- }
195
+ if (capture) renderCaptureHealth(capture, out);
202
196
  if (adapter === "mobile") {
203
197
  if (deviceView) renderMobileDeviceList(deviceView, out);
204
198
  if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
@@ -280,6 +274,25 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
280
274
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
281
275
  return EXIT.runtime;
282
276
  }
277
+ function captureHealth(adapter) {
278
+ if (adapter === "core") return null;
279
+ const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
280
+ const helper = captureHelperHealth();
281
+ if (!helper) {
282
+ return {
283
+ status: "fallback",
284
+ failing: [],
285
+ screenshots: { available: true, providers: fallback },
286
+ recording: { available: false, provider: null }
287
+ };
288
+ }
289
+ return {
290
+ status: helper.status,
291
+ failing: helper.failing,
292
+ screenshots: { available: true, providers: ["capture-helper", ...fallback] },
293
+ recording: { available: helper.status === "pass", provider: helper.status === "pass" ? "capture-helper" : null }
294
+ };
295
+ }
283
296
  function captureHelperHealth() {
284
297
  if (process.platform !== "darwin") return null;
285
298
  const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
@@ -292,6 +305,18 @@ function captureHelperHealth() {
292
305
  return null;
293
306
  }
294
307
  }
308
+ function renderCaptureHealth(capture, out) {
309
+ const video = capture.recording.available ? "capture-helper" : "unavailable";
310
+ console.log(
311
+ `${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", `(screenshots: ${capture.screenshots.providers.join(" \u2192 ")}; video: ${video})`)}`
312
+ );
313
+ if (capture.status === "warn") {
314
+ if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
315
+ console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
316
+ } else if (capture.status === "fallback") {
317
+ console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
318
+ }
319
+ }
295
320
  function detectOrphanMetros(target, livePort) {
296
321
  try {
297
322
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -22,8 +22,7 @@ import { listRecipeFiles } from "../recipe-files.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
23
  import {
24
24
  beginRunDiagnostics,
25
- finishRunDiagnostics,
26
- stopRunDiagnostics
25
+ finishRunDiagnostics
27
26
  } from "../run-diagnostics.js";
28
27
  import { EXIT } from "./shared.js";
29
28
  import {
@@ -90,7 +89,6 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
90
89
  await stopRecipeRecording(recording, result);
91
90
  return finishRunDiagnostics(diagnosticBaseline, result);
92
91
  } finally {
93
- stopRunDiagnostics(diagnosticBaseline);
94
92
  await stopRecipeRecording(recording);
95
93
  }
96
94
  } finally {
@@ -645,7 +643,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
645
643
  }
646
644
  }
647
645
  function emitHealViolation(json, command, result, violation, state, adapter) {
648
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'set MM_TEST_ACCOUNT_ADDRESS=<0x\u2026> in env, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
646
+ const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
649
647
  if (json) {
650
648
  console.log(
651
649
  JSON.stringify(
@@ -7,6 +7,7 @@ import { EXIT, flag, parseFlags } from "./shared.js";
7
7
  const PACKAGE_NAME = "@deeeed/metamask-harness";
8
8
  const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9
9
  const NUDGE_FETCH_TIMEOUT_MS = 2e3;
10
+ const REGISTRY_DIST_TAGS_ENDPOINT = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
10
11
  function currentVersion() {
11
12
  try {
12
13
  const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
@@ -210,7 +211,7 @@ function maybeNudge(now = Date.now()) {
210
211
  try {
211
212
  const child = spawn(
212
213
  process.execPath,
213
- [worker, file, String(NUDGE_FETCH_TIMEOUT_MS)],
214
+ [worker, file, String(NUDGE_FETCH_TIMEOUT_MS), REGISTRY_DIST_TAGS_ENDPOINT],
214
215
  { detached: true, stdio: "ignore" }
215
216
  );
216
217
  child.unref();
package/dist/harness.js CHANGED
@@ -71,6 +71,7 @@ function parseHarnessArgs(args) {
71
71
  const forward = [];
72
72
  let adapter;
73
73
  let json = false;
74
+ let quiet = false;
74
75
  let separator = false;
75
76
  for (let i = 0; i < args.length; i += 1) {
76
77
  const arg = args[i];
@@ -86,6 +87,10 @@ function parseHarnessArgs(args) {
86
87
  json = true;
87
88
  continue;
88
89
  }
90
+ if (arg === "--quiet") {
91
+ quiet = true;
92
+ continue;
93
+ }
89
94
  if (arg === "--adapter" || arg === "--platform") {
90
95
  const value = args[i + 1];
91
96
  if (arg === "--adapter") {
@@ -116,7 +121,7 @@ function parseHarnessArgs(args) {
116
121
  }
117
122
  forward.push(arg);
118
123
  }
119
- return { adapter, json, forward };
124
+ return { adapter, json, quiet, forward };
120
125
  }
121
126
  const ADAPTER_DETECT_NEXT = "cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>";
122
127
  function detectAdapter(target) {
@@ -276,12 +281,14 @@ async function handleHarness(argv) {
276
281
  return 2;
277
282
  }
278
283
  const harnessAction = action;
279
- const { adapter: parsedAdapter, json, forward } = parseHarnessArgs(argv.slice(1));
284
+ const { adapter: parsedAdapter, json, quiet, forward } = parseHarnessArgs(argv.slice(1));
280
285
  const rawTarget = argValue(forward, "--target");
281
286
  const target = path.resolve(rawTarget ?? process.cwd());
282
287
  const adapter = parsedAdapter ?? detectAdapter(target);
283
288
  if (!adapter || !isAdapter(adapter)) {
284
- if (json) {
289
+ if (quiet) {
290
+ return 2;
291
+ } else if (json) {
285
292
  const detectError = adapter ? { code: "UNSUPPORTED_PLATFORM", message: `unsupported platform: ${adapter}`, userAction: "pass --adapter <mobile|extension|core> to specify a supported adapter" } : { code: "ADAPTER_DETECTION_FAILED", message: `could not detect the MetaMask repo type for ${target}`, userAction: ADAPTER_DETECT_NEXT };
286
293
  console.log(harnessSummary(harnessAction, void 0, target, "fail", 2, false, detectError));
287
294
  } else {
@@ -305,7 +312,9 @@ async function handleHarness(argv) {
305
312
  }
306
313
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
307
314
  if ("error" in dispatch) {
308
- if (json) {
315
+ if (quiet) {
316
+ return 1;
317
+ } else if (json) {
309
318
  console.log(
310
319
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
311
320
  code: "DISPATCH_UNAVAILABLE",
@@ -321,14 +330,16 @@ async function handleHarness(argv) {
321
330
  }
322
331
  return 1;
323
332
  }
324
- if (!json) {
333
+ if (!json && !quiet) {
325
334
  const detected = autoDetected ? ", auto-detected" : "";
326
335
  console.error(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`);
327
336
  }
328
337
  const start = Date.now();
329
338
  if (shellLeafMissing(dispatch.command)) {
330
339
  const message = missingShellLeafMessage(dispatch.command);
331
- if (json) {
340
+ if (quiet) {
341
+ return 1;
342
+ } else if (json) {
332
343
  console.log(
333
344
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
334
345
  code: "HARNESS_SPAWN_FAILED",
@@ -348,12 +359,14 @@ async function handleHarness(argv) {
348
359
  const result = spawnSync(dispatchBin, dispatchArgs, {
349
360
  // Default: stream child output verbatim (byte-identical to the skill path).
350
361
  // --json: route child output to our stderr so stdout carries only the summary.
351
- stdio: json ? ["inherit", 2, "inherit"] : "inherit",
362
+ stdio: quiet ? "ignore" : json ? ["inherit", 2, "inherit"] : "inherit",
352
363
  env: process.env
353
364
  });
354
365
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
355
366
  if (result.error) {
356
- if (json) {
367
+ if (quiet) {
368
+ return 1;
369
+ } else if (json) {
357
370
  console.log(
358
371
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
359
372
  code: "HARNESS_SPAWN_FAILED",
@@ -368,7 +381,9 @@ async function handleHarness(argv) {
368
381
  return 1;
369
382
  }
370
383
  const exitCode = result.status ?? 1;
371
- if (json) {
384
+ if (quiet) {
385
+ return exitCode;
386
+ } else if (json) {
372
387
  console.log(
373
388
  harnessSummary(
374
389
  harnessAction,
@@ -76,11 +76,11 @@ Example:
76
76
  },
77
77
  {
78
78
  name: "actions",
79
- summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
79
+ summary: "Discover typed single operations and their fields.",
80
80
  example: "mm-harness actions --adapter mobile",
81
81
  helpText: `mm-harness actions [query] [flags]
82
82
 
83
- List the action vocabulary + field schemas for the checkout adapter.
83
+ Discover typed single operations and their field schemas for this checkout.
84
84
 
85
85
  query Search names, categories, fields, and descriptions (typo-tolerant)
86
86
  --action <name> Describe one action; fuzzy-resolves like call (short or full name)
@@ -152,11 +152,11 @@ Example (real actions; run mm-harness actions for this checkout's full set):
152
152
  },
153
153
  {
154
154
  name: "flows",
155
- 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.",
156
156
  example: "mm-harness flows",
157
157
  helpText: `mm-harness flows [flags]
158
158
 
159
- Browse the reusable flow library. Flows resolve across libraries by precedence
159
+ Browse reusable parameterized action sequences. Flows resolve by precedence
160
160
  (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
161
161
  Flow resolution is adapter-global, so there is no --adapter flag here.
162
162
 
@@ -169,11 +169,12 @@ Example:
169
169
  },
170
170
  {
171
171
  name: "run",
172
- summary: "Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.",
172
+ summary: "Execute a complete proof and write its evidence.",
173
173
  example: "mm-harness run recipe.json",
174
174
  helpText: `mm-harness run <recipe.json> [flags]
175
175
 
176
- Validate + run a recipe and write evidence (summary / trace / artifacts).
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.
177
178
 
178
179
  --list List everything invocable for the adapter (actions + flows); no <recipe> needed
179
180
  --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
@@ -52,7 +52,6 @@ function isRunDiagnosticsDocument(value) {
52
52
  function finishRunDiagnostics(baseline, result) {
53
53
  if (!baseline) return result;
54
54
  try {
55
- stopRunDiagnostics(baseline);
56
55
  const bufferedIssues = baseline.mobileIssueBuffer ? collectMobileIssueBuffer(baseline.mobileIssueBuffer.projectRoot) : void 0;
57
56
  const diagnostics = collectRunDiagnostics(baseline, bufferedIssues);
58
57
  const artifactsDir = path.dirname(result.summaryPath);
@@ -83,9 +82,6 @@ function finishRunDiagnostics(baseline, result) {
83
82
  };
84
83
  }
85
84
  }
86
- function stopRunDiagnostics(baseline) {
87
- void baseline;
88
- }
89
85
  function collectRunDiagnostics(baseline, bufferedIssues) {
90
86
  const stat = safeStat(baseline.source.path);
91
87
  const source = {
@@ -261,6 +257,5 @@ export {
261
257
  collectRunDiagnostics,
262
258
  finishRunDiagnostics,
263
259
  formatRunDiagnosticsForHuman,
264
- readRunDiagnosticsDocument,
265
- stopRunDiagnostics
260
+ readRunDiagnosticsDocument
266
261
  };
@@ -1,119 +1,26 @@
1
- # The adapter surface — one seam for per-platform behavior
1
+ # Adapter surface
2
2
 
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.
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.
10
5
 
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.
6
+ Each adapter implements:
13
7
 
14
- ## The interface
8
+ - runtime readiness;
9
+ - slot-owned ports;
10
+ - dev-server lifecycle;
11
+ - log sources;
12
+ - launch/recovery hints;
13
+ - headless capability.
15
14
 
16
- `src/adapters/surface.ts` defines `AdapterSurface` and the registry
17
- `getAdapterSurface(adapter)`. Each platform ships one implementation:
15
+ Implementations live in `src/adapters/{mobile,extension,core}/surface.ts`.
16
+ Platform-specific shell and Node operations live under `adapters/<platform>/`.
18
17
 
19
- - `src/adapters/mobile/surface.ts`
20
- - `src/adapters/extension/surface.ts`
21
- - `src/adapters/core/surface.ts`
18
+ When adding behavior:
22
19
 
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
- ```
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.
37
24
 
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.
25
+ Mechanisms unique to one command may remain platform-specific. Shared readiness,
26
+ ports, logs, lifecycle, and recovery language belong on the surface.
@@ -3,11 +3,27 @@
3
3
  Run this from each product checkout before a release. Verify the visible outcome,
4
4
  not only the exit code.
5
5
 
6
+ Run the non-visual gate first:
7
+
8
+ ```bash
9
+ yarn qa:human \
10
+ --extension /path/to/metamask-extension \
11
+ --mobile /path/to/metamask-mobile \
12
+ --core /path/to/metamask-core \
13
+ --artifacts-dir temp/human-qa
14
+ ```
15
+
16
+ It verifies live readiness, installed runtime, debugger target resolution, smoke
17
+ evidence, and unchanged tracked product source. The checks below cover outcomes
18
+ that still require eyes or an intentional source edit.
19
+
6
20
  ## Common
7
21
 
8
22
  - [ ] `mm-harness --version` prints the candidate version.
9
23
  - [ ] `mm-harness doctor` detects the correct product and reports only current
10
24
  configuration.
25
+ - [ ] Without `capture-helper`, `doctor` names the CDP or simulator/ADB screenshot
26
+ fallback and says video is unavailable.
11
27
  - [ ] A misspelled flag explains the error and gives a runnable next command.
12
28
  - [ ] `mm-harness actions` and `mm-harness flows` return useful, bounded discovery.
13
29
  - [ ] `mm-harness fixtures set` either applies the fixture or names the missing
@@ -5,7 +5,7 @@ a PR that adds a command or output path is checked against each one.
5
5
 
6
6
  **Scope: the HUMAN surface.** The agent contract is exempt and inviolable: under
7
7
  `--json`, stdout carries exactly one machine-readable envelope — no color, no
8
- banners, no decorative lines, schema per CLI-SPEC.md. Progress/streaming exists
8
+ banners, no decorative lines. Progress/streaming exists
9
9
  for agents too, but only ever on stderr. When a rule below conflicts with envelope
10
10
  purity, the envelope wins.
11
11
 
@@ -1,62 +1,74 @@
1
1
  # Video demo validation
2
2
 
3
- Candidate: local packed `@deeeed/metamask-harness` 0.17.1 installed globally.
4
- Run commands from the named checkout; product/ports are auto-detected.
3
+ Use this checklist against the installed release before recording. Each checkout
4
+ auto-detects its platform, ports, and slot; the paths below are placeholders only.
5
+
6
+ ```bash
7
+ export EXTENSION_CHECKOUT=/path/to/metamask-extension
8
+ export MOBILE_CHECKOUT=/path/to/metamask-mobile
9
+ export CORE_CHECKOUT=/path/to/core
10
+ ```
5
11
 
6
12
  ## Common
7
13
 
8
- - [x] `mm-harness --version` prints `0.17.1` from the global npm install.
9
- - [x] `yarn check` passes.
10
- - [x] No `package-lock.json`; Yarn state is valid.
11
- - [x] Current source/docs contain no compatibility or removed-command paths.
12
- - [x] `doctor` reports current runtime fields only.
13
- - [x] Recipe results print diagnostics and absolute artifact paths inline.
14
+ - [ ] `mm-harness --version` prints the intended release.
15
+ - [ ] `mm-harness doctor` explains any unavailable capability and gives one next step.
16
+ - [ ] Recipe results print diagnostics and absolute artifact paths inline.
17
+ - [ ] `git status --short` is unchanged in each product checkout after validation.
14
18
 
15
- ## Extension (`mme-2`)
19
+ ## Extension
16
20
 
17
- - [x] `mm-harness doctor` reports healthy CDP and active webpack watcher.
18
- - [x] `mm-harness run runner.smoke --artifacts-dir temp/final-demo-smoke` passes.
19
- - [x] Smoke prints six absolute evidence paths and `diagnostics: CLEAN`.
20
- - [x] `mm-harness debug` connects native DevTools to the active slot target.
21
- - [x] Fullscreen and sidepanel targets are selectable.
22
- - [x] Two source edits plus revert compiled incrementally with the same watcher;
23
- browser refresh showed each change without relaunch.
21
+ ```bash
22
+ cd "$EXTENSION_CHECKOUT"
23
+ mm-harness launch --build --verify
24
+ mm-harness doctor
25
+ mm-harness actions positions
26
+ mm-harness flows
27
+ mm-harness run runner.smoke --artifacts-dir temp/demo
28
+ mm-harness run wallet.smoke --artifacts-dir temp/wallet-demo
29
+ mm-harness logs --source app
30
+ mm-harness debug
31
+ ```
24
32
 
25
- ## Mobile (`mm-1`, iOS)
33
+ - [ ] Launch keeps an incremental webpack watcher alive.
34
+ - [ ] Two small source edits compile incrementally; browser refresh shows both changes.
35
+ - [ ] `debug` connects to the active slot's MetaMask target and opens Console.
36
+ - [ ] `logs --source app` shows extension logs; `--source dapp` and `--source webpack`
37
+ show their separate streams.
38
+ - [ ] Each recipe prints `diagnostics: CLEAN` or expands every warning/error inline.
39
+ - [ ] Each recipe prints absolute paths for its report, trace, diagnostics, screenshots,
40
+ and artifact manifest.
26
41
 
27
- - [x] `mm-harness install` writes metadata only; product checkout remains clean.
28
- - [x] `mm-harness fixtures set` reads the canonical fixture and unlocks `dev1`.
29
- - [x] `mm-harness call metamask.wallet.ensure_unlocked` passes from the global package.
30
- - [x] `mm-harness verify` passes after fixture setup.
31
- - [x] `mm-harness status` reports `WalletView`, unlocked wallet, selected account,
32
- and fixture `READY`.
33
- - [ ] During the recording, visually confirm the expected simulator is foregrounded.
42
+ ## Mobile
34
43
 
35
- ## Core (`core-1`)
44
+ ```bash
45
+ cd "$MOBILE_CHECKOUT"
46
+ mm-harness launch ios --verify
47
+ mm-harness fixtures set
48
+ mm-harness call metamask.wallet.ensure_unlocked
49
+ mm-harness status
50
+ mm-harness logs --source app
51
+ ```
36
52
 
37
- - [x] `mm-harness doctor` reports the headless controller ready.
38
- - [x] Canonical wallet fixture is present.
39
- - [x] `mm-harness run perps.smoke --artifacts-dir temp/final-demo-smoke` passes.
40
- - [x] Positions, orders, and account state are read through the controller.
41
- - [x] Evidence prints absolute paths; diagnostics correctly reports `N/A` for headless Core.
53
+ - [ ] The intended simulator is running; focus changes only when explicitly requested.
54
+ - [ ] Fixture setup reads the canonical checkout fixture and unlocks the wallet.
55
+ - [ ] Status reports the live route, wallet state, account, and fixture readiness.
56
+ - [ ] App logs are separate from Metro logs (`mm-harness logs --source metro`).
42
57
 
43
- ## Recording sequence
58
+ ## Core
44
59
 
45
60
  ```bash
46
- # Extension
47
- cd /Users/deeeed/farmslot/repos/mme-2
61
+ cd "$CORE_CHECKOUT"
48
62
  mm-harness doctor
49
- mm-harness actions positions
50
- mm-harness flows
51
63
  mm-harness run runner.smoke --artifacts-dir temp/demo
64
+ ```
52
65
 
53
- # Mobile
54
- cd /Users/deeeed/farmslot/repos/mm-1
55
- mm-harness launch ios
56
- mm-harness fixtures set
57
- mm-harness call metamask.wallet.ensure_unlocked
66
+ - [ ] Doctor reports the headless controller ready.
67
+ - [ ] The canonical wallet fixture supplies the default account.
68
+ - [ ] Evidence paths are absolute; diagnostics is explicitly `N/A` for headless Core.
58
69
 
59
- # Core
60
- cd /Users/deeeed/farmslot/repos/core-1
61
- mm-harness run perps.smoke --artifacts-dir temp/demo
62
- ```
70
+ ## Recording path
71
+
72
+ Show the Extension discovery and proof commands, then the equivalent Mobile and Core
73
+ commands. The useful story is: one auto-detected CLI, reusable team libraries,
74
+ parameterized recipes, app diagnostics, and proof artifacts that a human can inspect.