@deeeed/metamask-harness 0.17.5 → 0.19.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 (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
@@ -25,10 +25,19 @@ function renderRunReport(summary, entries) {
25
25
  "",
26
26
  `Status: ${String(summary.status ?? "unknown")}`,
27
27
  `Duration: ${formatDuration(Number(summary.durationMs ?? 0))}`,
28
- `Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`,
29
- "",
30
- "## Steps"
28
+ `Nodes: ${Number(summary.passed ?? 0)}/${Number(summary.total ?? entries.length)} passed`
31
29
  ];
30
+ const sideFindings = isRecord(summary.sideFindings) ? summary.sideFindings : void 0;
31
+ const sideFindingCounts = sideFindings && isRecord(sideFindings.counts) ? sideFindings.counts : void 0;
32
+ const sideFindingTotal = Number(sideFindingCounts?.total ?? 0);
33
+ if (sideFindingTotal > 0) {
34
+ lines.push(
35
+ "",
36
+ "## Side findings",
37
+ `- REVIEW ${sideFindingTotal} distinct application warning/error event(s) (non-blocking; expanded below and stored in diagnostics.json)`
38
+ );
39
+ }
40
+ lines.push("", "## Steps");
32
41
  for (const entry of entries) {
33
42
  if (!isRecord(entry)) continue;
34
43
  const mark = entry.ok === false ? "FAIL" : "PASS";
@@ -3,85 +3,283 @@ 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 { EXIT } from "./shared.js";
6
+ import { checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress } from "./shared.js";
7
7
  import {
8
8
  optionFlag,
9
9
  optionString,
10
- requiredOption,
11
10
  resolveAdapter,
12
11
  runtimeOptionsFromCli,
13
12
  isRecord,
13
+ parseRecipeParamAssignments,
14
+ shellQuote,
15
+ targetPath,
14
16
  usageError
15
17
  } from "./parse-args.js";
16
18
  import {
17
19
  countRecipeNodes,
18
20
  emitHealViolation,
19
21
  executeWithHealBounds,
22
+ preflightRecipe,
20
23
  prepareHeal,
21
24
  recoverRunInfra,
22
25
  runRecipe,
23
26
  validateRunRecipeStatic
24
27
  } from "./run-engine.js";
25
- import { handleListExecutables } from "./list-executables.js";
28
+ import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
26
29
  import { applyDeviceTargeting } from "./device-target.js";
27
30
  import { getAdapterSurface } from "../adapters/surface.js";
28
31
  import { coreDependencyBlock } from "./core-readiness.js";
29
32
  import { writeRunReport } from "./run-report.js";
30
33
  import { acquireCheckoutLock } from "../checkout-lock.js";
31
- async function handleRun({ positional, options }) {
32
- if (optionFlag(options, "list")) return handleListExecutables("run", options);
34
+ import { JsonStreamWriter } from "../json-stream.js";
35
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
36
+ import { recipeTrustFailure } from "../recipe-security.js";
37
+ import { recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
38
+ async function handleRun(parsed) {
39
+ const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
40
+ const restoreStdout = stream.isolateStdout();
41
+ const target = targetPath(parsed.options);
42
+ try {
43
+ const exitCode = await handleRunInner(parsed, stream);
44
+ stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
45
+ return exitCode;
46
+ } catch (error) {
47
+ const trustFailure = recipeTrustFailure(error);
48
+ if (trustFailure) {
49
+ if (stream.enabled) {
50
+ stream.error(trustFailure);
51
+ stream.complete("fail", EXIT.validation);
52
+ } else if (optionFlag(parsed.options, "json")) {
53
+ console.log(JSON.stringify({
54
+ schemaVersion: 1,
55
+ command: "run",
56
+ status: "fail",
57
+ error: trustFailure,
58
+ exitCode: EXIT.validation
59
+ }, null, 2));
60
+ } else {
61
+ console.error(`\u2717 mm-harness run: ${trustFailure.message}`);
62
+ const blocked = trustFailure.details?.blocked;
63
+ if (blocked?.length) {
64
+ console.error(" Restricted plan nodes:");
65
+ for (const node of blocked.slice(0, 10)) {
66
+ const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
67
+ console.error(
68
+ ` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
69
+ );
70
+ }
71
+ if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
72
+ }
73
+ console.error(` Next: ${trustFailure.userAction}`);
74
+ }
75
+ return EXIT.validation;
76
+ }
77
+ const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
78
+ stream.error({
79
+ code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
80
+ message: error instanceof Error ? error.message : String(error),
81
+ userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
82
+ });
83
+ stream.complete("fail", exitCode);
84
+ throw error;
85
+ } finally {
86
+ restoreStdout();
87
+ }
88
+ }
89
+ async function handleRunInner({ positional, options }, stream) {
90
+ if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
91
+ const message = "--describe cannot be combined with --list or --plan.";
92
+ const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
93
+ if (stream.enabled) {
94
+ stream.error({ code: "CLI_USAGE_ERROR", message, userAction });
95
+ return EXIT.usage;
96
+ }
97
+ return usageOut(optionFlag(options, "json"), "run", message, userAction);
98
+ }
99
+ if (optionFlag(options, "list")) {
100
+ if (stream.enabled) {
101
+ const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
102
+ stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
103
+ return EXIT.usage;
104
+ }
105
+ return handleListExecutables("run", options);
106
+ }
33
107
  const targetRecipe = positional[0];
34
108
  if (!targetRecipe) throw usageError("run requires <recipe.json>.");
35
- if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
109
+ const paramAssignments = positional.slice(1);
110
+ if (optionFlag(options, "describe")) {
111
+ if (paramAssignments.length > 0) {
112
+ throw usageError("run --describe does not accept parameter values; inspect the declaration first.");
113
+ }
114
+ if (stream.enabled) {
115
+ const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
116
+ stream.error({
117
+ code: "JSON_STREAM_UNSUPPORTED_MODE",
118
+ message,
119
+ userAction: `mm-harness run ${shellQuote(targetRecipe)} --describe --json`
120
+ });
121
+ return EXIT.usage;
122
+ }
123
+ return handleDescribeRecipe(targetRecipe, options);
124
+ }
125
+ const params = parseRecipeParamAssignments(paramAssignments);
126
+ if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, params, options, stream);
36
127
  const { adapter, target } = resolveAdapter(options);
37
128
  const json = optionFlag(options, "json");
129
+ const jsonOutput = json && !stream.enabled;
130
+ const machine = json || stream.enabled;
131
+ stream.phase("resolve", { adapter, target, recipe: targetRecipe });
132
+ if (!fs.existsSync(target)) {
133
+ const message = `target does not exist: ${target}`;
134
+ const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
135
+ if (stream.enabled) {
136
+ stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
137
+ return EXIT.usage;
138
+ }
139
+ return usageOut(jsonOutput, "run", message, userAction);
140
+ }
141
+ writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
38
142
  getAdapterSurface(adapter).resolveSlotPorts(target);
39
143
  const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
40
144
  if ("code" in dtResult) {
41
- return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
145
+ return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
42
146
  }
43
- if (recipeRunning(target)) return emitRunRecipeRunning(json);
44
- if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
45
- requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
46
- }
47
- const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
147
+ if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
148
+ stream.phase("validate");
149
+ const validated = await validateRunRecipeStatic(targetRecipe, adapter, options, params);
48
150
  if (validated.usageError) {
49
- return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
151
+ const userAction = runUsageRecovery(
152
+ validated.usageError.code,
153
+ validated.usageError.message,
154
+ adapter,
155
+ validated.recipeFile
156
+ );
157
+ return emitRunUsageError(
158
+ jsonOutput,
159
+ stream,
160
+ adapter,
161
+ validated.recipeFile,
162
+ validated.usageError.code,
163
+ validated.usageError.message,
164
+ userAction
165
+ );
50
166
  }
51
167
  if (validated.errorCount > 0) {
52
- return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
168
+ return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
53
169
  }
54
- const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
170
+ const depsBlock = adapter === "core" && await recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
55
171
  if (depsBlock) {
56
- return emitRunUsageError(json, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
172
+ return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
57
173
  }
58
- const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
174
+ let artifactsDir;
175
+ try {
176
+ artifactsDir = resolveRunArtifactsDir(target, optionString(options, "artifactsDir"));
177
+ } catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ return emitRunUsageError(
180
+ jsonOutput,
181
+ stream,
182
+ adapter,
183
+ validated.recipeFile,
184
+ "ARTIFACT_DIR_INVALID",
185
+ message,
186
+ "set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
187
+ );
188
+ }
189
+ recordCommandEvidence(artifactsDir);
190
+ const librarySources = validated.librarySources;
191
+ const runtimeOptions = {
192
+ ...runtimeOptionsFromCli(options),
193
+ params,
194
+ ...librarySources ? { librarySources } : {},
195
+ stdoutIsMachineContract: machine,
196
+ onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
197
+ };
198
+ stream.phase("authorize");
199
+ let preflightedExecution = await preflightRecipe(
200
+ adapter,
201
+ validated.recipeFile,
202
+ artifactsDir,
203
+ target,
204
+ optionString(options, "actionManifest"),
205
+ runtimeOptions
206
+ );
59
207
  const lock = acquireCheckoutLock(target, "run");
60
- if ("message" in lock) return emitRunRecipeRunning(json, lock.message);
208
+ if ("message" in lock) {
209
+ const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
210
+ stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
211
+ return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
212
+ }
61
213
  try {
62
- const prepared = await prepareHeal(adapter, target, options, json);
63
- if (typeof prepared === "number") return prepared;
214
+ const prepared = await prepareHeal(adapter, target, options, machine, {
215
+ onPhase: (phase, fields) => stream.phase(phase, fields)
216
+ });
217
+ if (typeof prepared === "number") {
218
+ stream.error({
219
+ code: "RUN_PREPARE_FAILED",
220
+ message: "runtime preparation failed; inspect stderr for the exact probe",
221
+ userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
222
+ });
223
+ return prepared;
224
+ }
64
225
  const { state, heal } = prepared;
65
- const librarySources = validated.librarySources;
66
- const runtimeOptions = {
67
- ...runtimeOptionsFromCli(options),
68
- ...librarySources ? { librarySources } : {},
69
- stdoutIsMachineContract: json
70
- };
226
+ stream.phase("execute");
71
227
  const { result, violation } = await executeWithHealBounds(
72
228
  // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
73
229
  // that only the resolver knows how to turn into a file.
74
- () => runRecipe(adapter, validated.recipeFile, artifactsDir, target, optionString(options, "actionManifest"), runtimeOptions),
230
+ () => {
231
+ const execution = preflightedExecution;
232
+ preflightedExecution = void 0;
233
+ return runRecipe(
234
+ adapter,
235
+ validated.recipeFile,
236
+ artifactsDir,
237
+ target,
238
+ optionString(options, "actionManifest"),
239
+ runtimeOptions,
240
+ execution
241
+ );
242
+ },
75
243
  adapter,
76
244
  target,
77
245
  heal,
78
246
  state,
79
- () => recoverRunInfra(adapter, target, json)
247
+ () => recoverRunInfra(adapter, target, machine),
248
+ (code) => stream.phase("recover", { code })
80
249
  );
81
- if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
250
+ for (const mutation of state.mutations) stream.mutation(mutation);
251
+ for (const recovery of state.recovered) stream.recovery(recovery);
252
+ if (violation !== null) {
253
+ const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
254
+ stream.error({
255
+ code: violation.code,
256
+ message: violation.message,
257
+ userAction,
258
+ originalError: violation.originalError ?? null
259
+ });
260
+ return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
261
+ }
82
262
  const report = writeRunReport(result);
83
263
  const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
84
- if (json) {
264
+ const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
265
+ if (stream.enabled) {
266
+ if (result.status === "fail") {
267
+ stream.error({
268
+ code: "RECIPE_EXECUTION_FAILED",
269
+ message: "recipe execution failed; inspect the persisted result and evidence paths",
270
+ userAction: failureUserAction
271
+ });
272
+ }
273
+ stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
274
+ adapter,
275
+ reportPath: report.path,
276
+ summaryPath: result.summaryPath,
277
+ tracePath: result.tracePath,
278
+ artifactManifestPath: result.artifactManifestPath,
279
+ recovered: state.recovered,
280
+ mutations: state.mutations
281
+ });
282
+ } else if (json) {
85
283
  console.log(
86
284
  JSON.stringify(
87
285
  {
@@ -93,7 +291,8 @@ async function handleRun({ positional, options }) {
93
291
  recovered: state.recovered,
94
292
  mutations: state.mutations,
95
293
  reportPath: report.path,
96
- result
294
+ result,
295
+ ...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
97
296
  },
98
297
  null,
99
298
  2
@@ -106,14 +305,75 @@ async function handleRun({ positional, options }) {
106
305
  console.log(out("label", "summary:"));
107
306
  for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
108
307
  }
109
- console.log(`${out("label", "report:")} ${out("path", report.path)}`);
110
- console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
308
+ const artifacts = runArtifactInventory(result.artifactManifestPath);
309
+ console.log(out("label", `artifacts (${artifacts.length}):`));
310
+ for (const artifact of artifacts) {
311
+ console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
312
+ }
313
+ if (result.status === "fail") {
314
+ console.error(` Next: ${failureUserAction}`);
315
+ }
316
+ console.log(out("label", "diagnostics:"));
317
+ const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
318
+ for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
319
+ console.log(` ${formatDiagnosticLine(line, out)}`);
320
+ }
111
321
  }
112
322
  return exitCode;
113
323
  } finally {
114
324
  lock.release();
115
325
  }
116
326
  }
327
+ function formatDiagnosticLine(line, out) {
328
+ const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
329
+ if (!match) return line;
330
+ const status = match[1];
331
+ const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
332
+ return `${out(style, status)}${match[2]}`;
333
+ }
334
+ function resolveRunArtifactsDir(target, explicit) {
335
+ if (explicit !== void 0) return path.resolve(explicit);
336
+ const taskDir = process.env.RECIPE_TASK_DIR || process.env.FARMSLOT_TASK_DIR;
337
+ if (taskDir) {
338
+ const resolvedTask = path.resolve(target, taskDir);
339
+ const relative = path.relative(path.resolve(target), resolvedTask);
340
+ if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
341
+ return path.join(resolvedTask, "artifacts");
342
+ }
343
+ throw new Error(`task directory must be inside the target checkout: ${taskDir}`);
344
+ }
345
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/gu, "");
346
+ return path.join(target, "temp", "recipe", "runs", `${stamp}-${process.pid}`);
347
+ }
348
+ function runArtifactInventory(manifestPathValue) {
349
+ const manifestPath = path.resolve(String(manifestPathValue));
350
+ const root = path.dirname(manifestPath);
351
+ let manifest;
352
+ try {
353
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
354
+ } catch {
355
+ return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
356
+ }
357
+ const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
358
+ const artifacts = [];
359
+ for (const entry of entries) {
360
+ if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
361
+ const absolutePath = path.resolve(root, entry.path);
362
+ const basename = path.basename(absolutePath);
363
+ const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
364
+ artifacts.push({ absolutePath, basename, label });
365
+ }
366
+ artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
367
+ const priority = /* @__PURE__ */ new Map([
368
+ ["diagnostics.json", 0],
369
+ ["report.md", 1],
370
+ ["summary.json", 2],
371
+ ["trace.json", 3],
372
+ ["recipe.json", 4],
373
+ ["artifact-manifest.json", 100]
374
+ ]);
375
+ 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));
376
+ }
117
377
  function formatPreviewLine(line, out) {
118
378
  const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
119
379
  if (!match) return line;
@@ -121,19 +381,22 @@ function formatPreviewLine(line, out) {
121
381
  const rest = match[2];
122
382
  return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
123
383
  }
124
- function recipeUsesCoreController(recipe, librarySources) {
384
+ async function recipeUsesCoreController(recipe, librarySources) {
125
385
  if (objectUsesCoreController(recipe)) return true;
126
386
  const refs = collectCallRefs(recipe);
127
387
  if (refs.length === 0) return false;
128
- const flows = loadFlowCatalogs(librarySources ?? []);
388
+ const sources = librarySources ?? [];
389
+ if (sources.length === 0) return false;
390
+ const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
391
+ const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
129
392
  const visited = /* @__PURE__ */ new Set();
130
393
  const visit = (ref) => {
131
394
  if (visited.has(ref)) return false;
132
395
  visited.add(ref);
133
- const flow = flows.get(ref);
134
- if (!flow) return false;
135
- if (objectUsesCoreController(flow)) return true;
136
- return collectCallRefs(flow).some(visit);
396
+ const dependency = recipes.get(ref);
397
+ if (!dependency) return false;
398
+ if (objectUsesCoreController(dependency.document)) return true;
399
+ return collectCallRefs(dependency.document).some(visit);
137
400
  };
138
401
  return refs.some(visit);
139
402
  }
@@ -157,42 +420,34 @@ function collectCallRefs(value) {
157
420
  visit(value);
158
421
  return [...new Set(refs)];
159
422
  }
160
- function loadFlowCatalogs(librarySources) {
161
- const flows = /* @__PURE__ */ new Map();
162
- for (const source of librarySources) {
163
- const flowsDir = path.join(source.root, "flows");
164
- let entries;
165
- try {
166
- entries = fs.readdirSync(flowsDir);
167
- } catch {
168
- continue;
169
- }
170
- for (const entry of entries) {
171
- if (!entry.endsWith(".json")) continue;
172
- try {
173
- const parsed = JSON.parse(fs.readFileSync(path.join(flowsDir, entry), "utf8"));
174
- if (!isRecord(parsed) || !isRecord(parsed.flows)) continue;
175
- for (const [id, flow] of Object.entries(parsed.flows)) {
176
- if (!flows.has(id)) flows.set(id, flow);
177
- }
178
- } catch {
179
- continue;
180
- }
181
- }
182
- }
183
- return flows;
184
- }
185
423
  function actionUsesCoreController(action) {
186
424
  return typeof action === "string" && action.startsWith("metamask.perps.");
187
425
  }
188
- async function handleRunPlan(recipeArg, options) {
426
+ async function handleRunPlan(recipeArg, params, options, stream) {
189
427
  const json = optionFlag(options, "json");
428
+ const jsonOutput = json && !stream.enabled;
190
429
  const { adapter, target } = resolveAdapter(options);
191
- const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
430
+ stream.phase("resolve", { adapter, target, recipe: recipeArg });
431
+ stream.phase("validate");
432
+ const validated = await validateRunRecipeStatic(recipeArg, adapter, options, params);
192
433
  if (validated.usageError) {
193
- return emitPlanUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
434
+ const userAction = runUsageRecovery(
435
+ validated.usageError.code,
436
+ validated.usageError.message,
437
+ adapter,
438
+ validated.recipeFile
439
+ );
440
+ return emitPlanUsageError(
441
+ jsonOutput,
442
+ stream,
443
+ adapter,
444
+ validated.recipeFile,
445
+ validated.usageError.code,
446
+ validated.usageError.message,
447
+ userAction
448
+ );
194
449
  }
195
- const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
450
+ const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid, effectiveParams } = validated;
196
451
  const status = errorCount === 0 ? "pass" : "fail";
197
452
  const nodeCount = countRecipeNodes(recipe);
198
453
  const artifactsDir = optionString(options, "artifactsDir");
@@ -244,9 +499,28 @@ async function handleRunPlan(recipeArg, options) {
244
499
  detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
245
500
  }
246
501
  ];
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)` };
502
+ const payload = {
503
+ schemaVersion: 1,
504
+ command: "run",
505
+ mode: "plan",
506
+ status,
507
+ adapter,
508
+ recipe: recipeFile,
509
+ params: redactStructuredValue(effectiveParams),
510
+ findings,
511
+ plan
512
+ };
513
+ if (status === "fail") {
514
+ payload.error = {
515
+ code: "RECIPE_VALIDATION_FAILED",
516
+ message: `recipe validation found ${errorCount} error(s)`,
517
+ userAction: runPlanProbe(adapter, recipeFile)
518
+ };
519
+ }
520
+ if (stream.enabled) {
521
+ if (status === "fail") stream.error(payload.error);
522
+ stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
523
+ } else if (json) {
250
524
  console.log(JSON.stringify(payload, null, 2));
251
525
  } else {
252
526
  console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
@@ -260,52 +534,58 @@ async function handleRunPlan(recipeArg, options) {
260
534
  console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
261
535
  }
262
536
  }
537
+ if (status === "fail") console.error(` Next: ${runPlanProbe(adapter, recipeFile)}`);
263
538
  }
264
539
  return status === "pass" ? EXIT.ok : EXIT.validation;
265
540
  }
266
- function emitPlanUsageError(json, adapter, recipeFile, code, message) {
541
+ function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
542
+ stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
267
543
  if (json) {
268
544
  console.log(
269
545
  JSON.stringify(
270
- { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
546
+ { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message, userAction } },
271
547
  null,
272
548
  2
273
549
  )
274
550
  );
275
551
  } else {
276
- console.error(`\u2717 run --plan: ${message}`);
552
+ console.error(`\u2717 run --plan: ${message}
553
+ Next: ${userAction}`);
277
554
  }
278
555
  return EXIT.usage;
279
556
  }
280
- function runArgLooksLikeRecipeFile(value) {
281
- return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
282
- }
283
- function emitRunRecipeRunning(json, detail) {
557
+ function emitRunRecipeRunning(json, stream, target, detail) {
284
558
  const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
559
+ const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
560
+ stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
285
561
  if (json) {
286
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
562
+ console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message, userAction } }, null, 2));
287
563
  } else {
288
- console.error(`\u2717 mm-harness: ${message}`);
564
+ console.error(`\u2717 mm-harness: ${message}
565
+ Next: ${userAction}`);
289
566
  }
290
567
  return EXIT.bounded;
291
568
  }
292
- function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
569
+ function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
570
+ stream.error({ code, message, userAction });
293
571
  if (json) {
294
572
  console.log(
295
573
  JSON.stringify(
296
- { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, ...userAction ? { userAction } : {} } },
574
+ { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
297
575
  null,
298
576
  2
299
577
  )
300
578
  );
301
579
  } else {
302
580
  console.error(`\u2717 run: ${message}`);
303
- if (userAction) console.error(` Next: ${userAction}`);
581
+ console.error(` Next: ${userAction}`);
304
582
  }
305
583
  return EXIT.usage;
306
584
  }
307
- function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
585
+ function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount) {
308
586
  const message = `recipe validation found ${errorCount} error(s)`;
587
+ const userAction = runPlanProbe(adapter, recipeFile);
588
+ stream.error({ code: "RECIPE_VALIDATION_FAILED", message, userAction, findings });
309
589
  if (json) {
310
590
  console.log(
311
591
  JSON.stringify(
@@ -319,7 +599,7 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
319
599
  mutations: [],
320
600
  recipe: recipeFile,
321
601
  findings,
322
- error: { code: "RECIPE_VALIDATION_FAILED", message }
602
+ error: { code: "RECIPE_VALIDATION_FAILED", message, userAction }
323
603
  },
324
604
  null,
325
605
  2
@@ -330,9 +610,22 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
330
610
  for (const finding of findings) {
331
611
  if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
332
612
  }
613
+ console.error(` Next: ${userAction}`);
333
614
  }
334
615
  return EXIT.validation;
335
616
  }
617
+ function runPlanProbe(adapter, recipeFile) {
618
+ return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
619
+ }
620
+ function runUsageRecovery(code, message, adapter, recipeFile) {
621
+ const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
622
+ if (actionCommand) return actionCommand;
623
+ if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
624
+ if (code === "RECIPE_UNPARSEABLE") {
625
+ return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
626
+ }
627
+ return runPlanProbe(adapter, recipeFile);
628
+ }
336
629
  export {
337
630
  handleRun
338
631
  };