@deeeed/metamask-harness 0.34.2 → 0.34.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/adapters.js +5 -2
  3. package/dist/recipe-security.js +1 -0
  4. package/library/actions/core/perps/_controller.mjs +75 -23
  5. package/library/actions/core/perps/read_account.mjs +6 -12
  6. package/library/actions/core/perps/read_snapshot.mjs +66 -0
  7. package/library/actions/extension/performance/_navigation-memory.mjs +372 -0
  8. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +18 -0
  9. package/library/actions/extension/performance/measure_detached_dom.mjs +18 -0
  10. package/library/actions/extension/performance/measure_navigation_memory.mjs +18 -0
  11. package/library/actions/mobile/app/network-control.mjs +24 -12
  12. package/library/manifests/core.action-manifest.json +55 -0
  13. package/library/manifests/extension.action-manifest.json +100 -0
  14. package/library/recipes/core/perps/snapshot.recipe.json +25 -0
  15. package/library/recipes/extension/performance/navigation-memory.recipe.json +175 -0
  16. package/package.json +1 -1
  17. package/scripts/site-contrast.mjs +39 -3
  18. package/site/architecture.html +22 -16
  19. package/site/assets/progress.mjs +1 -1
  20. package/site/assets/style.css +113 -1
  21. package/site/cheatsheet.html +14 -12
  22. package/site/how-it-works.html +693 -0
  23. package/site/index.html +70 -640
  24. package/site/perps.html +7 -6
  25. package/site/recipes.html +23 -17
  26. package/site/reviewers.html +7 -6
  27. package/site/tutorials/index.html +7 -6
  28. package/site/tutorials/v1.html +12 -11
  29. package/site/tutorials/v2.html +7 -6
  30. package/site/tutorials/v3.html +15 -11
  31. package/site/tutorials/v4.html +6 -5
  32. package/site/tutorials/v5.html +6 -5
  33. package/site/tutorials/v6.html +6 -5
  34. package/site/tutorials/v7.html +6 -5
@@ -0,0 +1,18 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import {
3
+ compareIdleNavigation,
4
+ idleControlBound,
5
+ navigationMemoryOptions,
6
+ } from './_navigation-memory.mjs';
7
+
8
+ runAdapter((input) =>
9
+ withExtensionPage(input, async (page) => ({
10
+ action: input.action,
11
+ status: 'pass',
12
+ ...(await compareIdleNavigation(
13
+ page,
14
+ navigationMemoryOptions(input.node, 'rounds', 8),
15
+ idleControlBound(input.node),
16
+ )),
17
+ })),
18
+ );
@@ -0,0 +1,18 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import {
3
+ detachedDomBound,
4
+ measureDetachedDom,
5
+ navigationMemoryOptions,
6
+ } from './_navigation-memory.mjs';
7
+
8
+ runAdapter((input) =>
9
+ withExtensionPage(input, async (page) => ({
10
+ action: input.action,
11
+ status: 'pass',
12
+ ...(await measureDetachedDom(
13
+ page,
14
+ navigationMemoryOptions(input.node, 'cycles', 5),
15
+ detachedDomBound(input.node),
16
+ )),
17
+ })),
18
+ );
@@ -0,0 +1,18 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import {
3
+ measureNavigationSlope,
4
+ navigationMemoryOptions,
5
+ navigationSlopeBounds,
6
+ } from './_navigation-memory.mjs';
7
+
8
+ runAdapter((input) =>
9
+ withExtensionPage(input, async (page) => ({
10
+ action: input.action,
11
+ status: 'pass',
12
+ ...(await measureNavigationSlope(
13
+ page,
14
+ navigationMemoryOptions(input.node),
15
+ navigationSlopeBounds(input.node),
16
+ )),
17
+ })),
18
+ );
@@ -42,6 +42,12 @@ async function defaultRun(file, args) {
42
42
  return execFileAsync(file, args, { timeout: 15_000, encoding: 'utf8' });
43
43
  }
44
44
 
45
+ function isAppFocused(output, appId) {
46
+ return String(output ?? '')
47
+ .split('\n')
48
+ .some((line) => line.includes('mCurrentFocus=') && line.includes(appId));
49
+ }
50
+
45
51
  export async function setMobileNetworkState(input, deps = {}) {
46
52
  const state = String(input.node?.state ?? '').toLowerCase();
47
53
  if (state !== 'offline' && state !== 'online') {
@@ -87,19 +93,26 @@ export async function setMobileNetworkState(input, deps = {}) {
87
93
  `app.network requested ${state}, but Android airplane_mode_on was ${JSON.stringify(observed)} instead of ${expected}.`,
88
94
  );
89
95
  }
90
-
91
-
92
- await run(adb, [
96
+ const focusBefore = await run(adb, [
93
97
  '-s',
94
98
  serial,
95
99
  'shell',
96
- 'monkey',
97
- '-p',
98
- appId,
99
- '-c',
100
- 'android.intent.category.LAUNCHER',
101
- '1',
100
+ 'dumpsys',
101
+ 'window',
102
102
  ]);
103
+ if (!isAppFocused(focusBefore.stdout, appId)) {
104
+ await run(adb, [
105
+ '-s',
106
+ serial,
107
+ 'shell',
108
+ 'monkey',
109
+ '-p',
110
+ appId,
111
+ '-c',
112
+ 'android.intent.category.LAUNCHER',
113
+ '1',
114
+ ]);
115
+ }
103
116
 
104
117
  const fallbackSettleMs = state === 'offline' ? 3000 : 10000;
105
118
  const settleMs = Number(input.node?.settle_ms ?? fallbackSettleMs);
@@ -113,10 +126,9 @@ export async function setMobileNetworkState(input, deps = {}) {
113
126
  serial,
114
127
  'shell',
115
128
  'dumpsys',
116
- 'activity',
117
- 'activities',
129
+ 'window',
118
130
  ]);
119
- if (!String(foreground.stdout ?? '').includes(appId)) {
131
+ if (!isAppFocused(foreground.stdout, appId)) {
120
132
  throw new Error(
121
133
  `app.network changed Android networking but ${appId} was not foregrounded afterward.`,
122
134
  );
@@ -728,6 +728,61 @@
728
728
  ],
729
729
  "execution_capabilities": []
730
730
  },
731
+ "metamask.perps.read_snapshot": {
732
+ "description": "Read global market and account snapshots through the public PerpsController API used by wallet clients.",
733
+ "schema": {
734
+ "type": "object",
735
+ "properties": {
736
+ "scope": {
737
+ "type": "string",
738
+ "enum": [
739
+ "markets",
740
+ "account",
741
+ "all"
742
+ ],
743
+ "default": "all",
744
+ "description": "Select the public controller snapshot calls to execute."
745
+ },
746
+ "account": {
747
+ "type": "string",
748
+ "description": "Wallet fixture account name or EVM address; defaults to dev1."
749
+ },
750
+ "account_name": {
751
+ "type": "string",
752
+ "description": "Alias for a wallet fixture account name."
753
+ },
754
+ "network": {
755
+ "type": "string",
756
+ "enum": [
757
+ "testnet",
758
+ "mainnet"
759
+ ],
760
+ "default": "testnet"
761
+ },
762
+ "terminal_market_data_url": {
763
+ "type": "string",
764
+ "description": "Optional full URL for the legacy Terminal market-data endpoint."
765
+ },
766
+ "terminal_global_snapshot_url": {
767
+ "type": "string",
768
+ "description": "Optional full URL for the Terminal schema-v2 global snapshot endpoint."
769
+ },
770
+ "timeout_ms": {
771
+ "type": "number"
772
+ }
773
+ },
774
+ "additionalProperties": false
775
+ },
776
+ "examples": [
777
+ {
778
+ "action": "metamask.perps.read_snapshot",
779
+ "scope": "all",
780
+ "intent": "Read coherent Perps snapshots through the public controller API",
781
+ "next": "done"
782
+ }
783
+ ],
784
+ "execution_capabilities": []
785
+ },
731
786
  "metamask.perps.place_order": {
732
787
  "description": "core Place a real Perps order on HyperLiquid testnet by driving the headless perps controller placeOrder() through the full signing/provider path. Supports market (default), resting limit orders (order_type=limit with price/offset_pct), and resting trigger placements (stop_market | stop_limit | take_profit_market | take_profit_limit with trigger_price/trigger_offset_pct), plus reduce_only and attached/partial TP/SL (take_profit_price/take_profit_size, stop_loss_price/stop_loss_size, tpsl_linkage). Venue is selected with network (testnet default; mainnet signs with REAL funds and also requires CORE_PERPS_ALLOW_MAINNET_WRITES=1). Plain limit orders accept time_in_force GTC | ALO (post-only).",
733
788
  "schema": {
@@ -1221,6 +1221,106 @@
1221
1221
  ],
1222
1222
  "execution_capabilities": []
1223
1223
  },
1224
+ "metamask.performance.measure_navigation_memory": {
1225
+ "description": "Measure retained DOM nodes and listeners across repeated Extension navigation after forced garbage collection, using least-squares slopes and separate declared bounds.",
1226
+ "schema": {
1227
+ "type": "object",
1228
+ "properties": {
1229
+ "screen_hash": { "type": "string" },
1230
+ "screen_test_id": { "type": "string" },
1231
+ "return_hash": { "type": "string", "default": "#/" },
1232
+ "cycles": { "type": "integer", "minimum": 2, "default": 8 },
1233
+ "max_nodes_per_cycle": { "type": "number", "default": 2 },
1234
+ "max_listeners_per_cycle": { "type": "number", "default": 4 },
1235
+ "settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
1236
+ "gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
1237
+ "screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
1238
+ "timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
1239
+ },
1240
+ "required": ["screen_hash", "screen_test_id"],
1241
+ "additionalProperties": false
1242
+ },
1243
+ "examples": [
1244
+ {
1245
+ "action": "metamask.performance.measure_navigation_memory",
1246
+ "screen_hash": "#/settings",
1247
+ "screen_test_id": "settings-tab-bar-grouped",
1248
+ "return_hash": "#/",
1249
+ "cycles": 8,
1250
+ "max_nodes_per_cycle": 2,
1251
+ "max_listeners_per_cycle": 4,
1252
+ "intent": "Measure post-GC retained-memory slopes across repeated navigation.",
1253
+ "next": "done"
1254
+ }
1255
+ ],
1256
+ "execution_capabilities": []
1257
+ },
1258
+ "metamask.performance.measure_detached_dom": {
1259
+ "description": "Compare retained detached-DOM counts before and after repeated Extension navigation, with forced garbage collection before each heap snapshot.",
1260
+ "schema": {
1261
+ "type": "object",
1262
+ "properties": {
1263
+ "screen_hash": { "type": "string" },
1264
+ "screen_test_id": { "type": "string" },
1265
+ "return_hash": { "type": "string", "default": "#/" },
1266
+ "cycles": { "type": "integer", "minimum": 2, "default": 5 },
1267
+ "max_detached_growth": { "type": "number", "default": 50 },
1268
+ "settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
1269
+ "gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
1270
+ "screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
1271
+ "timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
1272
+ },
1273
+ "required": ["screen_hash", "screen_test_id"],
1274
+ "additionalProperties": false
1275
+ },
1276
+ "examples": [
1277
+ {
1278
+ "action": "metamask.performance.measure_detached_dom",
1279
+ "screen_hash": "#/settings",
1280
+ "screen_test_id": "settings-tab-bar-grouped",
1281
+ "return_hash": "#/",
1282
+ "cycles": 5,
1283
+ "max_detached_growth": 50,
1284
+ "intent": "Measure detached DOM retained after navigation and forced garbage collection.",
1285
+ "next": "done"
1286
+ }
1287
+ ],
1288
+ "execution_capabilities": []
1289
+ },
1290
+ "metamask.performance.compare_idle_navigation_memory": {
1291
+ "description": "Compare equal-duration idle and navigating Extension arms to isolate navigation-attributable listener growth after forced garbage collection.",
1292
+ "schema": {
1293
+ "type": "object",
1294
+ "properties": {
1295
+ "screen_hash": { "type": "string" },
1296
+ "screen_test_id": { "type": "string" },
1297
+ "return_hash": { "type": "string", "default": "#/" },
1298
+ "rounds": { "type": "integer", "minimum": 2, "default": 8 },
1299
+ "round_duration_ms": { "type": "integer", "minimum": 1, "default": 10000 },
1300
+ "max_attributable_listeners_per_cycle": { "type": "number", "default": 2 },
1301
+ "settle_ms": { "type": "integer", "minimum": 0, "default": 1500 },
1302
+ "gc_settle_ms": { "type": "integer", "minimum": 0, "default": 300 },
1303
+ "screen_timeout_ms": { "type": "integer", "minimum": 1, "default": 15000 },
1304
+ "timeout_ms": { "type": "integer", "minimum": 1, "default": 400000 }
1305
+ },
1306
+ "required": ["screen_hash", "screen_test_id"],
1307
+ "additionalProperties": false
1308
+ },
1309
+ "examples": [
1310
+ {
1311
+ "action": "metamask.performance.compare_idle_navigation_memory",
1312
+ "screen_hash": "#/settings",
1313
+ "screen_test_id": "settings-tab-bar-grouped",
1314
+ "return_hash": "#/",
1315
+ "rounds": 8,
1316
+ "round_duration_ms": 10000,
1317
+ "max_attributable_listeners_per_cycle": 2,
1318
+ "intent": "Separate navigation-attributable listener growth from equal-duration idle growth.",
1319
+ "next": "done"
1320
+ }
1321
+ ],
1322
+ "execution_capabilities": []
1323
+ },
1224
1324
  "metamask.perps.read_positions": {
1225
1325
  "description": "extension Read live Perps positions; without a selector, return all live positions.",
1226
1326
  "schema": {
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
3
+ "title": "MetaMask core Perps snapshots",
4
+ "description": "Reads global market and account snapshots through the public PerpsController API used by wallet clients. Configure CORE_PERPS_TERMINAL_GLOBAL_SNAPSHOT_URL to exercise a Terminal schema-v2 endpoint.",
5
+ "workflow": {
6
+ "entry": "status",
7
+ "nodes": {
8
+ "status": {
9
+ "action": "app.status",
10
+ "next": "read_snapshot",
11
+ "intent": "Resolve the Core checkout and report headless compatibility"
12
+ },
13
+ "read_snapshot": {
14
+ "action": "metamask.perps.read_snapshot",
15
+ "scope": "all",
16
+ "next": "done",
17
+ "intent": "Read coherent market and account snapshots through public controller methods"
18
+ },
19
+ "done": {
20
+ "action": "end",
21
+ "status": "pass"
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,175 @@
1
+ {
2
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
3
+ "title": "Extension navigation retained-memory audit",
4
+ "description": "Measures post-GC retained nodes and listeners across eight representative Extension screens, verifies detached DOM does not grow, and separates navigation-attributable listener growth from an equal-duration idle control.",
5
+ "proofTargets": [
6
+ {
7
+ "id": "screen-slopes",
8
+ "claim": "Eight representative Extension screens remain within separate post-GC retained-node and listener growth bounds during navigation churn."
9
+ },
10
+ {
11
+ "id": "detached-dom",
12
+ "claim": "Detached DOM retained after forced garbage collection does not grow beyond the declared bound."
13
+ },
14
+ {
15
+ "id": "idle-control",
16
+ "claim": "Navigation-attributable listener growth remains within its declared bound after subtracting an equal-duration idle control."
17
+ }
18
+ ],
19
+ "workflow": {
20
+ "entry": "require-cdp",
21
+ "nodes": {
22
+ "require-cdp": {
23
+ "action": "cdp.target",
24
+ "required": true,
25
+ "require_reachable": true,
26
+ "intent": "Confirm the live Extension CDP target is reachable before measuring retained memory.",
27
+ "next": "ensure-unlocked"
28
+ },
29
+ "ensure-unlocked": {
30
+ "action": "metamask.wallet.ensure_unlocked",
31
+ "intent": "Ensure the representative screens are reachable from an unlocked wallet.",
32
+ "next": "market-detail"
33
+ },
34
+ "market-detail": {
35
+ "action": "metamask.performance.measure_navigation_memory",
36
+ "screen_hash": "#/perps/market/ETH",
37
+ "screen_test_id": "perps-market-detail-page",
38
+ "return_hash": "#/",
39
+ "cycles": 8,
40
+ "timeout_ms": 400000,
41
+ "max_nodes_per_cycle": 2,
42
+ "max_listeners_per_cycle": 4,
43
+ "intent": "Measure the post-GC navigation slope for Perps market detail.",
44
+ "proves": ["screen-slopes"],
45
+ "next": "order-entry"
46
+ },
47
+ "order-entry": {
48
+ "action": "metamask.performance.measure_navigation_memory",
49
+ "screen_hash": "#/perps/trade/ETH?direction=long&mode=new",
50
+ "screen_test_id": "perps-order-entry-page",
51
+ "return_hash": "#/",
52
+ "cycles": 6,
53
+ "timeout_ms": 400000,
54
+ "max_nodes_per_cycle": 2,
55
+ "max_listeners_per_cycle": 4,
56
+ "intent": "Measure the post-GC navigation slope for Perps order entry.",
57
+ "proves": ["screen-slopes"],
58
+ "next": "perps-home"
59
+ },
60
+ "perps-home": {
61
+ "action": "metamask.performance.measure_navigation_memory",
62
+ "screen_hash": "#/perps-home",
63
+ "screen_test_id": "perps-home-page",
64
+ "return_hash": "#/",
65
+ "cycles": 6,
66
+ "timeout_ms": 400000,
67
+ "max_nodes_per_cycle": 2,
68
+ "max_listeners_per_cycle": 4,
69
+ "intent": "Measure the post-GC navigation slope for Perps home.",
70
+ "proves": ["screen-slopes"],
71
+ "next": "market-list"
72
+ },
73
+ "market-list": {
74
+ "action": "metamask.performance.measure_navigation_memory",
75
+ "screen_hash": "#/perps/market-list",
76
+ "screen_test_id": "market-list-view",
77
+ "return_hash": "#/",
78
+ "cycles": 6,
79
+ "timeout_ms": 400000,
80
+ "max_nodes_per_cycle": 2,
81
+ "max_listeners_per_cycle": 4,
82
+ "intent": "Measure the post-GC navigation slope for the Perps market list.",
83
+ "proves": ["screen-slopes"],
84
+ "next": "perps-activity"
85
+ },
86
+ "perps-activity": {
87
+ "action": "metamask.performance.measure_navigation_memory",
88
+ "screen_hash": "#/perps/activity",
89
+ "screen_test_id": "perps-activity-page",
90
+ "return_hash": "#/",
91
+ "cycles": 6,
92
+ "timeout_ms": 400000,
93
+ "max_nodes_per_cycle": 2,
94
+ "max_listeners_per_cycle": 4,
95
+ "intent": "Measure the post-GC navigation slope for Perps activity.",
96
+ "proves": ["screen-slopes"],
97
+ "next": "asset-details"
98
+ },
99
+ "asset-details": {
100
+ "action": "metamask.performance.measure_navigation_memory",
101
+ "screen_hash": "#/asset/0x1/",
102
+ "screen_test_id": "asset-price-chart",
103
+ "return_hash": "#/",
104
+ "cycles": 6,
105
+ "timeout_ms": 400000,
106
+ "max_nodes_per_cycle": 2,
107
+ "max_listeners_per_cycle": 4,
108
+ "intent": "Measure the post-GC navigation slope for asset details.",
109
+ "proves": ["screen-slopes"],
110
+ "next": "swaps-bridge"
111
+ },
112
+ "swaps-bridge": {
113
+ "action": "metamask.performance.measure_navigation_memory",
114
+ "screen_hash": "#/cross-chain/swaps/prepare-bridge-page",
115
+ "screen_test_id": "bridge-source-button",
116
+ "return_hash": "#/",
117
+ "cycles": 6,
118
+ "timeout_ms": 400000,
119
+ "max_nodes_per_cycle": 2,
120
+ "max_listeners_per_cycle": 4,
121
+ "intent": "Measure the post-GC navigation slope for Swaps and Bridge.",
122
+ "proves": ["screen-slopes"],
123
+ "next": "settings-control"
124
+ },
125
+ "settings-control": {
126
+ "action": "metamask.performance.measure_navigation_memory",
127
+ "screen_hash": "#/settings",
128
+ "screen_test_id": "settings-tab-bar-grouped",
129
+ "return_hash": "#/",
130
+ "cycles": 6,
131
+ "timeout_ms": 400000,
132
+ "max_nodes_per_cycle": 2,
133
+ "max_listeners_per_cycle": 4,
134
+ "intent": "Measure a non-Perps control screen with the same retained-memory gate.",
135
+ "proves": ["screen-slopes"],
136
+ "next": "detached-market-detail"
137
+ },
138
+ "detached-market-detail": {
139
+ "action": "metamask.performance.measure_detached_dom",
140
+ "screen_hash": "#/perps/market/ETH",
141
+ "screen_test_id": "perps-market-detail-page",
142
+ "return_hash": "#/",
143
+ "cycles": 5,
144
+ "timeout_ms": 400000,
145
+ "max_detached_growth": 50,
146
+ "intent": "Compare retained detached DOM before and after market-detail navigation churn.",
147
+ "proves": ["detached-dom"],
148
+ "next": "idle-control-market-detail"
149
+ },
150
+ "idle-control-market-detail": {
151
+ "action": "metamask.performance.compare_idle_navigation_memory",
152
+ "screen_hash": "#/perps/market/ETH",
153
+ "screen_test_id": "perps-market-detail-page",
154
+ "return_hash": "#/",
155
+ "rounds": 8,
156
+ "round_duration_ms": 10000,
157
+ "timeout_ms": 400000,
158
+ "max_attributable_listeners_per_cycle": 2,
159
+ "intent": "Subtract equal-duration idle growth from the market-detail navigation slope.",
160
+ "proves": ["idle-control"],
161
+ "next": "return-home"
162
+ },
163
+ "return-home": {
164
+ "action": "ui.navigate",
165
+ "page": "home",
166
+ "intent": "Leave the Extension on a known screen after the audit.",
167
+ "next": "done"
168
+ },
169
+ "done": {
170
+ "action": "end",
171
+ "status": "pass"
172
+ }
173
+ }
174
+ }
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.34.2",
3
+ "version": "0.34.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -351,11 +351,33 @@ async function checkBehavior(browserEndpoint, port, pages) {
351
351
  return window.__siteCopied;
352
352
  })()
353
353
  `);
354
- const expected = 'mm-harness run <recipe> --artifacts-dir ./first-recipe-artifacts';
355
- if (!copied?.includes(expected)) {
356
- throw new Error(`copy prompt omitted "${expected}"`);
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));
358
+ if (missing.length) {
359
+ throw new Error(`copy prompt omitted ${missing.map((m) => `"${m}"`).join(', ')}`);
357
360
  }
358
361
 
362
+ // The landing page is the lobby: one prompt, one way onward, nothing else.
363
+ const lobby = await page.evaluate(String.raw`
364
+ (() => ({
365
+ nav: document.querySelectorAll('.nav a').length,
366
+ steps: document.querySelectorAll('.step[data-step]').length,
367
+ depth: document.querySelector('a[href="how-it-works.html"]') !== null,
368
+ anchor: document.getElementById('prompt') !== null,
369
+ }))()
370
+ `);
371
+ if (lobby.nav || lobby.steps || !lobby.depth || !lobby.anchor) {
372
+ throw new Error(
373
+ `landing page is not a lobby: ${lobby.nav} nav links, ${lobby.steps} steps, ` +
374
+ `depth link ${lobby.depth}, prompt anchor ${lobby.anchor}`,
375
+ );
376
+ }
377
+ });
378
+
379
+ // The walkthrough and its saved progress live one click deeper.
380
+ await withPage(browserEndpoint, `${base}how-it-works.html`, async (page) => {
359
381
  const progress = await page.evaluate(String.raw`
360
382
  (() => {
361
383
  const box = document.querySelector('.step-check');
@@ -426,6 +448,20 @@ async function checkBehavior(browserEndpoint, port, pages) {
426
448
  if (broken.length) {
427
449
  throw new Error(`${relative} has broken internal links: ${broken.join(', ')}`);
428
450
  }
451
+ // Every page past the lobby owes the reader one click back to the prompt.
452
+ if (relative !== 'index.html') {
453
+ const back = await page.evaluate(String.raw`
454
+ (() => {
455
+ const link = document.querySelector('.nav a.nav-cta');
456
+ if (!link) return null;
457
+ const url = new URL(link.getAttribute('href'), location.href);
458
+ return url.pathname + url.hash;
459
+ })()
460
+ `);
461
+ if (back !== '/index.html#prompt') {
462
+ throw new Error(`${relative} has no nav link back to the setup prompt (found ${back})`);
463
+ }
464
+ }
429
465
  for (const width of [834, 390]) {
430
466
  await page.send('Emulation.setDeviceMetricsOverride', {
431
467
  width,
@@ -14,11 +14,12 @@
14
14
  <div class="wrap topbar-inner">
15
15
  <a class="brand" href="index.html">
16
16
  <span class="brand-mark" aria-hidden="true"></span>
17
- <span class="brand-name">mm-harness</span>
17
+ <span class="brand-name">recipes</span>
18
18
  </a>
19
19
  <nav class="nav" aria-label="Main">
20
- <a href="index.html">Start Here</a>
21
- <a href="recipes.html">Recipes</a>
20
+ <a class="nav-cta" href="index.html#prompt">Quick start</a>
21
+ <a href="how-it-works.html">How it works</a>
22
+ <a href="recipes.html">Recipe anatomy</a>
22
23
  <a href="perps.html">Perps</a>
23
24
  <a href="cheatsheet.html">Cheatsheet</a>
24
25
  <a href="architecture.html" aria-current="page">Architecture</a>
@@ -30,11 +31,12 @@
30
31
 
31
32
  <main>
32
33
  <section class="wrap hero" style="padding-bottom:1rem">
33
- <span class="eyebrow">The stack</span>
34
+ <span class="eyebrow">The stack behind a recipe</span>
34
35
  <h1>How the pieces fit together</h1>
35
36
  <p class="lede">
36
- Four moving parts, two of which people routinely confuse. What each layer is, where it lives, which
37
- workflow you are in, and the mistakes that cost newcomers their first afternoon.
37
+ The recipe is the unit of trust; the rest of the stack runs it. Four moving parts, two of which
38
+ people routinely confuse. What each layer is, where it lives, which workflow you are in, and the
39
+ mistakes that cost newcomers their first afternoon.
38
40
  </p>
39
41
  </section>
40
42
 
@@ -45,8 +47,9 @@
45
47
  <span class="k">A recipe proves a task with actions.</span> Recipes are JSON graphs of typed
46
48
  actions; <code>mm-harness</code> executes them against a real app and produces an evidence bundle
47
49
  a reviewer can trust. <strong>Skills</strong> teach an agent a workflow — author a recipe, review
48
- a PR, validate a release. <strong>Recipe libraries</strong> hold each team's recipes and domain
49
- actions. Compose in that order, against the product repo under test.
50
+ a PR, validate a release and are the layer you work through; they call the harness, which runs
51
+ the recipe. <strong>Recipe libraries</strong> hold each team's recipes and domain actions.
52
+ Compose in that order, against the product repo under test.
50
53
  </p>
51
54
  </div>
52
55
  </section>
@@ -67,9 +70,12 @@
67
70
  release — turning "figure it out" into a procedure with gates you can steer.
68
71
  </p>
69
72
  <p>
70
- Skills live in the internal <code>Consensys/skills</code> repo, installed per checkout with that
71
- repo's tooling. <strong>You do not need skills to run a recipe</strong>, only to have an agent
72
- follow a proven workflow instead of improvising one.
73
+ Skills come from the public <code>MetaMask/skills</code> repo plus the internal
74
+ <code>Consensys/skills</code> overlay, which needs access to that org; private skills override
75
+ public ones on a name collision. Clone each, point <code>METAMASK_SKILLS_DIR</code> and
76
+ <code>CONSENSYS_SKILLS_DIR</code> at them, then install per checkout with
77
+ <code>yarn skills</code>. <strong>You do not need skills to run a recipe</strong>, only to have
78
+ an agent follow a proven workflow instead of improvising one.
73
79
  <a href="tutorials/v3.html">V3 walks the install.</a>
74
80
  </p>
75
81
  </div>
@@ -129,8 +135,8 @@
129
135
  </p>
130
136
  <p style="margin-bottom:0">
131
137
  They differ, and the harness says so rather than pretending otherwise: Core is headless with
132
- nothing to launch and no UI actions; Mobile carries video capture; Extension is screenshots
133
- only.
138
+ nothing to launch and no UI actions; Extension and iOS can record full-run video; Android
139
+ replay video is not implemented yet, so use screenshot evidence there.
134
140
  </p>
135
141
  </div>
136
142
  </div>
@@ -466,7 +472,7 @@
466
472
 
467
473
  <hr class="sep">
468
474
  <div class="btn-row">
469
- <a class="btn btn-primary" href="index.html#steps">Do the walkthrough →</a>
475
+ <a class="btn btn-primary" href="how-it-works.html#steps">Do the walkthrough →</a>
470
476
  <a class="btn btn-ghost" href="reviewers.html">Read an evidence bundle</a>
471
477
  </div>
472
478
  </section>
@@ -474,8 +480,8 @@
474
480
 
475
481
  <footer class="footer">
476
482
  <div class="wrap">
477
- <p>Internal getting-started guide for the MetaMask agentic coding workflow. Not official MetaMask product documentation.</p>
478
- <p>Verified against mm-harness 0.33+.</p>
483
+ <p>Internal getting-started guide for proving MetaMask changes with recipes. Not official MetaMask product documentation.</p>
484
+ <p>Sample output moves between releases; trust your terminal over this page.</p>
479
485
  </div>
480
486
  </footer>
481
487
 
@@ -3,7 +3,7 @@
3
3
  * buttons, platform filters, 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
- * Pages that share a namespace share their state (Start Here and the V1
6
+ * Pages that share a namespace share their state (the How it works walkthrough and the V1
7
7
  * tutorial are deliberately the same checklist).
8
8
  */
9
9
  (function () {