@deeeed/metamask-harness 0.17.4 → 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 -103
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -55
  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 -58
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -107
  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 -43
  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
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { walletFixturePath } from "../paths.js";
4
4
  import { color } from "../cli-color.js";
5
5
  import { recipeRunning } from "../heal-bounds.js";
6
- import { checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress } from "./shared.js";
6
+ import { EXIT } from "./shared.js";
7
7
  import {
8
8
  optionFlag,
9
9
  optionString,
@@ -11,8 +11,6 @@ import {
11
11
  resolveAdapter,
12
12
  runtimeOptionsFromCli,
13
13
  isRecord,
14
- shellQuote,
15
- targetPath,
16
14
  usageError
17
15
  } from "./parse-args.js";
18
16
  import {
@@ -30,119 +28,46 @@ import { getAdapterSurface } from "../adapters/surface.js";
30
28
  import { coreDependencyBlock } from "./core-readiness.js";
31
29
  import { writeRunReport } from "./run-report.js";
32
30
  import { acquireCheckoutLock } from "../checkout-lock.js";
33
- import { JsonStreamWriter } from "../json-stream.js";
34
- import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
35
- async function handleRun(parsed) {
36
- const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
37
- const restoreStdout = stream.isolateStdout();
38
- const target = targetPath(parsed.options);
39
- try {
40
- const exitCode = await handleRunInner(parsed, stream);
41
- stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
42
- return exitCode;
43
- } catch (error) {
44
- const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
45
- stream.error({
46
- code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
47
- message: error instanceof Error ? error.message : String(error),
48
- userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
49
- });
50
- stream.complete("fail", exitCode);
51
- throw error;
52
- } finally {
53
- restoreStdout();
54
- }
55
- }
56
- async function handleRunInner({ positional, options }, stream) {
57
- if (optionFlag(options, "list")) {
58
- if (stream.enabled) {
59
- const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
60
- stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
61
- return EXIT.usage;
62
- }
63
- return handleListExecutables("run", options);
64
- }
31
+ async function handleRun({ positional, options }) {
32
+ if (optionFlag(options, "list")) return handleListExecutables("run", options);
65
33
  const targetRecipe = positional[0];
66
34
  if (!targetRecipe) throw usageError("run requires <recipe.json>.");
67
- if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options, stream);
35
+ if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
68
36
  const { adapter, target } = resolveAdapter(options);
69
37
  const json = optionFlag(options, "json");
70
- const jsonOutput = json && !stream.enabled;
71
- const machine = json || stream.enabled;
72
- stream.phase("resolve", { adapter, target, recipe: targetRecipe });
73
- if (!fs.existsSync(target)) {
74
- const message = `target does not exist: ${target}`;
75
- const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
76
- if (stream.enabled) {
77
- stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
78
- return EXIT.usage;
79
- }
80
- return usageOut(jsonOutput, "run", message, userAction);
81
- }
82
- writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
83
38
  getAdapterSurface(adapter).resolveSlotPorts(target);
84
39
  const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
85
40
  if ("code" in dtResult) {
86
- return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
41
+ return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
87
42
  }
88
- if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
43
+ if (recipeRunning(target)) return emitRunRecipeRunning(json);
89
44
  if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
90
45
  requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
91
46
  }
92
- stream.phase("validate");
93
47
  const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
94
48
  if (validated.usageError) {
95
- const userAction = runUsageRecovery(
96
- validated.usageError.code,
97
- validated.usageError.message,
98
- adapter,
99
- validated.recipeFile
100
- );
101
- return emitRunUsageError(
102
- jsonOutput,
103
- stream,
104
- adapter,
105
- validated.recipeFile,
106
- validated.usageError.code,
107
- validated.usageError.message,
108
- userAction
109
- );
49
+ return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
110
50
  }
111
51
  if (validated.errorCount > 0) {
112
- return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
52
+ return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
113
53
  }
114
54
  const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
115
55
  if (depsBlock) {
116
- return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
56
+ return emitRunUsageError(json, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
117
57
  }
118
58
  const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
119
59
  const lock = acquireCheckoutLock(target, "run");
120
- if ("message" in lock) {
121
- const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
122
- stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
123
- return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
124
- }
60
+ if ("message" in lock) return emitRunRecipeRunning(json, lock.message);
125
61
  try {
126
- const prepared = await prepareHeal(adapter, target, options, machine, {
127
- onPhase: (phase, fields) => stream.phase(phase, fields)
128
- });
129
- if (typeof prepared === "number") {
130
- stream.error({
131
- code: "RUN_PREPARE_FAILED",
132
- message: "runtime preparation failed; inspect stderr for the exact probe",
133
- userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
134
- });
135
- return prepared;
136
- }
62
+ const prepared = await prepareHeal(adapter, target, options, json);
63
+ if (typeof prepared === "number") return prepared;
137
64
  const { state, heal } = prepared;
138
65
  const librarySources = validated.librarySources;
139
66
  const runtimeOptions = {
140
67
  ...runtimeOptionsFromCli(options),
141
68
  ...librarySources ? { librarySources } : {},
142
- stdoutIsMachineContract: machine,
143
- onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
69
+ stdoutIsMachineContract: json
144
70
  };
145
- stream.phase("execute");
146
71
  const { result, violation } = await executeWithHealBounds(
147
72
  // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
148
73
  // that only the resolver knows how to turn into a file.
@@ -151,42 +76,12 @@ async function handleRunInner({ positional, options }, stream) {
151
76
  target,
152
77
  heal,
153
78
  state,
154
- () => recoverRunInfra(adapter, target, machine),
155
- (code) => stream.phase("recover", { code })
79
+ () => recoverRunInfra(adapter, target, json)
156
80
  );
157
- for (const mutation of state.mutations) stream.mutation(mutation);
158
- for (const recovery of state.recovered) stream.recovery(recovery);
159
- if (violation !== null) {
160
- const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
161
- stream.error({
162
- code: violation.code,
163
- message: violation.message,
164
- userAction,
165
- originalError: violation.originalError ?? null
166
- });
167
- return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
168
- }
81
+ if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
169
82
  const report = writeRunReport(result);
170
83
  const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
171
- const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
172
- if (stream.enabled) {
173
- if (result.status === "fail") {
174
- stream.error({
175
- code: "RECIPE_EXECUTION_FAILED",
176
- message: "recipe execution failed; inspect the persisted result and evidence paths",
177
- userAction: failureUserAction
178
- });
179
- }
180
- stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
181
- adapter,
182
- reportPath: report.path,
183
- summaryPath: result.summaryPath,
184
- tracePath: result.tracePath,
185
- artifactManifestPath: result.artifactManifestPath,
186
- recovered: state.recovered,
187
- mutations: state.mutations
188
- });
189
- } else if (json) {
84
+ if (json) {
190
85
  console.log(
191
86
  JSON.stringify(
192
87
  {
@@ -198,8 +93,7 @@ async function handleRunInner({ positional, options }, stream) {
198
93
  recovered: state.recovered,
199
94
  mutations: state.mutations,
200
95
  reportPath: report.path,
201
- result,
202
- ...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
96
+ result
203
97
  },
204
98
  null,
205
99
  2
@@ -212,61 +106,14 @@ async function handleRunInner({ positional, options }, stream) {
212
106
  console.log(out("label", "summary:"));
213
107
  for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
214
108
  }
215
- const artifacts = runArtifactInventory(result.artifactManifestPath);
216
- console.log(out("label", `artifacts (${artifacts.length}):`));
217
- for (const artifact of artifacts) {
218
- console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
219
- }
220
- if (result.status === "fail") {
221
- console.error(` Next: ${failureUserAction}`);
222
- }
223
- console.log(out("label", "diagnostics:"));
224
- const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
225
- for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
226
- console.log(` ${formatDiagnosticLine(line, out)}`);
227
- }
109
+ console.log(`${out("label", "report:")} ${out("path", report.path)}`);
110
+ console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
228
111
  }
229
112
  return exitCode;
230
113
  } finally {
231
114
  lock.release();
232
115
  }
233
116
  }
234
- function formatDiagnosticLine(line, out) {
235
- const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
236
- if (!match) return line;
237
- const status = match[1];
238
- const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
239
- return `${out(style, status)}${match[2]}`;
240
- }
241
- function runArtifactInventory(manifestPathValue) {
242
- const manifestPath = path.resolve(String(manifestPathValue));
243
- const root = path.dirname(manifestPath);
244
- let manifest;
245
- try {
246
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
247
- } catch {
248
- return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
249
- }
250
- const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
251
- const artifacts = [];
252
- for (const entry of entries) {
253
- if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
254
- const absolutePath = path.resolve(root, entry.path);
255
- const basename = path.basename(absolutePath);
256
- const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
257
- artifacts.push({ absolutePath, basename, label });
258
- }
259
- artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
260
- const priority = /* @__PURE__ */ new Map([
261
- ["diagnostics.json", 0],
262
- ["report.md", 1],
263
- ["summary.json", 2],
264
- ["trace.json", 3],
265
- ["recipe.json", 4],
266
- ["artifact-manifest.json", 100]
267
- ]);
268
- return artifacts.filter((artifact, index, all) => all.findIndex((candidate) => candidate.absolutePath === artifact.absolutePath) === index).sort((left, right) => (priority.get(left.basename) ?? 50) - (priority.get(right.basename) ?? 50));
269
- }
270
117
  function formatPreviewLine(line, out) {
271
118
  const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
272
119
  if (!match) return line;
@@ -338,29 +185,12 @@ function loadFlowCatalogs(librarySources) {
338
185
  function actionUsesCoreController(action) {
339
186
  return typeof action === "string" && action.startsWith("metamask.perps.");
340
187
  }
341
- async function handleRunPlan(recipeArg, options, stream) {
188
+ async function handleRunPlan(recipeArg, options) {
342
189
  const json = optionFlag(options, "json");
343
- const jsonOutput = json && !stream.enabled;
344
190
  const { adapter, target } = resolveAdapter(options);
345
- stream.phase("resolve", { adapter, target, recipe: recipeArg });
346
- stream.phase("validate");
347
191
  const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
348
192
  if (validated.usageError) {
349
- const userAction = runUsageRecovery(
350
- validated.usageError.code,
351
- validated.usageError.message,
352
- adapter,
353
- validated.recipeFile
354
- );
355
- return emitPlanUsageError(
356
- jsonOutput,
357
- stream,
358
- adapter,
359
- validated.recipeFile,
360
- validated.usageError.code,
361
- validated.usageError.message,
362
- userAction
363
- );
193
+ return emitPlanUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
364
194
  }
365
195
  const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
366
196
  const status = errorCount === 0 ? "pass" : "fail";
@@ -414,27 +244,9 @@ async function handleRunPlan(recipeArg, options, stream) {
414
244
  detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
415
245
  }
416
246
  ];
417
- const payload = {
418
- schemaVersion: 1,
419
- command: "run",
420
- mode: "plan",
421
- status,
422
- adapter,
423
- recipe: recipeFile,
424
- findings,
425
- plan
426
- };
427
- if (status === "fail") {
428
- payload.error = {
429
- code: "RECIPE_VALIDATION_FAILED",
430
- message: `recipe validation found ${errorCount} error(s)`,
431
- userAction: runPlanProbe(adapter, recipeFile)
432
- };
433
- }
434
- if (stream.enabled) {
435
- if (status === "fail") stream.error(payload.error);
436
- stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
437
- } else if (json) {
247
+ if (json) {
248
+ const payload = { schemaVersion: 1, command: "run", mode: "plan", status, adapter, recipe: recipeFile, findings, plan };
249
+ if (status === "fail") payload.error = { code: "RECIPE_VALIDATION_FAILED", message: `recipe validation found ${errorCount} error(s)` };
438
250
  console.log(JSON.stringify(payload, null, 2));
439
251
  } else {
440
252
  console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
@@ -448,61 +260,52 @@ async function handleRunPlan(recipeArg, options, stream) {
448
260
  console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
449
261
  }
450
262
  }
451
- if (status === "fail") console.error(` Next: ${runPlanProbe(adapter, recipeFile)}`);
452
263
  }
453
264
  return status === "pass" ? EXIT.ok : EXIT.validation;
454
265
  }
455
- function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
456
- stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
266
+ function emitPlanUsageError(json, adapter, recipeFile, code, message) {
457
267
  if (json) {
458
268
  console.log(
459
269
  JSON.stringify(
460
- { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message, userAction } },
270
+ { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
461
271
  null,
462
272
  2
463
273
  )
464
274
  );
465
275
  } else {
466
- console.error(`\u2717 run --plan: ${message}
467
- Next: ${userAction}`);
276
+ console.error(`\u2717 run --plan: ${message}`);
468
277
  }
469
278
  return EXIT.usage;
470
279
  }
471
280
  function runArgLooksLikeRecipeFile(value) {
472
281
  return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
473
282
  }
474
- function emitRunRecipeRunning(json, stream, target, detail) {
283
+ function emitRunRecipeRunning(json, detail) {
475
284
  const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
476
- const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
477
- stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
478
285
  if (json) {
479
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message, userAction } }, null, 2));
286
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
480
287
  } else {
481
- console.error(`\u2717 mm-harness: ${message}
482
- Next: ${userAction}`);
288
+ console.error(`\u2717 mm-harness: ${message}`);
483
289
  }
484
290
  return EXIT.bounded;
485
291
  }
486
- function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
487
- stream.error({ code, message, userAction });
292
+ function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
488
293
  if (json) {
489
294
  console.log(
490
295
  JSON.stringify(
491
- { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
296
+ { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, ...userAction ? { userAction } : {} } },
492
297
  null,
493
298
  2
494
299
  )
495
300
  );
496
301
  } else {
497
302
  console.error(`\u2717 run: ${message}`);
498
- console.error(` Next: ${userAction}`);
303
+ if (userAction) console.error(` Next: ${userAction}`);
499
304
  }
500
305
  return EXIT.usage;
501
306
  }
502
- function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount) {
307
+ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
503
308
  const message = `recipe validation found ${errorCount} error(s)`;
504
- const userAction = runPlanProbe(adapter, recipeFile);
505
- stream.error({ code: "RECIPE_VALIDATION_FAILED", message, userAction, findings });
506
309
  if (json) {
507
310
  console.log(
508
311
  JSON.stringify(
@@ -516,7 +319,7 @@ function emitRunValidationError(json, stream, adapter, recipeFile, findings, err
516
319
  mutations: [],
517
320
  recipe: recipeFile,
518
321
  findings,
519
- error: { code: "RECIPE_VALIDATION_FAILED", message, userAction }
322
+ error: { code: "RECIPE_VALIDATION_FAILED", message }
520
323
  },
521
324
  null,
522
325
  2
@@ -527,22 +330,9 @@ function emitRunValidationError(json, stream, adapter, recipeFile, findings, err
527
330
  for (const finding of findings) {
528
331
  if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
529
332
  }
530
- console.error(` Next: ${userAction}`);
531
333
  }
532
334
  return EXIT.validation;
533
335
  }
534
- function runPlanProbe(adapter, recipeFile) {
535
- return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
536
- }
537
- function runUsageRecovery(code, message, adapter, recipeFile) {
538
- const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
539
- if (actionCommand) return actionCommand;
540
- if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
541
- if (code === "RECIPE_UNPARSEABLE") {
542
- return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
543
- }
544
- return runPlanProbe(adapter, recipeFile);
545
- }
546
336
  export {
547
337
  handleRun
548
338
  };
@@ -3,15 +3,6 @@ import path from "node:path";
3
3
  import { detectAdapter } from "../harness.js";
4
4
  import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
5
5
  const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
6
- function writeInteractiveProgress(json, message, {
7
- stdoutIsTTY = Boolean(process.stdout.isTTY),
8
- stream = process.stderr
9
- } = {}) {
10
- if (json || !stdoutIsTTY) return false;
11
- stream.write(`${message}
12
- `);
13
- return true;
14
- }
15
6
  function parseFlags(argv, booleans) {
16
7
  const positional = [];
17
8
  const options = {};
@@ -147,16 +138,11 @@ function spawnScriptStreaming(script, args, cwd, options) {
147
138
  let settled = false;
148
139
  let didTimeout = false;
149
140
  let escalation;
150
- const parentSignals = ["SIGINT", "SIGTERM"];
151
- const removeParentSignalHandlers = () => {
152
- for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
153
- };
154
141
  const finish = (result) => {
155
142
  if (settled) return;
156
143
  settled = true;
157
144
  if (timer) clearTimeout(timer);
158
- if (escalation) clearTimeout(escalation);
159
- removeParentSignalHandlers();
145
+ if (escalation && !didTimeout) clearTimeout(escalation);
160
146
  resolve(result);
161
147
  };
162
148
  const signalChildTree = (signal) => {
@@ -166,14 +152,6 @@ function spawnScriptStreaming(script, args, cwd, options) {
166
152
  } catch {
167
153
  }
168
154
  };
169
- const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
170
- signalChildTree(signal);
171
- removeParentSignalHandlers();
172
- process.kill(process.pid, signal);
173
- }]));
174
- if (ownsProcessGroup) {
175
- for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
176
- }
177
155
  const timeoutMs = spawnOptions.timeoutMs;
178
156
  const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
179
157
  didTimeout = true;
@@ -204,13 +182,8 @@ function spawnScriptStreaming(script, args, cwd, options) {
204
182
  `);
205
183
  finish({ status: 1, output: message });
206
184
  });
207
- child.on("exit", () => {
208
- if (!didTimeout) return;
209
- signalChildTree("SIGKILL");
210
- finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
211
- });
212
185
  child.on("close", (status) => {
213
- if (didTimeout) signalChildTree("SIGKILL");
186
+ if (didTimeout && escalation) return;
214
187
  finish({
215
188
  status: didTimeout ? 1 : status ?? 1,
216
189
  output,
@@ -235,33 +208,9 @@ function usageOut(json, command, message, userAction) {
235
208
  }
236
209
  return EXIT.usage;
237
210
  }
238
- function checkoutBusyOut(json, command, message, lockPath) {
239
- const userAction = `wait for the current owner, or inspect ${lockPath} if its process has exited`;
240
- if (json) {
241
- console.log(
242
- JSON.stringify(
243
- {
244
- schemaVersion: 1,
245
- command,
246
- status: "fail",
247
- exitCode: EXIT.bounded,
248
- recoverable: false,
249
- error: { code: "SANDBOX_BUSY", message, userAction }
250
- },
251
- null,
252
- 2
253
- )
254
- );
255
- } else {
256
- console.error(`\u2717 mm-harness ${command}: ${message}
257
- Next: ${userAction}`);
258
- }
259
- return EXIT.bounded;
260
- }
261
211
  export {
262
212
  ADAPTER_DETECT_NEXT,
263
213
  EXIT,
264
- checkoutBusyOut,
265
214
  flag,
266
215
  parseFlags,
267
216
  resolveAdapter,
@@ -271,6 +220,5 @@ export {
271
220
  spawnScriptStreaming,
272
221
  str,
273
222
  targetOf,
274
- usageOut,
275
- writeInteractiveProgress
223
+ usageOut
276
224
  };
@@ -34,7 +34,7 @@ function parseBridgeEntries(raw) {
34
34
  deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
35
35
  platform: typeof e.platform === "string" ? e.platform : "",
36
36
  // agenticPresent is only set when the bridge explicitly sends it (false = absent).
37
- // An omitted value remains unknown.
37
+ // Omitted by older bridge versions → leave undefined so caller treats as present.
38
38
  ...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
39
39
  }));
40
40
  }
@@ -31,7 +31,7 @@ async function handleStatus({ options }) {
31
31
  if (adapter === "mobile") {
32
32
  const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
33
33
  if ("code" in dtResult) {
34
- return usageOut(json, "status", dtResult.message, dtResult.userAction);
34
+ return usageOut(json, "status", dtResult.message, "mm-harness status --device <id> or --all-devices");
35
35
  }
36
36
  }
37
37
  const next = surface.hints.relaunch;
@@ -5,8 +5,7 @@ import {
5
5
  parseArgs,
6
6
  optionFlag,
7
7
  optionString,
8
- resolveAdapter,
9
- shellQuote
8
+ resolveAdapter
10
9
  } from "./parse-args.js";
11
10
  async function handleStop(argv) {
12
11
  const { options } = parseArgs(argv, "stop");
@@ -23,8 +22,6 @@ async function handleStop(argv) {
23
22
  if (stop.kind === "headless") {
24
23
  return usageOut(json, "stop", stop.message, stop.userAction);
25
24
  }
26
- const next = adapter === "extension" ? `${surface.hints.launch} --target ${shellQuote(target)}` : void 0;
27
- const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
28
25
  if (json) {
29
26
  console.log(
30
27
  JSON.stringify(
@@ -36,8 +33,7 @@ async function handleStop(argv) {
36
33
  status: stop.status === 0 ? "pass" : "fail",
37
34
  ...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
38
35
  exitCode: stop.status,
39
- ...stop.output ? { output: stop.output } : {},
40
- ...stop.status === 0 ? next ? { next } : {} : { error: { code: "DEV_SERVER_STOP_FAILED", message: stop.summary, userAction } }
36
+ ...stop.output ? { output: stop.output } : {}
41
37
  },
42
38
  null,
43
39
  2
@@ -47,7 +43,6 @@ async function handleStop(argv) {
47
43
  if (stop.output) process.stderr.write(`${stop.output}
48
44
  `);
49
45
  console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
50
- if (stop.status !== 0 || next) console.error(` Next: ${stop.status === 0 ? next : userAction}`);
51
46
  }
52
47
  return stop.status;
53
48
  }
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from "node:child_process";
1
+ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -6,8 +6,7 @@ import { runnerDir } from "../paths.js";
6
6
  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
- const NUDGE_FETCH_TIMEOUT_MS = 2e3;
10
- const REGISTRY_DIST_TAGS_ENDPOINT = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
9
+ const NUDGE_FETCH_TIMEOUT_MS = 300;
11
10
  function currentVersion() {
12
11
  try {
13
12
  const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
@@ -57,6 +56,24 @@ function fetchLatest(timeoutMs) {
57
56
  if (!latest) return { error: { kind: "unreachable", message: "npm returned no dist-tag for latest" } };
58
57
  return { latest };
59
58
  }
59
+ async function fetchLatestAsync(timeoutMs) {
60
+ const controller = new AbortController();
61
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
62
+ try {
63
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
64
+ const res = await fetch(url, { signal: controller.signal });
65
+ if (!res.ok) return { error: { kind: "unreachable", message: `registry returned HTTP ${res.status}` } };
66
+ const data = await res.json();
67
+ const latest = data["latest"];
68
+ if (!latest) return { error: { kind: "unreachable", message: "npm registry returned no latest dist-tag" } };
69
+ return { latest };
70
+ } catch (err) {
71
+ const msg = err.name === "AbortError" ? `registry fetch timed out after ${timeoutMs}ms` : err.message;
72
+ return { error: { kind: "unreachable", message: msg } };
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
60
77
  function teachFailure(json, current, code, message, userAction) {
61
78
  if (json) {
62
79
  console.log(
@@ -194,28 +211,23 @@ function nudgeDisabled() {
194
211
  function nudgeLine(current, latest) {
195
212
  return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
196
213
  }
197
- function maybeNudge(now = Date.now()) {
214
+ async function maybeNudge(now = Date.now()) {
198
215
  if (nudgeDisabled()) return;
199
216
  const file = cacheFile();
200
217
  const cache = readCache(file);
201
218
  const current = currentVersion();
219
+ let latest = cache?.latest ?? "";
202
220
  process.once("exit", () => {
203
- const line = nudgeLine(current, cache?.latest ?? "");
221
+ const line = nudgeLine(current, latest);
204
222
  if (line) process.stderr.write(`${line}
205
223
  `);
206
224
  });
207
225
  if (!cache || now - cache.lastCheck >= NUDGE_INTERVAL_MS) {
208
- writeCache(file, { lastCheck: now, latest: cache?.latest ?? "" });
209
- const worker = path.join(runnerDir, "adapters", "shared", "update-check-worker.mjs");
210
- if (!fs.existsSync(worker)) return;
211
- try {
212
- const child = spawn(
213
- process.execPath,
214
- [worker, file, String(NUDGE_FETCH_TIMEOUT_MS), REGISTRY_DIST_TAGS_ENDPOINT],
215
- { detached: true, stdio: "ignore" }
216
- );
217
- child.unref();
218
- } catch {
226
+ writeCache(file, { lastCheck: now, latest });
227
+ const fetched = await fetchLatestAsync(NUDGE_FETCH_TIMEOUT_MS);
228
+ if (fetched.latest) {
229
+ latest = fetched.latest;
230
+ writeCache(file, { lastCheck: now, latest });
219
231
  }
220
232
  }
221
233
  }