@deeeed/metamask-harness 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.22.0 - 2026-07-26
6
+
7
+ ### Added
8
+
9
+ - Added `execution-template` as the thin public boundary for shared checklist discovery and materialization.
10
+
11
+ ### Fixed
12
+
13
+ - `ui.navigate page=home` recognizes both the current bottom navigation and the selected legacy Tokens tab.
14
+ - Extension readiness recovers deterministically from transient target-list failures.
15
+ - Team-library calls retain their source provenance.
16
+
5
17
  ## 0.21.0 - 2026-07-24
6
18
 
7
19
  ### Changed
@@ -36,9 +36,11 @@ async function openHome(port, extensionId) {
36
36
  try {
37
37
  let res = await fetch(endpoint, { method: "PUT", signal: AbortSignal.timeout(8e3) });
38
38
  if (res.status === 404 || res.status === 405) res = await fetch(endpoint, { signal: AbortSignal.timeout(8e3) });
39
- return res.ok;
39
+ if (!res.ok) return null;
40
+ const value = await res.json();
41
+ return value && typeof value === "object" ? value : {};
40
42
  } catch {
41
- return false;
43
+ return null;
42
44
  }
43
45
  }
44
46
  function homePages(targets, extensionId) {
@@ -94,7 +96,7 @@ async function ensureExtensionReady(target, options) {
94
96
  continue;
95
97
  }
96
98
  action = "opened";
97
- opened = await openHome(cdpPort, extensionId) || opened;
99
+ opened = Boolean(await openHome(cdpPort, extensionId)) || opened;
98
100
  await sleep(1500);
99
101
  } else {
100
102
  action = "pruned";
@@ -149,20 +151,47 @@ async function ensureExtensionReady(target, options) {
149
151
  if (action === "pruned" && (health.status !== "PASS" || after !== 1)) {
150
152
  const listing = await jsonList(cdpPort);
151
153
  if (listing.ok) {
152
- action = "reopened";
153
- for (const h of homePages(listing.targets, extensionId)) {
154
- await closeTab(cdpPort, String(h.id));
155
- closed += 1;
156
- }
157
- await sleep(500);
158
- opened = await openHome(cdpPort, extensionId) || opened;
154
+ const existingHomeIds = new Set(
155
+ homePages(listing.targets, extensionId).map((home) => String(home.id))
156
+ );
157
+ const replacement = await openHome(cdpPort, extensionId);
158
+ opened = Boolean(replacement) || opened;
159
159
  await sleep(1500);
160
- const relisted = await jsonList(cdpPort);
161
- if (!relisted.ok) {
162
- return base({ extensionId, opened, action, homeTabs: { before, closed, after }, reasonCode: "cdp-unreachable" });
160
+ const replacementListing = await jsonList(cdpPort);
161
+ if (!replacementListing.ok) {
162
+ return base({
163
+ extensionId,
164
+ opened,
165
+ action,
166
+ homeTabs: { before, closed, after },
167
+ reasonCode: "cdp-unreachable"
168
+ });
169
+ }
170
+ const replacementHomes = homePages(replacementListing.targets, extensionId);
171
+ const replacementId = replacement?.id && replacementHomes.some((home) => home.id === replacement.id) ? String(replacement.id) : replacementHomes.find((home) => !existingHomeIds.has(String(home.id)))?.id;
172
+ if (replacementId) {
173
+ action = "reopened";
174
+ for (const home of replacementHomes) {
175
+ if (String(home.id) === replacementId) continue;
176
+ await closeTab(cdpPort, String(home.id));
177
+ closed += 1;
178
+ }
179
+ await sleep(500);
180
+ const relisted = await jsonList(cdpPort);
181
+ if (!relisted.ok) {
182
+ return base({
183
+ extensionId,
184
+ opened,
185
+ action,
186
+ homeTabs: { before, closed, after },
187
+ reasonCode: "cdp-unreachable"
188
+ });
189
+ }
190
+ after = homePages(relisted.targets, extensionId).length;
191
+ health = await checkHealth();
192
+ } else {
193
+ after = replacementHomes.length;
163
194
  }
164
- after = homePages(relisted.targets, extensionId).length;
165
- health = await checkHealth();
166
195
  }
167
196
  }
168
197
  let slotTitle;
@@ -18,6 +18,7 @@ const SPEC = {
18
18
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--categories", "--category", "--action", "--library"] },
19
19
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
20
20
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
21
+ { name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
21
22
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
22
23
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
23
24
  { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { handleRecipeQuality } from "./commands/recipe-quality.js";
20
20
  import { handleStatus } from "./commands/status.js";
21
21
  import { handleCheck } from "./commands/check.js";
22
22
  import { handleChecklist } from "./commands/checklist.js";
23
+ import { handleExecutionTemplate } from "./commands/execution-template.js";
23
24
  import { handleLast } from "./commands/last.js";
24
25
  import { parseArgs, targetPath } from "./commands/parse-args.js";
25
26
  const COMMANDS = {
@@ -121,6 +122,7 @@ async function main(argv) {
121
122
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
122
123
  if (command === "check") return handleCheck(argv.slice(1));
123
124
  if (command === "checklist") return handleChecklist(argv.slice(1));
125
+ if (command === "execution-template") return handleExecutionTemplate(argv.slice(1));
124
126
  if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
125
127
  const handler = COMMANDS[command];
126
128
  if (!handler) throw new Error(`Unknown command: ${command}`);
@@ -88,6 +88,32 @@ const PUBLIC_COMMAND_CONTRACTS = {
88
88
  ],
89
89
  minimumPositionals: 2
90
90
  },
91
+ "execution-template": {
92
+ usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
93
+ options: options(HELP, JSON, {
94
+ "--dir": value(),
95
+ "--domain-dir": value(),
96
+ "--project-worker": value(),
97
+ "--project-name": value(),
98
+ "--package-templates": value(),
99
+ "--package-id": value(),
100
+ "--flow": value(),
101
+ "--run-mode": value(["autonomous", "interactive", "validation"]),
102
+ "--platform": value(),
103
+ "--domain": value(),
104
+ "--id": value(),
105
+ "--provenance": value(),
106
+ "--include-shadowed": bool(),
107
+ "--no-include-shadowed": bool(),
108
+ "--title": value(),
109
+ "--force": bool()
110
+ }),
111
+ positionals: [
112
+ { label: "action", choices: ["list", "materialize", "lint", "new"] }
113
+ ],
114
+ minimumPositionals: 1,
115
+ variadic: { label: "target", validate: (value2) => value2.length > 0 }
116
+ },
91
117
  actions: {
92
118
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
93
119
  "--action": value(),
@@ -360,6 +386,37 @@ function validatePublicInvocation(argv, examples = {}) {
360
386
  );
361
387
  }
362
388
  }
389
+ if (name === "execution-template") {
390
+ const action = positionals[0];
391
+ const validAction = ["list", "materialize", "lint", "new"].includes(action ?? "");
392
+ const targetCount = Math.max(0, positionals.length - 1);
393
+ if (validAction && action === "list" && targetCount > 0) {
394
+ return usageFailure(
395
+ "CLI_EXCESS_POSITIONAL",
396
+ name,
397
+ `execution-template list does not accept positional '${positionals[1]}'.`,
398
+ contract,
399
+ examples
400
+ );
401
+ }
402
+ if (validAction && (action === "materialize" || action === "lint" || action === "new") && targetCount === 0) {
403
+ return missingPositionalFailure(
404
+ name,
405
+ `execution-template ${action} requires <target>.`,
406
+ contract,
407
+ examples
408
+ );
409
+ }
410
+ if (validAction && targetCount > 1) {
411
+ return usageFailure(
412
+ "CLI_EXCESS_POSITIONAL",
413
+ name,
414
+ `unexpected positional '${positionals[2]}'; execution-template ${action} accepts one <target>.`,
415
+ contract,
416
+ examples
417
+ );
418
+ }
419
+ }
363
420
  if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
364
421
  const missing = contract.positionals?.[0]?.label ?? "argument";
365
422
  return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
@@ -0,0 +1,28 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ const require2 = createRequire(import.meta.url);
4
+ async function handleExecutionTemplate(argv) {
5
+ let entrypoint;
6
+ try {
7
+ entrypoint = require2.resolve(
8
+ "@farmslot/agent-runtime/scripts/execution-template-cli.mjs"
9
+ );
10
+ } catch (error) {
11
+ console.error(
12
+ `mm-harness execution-template: installed @farmslot/agent-runtime does not expose the catalog command: ${error instanceof Error ? error.message : String(error)}`
13
+ );
14
+ return 1;
15
+ }
16
+ const result = spawnSync(process.execPath, [entrypoint, ...argv], {
17
+ env: process.env,
18
+ stdio: "inherit"
19
+ });
20
+ if (result.error) {
21
+ console.error(`mm-harness execution-template: ${result.error.message}`);
22
+ return 1;
23
+ }
24
+ return result.status ?? 1;
25
+ }
26
+ export {
27
+ handleExecutionTemplate
28
+ };
@@ -21,6 +21,7 @@ import {
21
21
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
23
  import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
24
+ import { gitLibraryProvenance } from "../library-provenance.js";
24
25
  import {
25
26
  beginRunDiagnostics,
26
27
  finishRunDiagnostics
@@ -655,14 +656,22 @@ async function resolveMetaMaskLibrarySources(libraryEntry, recipePath) {
655
656
  if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
656
657
  sources.push({ name: "metamask", root: canonicalRoot });
657
658
  }
658
- return sources.map((source) => ({
659
- ...source,
660
- provenance: source.provenance ?? {
661
- kind: source.name === "metamask" ? "bundled" : "library",
662
- trust: source.name === "metamask" ? "trusted" : "unknown",
663
- name: source.name ?? path.basename(source.root)
664
- }
665
- }));
659
+ return Promise.all(
660
+ sources.map(async (source) => {
661
+ const isBundled = source.name === "metamask";
662
+ const detected = isBundled ? {} : await gitLibraryProvenance(source.root);
663
+ return {
664
+ ...source,
665
+ provenance: {
666
+ kind: isBundled ? "bundled" : "library",
667
+ trust: isBundled ? "trusted" : "unknown",
668
+ name: source.name ?? path.basename(source.root),
669
+ ...detected,
670
+ ...source.provenance
671
+ }
672
+ };
673
+ })
674
+ );
666
675
  }
667
676
  function synthesizeOneNodeRecipe(action, args) {
668
677
  return {
@@ -0,0 +1,33 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ async function gitLibraryProvenance(root) {
5
+ try {
6
+ const { stdout: tracked } = await execFileAsync("git", ["-C", root, "ls-files", "--", "."], {
7
+ encoding: "utf8",
8
+ timeout: 5e3
9
+ });
10
+ if (!tracked.trim()) return {};
11
+ const [{ stdout: revision }, { stdout: status }] = await Promise.all([
12
+ execFileAsync("git", ["-C", root, "rev-parse", "HEAD"], {
13
+ encoding: "utf8",
14
+ timeout: 5e3
15
+ }),
16
+ execFileAsync(
17
+ "git",
18
+ ["-C", root, "status", "--porcelain=v1", "--untracked-files=normal", "--", "."],
19
+ {
20
+ encoding: "utf8",
21
+ timeout: 5e3
22
+ }
23
+ )
24
+ ]);
25
+ const trimmedRevision = revision.trim();
26
+ return trimmedRevision ? { revision: trimmedRevision, dirty: status.trim().length > 0 } : {};
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+ export {
32
+ gitLibraryProvenance
33
+ };
@@ -77,6 +77,40 @@ Example:
77
77
  mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
78
78
  mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
79
79
  },
80
+ {
81
+ name: "execution-template",
82
+ summary: "Discover, validate, and materialize shared agent checklists.",
83
+ example: "mm-harness execution-template list --package-templates <path> --json",
84
+ helpText: `mm-harness execution-template <list|materialize|lint|new> [options]
85
+
86
+ Shared Markdown checklist catalog used by direct and orchestrated workflows.
87
+ Sources use the flow-tree shape <root>/<flow>/<variant>.md.
88
+
89
+ list List compatible templates and portable source provenance.
90
+ materialize Select one template, copy its immutable snapshot, and optionally
91
+ write a provenance record.
92
+ lint Validate one template file or flow-tree directory.
93
+ new Create a minimal starter template.
94
+
95
+ Common source/filter options:
96
+ --dir <path> Custom flow-tree source (repeatable)
97
+ --domain-dir <domain=path> Domain-scoped team source (repeatable)
98
+ --package-templates <path> Canonical package flow-tree root
99
+ --package-id <id> Source label for the package root
100
+ --project-worker <path> Project template directory
101
+ --project-name <name> Source label for project templates
102
+ --flow <flow> Exact flow
103
+ --platform <platform> Exact platform
104
+ --run-mode <mode> autonomous|interactive|validation
105
+ --domain <domain> Exact domain
106
+ --id <id> Exact template id
107
+ --json Machine-readable output
108
+
109
+ Materialize:
110
+ mm-harness execution-template materialize <output> --flow fix-bug \\
111
+ --platform mobile --run-mode autonomous --id fix-bug/autonomous.mobile \\
112
+ --package-templates <path> --provenance <path> --json`
113
+ },
80
114
  {
81
115
  name: "actions",
82
116
  summary: "Discover typed single operations and their fields.",
@@ -552,7 +586,7 @@ const HELP_GROUPS = [
552
586
  {
553
587
  title: "DISCOVER",
554
588
  blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
555
- commands: ["actions", "call"]
589
+ commands: ["actions", "call", "execution-template"]
556
590
  },
557
591
  {
558
592
  title: "PROVE",
@@ -69,12 +69,17 @@ async function activateSemanticControl(page, selector) {
69
69
  export async function openHome(page, timeoutMs) {
70
70
  const home = dataTestId('bottom-nav-home');
71
71
  const selectedHome = `${home}[aria-current="page"]`;
72
+ const legacyHome = dataTestId('account-overview__asset-tab');
73
+ const selectedLegacyHome = `${legacyHome}[aria-selected="true"]`;
72
74
  const deadline = Date.now() + timeoutMs;
73
75
  const steps = [];
74
76
  let pendingActivation;
75
77
 
76
78
  while (Date.now() < deadline) {
77
- if (await hasVisibleSelector(page, selectedHome)) {
79
+ if (
80
+ (await hasVisibleSelector(page, selectedHome)) ||
81
+ (await hasVisibleSelector(page, selectedLegacyHome))
82
+ ) {
78
83
  return { method: 'visible-ui', href: await currentHref(page), steps };
79
84
  }
80
85
  const href = await currentHref(page);
@@ -83,12 +88,17 @@ export async function openHome(page, timeoutMs) {
83
88
  await pauseForUi(page);
84
89
  continue;
85
90
  }
86
- if (await hasVisibleSelector(page, home)) {
87
- if (pendingActivation?.selector !== home) pendingActivation = undefined;
91
+ const homeControl = (await hasVisibleSelector(page, home))
92
+ ? home
93
+ : (await hasVisibleSelector(page, legacyHome))
94
+ ? legacyHome
95
+ : undefined;
96
+ if (homeControl) {
97
+ if (pendingActivation?.selector !== homeControl) pendingActivation = undefined;
88
98
  if (!pendingActivation) {
89
- await activateSemanticControl(page, home);
90
- steps.push(home);
91
- pendingActivation = { selector: home, href };
99
+ await activateSemanticControl(page, homeControl);
100
+ steps.push(homeControl);
101
+ pendingActivation = { selector: homeControl, href };
92
102
  }
93
103
  await pauseForUi(page);
94
104
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -19,9 +19,9 @@
19
19
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
20
20
  },
21
21
  "dependencies": {
22
- "@farmslot/agent-runtime": "^0.3.1",
22
+ "@farmslot/agent-runtime": "^0.4.0",
23
23
  "@farmslot/handoff": "^0.3.1",
24
- "@farmslot/protocol": "^0.12.0",
24
+ "@farmslot/protocol": "^0.13.0",
25
25
  "@farmslot/recipe-harness": "^0.10.0",
26
26
  "commander": "^12.0.0",
27
27
  "es-module-lexer": "2.3.1",
@@ -29,7 +29,7 @@ _mmh_bin() {
29
29
  return 1
30
30
  }
31
31
 
32
- _mmh_commands="launch stop logs debug fixtures actions call run doctor check checklist recipe-quality install verify cleanup completions"
32
+ _mmh_commands="launch stop logs debug fixtures actions call run doctor check checklist execution-template recipe-quality install verify cleanup completions"
33
33
 
34
34
  # Per-command flags (static, from the mm-harness surface).
35
35
  _mmh_flags_for() {
@@ -40,6 +40,7 @@ _mmh_flags_for() {
40
40
  debug) printf '%s' "--worker --dev-menu --adapter --target --json" ;;
41
41
  fixtures) printf '%s' "--from --dev --force --fixture --adapter --target --json" ;;
42
42
  checklist) printf '%s' "" ;;
43
+ execution-template) printf '%s' "--dir --domain-dir --project-worker --project-name --package-templates --package-id --flow --run-mode --platform --domain --id --provenance --include-shadowed --no-include-shadowed --title --force --json" ;;
43
44
  actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
44
45
  call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
45
46
  run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;