@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,6 +4,9 @@ import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { EXIT } from "./shared.js";
6
6
  const require2 = createRequire(import.meta.url);
7
+ const MARK_SCRIPT = require2.resolve(
8
+ "@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
9
+ );
7
10
  const USAGE = "mm-harness checklist mark <task-dir> <step> [options]";
8
11
  function terminalSignalStatus(taskDir) {
9
12
  try {
@@ -122,7 +125,7 @@ async function handleChecklist(argv) {
122
125
  teachRecipeCompletion(resolvedTaskDir, markArgs);
123
126
  const result = spawnSync(
124
127
  process.execPath,
125
- [require2.resolve("@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"), resolvedTaskDir, ...markArgs],
128
+ [MARK_SCRIPT, resolvedTaskDir, ...markArgs],
126
129
  { stdio: "inherit" }
127
130
  );
128
131
  if (result.error) {
@@ -1,8 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { runnerDir } from "../paths.js";
3
- import { acquireCheckoutLock } from "../checkout-lock.js";
4
3
  import { getAdapterSurface } from "../adapters/surface.js";
5
- import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
4
+ import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
6
5
  const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json", "noOpen"]);
7
6
  async function handleDebug(argv) {
8
7
  const { options } = parseFlags(argv, DEBUG_BOOLEANS);
@@ -32,43 +31,37 @@ async function handleDebug(argv) {
32
31
  "mm-harness debug --worker"
33
32
  );
34
33
  }
35
- const lock = acquireCheckoutLock(target, "debug");
36
- if ("message" in lock) return checkoutBusyOut(json, "debug", lock.message, lock.path);
37
- try {
38
- const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
39
- const debugArgs = ["--adapter", adapter];
40
- if (adapter === "extension") {
41
- if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
42
- debugArgs.push("--target", worker ? "worker" : "page");
43
- } else {
44
- if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
45
- debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
46
- }
47
- if (json) debugArgs.push("--json");
48
- if (noOpen) debugArgs.push("--no-open");
49
- const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
50
- if (json) {
51
- const leaf = parseDebugLeaf(result.output);
52
- console.log(
53
- JSON.stringify(
54
- {
55
- ...leaf,
56
- schemaVersion: 1,
57
- command: "debug",
58
- adapter,
59
- mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
60
- opened: leaf.opened === true,
61
- exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
62
- },
63
- null,
64
- 2
65
- )
66
- );
67
- }
68
- return result.status === 0 ? EXIT.ok : EXIT.runtime;
69
- } finally {
70
- lock.release();
34
+ const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
35
+ const debugArgs = ["--adapter", adapter];
36
+ if (adapter === "extension") {
37
+ if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
38
+ debugArgs.push("--target", worker ? "worker" : "page");
39
+ } else {
40
+ if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
41
+ debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
42
+ }
43
+ if (json) debugArgs.push("--json");
44
+ if (noOpen) debugArgs.push("--no-open");
45
+ const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
46
+ if (json) {
47
+ const leaf = parseDebugLeaf(result.output);
48
+ console.log(
49
+ JSON.stringify(
50
+ {
51
+ ...leaf,
52
+ schemaVersion: 1,
53
+ command: "debug",
54
+ adapter,
55
+ mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
56
+ opened: leaf.opened === true,
57
+ exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
58
+ },
59
+ null,
60
+ 2
61
+ )
62
+ );
71
63
  }
64
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
72
65
  }
73
66
  function parseDebugLeaf(output) {
74
67
  try {
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { listConnectedDevices } from "../devices.js";
3
- import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
3
+ import { optionString } from "./parse-args.js";
4
4
  function normalizeDeviceName(value) {
5
5
  return value.replace(/_/gu, " ").trim();
6
6
  }
@@ -75,12 +75,6 @@ function configuredPinForPlatform(platform) {
75
75
  function oppositePlatform(platform) {
76
76
  return platform === "ios" ? "android" : "ios";
77
77
  }
78
- function deviceProbe(options) {
79
- return `mm-harness status --target ${shellQuoteArg(targetPath(options))} --all-devices --json`;
80
- }
81
- function deviceRecovery(options, rerun) {
82
- return rerun ? `${deviceProbe(options)}; then retry: ${rerun}` : deviceProbe(options);
83
- }
84
78
  function applyDeviceTargeting(command, adapter, options, opts) {
85
79
  const device = optionString(options, "device");
86
80
  const platformPreference = mobilePlatformPreference(options);
@@ -90,8 +84,7 @@ function applyDeviceTargeting(command, adapter, options, opts) {
90
84
  ok: false,
91
85
  code: "DEVICE_WRONG_ADAPTER",
92
86
  message: `--device is only supported on the mobile adapter (the ${adapter} adapter has no device to target).
93
- Drop --device, or run this ${command} inside a metamask-mobile checkout.`,
94
- userAction: `remove --device; ${adapter} has no device target`
87
+ Drop --device, or run this ${command} inside a metamask-mobile checkout.`
95
88
  };
96
89
  }
97
90
  return { ok: true };
@@ -104,8 +97,7 @@ function applyDeviceTargeting(command, adapter, options, opts) {
104
97
  ok: false,
105
98
  code: "DEVICE_PLATFORM_CONFLICT",
106
99
  message: `--device ${device} resolves to ${matched.platform}, but --platform ${platformPreference} was requested.
107
- To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device.`,
108
- userAction: deviceRecovery(options, opts.rerun)
100
+ To use this device intentionally, drop --platform ${platformPreference} or choose a ${platformPreference} device.`
109
101
  };
110
102
  }
111
103
  if (matched.platform === "android") setAndroidDeviceEnv(matched.id, matched.name);
@@ -134,8 +126,7 @@ function applyDeviceTargeting(command, adapter, options, opts) {
134
126
  ok: false,
135
127
  code: "DEVICE_NAME_AMBIGUOUS",
136
128
  message: `device name '${device}' is ambiguous; use the id:
137
- ` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n"),
138
- userAction: deviceRecovery(options, opts.rerun)
129
+ ` + byName.map((d) => ` - ${d.id}${d.name ? ` (${d.name})` : ""} [${d.state}] ${d.platform}`).join("\n")
139
130
  };
140
131
  }
141
132
  return {
@@ -143,8 +134,7 @@ function applyDeviceTargeting(command, adapter, options, opts) {
143
134
  code: "DEVICE_NOT_FOUND",
144
135
  message: `--device ${device} did not match any connected device.
145
136
  ` + (connected2.length > 0 ? ` Connected devices:
146
- ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
147
- userAction: deviceRecovery(options, opts.rerun)
137
+ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
148
138
  };
149
139
  }
150
140
  const connected = listConnectedDevices();
@@ -168,8 +158,7 @@ ${formatConnectedDevices(connected2)}` : ` No devices are currently connected (
168
158
  message: `--platform ${platformPreference} conflicts with the current slot-selected device.
169
159
  Selected devices:
170
160
  ${formatConnectedDevices(selectedTargetable2)}
171
- To use a different connected device intentionally, pass --device <id>.`,
172
- userAction: deviceRecovery(options, opts.rerun)
161
+ To use a different connected device intentionally, pass --device <id>.`
173
162
  };
174
163
  }
175
164
  if (!configuredPin && oppositePin) {
@@ -178,8 +167,7 @@ ${formatConnectedDevices(selectedTargetable2)}
178
167
  code: "DEVICE_PLATFORM_CONFLICT",
179
168
  message: `--platform ${platformPreference} conflicts with the configured slot target.
180
169
  Configured ${oppositePlatform(platformPreference)} target: ${oppositePin}
181
- To use a different connected device intentionally, pass --device <id>.`,
182
- userAction: deviceRecovery(options, opts.rerun)
170
+ To use a different connected device intentionally, pass --device <id>.`
183
171
  };
184
172
  }
185
173
  if (configuredPin) {
@@ -190,8 +178,8 @@ ${formatConnectedDevices(selectedTargetable2)}
190
178
  message: `--platform ${platformPreference} matches the configured slot target, but that target is not ready.
191
179
  Configured target: ${configuredPin}
192
180
  Connected devices:
193
- ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}`,
194
- userAction: `mm-harness launch ${platformPreference} --target ${shellQuoteArg(targetPath(options))}`
181
+ ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}
182
+ Next: run mm-harness launch ${platformPreference}, or pass --device <id> to intentionally use another target.`
195
183
  };
196
184
  }
197
185
  if (platformTargetable.length === 1) {
@@ -207,8 +195,7 @@ ${connected.length > 0 ? formatConnectedDevices(connected) : " - none"}`,
207
195
  code: "DEVICE_PLATFORM_NOT_FOUND",
208
196
  message: `--platform ${platformPreference} did not match any ready connected device.
209
197
  ` + (connected.length > 0 ? ` Connected devices:
210
- ${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`),
211
- userAction: deviceRecovery(options, opts.rerun)
198
+ ${formatConnectedDevices(connected)}` : ` No devices are currently connected (adb devices / booted iOS simulators).`)
212
199
  };
213
200
  }
214
201
  return {
@@ -218,8 +205,7 @@ ${formatConnectedDevices(connected)}` : ` No devices are currently connected (a
218
205
  Connected devices:
219
206
  ${formatConnectedDevices(connected)}
220
207
  Add --device <id> to disambiguate, e.g.:${platformTargetable.map((d) => `
221
- --device ${d.id}`).join("")}`,
222
- userAction: deviceRecovery(options, opts.rerun)
208
+ --device ${d.id}`).join("")}`
223
209
  };
224
210
  }
225
211
  const selectedTargetable = targetable.filter(deviceSelected);
@@ -238,8 +224,7 @@ ${formatConnectedDevices(connected)}
238
224
  Connected devices:
239
225
  ${formatConnectedDevices(connected)}
240
226
  ` + (selectedTargetable.length > 1 ? ` The current slot context selects more than one target; fix the slot device pins or add --device <id>.` : ` Add --device <id> to disambiguate, e.g.:${targetable.map((d) => `
241
- --device ${d.id}`).join("")}`),
242
- userAction: deviceRecovery(options, opts.rerun)
227
+ --device ${d.id}`).join("")}`)
243
228
  };
244
229
  }
245
230
  return { ok: true };
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { color } from "../cli-color.js";
6
6
  import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDoctorCheckSummary } from "../doctor.js";
7
- import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
7
+ import { detectAdapter } from "../harness.js";
8
8
  import { assertAdapter, runnerDir } from "../paths.js";
9
9
  import { getAdapterSurface } from "../adapters/surface.js";
10
10
  import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
@@ -21,13 +21,12 @@ import {
21
21
  import { farmslotReadyIndicator } from "./farmslot-ready.js";
22
22
  import { ensureRuntimeContext } from "../runtime-context.js";
23
23
  import { acquireCheckoutLock } from "../checkout-lock.js";
24
- import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
24
+ import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
25
25
  import {
26
26
  actionManifestPathOption,
27
27
  applyRuntimeDirOption,
28
28
  optionFlag,
29
29
  optionString,
30
- shellQuote,
31
30
  targetPath
32
31
  } from "./parse-args.js";
33
32
  function applyDoctorRuntimePorts(options) {
@@ -57,14 +56,6 @@ async function handleDoctor({ options }) {
57
56
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
58
57
  }
59
58
  assertAdapter(adapter);
60
- if (!fs.existsSync(target)) {
61
- return usageOut(
62
- json,
63
- "doctor",
64
- `target does not exist: ${target}`,
65
- "pass --target <metamask-checkout> pointing to an existing checkout"
66
- );
67
- }
68
59
  if (printReady && json) {
69
60
  return usageOut(
70
61
  json,
@@ -81,12 +72,12 @@ async function handleDoctor({ options }) {
81
72
  surface.resolveSlotPorts(target);
82
73
  const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
83
74
  if ("code" in dtResult) {
84
- return usageOut(json, "doctor", dtResult.message, dtResult.userAction);
75
+ return usageOut(json, "doctor", dtResult.message, "mm-harness doctor --fix --adapter mobile --device <adb-serial|simulator-udid>");
85
76
  }
86
77
  applyDoctorRuntimePorts(options);
87
78
  const lock = acquireCheckoutLock(target, "doctor-fix");
88
79
  if ("message" in lock) {
89
- return checkoutBusyOut(json, "doctor", lock.message, lock.path);
80
+ return usageOut(json, "doctor", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
90
81
  }
91
82
  let fixed;
92
83
  let failed;
@@ -96,16 +87,9 @@ async function handleDoctor({ options }) {
96
87
  lock.release();
97
88
  }
98
89
  const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
99
- const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
90
+ const nextActions = doctorFixNextActions(failed, result2.fixture);
100
91
  const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
101
- const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
102
- const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
103
- const error = status === "fail" ? {
104
- code: "DOCTOR_FIX_INCOMPLETE",
105
- message: "doctor --fix could not make every required check pass",
106
- userAction: fixUserAction
107
- } : void 0;
108
- if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions, ...error ? { error } : {} }, null, 2));
92
+ if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions }, null, 2));
109
93
  else {
110
94
  console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
111
95
  for (const action of nextActions) console.error(` Next: ${action}`);
@@ -120,9 +104,8 @@ async function handleDoctor({ options }) {
120
104
  surface.resolveSlotPorts(target);
121
105
  const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
122
106
  if ("code" in dtResult) {
123
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
124
- else console.error(`\u2717 doctor: ${dtResult.message}
125
- Next: ${dtResult.userAction}`);
107
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
108
+ else console.error(`\u2717 doctor: ${dtResult.message}`);
126
109
  return EXIT.usage;
127
110
  }
128
111
  applyDoctorRuntimePorts(options);
@@ -140,7 +123,7 @@ async function handleDoctor({ options }) {
140
123
  requiredChecks
141
124
  };
142
125
  const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
143
- const capture = captureHealth(adapter);
126
+ const capture = captureHelperHealth();
144
127
  const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
145
128
  const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
146
129
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
@@ -159,9 +142,7 @@ async function handleDoctor({ options }) {
159
142
  printReady
160
143
  );
161
144
  }
162
- const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
163
- const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
164
- if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...doctorError ? { error: doctorError } : {} }, null, 2));
145
+ if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
165
146
  else {
166
147
  const out = (style, text) => color(style, text, { stream: process.stdout });
167
148
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
@@ -192,13 +173,18 @@ async function handleDoctor({ options }) {
192
173
  );
193
174
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
194
175
  }
195
- if (capture) renderCaptureHealth(capture, out);
176
+ if (capture) {
177
+ console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
178
+ if (capture.status !== "pass") {
179
+ if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
180
+ console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
181
+ }
182
+ }
196
183
  if (adapter === "mobile") {
197
184
  if (deviceView) renderMobileDeviceList(deviceView, out);
198
185
  if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
199
186
  if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
200
187
  }
201
- if (doctorError) console.log(` ${out("dim", `Next: ${doctorUserAction}`)}`);
202
188
  }
203
189
  return doctorResult.status === "pass" ? 0 : 1;
204
190
  }
@@ -221,21 +207,15 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
221
207
  detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
222
208
  };
223
209
  }
224
- function doctorFixNextActions(failed, fixture, adapter, target) {
210
+ function doctorFixNextActions(failed, fixture) {
225
211
  const actions = [];
226
- const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
227
- const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
228
212
  if (failed.includes("wallet-fixture")) {
229
213
  const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
230
- actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
231
- actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
232
- }
233
- if (failed.includes("runtime-context")) {
234
- actions.push(
235
- `inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
236
- );
214
+ actions.push(`1) mm-harness fixtures init --from <path>${force} # existing team/test fixture`);
215
+ actions.push(`2) mm-harness fixtures init --dev${force} # disposable public test wallet; never real funds`);
237
216
  }
238
- if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
217
+ if (failed.includes("runtime-context")) actions.push("inspect temp/recipe/runtime/agentic-runtime.json, then retry mm-harness doctor --fix");
218
+ if (failed.includes("overlay")) actions.push("mm-harness install --target .");
239
219
  return actions;
240
220
  }
241
221
  function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
@@ -261,9 +241,7 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
261
241
  return EXIT.runtime;
262
242
  }
263
243
  if (json) {
264
- const userAction = adapter === "extension" ? `mm-harness launch --adapter extension --target ${shellQuote(target)}` : adapter === "core" ? `mm-harness verify --adapter core --target ${shellQuote(target)}` : `mm-harness status --target ${shellQuote(target)} --json`;
265
- const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
266
- console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
244
+ console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
267
245
  return live ? EXIT.ok : EXIT.runtime;
268
246
  }
269
247
  const out = (style, text) => color(style, text, { stream: process.stdout });
@@ -274,25 +252,6 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
274
252
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
275
253
  return EXIT.runtime;
276
254
  }
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
- }
296
255
  function captureHelperHealth() {
297
256
  if (process.platform !== "darwin") return null;
298
257
  const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
@@ -305,18 +264,6 @@ function captureHelperHealth() {
305
264
  return null;
306
265
  }
307
266
  }
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
- }
320
267
  function detectOrphanMetros(target, livePort) {
321
268
  try {
322
269
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -434,7 +381,6 @@ function readTextSnapshot(file) {
434
381
  }
435
382
  }
436
383
  export {
437
- doctorFixNextActions,
438
384
  ensureExtensionPerpsBuildFlag,
439
385
  handleDoctor
440
386
  };
@@ -6,9 +6,9 @@ import { getAdapterSurface } from "../adapters/surface.js";
6
6
  import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
7
7
  import { acquireCheckoutLock } from "../checkout-lock.js";
8
8
  import { applyDeviceTargeting } from "./device-target.js";
9
- import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
9
+ import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
10
10
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
11
- const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
11
+ const MOBILE_FIXTURE_TIMEOUT_MS = 12e4;
12
12
  async function handleFixtures(argv, deps) {
13
13
  const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
14
14
  const json = flag(options, "json");
@@ -16,7 +16,7 @@ async function handleFixtures(argv, deps) {
16
16
  if (!fs.existsSync(target)) return handleFixturesLocked(argv, deps);
17
17
  const lock = acquireCheckoutLock(target, "fixtures");
18
18
  if ("message" in lock) {
19
- return checkoutBusyOut(json, "fixtures", lock.message, lock.path);
19
+ return usageOut(json, "fixtures", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
20
20
  }
21
21
  try {
22
22
  return await handleFixturesLocked(argv, deps);
@@ -56,14 +56,6 @@ async function handleFixturesLocked(argv, deps) {
56
56
  if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
57
57
  return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
58
58
  }
59
- if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
60
- return usageOut(
61
- json,
62
- "fixtures",
63
- "--dev, --from, and --force belong to fixtures init; they do not apply a fixture.",
64
- "mm-harness fixtures init --dev && mm-harness fixtures set"
65
- );
66
- }
67
59
  const target = targetOf(options);
68
60
  const adapter = resolveAdapter(options, target);
69
61
  if (!adapter) {
@@ -73,7 +65,7 @@ async function handleFixturesLocked(argv, deps) {
73
65
  surface.resolveSlotPorts(target);
74
66
  const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
75
67
  if ("code" in dtResult) {
76
- return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
68
+ return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
77
69
  }
78
70
  if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
79
71
  if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
@@ -104,14 +96,6 @@ async function handleFixturesLocked(argv, deps) {
104
96
  return exitCode;
105
97
  }
106
98
  const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
107
- if (!fs.existsSync(fixturePath)) {
108
- return usageOut(
109
- json,
110
- "fixtures",
111
- `no wallet fixture at ${fixturePath}.`,
112
- "mm-harness fixtures init --dev && mm-harness fixtures set"
113
- );
114
- }
115
99
  process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
116
100
  `);
117
101
  let status;
@@ -121,8 +105,8 @@ async function handleFixturesLocked(argv, deps) {
121
105
  const previousAppRoot = process.env.APP_ROOT;
122
106
  process.env.APP_ROOT = target;
123
107
  try {
124
- const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
125
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
108
+ const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_FIXTURE_TIMEOUT_MS);
109
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_FIXTURE_TIMEOUT_MS;
126
110
  mobileSetupResult = await spawnScriptStreaming(
127
111
  setupWalletSh,
128
112
  ["--fixture", fixturePath],
@@ -145,6 +129,14 @@ async function handleFixturesLocked(argv, deps) {
145
129
  console.error(` Next: ${retryHint}`);
146
130
  }
147
131
  } else {
132
+ if (!fs.existsSync(fixturePath)) {
133
+ return usageOut(
134
+ json,
135
+ "fixtures",
136
+ `no wallet fixture at ${fixturePath}.`,
137
+ "create it (or pass --fixture <path>), then re-run: mm-harness fixtures set"
138
+ );
139
+ }
148
140
  const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
149
141
  process.env.RECIPE_WALLET_FIXTURE = fixturePath;
150
142
  try {
@@ -158,7 +150,7 @@ async function handleFixturesLocked(argv, deps) {
158
150
  console.error(` Next: ${retryHint}`);
159
151
  }
160
152
  }
161
- const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
153
+ const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
162
154
  if (json) {
163
155
  console.log(
164
156
  JSON.stringify(
@@ -182,7 +174,7 @@ async function handleFixturesLocked(argv, deps) {
182
174
  2
183
175
  )
184
176
  );
185
- } else if (status === "pass") {
177
+ } else {
186
178
  console.error(teaching);
187
179
  }
188
180
  return status === "pass" ? EXIT.ok : EXIT.runtime;
@@ -306,9 +298,7 @@ async function fixturesFinalize(adapter, target, options, json) {
306
298
  if (outPath) leafArgs.push("--out", outPath);
307
299
  process.stderr.write(`\u2192 fixtures finalize extension \u2014 seeding account labels via CDP on port ${cdpPort} (can take ~30s)\u2026
308
300
  `);
309
- const result = await spawnScriptStreaming(process.execPath, leafArgs, target, {
310
- timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
311
- });
301
+ const result = await spawnScriptStreaming(process.execPath, leafArgs, target);
312
302
  const status = result.status === 0 ? "pass" : "fail";
313
303
  const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
314
304
  const retry = `mm-harness fixtures finalize --fixture ${fixturePath} --state ${statePath} --cdp-port ${cdpPort} --extension-dir ${extensionDirPath}`;
@@ -7,6 +7,7 @@ import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
7
7
  import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
8
8
  import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
9
9
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
10
+ import { ensureExtensionHarnessFresh } from "../../adapters/extension/harness-freshness.js";
10
11
  import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
11
12
  import { spawnScriptStreaming } from "../shared.js";
12
13
  function extensionDepsBlock(target) {
@@ -29,6 +30,7 @@ function installExtensionDeps(target) {
29
30
  return spawnScriptStreaming(installScript, ["--target", target], target);
30
31
  }
31
32
  async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
33
+ await ensureExtensionHarnessFresh(target);
32
34
  if (wantWatch) {
33
35
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
34
36
  const watchArgs = ["--target", target];