@deeeed/metamask-harness 0.16.0 → 0.17.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 +16 -0
- package/README.md +4 -1
- package/dist/cli.js +2 -0
- package/dist/command-contract.js +441 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +35 -17
- package/dist/commands/check.js +9 -3
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +19 -6
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/index.js +156 -59
- package/dist/commands/manifest.js +109 -12
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +26 -10
- package/dist/commands/run.js +194 -39
- package/dist/commands/shared.js +11 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +7 -2
- package/dist/harness.js +16 -4
- package/dist/json-stream.js +57 -0
- package/dist/mm-harness-cli.js +110 -3
- package/dist/runner.js +32 -1
- package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +104 -0
- package/docs/CLI-SPEC.md +58 -18
- package/docs/MENTAL-MODEL.md +1 -1
- package/docs/UX-PRINCIPLES.md +2 -0
- package/package.json +2 -1
package/dist/harness.js
CHANGED
|
@@ -50,6 +50,12 @@ function failureHint(adapter, action) {
|
|
|
50
50
|
const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
|
|
51
51
|
return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
|
|
52
52
|
}
|
|
53
|
+
function successNext(adapter, action, target) {
|
|
54
|
+
const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
55
|
+
if (action === "install") return `mm-harness verify ${base}`;
|
|
56
|
+
if (action === "cleanup") return `mm-harness install ${base}`;
|
|
57
|
+
return `mm-harness status --target ${shellQuote(target)} --json`;
|
|
58
|
+
}
|
|
53
59
|
function hasArg(args, needle) {
|
|
54
60
|
return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
|
|
55
61
|
}
|
|
@@ -347,7 +353,8 @@ async function handleMobileLive(target, forwardArgs, json, autoDetected) {
|
|
|
347
353
|
)
|
|
348
354
|
);
|
|
349
355
|
} else if (exitCode === 0) {
|
|
350
|
-
console.error(`\u2713 live mobile passed (${elapsed}s)
|
|
356
|
+
console.error(`\u2713 live mobile passed (${elapsed}s)
|
|
357
|
+
Next: ${successNext("mobile", "live", target)}`);
|
|
351
358
|
} else {
|
|
352
359
|
console.error(
|
|
353
360
|
`\u2717 live mobile failed (exit ${exitCode}, ${elapsed}s)
|
|
@@ -492,7 +499,8 @@ async function handleHarness(argv) {
|
|
|
492
499
|
)
|
|
493
500
|
);
|
|
494
501
|
} else if (exitCode === 0) {
|
|
495
|
-
console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
502
|
+
console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
|
|
503
|
+
Next: ${successNext(adapter, harnessAction, target)}`);
|
|
496
504
|
} else {
|
|
497
505
|
console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
|
|
498
506
|
${failureHint(adapter, harnessAction)}`);
|
|
@@ -522,8 +530,10 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
522
530
|
resolveOnly: hasArg(forward, "--resolve-only"),
|
|
523
531
|
rerunCommand
|
|
524
532
|
});
|
|
533
|
+
const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` : void 0;
|
|
534
|
+
const output = next ? { ...result, next } : result;
|
|
525
535
|
if (json) {
|
|
526
|
-
console.log(JSON.stringify(
|
|
536
|
+
console.log(JSON.stringify(output, null, 2));
|
|
527
537
|
} else if (result.status === "pass") {
|
|
528
538
|
const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
|
|
529
539
|
const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
@@ -534,6 +544,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
534
544
|
const action = result.skipped ? "already provisioned" : "installed Runway app";
|
|
535
545
|
console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
|
|
536
546
|
}
|
|
547
|
+
if (next) console.error(` Next: ${next}`);
|
|
537
548
|
} else {
|
|
538
549
|
console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
|
|
539
550
|
Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
@@ -578,7 +589,8 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
|
|
|
578
589
|
// Error contract: every --json failure carries a stable machine code + human
|
|
579
590
|
// message (CLI-SPEC.md §5.1). userAction is included when present so callers
|
|
580
591
|
// can surface the reachable escape without parsing the human message.
|
|
581
|
-
...status === "fail" && error ? { error } : {}
|
|
592
|
+
...status === "fail" && error ? { error } : {},
|
|
593
|
+
...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
|
|
582
594
|
});
|
|
583
595
|
}
|
|
584
596
|
export {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class JsonStreamWriter {
|
|
2
|
+
enabled;
|
|
3
|
+
command;
|
|
4
|
+
completed = false;
|
|
5
|
+
writeLine;
|
|
6
|
+
constructor(command, enabled, output = process.stdout) {
|
|
7
|
+
this.command = command;
|
|
8
|
+
this.enabled = enabled;
|
|
9
|
+
const write = output.write.bind(output);
|
|
10
|
+
this.writeLine = (line) => {
|
|
11
|
+
write(`${line}
|
|
12
|
+
`);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
isolateStdout() {
|
|
16
|
+
if (!this.enabled) return () => void 0;
|
|
17
|
+
const original = process.stdout.write;
|
|
18
|
+
const redirect = ((chunk, encoding, callback) => process.stderr.write(chunk, encoding, callback));
|
|
19
|
+
process.stdout.write = redirect;
|
|
20
|
+
return () => {
|
|
21
|
+
if (process.stdout.write === redirect) process.stdout.write = original;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
emit(event, fields = {}) {
|
|
25
|
+
if (!this.enabled || this.completed) return;
|
|
26
|
+
this.writeLine(JSON.stringify({
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
command: this.command,
|
|
29
|
+
event,
|
|
30
|
+
...fields,
|
|
31
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
phase(phase, fields = {}) {
|
|
35
|
+
this.emit("phase", { phase, ...fields });
|
|
36
|
+
}
|
|
37
|
+
node(nodeId, action, status) {
|
|
38
|
+
this.emit("node", { nodeId, action, status });
|
|
39
|
+
}
|
|
40
|
+
mutation(mutation) {
|
|
41
|
+
this.emit("mutation", { mutation });
|
|
42
|
+
}
|
|
43
|
+
recovery(code) {
|
|
44
|
+
this.emit("recovery", { code });
|
|
45
|
+
}
|
|
46
|
+
error(error) {
|
|
47
|
+
this.emit("error", { error });
|
|
48
|
+
}
|
|
49
|
+
complete(status, exitCode, fields = {}) {
|
|
50
|
+
if (!this.enabled || this.completed) return;
|
|
51
|
+
this.emit("complete", { status, exitCode, ...fields });
|
|
52
|
+
this.completed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
JsonStreamWriter
|
|
57
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -4,10 +4,17 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { color } from "./cli-color.js";
|
|
7
|
+
import { withCommandJournal } from "./command-journal.js";
|
|
8
|
+
import { JsonStreamWriter } from "./json-stream.js";
|
|
7
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
8
10
|
import { handleCallHelp } from "./commands/call.js";
|
|
9
11
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
10
12
|
import { detectAdapter } from "./harness.js";
|
|
13
|
+
import {
|
|
14
|
+
PUBLIC_COMMAND_CONTRACTS,
|
|
15
|
+
publicCommandNames,
|
|
16
|
+
validatePublicInvocation
|
|
17
|
+
} from "./command-contract.js";
|
|
11
18
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
19
|
globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
13
20
|
const { main: recipeMain } = await import("./cli.js");
|
|
@@ -71,10 +78,11 @@ Example:
|
|
|
71
78
|
name: "actions",
|
|
72
79
|
summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
|
|
73
80
|
example: "mm-harness actions --adapter mobile",
|
|
74
|
-
helpText: `mm-harness actions [flags]
|
|
81
|
+
helpText: `mm-harness actions [query] [flags]
|
|
75
82
|
|
|
76
83
|
List the action vocabulary + field schemas for the checkout adapter.
|
|
77
84
|
|
|
85
|
+
query Search names, categories, fields, and descriptions (typo-tolerant)
|
|
78
86
|
--action <name> Describe one action; fuzzy-resolves like call (short or full name)
|
|
79
87
|
--categories List compact action categories and counts
|
|
80
88
|
--category <name> List only one category (for example ui, wallet, or perps)
|
|
@@ -85,6 +93,7 @@ Example:
|
|
|
85
93
|
|
|
86
94
|
Example:
|
|
87
95
|
mm-harness actions --adapter mobile
|
|
96
|
+
mm-harness actions positions --adapter mobile
|
|
88
97
|
mm-harness actions --adapter mobile --categories --json
|
|
89
98
|
mm-harness actions --adapter mobile --category ui --json
|
|
90
99
|
mm-harness actions --adapter mobile --action assert_orders
|
|
@@ -176,12 +185,29 @@ Example:
|
|
|
176
185
|
--library <name=path> Add/override a recipe-library source (repeatable)
|
|
177
186
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
178
187
|
--json Machine-readable output
|
|
188
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
179
189
|
--record-video=full-run Record a video of the run
|
|
180
190
|
|
|
181
191
|
Example:
|
|
182
192
|
mm-harness run recipe.json --plan --adapter mobile
|
|
183
193
|
mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
|
|
184
194
|
},
|
|
195
|
+
{
|
|
196
|
+
name: "last",
|
|
197
|
+
summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
|
|
198
|
+
example: "mm-harness last --json",
|
|
199
|
+
helpText: `mm-harness last [flags]
|
|
200
|
+
|
|
201
|
+
Read the atomic per-checkout resumability journal. Discovery commands do not
|
|
202
|
+
replace it, and an interrupted process remains recorded as verdict=running.
|
|
203
|
+
|
|
204
|
+
--target <path> Checkout path (default: cwd)
|
|
205
|
+
--runtime-dir <dir> Runtime dir containing last-command.json
|
|
206
|
+
--json Machine-readable envelope
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
mm-harness last --json`
|
|
210
|
+
},
|
|
185
211
|
{
|
|
186
212
|
name: "doctor",
|
|
187
213
|
summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
|
|
@@ -379,6 +405,7 @@ Example:
|
|
|
379
405
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
380
406
|
--target <path> Checkout path (default: cwd)
|
|
381
407
|
--json Machine-readable summary (recovered[] / mutations[] / phase)
|
|
408
|
+
--json-stream Line-flushed JSONL progress + terminal event
|
|
382
409
|
|
|
383
410
|
Example:
|
|
384
411
|
mm-harness launch ios
|
|
@@ -477,6 +504,7 @@ Example:
|
|
|
477
504
|
--cdp-port <port> finalize: CDP port of the running extension
|
|
478
505
|
--extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
|
|
479
506
|
--extension-id-file <path> finalize: optional file to read/write the resolved extension id
|
|
507
|
+
--action-manifest <path> Extension set: override the wallet action manifest
|
|
480
508
|
--adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
|
|
481
509
|
--target <path> Checkout path (default: cwd)
|
|
482
510
|
--device <udid|serial|name> Mobile only: target this device for sync/set
|
|
@@ -491,6 +519,28 @@ Example:
|
|
|
491
519
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
492
520
|
}
|
|
493
521
|
];
|
|
522
|
+
const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
|
|
523
|
+
REAL.map((command) => [command.name, command.example])
|
|
524
|
+
);
|
|
525
|
+
function assertPublicContractMatchesSurface() {
|
|
526
|
+
const registered = REAL.map((command) => command.name).sort();
|
|
527
|
+
const contracted = publicCommandNames().sort();
|
|
528
|
+
if (registered.join("\n") !== contracted.join("\n")) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
for (const command of REAL) {
|
|
534
|
+
const registeredAliases = [...command.aliases ?? []].sort();
|
|
535
|
+
const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
|
|
536
|
+
if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
assertPublicContractMatchesSurface();
|
|
494
544
|
const RETIRED = [
|
|
495
545
|
{
|
|
496
546
|
name: "live",
|
|
@@ -520,7 +570,7 @@ const HELP_GROUPS = [
|
|
|
520
570
|
{
|
|
521
571
|
title: "PROVE",
|
|
522
572
|
blurb: "run recipes and inspect readiness",
|
|
523
|
-
commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
|
|
573
|
+
commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
|
|
524
574
|
},
|
|
525
575
|
{
|
|
526
576
|
title: "RUNTIME OVERLAY",
|
|
@@ -643,7 +693,7 @@ for (const command of REAL) {
|
|
|
643
693
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
644
694
|
}
|
|
645
695
|
const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
|
|
646
|
-
process.exit(await delegate(argv));
|
|
696
|
+
process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(argv)));
|
|
647
697
|
});
|
|
648
698
|
}
|
|
649
699
|
const HIDDEN = [
|
|
@@ -696,10 +746,67 @@ function isCallActionHelp(argv) {
|
|
|
696
746
|
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
697
747
|
return scope.includes("--help") || scope.includes("-h");
|
|
698
748
|
}
|
|
749
|
+
function jsonRequestedBeforePassthrough(argv) {
|
|
750
|
+
const divider = argv.indexOf("--");
|
|
751
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
752
|
+
return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
|
|
753
|
+
}
|
|
754
|
+
function jsonStreamRequestedBeforePassthrough(argv) {
|
|
755
|
+
const divider = argv.indexOf("--");
|
|
756
|
+
const scope = divider === -1 ? argv : argv.slice(0, divider);
|
|
757
|
+
return scope.includes("--json-stream");
|
|
758
|
+
}
|
|
759
|
+
function emitUsageError(error, json, jsonStream) {
|
|
760
|
+
if (jsonStream) {
|
|
761
|
+
const stream = new JsonStreamWriter(error.command, true);
|
|
762
|
+
stream.error({ code: error.code, message: error.message, userAction: error.userAction });
|
|
763
|
+
stream.complete("fail", 2);
|
|
764
|
+
} else if (json) {
|
|
765
|
+
process.stdout.write(
|
|
766
|
+
`${JSON.stringify(
|
|
767
|
+
{
|
|
768
|
+
schemaVersion: 1,
|
|
769
|
+
command: error.command,
|
|
770
|
+
status: "fail",
|
|
771
|
+
error: {
|
|
772
|
+
code: error.code,
|
|
773
|
+
message: error.message,
|
|
774
|
+
userAction: error.userAction
|
|
775
|
+
},
|
|
776
|
+
exitCode: 2
|
|
777
|
+
},
|
|
778
|
+
null,
|
|
779
|
+
2
|
|
780
|
+
)}
|
|
781
|
+
`
|
|
782
|
+
);
|
|
783
|
+
} else {
|
|
784
|
+
const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
|
|
785
|
+
process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
|
|
786
|
+
Next: ${error.userAction}
|
|
787
|
+
`);
|
|
788
|
+
}
|
|
789
|
+
process.exit(2);
|
|
790
|
+
}
|
|
699
791
|
if (rawArgv.length === 0) {
|
|
700
792
|
process.stdout.write(groupedHelp());
|
|
701
793
|
process.exit(0);
|
|
702
794
|
}
|
|
795
|
+
const preflightBypass = /* @__PURE__ */ new Set([
|
|
796
|
+
...HIDDEN,
|
|
797
|
+
"completions",
|
|
798
|
+
...RETIRED.map((command) => command.name)
|
|
799
|
+
]);
|
|
800
|
+
if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
801
|
+
const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
|
|
802
|
+
if (usageError) {
|
|
803
|
+
emitUsageError(
|
|
804
|
+
usageError,
|
|
805
|
+
jsonRequestedBeforePassthrough(rawArgv),
|
|
806
|
+
jsonStreamRequestedBeforePassthrough(rawArgv)
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
703
810
|
if (hasPassthroughHelp(rawArgv)) {
|
|
704
811
|
process.exit(await delegate(rawArgv));
|
|
705
812
|
}
|
package/dist/runner.js
CHANGED
|
@@ -58,7 +58,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
58
58
|
const logger = options.quietStdout ? new console.Console(process.stderr) : console;
|
|
59
59
|
return createRecipeRunner({
|
|
60
60
|
actionManifest,
|
|
61
|
-
adapters: [...core, ...ui, ...lifecycle, ...custom],
|
|
61
|
+
adapters: withActionProgress([...core, ...ui, ...lifecycle, ...custom], options.onActionEvent),
|
|
62
62
|
logger,
|
|
63
63
|
recording: {
|
|
64
64
|
targetProvider: createMetaMaskRecordingTargetProvider(adapter)
|
|
@@ -76,6 +76,36 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
+
function withActionProgress(adapters, onActionEvent) {
|
|
80
|
+
if (!onActionEvent) return adapters;
|
|
81
|
+
return adapters.map((entry) => ({
|
|
82
|
+
...entry,
|
|
83
|
+
async execute(node, context) {
|
|
84
|
+
const event = { nodeId: context.nodeId, action: entry.action };
|
|
85
|
+
const reportProgress = !isAutomaticHudProgress(entry.action, node, context.nodeId);
|
|
86
|
+
if (reportProgress) onActionEvent({ ...event, status: "running" });
|
|
87
|
+
try {
|
|
88
|
+
const result = await entry.execute(node, context);
|
|
89
|
+
if (reportProgress) {
|
|
90
|
+
onActionEvent({ ...event, status: result.status === "fail" ? "failed" : "passed" });
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (reportProgress) onActionEvent({ ...event, status: "failed" });
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
function isAutomaticHudProgress(action, node, nodeId) {
|
|
101
|
+
if (action !== "app.hud" || node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const payload = node;
|
|
105
|
+
if (typeof payload.action_name === "string") return true;
|
|
106
|
+
const progress = payload.progress;
|
|
107
|
+
return nodeId === "recipe-complete" && progress !== null && typeof progress === "object" && !Array.isArray(progress) && progress.complete === true;
|
|
108
|
+
}
|
|
79
109
|
function mobileSourceAwareLifecycleAdapters(adapter, lifecycle) {
|
|
80
110
|
if (adapter !== "mobile") return lifecycle;
|
|
81
111
|
return lifecycle.map((entry) => ({
|
|
@@ -120,5 +150,6 @@ export {
|
|
|
120
150
|
createMetaMaskExtensionRunner,
|
|
121
151
|
createMetaMaskMobileRunner,
|
|
122
152
|
createMetaMaskRunner,
|
|
153
|
+
isAutomaticHudProgress,
|
|
123
154
|
mobileSourceAwareLifecycleAdapters
|
|
124
155
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- Generated by scripts/generate-cli-ergonomics-audit.mjs. Do not edit by hand. -->
|
|
2
|
+
# CLI ergonomics audit
|
|
3
|
+
|
|
4
|
+
This matrix tracks the public command surface. “Covered” means the shared preflight contract and
|
|
5
|
+
`tests/contract/cli-teaching-errors.test.sh` exercise the behavior without dispatching runtime work.
|
|
6
|
+
Runtime recovery is claimed only where command-specific failures return a stable `userAction`.
|
|
7
|
+
Success hints are intentionally limited to lifecycle transitions with one truthful next command;
|
|
8
|
+
discovery, read-only, and terminal evidence commands do not invent one. ID prefix support is N/A
|
|
9
|
+
unless a command displays a shortened ID that another command accepts.
|
|
10
|
+
|
|
11
|
+
| Command | Bad flag | First feedback | `--json` recovery | Success next step | Displayed ID prefix |
|
|
12
|
+
|---|---|---|---|---|---|
|
|
13
|
+
| `status` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | structured `next` | N/A: command displays no shortened ID |
|
|
14
|
+
| `checklist` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
15
|
+
| `actions` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
16
|
+
| `stop` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | launch `next` when the adapter has one unambiguous launch | N/A: command displays no shortened ID |
|
|
17
|
+
| `call` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
18
|
+
| `flows` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
19
|
+
| `run` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
20
|
+
| `last` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
21
|
+
| `doctor` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
22
|
+
| `check` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
23
|
+
| `recipe-quality` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
24
|
+
| `provision` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | launch `next` after install | N/A: command displays no shortened ID |
|
|
25
|
+
| `install` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | verify `next` | N/A: command displays no shortened ID |
|
|
26
|
+
| `verify` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | status `next` | N/A: command displays no shortened ID |
|
|
27
|
+
| `cleanup` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | install `next` | N/A: command displays no shortened ID |
|
|
28
|
+
| `launch` | covered: `CLI_UNKNOWN_OPTION` | TTY intent before slow work; machine output clean | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
29
|
+
| `logs` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
30
|
+
| `debug` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage errors covered; no runtime recovery claim | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
31
|
+
| `update` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
32
|
+
| `fixtures` | covered: `CLI_UNKNOWN_OPTION` | N/A: no newly introduced slow path | usage + runtime `error.code/message/userAction` | N/A: discovery, read-only, or terminal result | N/A: command displays no shortened ID |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# CLI ergonomics human QA
|
|
2
|
+
|
|
3
|
+
Use this short pass after installing a release candidate. Automated contracts remain the release gate; this checklist confirms the experience in a real shell and checkout.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export TARGET=/absolute/path/to/a/metamask-checkout
|
|
9
|
+
export ADAPTER=extension # extension, mobile, or core
|
|
10
|
+
mm-harness --version
|
|
11
|
+
mm-harness doctor --adapter "$ADAPTER" --target "$TARGET" --json | jq .status
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Expected: the intended version is printed and `doctor` returns parseable JSON. Resolve required doctor failures before continuing.
|
|
15
|
+
|
|
16
|
+
## 1. Teaching errors
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
out="$(mm-harness actions --categoriez --adapter "$ADAPTER" --json 2>/dev/null || true)"
|
|
20
|
+
jq '.error | {code, message, userAction}' <<<"$out"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Expected: `code` is stable, the message names the bad flag and valid options, and `userAction` suggests the closest valid invocation.
|
|
24
|
+
|
|
25
|
+
## 2. In-tool discovery
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mm-harness actions --adapter "$ADAPTER" --categories
|
|
29
|
+
mm-harness actions status --adapter "$ADAPTER"
|
|
30
|
+
mm-harness actions --action app.status --adapter "$ADAPTER" --json | jq '{actions, relatedActions}'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Expected: category output is compact, fuzzy search is bounded, and action detail includes related actions when available. A missing example action should produce a teaching error, not silent output.
|
|
34
|
+
|
|
35
|
+
## 3. Machine-output cleanliness and recovery
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
out="$(mm-harness call definitely.not.an.action --adapter "$ADAPTER" --target "$TARGET" --json 2>/dev/null || true)"
|
|
39
|
+
jq '{status, error}' <<<"$out"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expected: stdout is one parseable JSON document with `error.code`, `error.message`, and `error.userAction`; it contains no spinner, banner, or human-only glyph.
|
|
43
|
+
|
|
44
|
+
## 4. Immediate human feedback
|
|
45
|
+
|
|
46
|
+
Run the platform-appropriate launch in an interactive terminal:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Extension
|
|
50
|
+
mm-harness launch --adapter extension --target "$TARGET"
|
|
51
|
+
|
|
52
|
+
# Mobile: choose the intended platform
|
|
53
|
+
mm-harness launch ios --target "$TARGET"
|
|
54
|
+
# mm-harness launch android --target "$TARGET"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Expected: intent/progress appears immediately, long work continues to report progress, and the app becomes ready. Redirected or `--json` stdout stays decoration-free.
|
|
58
|
+
|
|
59
|
+
## 5. Lifecycle next step
|
|
60
|
+
|
|
61
|
+
For Extension:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mm-harness stop --adapter extension --target "$TARGET" --json | jq '{status, next}'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Expected: `next` is a target-scoped launch command. Mobile `stop` intentionally omits `next` because Metro does not identify whether iOS or Android should be relaunched.
|
|
68
|
+
|
|
69
|
+
## 6. Resume after interruption
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
mm-harness last --target "$TARGET" --json | jq '.last | {command, args, verdict, evidencePaths, startedAt, finishedAt}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Expected: the last operation can be reconstructed without rerunning it, including its arguments, verdict, evidence paths, and timestamps.
|
|
76
|
+
|
|
77
|
+
## 7. Recipe proof smoke
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
mm-harness run --list --adapter "$ADAPTER" --json | jq '{actions: .actions[0:5], flows: .flows[0:5]}'
|
|
81
|
+
export RECIPE=replace-with-a-safe-recipe-name-or-path
|
|
82
|
+
export ARTIFACTS_DIR="$(mktemp -d)"
|
|
83
|
+
mm-harness run "$RECIPE" --plan --adapter "$ADAPTER" --target "$TARGET" --json | jq '{status, plan}'
|
|
84
|
+
mm-harness run "$RECIPE" --adapter "$ADAPTER" --target "$TARGET" --artifacts-dir "$ARTIFACTS_DIR"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Expected: the plan validates before execution; the final result identifies evidence paths and `mm-harness last --json` records the run.
|
|
88
|
+
|
|
89
|
+
Optional JSONL check:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
mm-harness run "$RECIPE" --adapter "$ADAPTER" --target "$TARGET" --artifacts-dir "$ARTIFACTS_DIR-stream" --json-stream | jq -c .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Expected: complete JSON objects arrive line by line while the run is active.
|
|
96
|
+
|
|
97
|
+
## Cleanup
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
[ "$ADAPTER" = core ] || mm-harness stop --adapter "$ADAPTER" --target "$TARGET"
|
|
101
|
+
mm-harness status --target "$TARGET" --json | jq .
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Record the harness version, checkout SHA, adapter/platform, failed command, exit code, and JSON envelope for any discrepancy.
|
package/docs/CLI-SPEC.md
CHANGED
|
@@ -379,7 +379,7 @@ Installs the cached Runway iOS dev client onto a prepared mobile slot. It does n
|
|
|
379
379
|
> recipe) validate fully; the execute path relies on the engine's own manifest
|
|
380
380
|
> validation. Tagged in code (`runner/src/cli.ts` `validateRecipeAdapterAware`).
|
|
381
381
|
|
|
382
|
-
**
|
|
382
|
+
**Execution phases:** `resolve → validate → install → healthcheck → execute`, with `recover` emitted only when bounded recovery actually runs. `--json-stream` emits each transition as it occurs.
|
|
383
383
|
|
|
384
384
|
**Auto-ensure overlay:** `run` checks for the runtime overlay before executing (phase: `install`). If missing: installs it inline with a one-line notice, then proceeds. **First-install overlay notice:** on the very first install for this checkout, prints `installed mm-harness overlay v<version> → <path>` to stderr in human mode; in `--json` it appears solely as `mutations[]`. Use `--heal=off` to disable all auto-install and healing.
|
|
385
385
|
|
|
@@ -389,6 +389,14 @@ Installs the cached Runway iOS dev client onto a prepared mobile slot. It does n
|
|
|
389
389
|
**Outputs:** `summary.json`, `trace.json`, `artifact-manifest.json`, screenshots/logs under `--artifacts-dir`.
|
|
390
390
|
**Maps-to:** A:`run` (ROUTES-NOW); C/D:`run` (ABSORB-LATER — supply the auto adapter+artifacts defaults the typed CLI lacks).
|
|
391
391
|
|
|
392
|
+
## `last` (resumability inspect verb)
|
|
393
|
+
|
|
394
|
+
**Synopsis:** `mm-harness last [--target <checkout>] [--runtime-dir <dir>] [--json]`
|
|
395
|
+
|
|
396
|
+
Reads `last-command.json`, the atomic per-checkout journal written before and after significant runtime/proof commands. The record contains redacted args, `running|pass|fail` verdict, exit code, explicit evidence/output paths, and start/finish timestamps. A process killed between writes remains `running`, allowing an agent to inspect rather than repeat side effects. Read-only discovery commands do not replace the record, and `last` never journals itself.
|
|
397
|
+
|
|
398
|
+
The file is mode `0600`; password, credential, auth, mnemonic, private-key, seed/SRP, secret, and token values are redacted before the first write. `--json` returns `{ command:"last", target, journalPath, status, exitCode, last }`. Missing or invalid state returns `LAST_NOT_FOUND` with an exact `userAction`.
|
|
399
|
+
|
|
392
400
|
## `doctor` (ROUTES-NOW → PROVE inspect verb)
|
|
393
401
|
|
|
394
402
|
Readiness check for a checkout without launching the app. Doctor is the single place to understand the full slot context — **no hunting for files**. It also absorbs the retired `manifest` verb's validation function: the readiness checks include manifest well-formedness (schema valid, no unknown action references) and report the manifest path + protocol version in the explain-my-setup section. It includes three grounded sub-sections:
|
|
@@ -561,9 +569,9 @@ pretty-printed fallback. `manifest` is **RETIRED**: raw protocol dump rehomes to
|
|
|
561
569
|
|
|
562
570
|
### `actions` (ROUTES-NOW → DISCOVER verb)
|
|
563
571
|
|
|
564
|
-
**Synopsis:** `mm-harness actions --adapter <p> [--json] [--categories | --category <name> | --action <name>]`
|
|
572
|
+
**Synopsis:** `mm-harness actions [query] --adapter <p> [--json] [--categories | --category <name> | --action <name>]`
|
|
565
573
|
|
|
566
|
-
**PRIMARY (agent):** `mm-harness actions --adapter mobile --
|
|
574
|
+
**PRIMARY (agent):** search from the task vocabulary, for example `mm-harness actions positions --adapter mobile --json`, then request one full `--action` schema. Use `--categories` only when the task does not provide a useful search term.
|
|
567
575
|
|
|
568
576
|
**`--json` output shape** (grounded — `actions --adapter core --json` confirmed):
|
|
569
577
|
```json
|
|
@@ -581,7 +589,7 @@ pretty-printed fallback. `manifest` is **RETIRED**: raw protocol dump rehomes to
|
|
|
581
589
|
]
|
|
582
590
|
}
|
|
583
591
|
```
|
|
584
|
-
`kind` is `"official"` (engine built-ins) or `"custom"` (MetaMask adapter actions). `category`
|
|
592
|
+
`kind` is `"official"` (engine built-ins) or `"custom"` (MetaMask adapter actions). `category` uses manifest metadata when present, then stable generic groups for protocol primitives and durable action namespaces (`ui.*` → `ui`, `metamask.perps.*` → `perps`). `fields` lists every accepted parameter name. `examples[].node` is a copy-pasteable recipe node.
|
|
585
593
|
|
|
586
594
|
**Human form:** `mm-harness actions --adapter mobile` — one line per action: `<name> (<kind>) <description> fields=<f1,f2,...>`.
|
|
587
595
|
|
|
@@ -595,7 +603,7 @@ pretty-printed fallback. `manifest` is **RETIRED**: raw protocol dump rehomes to
|
|
|
595
603
|
| `--raw` | bool | false | — | agent | Dump the underlying action manifest JSON (protocol version, registry version, all entries in raw registry format — same output as `manifest --json` today; replaces the retired `manifest` verb) |
|
|
596
604
|
| `--json` | bool | false | — | **agent PRIMARY** | Full schema + fields + examples per action |
|
|
597
605
|
|
|
598
|
-
|
|
606
|
+
The optional positional `query` searches names, categories, fields, and descriptions with typo tolerance. A single `--action` detail also returns up to five compact `relatedActions` names. Flow discovery remains separate.
|
|
599
607
|
**Exit:** 0 / non-zero on engine error.
|
|
600
608
|
**Maps-to:** A:`actions` (ROUTES-NOW); C/D:`actions` (ABSORB-LATER).
|
|
601
609
|
|
|
@@ -901,6 +909,36 @@ Every failure in `--json` mode produces an `error` field at the top level:
|
|
|
901
909
|
| `retryable` | boolean | `true` = same command may succeed on retry; `false` = agent must change inputs or escalate |
|
|
902
910
|
| `userAction` | string \| null | Exact next command or manual step |
|
|
903
911
|
|
|
912
|
+
Successful lifecycle transitions may add a top-level `next` command when exactly
|
|
913
|
+
one follow-up is correct (for example install → verify or stop → relaunch).
|
|
914
|
+
Discovery, read-only, and terminal evidence commands intentionally omit it rather
|
|
915
|
+
than inventing guidance. Agents must therefore treat `next` as optional.
|
|
916
|
+
|
|
917
|
+
Public command-grammar failures happen before runtime dispatch and always exit 2.
|
|
918
|
+
Their machine envelope is deliberately smaller and stable: agents branch on
|
|
919
|
+
`error.code`, never on prose.
|
|
920
|
+
|
|
921
|
+
```json
|
|
922
|
+
{
|
|
923
|
+
"schemaVersion": 1,
|
|
924
|
+
"command": "actions",
|
|
925
|
+
"status": "fail",
|
|
926
|
+
"error": {
|
|
927
|
+
"code": "CLI_UNKNOWN_OPTION",
|
|
928
|
+
"message": "unknown option '--categoriez'. Valid options for mm-harness actions: ...",
|
|
929
|
+
"userAction": "Did you mean '--categories' instead of '--categoriez'? Try: mm-harness actions --adapter mobile"
|
|
930
|
+
},
|
|
931
|
+
"exitCode": 2
|
|
932
|
+
}
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
Stable grammar codes are `CLI_UNKNOWN_COMMAND`, `CLI_UNKNOWN_OPTION`,
|
|
936
|
+
`CLI_MISSING_OPTION_VALUE`, `CLI_INVALID_OPTION_VALUE`,
|
|
937
|
+
`CLI_MISSING_POSITIONAL`, `CLI_INVALID_POSITIONAL`,
|
|
938
|
+
`CLI_EXCESS_POSITIONAL`, and `CLI_UNEXPECTED_PASSTHROUGH`. Every public
|
|
939
|
+
command is tracked in `docs/CLI-ERGONOMICS-AUDIT.md`; private adapter leaves keep
|
|
940
|
+
their own grammar behind explicit `--` passthrough.
|
|
941
|
+
|
|
904
942
|
## §5.2 Stable recovery codes (`recovered[]`)
|
|
905
943
|
|
|
906
944
|
`recovered[]` in `--json` output lists what was healed. These are stable enum values — not prose, safe to match in agent code:
|
|
@@ -941,18 +979,20 @@ Every `--json` response includes `mutations[]` listing all side effects the comm
|
|
|
941
979
|
|
|
942
980
|
## §5.4 Phase state-machine
|
|
943
981
|
|
|
944
|
-
|
|
982
|
+
Summary `--json` responses retain their command-specific `phase` field. `--json-stream` emits transitions as they occur, so an agent can distinguish active work from a stalled command.
|
|
945
983
|
|
|
946
984
|
**`run` phases:**
|
|
947
985
|
```
|
|
948
|
-
resolve →
|
|
986
|
+
resolve → validate → install → healthcheck → execute
|
|
949
987
|
```
|
|
950
988
|
|
|
951
989
|
**`launch` phases:**
|
|
952
990
|
```
|
|
953
|
-
resolve → install → healthcheck →
|
|
991
|
+
resolve → install → healthcheck → launch → verify
|
|
954
992
|
```
|
|
955
993
|
|
|
994
|
+
`recover` is conditional and appears immediately before a bounded recovery attempt; `verify` appears only for `launch --verify`.
|
|
995
|
+
|
|
956
996
|
| Phase | What happens |
|
|
957
997
|
|---|---|
|
|
958
998
|
| `resolve` | Recipe/target/adapter resolved; library sources loaded |
|
|
@@ -966,20 +1006,20 @@ resolve → install → healthcheck → recover → launch → verify
|
|
|
966
1006
|
|
|
967
1007
|
## §5.5 JSONL event stream (`--json-stream`)
|
|
968
1008
|
|
|
969
|
-
`--json-stream
|
|
1009
|
+
`run` and `launch` accept `--json-stream`. Each progress event is written as one complete JSON object plus a newline, so a piped reader can parse it before the command finishes. Schema is versioned via `schemaVersion`; every event also carries `command` and `ts`.
|
|
1010
|
+
|
|
1011
|
+
Stream mode owns stdout: existing human or subprocess output is routed to stderr. If both `--json` and `--json-stream` are supplied, stream mode wins. The existing summary `--json` bytes are unchanged when stream mode is absent.
|
|
970
1012
|
|
|
971
1013
|
```jsonl
|
|
972
|
-
{"schemaVersion":1,"event":"phase","phase":"resolve","ts":"2026-07-02T10:00:00.000Z"}
|
|
973
|
-
{"schemaVersion":1,"event":"phase","phase":"
|
|
974
|
-
{"schemaVersion":1,"
|
|
975
|
-
{"schemaVersion":1,"event":"
|
|
976
|
-
{"schemaVersion":1,"event":"
|
|
977
|
-
{"schemaVersion":1,"event":"
|
|
978
|
-
{"schemaVersion":1,"event":"node","index":0,"action":"metamask.wallet.unlock","status":"passed","ts":"..."}
|
|
979
|
-
{"schemaVersion":1,"event":"complete","status":"pass","exitCode":0,"recovered":[],"mutations":[...],"ts":"..."}
|
|
1014
|
+
{"schemaVersion":1,"command":"run","event":"phase","phase":"resolve","ts":"2026-07-02T10:00:00.000Z"}
|
|
1015
|
+
{"schemaVersion":1,"command":"run","event":"phase","phase":"validate","ts":"2026-07-02T10:00:00.100Z"}
|
|
1016
|
+
{"schemaVersion":1,"command":"run","event":"phase","phase":"execute","ts":"..."}
|
|
1017
|
+
{"schemaVersion":1,"command":"run","event":"node","nodeId":"unlock","action":"metamask.wallet.unlock","status":"running","ts":"..."}
|
|
1018
|
+
{"schemaVersion":1,"command":"run","event":"node","nodeId":"unlock","action":"metamask.wallet.unlock","status":"passed","ts":"..."}
|
|
1019
|
+
{"schemaVersion":1,"command":"run","event":"complete","status":"pass","exitCode":0,"reportPath":"/.../report.md","artifactManifestPath":"/.../artifact-manifest.json","recovered":[],"mutations":[],"ts":"..."}
|
|
980
1020
|
```
|
|
981
1021
|
|
|
982
|
-
Event types: `phase` · `node` · `mutation` · `recovery` · `error` · `complete`.
|
|
1022
|
+
Event types: `phase` · `node` · `mutation` · `recovery` · `error` · `complete`. Node events use stable recipe `nodeId` values rather than an inferred numeric order. Exactly one terminal `complete` event is last on normal success or handled failure, giving agents a clean sentinel and compact evidence paths without repeating the full run result.
|
|
983
1023
|
|
|
984
1024
|
## §5.6 Exit code taxonomy
|
|
985
1025
|
|