@deeeed/metamask-harness 0.83.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +8 -1
  3. package/adapters/extension/console-tail.mjs +11 -7
  4. package/adapters/extension/ensure-browser.sh +17 -18
  5. package/adapters/manifest.json +14 -110
  6. package/adapters/mobile/start-console-forwarder.sh +8 -0
  7. package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
  8. package/adapters/shared/session-lock.mjs +43 -0
  9. package/adapters/web-dapp/metamask-signer.mjs +212 -0
  10. package/dist/adapters/extension/actions.js +5 -0
  11. package/dist/adapters/extension/console-capture.js +18 -0
  12. package/dist/adapters/extension/fixtures.js +2 -0
  13. package/dist/adapters/extension/overlay.js +5 -4
  14. package/dist/adapters/extension/run.js +4 -2
  15. package/dist/adapters/extension/runtime.js +73 -18
  16. package/dist/adapters/mobile/devices.js +5 -1
  17. package/dist/adapters/mobile/launch.js +19 -2
  18. package/dist/adapters/mobile/provision.js +4 -2
  19. package/dist/adapters/mobile/run.js +3 -2
  20. package/dist/adapters/mobile/surface.js +1 -2
  21. package/dist/adapters/mobile/verify.js +15 -7
  22. package/dist/adapters/plugin-surface.js +14 -42
  23. package/dist/adapters/session-lock.js +23 -0
  24. package/dist/adapters/web-dapp/actions.js +7 -5
  25. package/dist/adapters/web-dapp/console-capture.js +2 -20
  26. package/dist/adapters/web-dapp/run.js +25 -24
  27. package/dist/adapters/web-dapp/surface.js +32 -108
  28. package/dist/adapters.js +5 -3
  29. package/dist/command-contract.js +1351 -643
  30. package/dist/commands/check.js +5 -7
  31. package/dist/commands/debug.js +2 -2
  32. package/dist/commands/doctor.js +3 -4
  33. package/dist/commands/domain.js +4 -3
  34. package/dist/commands/fixtures.js +3 -3
  35. package/dist/commands/help.js +6 -5
  36. package/dist/commands/launch/device.js +1 -1
  37. package/dist/commands/launch/extension.js +9 -7
  38. package/dist/commands/logs.js +2 -2
  39. package/dist/commands/prepare.js +4 -4
  40. package/dist/commands/provision.js +2 -2
  41. package/dist/commands/review.js +8 -10
  42. package/dist/commands/runtime-launch.js +2 -0
  43. package/dist/commands/status.js +4 -2
  44. package/dist/commands/task-init.js +2 -3
  45. package/dist/console-classifier.js +3 -2
  46. package/dist/doctor.js +11 -4
  47. package/dist/mm-harness-cli.js +241 -1210
  48. package/dist/paths.js +9 -4
  49. package/dist/recipe-security.js +1 -1
  50. package/dist/review/knowledge.js +10 -2
  51. package/dist/runner.js +18 -3
  52. package/docs/CONTRIBUTING.md +14 -4
  53. package/docs/RECIPES.md +14 -1
  54. package/docs/cli-contract.md +8 -7
  55. package/docs/feature-guard.md +1 -1
  56. package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
  57. package/library/actions/extension/platform/cdp.mjs +26 -0
  58. package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
  59. package/library/actions/shared/console-assert.mjs +174 -0
  60. package/library/actions/shared/console-collector.mjs +11 -4
  61. package/library/actions/shared/console-findings.mjs +108 -7
  62. package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
  63. package/library/actions/web-dapp/app/launch.mjs +1 -1
  64. package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
  65. package/library/actions/web-dapp/platform/hud.mjs +3 -10
  66. package/library/actions/web-dapp/platform/page.mjs +63 -18
  67. package/library/actions/web-dapp/platform/runtime.mjs +30 -179
  68. package/library/diagnostics/console-allowlist.json +13 -0
  69. package/library/manifests/extension.action-manifest.json +36 -0
  70. package/library/manifests/mobile.action-manifest.json +36 -0
  71. package/library/manifests/web-dapp.action-manifest.json +1 -1
  72. package/library/recipes/core/health/check.recipe.json +40 -0
  73. package/library/recipes/extension/health/check.recipe.json +71 -0
  74. package/library/recipes/mobile/health/check.recipe.json +71 -0
  75. package/package.json +9 -9
  76. package/scripts/completions.sh +1 -1
  77. package/site/assets/help-recipes.json +2 -1
  78. package/adapters/web-dapp/cleanup.mjs +0 -69
  79. package/adapters/web-dapp/inject.mjs +0 -75
  80. package/adapters/web-dapp/launch.mjs +0 -631
  81. package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
  82. package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
  83. package/adapters/web-dapp/lib/display.mjs +0 -115
  84. package/adapters/web-dapp/lib/hud.mjs +0 -148
  85. package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
  86. package/adapters/web-dapp/lib/policy.mjs +0 -84
  87. package/adapters/web-dapp/lib/processes.mjs +0 -187
  88. package/adapters/web-dapp/lib/readiness.mjs +0 -272
  89. package/adapters/web-dapp/stop.mjs +0 -38
  90. package/adapters/web-dapp/verify.mjs +0 -51
  91. package/adapters/web-dapp/wallet-host.mjs +0 -520
  92. package/dist/adapters/web-dapp/doctor-checks.js +0 -33
  93. package/dist/adapters/web-dapp/launch.js +0 -87
  94. package/dist/adapters/web-dapp/overlay.js +0 -21
  95. package/dist/adapters/web-dapp/readiness.js +0 -10
  96. package/dist/adapters/web-dapp/stop.js +0 -20
  97. package/dist/cli.js +0 -202
@@ -1,11 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { adapterChoices, closest } from "@farmslot/recipe-cli/harness";
3
+ import {
4
+ adapterChoices,
5
+ booleanOption as bool,
6
+ contractOptions as options,
7
+ optionalValueOption as optionalValue,
8
+ optionValues,
9
+ valueOption as value
10
+ } from "@farmslot/recipe-cli/harness";
4
11
  import { configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
5
- const bool = () => ({ kind: "boolean" });
6
- const value = (choices) => ({ kind: "value", ...choices ? { choices } : {} });
7
- const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { choices } : {} });
8
- const options = (...groups) => Object.assign({}, ...groups);
9
12
  const HELP = { "--help": bool(), "-h": bool() };
10
13
  const JSON = { "--json": bool() };
11
14
  const NO_OPEN = { "--no-open": bool() };
@@ -15,19 +18,19 @@ const adapterIds = (tokens) => adapterChoices(optionValues(tokens, "--library"),
15
18
  configured: configuredLibrarySources(),
16
19
  env: operatorLibraryEnv()
17
20
  });
18
- const ADAPTER = { "--adapter": { kind: "value", choices: adapterIds } };
21
+ const ADAPTER = { "--adapter": value(adapterIds) };
22
+ function selectableAdapters(libraries = []) {
23
+ return adapterIds(libraries.flatMap((entry) => ["--library", entry]));
24
+ }
25
+ function withAdapterList(text, libraries = []) {
26
+ return text.replaceAll("{adapters}", selectableAdapters(libraries).join("|"));
27
+ }
19
28
  const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
20
- const ADAPTER_OR_MOBILE_PLATFORM = {
21
- "--platform": {
22
- kind: "value",
23
- choices: (tokens) => [...adapterIds(tokens), "ios", "android"]
24
- }
25
- };
29
+ const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value((tokens) => [...adapterIds(tokens), "ios", "android"]) };
26
30
  const DEVICE = { "--device": value() };
27
31
  const RUNTIME_PORTS = {
28
32
  "--cdp-port": value(),
29
33
  "--watcher-port": value(),
30
- "--metro-port": value(),
31
34
  "--runtime-dir": value()
32
35
  };
33
36
  const RECIPE_RUNTIME = {
@@ -65,390 +68,1376 @@ const RUNWAY = {
65
68
  "--force": bool(),
66
69
  "--resolve-only": bool()
67
70
  };
68
- const REMOVED_OPTION_REPLACEMENTS = {
71
+ const REPLACED_OPTIONS = {
69
72
  "--project-root": "--target",
70
- "--record": "--record-video"
73
+ "--record": "--record-video",
74
+ "--metro-port": "--watcher-port",
75
+ "--arg": "key=value"
71
76
  };
72
- const PUBLIC_COMMAND_CONTRACTS = {
73
- help: {
74
- options: options(HELP, JSON, TARGET, ADAPTER, { "--domain": value() }),
75
- positionals: [{ label: "topic", choices: ["review"] }]
77
+ const MARK_ONLY = [
78
+ "--mark-last",
79
+ "--already-fixed",
80
+ "--no-self-review",
81
+ "--skip-learnings",
82
+ "--skip-checklist",
83
+ "--reason",
84
+ "--checklist",
85
+ "--signal"
86
+ ];
87
+ const CLOSEOUT_ONLY = ["--json", "--share", "--config", "--destination", "--metadata"];
88
+ const PUBLIC_COMMANDS = [
89
+ {
90
+ name: "help",
91
+ summary: "Load the version-matched recipe guide for a person or agent.",
92
+ example: "mm-harness help",
93
+ helpText: `mm-harness help [flags]
94
+
95
+ Loads the complete recipe guide shipped with this harness.
96
+
97
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
98
+ --target <path> Checkout path (default: cwd)
99
+ --json Load the same guide as structured agent context
100
+
101
+ help review [--domain <name>] prints the review guide: the base review every PR
102
+ gets, plus the team library's anti-pattern families, parity rule, and shared
103
+ package notes when --domain names a library (see mm-harness review --help).
104
+
105
+ Example:
106
+ mm-harness help
107
+ mm-harness help --json
108
+ mm-harness help review --domain perps`,
109
+ contract: {
110
+ options: options(HELP, JSON, TARGET, ADAPTER, { "--domain": value() }),
111
+ positionals: [{ label: "topic", choices: ["review"] }]
112
+ }
76
113
  },
77
- review: {
78
- options: options(HELP, JSON, TARGET, ADAPTER, {
79
- "--domain": value(),
80
- "--since": value(),
81
- "--base": value(),
82
- "--out": value()
83
- }),
84
- positionals: [{ label: "action", choices: ["checklist"] }],
85
- minimumPositionals: 1
114
+ {
115
+ name: "review",
116
+ summary: "Materialize a review checklist: base review + team library patterns + parity + verdict.",
117
+ example: "mm-harness review checklist --domain perps",
118
+ helpText: `mm-harness review checklist [flags]
119
+
120
+ Writes a phase-structured checklist (Setup, Base review, Domain patterns,
121
+ Parity, Verdict) that mm-harness checklist mark can track. Domain patterns are
122
+ one line per "##" section of the library's review/antipatterns.md; Parity names
123
+ the other platform's checkout when one resolves and says "not checked" otherwise.
124
+
125
+ Library resolution for --domain <name>: RECIPE_LIBRARY_PATH entry \u2192 engineer
126
+ config (mm-harness config) \u2192 sibling directory beside the checkout \u2192 shallow
127
+ fetch into <checkout>/.skills-cache/<name> (known libraries only). Reference
128
+ checkout resolution: MM_HARNESS_REF_<ADAPTER> \u2192 engineer config \u2192 sibling
129
+ checkout of that adapter.
130
+
131
+ --domain <name> Team library to compose (default: MM_HARNESS_DOMAIN; none = base review only)
132
+ --since <sha> Incremental re-review: scope the checklist to <sha>..HEAD
133
+ --base <ref> Base ref for the diff range (default: origin/main when present)
134
+ --out <file> Also write the checklist to <file>
135
+ --adapter, --target, --json
136
+
137
+ Example:
138
+ mm-harness review checklist --domain perps
139
+ mm-harness review checklist --domain perps --since 4dd9be9 --out temp/tasks/review/CHECKLIST.md
140
+ mm-harness review checklist --json`,
141
+ contract: {
142
+ options: options(HELP, JSON, TARGET, ADAPTER, {
143
+ "--domain": value(),
144
+ "--since": value(),
145
+ "--base": value(),
146
+ "--out": value()
147
+ }),
148
+ positionals: [{ label: "action", choices: ["checklist"] }],
149
+ minimumPositionals: 1
150
+ }
86
151
  },
87
- domain: {
88
- options: options(HELP, JSON, TARGET, ADAPTER, {
89
- "--domain": value(),
90
- "--base": value()
91
- })
152
+ {
153
+ name: "domain",
154
+ summary: "Which team library owns the change: declared value first, else owned-paths.json against the changed files.",
155
+ example: "mm-harness domain",
156
+ helpText: `mm-harness domain [flags]
157
+
158
+ Prints the domain name (or "none"). A declared --domain / MM_HARNESS_DOMAIN wins;
159
+ otherwise the changed files (base...HEAD, working tree, untracked) are matched
160
+ against owned-paths.json in every reachable library (RECIPE_LIBRARY_PATH,
161
+ mm-harness config, sibling directories, .skills-cache).
162
+
163
+ --domain <name> Declared domain (skips classification)
164
+ --base <ref> Base ref for the changed-file range (default: origin/main when present)
165
+ --adapter, --target, --json
166
+
167
+ Example:
168
+ mm-harness domain
169
+ mm-harness domain --json`,
170
+ contract: {
171
+ options: options(HELP, JSON, TARGET, ADAPTER, {
172
+ "--domain": value(),
173
+ "--base": value()
174
+ })
175
+ }
92
176
  },
93
- "pr-body": {
94
- options: options(HELP, JSON, {
95
- "--command": value(),
96
- "--out": value()
97
- }),
98
- positionals: [{ label: "action", choices: ["render"] }, { label: "task-dir" }],
99
- minimumPositionals: 2
177
+ {
178
+ name: "pr-body",
179
+ summary: "Render the publishable PR body: the authored description plus the recipe and run log sections built from artifacts.",
180
+ example: "mm-harness pr-body render temp/tasks/dev/TAT-1",
181
+ helpText: `mm-harness pr-body render <task-dir> [flags]
182
+
183
+ Reads <task-dir>/artifacts/pr-description.md (the PR body the worker wrote in
184
+ the repository PR template shape) and inserts the machine sections rendered
185
+ from artifacts: "Validation Recipe" from artifacts/recipe.json, "Validation
186
+ Logs" from artifacts/recipe-run/report.md, and "Recipe Workflow" from
187
+ artifacts/workflow.mmd when it exists. Any such section the worker pasted by
188
+ hand is replaced. The sections go before the first checklist section, else at
189
+ the end. Fences are always longer than any backtick run in the content and
190
+ close on their own line. Writes <task-dir>/artifacts/pr-body.md.
191
+
192
+ --command <text> Recipe invocation to show above the run log
193
+ --out <file> Write elsewhere than artifacts/pr-body.md
194
+ --json Machine-readable result
195
+
196
+ Example:
197
+ mm-harness pr-body render temp/tasks/dev/TAT-1 --json`,
198
+ contract: {
199
+ options: options(HELP, JSON, {
200
+ "--command": value(),
201
+ "--out": value()
202
+ }),
203
+ positionals: [{ label: "action", choices: ["render"] }, { label: "task-dir" }],
204
+ minimumPositionals: 2
205
+ }
100
206
  },
101
- config: {
102
- options: options(HELP, JSON),
103
- positionals: [
104
- { label: "action", choices: ["list", "path", "get", "set", "unset"] },
105
- { label: "key" },
106
- { label: "value" }
107
- ]
207
+ {
208
+ name: "config",
209
+ summary: "Per-engineer locations and optional recipe advisor settings.",
210
+ example: "mm-harness config set references.extension ~/dev/metamask-extension",
211
+ helpText: `mm-harness config <list|path|get <key>|set <key> <value>|unset <key>> [--json]
212
+
213
+ Stored outside every repo in ~/.mm-harness/config.json (override: MM_HARNESS_CONFIG).
214
+ Keys: libraries.<name> (team library checkout), references.<mobile|extension|core|terminal>
215
+ (the checkout used as the parity reference), advisor.enabled (true/false),
216
+ advisor.provider, advisor.model, advisor.stages (plan,draft,evidence). Advisor
217
+ settings contain no credentials and default to disabled with planning only.
218
+ Env always wins over location config:
219
+ RECIPE_LIBRARY_PATH for libraries, MM_HARNESS_REF_<ADAPTER> for references.
220
+
221
+ Example:
222
+ mm-harness config set libraries.perps ~/dev/experimental-metamask-recipe-perps
223
+ mm-harness config set references.extension ~/dev/metamask-extension
224
+ mm-harness config set advisor.provider typesafe
225
+ mm-harness config set advisor.enabled true
226
+ mm-harness config list`,
227
+ contract: {
228
+ options: options(HELP, JSON),
229
+ positionals: [
230
+ { label: "action", choices: ["list", "path", "get", "set", "unset"] },
231
+ { label: "key" },
232
+ { label: "value" }
233
+ ]
234
+ }
108
235
  },
109
- tutorial: {
110
- options: options(HELP, JSON, NO_OPEN)
236
+ {
237
+ name: "tutorial",
238
+ summary: "Open the protected visual recipe tutorial.",
239
+ example: "mm-harness tutorial",
240
+ helpText: `mm-harness tutorial [flags]
241
+
242
+ Source checkouts open the local tutorial on port 8765. Installed releases
243
+ open the protected MetaMask GitHub Pages site.
244
+
245
+ --no-open Print the tutorial URL without opening it
246
+ --json Print version, URL, and open status as JSON
247
+
248
+ Example:
249
+ mm-harness tutorial`,
250
+ contract: {
251
+ options: options(HELP, JSON, NO_OPEN)
252
+ }
111
253
  },
112
- "setup-base": {
113
- options: options(HELP, JSON, {
114
- "--dir": value(),
115
- "--counts": value(),
116
- "--only": value(),
117
- "--dry-run": bool(),
118
- "--force": bool(),
119
- "--show-config": bool(),
120
- "--reset-config": bool(),
121
- "--skip-harness-update": bool()
122
- })
254
+ {
255
+ name: "setup-base",
256
+ summary: "Bootstrap numbered MetaMask product checkouts and install their dependencies.",
257
+ example: "mm-harness setup-base --only core --counts core=1",
258
+ helpText: `mm-harness setup-base [flags]
259
+
260
+ Bootstrap numbered MetaMask Extension, Mobile, and Core checkouts under one
261
+ base directory, then run each checkout's pinned dependency install.
262
+
263
+ --dir <base> Destination root
264
+ --counts <repo=n,...> Copies per selected repository
265
+ --only <extension,mobile,core> Repository subset
266
+ --dry-run Print the plan without changing anything
267
+ --force Override the free-space refusal
268
+ --json Machine-readable terminal summary
269
+ --show-config Print saved preferences and exit
270
+ --reset-config Remove saved preferences and exit
271
+ --skip-harness-update Skip the installed-version notice
272
+
273
+ Example:
274
+ mm-harness setup-base --only core --counts core=1`,
275
+ contract: {
276
+ options: options(HELP, JSON, {
277
+ "--dir": value(),
278
+ "--counts": value(),
279
+ "--only": value(),
280
+ "--dry-run": bool(),
281
+ "--force": bool(),
282
+ "--show-config": bool(),
283
+ "--reset-config": bool(),
284
+ "--skip-harness-update": bool()
285
+ })
286
+ }
123
287
  },
124
- status: {
288
+ {
289
+ name: "status",
125
290
  aliases: ["health", "home"],
126
- options: options(HELP, JSON, TARGET, DEVICE, {
127
- "--fast": bool(),
128
- "--all-devices": bool(),
129
- "--watch": bool(),
130
- "--task": value()
131
- })
291
+ summary: "Home dashboard for a checkout: detected adapter, next command, and (mobile) connected devices with the selected target.",
292
+ example: "mm-harness status",
293
+ helpText: `mm-harness status [flags]
294
+
295
+ Home dashboard for the current checkout \u2014 the detected adapter, the next command
296
+ to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
297
+
298
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
299
+ --target <path> Checkout path (default: cwd)
300
+ --json Machine-readable envelope { adapter, target, view, devices[], next }
301
+ --fast Skip all live-state probes (instant output, safe for scripts)
302
+ --all-devices Mobile only: show every connected device instead of the slot-scoped target
303
+ --watch Live read-only view of the task this checkout is working through (refreshes every 2s, Ctrl-C to leave)
304
+ --task <dir> Task directory for the view (default: the most recently marked one under temp/tasks/)
305
+
306
+ The task view reads task files only \u2014 no probe, no launch, no write \u2014 and shows
307
+ the same facts whether a skill or a control plane started the run: slot and
308
+ device isolation, template and flow, fixture and sandbox readiness, then the
309
+ checklist with the time each row took, how long the current row has been
310
+ running, and acceptance criteria with recorded verdicts. The home dashboard
311
+ also shows criteria for an ongoing task. --task without --watch prints it once.
312
+ With --json, --watch emits one
313
+ object per change (newline-delimited) instead of redrawing.
314
+
315
+ The checkout view (also under "view" in --json, and printed by doctor) is what
316
+ this checkout is bound to: the runtime-context fields that keep it off other
317
+ slots' ports and devices, the recipe libraries this machine resolves, and the
318
+ wallet fixture it last applied.
319
+
320
+ Human output: static device list prints immediately; live-state lines append
321
+ after the ~2s probe window (screen, wallet, account, fixture per device).
322
+ By default mobile status shows the configured target only; --device <id>
323
+ switches the active target for that command. Other devices that answer on the
324
+ same Metro are reported as additional reachable targets.
325
+
326
+ The devices[] section carries per device:
327
+ { platform, id, name, state, selected } always present
328
+ { currentScreen, walletState, selectedAccount, fixtureStatus } when bridge reachable
329
+ { liveState: 'no-bridge' } when bridge unreachable or --fast
330
+
331
+ All RN targets attached to the checkout's Metro are probed and matched to their
332
+ devices; a device whose target cannot be confidently matched (Metro's device
333
+ description matches neither the serial nor the model name) degrades to
334
+ liveState:'no-bridge'. An attached target whose build predates __AGENTIC__
335
+ reports liveState:'bridge-absent'.
336
+ Extension/core live-state probing is V1-pending (use doctor for runtime status).
337
+
338
+ Example:
339
+ mm-harness status
340
+ mm-harness status --json
341
+ mm-harness status --fast
342
+ mm-harness status --watch`,
343
+ contract: {
344
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
345
+ "--fast": bool(),
346
+ "--all-devices": bool(),
347
+ "--watch": bool(),
348
+ "--task": value()
349
+ })
350
+ }
132
351
  },
133
- checklist: {
134
- usage: "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]",
135
- options: options(HELP, JSON, {
136
- "--mark-last": bool(),
137
- "--already-fixed": bool(),
138
- "--no-self-review": bool(),
139
- "--skip-learnings": bool(),
140
- "--skip-checklist": bool(),
141
- "--reason": value(),
142
- "--checklist": value(),
143
- "--signal": value(),
144
- "--share": bool(),
145
- "--config": value(),
146
- "--destination": value(),
147
- "--metadata": value()
148
- }),
149
- positionals: [
150
- { label: "action", choices: ["mark", "closeout"] },
151
- { label: "task-dir" },
152
- {
153
- label: "step",
154
- validate: (step) => {
155
- if (["start", "complete", "no-change", "blocked"].includes(step)) return true;
156
- const numeric = Number(step);
157
- return Number.isInteger(numeric) && numeric >= 1;
158
- },
159
- validDescription: "start|complete|no-change|blocked|a positive numeric step"
352
+ {
353
+ name: "checklist",
354
+ summary: "Mark checklist progress and close out scrubbed learning packages.",
355
+ example: "mm-harness checklist mark <task-dir> start",
356
+ helpText: `mm-harness checklist <mark|closeout> <task-dir> [step] [options]
357
+
358
+ mark delegates checklist state to @farmslot/agent-runtime. Successful complete
359
+ and no-change marks stage a learning package through @farmslot/handoff.
360
+ closeout exposes that same Handoff boundary directly; --share remains explicit.
361
+ The task directory is the one mm-harness task init wrote. Without a
362
+ checklist-target.json it marks CHECKLIST.md and writes SIGNAL.json; only a
363
+ role switch writes that manifest, to point at its own checklist.
364
+
365
+ Steps: start | 1 | 2 | ... | complete | no-change | blocked
366
+
367
+ Example:
368
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> start
369
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
370
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> sub start qa-cases --step 2 --from inputs/qa-cases.md
371
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> sub qa-cases 1
372
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
373
+ mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`,
374
+ contract: {
375
+ usage: "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]",
376
+ options: options(HELP, JSON, {
377
+ "--mark-last": bool(),
378
+ "--already-fixed": bool(),
379
+ "--no-self-review": bool(),
380
+ "--skip-learnings": bool(),
381
+ "--skip-checklist": bool(),
382
+ "--reason": value(),
383
+ "--checklist": value(),
384
+ "--signal": value(),
385
+ "--share": bool(),
386
+ "--config": value(),
387
+ "--destination": value(),
388
+ "--metadata": value()
389
+ }),
390
+ positionals: [
391
+ { label: "action", choices: ["mark", "closeout"] },
392
+ { label: "task-dir" },
393
+ {
394
+ label: "step",
395
+ validate: (step) => {
396
+ if (["start", "complete", "no-change", "blocked"].includes(step)) return true;
397
+ const numeric = Number(step);
398
+ return Number.isInteger(numeric) && numeric >= 1;
399
+ },
400
+ validDescription: "start|complete|no-change|blocked|a positive numeric step"
401
+ }
402
+ ],
403
+ minimumPositionals: 2,
404
+ // `mark <task-dir> sub …` is agent-runtime's child-unit grammar; it validates it.
405
+ bypass: (tokens) => tokens[0] === "mark" && Boolean(tokens[1]) && !tokens[1]?.startsWith("-") && tokens[2] === "sub",
406
+ refine: (positionals, seen, fail) => {
407
+ const action = positionals[0];
408
+ if (action !== "mark" && action !== "closeout") return null;
409
+ const invalid = (action === "mark" ? CLOSEOUT_ONLY : MARK_ONLY).find((option) => seen.has(option));
410
+ if (invalid) return fail.usage("CLI_UNKNOWN_OPTION", `${invalid} is not valid with checklist ${action}.`);
411
+ if (action === "mark" && positionals.length < 3) return fail.missingPositional("checklist mark requires <step>.");
412
+ if (action === "closeout" && positionals.length > 2) {
413
+ return fail.usage(
414
+ "CLI_EXCESS_POSITIONAL",
415
+ `unexpected positional '${positionals[2]}'; checklist closeout accepts only <task-dir>.`
416
+ );
417
+ }
418
+ return null;
160
419
  }
161
- ],
162
- minimumPositionals: 2
420
+ }
163
421
  },
164
- prepare: {
165
- usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]",
166
- options: options(HELP, JSON, TARGET, {
167
- "--platform": { kind: "value", choices: adapterIds },
168
- "--mobile-platform": value(["ios", "android"]),
169
- "--clear-metro": bool(),
170
- "--artifacts-dir": value(),
171
- "--remote-flag": value()
172
- })
422
+ {
423
+ name: "prepare",
424
+ summary: "Bring this checkout to a proven-ready state and write the readiness record.",
425
+ example: "mm-harness prepare --platform extension",
426
+ helpText: `mm-harness prepare [flags]
427
+
428
+ Run the readiness chain for this checkout and record what it did as one
429
+ protocol readiness record (sandbox.json). Steps run in order and stop at the
430
+ first failure; later steps are recorded as skipped.
431
+
432
+ doctor --fix status launch --verify fixtures set verify
433
+
434
+ Core skips launch and fixtures (headless; verify is its readiness gate).
435
+ Mobile infers ios|android from the connected devices reported by status and
436
+ refuses with the numbered choice when that is ambiguous.
437
+
438
+ --target <path> Checkout path (default: cwd)
439
+ --platform <{adapters}> Force the platform (auto-detected inside a checkout)
440
+ --mobile-platform <ios|android> Mobile target when status cannot decide it
441
+ --clear-metro Mobile only: clear Metro's transform cache in the launch step
442
+ --artifacts-dir <dir> Where sandbox.json and the per-step reports land
443
+ (default: <target>/temp/recipe/runtime)
444
+ --json Print the readiness record
445
+
446
+ Exit 0 when the record is ready, 1 otherwise.
447
+
448
+ Example:
449
+ mm-harness prepare
450
+ mm-harness prepare --platform mobile --mobile-platform ios --json
451
+ mm-harness prepare --artifacts-dir temp/tasks/fix/TAT-1/artifacts --json`,
452
+ contract: {
453
+ usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]",
454
+ options: options(HELP, JSON, TARGET, {
455
+ "--platform": value(adapterIds),
456
+ "--mobile-platform": value(["ios", "android"]),
457
+ "--clear-metro": bool(),
458
+ "--artifacts-dir": value(),
459
+ "--remote-flag": value()
460
+ })
461
+ }
173
462
  },
174
- task: {
175
- usage: "mm-harness task init <task-dir> --flow f --template id --title t [options]",
176
- options: options(HELP, JSON, {
177
- "--flow": value(),
178
- "--run-mode": value(["autonomous", "interactive", "validation"]),
179
- "--platform": value(),
180
- "--template": value(),
181
- "--title": value(),
182
- "--task-text": value(),
183
- "--task-file": value(),
184
- "--acceptance": value(),
185
- "--ticket": value(),
186
- "--source-ref": value(),
187
- "--surface": value(),
188
- "--project": value(),
189
- "--repo": value(),
190
- "--attempt-id": value(),
191
- "--var": value(),
192
- "--task-dir-label": value(),
193
- "--mode-preamble": value(),
194
- "--addendum-file": value(),
195
- "--mark-command": value(),
196
- "--dir": value(),
197
- "--domain-dir": value(),
198
- "--project-worker": value(),
199
- "--project-name": value(),
200
- "--package-templates": value(),
201
- "--package-id": value(),
202
- "--domain": value()
203
- }),
204
- positionals: [
205
- { label: "action", choices: ["init"] },
206
- { label: "task-dir" }
207
- ],
208
- minimumPositionals: 2
463
+ {
464
+ name: "task",
465
+ summary: "Create the task directory an agent works from, out of a shared execution template.",
466
+ example: 'mm-harness task init temp/tasks/fix/TAT-1 --flow fix-bug --template fix-bug/mobile --title "Wrong total"',
467
+ helpText: `mm-harness task init <task-dir> [options]
468
+
469
+ Write the task directory from a selected execution template: TASK.md, the
470
+ CHECKLIST.md whose boxes are the steps, a mark shim routed through
471
+ mm-harness checklist mark, and inputs/ (handoff.json, worker-terminal-contract.json).
472
+ checklist-target.json is NOT written \u2014 absent means CHECKLIST.md + SIGNAL.json.
473
+
474
+ Applied when you do not pass them: --surface skill, --mark-command for this
475
+ executable, and --platform from the detected checkout.
476
+
477
+ Task:
478
+ --title <t> Task title (required)
479
+ --task-text <s> | --task-file <p> Task description, inline or from a file
480
+ --acceptance <criterion> Authored acceptance criterion (repeatable, preserved in order)
481
+ --ticket <key> Ticket key
482
+ --source-ref <url> Where the task came from
483
+
484
+ Selection (same catalog as execution-template):
485
+ --flow <flow> Exact flow (required)
486
+ --run-mode <mode> autonomous|interactive|validation (optional;
487
+ set by the Farmslot control plane, omit for
488
+ skill/manual runs)
489
+ --platform <platform> Exact platform
490
+ --template <id> Exact template id (required)
491
+ --domain <domain> Exact domain
492
+ --dir <path> Custom flow-tree source (repeatable)
493
+ --domain-dir <domain=path> Domain-scoped team source (repeatable)
494
+ --package-templates <path> Canonical package flow-tree root
495
+ --package-id <id> Source label for the package root
496
+ --project-worker <path> Project template directory
497
+ --project-name <name> Source label for project templates
498
+
499
+ Identity and rendering:
500
+ --surface <s> Handoff surface (default: skill)
501
+ --project <name> --repo <owner/name> --attempt-id <id>
502
+ --var KEY=VALUE Template placeholder value (repeatable)
503
+ --task-dir-label <label> Task directory as written into the documents
504
+ --mode-preamble <text> Run-mode preamble for TASK.md
505
+ --addendum-file <path> Extra task-document section
506
+ --mark-command <cmd> Override the recorded mark command
507
+ --json Machine-readable output
508
+
509
+ Example:
510
+ mm-harness task init temp/tasks/fix/TAT-1 --flow fix-bug \\
511
+ --template fix-bug/mobile --package-templates <path> \\
512
+ --title "Wrong total on the receipt" --ticket TAT-1 --json`,
513
+ contract: {
514
+ usage: "mm-harness task init <task-dir> --flow f --template id --title t [options]",
515
+ options: options(HELP, JSON, {
516
+ "--flow": value(),
517
+ "--run-mode": value(["autonomous", "interactive", "validation"]),
518
+ "--platform": value(),
519
+ "--template": value(),
520
+ "--title": value(),
521
+ "--task-text": value(),
522
+ "--task-file": value(),
523
+ "--acceptance": value(),
524
+ "--ticket": value(),
525
+ "--source-ref": value(),
526
+ "--surface": value(),
527
+ "--project": value(),
528
+ "--repo": value(),
529
+ "--attempt-id": value(),
530
+ "--var": value(),
531
+ "--task-dir-label": value(),
532
+ "--mode-preamble": value(),
533
+ "--addendum-file": value(),
534
+ "--mark-command": value(),
535
+ "--dir": value(),
536
+ "--domain-dir": value(),
537
+ "--project-worker": value(),
538
+ "--project-name": value(),
539
+ "--package-templates": value(),
540
+ "--package-id": value(),
541
+ "--domain": value()
542
+ }),
543
+ positionals: [
544
+ { label: "action", choices: ["init"] },
545
+ { label: "task-dir" }
546
+ ],
547
+ minimumPositionals: 2
548
+ }
209
549
  },
210
- "execution-template": {
211
- usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
212
- options: options(HELP, JSON, {
213
- "--dir": value(),
214
- "--domain-dir": value(),
215
- "--project-worker": value(),
216
- "--project-name": value(),
217
- "--package-templates": value(),
218
- "--package-id": value(),
219
- "--flow": value(),
220
- "--run-mode": value(["autonomous", "interactive", "validation"]),
221
- "--platform": value(),
222
- "--domain": value(),
223
- "--id": value(),
224
- "--provenance": value(),
225
- "--include-shadowed": bool(),
226
- "--no-include-shadowed": bool(),
227
- "--title": value(),
228
- "--force": bool()
229
- }),
230
- positionals: [
231
- { label: "action", choices: ["list", "materialize", "lint", "new"] }
232
- ],
233
- minimumPositionals: 1,
234
- variadic: { label: "target", validate: (value2) => value2.length > 0 }
550
+ {
551
+ name: "execution-template",
552
+ summary: "Discover, validate, and materialize shared agent checklists.",
553
+ example: "mm-harness execution-template list --package-templates <path> --json",
554
+ helpText: `mm-harness execution-template <list|materialize|lint|new> [options]
555
+
556
+ Shared Markdown checklist catalog used by direct and orchestrated workflows.
557
+ Sources use the flow-tree shape <root>/<flow>/<variant>.md.
558
+
559
+ list List compatible templates and portable source provenance.
560
+ materialize Select one template, copy its immutable snapshot, and optionally
561
+ write a provenance record.
562
+ lint Validate one template file or flow-tree directory.
563
+ new Create a minimal starter template.
564
+
565
+ Common source/filter options:
566
+ --dir <path> Custom flow-tree source (repeatable)
567
+ --domain-dir <domain=path> Domain-scoped team source (repeatable)
568
+ --package-templates <path> Canonical package flow-tree root
569
+ --package-id <id> Source label for the package root
570
+ --project-worker <path> Project template directory
571
+ --project-name <name> Source label for project templates
572
+ --flow <flow> Exact flow
573
+ --platform <platform> Exact platform
574
+ --run-mode <mode> autonomous|interactive|validation (optional;
575
+ set by the Farmslot control plane, omit for
576
+ skill/manual runs)
577
+ --domain <domain> Exact domain
578
+ --id <id> Exact template id
579
+ --json Machine-readable output
580
+
581
+ Materialize:
582
+ mm-harness execution-template materialize <output> --flow fix-bug \\
583
+ --platform mobile --id fix-bug/mobile \\
584
+ --package-templates <path> --provenance <path> --json`,
585
+ contract: {
586
+ usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
587
+ options: options(HELP, JSON, {
588
+ "--dir": value(),
589
+ "--domain-dir": value(),
590
+ "--project-worker": value(),
591
+ "--project-name": value(),
592
+ "--package-templates": value(),
593
+ "--package-id": value(),
594
+ "--flow": value(),
595
+ "--run-mode": value(["autonomous", "interactive", "validation"]),
596
+ "--platform": value(),
597
+ "--domain": value(),
598
+ "--id": value(),
599
+ "--provenance": value(),
600
+ "--include-shadowed": bool(),
601
+ "--no-include-shadowed": bool(),
602
+ "--title": value(),
603
+ "--force": bool()
604
+ }),
605
+ positionals: [
606
+ { label: "action", choices: ["list", "materialize", "lint", "new"] }
607
+ ],
608
+ minimumPositionals: 1,
609
+ variadic: { label: "target", validate: (value2) => value2.length > 0 },
610
+ refine: (positionals, _seen, fail) => {
611
+ const action = positionals[0] ?? "";
612
+ if (!["list", "materialize", "lint", "new"].includes(action)) return null;
613
+ const targetCount = Math.max(0, positionals.length - 1);
614
+ if (action === "list" && targetCount > 0) {
615
+ return fail.usage("CLI_EXCESS_POSITIONAL", `execution-template list does not accept positional '${positionals[1]}'.`);
616
+ }
617
+ if (action !== "list" && targetCount === 0) {
618
+ return fail.missingPositional(`execution-template ${action} requires <target>.`);
619
+ }
620
+ if (targetCount > 1) {
621
+ return fail.usage(
622
+ "CLI_EXCESS_POSITIONAL",
623
+ `unexpected positional '${positionals[2]}'; execution-template ${action} accepts one <target>.`
624
+ );
625
+ }
626
+ return null;
627
+ }
628
+ }
235
629
  },
236
- actions: {
237
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
238
- "--action": value(),
239
- "--matrix": bool(),
240
- "--categories": bool(),
241
- "--category": value(),
242
- "--action-manifest": value(),
243
- "--library": value(),
244
- "--raw": bool()
245
- }),
246
- positionals: [{ label: "query" }],
247
- noPositionalsWith: ["--action", "--categories", "--raw"]
630
+ {
631
+ name: "actions",
632
+ summary: "Discover typed single operations and their fields.",
633
+ example: "mm-harness actions --adapter mobile",
634
+ helpText: `mm-harness actions [query] [flags]
635
+
636
+ Discover typed single operations and their field schemas for this checkout.
637
+
638
+ query Search names, categories, fields, and descriptions (typo-tolerant)
639
+ --action <name> Describe one action; fuzzy-resolves like call (short or full name)
640
+ --matrix Compare action availability across Mobile, Extension, Core, and Terminal
641
+ --categories List compact action categories and counts
642
+ --category <name> List only one category (for example ui, wallet, or perps)
643
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
644
+ --target <path> Checkout path (default: cwd)
645
+ --raw Dump raw action registry JSON
646
+ --json Machine-readable output
647
+
648
+ Example:
649
+ mm-harness actions --adapter mobile
650
+ mm-harness actions --matrix --json
651
+ mm-harness actions positions --adapter mobile
652
+ mm-harness actions --adapter mobile --categories --json
653
+ mm-harness actions --adapter mobile --category ui --json
654
+ mm-harness actions --adapter mobile --action assert_orders
655
+ mm-harness actions --adapter extension --raw`,
656
+ contract: {
657
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
658
+ "--action": value(),
659
+ "--matrix": bool(),
660
+ "--categories": bool(),
661
+ "--category": value(),
662
+ "--action-manifest": value(),
663
+ "--library": value(),
664
+ "--raw": bool()
665
+ }),
666
+ positionals: [{ label: "query" }],
667
+ noPositionalsWith: ["--action", "--categories", "--raw"]
668
+ }
248
669
  },
249
- stop: {
250
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
251
- "--port": value(),
252
- "--watcher-port": value()
253
- })
670
+ {
671
+ name: "stop",
672
+ summary: "Stop this checkout runtime (mobile Metro / Extension watcher + owned browser) and close its log windows.",
673
+ example: "mm-harness stop",
674
+ helpText: `mm-harness stop [flags]
675
+
676
+ Stop the runtime this checkout owns and close its tmux log-tail windows,
677
+ scoped to this checkout so concurrent slots are untouched. Idempotent \u2014
678
+ nothing running is success, not an error. Behavior is per platform:
679
+ mobile stop the port-scoped Metro dev server
680
+ extension stop webpack watcher, owned Chrome/CDP profile processes, viewers, and stale markers
681
+ core headless \u2014 no dev server to stop (teaching error)
682
+
683
+ --port <port> Metro/webpack port override; Extension CDP remains checkout-context scoped
684
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
685
+ --target <path> Checkout path (default: cwd)
686
+ --json Machine-readable output
687
+
688
+ Example:
689
+ mm-harness stop
690
+ mm-harness stop --port 8061`,
691
+ contract: {
692
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
693
+ "--port": value(),
694
+ "--watcher-port": value()
695
+ })
696
+ }
254
697
  },
255
- call: {
256
- options: options(HELP, JSON, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
257
- "--list": bool(),
258
- "--domain": value(),
259
- "--source": value(),
260
- "--sort": value(["domain", "name", "library"]),
261
- "--arg": value()
262
- }),
263
- positionals: [{ label: "action" }],
264
- minimumPositionals: 0,
265
- requiredUnless: ["--list"],
266
- noPositionalsWith: ["--list"],
267
- leadingPositionals: 1,
268
- variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
698
+ {
699
+ name: "call",
700
+ summary: "Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; key=value inputs; same trace/evidence as run).",
701
+ example: "mm-harness call ensure_unlocked",
702
+ helpText: `mm-harness call <action> [key=value ...] [flags]
703
+
704
+ Run one action in isolation as a one-node recipe through the real engine path.
705
+ Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
706
+ if unique; ambiguous = exit 2. Pass action fields as key=value; numbers,
707
+ booleans, arrays, and objects are parsed as JSON-like values.
708
+ Actions differ per adapter \u2014 list this checkout's with:
709
+ mm-harness actions.
710
+
711
+ --list List actions accepted by call for this adapter; no <action> needed
712
+ --domain <name> With --list: filter by action domain, e.g. perps or swap-bridge
713
+ --source <name> With --list: filter by origin, e.g. metamask, perps, or official
714
+ --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
715
+ --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
716
+ --cdp-port <port> Extension only: bind the action to this live browser runtime.
717
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
718
+ --target <path> Checkout path (default: cwd)
719
+ --artifacts-dir <dir> Where to write evidence (default: <task>/artifacts/calls/<action>-<id>
720
+ inside a task, else temp/recipe/calls/<action>-<id>)
721
+ --action-manifest <path> Override the action manifest
722
+ --library <name=path> Add/override a recipe-library source (repeatable)
723
+ --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
724
+ --json Machine-readable output
725
+
726
+ Example (real actions; run mm-harness actions for this checkout's full set):
727
+ mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
728
+ mm-harness call navigate page=perps --adapter mobile
729
+ mm-harness call command cmd="echo hi" --adapter core # the universal action (all adapters)
730
+
731
+ Remote feature flags (mobile applies at runtime; extension pins at launch):
732
+ mm-harness call metamask.feature_flags.read --adapter mobile
733
+ mm-harness call metamask.feature_flags.set --adapter mobile \\
734
+ 'flags={"perpsTAT3938AbtestScreenVsBottomSheet":"treatment"}'
735
+ mm-harness call metamask.feature_flags.clear --adapter mobile`,
736
+ contract: {
737
+ options: options(HELP, JSON, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
738
+ "--list": bool(),
739
+ "--domain": value(),
740
+ "--source": value(),
741
+ "--sort": value(["domain", "name", "library"])
742
+ }),
743
+ positionals: [{ label: "action" }],
744
+ minimumPositionals: 0,
745
+ requiredUnless: ["--list"],
746
+ noPositionalsWith: ["--list"],
747
+ leadingPositionals: 1,
748
+ variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u },
749
+ missingPositionalAction: (example) => `List actions: mm-harness call --list
750
+ Example: ${example}
751
+ More: mm-harness call --help`
752
+ }
269
753
  },
270
- run: {
271
- options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, RUN_FUNDING, {
272
- "--list": bool(),
273
- "--domain": value(),
274
- "--source": value(),
275
- "--sort": value(["domain", "name", "library"]),
276
- "--plan": bool(),
277
- "--proof": bool(),
278
- "--describe": bool(),
279
- "--hud": value(["auto", "show", "hide"])
280
- }),
281
- positionals: [{ label: "recipe" }],
282
- minimumPositionals: 1,
283
- requiredUnless: ["--list"],
284
- noPositionalsWith: ["--list"],
285
- variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
754
+ {
755
+ name: "run",
756
+ summary: "Execute a complete proof and write its evidence.",
757
+ example: "mm-harness run recipe.json",
758
+ helpText: `mm-harness run <recipe|recipe.json> [key=value ...] [flags]
759
+
760
+ Validate and execute a complete proof, then write summary, trace, diagnostics,
761
+ and artifacts. Inside a checkout, the adapter, target, ports, and artifact
762
+ directory are normally detected automatically.
763
+
764
+ Discovery:
765
+ --list List complete recipes accepted by run for this adapter; no <recipe> needed
766
+ --domain <name> With --list: filter by recipe domain, e.g. perps or swap-bridge
767
+ --source <name> With --list: filter by library origin, e.g. metamask or perps
768
+ --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
769
+ --describe Show one recipe's parameters, composed actions/recipes, and runnable command
770
+ --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
771
+ --proof Preflight behavioral proof bindings before planning/execution; runtime evidence is still required.
772
+
773
+ Common overrides:
774
+ --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
775
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
776
+ --platform <ios|android> Mobile platform preference; use --adapter for the product adapter
777
+ --target <path> Checkout path (default: cwd)
778
+ --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
779
+ --funding-request <absolute-file> Original consented mutation request for external-mutation actions
780
+ --funding-preflight <absolute-file> Eligible funding preflight bound to the request bytes
781
+ --funding-release-proof <absolute-file> Exact release proof independently bound to funding eligibility
782
+ --funding-balance-proof <absolute-file> Exact balance proof and contained visible evidence chain
783
+ --library <name=path> Add/override a recipe-library source (repeatable)
784
+ --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
785
+ --json Machine-readable output
786
+ --json-stream Line-flushed JSONL progress + terminal event
787
+ --record-video=full-run Record a video of the run
788
+ --hud <show|hide> Recipe HUD: each node's intent drawn over the app on every adapter, headless included (default: show)
789
+
790
+ Advanced integrations:
791
+ runtime: --cdp-port, --watcher-port, --runtime-dir, --slot,
792
+ --validation-runtime-dir, --launch-existing-dist
793
+ managed trust boundary (not needed for normal runs; Farmslot supplies it):
794
+ --source-trust, --source-kind, --source-name, --source-digest,
795
+ --approve-plan (binds approval to the exact reviewed plan)
796
+ schema: --action-manifest
797
+
798
+ Example:
799
+ mm-harness run wallet.smoke --describe
800
+ mm-harness run perps.release-market-search query=Bitcoin --plan --adapter mobile
801
+ mm-harness run recipe.json market=ETH --adapter extension`,
802
+ contract: {
803
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, RUN_FUNDING, {
804
+ "--list": bool(),
805
+ "--domain": value(),
806
+ "--source": value(),
807
+ "--sort": value(["domain", "name", "library"]),
808
+ "--plan": bool(),
809
+ "--proof": bool(),
810
+ "--describe": bool(),
811
+ "--hud": value(["show", "hide"])
812
+ }),
813
+ positionals: [{ label: "recipe" }],
814
+ minimumPositionals: 1,
815
+ requiredUnless: ["--list"],
816
+ noPositionalsWith: ["--list"],
817
+ variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u },
818
+ missingPositionalAction: (example) => `List recipes: mm-harness run --list
819
+ Example: ${example}
820
+ More: mm-harness run --help`
821
+ }
286
822
  },
287
- last: {
288
- options: options(HELP, JSON, TARGET, {
289
- "--runtime-dir": value()
290
- })
823
+ {
824
+ name: "last",
825
+ summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
826
+ example: "mm-harness last --json",
827
+ helpText: `mm-harness last [flags]
828
+
829
+ Read the atomic per-checkout resumability journal. Discovery commands do not
830
+ replace it, and an interrupted process remains recorded as verdict=running.
831
+
832
+ --target <path> Checkout path (default: cwd)
833
+ --runtime-dir <dir> Runtime dir containing command operation records
834
+ --json Machine-readable envelope
835
+
836
+ Example:
837
+ mm-harness last --json`,
838
+ contract: {
839
+ options: options(HELP, JSON, TARGET, {
840
+ "--runtime-dir": value()
841
+ })
842
+ }
291
843
  },
292
- doctor: {
293
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
294
- "--fix": bool(),
295
- "--expect-live": bool(),
296
- "--print-ready": bool(),
297
- "--all-devices": bool(),
298
- "--action-manifest": value(),
299
- "--port": value()
300
- })
844
+ {
845
+ name: "doctor",
846
+ summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
847
+ example: "mm-harness doctor",
848
+ helpText: `mm-harness doctor [flags]
849
+
850
+ Readiness check for a checkout \u2014 no app launch. Reports the same checkout view
851
+ status does \u2014 runtime-context isolation, resolved recipe libraries, and the last
852
+ applied wallet fixture \u2014 so there is no hunting for files.
853
+
854
+ --fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
855
+ --expect-live Exit-coded liveness gate only (human verdict on stderr). Use without --print-ready when callers need pass/fail without a Farmslot indicator line
856
+ --print-ready Farmslot health_check mode: implies --expect-live and prints health.ready_indicator on stdout (extension/mobile: OK; core: ready)
857
+ --cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
858
+ --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
859
+ --all-devices Mobile only: show every connected device instead of the slot-scoped target
860
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
861
+ --target <path> Checkout path (default: cwd)
862
+ --runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
863
+ --json Machine-readable output (incompatible with --print-ready)
864
+
865
+ Example:
866
+ mm-harness doctor
867
+ mm-harness doctor --fix --json
868
+ mm-harness doctor --adapter extension --target /path/to/checkout --cdp-port 6662 --print-ready
869
+ mm-harness doctor --adapter mobile --target /path/to/checkout`,
870
+ contract: {
871
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
872
+ "--fix": bool(),
873
+ "--expect-live": bool(),
874
+ "--print-ready": bool(),
875
+ "--all-devices": bool(),
876
+ "--action-manifest": value(),
877
+ "--port": value()
878
+ })
879
+ }
301
880
  },
302
- check: {
303
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
304
- "--base": value(),
305
- "--profile": value(["fast", "full"]),
306
- "--fix": bool(),
307
- "--artifacts-dir": value()
308
- }),
309
- positionals: [{ label: "action", choices: ["diff"] }],
310
- minimumPositionals: 1
881
+ {
882
+ name: "check",
883
+ summary: "Run bounded repo-local checks for an active git diff and write validation artifacts.",
884
+ example: "mm-harness check diff --profile fast",
885
+ helpText: `mm-harness check diff [flags]
886
+
887
+ Run bounded repo-local checks for the active git diff. Fast profile runs changed-file
888
+ ESLint, Prettier, and changed test files. Full profile also runs repo typecheck
889
+ when available. Does not launch an app or run a recipe.
890
+
891
+ --target <path> Checkout path (default: cwd)
892
+ --adapter <{adapters}> Adapter label (auto-detected when possible)
893
+ --base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
894
+ --profile <fast|full> Validation depth (default: fast)
895
+ --fix Fix changed files, then validate them
896
+ --artifacts-dir <dir> Write validation-summary.json/.md and per-check logs
897
+ --json Machine-readable envelope
898
+
899
+ Example:
900
+ mm-harness check diff --fix --profile fast --artifacts-dir artifacts/validation
901
+ mm-harness check diff --profile full --json`,
902
+ contract: {
903
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
904
+ "--base": value(),
905
+ "--profile": value(["fast", "full"]),
906
+ "--fix": bool(),
907
+ "--artifacts-dir": value()
908
+ }),
909
+ positionals: [{ label: "action", choices: ["diff"] }],
910
+ minimumPositionals: 1
911
+ }
311
912
  },
312
- "recipe-quality": {
313
- options: options(HELP, JSON, ADAPTER, {
314
- "--input": value(),
315
- "--output": value(),
316
- "--recipe": value(),
317
- "--task": value(),
318
- "--evidence": value(),
319
- "--params": value(),
320
- "--library": value(),
321
- "--advisor": value(),
322
- "--advisor-model": value(),
323
- "--advisor-stages": value()
324
- }),
325
- positionals: [{ label: "action", choices: ["build", "plan", "advise", "advisor-status"] }]
913
+ {
914
+ name: "recipe-quality",
915
+ summary: "Build canonical recipe quality or request optional recipe advice.",
916
+ example: "mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json",
917
+ helpText: `mm-harness recipe-quality build|plan|advise|advisor-status [flags]
918
+
919
+ Build artifacts/recipe-quality.json from the compact fields a recipe-quality pass
920
+ produces (verdict + reasons + optional guidance/dimensions/findings/delta/training).
921
+ The artifact is validated against the RecipeQualityArtifact schema before it is
922
+ written \u2014 an invalid input never reaches disk. Shorthand without "build" is
923
+ also accepted when --input/--output are present.
924
+
925
+ Apply the quality bar from mm-harness help to the real graph and
926
+ evidence, then use this command to build the canonical artifact from that
927
+ compact verdict. An optional independent reviewer may still critique it.
928
+
929
+ --input <compact.json> Compact verdict JSON: { "verdict": "pass|warn|fail", "reasons": [..],
930
+ "betterVersionGuidance"?: [..], "dimensions"?: {..}, "trainingFields"?: {..}, \u2026 }
931
+ --output <path> Where to write the built artifact (parent dirs created)
932
+ --json Machine-readable envelope { status, outputPath, artifact }
933
+
934
+ Exit: 0 built \xB7 2 missing/unreadable args \xB7 5 invalid input (teaching escape names the invalid field).
935
+
936
+ Optional advice:
937
+ advisor-status --json
938
+ Resolve saved settings and overrides without reading tasks or calling a model.
939
+ plan --task <task.json> --adapter <{adapters}> --advisor typesafe
940
+ Assess each acceptance criterion before a recipe exists. Task JSON contains
941
+ acceptanceCriteria: ["criterion", "..."] (1\u201320 entries), with optional title/description.
942
+ Returns known candidate refs for reuse/composition, or an explicit discovery gap.
943
+ advise --recipe <recipe.json> --task <TASK.md> --adapter <{adapters}>
944
+ --advisor <provider|off> Override saved policy for this task (currently: typesafe)
945
+ --advisor-model <model> Override the provider default model or alias
946
+ --advisor-stages <csv> Select plan,draft,evidence (saved/default: plan)
947
+ --evidence <file.json> Explicit trace, summary, or evidence metadata to review
948
+ --params <json> Root recipe parameter values
949
+ --library <name=path> Resolve the same library sources as run (repeatable)
950
+ --output <advice.json> Optional separate advisory artifact
951
+
952
+ Precedence: explicit CLI > MM_HARNESS_ADVISOR[_MODEL|_STAGES] > advisor settings > disabled.
953
+ Set MM_HARNESS_ADVISOR to a provider or off for a Farmslot run. A key never enables advice.
954
+ Saved settings only run selected stages; an explicit --advisor call can request a stage directly.
955
+ --advisor off disables calls regardless of saved settings. No disabled advice artifact is written.
956
+ TypeSafe uses TYPESAFE_API_KEY. Missing keys skip advice; provider failures report unavailable.
957
+ Results record provider, requestedModel, and the model that answered.
958
+ Advice never changes recipe execution, canonical quality, or existing exit status.
959
+ Input must be statically valid. Context beyond the bound is skipped, never truncated.
960
+
961
+ Example:
962
+ mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json
963
+ mm-harness recipe-quality --input compact.json --output artifacts/recipe-quality.json
964
+ mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json --json
965
+ mm-harness recipe-quality advise --recipe artifacts/recipe.json --task TASK.md --adapter extension --advisor typesafe --output artifacts/recipe-advice.json --json
966
+ mm-harness recipe-quality plan --task task.json --adapter extension --advisor typesafe --output artifacts/recipe-plan.json --json`,
967
+ contract: {
968
+ options: options(HELP, JSON, ADAPTER, {
969
+ "--input": value(),
970
+ "--output": value(),
971
+ "--recipe": value(),
972
+ "--task": value(),
973
+ "--evidence": value(),
974
+ "--params": value(),
975
+ "--library": value(),
976
+ "--advisor": value(),
977
+ "--advisor-model": value(),
978
+ "--advisor-stages": value()
979
+ }),
980
+ positionals: [{ label: "action", choices: ["build", "plan", "advise", "advisor-status"] }]
981
+ }
326
982
  },
327
- provision: {
328
- options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
329
- "--platform": value(["ios"]),
330
- "--device-platform": value(["ios"])
331
- }),
332
- positionals: [
333
- { label: "source", choices: ["runway", "ios"] },
334
- { label: "platform", choices: ["ios"] }
335
- ]
983
+ {
984
+ name: "provision",
985
+ summary: "Install the cached Runway iOS dev client on a prepared mobile slot (no deps, no Metro).",
986
+ example: "mm-harness provision runway ios --adapter mobile",
987
+ helpText: `mm-harness provision [runway ios] [flags]
988
+
989
+ Install the cached Runway iOS dev client on the slot simulator. This is a thin
990
+ provisioning path only: artifact cache + simulator create + simctl install.
991
+ JavaScript dependencies and Metro remain dispatch-time launch concerns.
992
+
993
+ --adapter <{adapters}> Target adapter (mobile supported; the others teach)
994
+ --target <path> Slot checkout path (default: cwd)
995
+ --platform <ios> Platform (default ios)
996
+ --simulator <name|udid> Override agentic-runtime.json simulator (alias: --device)
997
+ --device <name|udid> Alias for --simulator
998
+ --slot <id> Farm slot id recorded in the provision baseline
999
+ --watcher-port <port> Farm Metro/watcher port carried through context and Next:
1000
+ --runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
1001
+ --runtime <id> iOS runtime id used if simulator must be created
1002
+ --device-type <id> Device type id used if simulator must be created
1003
+ --branch <ref> Probe this ref before default branch
1004
+ --default-branch <ref> Fallback ref (default main)
1005
+ --run <id> Exact GitHub Actions run id
1006
+ --cache-root <dir> Override shared runway cache root
1007
+ --force Reinstall even when the app is already present
1008
+ --resolve-only Resolve artifact metadata only; no simulator/cache/install
1009
+ --json Machine-readable envelope; progress stays stderr
1010
+
1011
+ Example:
1012
+ mm-harness provision runway ios --adapter mobile --target /path/to/slot
1013
+ mm-harness provision runway ios --adapter mobile --slot scratch-1 --runtime-dir temp/recipe/runtime-8081
1014
+ mm-harness provision runway ios --run 28676856835 --resolve-only --json`,
1015
+ contract: {
1016
+ options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
1017
+ "--platform": value(["ios"]),
1018
+ "--device-platform": value(["ios"])
1019
+ }),
1020
+ positionals: [
1021
+ { label: "source", choices: ["runway", "ios"] },
1022
+ { label: "platform", choices: ["ios"] }
1023
+ ]
1024
+ }
336
1025
  },
337
- install: {
338
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNWAY, {
339
- "--runway": bool()
340
- }),
341
- allowPassthrough: true
1026
+ {
1027
+ name: "install",
1028
+ summary: "Install the per-checkout runtime overlay, or --runway to install a cached mobile dev client.",
1029
+ example: "mm-harness install",
1030
+ helpText: `mm-harness install [flags]
1031
+
1032
+ Install the per-checkout runtime overlay (for CI / agents).
1033
+ Add --runway on a mobile slot to install the cached Runway iOS dev client only:
1034
+ artifact cache + simulator create + simctl install. JavaScript dependencies
1035
+ and Metro remain dispatch-time launch concerns.
1036
+
1037
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1038
+ --target <path> Checkout path (default: cwd)
1039
+ --runway Thin mobile Runway artifact install instead of overlay install
1040
+ --platform <ios> Runway platform (default ios)
1041
+ --simulator <name|udid> Override agentic-runtime.json simulator
1042
+ --runtime <id> iOS runtime id used if simulator must be created
1043
+ --device-type <id> Device type id used if simulator must be created
1044
+ --branch <ref> Probe this ref before default branch
1045
+ --default-branch <ref> Fallback ref (default main)
1046
+ --run <id> Exact GitHub Actions run id
1047
+ --cache-root <dir> Override shared runway cache root
1048
+ --force Reinstall even when the app is already present
1049
+ --resolve-only Runway metadata only; no simulator/cache/install
1050
+
1051
+ Example:
1052
+ mm-harness install
1053
+ mm-harness install --adapter extension --target /path/to/checkout
1054
+ mm-harness install --runway --adapter mobile --target /path/to/slot`,
1055
+ contract: {
1056
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNWAY, {
1057
+ "--runway": bool()
1058
+ }),
1059
+ allowPassthrough: true
1060
+ }
342
1061
  },
343
- verify: {
344
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, RUNTIME_PORTS),
345
- allowPassthrough: true
1062
+ {
1063
+ name: "verify",
1064
+ summary: "Check the overlay/runtime is present and healthy (no launch).",
1065
+ example: "mm-harness verify",
1066
+ helpText: `mm-harness verify [flags]
1067
+
1068
+ Check the runtime overlay is present and healthy (no app launch).
1069
+ Extension live checks infer the checkout CDP port from runtime context,
1070
+ pool, or slot suffix when --cdp-port is omitted.
1071
+
1072
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1073
+ --target <path> Checkout path (default: cwd)
1074
+ --json Machine-readable output
1075
+
1076
+ Example:
1077
+ mm-harness verify
1078
+ mm-harness verify --adapter core --target /path/to/checkout`,
1079
+ contract: {
1080
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, RUNTIME_PORTS),
1081
+ allowPassthrough: true
1082
+ }
346
1083
  },
347
- cleanup: {
348
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM),
349
- allowPassthrough: true
1084
+ {
1085
+ name: "cleanup",
1086
+ summary: "Remove the installed overlay and restore the checkout.",
1087
+ example: "mm-harness cleanup",
1088
+ helpText: `mm-harness cleanup [flags]
1089
+
1090
+ Remove the installed overlay and restore the checkout.
1091
+
1092
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1093
+ --target <path> Checkout path (default: cwd)
1094
+
1095
+ Example:
1096
+ mm-harness cleanup
1097
+ mm-harness cleanup --adapter mobile --target /path/to/checkout`,
1098
+ contract: {
1099
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM),
1100
+ allowPassthrough: true
1101
+ }
350
1102
  },
351
- launch: {
352
- options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
353
- "--build": bool(),
354
- "--clear-metro": bool(),
355
- "--verify": bool(),
356
- "--runway": bool(),
357
- "--watch": bool(),
358
- "--surface": value(["fullscreen", "sidepanel"]),
359
- "--sidepanel": bool(),
360
- "--fullscreen": bool(),
361
- "--url": value(),
362
- "--heal": value(["off", "infra-only", "auto"]),
363
- "--remote-flag": value(),
364
- "--signer": value(["extension", "injected"]),
365
- "--account": value(),
366
- "--fresh-profile": bool(),
367
- "--headless": bool(),
368
- "--headful": bool(),
369
- "--slow-mo": value(),
370
- "--network": value(["testnet", "mainnet"]),
371
- "--mainnet-confirmation": value()
372
- }),
373
- positionals: [{ label: "platform", choices: ["ios", "android"] }]
1103
+ {
1104
+ name: "launch",
1105
+ summary: "Launch the app and adapter dev server, auto-ensuring the runtime overlay first. Mobile: ios|android required.",
1106
+ example: "mm-harness launch ios",
1107
+ helpText: `mm-harness launch [ios|android] [flags]
1108
+
1109
+ Launch the app \u2014 quick relaunch by default; --build for a full native/webpack build.
1110
+ Mobile: target is MANDATORY (ios | android). Extension: no target (--fullscreen default;
1111
+ --sidepanel to override). core is headless (teaching error \u2014 use verify/run).
1112
+ Terminal: starts or reuses the slot browser (visible by default) against the slot dev server.
1113
+ Extension quick launch REUSES a healthy live runtime (reload in place, no rebuild);
1114
+ --build is the escape hatch that always clean-builds + relaunches a fresh runtime.
1115
+
1116
+ --build Full production-like build + relaunch (escape hatch; skips extension reuse)
1117
+ --clear-metro Mobile only: clear Metro's transform cache before launch
1118
+ --verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
1119
+ --runway Post-launch runway check (mobile only; teaching error elsewhere)
1120
+ --watch Persistent webpack watcher then relaunch (extension only)
1121
+ --surface <fullscreen|sidepanel> Extension display mode (default: fullscreen)
1122
+ --sidepanel | --fullscreen Legacy aliases for --surface
1123
+ --url <dapp-url> Open a dapp beside the sidepanel (default: MetaMask test dapp)
1124
+ --remote-flag <KEY=VALUE[,...]> Pin remote feature flags. VALUE is a variant name (treatment),
1125
+ true/false, or JSON. Mobile applies it through
1126
+ RemoteFeatureFlagController once the bridge is ready and fails the
1127
+ launch if it does not read back; Extension pins it into the per-run
1128
+ dist snapshot manifest (so it skips the reuse fast path).
1129
+ --heal <off|infra-only|auto> Healing policy (default auto); bounds always enforced
1130
+ --device <udid|name> Target simulator/device (env: IOS_SIMULATOR / ADB_SERIAL)
1131
+ --cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
1132
+ --watcher-port <port> Metro/webpack port (env: WATCHER_PORT / METRO_PORT)
1133
+ --signer <extension|injected> Terminal: MetaMask extension (default) or the injected test wallet
1134
+ --account <name> Terminal: wallet-fixture account (default dev1)
1135
+ --fresh-profile Terminal: start from a new browser profile
1136
+ --headless Terminal: hide the browser (env: TERMINAL_HEADLESS=1); default is a
1137
+ visible window placed per slot (--headful is accepted as the default)
1138
+ --slow-mo <ms> Terminal: pause before each UI action (env: TERMINAL_SLOW_MO; default 0)
1139
+ --network <testnet|mainnet> Terminal: venue the slot dev server serves (default testnet)
1140
+ --mainnet-confirmation <REAL_FUNDS> Terminal: required with --network mainnet (real funds)
1141
+ Terminal flags are rejected for other adapters. The terminal launch
1142
+ refuses unless the farm next dev server on --watcher-port forces
1143
+ Hyperliquid testnet, or --network mainnet --mainnet-confirmation REAL_FUNDS
1144
+ is passed (a recipe launch node carries network/mainnet_confirmation).
1145
+ --adapter <mobile|extension|terminal> Target adapter (auto-detected inside a checkout)
1146
+ --target <path> Checkout path (default: cwd)
1147
+ --json Machine-readable summary (recovered[] / mutations[] / phase)
1148
+ --json-stream Line-flushed JSONL progress + terminal event
1149
+
1150
+ Example:
1151
+ mm-harness launch ios
1152
+ mm-harness launch android --clear-metro --verify
1153
+ mm-harness launch ios --build
1154
+ mm-harness launch --verify --json
1155
+ mm-harness launch --remote-flag perpsTAT3938AbtestScreenVsBottomSheet=treatment`,
1156
+ contract: {
1157
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
1158
+ "--build": bool(),
1159
+ "--clear-metro": bool(),
1160
+ "--verify": bool(),
1161
+ "--runway": bool(),
1162
+ "--watch": bool(),
1163
+ "--surface": value(["fullscreen", "sidepanel"]),
1164
+ "--sidepanel": bool(),
1165
+ "--fullscreen": bool(),
1166
+ "--url": value(),
1167
+ "--heal": value(["off", "infra-only", "auto"]),
1168
+ "--remote-flag": value(),
1169
+ "--signer": value(["extension", "injected"]),
1170
+ "--account": value(),
1171
+ "--fresh-profile": bool(),
1172
+ "--headless": bool(),
1173
+ "--headful": bool(),
1174
+ "--slow-mo": value(),
1175
+ "--network": value(["testnet", "mainnet"]),
1176
+ "--mainnet-confirmation": value()
1177
+ }),
1178
+ positionals: [{ label: "platform", choices: ["ios", "android"] }]
1179
+ }
374
1180
  },
375
- "runtime-launch": {
376
- options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
377
- "--cdp-port": value(),
378
- "--chrome-user-data-dir": value(),
379
- "--reset-profile": bool(),
380
- "--preserve-profile": bool(),
381
- "--artifacts-dir": value(),
382
- "--start-watch": bool(),
383
- "--build-lavamoat": bool(),
384
- "--remote-flag": value(),
385
- "--artifact-file": value(),
386
- "--artifact-version": value(),
387
- "--artifact-build-id": value(),
388
- "--artifact-package-id": value(),
389
- "--artifact-sha256": value(),
390
- "--artifact-cache": value()
391
- })
1181
+ {
1182
+ name: "runtime-launch",
1183
+ summary: "Launch and verify an Extension runtime or local Android release artifact.",
1184
+ example: "mm-harness runtime-launch --adapter extension --target <checkout> --cdp-port 9222 --artifact-file <zip> --artifact-version <X.Y.Z> --artifact-sha256 <digest>",
1185
+ helpText: `mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
1186
+
1187
+ Verify and run an artifact already downloaded by the validation skill. The
1188
+ harness performs no network acquisition. Extension ZIPs run through the
1189
+ packaged browser/CDP path; Android APKs install to one explicitly named device.
1190
+
1191
+ --artifact-file <path> Artifact downloaded by the validation skill
1192
+ --artifact-version <X.Y.Z> Expected manifest or Android version name
1193
+ --artifact-build-id <id> Android version code
1194
+ --artifact-package-id <id> Android application ID
1195
+ --artifact-sha256 <digest> Trusted SHA-256 from the acquisition step
1196
+ --artifact-cache <dir> Extension artifact cache root
1197
+ --platform <android> Mobile platform (downloaded iOS is unsupported)
1198
+ --device <serial> Explicit Android device serial
1199
+ --cdp-port <port> Extension CDP port
1200
+ --chrome-user-data-dir <dir> Isolated Chrome profile path
1201
+ --reset-profile Explicitly clear that profile; default preserves wallet state
1202
+ --artifacts-dir <dir> Validation evidence directory
1203
+ --start-watch Build through the checkout watcher
1204
+ --build-lavamoat Build through the production-like path
1205
+ --remote-flag <KEY=VARIANT> Pin checkout snapshot flags (not allowed for artifacts)
1206
+ --json Machine-readable terminal report
1207
+
1208
+ Example:
1209
+ mm-harness runtime-launch --adapter extension --target /path/to/checkout --cdp-port 9222 --artifact-file /path/to/metamask-chrome-X.Y.Z.zip --artifact-version X.Y.Z --artifact-sha256 <digest>
1210
+ mm-harness runtime-launch --adapter mobile --platform android --target /path/to/checkout --device <serial> --artifact-file /path/to/app.apk --artifact-version X.Y.Z --artifact-build-id <code> --artifact-package-id io.metamask --artifact-sha256 <digest>`,
1211
+ contract: {
1212
+ options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
1213
+ "--cdp-port": value(),
1214
+ "--chrome-user-data-dir": value(),
1215
+ "--reset-profile": bool(),
1216
+ "--preserve-profile": bool(),
1217
+ "--artifacts-dir": value(),
1218
+ "--start-watch": bool(),
1219
+ "--build-lavamoat": bool(),
1220
+ "--remote-flag": value(),
1221
+ "--artifact-file": value(),
1222
+ "--artifact-version": value(),
1223
+ "--artifact-build-id": value(),
1224
+ "--artifact-package-id": value(),
1225
+ "--artifact-sha256": value(),
1226
+ "--artifact-cache": value()
1227
+ })
1228
+ }
392
1229
  },
393
- logs: {
394
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
395
- "--full": bool(),
396
- "--window": bool(),
397
- "--events": value(),
398
- "--source": value()
399
- })
1230
+ {
1231
+ name: "logs",
1232
+ summary: "Tail Metro/webpack + app logs for the active checkout.",
1233
+ example: "mm-harness logs",
1234
+ helpText: `mm-harness logs [flags]
1235
+
1236
+ Tail Metro/webpack + app logs for the active checkout.
1237
+ Teaching error if nothing is running (points at launch).
1238
+
1239
+ --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
1240
+ --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
1241
+ --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
1242
+ --source <label> Log source per adapter \u2014 mobile: metro|app;
1243
+ extension: extension|dapp|webpack|watcher|rebuild.
1244
+ Core is headless (teaching error).
1245
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1246
+ --target <path> Checkout path (default: cwd)
1247
+ --json Machine-readable output
1248
+
1249
+ Example:
1250
+ mm-harness logs
1251
+ mm-harness logs --full
1252
+ mm-harness logs --window`,
1253
+ contract: {
1254
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
1255
+ "--full": bool(),
1256
+ "--window": bool(),
1257
+ "--events": value(),
1258
+ "--source": value()
1259
+ })
1260
+ }
400
1261
  },
401
- debug: {
402
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
403
- "--worker": bool(),
404
- "--dev-menu": bool(),
405
- "--no-open": bool()
406
- })
1262
+ {
1263
+ name: "debug",
1264
+ summary: "Open the debug console \u2014 extension: Chrome DevTools via CDP; mobile: RN DevTools.",
1265
+ example: "mm-harness debug",
1266
+ helpText: `mm-harness debug [flags]
1267
+
1268
+ Open the debug console \u2014 extension: Chrome DevTools via CDP; mobile: RN DevTools.
1269
+
1270
+ --worker Extension service-worker DevTools
1271
+ --dev-menu Mobile RN developer menu
1272
+ --no-open Resolve target/method/endpoint without opening a UI
1273
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
1274
+ --target <path> Checkout path (default: cwd)
1275
+ --json Machine-readable output
1276
+
1277
+ Example:
1278
+ mm-harness debug
1279
+ mm-harness debug --worker`,
1280
+ contract: {
1281
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
1282
+ "--worker": bool(),
1283
+ "--dev-menu": bool(),
1284
+ "--no-open": bool()
1285
+ })
1286
+ }
407
1287
  },
408
- reload: {
409
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM)
1288
+ {
1289
+ name: "reload",
1290
+ summary: "Reload the connected Mobile or Extension runtime without restarting its dev server.",
1291
+ example: "mm-harness reload",
1292
+ helpText: `mm-harness reload [flags]
1293
+
1294
+ Mobile broadcasts the same reload command as Metro's interactive r key.
1295
+ Extension refreshes its loaded UI in place over CDP. Core is headless.
1296
+
1297
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
1298
+ --target <path> Checkout path (default: cwd)
1299
+ --json Machine-readable output
1300
+
1301
+ Example:
1302
+ mm-harness reload`,
1303
+ contract: {
1304
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM)
1305
+ }
410
1306
  },
411
- update: {
412
- options: options(HELP, JSON, { "--check": bool() })
1307
+ {
1308
+ name: "update",
1309
+ summary: "Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).",
1310
+ example: "mm-harness update",
1311
+ helpText: `mm-harness update [flags]
1312
+
1313
+ Update the globally-installed mm-harness to the npm registry's latest.
1314
+ Bare form upgrades (npm i -g @deeeed/metamask-harness@latest) and prints old \u2192 new;
1315
+ already-current exits 0.
1316
+
1317
+ --check Report only \u2014 exit 0 up-to-date / exit 1 update available (no install)
1318
+ --json Machine-readable { current, latest, updateAvailable }
1319
+
1320
+ A passive once-a-day nudge also prints one stderr line when a newer version exists.
1321
+ Silence it with MM_HARNESS_NO_UPDATE_CHECK=1 (auto-off in CI).
1322
+
1323
+ Example:
1324
+ mm-harness update
1325
+ mm-harness update --check --json`,
1326
+ contract: {
1327
+ options: options(HELP, JSON, { "--check": bool() })
1328
+ }
413
1329
  },
414
- pin: {
415
- options: options(HELP, JSON)
1330
+ {
1331
+ name: "pin",
1332
+ summary: "Copy this installed mm-harness into an immutable, versioned store and print the copy for a task to run, so a later global upgrade cannot change a running task.",
1333
+ example: "mm-harness pin",
1334
+ helpText: `mm-harness pin [flags]
1335
+
1336
+ Copies the installed mm-harness running this command into an immutable,
1337
+ versioned store (~/.local/share/mm-harness/pins/<version>-<hash>/; APFS clone
1338
+ where available) and prints the copy's executable. A task records that path,
1339
+ so \`npm i -g\` upgrading the global install never changes the harness a running
1340
+ task runs. Idempotent: the same install returns the same pin. A source checkout
1341
+ (MM_HARNESS_BIN) or an existing pin is printed as is.
1342
+
1343
+ --json Machine-readable { version, source, executable, pinned, created }
1344
+
1345
+ MM_HARNESS_PIN_ROOT overrides the store location.
1346
+
1347
+ Example:
1348
+ mm-harness pin
1349
+ mm-harness pin --json`,
1350
+ contract: {
1351
+ options: options(HELP, JSON)
1352
+ }
416
1353
  },
417
- fixtures: {
418
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
419
- "--from": value(),
420
- "--dev": bool(),
421
- "--force": bool(),
422
- "--fixture": value(),
423
- "--out": value(),
424
- "--state": value(),
425
- "--cdp-port": value(),
426
- "--extension-dir": value(),
427
- "--extension-id-file": value(),
428
- "--action-manifest": value(),
429
- "--remote-flag": value()
430
- }),
431
- positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
432
- minimumPositionals: 0
433
- }
434
- };
435
- function optionValues(args, name) {
436
- const values = [];
437
- for (let index = 0; index < args.length; index += 1) {
438
- const arg = args[index];
439
- if (arg === "--") break;
440
- if (arg === name && args[index + 1] !== void 0) values.push(args[index += 1]);
441
- else if (arg.startsWith(`${name}=`)) values.push(arg.slice(name.length + 1));
1354
+ {
1355
+ name: "fixtures",
1356
+ summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / reset / generate / finalize.",
1357
+ example: "mm-harness fixtures set",
1358
+ helpText: `mm-harness fixtures [<init|sync|set|reset|generate|finalize>] [flags]
1359
+
1360
+ Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
1361
+ With no action, show the current fixture status and safe next command.
1362
+ init Create it from --from <path>, or explicitly choose --dev for a
1363
+ disposable public test wallet that must never hold real funds.
1364
+ sync Refresh the wallet fixture files on the target.
1365
+ set Apply the canonical fixture (SRP/password/accounts); the password is
1366
+ read FROM the fixture, never typed.
1367
+ reset Delete the current Mobile/Extension wallet, then apply and validate
1368
+ the canonical fixture from a clean state.
1369
+ generate Render the extension fixture-state.json from a wallet fixture, for
1370
+ pre-launch profile prefill (extension-only). Requires --fixture and --out.
1371
+ finalize Seed account labels/selection into the LIVE extension over CDP and
1372
+ validate parity (extension-only, post-launch). Requires --fixture,
1373
+ --state, --cdp-port, --extension-dir.
1374
+ Want different accounts? Edit the fixture file directly:
1375
+ <checkout>/temp/recipe/runtime/wallet-fixture.json
1376
+
1377
+ --from <path> Existing wallet fixture source (init)
1378
+ --dev Create a disposable public test wallet (init)
1379
+ --force Replace an existing canonical fixture (init)
1380
+ --fixture <path> Wallet fixture path (generate/finalize input; agent override for set/sync \u2014 env: RECIPE_WALLET_FIXTURE)
1381
+ --out <path> generate: output fixture-state.json; finalize: optional validation report
1382
+ --state <path> finalize: the pre-launch fixture-state.json to seed
1383
+ --cdp-port <port> finalize: CDP port of the running extension
1384
+ --extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
1385
+ --extension-id-file <path> finalize: optional file to read/write the resolved extension id
1386
+ --action-manifest <path> Extension set: override the wallet action manifest
1387
+ --remote-flag <KEY=VALUE[,...]> set/reset: one-run override layered over the fixture's remoteFeatureFlags
1388
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
1389
+ --target <path> Checkout path (default: cwd)
1390
+ --device <udid|serial|name> Mobile only: target this device for sync/set
1391
+ --json Machine-readable output
1392
+
1393
+ Example:
1394
+ mm-harness fixtures init --from /secure/path/wallet-fixture.json
1395
+ mm-harness fixtures init --dev
1396
+ mm-harness fixtures sync
1397
+ mm-harness fixtures set
1398
+ mm-harness fixtures reset
1399
+ mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
1400
+ mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`,
1401
+ contract: {
1402
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
1403
+ "--from": value(),
1404
+ "--dev": bool(),
1405
+ "--force": bool(),
1406
+ "--fixture": value(),
1407
+ "--out": value(),
1408
+ "--state": value(),
1409
+ "--cdp-port": value(),
1410
+ "--extension-dir": value(),
1411
+ "--extension-id-file": value(),
1412
+ "--action-manifest": value(),
1413
+ "--remote-flag": value()
1414
+ }),
1415
+ positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
1416
+ minimumPositionals: 0
1417
+ }
442
1418
  }
443
- return values;
444
- }
1419
+ ];
1420
+ const HIDDEN_COMMANDS = [
1421
+ "completion-candidates",
1422
+ "runtime-health",
1423
+ "runtime-decision",
1424
+ "resolve-extension",
1425
+ "ensure-ready",
1426
+ "self-test",
1427
+ "completions"
1428
+ ];
445
1429
  const LIBRARY_ADAPTERS = {
446
1430
  terminal: { title: "MetaMask Web Terminal", repository: "MetaMask/experimental-metamask-recipe-terminal" }
447
1431
  };
448
1432
  const quoteArg = (value2) => /^[A-Za-z0-9_./:=@%+-]+$/u.test(value2) ? value2 : `'${value2.replaceAll("'", `'\\''`)}'`;
449
- function missingAdapterLibrary(command, optionName, id, tokens) {
1433
+ function explainMissingAdapterLibrary({
1434
+ command,
1435
+ option: optionName,
1436
+ value: id,
1437
+ tokens
1438
+ }) {
450
1439
  const library = LIBRARY_ADAPTERS[id];
451
- if (!library || optionName !== "--adapter" && optionName !== "--platform") return void 0;
1440
+ if (!library || optionName !== "--adapter" && optionName !== "--platform") return null;
452
1441
  const current = operatorLibraryEnv().RECIPE_LIBRARY_PATH;
453
1442
  const rerun = (libraryPath) => `RECIPE_LIBRARY_PATH=${quoteArg(libraryPath)} mm-harness ${[command, ...tokens].map(quoteArg).join(" ")}`;
454
1443
  const revision = `a revision whose recipe-library.json declares adapters.${id}`;
@@ -472,292 +1461,11 @@ function missingAdapterLibrary(command, optionName, id, tokens) {
472
1461
  userAction: `Keep ${optionName} ${id} and put that library, at ${revision}, on RECIPE_LIBRARY_PATH (git clone https://github.com/${library.repository}.git ${checkout}): ${rerun(current ? `${id}=${checkout}:${current}` : `${id}=${checkout}`)}`
473
1462
  };
474
1463
  }
475
- function validatePublicInvocation(argv, examples = {}) {
476
- const token = argv[0];
477
- if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
478
- const resolved = resolveCommand(token);
479
- if (!resolved) {
480
- const names = publicCommandTokens();
481
- const suggestion = closest(token, names);
482
- if (token.startsWith("-")) {
483
- return {
484
- code: "CLI_UNKNOWN_OPTION",
485
- command: "mm-harness",
486
- message: `unknown top-level option '${token}'. Valid top-level options: --help, --version.`,
487
- userAction: actionFor(token, closest(token, ["--help", "--version"]), "mm-harness --help")
488
- };
489
- }
490
- return {
491
- code: "CLI_UNKNOWN_COMMAND",
492
- command: token,
493
- message: `unknown command '${token}'. Valid commands: ${Object.keys(PUBLIC_COMMAND_CONTRACTS).join(", ")}.`,
494
- userAction: actionFor(token, suggestion, suggestion ? exampleFor(resolveCommand(suggestion)?.name, examples) : "mm-harness --help")
495
- };
496
- }
497
- const { name, contract } = resolved;
498
- const tokens = argv.slice(1);
499
- if (name === "checklist" && tokens[0] === "mark" && tokens[1] && !tokens[1].startsWith("-") && tokens[2] === "sub") return null;
500
- const positionals = [];
501
- const seenOptions = /* @__PURE__ */ new Set();
502
- const validOptions = Object.keys(contract.options);
503
- for (let index = 0; index < tokens.length; index += 1) {
504
- const argument = tokens[index] ?? "";
505
- if (argument === "--") {
506
- if (!contract.allowPassthrough) {
507
- return usageFailure("CLI_UNEXPECTED_PASSTHROUGH", name, "this command does not accept `--` passthrough.", contract, examples);
508
- }
509
- break;
510
- }
511
- if (!argument.startsWith("-") || argument === "-") {
512
- positionals.push(argument);
513
- continue;
514
- }
515
- const equals = argument.indexOf("=");
516
- const optionName = equals === -1 ? argument : argument.slice(0, equals);
517
- const inlineValue = equals === -1 ? void 0 : argument.slice(equals + 1);
518
- const spec = contract.options[optionName];
519
- if (!spec) {
520
- const suggestion = REMOVED_OPTION_REPLACEMENTS[optionName] ?? closest(optionName, validOptions);
521
- return {
522
- code: "CLI_UNKNOWN_OPTION",
523
- command: name,
524
- message: `unknown option '${optionName}'.`,
525
- userAction: actionFor(optionName, suggestion, exampleFor(name, examples))
526
- };
527
- }
528
- seenOptions.add(optionName);
529
- if (spec.kind === "boolean") {
530
- if (inlineValue !== void 0) {
531
- return usageFailure("CLI_INVALID_OPTION_VALUE", name, `${optionName} is a flag and does not take a value.`, contract, examples);
532
- }
533
- continue;
534
- }
535
- if (spec.kind === "optional-value" && inlineValue === void 0) continue;
536
- const optionValue = inlineValue ?? tokens[index + 1];
537
- const nextTokenIsOption = inlineValue === void 0 && (optionValue === "-h" || optionValue === "--" || optionValue?.startsWith("--"));
538
- if (optionValue === void 0 || spec.kind === "value" && optionValue === "" || nextTokenIsOption) {
539
- return usageFailure("CLI_MISSING_OPTION_VALUE", name, `${optionName} requires a value.`, contract, examples);
540
- }
541
- if (inlineValue === void 0) index += 1;
542
- const choices = typeof spec.choices === "function" ? spec.choices(tokens) : spec.choices;
543
- if (choices && !choices.includes(optionValue)) {
544
- const missing = missingAdapterLibrary(name, optionName, optionValue, tokens);
545
- if (missing) return missing;
546
- const suggestion = closest(optionValue, choices);
547
- return {
548
- code: "CLI_INVALID_OPTION_VALUE",
549
- command: name,
550
- message: `${optionName} must be ${formatChoiceList(choices)}; received '${optionValue}'.`,
551
- userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
552
- };
553
- }
554
- }
555
- if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
556
- if (name === "checklist") {
557
- const action = positionals[0];
558
- const markOnly = [
559
- "--mark-last",
560
- "--already-fixed",
561
- "--no-self-review",
562
- "--skip-learnings",
563
- "--skip-checklist",
564
- "--reason",
565
- "--checklist",
566
- "--signal"
567
- ];
568
- const closeoutOnly = [
569
- "--json",
570
- "--share",
571
- "--config",
572
- "--destination",
573
- "--metadata"
574
- ];
575
- const invalidOption = (action === "mark" ? closeoutOnly : markOnly).find(
576
- (option) => seenOptions.has(option)
577
- );
578
- if (invalidOption && (action === "mark" || action === "closeout")) {
579
- return usageFailure(
580
- "CLI_UNKNOWN_OPTION",
581
- name,
582
- `${invalidOption} is not valid with checklist ${action}.`,
583
- contract,
584
- examples
585
- );
586
- }
587
- if (action === "mark" && positionals.length < 3) {
588
- return missingPositionalFailure(
589
- name,
590
- "checklist mark requires <step>.",
591
- contract,
592
- examples
593
- );
594
- }
595
- if (action === "closeout" && positionals.length > 2) {
596
- return usageFailure(
597
- "CLI_EXCESS_POSITIONAL",
598
- name,
599
- `unexpected positional '${positionals[2]}'; checklist closeout accepts only <task-dir>.`,
600
- contract,
601
- examples
602
- );
603
- }
604
- }
605
- if (name === "execution-template") {
606
- const action = positionals[0];
607
- const validAction = ["list", "materialize", "lint", "new"].includes(action ?? "");
608
- const targetCount = Math.max(0, positionals.length - 1);
609
- if (validAction && action === "list" && targetCount > 0) {
610
- return usageFailure(
611
- "CLI_EXCESS_POSITIONAL",
612
- name,
613
- `execution-template list does not accept positional '${positionals[1]}'.`,
614
- contract,
615
- examples
616
- );
617
- }
618
- if (validAction && (action === "materialize" || action === "lint" || action === "new") && targetCount === 0) {
619
- return missingPositionalFailure(
620
- name,
621
- `execution-template ${action} requires <target>.`,
622
- contract,
623
- examples
624
- );
625
- }
626
- if (validAction && targetCount > 1) {
627
- return usageFailure(
628
- "CLI_EXCESS_POSITIONAL",
629
- name,
630
- `unexpected positional '${positionals[2]}'; execution-template ${action} accepts one <target>.`,
631
- contract,
632
- examples
633
- );
634
- }
635
- }
636
- if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
637
- const missing = contract.positionals?.[0]?.label ?? "argument";
638
- return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
639
- }
640
- const forbiddingOption = contract.noPositionalsWith?.find((option) => seenOptions.has(option));
641
- if (forbiddingOption && positionals.length > 0) {
642
- return usageFailure(
643
- "CLI_EXCESS_POSITIONAL",
644
- name,
645
- `${forbiddingOption} does not accept positional '${positionals[0]}'.`,
646
- contract,
647
- examples
648
- );
649
- }
650
- const minimum = contract.requiredUnless?.some((option) => seenOptions.has(option)) ? 0 : contract.minimumPositionals ?? 0;
651
- if (positionals.length < minimum) {
652
- const missingSpec = contract.positionals?.[positionals.length];
653
- const missing = missingSpec?.label ?? "argument";
654
- const valid = missingSpec?.choices?.length ? ` Choose one: ${missingSpec.choices.join("|")}.` : "";
655
- const usage = contract.usage ? ` Usage: ${contract.usage}.` : "";
656
- return missingPositionalFailure(name, `missing required <${missing}>.${valid}${usage}`, contract, examples);
657
- }
658
- const fixed = contract.positionals ?? [];
659
- for (let index = 0; index < Math.min(positionals.length, fixed.length); index += 1) {
660
- const invalid = invalidPositional(positionals[index] ?? "", fixed[index]);
661
- if (invalid) return positionalFailure(name, positionals[index] ?? "", fixed[index], contract, examples);
662
- }
663
- if (positionals.length > fixed.length && !contract.variadic) {
664
- const unexpected = positionals[fixed.length] ?? "";
665
- return usageFailure("CLI_EXCESS_POSITIONAL", name, `unexpected positional '${unexpected}'; this command accepts ${formatPositionals(contract)}.`, contract, examples);
666
- }
667
- if (contract.variadic) {
668
- for (const positional of positionals.slice(fixed.length)) {
669
- if (invalidPositional(positional, contract.variadic)) {
670
- return positionalFailure(name, positional, contract.variadic, contract, examples);
671
- }
672
- }
673
- }
674
- return null;
675
- }
676
- function publicCommandTokens() {
677
- const tokens = [];
678
- for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
679
- tokens.push(name, ...contract.aliases ?? []);
680
- }
681
- return tokens;
682
- }
683
- function publicCommandNames() {
684
- return Object.keys(PUBLIC_COMMAND_CONTRACTS);
685
- }
686
- function resolveCommand(token) {
687
- const direct = PUBLIC_COMMAND_CONTRACTS[token];
688
- if (direct) return { name: token, contract: direct };
689
- for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
690
- if (contract.aliases?.includes(token)) return { name, contract };
691
- }
692
- return null;
693
- }
694
- function usageFailure(code, command, message, _contract, examples) {
695
- return {
696
- code,
697
- command,
698
- message,
699
- userAction: `Try: ${exampleFor(command, examples)}. Inspect options: mm-harness ${command} --help`
700
- };
701
- }
702
- function missingPositionalFailure(command, message, contract, examples) {
703
- if (command === "run") {
704
- return {
705
- code: "CLI_MISSING_POSITIONAL",
706
- command,
707
- message,
708
- userAction: `List recipes: mm-harness run --list
709
- Example: ${exampleFor(command, examples)}
710
- More: mm-harness run --help`
711
- };
712
- }
713
- if (command === "call") {
714
- return {
715
- code: "CLI_MISSING_POSITIONAL",
716
- command,
717
- message,
718
- userAction: `List actions: mm-harness call --list
719
- Example: ${exampleFor(command, examples)}
720
- More: mm-harness call --help`
721
- };
722
- }
723
- return usageFailure("CLI_MISSING_POSITIONAL", command, message, contract, examples);
724
- }
725
- function positionalFailure(command, received, spec, _contract, examples) {
726
- const valid = spec.validDescription ?? (spec.choices ? spec.choices.join(", ") : spec.pattern || spec.validate ? `a valid <${spec.label}>` : `<${spec.label}>`);
727
- const suggestion = spec.choices ? closest(received, spec.choices) : void 0;
728
- return {
729
- code: "CLI_INVALID_POSITIONAL",
730
- command,
731
- message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}.`,
732
- userAction: actionFor(received, suggestion, exampleFor(command, examples))
733
- };
734
- }
735
- function invalidPositional(value2, spec) {
736
- if (!spec) return true;
737
- if (spec.choices && !spec.choices.includes(value2)) return true;
738
- if (spec.validate && !spec.validate(value2)) return true;
739
- return Boolean(spec.pattern && !spec.pattern.test(value2));
740
- }
741
- function formatChoiceList(choices) {
742
- if (choices.length === 1) return choices[0] ?? "";
743
- if (choices.length === 2) return `${choices[0]} or ${choices[1]}`;
744
- return `${choices.slice(0, -1).join(", ")}, or ${choices[choices.length - 1]}`;
745
- }
746
- function formatPositionals(contract) {
747
- const fixed = (contract.positionals ?? []).map((spec) => `<${spec.label}>`);
748
- if (contract.variadic) fixed.push(`[<${contract.variadic.label}> ...]`);
749
- return fixed.length > 0 ? fixed.join(" ") : "no positionals";
750
- }
751
- function actionFor(received, suggestion, example) {
752
- return `${suggestion ? `Did you mean '${suggestion}' instead of '${received}'? ` : ""}Try: ${example}`;
753
- }
754
- function exampleFor(command, examples) {
755
- return command ? examples[command] ?? `mm-harness ${command} --help` : "mm-harness --help";
756
- }
757
1464
  export {
758
- PUBLIC_COMMAND_CONTRACTS,
759
- optionValues,
760
- publicCommandNames,
761
- publicCommandTokens,
762
- validatePublicInvocation
1465
+ HIDDEN_COMMANDS,
1466
+ PUBLIC_COMMANDS,
1467
+ REPLACED_OPTIONS,
1468
+ explainMissingAdapterLibrary,
1469
+ selectableAdapters,
1470
+ withAdapterList
763
1471
  };