@deeeed/metamask-harness 0.34.3 → 0.34.4

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,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.34.4 - 2026-08-12
6
+
7
+ ### Changed
8
+
9
+ - Split the site quick start into one-time setup and task-focused workflow tabs.
10
+
11
+ ### Fixed
12
+
13
+ - Update the transitive `tar` dependency from 7.5.19 to 7.5.22.
14
+ - Update transitive `js-yaml`, `nanoid`, and `postcss` dependencies.
15
+
5
16
  ## 0.34.3 - 2026-08-12
6
17
 
7
18
  ### Added
@@ -21,20 +21,28 @@ const ENV_INPUTS = [".js.env", ".env", ".env.local"];
21
21
  const BASELINE_FILE = "mobile-source-baseline.json";
22
22
  function mobileSourceFingerprint(target) {
23
23
  const hash = createHash("sha256");
24
- hash.update(git(target, ["rev-parse", "HEAD"]));
25
- hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
26
- const untracked = git(target, [
27
- "ls-files",
28
- "--others",
29
- "--exclude-standard",
30
- "-z",
31
- "--",
32
- ...SOURCE_PATHS
33
- ]).toString("utf8").split("\0").filter(Boolean).sort();
34
- for (const relative of untracked) {
35
- hash.update(`untracked\0${relative}\0`);
36
- hash.update(fs.readFileSync(path.join(target, relative)));
37
- hash.update("\0");
24
+ const head = tryGit(target, ["rev-parse", "HEAD"]);
25
+ if (head === null) {
26
+ hash.update("non-git\0");
27
+ for (const relative of SOURCE_PATHS) {
28
+ hashPath(hash, target, relative);
29
+ }
30
+ } else {
31
+ hash.update(head);
32
+ hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
33
+ const untracked = git(target, [
34
+ "ls-files",
35
+ "--others",
36
+ "--exclude-standard",
37
+ "-z",
38
+ "--",
39
+ ...SOURCE_PATHS
40
+ ]).toString("utf8").split("\0").filter(Boolean).sort();
41
+ for (const relative of untracked) {
42
+ hash.update(`untracked\0${relative}\0`);
43
+ hash.update(fs.readFileSync(path.join(target, relative)));
44
+ hash.update("\0");
45
+ }
38
46
  }
39
47
  for (const relative of ENV_INPUTS) {
40
48
  const absolute = path.join(target, relative);
@@ -84,6 +92,35 @@ function git(target, args) {
84
92
  stdio: ["ignore", "pipe", "ignore"]
85
93
  });
86
94
  }
95
+ function tryGit(target, args) {
96
+ try {
97
+ return git(target, args);
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function hashPath(hash, root, relative) {
103
+ const absolute = path.join(root, relative);
104
+ if (!fs.existsSync(absolute)) {
105
+ hash.update(`absent\0${relative}\0`);
106
+ return;
107
+ }
108
+ const stat = fs.lstatSync(absolute);
109
+ if (stat.isDirectory()) {
110
+ hash.update(`directory\0${relative}\0`);
111
+ for (const child of fs.readdirSync(absolute).sort()) {
112
+ hashPath(hash, root, path.join(relative, child));
113
+ }
114
+ return;
115
+ }
116
+ if (stat.isSymbolicLink()) {
117
+ hash.update(`symlink\0${relative}\0${fs.readlinkSync(absolute)}\0`);
118
+ return;
119
+ }
120
+ hash.update(`file\0${relative}\0`);
121
+ hash.update(fs.readFileSync(absolute));
122
+ hash.update("\0");
123
+ }
87
124
  function readBaseline(target) {
88
125
  try {
89
126
  const parsed = JSON.parse(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.34.3",
3
+ "version": "0.34.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -146,6 +146,7 @@ const REVEAL = String.raw`
146
146
  const d = document.getElementById(l.getAttribute('aria-controls'));
147
147
  if (d) d.hidden = false;
148
148
  });
149
+ document.querySelectorAll('[role="tabpanel"][hidden]').forEach((p) => { p.hidden = false; });
149
150
  return true;
150
151
  })()
151
152
  `;
@@ -331,6 +332,9 @@ async function auditPage(browserEndpoint, url) {
331
332
  async function checkBehavior(browserEndpoint, port, pages) {
332
333
  const base = `http://127.0.0.1:${port}/`;
333
334
  await withPage(browserEndpoint, `${base}index.html`, async (page) => {
335
+ // Step 1 is the hero call to action; it was a collapsible before, so accept
336
+ // either shape. Step 2 is a tabbed set of prompts, one per kind of work —
337
+ // every one of them has to survive a copy, including the hidden panels.
334
338
  const copied = await page.evaluate(String.raw`
335
339
  (async () => {
336
340
  Object.defineProperty(navigator, 'clipboard', {
@@ -342,22 +346,38 @@ async function checkBehavior(browserEndpoint, port, pages) {
342
346
  },
343
347
  },
344
348
  });
345
- // The onboarding prompt is the hero call to action; it was a collapsible
346
- // before, so accept either shape.
347
- const button = document.querySelector('.cmd-hero .copy, .prompt .copy');
348
- if (!button) throw new Error('copy button was not initialized');
349
- button.click();
350
- await new Promise((resolve) => setTimeout(resolve, 25));
351
- return window.__siteCopied;
349
+ async function grab(button) {
350
+ if (!button) throw new Error('copy button was not initialized');
351
+ window.__siteCopied = null;
352
+ button.click();
353
+ await new Promise((resolve) => setTimeout(resolve, 25));
354
+ return window.__siteCopied;
355
+ }
356
+ const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [] };
357
+ for (const tab of document.querySelectorAll('[role="tab"]')) {
358
+ tab.click();
359
+ const panel = document.getElementById(tab.getAttribute('aria-controls'));
360
+ if (!panel || panel.hidden) throw new Error('tab ' + tab.id + ' did not reveal its panel');
361
+ out.work.push(await grab(panel.querySelector('.cmd .copy')));
362
+ }
363
+ return out;
352
364
  })()
353
365
  `);
354
- // Both halves of the onboarding have to survive a copy: set up the harness
355
- // and the skills, then drive real work through one.
356
- const expected = ['npm i -g @deeeed/metamask-harness@latest', 'yarn skills', '/mms-recipe-cook'];
357
- const missing = expected.filter((line) => !copied?.includes(line));
366
+ // Setting up installs the harness and the skills; the work prompts invoke
367
+ // them by their exact installed names.
368
+ const expected = [
369
+ ['npm i -g @deeeed/metamask-harness@latest', copied.setup],
370
+ ['yarn skills', copied.setup],
371
+ ['/mms-recipe-pr-qa-review', copied.work.join('\n')],
372
+ ['/mms-recipe-cook', copied.work.join('\n')],
373
+ ];
374
+ const missing = expected.filter(([line, text]) => !text?.includes(line)).map(([line]) => line);
358
375
  if (missing.length) {
359
376
  throw new Error(`copy prompt omitted ${missing.map((m) => `"${m}"`).join(', ')}`);
360
377
  }
378
+ if (copied.work.length < 3) {
379
+ throw new Error(`expected three work prompts, copied ${copied.work.length}`);
380
+ }
361
381
 
362
382
  // The landing page is the lobby: one prompt, one way onward, nothing else.
363
383
  const lobby = await page.evaluate(String.raw`
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Shared behaviour for the getting-started site: checklist progress, copy
3
- * buttons, platform filters, and the layer diagram. No dependencies, no
3
+ * buttons, platform filters, tabs, and the layer diagram. No dependencies, no
4
4
  * network, no tracking. Progress lives in localStorage under
5
5
  * `mmh.progress.<page>`, where <page> comes from body[data-progress-page].
6
6
  * Pages that share a namespace share their state (the How it works walkthrough and the V1
@@ -235,6 +235,50 @@
235
235
  select('all');
236
236
  }
237
237
 
238
+ /* ---------- tabs ---------- */
239
+
240
+ /*
241
+ * One tablist per group, roving tabindex: only the selected tab is in the
242
+ * tab order, arrows move between them. Panels stay in the DOM so their
243
+ * commands remain copyable the moment a tab is shown.
244
+ */
245
+ function initTabs() {
246
+ [].slice.call(document.querySelectorAll('[role="tablist"]')).forEach(function (list) {
247
+ var tabs = [].slice.call(list.querySelectorAll('[role="tab"]'));
248
+ if (!tabs.length) return;
249
+
250
+ function select(tab, focus) {
251
+ tabs.forEach(function (other) {
252
+ var on = other === tab;
253
+ other.setAttribute('aria-selected', String(on));
254
+ other.tabIndex = on ? 0 : -1;
255
+ var panel = document.getElementById(other.getAttribute('aria-controls'));
256
+ if (panel) panel.hidden = !on;
257
+ });
258
+ if (focus) tab.focus();
259
+ }
260
+
261
+ tabs.forEach(function (tab, i) {
262
+ tab.addEventListener('click', function () { select(tab, false); });
263
+ tab.addEventListener('keydown', function (e) {
264
+ var next = null;
265
+ if (e.key === 'ArrowRight') next = tabs[(i + 1) % tabs.length];
266
+ if (e.key === 'ArrowLeft') next = tabs[(i - 1 + tabs.length) % tabs.length];
267
+ if (e.key === 'Home') next = tabs[0];
268
+ if (e.key === 'End') next = tabs[tabs.length - 1];
269
+ if (!next) return;
270
+ e.preventDefault();
271
+ select(next, true);
272
+ });
273
+ });
274
+
275
+ var current = tabs.filter(function (t) {
276
+ return t.getAttribute('aria-selected') === 'true';
277
+ })[0];
278
+ select(current || tabs[0], false);
279
+ });
280
+ }
281
+
238
282
  /* ---------- layer diagram ---------- */
239
283
 
240
284
  function initLayers() {
@@ -261,6 +305,7 @@
261
305
  initCopy();
262
306
  initCellCopy();
263
307
  initFilters();
308
+ initTabs();
264
309
  initLayers();
265
310
  }
266
311
 
@@ -341,6 +341,74 @@ main { padding-bottom: 6rem; }
341
341
  border-color: var(--pass);
342
342
  }
343
343
 
344
+ /* ---------- quick start (lobby) ----------
345
+ Two steps: set up once, then pick the work. Left-aligned inside the centred
346
+ lobby, because everything in here is read line by line. */
347
+
348
+ .qs { text-align: left; margin-top: 1.8rem; }
349
+
350
+ .qs-step + .qs-step { margin-top: 2.6rem; }
351
+
352
+ .qs-title {
353
+ display: flex;
354
+ align-items: center;
355
+ gap: 0.65rem;
356
+ font-size: 1.16rem;
357
+ font-weight: 640;
358
+ margin: 0 0 0.35rem;
359
+ }
360
+
361
+ .qs-num {
362
+ flex: none;
363
+ width: 1.6rem;
364
+ height: 1.6rem;
365
+ display: grid;
366
+ place-items: center;
367
+ border-radius: 50%;
368
+ background: var(--accent-soft);
369
+ border: 1px solid var(--accent-line);
370
+ color: var(--accent);
371
+ font-family: var(--mono);
372
+ font-size: 0.8rem;
373
+ }
374
+
375
+ .qs-why { color: var(--text-dim); font-size: 0.95rem; margin-bottom: 0.9rem; }
376
+
377
+ /* ---------- tabs ---------- */
378
+
379
+ .tablist {
380
+ display: flex;
381
+ flex-wrap: wrap;
382
+ gap: 0.4rem;
383
+ margin-bottom: 0.9rem;
384
+ }
385
+
386
+ .tab {
387
+ font: 560 0.9rem var(--sans);
388
+ color: var(--text-dim);
389
+ background: var(--surface);
390
+ border: 1px solid var(--line);
391
+ border-radius: 99px;
392
+ padding: 0.4rem 1rem;
393
+ cursor: pointer;
394
+ transition: all 0.16s var(--ease);
395
+ }
396
+ .tab:hover { color: var(--text); border-color: var(--accent-line); }
397
+ .tab[aria-selected="true"] {
398
+ color: var(--accent);
399
+ background: var(--accent-soft);
400
+ border-color: var(--accent-line);
401
+ }
402
+
403
+ .qs-panel[hidden] { display: none; }
404
+ .qs-panel:focus-visible { outline-offset: 4px; }
405
+
406
+ .qs-panel-why { color: var(--text-dim); font-size: 0.92rem; margin-bottom: 0.5rem; }
407
+
408
+ /* These prompts are the call to action too, so their copy control never hides. */
409
+ .qs-panel .cmd .copy { opacity: 1; }
410
+ .qs-panel .cmd pre { white-space: pre-wrap; word-break: break-word; font-size: 0.82rem; }
411
+
344
412
  .lobby-cta { display: flex; justify-content: center; gap: 0.7rem; flex-wrap: wrap; margin-top: 1.6rem; }
345
413
 
346
414
  .lobby-fine {
package/site/index.html CHANGED
@@ -21,14 +21,20 @@
21
21
  self-checked in the same context, then kept as a regression guard.
22
22
  </p>
23
23
  <p class="lobby-sub">
24
- Set up, then work a real ticket. Paste this into your agent:
24
+ Set up once, then work a real ticket.
25
25
  </p>
26
26
 
27
- <div class="cmd cmd-hero" id="prompt"><pre><code>Task: set me up with the MetaMask skills + recipe workflow, then use it on real
28
- work. Follow these steps in order. When a command fails, its message names the
29
- exact next command — run that instead of improvising.
27
+ <section class="qs">
30
28
 
31
- PART 1 — SET UP
29
+ <div class="qs-step">
30
+ <h2 class="qs-title"><span class="qs-num" aria-hidden="true">1</span>Set up</h2>
31
+ <p class="qs-why">
32
+ One time per machine. Paste this into your agent inside a MetaMask checkout.
33
+ </p>
34
+
35
+ <div class="cmd cmd-hero" id="prompt"><pre><code>Task: set me up for recipe-backed work on MetaMask. Follow these steps in order.
36
+ When a command fails, its message names the exact next command — run that
37
+ instead of improvising.
32
38
 
33
39
  1. Install the harness: `npm i -g @deeeed/metamask-harness@latest`, then
34
40
  `mm-harness --version` to confirm it resolved.
@@ -74,35 +80,89 @@ PART 1 — SET UP
74
80
  metamask-extension, metamask-mobile, and core. Use `--maturity experimental`
75
81
  when listing: the default only prints stable skills, so the recipe skills
76
82
  below do not appear without it.
77
- 8. Add the skill that drives recipe-backed work:
78
- `yarn skills --include agentic/recipe-cook --save`
79
- It installs as `mms-recipe-cook`. It comes from the `Consensys/skills` clone
80
- in step 7 and is marked experimental, which is why the explicit `--include`
83
+ 8. Add the skills that drive recipe-backed work:
84
+ `yarn skills --include agentic/recipe-cook,agentic/recipe-quality,agentic/recipe-pr-qa-review --save`
85
+ They install as `mms-recipe-cook`, `mms-recipe-quality`, and
86
+ `mms-recipe-pr-qa-review`. They come from the `Consensys/skills` clone in
87
+ step 7 and are marked experimental, which is why the explicit `--include`
81
88
  is required. If `yarn metamask-skills describe agentic/recipe-cook` cannot
82
89
  find it, check `CONSENSYS_SKILLS_DIR` points at that clone, then stop and
83
90
  tell me. Do not substitute a different skill.
91
+ 9. Run `mm-harness actions`. That list is the ONLY set of capabilities you may
92
+ use, now and in every later session — never invent an action or a flag. Then
93
+ stop and tell me setup is done.
94
+
95
+ Not optional, from here on: evidence comes only from executed actions — never
96
+ fabricate a result or edit state to manufacture one. If the harness reports a
97
+ capability as unsupported, stop and tell me rather than working around it. If a
98
+ step fails twice after following its error's instructions, stop and show me the
99
+ exact error.</code></pre></div>
100
+ </div>
101
+
102
+ <div class="qs-step">
103
+ <h2 class="qs-title"><span class="qs-num" aria-hidden="true">2</span>Do real work</h2>
104
+ <p class="qs-why">
105
+ Pick what you are doing. Each one runs the skill that owns that flow, and leaves a
106
+ rerunnable recipe behind as the proof.
107
+ </p>
108
+
109
+ <div class="tabs">
110
+ <div class="tablist" role="tablist" aria-label="Choose what to do">
111
+ <button type="button" role="tab" id="tab-review" class="tab" aria-controls="panel-review" aria-selected="true" tabindex="0">Review a PR</button>
112
+ <button type="button" role="tab" id="tab-fix" class="tab" aria-controls="panel-fix" aria-selected="false" tabindex="-1">Fix a bug</button>
113
+ <button type="button" role="tab" id="tab-build" class="tab" aria-controls="panel-build" aria-selected="false" tabindex="-1">Build a feature</button>
114
+ </div>
115
+
116
+ <div class="qs-panel" role="tabpanel" id="panel-review" aria-labelledby="tab-review" tabindex="0">
117
+ <p class="qs-panel-why">
118
+ <code>mms-recipe-pr-qa-review</code> — read-only. It freezes the acceptance criteria
119
+ before it touches the runtime, validates each one on a real build, and returns a report.
120
+ It never edits product code and never posts to GitHub. The PR must be in the same
121
+ product as your checkout.
122
+ </p>
123
+ <div class="cmd"><pre><code>/mms-recipe-pr-qa-review https://github.com/MetaMask/metamask-mobile/pull/&lt;number&gt;
124
+
125
+ Extract the acceptance criteria from the PR yourself and freeze them before any
126
+ runtime work. Validate each one against the running build, then give me the
127
+ per-AC verdict table, the overall verdict, and the artifact paths. Do not post
128
+ anything on the PR.</code></pre></div>
129
+ </div>
130
+
131
+ <div class="qs-panel" role="tabpanel" id="panel-fix" aria-labelledby="tab-fix" tabindex="0" hidden>
132
+ <p class="qs-panel-why">
133
+ <code>mms-recipe-cook</code>, classified as <code>fix-bug</code>. It lists the
134
+ compatible checklist templates and stops for your pick, then works on a local task
135
+ branch. No product edits until the bug is proven to reproduce.
136
+ </p>
137
+ <div class="cmd"><pre><code>/mms-recipe-cook
138
+
139
+ Fix this in the checkout I started you in — treat it as fix-bug:
140
+ &lt;paste the ticket, or the broken behaviour and how to reproduce it&gt;
141
+
142
+ Reproduce it on the real build first and keep that failing run as the baseline.
143
+ Then fix it and re-run the same recipe until it passes. Show me the recipe
144
+ status, the per-node results, and the artifact paths.</code></pre></div>
145
+ </div>
146
+
147
+ <div class="qs-panel" role="tabpanel" id="panel-build" aria-labelledby="tab-build" tabindex="0" hidden>
148
+ <p class="qs-panel-why">
149
+ <code>mms-recipe-cook</code>, classified as <code>dev</code>. Same flow: it lists the
150
+ compatible templates, stops for your pick, and keeps the diff uncommitted until you
151
+ ask it to package the PR.
152
+ </p>
153
+ <div class="cmd"><pre><code>/mms-recipe-cook
154
+
155
+ Build this in the checkout I started you in — treat it as dev:
156
+ &lt;paste the ticket, or the change you want&gt;
157
+
158
+ Plan the proof before you edit anything, implement it, then prove it with a
159
+ recipe that drives the real build. Show me the recipe status, the per-node
160
+ results, and the artifact paths.</code></pre></div>
161
+ </div>
162
+ </div>
163
+ </div>
84
164
 
85
- PART 2 — DO REAL WORK
86
-
87
- 9. Discover before acting: run `mm-harness actions`. That list is the ONLY set
88
- of capabilities you may use, now and in every later session. Never invent an
89
- action or a flag. If you need something not listed, stop and tell me.
90
- 10. Ask me for a ticket or a bug to work on. Then run `/mms-recipe-cook` and
91
- follow it. It classifies the task as dev, fix-bug, or review-pr, lists the
92
- compatible checklist templates, and stops for my choice — do not choose for
93
- me, and do not create a second task if one is already materialized.
94
- 11. Work the checklist it gives you. The recipe it runs is the proof: it drives
95
- the real build, asserts, and writes the evidence. Show me the status from
96
- summary.json, the per-node results from trace.json, and the artifact paths.
97
- 12. Re-run that recipe without starting a new session and confirm it still
98
- passes. Checking your own work in the context you did it in is the point;
99
- replay it after every change you make from here on.
100
-
101
- Not optional: evidence comes only from executed actions — never fabricate a
102
- result or edit state to manufacture one. If the harness reports a capability as
103
- unsupported, stop and tell me rather than working around it. If a step fails
104
- twice after following its error's instructions, stop and show me the exact
105
- error.</code></pre></div>
165
+ </section>
106
166
 
107
167
  <div class="lobby-cta">
108
168
  <a class="btn btn-primary" href="how-it-works.html">How it works →</a>