@deeeed/metamask-harness 0.80.0 → 0.82.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 (118) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/adapters/manifest.json +48 -64
  3. package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
  4. package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
  5. package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
  6. package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
  7. package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
  8. package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
  9. package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
  10. package/adapters/web-dapp/lib/policy.mjs +79 -0
  11. package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
  12. package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
  16. package/dist/adapters/core/actions.js +4 -1
  17. package/dist/adapters/extension/actions.js +6 -2
  18. package/dist/adapters/extension/network-observer.js +13 -0
  19. package/dist/adapters/extension/run.js +115 -31
  20. package/dist/adapters/mobile/actions.js +4 -1
  21. package/dist/adapters/mobile/network-observer.js +68 -0
  22. package/dist/adapters/mobile/performance-observer.js +1 -3
  23. package/dist/adapters/mobile/run.js +32 -9
  24. package/dist/adapters/perps-order-inputs.js +36 -0
  25. package/dist/adapters/plugin-surface.js +98 -0
  26. package/dist/adapters/proof-failure.js +11 -0
  27. package/dist/adapters/surface.js +1 -1
  28. package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
  29. package/dist/adapters/web-dapp/console-capture.js +63 -0
  30. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  31. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
  32. package/dist/adapters/web-dapp/overlay.js +21 -0
  33. package/dist/adapters/web-dapp/readiness.js +10 -0
  34. package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
  35. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  36. package/dist/adapters/web-dapp/surface.js +149 -0
  37. package/dist/adapters.js +20 -4
  38. package/dist/cli.js +34 -6
  39. package/dist/command-contract.js +29 -7
  40. package/dist/commands/check.js +32 -8
  41. package/dist/commands/completion-candidates.js +5 -5
  42. package/dist/commands/doctor.js +19 -2
  43. package/dist/commands/farmslot-ready.js +1 -0
  44. package/dist/commands/help.js +1 -1
  45. package/dist/commands/launch/device.js +5 -5
  46. package/dist/commands/self-test.js +10 -5
  47. package/dist/engine.js +47 -0
  48. package/dist/funding-execution-context.js +81 -0
  49. package/dist/manifest.js +48 -24
  50. package/dist/mm-harness-cli.js +20 -3
  51. package/dist/performance-observation.js +7 -15
  52. package/dist/recipe-assistance-context.js +6 -7
  53. package/dist/runner.js +28 -8
  54. package/docs/CONTRIBUTING.md +4 -2
  55. package/docs/RECIPES.md +31 -3
  56. package/docs/cli-contract.md +5 -3
  57. package/library/actions/core/perps/_controller.mjs +24 -28
  58. package/library/actions/core/perps/_rewards.mjs +186 -0
  59. package/library/actions/core/perps/close_positions.mjs +1 -1
  60. package/library/actions/core/perps/place_order.mjs +167 -45
  61. package/library/actions/core/perps/quote_fees.mjs +7 -4
  62. package/library/actions/core/wallet/list_accounts.mjs +1 -1
  63. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  64. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  65. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  66. package/library/actions/extension/perps/perps.mjs +320 -3
  67. package/library/actions/extension/perps/place_order.mjs +3 -1
  68. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  69. package/library/actions/extension/platform/cdp.mjs +71 -3
  70. package/library/actions/mobile/perps/perps.mjs +216 -11
  71. package/library/actions/mobile/perps/place_order.mjs +3 -1
  72. package/library/actions/shared/console-collector.mjs +1 -1
  73. package/library/actions/shared/perps/order-policy.mjs +177 -0
  74. package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
  75. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  76. package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
  77. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
  78. package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
  79. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  80. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  81. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  82. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
  83. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  84. package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
  85. package/library/manifests/core.action-manifest.json +6 -2
  86. package/library/manifests/extension.action-manifest.json +4 -0
  87. package/library/manifests/mobile.action-manifest.json +12 -0
  88. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  89. package/package.json +6 -6
  90. package/site/assets/help-recipes.json +1 -0
  91. package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
  92. package/adapters/terminal/lib/origin.mjs +0 -30
  93. package/adapters/terminal/lib/page-script.mjs +0 -246
  94. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  95. package/dist/adapters/terminal/console-capture.js +0 -59
  96. package/dist/adapters/terminal/doctor-checks.js +0 -22
  97. package/dist/adapters/terminal/overlay.js +0 -18
  98. package/dist/adapters/terminal/readiness.js +0 -11
  99. package/dist/adapters/terminal/surface.js +0 -133
  100. package/dist/commands/call.js +0 -671
  101. package/dist/commands/list-executables.js +0 -245
  102. package/dist/commands/manifest.js +0 -477
  103. package/dist/commands/run-engine.js +0 -1301
  104. package/dist/commands/run.js +0 -730
  105. package/dist/discovery.js +0 -15
  106. package/dist/metamask-action-validation.js +0 -68
  107. package/dist/network-observation.js +0 -290
  108. package/library/actions/terminal/perps/_venue.mjs +0 -242
  109. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  110. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  111. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  112. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  113. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  114. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  115. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  116. package/library/actions/terminal/ui/navigate.mjs +0 -31
  117. package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
  118. package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
@@ -1,730 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { importRecipeHarness, walletFixturePath } from "../paths.js";
4
- import { formatRunDiagnosticsForHuman, indexProductProvenanceArtifact, ProvenanceDriftError, readRunDiagnosticsDocument, recipeRunOptionsFromCli, recipeTrustFailure, writeRunReport, color, acquireCheckoutLock, JsonStreamWriter, recordCommandEvidence, recipeRunning, checkoutBusyOut, EXIT, usageOut, writeInteractiveProgress, optionFlag, optionString, resolveAdapter, isRecord, parseRecipeParamAssignments, shellQuote, targetPath, usageError } from "@farmslot/recipe-cli/harness";
5
- import {
6
- countRecipeNodes,
7
- emitHealViolation,
8
- executeWithHealBounds,
9
- preflightRecipe,
10
- prepareHeal,
11
- persistRunEffects,
12
- runRecipe,
13
- validateRunRecipeStatic
14
- } from "./run-engine.js";
15
- import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
16
- import { applyDeviceTargeting } from "./device-target.js";
17
- import { getAdapterSurface } from "../adapters/surface.js";
18
- import { fundingOptionsFromCli } from "../funding-execution-context.js";
19
- import { actionLibraryContextArgs, resolveActionCapabilityMatrix } from "./manifest.js";
20
- import { missingActionCapabilities } from "@farmslot/recipe-cli";
21
- import {
22
- startRunNetworkObservation
23
- } from "../network-observation.js";
24
- import {
25
- startRunPerformanceObservation
26
- } from "../performance-observation.js";
27
- async function validationCapabilityRefusals(adapter, findings, librarySources) {
28
- const actionNames = findings.flatMap((finding) => {
29
- if (finding.code !== "recipe.action_not_declared_by_manifest") return [];
30
- const match = /^Recipe action ([^ ]+) is not declared by the runner action manifest[.]?$/u.exec(finding.message);
31
- return match?.[1] ? [match[1]] : [];
32
- });
33
- if (actionNames.length === 0) return [];
34
- return missingActionCapabilities(
35
- adapter,
36
- actionNames,
37
- await resolveActionCapabilityMatrix(librarySources)
38
- );
39
- }
40
- function capabilityValidationUserAction(adapter, refusals, libraryContextArgs) {
41
- const capabilities = refusals.map(
42
- (refusal) => `"${refusal.capability}" [${refusal.satisfyingAdapters.join(", ")}]`
43
- ).join("; ");
44
- return `Missing action capabilities for ${adapter}, with satisfying adapters: ${capabilities}. Inspect: mm-harness actions --matrix${libraryContextArgs} --json; then rerun from a checkout matching a satisfying adapter, splitting the recipe if no single adapter satisfies every capability`;
45
- }
46
- async function handleRun(parsed) {
47
- const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
48
- const restoreStdout = stream.isolateStdout();
49
- const target = targetPath(parsed.options);
50
- try {
51
- const exitCode = await handleRunInner(parsed, stream);
52
- stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
53
- return exitCode;
54
- } catch (error) {
55
- if (error instanceof ProvenanceDriftError) {
56
- const failure = {
57
- code: error.code,
58
- message: error.message,
59
- userAction: error.userAction,
60
- provenancePath: error.provenancePath,
61
- drift: error.drift
62
- };
63
- if (stream.enabled) {
64
- stream.error(failure);
65
- stream.complete("fail", error.exitCode);
66
- } else if (optionFlag(parsed.options, "json")) {
67
- console.log(JSON.stringify({
68
- schemaVersion: 1,
69
- command: "run",
70
- status: "fail",
71
- error: failure,
72
- exitCode: error.exitCode
73
- }, null, 2));
74
- } else {
75
- console.error(`\u2717 mm-harness run: ${error.message}`);
76
- console.error(` Next: ${error.userAction}`);
77
- }
78
- return error.exitCode;
79
- }
80
- const trustFailure = recipeTrustFailure(error);
81
- if (trustFailure) {
82
- if (stream.enabled) {
83
- stream.error(trustFailure);
84
- stream.complete("fail", EXIT.validation);
85
- } else if (optionFlag(parsed.options, "json")) {
86
- console.log(JSON.stringify({
87
- schemaVersion: 1,
88
- command: "run",
89
- status: "fail",
90
- error: trustFailure,
91
- exitCode: EXIT.validation
92
- }, null, 2));
93
- } else {
94
- console.error(`\u2717 mm-harness run: ${trustFailure.message}`);
95
- const blocked = trustFailure.details?.blocked;
96
- if (blocked?.length) {
97
- console.error(" Restricted plan nodes:");
98
- for (const node of blocked.slice(0, 10)) {
99
- const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
100
- console.error(
101
- ` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
102
- );
103
- }
104
- if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
105
- }
106
- console.error(` Next: ${trustFailure.userAction}`);
107
- }
108
- return EXIT.validation;
109
- }
110
- const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
111
- stream.error({
112
- code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
113
- message: error instanceof Error ? error.message : String(error),
114
- userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
115
- });
116
- stream.complete("fail", exitCode);
117
- throw error;
118
- } finally {
119
- restoreStdout();
120
- }
121
- }
122
- async function handleRunInner({ positional, options }, stream) {
123
- if (!optionFlag(options, "list") && ["domain", "source", "sort"].some((name) => optionString(options, name) !== void 0)) {
124
- throw usageError("--domain, --source, and --sort require run --list.");
125
- }
126
- if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
127
- const message = "--describe cannot be combined with --list or --plan.";
128
- const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
129
- if (stream.enabled) {
130
- stream.error({ code: "CLI_USAGE_ERROR", message, userAction });
131
- return EXIT.usage;
132
- }
133
- return usageOut(optionFlag(options, "json"), "run", message, userAction);
134
- }
135
- if (optionFlag(options, "list")) {
136
- if (stream.enabled) {
137
- const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
138
- stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
139
- return EXIT.usage;
140
- }
141
- return handleListExecutables("run", options);
142
- }
143
- const targetRecipe = positional[0];
144
- if (!targetRecipe) throw usageError("run requires <recipe.json>.");
145
- const paramAssignments = positional.slice(1);
146
- if (optionFlag(options, "describe")) {
147
- if (paramAssignments.length > 0) {
148
- throw usageError("run --describe does not accept parameter values; inspect the declaration first.");
149
- }
150
- if (stream.enabled) {
151
- const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
152
- stream.error({
153
- code: "JSON_STREAM_UNSUPPORTED_MODE",
154
- message,
155
- userAction: `mm-harness run ${shellQuote(targetRecipe)} --describe --json`
156
- });
157
- return EXIT.usage;
158
- }
159
- return handleDescribeRecipe(targetRecipe, options);
160
- }
161
- const params = parseRecipeParamAssignments(paramAssignments);
162
- if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, params, options, stream);
163
- const { adapter, target } = resolveAdapter(options);
164
- const json = optionFlag(options, "json");
165
- const jsonOutput = json && !stream.enabled;
166
- const machine = json || stream.enabled;
167
- stream.phase("resolve", { adapter, target, recipe: targetRecipe });
168
- if (!fs.existsSync(target)) {
169
- const message = `target does not exist: ${target}`;
170
- const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
171
- if (stream.enabled) {
172
- stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
173
- return EXIT.usage;
174
- }
175
- return usageOut(jsonOutput, "run", message, userAction);
176
- }
177
- writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
178
- getAdapterSurface(adapter).resolveSlotPorts(target);
179
- const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
180
- if ("code" in dtResult) {
181
- return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
182
- }
183
- if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
184
- stream.phase("validate");
185
- const validated = await validateRunRecipeStatic(targetRecipe, adapter, options, params);
186
- if (validated.usageError) {
187
- const userAction = runUsageRecovery(
188
- validated.usageError.code,
189
- validated.usageError.message,
190
- adapter,
191
- validated.recipeFile
192
- );
193
- return emitRunUsageError(
194
- jsonOutput,
195
- stream,
196
- adapter,
197
- validated.recipeFile,
198
- validated.usageError.code,
199
- validated.usageError.message,
200
- userAction
201
- );
202
- }
203
- if (validated.errorCount > 0) {
204
- const capabilityRefusals = optionString(options, "actionManifest") ? [] : await validationCapabilityRefusals(
205
- adapter,
206
- validated.findings,
207
- validated.librarySources
208
- );
209
- return emitRunValidationError(
210
- jsonOutput,
211
- stream,
212
- adapter,
213
- validated.recipeFile,
214
- validated.findings,
215
- validated.errorCount,
216
- capabilityRefusals,
217
- actionLibraryContextArgs(validated.librarySources)
218
- );
219
- }
220
- const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, {
221
- recipe: validated.recipe,
222
- librarySources: validated.librarySources
223
- }) ?? null;
224
- if (depsBlock) {
225
- return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
226
- }
227
- let artifactsDir;
228
- try {
229
- artifactsDir = resolveRunArtifactsDir(target, optionString(options, "artifactsDir"));
230
- } catch (error) {
231
- const message = error instanceof Error ? error.message : String(error);
232
- return emitRunUsageError(
233
- jsonOutput,
234
- stream,
235
- adapter,
236
- validated.recipeFile,
237
- "ARTIFACT_DIR_INVALID",
238
- message,
239
- "set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
240
- );
241
- }
242
- recordCommandEvidence(artifactsDir);
243
- const librarySources = validated.librarySources;
244
- let networkObservation;
245
- let performanceObservation;
246
- const runtimeOptions = {
247
- ...recipeRunOptionsFromCli(adapter, options),
248
- ...fundingOptionsFromCli(options),
249
- params,
250
- ...librarySources ? { librarySources } : {},
251
- stdoutIsMachineContract: machine,
252
- onActionEvent: ({ nodeId, action, status }) => {
253
- stream.node(nodeId, action, status);
254
- networkObservation?.onActionEvent({ nodeId, action, status });
255
- performanceObservation?.onActionEvent({ nodeId, action, status });
256
- }
257
- };
258
- stream.phase("authorize");
259
- let preflightedExecution = await preflightRecipe(
260
- adapter,
261
- validated.recipeFile,
262
- artifactsDir,
263
- target,
264
- optionString(options, "actionManifest"),
265
- runtimeOptions
266
- );
267
- const lock = acquireCheckoutLock(target, "run");
268
- if ("message" in lock) {
269
- const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
270
- stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
271
- return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
272
- }
273
- try {
274
- const prepared = await prepareHeal(adapter, target, options, machine, {
275
- onPhase: (phase, fields) => stream.phase(phase, fields)
276
- });
277
- if (typeof prepared === "number") {
278
- stream.error({
279
- code: "RUN_PREPARE_FAILED",
280
- message: "runtime preparation failed; inspect stderr for the exact probe",
281
- userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
282
- });
283
- return prepared;
284
- }
285
- const { state } = prepared;
286
- preflightedExecution = await preflightRecipe(
287
- adapter,
288
- validated.recipeFile,
289
- artifactsDir,
290
- target,
291
- optionString(options, "actionManifest"),
292
- runtimeOptions
293
- );
294
- networkObservation = await startRunNetworkObservation(
295
- adapter,
296
- target,
297
- artifactsDir,
298
- process.env,
299
- {
300
- cdpPort: runtimeOptions.cdpPort,
301
- watcherPort: runtimeOptions.watcherPort
302
- }
303
- );
304
- performanceObservation = startRunPerformanceObservation(
305
- adapter,
306
- target,
307
- artifactsDir,
308
- process.env,
309
- {
310
- cdpPort: runtimeOptions.cdpPort,
311
- watcherPort: runtimeOptions.watcherPort
312
- }
313
- );
314
- stream.phase("execute");
315
- let executionResult;
316
- try {
317
- executionResult = await executeWithHealBounds(
318
- // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
319
- // that only the resolver knows how to turn into a file.
320
- () => {
321
- const execution = preflightedExecution;
322
- preflightedExecution = void 0;
323
- return runRecipe(
324
- adapter,
325
- validated.recipeFile,
326
- artifactsDir,
327
- target,
328
- optionString(options, "actionManifest"),
329
- runtimeOptions,
330
- execution,
331
- state
332
- );
333
- },
334
- target,
335
- state
336
- );
337
- } catch (error) {
338
- await networkObservation?.finalize().catch(() => void 0);
339
- networkObservation = void 0;
340
- await performanceObservation?.finalize().catch(() => void 0);
341
- performanceObservation = void 0;
342
- throw error;
343
- }
344
- const { result, violation } = executionResult;
345
- await networkObservation?.finalize(result.artifactManifestPath);
346
- networkObservation = void 0;
347
- await performanceObservation?.finalize(result.artifactManifestPath);
348
- performanceObservation = void 0;
349
- for (const mutation of state.mutations) stream.mutation(mutation);
350
- for (const recovery of state.recovered) stream.recovery(recovery);
351
- persistRunEffects(result.summaryPath, result.artifactManifestPath, state);
352
- indexProductProvenanceArtifact(
353
- result.artifactManifestPath,
354
- target,
355
- adapter,
356
- result.browser ?? null
357
- );
358
- if (violation !== null) {
359
- const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
360
- stream.error({
361
- code: violation.code,
362
- message: violation.message,
363
- userAction,
364
- originalError: violation.originalError ?? null
365
- });
366
- return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
367
- }
368
- const report = writeRunReport(result);
369
- const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
370
- const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
371
- if (stream.enabled) {
372
- if (result.status === "fail") {
373
- stream.error({
374
- code: "RECIPE_EXECUTION_FAILED",
375
- message: "recipe execution failed; inspect the persisted result and evidence paths",
376
- userAction: failureUserAction
377
- });
378
- }
379
- stream.complete(result.status, exitCode, {
380
- adapter,
381
- reportPath: report.path,
382
- summaryPath: result.summaryPath,
383
- tracePath: result.tracePath,
384
- artifactManifestPath: result.artifactManifestPath,
385
- recovered: state.recovered,
386
- mutations: state.mutations
387
- });
388
- } else if (json) {
389
- console.log(
390
- JSON.stringify(
391
- {
392
- schemaVersion: 1,
393
- command: "run",
394
- adapter,
395
- status: result.status,
396
- exitCode,
397
- recovered: state.recovered,
398
- mutations: state.mutations,
399
- reportPath: report.path,
400
- result,
401
- ...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
402
- },
403
- null,
404
- 2
405
- )
406
- );
407
- } else {
408
- const out = (style, text) => color(style, text, { stream: process.stdout });
409
- console.log(`${out(result.status === "pass" ? "ok" : "err", result.status.toUpperCase())} ${out("bold", "recipe run")} ${out("dim", `[${adapter}]`)}`);
410
- if (report.preview.length > 0) {
411
- console.log(out("label", "summary:"));
412
- for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
413
- }
414
- const artifacts = runArtifactInventory(result.artifactManifestPath);
415
- console.log(out("label", `artifacts (${artifacts.length}):`));
416
- for (const artifact of artifacts) {
417
- console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
418
- }
419
- if (result.status === "fail") {
420
- console.error(` Next: ${failureUserAction}`);
421
- }
422
- console.log(out("label", "diagnostics:"));
423
- const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
424
- for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
425
- console.log(` ${formatDiagnosticLine(line, out)}`);
426
- }
427
- }
428
- return exitCode;
429
- } finally {
430
- lock.release();
431
- }
432
- }
433
- function formatDiagnosticLine(line, out) {
434
- const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
435
- if (!match) return line;
436
- const status = match[1];
437
- const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
438
- return `${out(style, status)}${match[2]}`;
439
- }
440
- function resolveRunArtifactsDir(target, explicit) {
441
- if (explicit !== void 0) return path.resolve(explicit);
442
- const taskDir = process.env.RECIPE_TASK_DIR || process.env.FARMSLOT_TASK_DIR;
443
- if (taskDir) {
444
- const resolvedTask = path.resolve(target, taskDir);
445
- const relative = path.relative(path.resolve(target), resolvedTask);
446
- if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
447
- return path.join(resolvedTask, "artifacts");
448
- }
449
- throw new Error(`task directory must be inside the target checkout: ${taskDir}`);
450
- }
451
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/gu, "");
452
- return path.join(target, "temp", "recipe", "runs", `${stamp}-${process.pid}`);
453
- }
454
- function runArtifactInventory(manifestPathValue) {
455
- const manifestPath = path.resolve(String(manifestPathValue));
456
- const root = path.dirname(manifestPath);
457
- let manifest;
458
- try {
459
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
460
- } catch {
461
- return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
462
- }
463
- const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
464
- const artifacts = [];
465
- for (const entry of entries) {
466
- if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
467
- const absolutePath = path.resolve(root, entry.path);
468
- const basename = path.basename(absolutePath);
469
- const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
470
- artifacts.push({ absolutePath, basename, label });
471
- }
472
- artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
473
- const priority = /* @__PURE__ */ new Map([
474
- ["diagnostics.json", 0],
475
- ["report.md", 1],
476
- ["summary.json", 2],
477
- ["trace.json", 3],
478
- ["recipe.json", 4],
479
- ["artifact-manifest.json", 100]
480
- ]);
481
- 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));
482
- }
483
- function formatPreviewLine(line, out) {
484
- const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
485
- if (!match) return line;
486
- const status = match[1];
487
- const rest = match[2];
488
- return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
489
- }
490
- async function handleRunPlan(recipeArg, params, options, stream) {
491
- const json = optionFlag(options, "json");
492
- const jsonOutput = json && !stream.enabled;
493
- const { adapter, target } = resolveAdapter(options);
494
- stream.phase("resolve", { adapter, target, recipe: recipeArg });
495
- stream.phase("validate");
496
- const validated = await validateRunRecipeStatic(recipeArg, adapter, options, params);
497
- if (validated.usageError) {
498
- const userAction = runUsageRecovery(
499
- validated.usageError.code,
500
- validated.usageError.message,
501
- adapter,
502
- validated.recipeFile
503
- );
504
- return emitPlanUsageError(
505
- jsonOutput,
506
- stream,
507
- adapter,
508
- validated.recipeFile,
509
- validated.usageError.code,
510
- validated.usageError.message,
511
- userAction
512
- );
513
- }
514
- const {
515
- recipe,
516
- recipeFile,
517
- findings,
518
- errorCount,
519
- manifestOk,
520
- schemaValid,
521
- effectiveParams,
522
- librarySources
523
- } = validated;
524
- const status = errorCount === 0 ? "pass" : "fail";
525
- const capabilityRefusals = status === "fail" && !optionString(options, "actionManifest") ? await validationCapabilityRefusals(adapter, findings, librarySources) : [];
526
- const failureUserAction = capabilityRefusals.length > 0 ? capabilityValidationUserAction(
527
- adapter,
528
- capabilityRefusals,
529
- actionLibraryContextArgs(librarySources)
530
- ) : runPlanProbe(adapter, recipeFile);
531
- const nodeCount = countRecipeNodes(recipe);
532
- const artifactsDir = optionString(options, "artifactsDir");
533
- const plan = [
534
- { step: "resolve.recipe", confidence: "static", status: "ok", detail: recipeFile },
535
- { step: "resolve.adapter", confidence: "static", status: "ok", detail: adapter },
536
- {
537
- step: "resolve.artifactsDir",
538
- confidence: "static",
539
- status: "ok",
540
- detail: artifactsDir ? path.resolve(artifactsDir) : "(resolved to the slot artifacts dir at run time)"
541
- },
542
- {
543
- step: "validate.manifest",
544
- confidence: "static",
545
- status: manifestOk ? "ok" : "error",
546
- detail: manifestOk ? "action manifest is well-formed" : "action manifest failed validation"
547
- },
548
- {
549
- step: "validate.schema",
550
- confidence: "static",
551
- status: manifestOk && schemaValid ? "ok" : "error",
552
- detail: "recipe document schema + action existence/platform vs the adapter manifest"
553
- },
554
- ...optionFlag(options, "proof") ? [{
555
- step: "validate.proofBindings",
556
- confidence: "static",
557
- status: findings.some((finding) => finding.code.startsWith("proof.")) ? "error" : errorCount ? "planned" : "ok",
558
- detail: "checks possible behavioral assertion bindings only; runtime order, branches, results and proof artifact indexing remain unverified"
559
- }] : [],
560
- {
561
- step: "fixture.file",
562
- confidence: "static",
563
- status: "ok",
564
- detail: fs.existsSync(walletFixturePath(target)) ? `present: ${walletFixturePath(target)}` : `absent: ${walletFixturePath(target)} (fixtures set to seed)`
565
- },
566
- {
567
- step: "overlay.ensure",
568
- confidence: "conditional",
569
- status: "planned",
570
- detail: "would auto-ensure the runtime overlay if missing (install phase)"
571
- },
572
- ...getAdapterSurface(adapter).headless ? [] : [
573
- {
574
- step: "launch.app",
575
- confidence: "conditional",
576
- status: "planned",
577
- detail: "would launch/attach the app + heal transport (Metro/Chrome/CDP) before executing"
578
- }
579
- ],
580
- {
581
- step: "execute.nodes",
582
- confidence: "conditional",
583
- status: "planned",
584
- detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
585
- }
586
- ];
587
- const { redactRecipeParams } = await importRecipeHarness();
588
- const payload = {
589
- schemaVersion: 1,
590
- command: "run",
591
- mode: "plan",
592
- status,
593
- adapter,
594
- recipe: recipeFile,
595
- params: redactRecipeParams(effectiveParams, isRecord(recipe) ? recipe.paramsSchema : void 0).params,
596
- findings,
597
- plan
598
- };
599
- if (status === "fail") {
600
- payload.error = {
601
- code: "RECIPE_VALIDATION_FAILED",
602
- message: `recipe validation found ${errorCount} error(s)`,
603
- ...capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {},
604
- userAction: failureUserAction
605
- };
606
- }
607
- if (stream.enabled) {
608
- if (status === "fail") stream.error(payload.error);
609
- stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
610
- } else if (json) {
611
- console.log(JSON.stringify(payload, null, 2));
612
- } else {
613
- console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
614
- for (const item of plan) {
615
- const mark = item.status === "error" ? "\u2717" : item.status === "ok" ? "\u2713" : "\xB7";
616
- console.log(` ${mark} [${item.confidence}] ${item.step}: ${item.detail}`);
617
- }
618
- if (findings.length) {
619
- console.log("findings:");
620
- for (const finding of findings) {
621
- console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
622
- }
623
- }
624
- if (status === "fail") console.error(` Next: ${failureUserAction}`);
625
- }
626
- return status === "pass" ? EXIT.ok : EXIT.validation;
627
- }
628
- function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
629
- stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
630
- if (json) {
631
- console.log(
632
- JSON.stringify(
633
- { schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message, userAction } },
634
- null,
635
- 2
636
- )
637
- );
638
- } else {
639
- console.error(`\u2717 run --plan: ${message}
640
- Next: ${userAction}`);
641
- }
642
- return EXIT.usage;
643
- }
644
- function emitRunRecipeRunning(json, stream, target, detail) {
645
- const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
646
- const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
647
- stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
648
- if (json) {
649
- console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message, userAction } }, null, 2));
650
- } else {
651
- console.error(`\u2717 mm-harness: ${message}
652
- Next: ${userAction}`);
653
- }
654
- return EXIT.bounded;
655
- }
656
- function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
657
- stream.error({ code, message, userAction });
658
- if (json) {
659
- console.log(
660
- JSON.stringify(
661
- { schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
662
- null,
663
- 2
664
- )
665
- );
666
- } else {
667
- console.error(`\u2717 run: ${message}`);
668
- console.error(` Next: ${userAction}`);
669
- }
670
- return EXIT.usage;
671
- }
672
- function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount, capabilityRefusals, libraryContextArgs) {
673
- const message = `recipe validation found ${errorCount} error(s)`;
674
- const userAction = capabilityRefusals.length > 0 ? capabilityValidationUserAction(adapter, capabilityRefusals, libraryContextArgs) : runPlanProbe(adapter, recipeFile);
675
- const capabilityDetails = capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {};
676
- stream.error({
677
- code: "RECIPE_VALIDATION_FAILED",
678
- message,
679
- userAction,
680
- findings,
681
- ...capabilityDetails
682
- });
683
- if (json) {
684
- console.log(
685
- JSON.stringify(
686
- {
687
- schemaVersion: 1,
688
- command: "run",
689
- adapter,
690
- status: "fail",
691
- exitCode: EXIT.validation,
692
- recovered: [],
693
- mutations: [],
694
- recipe: recipeFile,
695
- findings,
696
- error: {
697
- code: "RECIPE_VALIDATION_FAILED",
698
- message,
699
- userAction,
700
- ...capabilityDetails
701
- }
702
- },
703
- null,
704
- 2
705
- )
706
- );
707
- } else {
708
- console.error(`\u2717 run: ${message}`);
709
- for (const finding of findings) {
710
- if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
711
- }
712
- console.error(` Next: ${userAction}`);
713
- }
714
- return EXIT.validation;
715
- }
716
- function runPlanProbe(adapter, recipeFile) {
717
- return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
718
- }
719
- function runUsageRecovery(code, message, adapter, recipeFile) {
720
- const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
721
- if (actionCommand) return actionCommand;
722
- if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
723
- if (code === "RECIPE_UNPARSEABLE") {
724
- return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
725
- }
726
- return runPlanProbe(adapter, recipeFile);
727
- }
728
- export {
729
- handleRun
730
- };