@deeeed/metamask-harness 0.54.0 → 0.55.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,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.55.0 - 2026-09-18
6
+
7
+ ### Added
8
+
9
+ - `doctor` reports two more setup answers, also carried in its `--json`: `update` says whether this harness is the published latest (`0.54.1 is latest`, `0.54.1 → 0.55.0 available`, or `not checked` when the registry is unreachable or `MM_HARNESS_NO_UPDATE_CHECK=1`), and `skills` counts the `mms-*` skill directories the checkout has installed (`none installed` when there are none). Both are advisory — neither joins `requiredChecks`, changes `status`/`ready`, or can fail doctor, because the harness does its job without either. The registry check answers from the existing update cache, so it costs nothing on a checkout that already looked today. A setup skill now reads one verdict from `doctor` instead of running checks of its own; which skill names are expected or retired stays with the installer and that skill, so the harness carries no list of them.
10
+
11
+ ### Changed
12
+
13
+ - The checkout view's fixture block is labelled `last fixtures set:`, because it reports the result of the last `fixtures set` this checkout ran, not the state of the wallet fixture file — which `doctor` already reports as `fixture.status`.
14
+ - The mobile deps note — raised when the manifests are newer than the install markers and no baseline is recorded — says what was observed, why it is treated as fine, and the one case that calls for action, with the install command it already teaches for a blocked preflight. It is set off from the command's own output by a blank line and a `mm-harness note (deps):` label, so a reader can tell the harness is talking.
15
+
16
+ ## 0.54.1 - 2026-09-18
17
+
18
+ ### Changed
19
+
20
+ - `help --json` quality section carries the verdict rules (pass, warn, fail), the shape of an independent critique, and per-adapter review notes for Mobile, Extension and Core; the separate recipe-quality skill is retired in favour of this guidance.
21
+
5
22
  ## 0.54.0 - 2026-09-17
6
23
 
7
24
  ### Changed
package/README.md CHANGED
@@ -271,7 +271,7 @@ Unset `MM_HARNESS_BIN` to return to the published installation.
271
271
 
272
272
  ### Workflow entry points and completion
273
273
 
274
- Explicitly invoke `mms-recipe-dev`, `mms-recipe-fixbug`, `mms-recipe-qa` or the team's static review skill. `mms-recipe-cook` selects or resumes their shared Markdown checklist. Matching descriptions or changed paths never activates these workflows.
274
+ Explicitly invoke `mms-recipe-dev`, `mms-recipe-fix-bug`, `mms-recipe-qa` or the team's static review skill. `mms-recipe-cook` selects or resumes their shared Markdown checklist. Matching descriptions or changed paths never activates these workflows.
275
275
 
276
276
  Static Perps review uses a generated source checklist without installing or launching the app harness. `recipe-qa` freezes a PR, range or daily change window and requires runtime smoke plus selected recipe proof. Official release validation uses its named release profile and proof lane.
277
277
 
@@ -11,6 +11,7 @@ import {
11
11
  supersededErrorCapture
12
12
  } from "@farmslot/recipe-harness/runtime/log-analysis";
13
13
  import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
14
+ import { color } from "../../cli-color.js";
14
15
  import { recipeRuntimePath } from "../../paths.js";
15
16
  import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
16
17
  import { mobileProductMarkers } from "./deps-markers.js";
@@ -147,14 +148,16 @@ const launchActions = (target, clearMetro = false) => {
147
148
  actions.push({ id: "wait-for-bridge", cwd: target });
148
149
  return actions;
149
150
  };
151
+ function depsInstallCommand(resolved) {
152
+ return fs.existsSync(path.join(resolved, "node_modules")) ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
153
+ }
150
154
  async function decideMobileReadiness(target, options = {}) {
151
155
  const resolved = path.resolve(target);
152
156
  if (options.record) recordDepsBaseline(resolved);
153
157
  const fast = options.preflightMode === "fast";
154
158
  const report = await computeMobileReadiness(resolved, options, fast);
155
159
  if (fast && report.decision === "install") {
156
- const hasNodeModules = fs.existsSync(path.join(resolved, "node_modules"));
157
- const standalone = hasNodeModules ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
160
+ const standalone = depsInstallCommand(resolved);
158
161
  return {
159
162
  ...report,
160
163
  decision: "blocked",
@@ -184,6 +187,15 @@ async function decideMobileReadiness(target, options = {}) {
184
187
  actions: report.decision === "ready" ? launchActions(resolved) : report.actions
185
188
  };
186
189
  }
190
+ function writeDepsNote(resolved) {
191
+ const stream = process.stderr;
192
+ const body = `package.json/yarn.lock are newer than the node_modules install markers and no install baseline is recorded yet, so the installed node_modules are used as they are. Pulling commits refreshes those timestamps without changing dependencies, and a required-dependency presence check still runs, so this on its own is not evidence that anything drifted. Act only if you know dependencies changed: run ${depsInstallCommand(resolved)} once \u2014 that refreshes the markers, and \`mm-harness prepare\` records a baseline when it runs the install, so this note stops.`;
193
+ stream.write(
194
+ `
195
+ ${color("warn", "mm-harness note (deps):", { stream })} ${color("dim", body, { stream })}
196
+ `
197
+ );
198
+ }
187
199
  async function computeMobileReadiness(resolved, options, fast) {
188
200
  const rawDeps = depsCheck(resolved, {
189
201
  productMarkers: mobileProductMarkers(options.platform)
@@ -191,9 +203,7 @@ async function computeMobileReadiness(resolved, options, fast) {
191
203
  let deps = rawDeps;
192
204
  if (rawDeps.status === "stale" && !rawDeps.hasBaseline) {
193
205
  deps = { installed: rawDeps.installed, status: "current", hasBaseline: false };
194
- process.stderr.write(
195
- "[runtime-decision] manifest is newer than install markers (no recorded baseline); trusting installed node_modules \u2014 run yarn setup manually if deps truly changed.\n"
196
- );
206
+ writeDepsNote(resolved);
197
207
  }
198
208
  if (deps.status === "current") {
199
209
  const absent = missingRequiredDeps(resolved);
@@ -47,7 +47,7 @@ function renderCheckoutView(view) {
47
47
  }
48
48
  }
49
49
  if (view.fixtures) {
50
- lines.push(`${out("label", "fixtures:")}`);
50
+ lines.push(`${out("label", "last fixtures set:")}`);
51
51
  for (const [key, value] of Object.entries(view.fixtures)) {
52
52
  const isSet = value !== null && value !== "";
53
53
  lines.push(` ${key.padEnd(22)} ${isSet ? out("ok", String(value)) : out("dim", "(unset)")}`);
@@ -21,6 +21,7 @@ import {
21
21
  renderMobileLiveBlock
22
22
  } from "./mobile-device-view.js";
23
23
  import { farmslotReadyIndicator } from "./farmslot-ready.js";
24
+ import { updateStatus } from "./update.js";
24
25
  import { ensureRuntimeContext } from "../runtime-context.js";
25
26
  import { acquireCheckoutLock } from "../checkout-lock.js";
26
27
  import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
@@ -206,12 +207,14 @@ async function handleDoctor({ options }) {
206
207
  discovery
207
208
  );
208
209
  }
210
+ const skills = skillsView(target);
211
+ const update = updateStatus();
209
212
  const doctorUserAction = doctorResult.checks.find(
210
213
  (check) => check.required && check.status === "fail" && check.userAction
211
214
  )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
212
215
  const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
213
216
  const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
214
- if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
217
+ if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, skills, update, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
215
218
  else {
216
219
  const out = (style, text) => color(style, text, { stream: process.stdout });
217
220
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
@@ -246,6 +249,8 @@ async function handleDoctor({ options }) {
246
249
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
247
250
  }
248
251
  if (capture) renderCaptureHealth(capture, out);
252
+ renderSkillsView(skills, out);
253
+ renderUpdateStatus(update, out);
249
254
  if (adapter === "mobile") {
250
255
  if (deviceView) renderMobileDeviceList(deviceView, out);
251
256
  if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
@@ -374,6 +379,41 @@ function renderCaptureHealth(capture, out) {
374
379
  console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
375
380
  }
376
381
  }
382
+ const SKILL_ROOTS = [path.join(".claude", "skills"), path.join(".agents", "skills")];
383
+ function skillsView(target) {
384
+ const names = /* @__PURE__ */ new Set();
385
+ let dir = null;
386
+ for (const root of SKILL_ROOTS) {
387
+ const absolute = path.join(target, root);
388
+ let entries;
389
+ try {
390
+ entries = fs.readdirSync(absolute, { withFileTypes: true });
391
+ } catch {
392
+ continue;
393
+ }
394
+ dir ??= root;
395
+ for (const entry of entries) {
396
+ if (entry.isDirectory() && entry.name.startsWith("mms-")) names.add(entry.name);
397
+ }
398
+ }
399
+ return { dir, installed: [...names].sort() };
400
+ }
401
+ function renderSkillsView(skills, out) {
402
+ if (skills.installed.length === 0) {
403
+ console.log(`${out("label", "skills:")} ${out("warn", "none installed")} ${out("dim", "(yarn skills)")}`);
404
+ return;
405
+ }
406
+ console.log(`${out("label", "skills:")} ${skills.installed.length} installed ${out("dim", skills.dir ?? "")}`);
407
+ }
408
+ function renderUpdateStatus(status, out) {
409
+ if (!status.checked) {
410
+ console.log(`${out("label", "update:")} ${out("dim", "not checked")}`);
411
+ return;
412
+ }
413
+ console.log(
414
+ status.updateAvailable ? `${out("label", "update:")} ${out("warn", `${status.current} \u2192 ${status.latest} available`)} ${out("dim", "(mm-harness update)")}` : status.latest && status.latest !== status.current ? `${out("label", "update:")} ${out("ok", status.current)} ${out("dim", `ahead of registry ${status.latest}`)}` : `${out("label", "update:")} ${out("ok", `${status.current} is latest`)}`
415
+ );
416
+ }
377
417
  function detectOrphanMetros(target, livePort) {
378
418
  try {
379
419
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -493,5 +533,6 @@ function readTextSnapshot(file) {
493
533
  export {
494
534
  doctorFixNextActions,
495
535
  ensureExtensionPerpsBuildFlag,
496
- handleDoctor
536
+ handleDoctor,
537
+ skillsView
497
538
  };
@@ -199,6 +199,23 @@ function nudgeDisabled() {
199
199
  function nudgeLine(current, latest) {
200
200
  return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
201
201
  }
202
+ const DOCTOR_FETCH_TIMEOUT_MS = 3e3;
203
+ const DOCTOR_CACHE_FRESH_MS = 60 * 60 * 1e3;
204
+ function updateStatus(now = Date.now()) {
205
+ const current = currentVersion();
206
+ if (nudgeDisabled()) return { current, checked: false };
207
+ const file = cacheFile();
208
+ const cache = readCache(file);
209
+ const latest = cache?.latest && now - cache.lastCheck < DOCTOR_CACHE_FRESH_MS ? cache.latest : probeLatest(file, now);
210
+ if (!latest) return { current, checked: false };
211
+ return { current, checked: true, latest, updateAvailable: isNewer(latest, current) };
212
+ }
213
+ function probeLatest(file, now) {
214
+ const fetched = fetchLatest(DOCTOR_FETCH_TIMEOUT_MS);
215
+ if (!fetched.latest) return null;
216
+ writeCache(file, { lastCheck: now, latest: fetched.latest });
217
+ return fetched.latest;
218
+ }
202
219
  function maybeNudge(now = Date.now()) {
203
220
  if (nudgeDisabled()) return;
204
221
  const file = cacheFile();
@@ -233,5 +250,6 @@ export {
233
250
  maybeNudge,
234
251
  nudgeDisabled,
235
252
  nudgeLine,
236
- registryCwd
253
+ registryCwd,
254
+ updateStatus
237
255
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -369,7 +369,7 @@ async function checkBehavior(browserEndpoint, port, pages) {
369
369
  await new Promise((resolve) => setTimeout(resolve, 25));
370
370
  return window.__siteCopied;
371
371
  }
372
- const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [] };
372
+ const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [], text: document.body.innerText };
373
373
  for (const tab of document.querySelectorAll('[role="tab"]')) {
374
374
  tab.click();
375
375
  const panel = document.getElementById(tab.getAttribute('aria-controls'));
@@ -389,14 +389,15 @@ async function checkBehavior(browserEndpoint, port, pages) {
389
389
  // Setting up installs the harness and the skills; the work prompts invoke
390
390
  // them by their exact installed names.
391
391
  const expected = [
392
- ['npm i -g @deeeed/metamask-harness@latest', copied.setup],
393
- ['yarn skills', copied.setup],
392
+ ['/mms-recipe-setup', copied.setup],
393
+ ['agentic/recipe-setup', copied.text],
394
394
  ['/mms-recipe-qa', copied.work.join('\n')],
395
- ['/mms-recipe-cook', copied.work.join('\n')],
395
+ ['/mms-recipe-fix-bug', copied.work.join('\n')],
396
+ ['/mms-recipe-dev', copied.work.join('\n')],
396
397
  ['/mms-recipe-performance', copied.work.join('\n')],
397
- ['/mms-recipe-evidence', copied.work.join('\n')],
398
- ['agentic/recipe-performance', copied.setup],
399
- ['agentic/recipe-evidence', copied.setup],
398
+ ['/mms-recipe-pretty-pr', copied.work.join('\n')],
399
+ ['perps/recipe-performance', copied.text],
400
+ ['agentic/recipe-pretty-pr', copied.text],
400
401
  ];
401
402
  const missing = expected.filter(([line, text]) => !text?.includes(line)).map(([line]) => line);
402
403
  if (missing.length) {
@@ -448,7 +449,7 @@ async function checkBehavior(browserEndpoint, port, pages) {
448
449
  return checked;
449
450
  })()
450
451
  `);
451
- if (!previews.includes('review-pr/static-perps.mobile') || !previews.includes('qa/pr.extension')) {
452
+ if (!previews.includes('review-pr/static-perps.mobile') || !previews.includes('qa/default')) {
452
453
  throw new Error('Static review and QA previews must both be available');
453
454
  }
454
455
  const progress = await page.evaluate(String.raw`
@@ -212,6 +212,26 @@
212
212
  "mobile",
213
213
  "extension"
214
214
  ]
215
+ },
216
+ "Verdict: pass when every named claim ran an executable path with an assertion and consistent evidence; warn when the supported claims pass but non-blocking gaps remain; fail when a named claim has no executable path or assertion, the graph can pass unconditionally, required evidence is missing or contradictory, or the relevant path did not run. A visual claim cannot pass without a settled capture. Write only the compact {verdict, reasons} to artifacts/recipe-quality-input.json and build the artifact with mm-harness recipe-quality build; never hand-author it.",
217
+ "Independent critique, when one is requested: the verdict with one evidence-backed sentence, uncovered or weak proof targets, graph, composition, adapter and reuse findings, evidence mismatches and flake risks, and the three highest-value concrete edits.",
218
+ {
219
+ "text": "State the simulator or device, build type and app state. Unlock through a fixture or a documented primitive; keep account, balance, network, permissions and feature flags deterministic; use existing page objects, test ids and fixtures; capture only after ui.wait_for, a route assertion or a state assertion; teardown must not leak balances, pending transactions, selected network, permissions or onboarding state into the next run.",
220
+ "adapters": [
221
+ "mobile"
222
+ ]
223
+ },
224
+ {
225
+ "text": "State browser, build, fixture and the popup, fullscreen or service-worker context. Popup UI proven only through CDP eval or controller reads fails a visual claim; capture after settle; prefer existing e2e and fixture helpers over raw steps.",
226
+ "adapters": [
227
+ "extension"
228
+ ]
229
+ },
230
+ {
231
+ "text": "Core is headless: prove claims with summary.json, trace.json, logs or package tests, never screenshots; use manifest-declared headless actions only; state the target package or workspace, fixture, start state and teardown for controller mutations; a non-visual criterion needs unit or package test coverage.",
232
+ "adapters": [
233
+ "core"
234
+ ]
215
235
  }
216
236
  ],
217
237
  "commands": [