@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 +17 -0
- package/README.md +1 -1
- package/dist/adapters/mobile/runtime-decision.js +15 -5
- package/dist/checkout-view.js +1 -1
- package/dist/commands/doctor.js +43 -2
- package/dist/commands/update.js +19 -1
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +9 -8
- package/site/assets/help-recipes.json +20 -0
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-
|
|
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
|
|
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
|
-
|
|
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);
|
package/dist/checkout-view.js
CHANGED
|
@@ -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)")}`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
};
|
package/dist/commands/update.js
CHANGED
|
@@ -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
|
@@ -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
|
-
['
|
|
393
|
-
['
|
|
392
|
+
['/mms-recipe-setup', copied.setup],
|
|
393
|
+
['agentic/recipe-setup', copied.text],
|
|
394
394
|
['/mms-recipe-qa', copied.work.join('\n')],
|
|
395
|
-
['/mms-recipe-
|
|
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-
|
|
398
|
-
['
|
|
399
|
-
['agentic/recipe-
|
|
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/
|
|
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": [
|