@deeeed/metamask-harness 0.14.7 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +81 -6
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +4 -2
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +73 -34
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +21 -10
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +3 -3
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +10 -4
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +2 -2
  17. package/adapters/extension/stop-viewers.sh +0 -0
  18. package/adapters/extension/verify.sh +19 -2
  19. package/adapters/extension/wallet-fixture-state.cjs +14 -1
  20. package/adapters/manifest.json +19 -3
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  22. package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
  23. package/adapters/mobile/cleanup.sh +0 -0
  24. package/adapters/mobile/inject.sh +0 -0
  25. package/adapters/mobile/launch-metro.cjs +74 -0
  26. package/adapters/mobile/lib/metro-listener.sh +0 -0
  27. package/adapters/mobile/lib/tmux-viewer.sh +4 -4
  28. package/adapters/mobile/open-device.sh +3 -1
  29. package/adapters/mobile/prewarm-bundle.sh +0 -0
  30. package/adapters/mobile/start-metro.sh +12 -18
  31. package/adapters/mobile/stop-metro.sh +1 -0
  32. package/adapters/mobile/verify.sh +5 -5
  33. package/adapters/mobile/wait-for-bridge.sh +0 -0
  34. package/adapters/mobile/yarn-setup.sh +0 -0
  35. package/adapters/shared/activate-repo-node.sh +0 -0
  36. package/adapters/shared/activate-repo-ruby.sh +0 -0
  37. package/adapters/shared/cli-ux.sh +0 -0
  38. package/adapters/shared/ensure-runner-deps.sh +0 -0
  39. package/adapters/shared/harness-path.sh +0 -0
  40. package/adapters/shared/hash-helpers.sh +0 -0
  41. package/adapters/shared/install-repo-deps.sh +29 -0
  42. package/adapters/shared/json-field.sh +0 -0
  43. package/adapters/shared/open-debug.mjs +35 -4
  44. package/adapters/shared/open-log-window.sh +1 -1
  45. package/adapters/shared/reap-checkout-metros.sh +0 -0
  46. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  47. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  48. package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
  49. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  50. package/adapters/shared/resolve-slot-ports.sh +21 -17
  51. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  52. package/adapters/shared/tmux-session.sh +0 -5
  53. package/adapters/shared/tmux-viewer.sh +7 -2
  54. package/dist/adapters/core/surface.js +3 -0
  55. package/dist/adapters/extension/ensure-ready.js +0 -7
  56. package/dist/adapters/extension/runtime-decision.js +93 -3
  57. package/dist/adapters/extension/surface.js +17 -6
  58. package/dist/adapters/mobile/metro-env.js +67 -0
  59. package/dist/adapters/mobile/perps-env.js +101 -0
  60. package/dist/adapters/mobile/prepare.js +32 -14
  61. package/dist/adapters/mobile/runtime-decision.js +21 -2
  62. package/dist/adapters/mobile/source-freshness.js +116 -0
  63. package/dist/adapters/mobile/surface.js +3 -0
  64. package/dist/adapters/resolve-slot-ports.js +4 -0
  65. package/dist/adapters/slot-ports.js +131 -49
  66. package/dist/adapters.js +7 -2
  67. package/dist/checkout-lock.js +72 -0
  68. package/dist/cli.js +2 -0
  69. package/dist/commands/call.js +91 -54
  70. package/dist/commands/check.js +266 -46
  71. package/dist/commands/checklist.js +136 -0
  72. package/dist/commands/debug.js +21 -2
  73. package/dist/commands/doctor.js +187 -12
  74. package/dist/commands/fixtures.js +177 -42
  75. package/dist/commands/launch/extension.js +6 -1
  76. package/dist/commands/launch/index.js +54 -22
  77. package/dist/commands/logs.js +24 -4
  78. package/dist/commands/run-engine.js +223 -7
  79. package/dist/commands/run.js +67 -50
  80. package/dist/commands/shared.js +86 -5
  81. package/dist/commands/stop.js +1 -2
  82. package/dist/doctor.js +39 -17
  83. package/dist/harness.js +5 -2
  84. package/dist/heal-bounds.js +1 -1
  85. package/dist/mm-harness-cli.js +37 -9
  86. package/dist/runner.js +35 -5
  87. package/dist/runtime-context.js +228 -0
  88. package/docs/CLI-SPEC.md +15 -11
  89. package/docs/MENTAL-MODEL.md +6 -6
  90. package/library/actions/extension/perps/perps.mjs +29 -8
  91. package/library/actions/extension/platform/cdp.mjs +128 -28
  92. package/library/actions/extension/ui/navigate.mjs +1 -1
  93. package/library/actions/mobile/perps/perps.mjs +13 -1
  94. package/library/actions/mobile/platform/bridge.mjs +302 -29
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
  96. package/library/manifests/extension.action-manifest.json +32 -12
  97. package/library/manifests/mobile.action-manifest.json +25 -3
  98. package/package.json +5 -5
  99. package/scripts/completions.sh +7 -4
  100. package/scripts/install-completions.sh +0 -0
@@ -111,7 +111,8 @@ function spawnInherit(script, args, cwd) {
111
111
  if (result.status !== null) return result.status;
112
112
  return result.signal === "SIGINT" || result.signal === "SIGTERM" ? 0 : 1;
113
113
  }
114
- function spawnScriptStreaming(script, args, cwd, env) {
114
+ function spawnScriptStreaming(script, args, cwd, options) {
115
+ const spawnOptions = options && ("env" in options || "timeoutMs" in options) ? options : { env: options };
115
116
  const isNodeScript = script === process.execPath && args.length > 0;
116
117
  const stem = isNodeScript ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase() : path.basename(script).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
117
118
  const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
@@ -126,12 +127,58 @@ function spawnScriptStreaming(script, args, cwd, env) {
126
127
  }
127
128
  const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
128
129
  return new Promise((resolve) => {
130
+ const ownsProcessGroup = process.platform !== "win32" && Number(spawnOptions.timeoutMs) > 0;
129
131
  const child = spawn(invokeBin, spawnArgs, {
130
132
  cwd,
131
- env: env ? { ...process.env, ...env } : process.env,
132
- stdio: ["ignore", "pipe", "pipe"]
133
+ env: spawnOptions.env ? { ...process.env, ...spawnOptions.env } : process.env,
134
+ stdio: ["ignore", "pipe", "pipe"],
135
+ detached: ownsProcessGroup
133
136
  });
134
137
  let output = "";
138
+ let settled = false;
139
+ let didTimeout = false;
140
+ let escalation;
141
+ const parentSignals = ["SIGINT", "SIGTERM"];
142
+ const removeParentSignalHandlers = () => {
143
+ for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
144
+ };
145
+ const finish = (result) => {
146
+ if (settled) return;
147
+ settled = true;
148
+ if (timer) clearTimeout(timer);
149
+ if (escalation) clearTimeout(escalation);
150
+ removeParentSignalHandlers();
151
+ resolve(result);
152
+ };
153
+ const signalChildTree = (signal) => {
154
+ try {
155
+ if (ownsProcessGroup && child.pid) process.kill(-child.pid, signal);
156
+ else child.kill(signal);
157
+ } catch {
158
+ }
159
+ };
160
+ const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
161
+ signalChildTree(signal);
162
+ removeParentSignalHandlers();
163
+ process.kill(process.pid, signal);
164
+ }]));
165
+ if (ownsProcessGroup) {
166
+ for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
167
+ }
168
+ const timeoutMs = spawnOptions.timeoutMs;
169
+ const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
170
+ didTimeout = true;
171
+ const message = `leaf timed out after ${String(timeoutMs)}ms: ${path.basename(script)}`;
172
+ output += `${output.endsWith("\n") || output.length === 0 ? "" : "\n"}${message}
173
+ `;
174
+ process.stderr.write(`${message}
175
+ `);
176
+ signalChildTree("SIGTERM");
177
+ escalation = setTimeout(() => {
178
+ signalChildTree("SIGKILL");
179
+ finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
180
+ }, 1e3);
181
+ }, Number(timeoutMs)) : void 0;
135
182
  const tee = (chunk) => {
136
183
  const text = chunk.toString("utf8");
137
184
  output += text;
@@ -146,10 +193,20 @@ function spawnScriptStreaming(script, args, cwd, env) {
146
193
  Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the shell leaf is missing or not executable`;
147
194
  process.stderr.write(`${message}
148
195
  `);
149
- resolve({ status: 1, output: message });
196
+ finish({ status: 1, output: message });
197
+ });
198
+ child.on("exit", () => {
199
+ if (!didTimeout) return;
200
+ signalChildTree("SIGKILL");
201
+ finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
150
202
  });
151
203
  child.on("close", (status) => {
152
- resolve({ status: status ?? 1, output });
204
+ if (didTimeout) signalChildTree("SIGKILL");
205
+ finish({
206
+ status: didTimeout ? 1 : status ?? 1,
207
+ output,
208
+ ...didTimeout ? { timedOut: true, timeoutMs: Number(timeoutMs) } : {}
209
+ });
153
210
  });
154
211
  });
155
212
  }
@@ -169,9 +226,33 @@ function usageOut(json, command, message, userAction) {
169
226
  }
170
227
  return EXIT.usage;
171
228
  }
229
+ function checkoutBusyOut(json, command, message, lockPath) {
230
+ const userAction = `wait for the current owner, or inspect ${lockPath} if its process has exited`;
231
+ if (json) {
232
+ console.log(
233
+ JSON.stringify(
234
+ {
235
+ schemaVersion: 1,
236
+ command,
237
+ status: "fail",
238
+ exitCode: EXIT.bounded,
239
+ recoverable: false,
240
+ error: { code: "SANDBOX_BUSY", message, userAction }
241
+ },
242
+ null,
243
+ 2
244
+ )
245
+ );
246
+ } else {
247
+ console.error(`\u2717 mm-harness ${command}: ${message}
248
+ Next: ${userAction}`);
249
+ }
250
+ return EXIT.bounded;
251
+ }
172
252
  export {
173
253
  ADAPTER_DETECT_NEXT,
174
254
  EXIT,
255
+ checkoutBusyOut,
175
256
  flag,
176
257
  parseFlags,
177
258
  resolveAdapter,
@@ -12,12 +12,11 @@ async function handleStop(argv) {
12
12
  const json = optionFlag(options, "json");
13
13
  const { adapter, target } = resolveAdapter(options);
14
14
  const surface = getAdapterSurface(adapter);
15
+ surface.resolveSlotPorts(target);
15
16
  const explicitPort = optionString(options, "port") ?? optionString(options, "watcherPort");
16
17
  if (explicitPort) {
17
18
  process.env.WATCHER_PORT = explicitPort;
18
19
  process.env.METRO_PORT = explicitPort;
19
- } else {
20
- surface.resolveSlotPorts(target);
21
20
  }
22
21
  const stop = surface.devServer.stop(target);
23
22
  if (stop.kind === "headless") {
package/dist/doctor.js CHANGED
@@ -1,8 +1,19 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { color } from "./cli-color.js";
4
+ import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
4
5
  import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
5
6
  import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
7
+ function requiredDoctorCheckSummary(checks) {
8
+ const required = checks.filter((check) => check.required);
9
+ const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
10
+ return {
11
+ status: failed.length === 0 ? "pass" : "fail",
12
+ total: required.length,
13
+ passed: required.length - failed.length,
14
+ failed
15
+ };
16
+ }
6
17
  function repoShape(target) {
7
18
  const exists = (rel) => fs.existsSync(path.join(target, rel));
8
19
  const packageInfo = readPackageInfo(target);
@@ -56,31 +67,36 @@ function readPackageInfo(target) {
56
67
  };
57
68
  }
58
69
  }
59
- function fixtureSummary(target) {
70
+ function fixtureSummary(target, adapter) {
60
71
  const candidates = [
61
72
  `${recipeRuntimeDir()}/wallet-fixture.json`
62
73
  ];
63
74
  const rel = candidates.find((candidate) => fs.existsSync(path.join(target, candidate)));
64
75
  if (!rel) return { status: "missing", path: null };
76
+ return fixtureFileSummary(path.join(target, rel), adapter, rel);
77
+ }
78
+ function fixtureFileSummary(file, adapter, reportedPath = file) {
65
79
  try {
66
- const data = readJsonObject(path.join(target, rel));
80
+ const data = readJsonObject(file);
81
+ const hasAccounts = Array.isArray(data.accounts) && data.accounts.length > 0;
82
+ const hasPassword = typeof data.password === "string" && data.password.length > 0;
67
83
  return {
68
- status: Array.isArray(data.accounts) && data.accounts.length > 0 ? "ready" : "incomplete",
69
- path: rel,
84
+ status: hasAccounts && (adapter === "core" || hasPassword) ? "ready" : "incomplete",
85
+ path: reportedPath,
70
86
  accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
71
- hasPassword: typeof data.password === "string" && data.password.length > 0
87
+ hasPassword
72
88
  };
73
89
  } catch (error) {
74
- return { status: "invalid", path: rel, error: error instanceof Error ? error.message : String(error) };
90
+ return { status: "invalid", path: reportedPath, error: error instanceof Error ? error.message : String(error) };
75
91
  }
76
92
  }
77
93
  const RUNTIME_CONTEXT_FIELDS = [
78
- { key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "farmslot dispatch writes this", adapters: ["mobile", "extension", "core"] },
79
- { key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "auto-resolved; edit file to pin", adapters: ["extension"] },
80
- { key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "edit file or pass --cdp-port", adapters: ["extension"] },
81
- { key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "edit file (true/false)", adapters: ["mobile", "extension"] },
82
- { key: "runtimeStart.command", envVars: [], envVar: null, customize: "edit file", adapters: ["mobile", "extension"] },
83
- { key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "edit file", adapters: ["mobile", "extension"] }
94
+ { key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "doctor --fix creates local identity; Farmslot may override", adapters: ["mobile", "extension", "core"] },
95
+ { key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "resolved from the built extension", adapters: ["extension"] },
96
+ { key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] },
97
+ { key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
98
+ { key: "runtimeStart.command", envVars: [], envVar: null, customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
99
+ { key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] }
84
100
  ];
85
101
  function runtimeContextSummary(target, adapter) {
86
102
  const contextPath = resolveRuntimeContextPath(target);
@@ -104,7 +120,7 @@ function renderRuntimeContext(runtimeContext) {
104
120
  const out = (style, text) => color(style, text, { stream: process.stdout });
105
121
  const lines = [];
106
122
  lines.push(
107
- runtimeContext.fileExists ? `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("ok", "(present)")}` : `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("dim", "(absent \u2014 written by farmslot prepare/dispatch)")}`
123
+ runtimeContext.fileExists ? `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("ok", "(present)")}` : `${out("label", "runtime-context:")} ${runtimeContext.file} ${out("dim", "(absent \u2014 run mm-harness doctor --fix)")}`
108
124
  );
109
125
  for (const [key, field] of Object.entries(runtimeContext.fields)) {
110
126
  const isSet = field.value !== void 0 && field.value !== null && field.value !== "";
@@ -118,25 +134,28 @@ function renderRuntimeContext(runtimeContext) {
118
134
  function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = manifestPath(adapter)) {
119
135
  const mode = compatibilityMode(adapter, target);
120
136
  const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
121
- const status = manifestErrors > 0 ? "fail" : "pass";
122
137
  const checks = [
123
138
  {
124
139
  id: "manifest",
125
140
  status: manifestErrors === 0 ? "pass" : "fail",
141
+ required: true,
126
142
  message: manifestErrors === 0 ? "Action manifest is valid Recipe v1." : `Action manifest has ${manifestErrors} validation error(s).`
127
143
  },
128
144
  {
129
145
  id: "bridge",
130
146
  status: mode === "unsupported/no bridge" ? "fail" : "pass",
147
+ required: false,
131
148
  message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
132
149
  }
133
150
  ];
151
+ const requiredChecks = requiredDoctorCheckSummary(checks);
134
152
  return {
135
153
  schemaVersion: 1,
136
154
  protocolVersion: "v1",
137
155
  runner_protocol_version: 1,
138
- status,
139
- checks: [...checks],
156
+ status: requiredChecks.status,
157
+ checks,
158
+ requiredChecks,
140
159
  adapter,
141
160
  target,
142
161
  runner: {
@@ -147,7 +166,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
147
166
  },
148
167
  compatibilityMode: mode,
149
168
  shape: repoShape(target),
150
- fixture: fixtureSummary(target),
169
+ fixture: fixtureSummary(target, adapter),
170
+ environment: adapter === "mobile" ? { mobilePerps: mobilePerpsEnvironment(target) } : {},
151
171
  runtimeContext: runtimeContextSummary(target, adapter),
152
172
  manifestValidation: manifestValidation.summary
153
173
  };
@@ -162,8 +182,10 @@ function readJsonObject(file) {
162
182
  export {
163
183
  compatibilityMode,
164
184
  createDoctorReport,
185
+ fixtureFileSummary,
165
186
  fixtureSummary,
166
187
  renderRuntimeContext,
167
188
  repoShape,
189
+ requiredDoctorCheckSummary,
168
190
  runtimeContextSummary
169
191
  };
package/dist/harness.js CHANGED
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
5
+ import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
5
6
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
6
7
  import { prepareMobile } from "./adapters/mobile/prepare.js";
7
8
  const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
@@ -186,11 +187,13 @@ function applyExtensionRuntimeEnv(target, action, args) {
186
187
  const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
187
188
  if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
188
189
  }
190
+ } else {
191
+ delete process.env.RECIPE_RUNTIME_CONTEXT;
189
192
  }
193
+ resolveExtensionSlotPorts(target);
190
194
  let result = [...args];
191
195
  if (!hasArg(result, "--cdp-port")) {
192
- const contextPort = contextExists ? readRuntimeContextField(contextPath, "cdpPort") : void 0;
193
- const cdpPort = contextPort ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
196
+ const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
194
197
  if (cdpPort) {
195
198
  process.env.RECIPE_CDP_PORT = cdpPort;
196
199
  process.env.CDP_PORT = cdpPort;
@@ -103,7 +103,7 @@ function checkHealBounds(target, output, state) {
103
103
  return {
104
104
  code: "SAME_RECOVERY_TWICE",
105
105
  exitCode: EXIT.bounded,
106
- message: "same recovery already failed once this invocation \u2014 refusing to loop.",
106
+ message: state.recovered.length > 0 ? "one recovery already succeeded this invocation \u2014 bounded policy refuses a second recovery." : "one recovery was already attempted this invocation \u2014 refusing to loop.",
107
107
  originalError
108
108
  };
109
109
  }
@@ -51,6 +51,22 @@ Example:
51
51
  mm-harness status --json
52
52
  mm-harness status --fast`
53
53
  },
54
+ {
55
+ name: "checklist",
56
+ summary: "Mark task-local checklist progress through the bundled agent runtime.",
57
+ example: "mm-harness checklist mark <task-dir> start",
58
+ helpText: `mm-harness checklist mark <task-dir> <step> [options]
59
+
60
+ Mark checklist progress through mm-harness's bundled @farmslot/agent-runtime.
61
+ The task directory must contain CHECKLIST.md and checklist-target.json.
62
+
63
+ Steps: start | 1 | 2 | ... | complete | no-change | blocked
64
+
65
+ Example:
66
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> start
67
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
68
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last`
69
+ },
54
70
  {
55
71
  name: "actions",
56
72
  summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
@@ -72,18 +88,18 @@ Example:
72
88
  },
73
89
  {
74
90
  name: "stop",
75
- summary: "Stop the dev server this checkout owns (mobile Metro / extension webpack watcher) and close its log window.",
91
+ summary: "Stop this checkout runtime (mobile Metro / Extension watcher + owned browser) and close its log windows.",
76
92
  example: "mm-harness stop",
77
93
  helpText: `mm-harness stop [flags]
78
94
 
79
- Stop the dev server this checkout owns and close its tmux log-tail window,
95
+ Stop the runtime this checkout owns and close its tmux log-tail windows,
80
96
  scoped to this checkout so concurrent slots are untouched. Idempotent \u2014
81
97
  nothing running is success, not an error. Behavior is per platform:
82
98
  mobile stop the port-scoped Metro dev server
83
- extension stop the checkout's webpack watcher (pid file + orphan scan)
99
+ extension stop webpack watcher, owned Chrome/CDP profile processes, viewers, and stale markers
84
100
  core headless \u2014 no dev server to stop (teaching error)
85
101
 
86
- --port <port> Dev-server port (default: the checkout's slot context)
102
+ --port <port> Metro/webpack port override; Extension CDP remains checkout-context scoped
87
103
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
88
104
  --target <path> Checkout path (default: cwd)
89
105
  --json Machine-readable output
@@ -101,7 +117,8 @@ Example:
101
117
  Run one action in isolation as a one-node recipe through the real engine path.
102
118
  Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
103
119
  if unique; ambiguous = exit 2. Pass action fields as key=value shorthand or
104
- with --arg k=v. Actions differ per adapter \u2014 list this checkout's with:
120
+ with --arg k=v; numbers, booleans, arrays, and objects are parsed as JSON-like values.
121
+ Actions differ per adapter \u2014 list this checkout's with:
105
122
  mm-harness actions.
106
123
 
107
124
  --list List everything invocable for the adapter (actions + flows); no <action> needed
@@ -201,11 +218,12 @@ Example:
201
218
  --adapter <mobile|extension|core> Adapter label (auto-detected when possible)
202
219
  --base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
203
220
  --profile <fast|full> Validation depth (default: fast)
221
+ --fix Fix changed files, then validate them
204
222
  --artifacts-dir <dir> Write validation-summary.json/.md and per-check logs
205
223
  --json Machine-readable envelope
206
224
 
207
225
  Example:
208
- mm-harness check diff --profile fast --artifacts-dir artifacts/validation
226
+ mm-harness check diff --fix --profile fast --artifacts-dir artifacts/validation
209
227
  mm-harness check diff --profile full --json`
210
228
  },
211
229
  {
@@ -306,6 +324,8 @@ Example:
306
324
  helpText: `mm-harness verify [flags]
307
325
 
308
326
  Check the runtime overlay is present and healthy (no app launch).
327
+ Extension live checks infer the checkout CDP port from runtime context,
328
+ pool, or slot suffix when --cdp-port is omitted.
309
329
 
310
330
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
311
331
  --target <path> Checkout path (default: cwd)
@@ -395,6 +415,7 @@ Example:
395
415
 
396
416
  --worker Extension service-worker DevTools
397
417
  --dev-menu Mobile RN developer menu
418
+ --no-open Resolve target/method/endpoint without opening a UI
398
419
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
399
420
  --target <path> Checkout path (default: cwd)
400
421
  --json Machine-readable output
@@ -425,11 +446,13 @@ Example:
425
446
  },
426
447
  {
427
448
  name: "fixtures",
428
- summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 sync / set / generate / finalize.",
449
+ summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
429
450
  example: "mm-harness fixtures set",
430
- helpText: `mm-harness fixtures <sync|set|generate|finalize> [flags]
451
+ helpText: `mm-harness fixtures <init|sync|set|generate|finalize> [flags]
431
452
 
432
453
  Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
454
+ init Create it from --from <path>, or explicitly choose --dev for a
455
+ disposable public test wallet that must never hold real funds.
433
456
  sync Refresh the wallet fixture files on the target.
434
457
  set Apply the canonical fixture (SRP/password/accounts); the password is
435
458
  read FROM the fixture, never typed.
@@ -441,6 +464,9 @@ Example:
441
464
  Want different accounts? Edit the fixture file directly:
442
465
  <checkout>/temp/recipe/runtime/wallet-fixture.json
443
466
 
467
+ --from <path> Existing wallet fixture source (init)
468
+ --dev Create a disposable public test wallet (init)
469
+ --force Replace an existing canonical fixture (init)
444
470
  --fixture <path> Wallet fixture path (generate/finalize input; agent override for set/sync \u2014 env: RECIPE_WALLET_FIXTURE)
445
471
  --out <path> generate: output fixture-state.json; finalize: optional validation report
446
472
  --state <path> finalize: the pre-launch fixture-state.json to seed
@@ -453,6 +479,8 @@ Example:
453
479
  --json Machine-readable output
454
480
 
455
481
  Example:
482
+ mm-harness fixtures init --from /secure/path/wallet-fixture.json
483
+ mm-harness fixtures init --dev
456
484
  mm-harness fixtures sync
457
485
  mm-harness fixtures set
458
486
  mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
@@ -488,7 +516,7 @@ const HELP_GROUPS = [
488
516
  {
489
517
  title: "PROVE",
490
518
  blurb: "run recipes and inspect readiness",
491
- commands: ["run", "doctor", "check", "recipe-quality"]
519
+ commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
492
520
  },
493
521
  {
494
522
  title: "RUNTIME OVERLAY",
package/dist/runner.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
3
+ import {
4
+ mobileSourceFingerprint,
5
+ recordMobileSourceBaseline
6
+ } from "./adapters/mobile/source-freshness.js";
3
7
  import { resolveMetaMaskMobileLifecycleTarget } from "./app-lifecycle.js";
4
8
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
5
9
  import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
@@ -40,10 +44,13 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
40
44
  })
41
45
  });
42
46
  const existing = new Set([...core, ...ui].map((entry) => entry.action));
43
- const lifecycle = createAppLifecycleAdapters({
44
- actions,
45
- targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
46
- });
47
+ const lifecycle = mobileSourceAwareLifecycleAdapters(
48
+ adapter,
49
+ createAppLifecycleAdapters({
50
+ actions,
51
+ targetProvider: { resolveTarget: resolveMetaMaskMobileLifecycleTarget }
52
+ })
53
+ );
47
54
  const custom = createMetaMaskAdapters(adapter).filter(
48
55
  (entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
49
56
  );
@@ -69,6 +76,28 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
69
76
  }
70
77
  });
71
78
  }
79
+ function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
80
+ if (adapter !== "mobile") return lifecycle;
81
+ return lifecycle.map((entry) => ({
82
+ ...entry,
83
+ async execute(node, context) {
84
+ const command = node.command ?? node.event ?? node.state;
85
+ const reloadsSource = command === "launch" || command === "foreground" || command === "restart";
86
+ const source = reloadsSource ? mobileSourceFingerprint(context.projectRoot) : void 0;
87
+ if (source?.status === "unavailable") {
88
+ throw new Error(`${source.error.message} Next: ${source.error.userAction}`);
89
+ }
90
+ const fingerprint = source?.fingerprint;
91
+ const result = await entry.execute(node, context);
92
+ if (fingerprint !== void 0 && !recordMobileSourceBaseline(context.projectRoot, fingerprint)) {
93
+ throw new Error(
94
+ "Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
95
+ );
96
+ }
97
+ return result;
98
+ }
99
+ }));
100
+ }
72
101
  function runnerProvenance() {
73
102
  return {
74
103
  source: runnerDir,
@@ -90,5 +119,6 @@ function runnerGitRef() {
90
119
  export {
91
120
  createMetaMaskExtensionRunner,
92
121
  createMetaMaskMobileRunner,
93
- createMetaMaskRunner
122
+ createMetaMaskRunner,
123
+ mobileSourceAwareLifecycleAdapters
94
124
  };