@deeeed/metamask-harness 0.17.5 → 0.18.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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- 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 +1 -1
- 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 +90 -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 +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- 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/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 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- 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 +2 -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 +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- 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/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- 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/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
|
@@ -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,274 @@ 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
|
+
shellQuote,
|
|
14
|
+
targetPath,
|
|
14
15
|
usageError
|
|
15
16
|
} from "./parse-args.js";
|
|
16
17
|
import {
|
|
17
18
|
countRecipeNodes,
|
|
18
19
|
emitHealViolation,
|
|
19
20
|
executeWithHealBounds,
|
|
21
|
+
preflightRecipe,
|
|
20
22
|
prepareHeal,
|
|
21
23
|
recoverRunInfra,
|
|
22
24
|
runRecipe,
|
|
23
25
|
validateRunRecipeStatic
|
|
24
26
|
} from "./run-engine.js";
|
|
25
|
-
import { handleListExecutables } from "./list-executables.js";
|
|
27
|
+
import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
|
|
26
28
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
27
29
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
28
30
|
import { coreDependencyBlock } from "./core-readiness.js";
|
|
29
31
|
import { writeRunReport } from "./run-report.js";
|
|
30
32
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
import { JsonStreamWriter } from "../json-stream.js";
|
|
34
|
+
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
35
|
+
import { recipeTrustFailure } from "../recipe-security.js";
|
|
36
|
+
async function handleRun(parsed) {
|
|
37
|
+
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
38
|
+
const restoreStdout = stream.isolateStdout();
|
|
39
|
+
const target = targetPath(parsed.options);
|
|
40
|
+
try {
|
|
41
|
+
const exitCode = await handleRunInner(parsed, stream);
|
|
42
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
43
|
+
return exitCode;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const trustFailure = recipeTrustFailure(error);
|
|
46
|
+
if (trustFailure) {
|
|
47
|
+
if (stream.enabled) {
|
|
48
|
+
stream.error(trustFailure);
|
|
49
|
+
stream.complete("fail", EXIT.validation);
|
|
50
|
+
} else if (optionFlag(parsed.options, "json")) {
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
command: "run",
|
|
54
|
+
status: "fail",
|
|
55
|
+
error: trustFailure,
|
|
56
|
+
exitCode: EXIT.validation
|
|
57
|
+
}, null, 2));
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`\u2717 mm-harness run: ${trustFailure.message}`);
|
|
60
|
+
const blocked = trustFailure.details?.blocked;
|
|
61
|
+
if (blocked?.length) {
|
|
62
|
+
console.error(" Restricted plan nodes:");
|
|
63
|
+
for (const node of blocked.slice(0, 10)) {
|
|
64
|
+
const implementation = node.implementation?.digest ? ` implementation=${node.implementation.kind ?? "custom"}@${node.implementation.digest}` : "";
|
|
65
|
+
console.error(
|
|
66
|
+
` - ${node.nodeId}: ${node.action} [${node.capabilities.join(", ")}] source=${node.source}${implementation}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (blocked.length > 10) console.error(` - \u2026 ${blocked.length - 10} more (use --json)`);
|
|
70
|
+
}
|
|
71
|
+
console.error(` Next: ${trustFailure.userAction}`);
|
|
72
|
+
}
|
|
73
|
+
return EXIT.validation;
|
|
74
|
+
}
|
|
75
|
+
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
76
|
+
stream.error({
|
|
77
|
+
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
|
|
78
|
+
message: error instanceof Error ? error.message : String(error),
|
|
79
|
+
userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
|
|
80
|
+
});
|
|
81
|
+
stream.complete("fail", exitCode);
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
restoreStdout();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function handleRunInner({ positional, options }, stream) {
|
|
88
|
+
if (optionFlag(options, "describe") && (optionFlag(options, "list") || optionFlag(options, "plan"))) {
|
|
89
|
+
const message = "--describe cannot be combined with --list or --plan.";
|
|
90
|
+
const userAction = "choose one: mm-harness run --list, mm-harness run <recipe> --describe, or mm-harness run <recipe> --plan";
|
|
91
|
+
if (stream.enabled) {
|
|
92
|
+
stream.error({ code: "CLI_USAGE_ERROR", message, userAction });
|
|
93
|
+
return EXIT.usage;
|
|
94
|
+
}
|
|
95
|
+
return usageOut(optionFlag(options, "json"), "run", message, userAction);
|
|
96
|
+
}
|
|
97
|
+
if (optionFlag(options, "list")) {
|
|
98
|
+
if (stream.enabled) {
|
|
99
|
+
const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
|
|
100
|
+
stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
|
|
101
|
+
return EXIT.usage;
|
|
102
|
+
}
|
|
103
|
+
return handleListExecutables("run", options);
|
|
104
|
+
}
|
|
33
105
|
const targetRecipe = positional[0];
|
|
34
106
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
35
|
-
if (optionFlag(options, "
|
|
107
|
+
if (optionFlag(options, "describe")) {
|
|
108
|
+
if (stream.enabled) {
|
|
109
|
+
const message = "--json-stream is for recipe execution; use --describe --json for discovery.";
|
|
110
|
+
stream.error({
|
|
111
|
+
code: "JSON_STREAM_UNSUPPORTED_MODE",
|
|
112
|
+
message,
|
|
113
|
+
userAction: `mm-harness run ${shellQuote(targetRecipe)} --describe --json`
|
|
114
|
+
});
|
|
115
|
+
return EXIT.usage;
|
|
116
|
+
}
|
|
117
|
+
return handleDescribeRecipe(targetRecipe, options);
|
|
118
|
+
}
|
|
119
|
+
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options, stream);
|
|
36
120
|
const { adapter, target } = resolveAdapter(options);
|
|
37
121
|
const json = optionFlag(options, "json");
|
|
122
|
+
const jsonOutput = json && !stream.enabled;
|
|
123
|
+
const machine = json || stream.enabled;
|
|
124
|
+
stream.phase("resolve", { adapter, target, recipe: targetRecipe });
|
|
125
|
+
if (!fs.existsSync(target)) {
|
|
126
|
+
const message = `target does not exist: ${target}`;
|
|
127
|
+
const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
|
|
128
|
+
if (stream.enabled) {
|
|
129
|
+
stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
|
|
130
|
+
return EXIT.usage;
|
|
131
|
+
}
|
|
132
|
+
return usageOut(jsonOutput, "run", message, userAction);
|
|
133
|
+
}
|
|
134
|
+
writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
|
|
38
135
|
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
39
136
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
40
137
|
if ("code" in dtResult) {
|
|
41
|
-
return emitRunUsageError(
|
|
42
|
-
}
|
|
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>.");
|
|
138
|
+
return emitRunUsageError(jsonOutput, stream, adapter, targetRecipe, dtResult.code, dtResult.message, dtResult.userAction);
|
|
46
139
|
}
|
|
140
|
+
if (recipeRunning(target)) return emitRunRecipeRunning(jsonOutput, stream, target);
|
|
141
|
+
stream.phase("validate");
|
|
47
142
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
48
143
|
if (validated.usageError) {
|
|
49
|
-
|
|
144
|
+
const userAction = runUsageRecovery(
|
|
145
|
+
validated.usageError.code,
|
|
146
|
+
validated.usageError.message,
|
|
147
|
+
adapter,
|
|
148
|
+
validated.recipeFile
|
|
149
|
+
);
|
|
150
|
+
return emitRunUsageError(
|
|
151
|
+
jsonOutput,
|
|
152
|
+
stream,
|
|
153
|
+
adapter,
|
|
154
|
+
validated.recipeFile,
|
|
155
|
+
validated.usageError.code,
|
|
156
|
+
validated.usageError.message,
|
|
157
|
+
userAction
|
|
158
|
+
);
|
|
50
159
|
}
|
|
51
160
|
if (validated.errorCount > 0) {
|
|
52
|
-
return emitRunValidationError(
|
|
161
|
+
return emitRunValidationError(jsonOutput, stream, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
53
162
|
}
|
|
54
163
|
const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
55
164
|
if (depsBlock) {
|
|
56
|
-
return emitRunUsageError(
|
|
165
|
+
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
57
166
|
}
|
|
58
|
-
|
|
167
|
+
let artifactsDir;
|
|
168
|
+
try {
|
|
169
|
+
artifactsDir = resolveRunArtifactsDir(target, optionString(options, "artifactsDir"));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return emitRunUsageError(
|
|
173
|
+
jsonOutput,
|
|
174
|
+
stream,
|
|
175
|
+
adapter,
|
|
176
|
+
validated.recipeFile,
|
|
177
|
+
"ARTIFACT_DIR_INVALID",
|
|
178
|
+
message,
|
|
179
|
+
"set RECIPE_TASK_DIR/FARMSLOT_TASK_DIR inside the checkout or pass --artifacts-dir <path>"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const librarySources = validated.librarySources;
|
|
183
|
+
const runtimeOptions = {
|
|
184
|
+
...runtimeOptionsFromCli(options),
|
|
185
|
+
...librarySources ? { librarySources } : {},
|
|
186
|
+
stdoutIsMachineContract: machine,
|
|
187
|
+
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
188
|
+
};
|
|
189
|
+
stream.phase("authorize");
|
|
190
|
+
let preflightedExecution = await preflightRecipe(
|
|
191
|
+
adapter,
|
|
192
|
+
validated.recipeFile,
|
|
193
|
+
artifactsDir,
|
|
194
|
+
target,
|
|
195
|
+
optionString(options, "actionManifest"),
|
|
196
|
+
runtimeOptions
|
|
197
|
+
);
|
|
59
198
|
const lock = acquireCheckoutLock(target, "run");
|
|
60
|
-
if ("message" in lock)
|
|
199
|
+
if ("message" in lock) {
|
|
200
|
+
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
201
|
+
stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
|
|
202
|
+
return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
|
|
203
|
+
}
|
|
61
204
|
try {
|
|
62
|
-
const prepared = await prepareHeal(adapter, target, options,
|
|
63
|
-
|
|
205
|
+
const prepared = await prepareHeal(adapter, target, options, machine, {
|
|
206
|
+
onPhase: (phase, fields) => stream.phase(phase, fields)
|
|
207
|
+
});
|
|
208
|
+
if (typeof prepared === "number") {
|
|
209
|
+
stream.error({
|
|
210
|
+
code: "RUN_PREPARE_FAILED",
|
|
211
|
+
message: "runtime preparation failed; inspect stderr for the exact probe",
|
|
212
|
+
userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
|
|
213
|
+
});
|
|
214
|
+
return prepared;
|
|
215
|
+
}
|
|
64
216
|
const { state, heal } = prepared;
|
|
65
|
-
|
|
66
|
-
const runtimeOptions = {
|
|
67
|
-
...runtimeOptionsFromCli(options),
|
|
68
|
-
...librarySources ? { librarySources } : {},
|
|
69
|
-
stdoutIsMachineContract: json
|
|
70
|
-
};
|
|
217
|
+
stream.phase("execute");
|
|
71
218
|
const { result, violation } = await executeWithHealBounds(
|
|
72
219
|
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
73
220
|
// that only the resolver knows how to turn into a file.
|
|
74
|
-
() =>
|
|
221
|
+
() => {
|
|
222
|
+
const execution = preflightedExecution;
|
|
223
|
+
preflightedExecution = void 0;
|
|
224
|
+
return runRecipe(
|
|
225
|
+
adapter,
|
|
226
|
+
validated.recipeFile,
|
|
227
|
+
artifactsDir,
|
|
228
|
+
target,
|
|
229
|
+
optionString(options, "actionManifest"),
|
|
230
|
+
runtimeOptions,
|
|
231
|
+
execution
|
|
232
|
+
);
|
|
233
|
+
},
|
|
75
234
|
adapter,
|
|
76
235
|
target,
|
|
77
236
|
heal,
|
|
78
237
|
state,
|
|
79
|
-
() => recoverRunInfra(adapter, target,
|
|
238
|
+
() => recoverRunInfra(adapter, target, machine),
|
|
239
|
+
(code) => stream.phase("recover", { code })
|
|
80
240
|
);
|
|
81
|
-
|
|
241
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
242
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
243
|
+
if (violation !== null) {
|
|
244
|
+
const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
245
|
+
stream.error({
|
|
246
|
+
code: violation.code,
|
|
247
|
+
message: violation.message,
|
|
248
|
+
userAction,
|
|
249
|
+
originalError: violation.originalError ?? null
|
|
250
|
+
});
|
|
251
|
+
return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
|
|
252
|
+
}
|
|
82
253
|
const report = writeRunReport(result);
|
|
83
254
|
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
84
|
-
|
|
255
|
+
const failureUserAction = `mm-harness last --target ${shellQuote(target)} --json`;
|
|
256
|
+
if (stream.enabled) {
|
|
257
|
+
if (result.status === "fail") {
|
|
258
|
+
stream.error({
|
|
259
|
+
code: "RECIPE_EXECUTION_FAILED",
|
|
260
|
+
message: "recipe execution failed; inspect the persisted result and evidence paths",
|
|
261
|
+
userAction: failureUserAction
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
|
|
265
|
+
adapter,
|
|
266
|
+
reportPath: report.path,
|
|
267
|
+
summaryPath: result.summaryPath,
|
|
268
|
+
tracePath: result.tracePath,
|
|
269
|
+
artifactManifestPath: result.artifactManifestPath,
|
|
270
|
+
recovered: state.recovered,
|
|
271
|
+
mutations: state.mutations
|
|
272
|
+
});
|
|
273
|
+
} else if (json) {
|
|
85
274
|
console.log(
|
|
86
275
|
JSON.stringify(
|
|
87
276
|
{
|
|
@@ -93,7 +282,8 @@ async function handleRun({ positional, options }) {
|
|
|
93
282
|
recovered: state.recovered,
|
|
94
283
|
mutations: state.mutations,
|
|
95
284
|
reportPath: report.path,
|
|
96
|
-
result
|
|
285
|
+
result,
|
|
286
|
+
...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
|
|
97
287
|
},
|
|
98
288
|
null,
|
|
99
289
|
2
|
|
@@ -106,14 +296,75 @@ async function handleRun({ positional, options }) {
|
|
|
106
296
|
console.log(out("label", "summary:"));
|
|
107
297
|
for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
|
|
108
298
|
}
|
|
109
|
-
|
|
110
|
-
console.log(
|
|
299
|
+
const artifacts = runArtifactInventory(result.artifactManifestPath);
|
|
300
|
+
console.log(out("label", `artifacts (${artifacts.length}):`));
|
|
301
|
+
for (const artifact of artifacts) {
|
|
302
|
+
console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
|
|
303
|
+
}
|
|
304
|
+
if (result.status === "fail") {
|
|
305
|
+
console.error(` Next: ${failureUserAction}`);
|
|
306
|
+
}
|
|
307
|
+
console.log(out("label", "diagnostics:"));
|
|
308
|
+
const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
|
|
309
|
+
for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
|
|
310
|
+
console.log(` ${formatDiagnosticLine(line, out)}`);
|
|
311
|
+
}
|
|
111
312
|
}
|
|
112
313
|
return exitCode;
|
|
113
314
|
} finally {
|
|
114
315
|
lock.release();
|
|
115
316
|
}
|
|
116
317
|
}
|
|
318
|
+
function formatDiagnosticLine(line, out) {
|
|
319
|
+
const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
|
|
320
|
+
if (!match) return line;
|
|
321
|
+
const status = match[1];
|
|
322
|
+
const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
|
|
323
|
+
return `${out(style, status)}${match[2]}`;
|
|
324
|
+
}
|
|
325
|
+
function resolveRunArtifactsDir(target, explicit) {
|
|
326
|
+
if (explicit !== void 0) return path.resolve(explicit);
|
|
327
|
+
const taskDir = process.env.RECIPE_TASK_DIR || process.env.FARMSLOT_TASK_DIR;
|
|
328
|
+
if (taskDir) {
|
|
329
|
+
const resolvedTask = path.resolve(target, taskDir);
|
|
330
|
+
const relative = path.relative(path.resolve(target), resolvedTask);
|
|
331
|
+
if (!relative.startsWith(`..${path.sep}`) && relative !== "..") {
|
|
332
|
+
return path.join(resolvedTask, "artifacts");
|
|
333
|
+
}
|
|
334
|
+
throw new Error(`task directory must be inside the target checkout: ${taskDir}`);
|
|
335
|
+
}
|
|
336
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/gu, "");
|
|
337
|
+
return path.join(target, "temp", "recipe", "runs", `${stamp}-${process.pid}`);
|
|
338
|
+
}
|
|
339
|
+
function runArtifactInventory(manifestPathValue) {
|
|
340
|
+
const manifestPath = path.resolve(String(manifestPathValue));
|
|
341
|
+
const root = path.dirname(manifestPath);
|
|
342
|
+
let manifest;
|
|
343
|
+
try {
|
|
344
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
345
|
+
} catch {
|
|
346
|
+
return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
|
|
347
|
+
}
|
|
348
|
+
const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
349
|
+
const artifacts = [];
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
|
|
352
|
+
const absolutePath = path.resolve(root, entry.path);
|
|
353
|
+
const basename = path.basename(absolutePath);
|
|
354
|
+
const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
|
|
355
|
+
artifacts.push({ absolutePath, basename, label });
|
|
356
|
+
}
|
|
357
|
+
artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
|
|
358
|
+
const priority = /* @__PURE__ */ new Map([
|
|
359
|
+
["diagnostics.json", 0],
|
|
360
|
+
["report.md", 1],
|
|
361
|
+
["summary.json", 2],
|
|
362
|
+
["trace.json", 3],
|
|
363
|
+
["recipe.json", 4],
|
|
364
|
+
["artifact-manifest.json", 100]
|
|
365
|
+
]);
|
|
366
|
+
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));
|
|
367
|
+
}
|
|
117
368
|
function formatPreviewLine(line, out) {
|
|
118
369
|
const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
|
|
119
370
|
if (!match) return line;
|
|
@@ -185,12 +436,29 @@ function loadFlowCatalogs(librarySources) {
|
|
|
185
436
|
function actionUsesCoreController(action) {
|
|
186
437
|
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
187
438
|
}
|
|
188
|
-
async function handleRunPlan(recipeArg, options) {
|
|
439
|
+
async function handleRunPlan(recipeArg, options, stream) {
|
|
189
440
|
const json = optionFlag(options, "json");
|
|
441
|
+
const jsonOutput = json && !stream.enabled;
|
|
190
442
|
const { adapter, target } = resolveAdapter(options);
|
|
443
|
+
stream.phase("resolve", { adapter, target, recipe: recipeArg });
|
|
444
|
+
stream.phase("validate");
|
|
191
445
|
const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
|
|
192
446
|
if (validated.usageError) {
|
|
193
|
-
|
|
447
|
+
const userAction = runUsageRecovery(
|
|
448
|
+
validated.usageError.code,
|
|
449
|
+
validated.usageError.message,
|
|
450
|
+
adapter,
|
|
451
|
+
validated.recipeFile
|
|
452
|
+
);
|
|
453
|
+
return emitPlanUsageError(
|
|
454
|
+
jsonOutput,
|
|
455
|
+
stream,
|
|
456
|
+
adapter,
|
|
457
|
+
validated.recipeFile,
|
|
458
|
+
validated.usageError.code,
|
|
459
|
+
validated.usageError.message,
|
|
460
|
+
userAction
|
|
461
|
+
);
|
|
194
462
|
}
|
|
195
463
|
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
|
|
196
464
|
const status = errorCount === 0 ? "pass" : "fail";
|
|
@@ -244,9 +512,27 @@ async function handleRunPlan(recipeArg, options) {
|
|
|
244
512
|
detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
|
|
245
513
|
}
|
|
246
514
|
];
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
515
|
+
const payload = {
|
|
516
|
+
schemaVersion: 1,
|
|
517
|
+
command: "run",
|
|
518
|
+
mode: "plan",
|
|
519
|
+
status,
|
|
520
|
+
adapter,
|
|
521
|
+
recipe: recipeFile,
|
|
522
|
+
findings,
|
|
523
|
+
plan
|
|
524
|
+
};
|
|
525
|
+
if (status === "fail") {
|
|
526
|
+
payload.error = {
|
|
527
|
+
code: "RECIPE_VALIDATION_FAILED",
|
|
528
|
+
message: `recipe validation found ${errorCount} error(s)`,
|
|
529
|
+
userAction: runPlanProbe(adapter, recipeFile)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
if (stream.enabled) {
|
|
533
|
+
if (status === "fail") stream.error(payload.error);
|
|
534
|
+
stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
|
|
535
|
+
} else if (json) {
|
|
250
536
|
console.log(JSON.stringify(payload, null, 2));
|
|
251
537
|
} else {
|
|
252
538
|
console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
|
|
@@ -260,52 +546,58 @@ async function handleRunPlan(recipeArg, options) {
|
|
|
260
546
|
console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
261
547
|
}
|
|
262
548
|
}
|
|
549
|
+
if (status === "fail") console.error(` Next: ${runPlanProbe(adapter, recipeFile)}`);
|
|
263
550
|
}
|
|
264
551
|
return status === "pass" ? EXIT.ok : EXIT.validation;
|
|
265
552
|
}
|
|
266
|
-
function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
553
|
+
function emitPlanUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
|
|
554
|
+
stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
|
|
267
555
|
if (json) {
|
|
268
556
|
console.log(
|
|
269
557
|
JSON.stringify(
|
|
270
|
-
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
|
|
558
|
+
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message, userAction } },
|
|
271
559
|
null,
|
|
272
560
|
2
|
|
273
561
|
)
|
|
274
562
|
);
|
|
275
563
|
} else {
|
|
276
|
-
console.error(`\u2717 run --plan: ${message}
|
|
564
|
+
console.error(`\u2717 run --plan: ${message}
|
|
565
|
+
Next: ${userAction}`);
|
|
277
566
|
}
|
|
278
567
|
return EXIT.usage;
|
|
279
568
|
}
|
|
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) {
|
|
569
|
+
function emitRunRecipeRunning(json, stream, target, detail) {
|
|
284
570
|
const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
571
|
+
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
|
|
572
|
+
stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
|
|
285
573
|
if (json) {
|
|
286
|
-
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
|
|
574
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message, userAction } }, null, 2));
|
|
287
575
|
} else {
|
|
288
|
-
console.error(`\u2717 mm-harness: ${message}
|
|
576
|
+
console.error(`\u2717 mm-harness: ${message}
|
|
577
|
+
Next: ${userAction}`);
|
|
289
578
|
}
|
|
290
579
|
return EXIT.bounded;
|
|
291
580
|
}
|
|
292
|
-
function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
|
|
581
|
+
function emitRunUsageError(json, stream, adapter, recipeFile, code, message, userAction) {
|
|
582
|
+
stream.error({ code, message, userAction });
|
|
293
583
|
if (json) {
|
|
294
584
|
console.log(
|
|
295
585
|
JSON.stringify(
|
|
296
|
-
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message,
|
|
586
|
+
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
|
|
297
587
|
null,
|
|
298
588
|
2
|
|
299
589
|
)
|
|
300
590
|
);
|
|
301
591
|
} else {
|
|
302
592
|
console.error(`\u2717 run: ${message}`);
|
|
303
|
-
|
|
593
|
+
console.error(` Next: ${userAction}`);
|
|
304
594
|
}
|
|
305
595
|
return EXIT.usage;
|
|
306
596
|
}
|
|
307
|
-
function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
|
|
597
|
+
function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount) {
|
|
308
598
|
const message = `recipe validation found ${errorCount} error(s)`;
|
|
599
|
+
const userAction = runPlanProbe(adapter, recipeFile);
|
|
600
|
+
stream.error({ code: "RECIPE_VALIDATION_FAILED", message, userAction, findings });
|
|
309
601
|
if (json) {
|
|
310
602
|
console.log(
|
|
311
603
|
JSON.stringify(
|
|
@@ -319,7 +611,7 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
|
|
|
319
611
|
mutations: [],
|
|
320
612
|
recipe: recipeFile,
|
|
321
613
|
findings,
|
|
322
|
-
error: { code: "RECIPE_VALIDATION_FAILED", message }
|
|
614
|
+
error: { code: "RECIPE_VALIDATION_FAILED", message, userAction }
|
|
323
615
|
},
|
|
324
616
|
null,
|
|
325
617
|
2
|
|
@@ -330,9 +622,22 @@ function emitRunValidationError(json, adapter, recipeFile, findings, errorCount)
|
|
|
330
622
|
for (const finding of findings) {
|
|
331
623
|
if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
332
624
|
}
|
|
625
|
+
console.error(` Next: ${userAction}`);
|
|
333
626
|
}
|
|
334
627
|
return EXIT.validation;
|
|
335
628
|
}
|
|
629
|
+
function runPlanProbe(adapter, recipeFile) {
|
|
630
|
+
return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
|
|
631
|
+
}
|
|
632
|
+
function runUsageRecovery(code, message, adapter, recipeFile) {
|
|
633
|
+
const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
|
|
634
|
+
if (actionCommand) return actionCommand;
|
|
635
|
+
if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
|
|
636
|
+
if (code === "RECIPE_UNPARSEABLE") {
|
|
637
|
+
return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
|
|
638
|
+
}
|
|
639
|
+
return runPlanProbe(adapter, recipeFile);
|
|
640
|
+
}
|
|
336
641
|
export {
|
|
337
642
|
handleRun
|
|
338
643
|
};
|