@deeeed/metamask-harness 0.7.1 → 0.7.3

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
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3
4
+
5
+ ### Fixed
6
+ - **`provision runway` gates its install-skip on artifact identity.** The baseline now records the installed artifact identity (run id, branch, digest) at install time and a skip preserves it; provisioning skips ONLY when the recorded identity matches the requested (or probe-resolved) run — a different or unknown installed app is reinstalled from the cache instead of being silently kept. Previously any installed MetaMask.app suppressed the install, so a stale binary could be served against a newer JS bundle.
7
+ - **Provision decisions are machine-readable**: the --json envelope carries `reason` (`identity-match` | `identity-mismatch` | `unknown-identity` | `fresh-install` | `forced`) and a skip envelope includes the matched artifact identity.
8
+
9
+ ## 0.7.2
10
+
11
+ ### Added
12
+ - **`recipe-quality build`** — the single worker surface for producing `recipe-quality.json`: fronts @farmslot/agent-runtime's canonical builder (no reimplementation), validates against @farmslot/protocol `RecipeQualityArtifact` before writing (invalid input writes nothing, exit 5 with a teaching escape naming the invalid field). New production dependency: `@farmslot/agent-runtime`.
13
+
14
+ ### Changed
15
+ - `@farmslot/protocol` dependency raised to `^0.7.3` so one protocol version serves both the harness and the builder.
16
+ - The packed-install contract test now executes `recipe-quality build` from the published layout, guarding the new dependency's packaging path.
17
+
3
18
  ## 0.7.1
4
19
 
5
20
  ### Added
@@ -71,25 +71,43 @@ async function provisionRunwayMobile(target, options) {
71
71
  };
72
72
  }
73
73
  const sim = ensureSimulator(simulator, runtime, deviceType);
74
+ let resolved;
75
+ let reason = options.force ? "forced" : "fresh-install";
74
76
  if (!options.force && appInstalled(sim.udid ?? sim.name)) {
75
- const baselinePath2 = writeRunwayBaseline(resolvedTarget, slot, platform, void 0, void 0, sim, true, options.runtimeDir);
76
- return {
77
- schemaVersion: 1,
78
- command: "provision",
79
- adapter: "mobile",
80
- target: resolvedTarget,
81
- platform,
82
- status: "pass",
83
- exitCode: 0,
84
- slot,
85
- simulator: sim,
86
- skipped: true,
87
- installed: false,
88
- baselinePath: baselinePath2
89
- };
77
+ const installed = readInstalledBaselineArtifact(resolvedTarget, sim, options.runtimeDir);
78
+ let requestedRunId = options.run;
79
+ if (installed && !requestedRunId) {
80
+ log(options, `runway: resolving Runway artifact for ${branch} (default ${defaultBranch})`);
81
+ resolved = resolveArtifactRun(repo, branch, defaultBranch, void 0);
82
+ requestedRunId = resolved.runId;
83
+ }
84
+ if (installed && installed.runId === requestedRunId) {
85
+ const baselinePath2 = writeRunwayBaseline(resolvedTarget, slot, platform, void 0, void 0, sim, true, options.runtimeDir, installed.record);
86
+ return {
87
+ schemaVersion: 1,
88
+ command: "provision",
89
+ adapter: "mobile",
90
+ target: resolvedTarget,
91
+ platform,
92
+ status: "pass",
93
+ exitCode: 0,
94
+ slot,
95
+ // State WHAT matched: the recorded artifact identity the skip trusted.
96
+ artifact: installed.record,
97
+ simulator: sim,
98
+ skipped: true,
99
+ installed: false,
100
+ baselinePath: baselinePath2,
101
+ reason: "identity-match"
102
+ };
103
+ }
104
+ reason = installed ? "identity-mismatch" : "unknown-identity";
105
+ log(options, installed ? `runway: installed app is run ${installed.runId} but run ${requestedRunId} is requested; reinstalling` : "runway: installed app has no recorded Runway identity; installing the requested artifact");
106
+ }
107
+ if (!resolved) {
108
+ log(options, `runway: resolving Runway artifact for ${branch} (default ${defaultBranch})`);
109
+ resolved = resolveArtifactRun(repo, branch, defaultBranch, options.run);
90
110
  }
91
- log(options, `runway: resolving Runway artifact for ${branch} (default ${defaultBranch})`);
92
- const resolved = resolveArtifactRun(repo, branch, defaultBranch, options.run);
93
111
  const cacheRoot = options.cacheRoot ?? defaultRunwayCacheRoot();
94
112
  const cache = ensureCachedArtifact(repo, resolved.branch, resolved.runId, cacheRoot, options);
95
113
  log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
@@ -111,7 +129,8 @@ async function provisionRunwayMobile(target, options) {
111
129
  simulator: sim,
112
130
  installed: true,
113
131
  skipped: false,
114
- baselinePath
132
+ baselinePath,
133
+ reason
115
134
  };
116
135
  } catch (error) {
117
136
  return fail(resolvedTarget, platform, "PROVISION_FAILED", errorMessage(error), command, slot);
@@ -465,6 +484,22 @@ function baselineSimulatorCandidates(data, current) {
465
484
  }
466
485
  return [...new Set(candidates)];
467
486
  }
487
+ function readInstalledBaselineArtifact(target, sim, runtimeDir) {
488
+ let data;
489
+ try {
490
+ data = JSON.parse(fs.readFileSync(runwayBaselinePath(target, runtimeDir), "utf8"));
491
+ } catch {
492
+ return void 0;
493
+ }
494
+ if (data.appInstalled !== true) return void 0;
495
+ const record = data.artifact;
496
+ if (!record || typeof record !== "object" || Array.isArray(record)) return void 0;
497
+ const runId = record.runId;
498
+ if (typeof runId !== "string" || !runId) return void 0;
499
+ const recordedSims = baselineSimulatorCandidates(data, {});
500
+ if (!recordedSims.includes(sim.name) && (!sim.udid || !recordedSims.includes(sim.udid))) return void 0;
501
+ return { runId, record };
502
+ }
468
503
  function appInstalled(device) {
469
504
  try {
470
505
  execFileSync("xcrun", ["simctl", "get_app_container", device, RUNWAY_IOS_METADATA.bundleId, "app"], { stdio: ["ignore", "ignore", "ignore"] });
@@ -473,7 +508,7 @@ function appInstalled(device) {
473
508
  return false;
474
509
  }
475
510
  }
476
- function writeRunwayBaseline(target, slot, platform, resolved, artifact, simulator, alreadyInstalled, runtimeDir) {
511
+ function writeRunwayBaseline(target, slot, platform, resolved, artifact, simulator, alreadyInstalled, runtimeDir, preservedArtifact) {
477
512
  const file = runwayBaselinePath(target, runtimeDir);
478
513
  fs.mkdirSync(path.dirname(file), { recursive: true });
479
514
  fs.writeFileSync(file, `${JSON.stringify({
@@ -484,7 +519,7 @@ function writeRunwayBaseline(target, slot, platform, resolved, artifact, simulat
484
519
  slotId: slot.slotId ?? null,
485
520
  watcherPort: slot.watcherPort ?? null,
486
521
  simulator,
487
- artifact: resolved && artifact ? { ...resolved, path: artifact.appPath, sizeBytes: artifact.sizeBytes, sha256: artifact.sha256 } : null,
522
+ artifact: resolved && artifact ? { ...resolved, path: artifact.appPath, sizeBytes: artifact.sizeBytes, sha256: artifact.sha256 } : preservedArtifact ?? null,
488
523
  alreadyInstalled,
489
524
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
490
525
  }, null, 2)}
@@ -16,6 +16,7 @@ const SPEC = {
16
16
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
17
17
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--cdp-port"] },
18
18
  { name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
19
+ { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
19
20
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
20
21
  { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
21
22
  { name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ import { handleLaunch } from "./commands/launch/index.js";
17
17
  import { handleLogs } from "./commands/logs.js";
18
18
  import { handleDebug } from "./commands/debug.js";
19
19
  import { handleFixtures } from "./commands/fixtures.js";
20
+ import { handleRecipeQuality } from "./commands/recipe-quality.js";
20
21
  import { parseArgs, targetPath } from "./commands/parse-args.js";
21
22
  import { runOneNode } from "./commands/run-engine.js";
22
23
  const COMMANDS = {
@@ -121,6 +122,7 @@ async function main(argv) {
121
122
  if (command === "logs") return handleLogs(argv.slice(1));
122
123
  if (command === "debug") return handleDebug(argv.slice(1));
123
124
  if (command === "fixtures") return handleFixtures(argv.slice(1), { runOneNode });
125
+ if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
124
126
  const handler = COMMANDS[command];
125
127
  if (!handler) throw new Error(`Unknown command: ${command}`);
126
128
  return handler(parseArgs(argv.slice(1), command));
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ buildRecipeQualityArtifact
5
+ } from "@farmslot/agent-runtime";
6
+ import { EXIT, usageOut } from "./shared.js";
7
+ import { optionFlag, optionString, parseArgs } from "./parse-args.js";
8
+ const BUILD_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]";
9
+ function errorMessage(error) {
10
+ return error instanceof Error ? error.message : String(error);
11
+ }
12
+ function invalidOut(json, message, source) {
13
+ const userAction = `fix ${source} (${message}), then re-run: ${BUILD_USAGE}`;
14
+ if (json) {
15
+ console.log(
16
+ JSON.stringify(
17
+ {
18
+ schemaVersion: 1,
19
+ command: "recipe-quality",
20
+ action: "build",
21
+ status: "fail",
22
+ exitCode: EXIT.validation,
23
+ error: { code: "RECIPE_QUALITY_INVALID", message, userAction }
24
+ },
25
+ null,
26
+ 2
27
+ )
28
+ );
29
+ } else {
30
+ console.error(`\u2717 mm-harness recipe-quality: ${message}
31
+ Next: ${userAction}`);
32
+ }
33
+ return EXIT.validation;
34
+ }
35
+ async function handleRecipeQuality(argv) {
36
+ const { positional, options } = parseArgs(argv, "recipe-quality");
37
+ const json = optionFlag(options, "json");
38
+ const action = positional[0];
39
+ if (action !== "build") {
40
+ const message = action ? `unknown action '${action}'` : "missing action";
41
+ return usageOut(json, "recipe-quality", message, BUILD_USAGE);
42
+ }
43
+ return buildArtifact(options, json);
44
+ }
45
+ function buildArtifact(options, json) {
46
+ const input = optionString(options, "input");
47
+ const output = optionString(options, "output");
48
+ if (!input) return usageOut(json, "recipe-quality", "missing --input <compact.json>", BUILD_USAGE);
49
+ if (!output) return usageOut(json, "recipe-quality", "missing --output <path>", BUILD_USAGE);
50
+ const inputPath = path.resolve(input);
51
+ let raw;
52
+ try {
53
+ raw = fs.readFileSync(inputPath, "utf8");
54
+ } catch (error) {
55
+ return usageOut(
56
+ json,
57
+ "recipe-quality",
58
+ `cannot read --input ${input}: ${errorMessage(error)}`,
59
+ `write the compact recipe-quality JSON to that path, then: ${BUILD_USAGE}`
60
+ );
61
+ }
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(raw);
65
+ } catch (error) {
66
+ return invalidOut(json, `--input ${input} is not valid JSON: ${errorMessage(error)}`, `--input ${input}`);
67
+ }
68
+ let artifact;
69
+ try {
70
+ artifact = buildRecipeQualityArtifact(parsed);
71
+ } catch (error) {
72
+ return invalidOut(json, errorMessage(error), `--input ${input}`);
73
+ }
74
+ const outputPath = path.resolve(output);
75
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
76
+ fs.writeFileSync(outputPath, `${JSON.stringify(artifact, null, 2)}
77
+ `);
78
+ if (json) {
79
+ console.log(
80
+ JSON.stringify(
81
+ { schemaVersion: 1, command: "recipe-quality", action: "build", status: "ok", outputPath: output, artifact },
82
+ null,
83
+ 2
84
+ )
85
+ );
86
+ } else {
87
+ console.log(`\u2713 recipe-quality build \u2192 ${output} (verdict: ${artifact.verdict})`);
88
+ }
89
+ return EXIT.ok;
90
+ }
91
+ export {
92
+ handleRecipeQuality
93
+ };
@@ -139,6 +139,28 @@ Example:
139
139
  mm-harness doctor --adapter extension --target /path/to/checkout --cdp-port 6662 --expect-live
140
140
  mm-harness doctor --adapter mobile --target /path/to/checkout`
141
141
  },
142
+ {
143
+ name: "recipe-quality",
144
+ summary: "Build the recipe-quality artifact from a compact verdict JSON (validates before writing).",
145
+ example: "mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json",
146
+ helpText: `mm-harness recipe-quality build [flags]
147
+
148
+ Build artifacts/recipe-quality.json from the compact fields a recipe-quality pass
149
+ produces (verdict + reasons + optional guidance/dimensions/findings/delta/training).
150
+ The artifact is validated against the RecipeQualityArtifact schema before it is
151
+ written \u2014 an invalid input never reaches disk.
152
+
153
+ --input <compact.json> Compact verdict JSON: { "verdict": "pass|warn|fail", "reasons": [..],
154
+ "betterVersionGuidance"?: [..], "dimensions"?: {..}, "trainingFields"?: {..}, \u2026 }
155
+ --output <path> Where to write the built artifact (parent dirs created)
156
+ --json Machine-readable envelope { status, outputPath, artifact }
157
+
158
+ Exit: 0 built \xB7 2 missing/unreadable args \xB7 5 invalid input (teaching escape names the invalid field).
159
+
160
+ Example:
161
+ mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json
162
+ mm-harness recipe-quality build --input compact.json --output artifacts/recipe-quality.json --json`
163
+ },
142
164
  {
143
165
  name: "provision",
144
166
  summary: "Install the cached Runway iOS dev client on a prepared mobile slot (no deps, no Metro).",
@@ -393,7 +415,7 @@ const HELP_GROUPS = [
393
415
  {
394
416
  title: "PROVE",
395
417
  blurb: "run recipes and inspect readiness",
396
- commands: ["run", "doctor"]
418
+ commands: ["run", "doctor", "recipe-quality"]
397
419
  },
398
420
  {
399
421
  title: "RUNTIME OVERLAY",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -16,7 +16,8 @@
16
16
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
17
17
  },
18
18
  "dependencies": {
19
- "@farmslot/protocol": "^0.7.2",
19
+ "@farmslot/agent-runtime": "^0.1.0",
20
+ "@farmslot/protocol": "^0.7.3",
20
21
  "@farmslot/recipe-harness": "^0.3.3",
21
22
  "commander": "^12.0.0",
22
23
  "viem": "^2.54.3"