@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.
- package/CHANGELOG.md +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- 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";
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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(
|
|
145
|
+
return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
|
|
42
146
|
}
|
|
43
|
-
if (recipeRunning(target)) return emitRunRecipeRunning(
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
172
|
+
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
57
173
|
}
|
|
58
|
-
|
|
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)
|
|
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,
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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,
|
|
247
|
+
() => recoverRunInfra(adapter, target, machine),
|
|
248
|
+
(code) => stream.phase("recover", { code })
|
|
80
249
|
);
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
console.log(
|
|
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
|
|
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
|
|
134
|
-
if (!
|
|
135
|
-
if (objectUsesCoreController(
|
|
136
|
-
return collectCallRefs(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
};
|