@deeeed/metamask-harness 0.47.0 → 0.47.2
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 +21 -0
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +15 -0
- package/dist/commands/tutorial.js +4 -4
- package/dist/mm-harness-cli.js +3 -3
- package/library/actions/mobile/perps/perps.mjs +41 -12
- package/library/actions/mobile/perps/set_market_favorite.mjs +0 -11
- package/library/actions/mobile/platform/bridge.mjs +6 -1
- package/library/actions/mobile/ui/native-navigation.mjs +9 -2
- package/library/actions/mobile/ui/navigate.mjs +13 -4
- package/library/manifests/mobile.action-manifest.json +1 -2
- package/package.json +2 -2
- package/site/assets/help-recipes.json +6 -1
- package/site/architecture.html +0 -497
- package/site/assets/help-recipes.mjs +0 -25
- package/site/assets/metamask-fox.svg +0 -24
- package/site/assets/progress.mjs +0 -325
- package/site/assets/style.css +0 -1066
- package/site/cheatsheet.html +0 -307
- package/site/ecosystem.html +0 -162
- package/site/how-it-works.html +0 -697
- package/site/index.html +0 -236
- package/site/perps-advanced-orders-qa.html +0 -96
- package/site/perps.html +0 -265
- package/site/recipes.html +0 -430
- package/site/reviewers.html +0 -375
- package/site/tutorials/index.html +0 -181
- package/site/tutorials/v1.html +0 -216
- package/site/tutorials/v2.html +0 -207
- package/site/tutorials/v3.html +0 -258
- package/site/tutorials/v4.html +0 -196
- package/site/tutorials/v5.html +0 -164
- package/site/tutorials/v6.html +0 -166
- package/site/tutorials/v7.html +0 -185
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.47.2 - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Teach recipe and action discovery, bounded setup probes, source-based diagnosis, and honest blocked-proof reporting through version-matched CLI help.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Flush machine-readable command output before exit so large JSON documents remain complete when piped to another process.
|
|
14
|
+
- Keep `metamask.perps.set_market_favorite` in the current mode while waiting for a temporarily absent favorite control.
|
|
15
|
+
- Preserve the current Mobile Perps mode when `ui.navigate` omits `mode`, including opaque Android navigation.
|
|
16
|
+
- Prefer an exact visible Mobile Perps market row or recently-viewed tile before falling back to search.
|
|
17
|
+
- Preserve Mobile Perps teardown navigation when embedded and fallback bridges use different wording for a missing scrollable target.
|
|
18
|
+
|
|
19
|
+
## 0.47.1 - 2026-09-04
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Open the tutorial on the protected MetaMask GitHub Pages site instead of the public unpkg CDN.
|
|
24
|
+
- Stop shipping the static HTML site in the public npm package; retain only the recipe-help JSON required by the CLI.
|
|
25
|
+
|
|
5
26
|
## 0.47.0 - 2026-09-04
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -18,6 +18,10 @@ const BRIDGE_ERROR_CODES = {
|
|
|
18
18
|
METRO_UNREACHABLE: 'METRO_UNREACHABLE',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const BRIDGE_RESULT_ERROR_CODES = {
|
|
22
|
+
SCROLLABLE_NOT_FOUND: 'SCROLLABLE_NOT_FOUND',
|
|
23
|
+
};
|
|
24
|
+
|
|
21
25
|
// Process exit code the bridge returns per failure code so a caller that only
|
|
22
26
|
// sees the child's exit status (no stderr) can still recover the code. Kept
|
|
23
27
|
// clear of exit 1 (unknown/uncoded) and 2 (usage).
|
|
@@ -78,6 +82,15 @@ function classifyBridgeErrorMessage(message) {
|
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
function classifyBridgeResultError(command, result) {
|
|
86
|
+
if (command !== 'scroll-view' || result?.ok !== false) return null;
|
|
87
|
+
return /^No scrollable(?: found)? near testID=/u.test(
|
|
88
|
+
String(result.error ?? ''),
|
|
89
|
+
)
|
|
90
|
+
? BRIDGE_RESULT_ERROR_CODES.SCROLLABLE_NOT_FOUND
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
// Attach a code to an error at its throw site (source classification).
|
|
82
95
|
function coded(error, code) {
|
|
83
96
|
if (error && typeof error === 'object') error.code = code;
|
|
@@ -99,10 +112,12 @@ function parseErrorMarker(text) {
|
|
|
99
112
|
|
|
100
113
|
module.exports = {
|
|
101
114
|
BRIDGE_ERROR_CODES,
|
|
115
|
+
BRIDGE_RESULT_ERROR_CODES,
|
|
102
116
|
EXIT_CODE_BY_ERROR_CODE,
|
|
103
117
|
ERROR_CODE_BY_EXIT_CODE,
|
|
104
118
|
TEACHING_BY_ERROR_CODE,
|
|
105
119
|
classifyBridgeErrorMessage,
|
|
120
|
+
classifyBridgeResultError,
|
|
106
121
|
coded,
|
|
107
122
|
formatErrorMarker,
|
|
108
123
|
parseErrorMarker,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { parseArgs } from "./parse-args.js";
|
|
3
|
-
const
|
|
4
|
-
function recipeTutorialUrl(
|
|
3
|
+
const PROTECTED_TUTORIAL_URL = "https://glowing-chainsaw-gwzrn8k.pages.github.io/recipes.html#agent-method";
|
|
4
|
+
function recipeTutorialUrl(runMode = process.env.MM_HARNESS_RUN_MODE) {
|
|
5
5
|
if (runMode === "src") {
|
|
6
6
|
return "http://127.0.0.1:8765/site/recipes.html#agent-method";
|
|
7
7
|
}
|
|
8
|
-
return
|
|
8
|
+
return PROTECTED_TUTORIAL_URL;
|
|
9
9
|
}
|
|
10
10
|
function openUrl(url) {
|
|
11
11
|
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
@@ -14,7 +14,7 @@ function openUrl(url) {
|
|
|
14
14
|
}
|
|
15
15
|
function handleTutorial(argv, context) {
|
|
16
16
|
const parsed = parseArgs(argv, "tutorial");
|
|
17
|
-
const url = recipeTutorialUrl(
|
|
17
|
+
const url = recipeTutorialUrl();
|
|
18
18
|
const noOpen = parsed.options.noOpen === true;
|
|
19
19
|
const opened = noOpen ? false : (context.open ?? openUrl)(url);
|
|
20
20
|
const payload = {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -41,12 +41,12 @@ Example:
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: "tutorial",
|
|
44
|
-
summary: "Open the visual recipe tutorial
|
|
44
|
+
summary: "Open the protected visual recipe tutorial.",
|
|
45
45
|
example: "mm-harness tutorial",
|
|
46
46
|
helpText: `mm-harness tutorial [flags]
|
|
47
47
|
|
|
48
48
|
Source checkouts open the local tutorial on port 8765. Installed releases
|
|
49
|
-
open the
|
|
49
|
+
open the protected MetaMask GitHub Pages site.
|
|
50
50
|
|
|
51
51
|
--no-open Print the tutorial URL without opening it
|
|
52
52
|
--json Print version, URL, and open status as JSON
|
|
@@ -830,7 +830,7 @@ for (const command of REAL) {
|
|
|
830
830
|
if (command.name === "tutorial") {
|
|
831
831
|
process.exit(handleTutorial(rawArgv.slice(1), { harnessVersion: pkgVersion }));
|
|
832
832
|
}
|
|
833
|
-
process.
|
|
833
|
+
process.exitCode = await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv));
|
|
834
834
|
});
|
|
835
835
|
}
|
|
836
836
|
const HIDDEN = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import {
|
|
3
|
+
MOBILE_BRIDGE_RESULT_ERROR_CODES,
|
|
3
4
|
bridgeCommand,
|
|
4
5
|
evalAsync,
|
|
5
6
|
isTargetTransition,
|
|
@@ -915,29 +916,50 @@ async function readVisiblePerpsTestIds(input, deadline) {
|
|
|
915
916
|
return new Set(visible.items.map((item) => String(item.test_id ?? '')));
|
|
916
917
|
}
|
|
917
918
|
|
|
918
|
-
async function
|
|
919
|
+
async function waitForPerpsUiTargets(input, testIds, deadline) {
|
|
919
920
|
let lastTestIds = [];
|
|
920
921
|
while (Date.now() < deadline) {
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
922
|
+
const visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
923
|
+
const matchedTestId = testIds.find((testId) =>
|
|
924
|
+
visibleTestIds.has(testId),
|
|
925
|
+
);
|
|
926
|
+
if (matchedTestId) return { matchedTestId, visibleTestIds };
|
|
927
|
+
lastTestIds = [...visibleTestIds].filter(Boolean).slice(0, 20);
|
|
924
928
|
await sleepBeforeDeadline(deadline, 250);
|
|
925
929
|
}
|
|
930
|
+
const targetDescription =
|
|
931
|
+
testIds.length === 1
|
|
932
|
+
? `control ${testIds[0]}`
|
|
933
|
+
: `controls ${testIds.join(' or ')}`;
|
|
926
934
|
throw new Error(
|
|
927
|
-
`Perps
|
|
935
|
+
`Perps ${targetDescription} did not become visible before timeout. Observed: ${lastTestIds.join(', ') || 'none'}.`,
|
|
928
936
|
);
|
|
929
937
|
}
|
|
930
938
|
|
|
939
|
+
async function waitForPerpsUiTarget(input, testId, deadline) {
|
|
940
|
+
const { visibleTestIds } = await waitForPerpsUiTargets(
|
|
941
|
+
input,
|
|
942
|
+
[testId],
|
|
943
|
+
deadline,
|
|
944
|
+
);
|
|
945
|
+
return visibleTestIds;
|
|
946
|
+
}
|
|
947
|
+
|
|
931
948
|
async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
932
949
|
const timeoutMs = Number(input.node?.navigation_timeout_ms ?? 15_000);
|
|
933
950
|
const deadline = deadlineFromTimeout(input, timeoutMs);
|
|
934
951
|
const params = { market: { symbol } };
|
|
935
952
|
const rowTestId = `perps-market-row-item-${symbol}`;
|
|
953
|
+
const recentlyViewedTestId = `perps-recently-viewed-tile-${symbol}`;
|
|
954
|
+
const marketTestIds = [rowTestId, recentlyViewedTestId];
|
|
936
955
|
const searchTestId = 'perps-market-list-search-bar';
|
|
937
956
|
let visibleTestIds = await readVisiblePerpsTestIds(input, deadline);
|
|
957
|
+
let marketTestId = marketTestIds.find((testId) =>
|
|
958
|
+
visibleTestIds.has(testId),
|
|
959
|
+
);
|
|
938
960
|
let usedSearch = false;
|
|
939
961
|
|
|
940
|
-
if (!
|
|
962
|
+
if (!marketTestId) {
|
|
941
963
|
if (!visibleTestIds.has(searchTestId)) {
|
|
942
964
|
if (!visibleTestIds.has('perps-home-search-toggle')) {
|
|
943
965
|
throw new Error(
|
|
@@ -959,17 +981,19 @@ async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
|
959
981
|
searchTestId,
|
|
960
982
|
symbol,
|
|
961
983
|
]);
|
|
962
|
-
|
|
984
|
+
const visibleMarketTarget = await waitForPerpsUiTargets(
|
|
963
985
|
input,
|
|
964
|
-
|
|
986
|
+
marketTestIds,
|
|
965
987
|
deadline,
|
|
966
988
|
);
|
|
989
|
+
visibleTestIds = visibleMarketTarget.visibleTestIds;
|
|
990
|
+
marketTestId = visibleMarketTarget.matchedTestId;
|
|
967
991
|
usedSearch = true;
|
|
968
992
|
}
|
|
969
993
|
|
|
970
994
|
await bridgeCommand(withRemainingDeadline(input, deadline), [
|
|
971
995
|
'press-test-id',
|
|
972
|
-
|
|
996
|
+
marketTestId,
|
|
973
997
|
]);
|
|
974
998
|
const currentRoute = await waitForRoute(
|
|
975
999
|
withRemainingDeadline(input, deadline),
|
|
@@ -983,7 +1007,7 @@ async function selectPerpsMarketFromHome(input, symbol, previousRoute) {
|
|
|
983
1007
|
previousRoute,
|
|
984
1008
|
currentRoute,
|
|
985
1009
|
verifiedRoute: 'PerpsMarketDetails',
|
|
986
|
-
visibleMarketSelection: { usedSearch },
|
|
1010
|
+
visibleMarketSelection: { usedSearch, testId: marketTestId },
|
|
987
1011
|
};
|
|
988
1012
|
}
|
|
989
1013
|
|
|
@@ -1033,7 +1057,12 @@ async function navigateFromMarketDetailsToHome(input, previousRoute) {
|
|
|
1033
1057
|
backTestId = expectedBackTestId;
|
|
1034
1058
|
break;
|
|
1035
1059
|
} catch (error) {
|
|
1036
|
-
if (
|
|
1060
|
+
if (
|
|
1061
|
+
error?.code !==
|
|
1062
|
+
MOBILE_BRIDGE_RESULT_ERROR_CODES.SCROLLABLE_NOT_FOUND
|
|
1063
|
+
) {
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1037
1066
|
}
|
|
1038
1067
|
}
|
|
1039
1068
|
}
|
|
@@ -1079,7 +1108,7 @@ async function navigateFromMarketDetailsToHome(input, previousRoute) {
|
|
|
1079
1108
|
};
|
|
1080
1109
|
}
|
|
1081
1110
|
|
|
1082
|
-
async function navigatePerps(input) {
|
|
1111
|
+
export async function navigatePerps(input) {
|
|
1083
1112
|
const selected = String(
|
|
1084
1113
|
input.node?.target ?? input.node?.destination ?? defaultNavigationTarget(input.node),
|
|
1085
1114
|
).toLowerCase();
|
|
@@ -51,17 +51,6 @@ runAdapter(async (input) => {
|
|
|
51
51
|
throw new Error(`Visible Perps market ${JSON.stringify(subtitle)} does not match ${market}.`);
|
|
52
52
|
}
|
|
53
53
|
let control = favoriteControl(snapshot);
|
|
54
|
-
if (!control && nativeNode(snapshot, 'perps-mode-toggle-lite')?.enabled !== false) {
|
|
55
|
-
try {
|
|
56
|
-
await session.execute('ui.press', {
|
|
57
|
-
test_id: 'perps-mode-toggle-lite',
|
|
58
|
-
timeout_ms: timeoutMs,
|
|
59
|
-
settle: false,
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (!/Selector did not match/iu.test(String(error?.message ?? error))) throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
54
|
const controlDeadline = Date.now() + timeoutMs;
|
|
66
55
|
let lastControls = [];
|
|
67
56
|
while (Date.now() <= controlDeadline && !control) {
|
|
@@ -13,8 +13,10 @@ import { resolveMobileToolPath } from './tool-paths.mjs';
|
|
|
13
13
|
|
|
14
14
|
const {
|
|
15
15
|
BRIDGE_ERROR_CODES,
|
|
16
|
+
BRIDGE_RESULT_ERROR_CODES,
|
|
16
17
|
ERROR_CODE_BY_EXIT_CODE,
|
|
17
18
|
classifyBridgeErrorMessage,
|
|
19
|
+
classifyBridgeResultError,
|
|
18
20
|
coded,
|
|
19
21
|
parseErrorMarker,
|
|
20
22
|
} = bridgeErrors;
|
|
@@ -24,6 +26,7 @@ const { resolvePort } = configModule;
|
|
|
24
26
|
// Re-exported so the TS adapter classifies on the same code constants without a
|
|
25
27
|
// second import path into the cjs bridge-runtime.
|
|
26
28
|
export const MOBILE_BRIDGE_ERROR_CODES = BRIDGE_ERROR_CODES;
|
|
29
|
+
export const MOBILE_BRIDGE_RESULT_ERROR_CODES = BRIDGE_RESULT_ERROR_CODES;
|
|
27
30
|
|
|
28
31
|
const execFileAsync = promisify(execFile);
|
|
29
32
|
const ACTION_DEADLINE_FIELD = '_action_deadline_epoch_ms';
|
|
@@ -536,9 +539,11 @@ export async function bridgeCommand(input, args) {
|
|
|
536
539
|
try {
|
|
537
540
|
const parsed = JSON.parse(result.stdout);
|
|
538
541
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.ok === false) {
|
|
539
|
-
|
|
542
|
+
const error = new Error(
|
|
540
543
|
`Mobile CDP bridge command reported failure for ${redactBridgeArgs(args).join(' ')}: ${redactBridgeOutput(JSON.stringify(parsed), sensitiveBridgeArgs(args))}`,
|
|
541
544
|
);
|
|
545
|
+
const code = classifyBridgeResultError(command, parsed);
|
|
546
|
+
throw code ? coded(error, code) : error;
|
|
542
547
|
}
|
|
543
548
|
return parsed;
|
|
544
549
|
} catch (error) {
|
|
@@ -73,7 +73,9 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
73
73
|
artifactsDir: input.context.artifactsDir,
|
|
74
74
|
env,
|
|
75
75
|
};
|
|
76
|
-
const perpsMode =
|
|
76
|
+
const perpsMode = ['lite', 'pro'].includes(destination.params?.mode)
|
|
77
|
+
? destination.params.mode
|
|
78
|
+
: undefined;
|
|
77
79
|
const press = (testId) => transport.execute('ui.press', {
|
|
78
80
|
test_id: testId,
|
|
79
81
|
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
@@ -542,11 +544,16 @@ async function navigatePerpsMarketList({ identifiers, press, wait, back, timeout
|
|
|
542
544
|
throw new Error(`Opaque Mobile navigation reached ${observed}, but no visible market-list control was available.`);
|
|
543
545
|
}
|
|
544
546
|
|
|
545
|
-
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode
|
|
547
|
+
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode) {
|
|
546
548
|
const deadline = Date.now() + timeoutMs;
|
|
547
549
|
while (Date.now() <= deadline) {
|
|
548
550
|
const ids = await identifiers();
|
|
549
551
|
if (ids.has(IDS.modeLite) || ids.has(IDS.modePro)) {
|
|
552
|
+
if (!perpsMode) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
'Opaque Mobile navigation requires mode=lite or mode=pro when the product requests a Perps mode choice.',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
550
557
|
await press(perpsMode === 'pro' ? IDS.modePro : IDS.modeLite);
|
|
551
558
|
continue;
|
|
552
559
|
}
|
|
@@ -23,8 +23,8 @@ function text(value) {
|
|
|
23
23
|
function pageRoute(node) {
|
|
24
24
|
const page = text(node?.page);
|
|
25
25
|
if (!page) return undefined;
|
|
26
|
-
const mode = text(node?.mode)?.toLowerCase()
|
|
27
|
-
if (!['lite', 'pro'].includes(mode)) {
|
|
26
|
+
const mode = text(node?.mode)?.toLowerCase();
|
|
27
|
+
if (mode && !['lite', 'pro'].includes(mode)) {
|
|
28
28
|
throw new Error('mobile ui.navigate mode must be lite or pro.');
|
|
29
29
|
}
|
|
30
30
|
if (PAGE_ROUTES[page]) {
|
|
@@ -32,13 +32,22 @@ function pageRoute(node) {
|
|
|
32
32
|
return {
|
|
33
33
|
page,
|
|
34
34
|
...route,
|
|
35
|
-
params: page.startsWith('perps')
|
|
35
|
+
params: page.startsWith('perps') && mode
|
|
36
|
+
? { ...route.params, mode }
|
|
37
|
+
: route.params,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
if (page === 'perps-market') {
|
|
39
41
|
const market = text(node.market) ?? text(node.symbol) ?? text(node.params?.market?.symbol);
|
|
40
42
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
41
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
page,
|
|
45
|
+
route: 'PerpsMarketDetails',
|
|
46
|
+
params: {
|
|
47
|
+
market: { symbol: market },
|
|
48
|
+
...(mode ? { mode } : {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
42
51
|
}
|
|
43
52
|
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market-list, perps-market, swap.');
|
|
44
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dist",
|
|
65
65
|
"adapters",
|
|
66
66
|
"library",
|
|
67
|
-
"site",
|
|
67
|
+
"site/assets/help-recipes.json",
|
|
68
68
|
"scripts/completions.sh",
|
|
69
69
|
"scripts/install-completions.sh",
|
|
70
70
|
"scripts/site-contrast.mjs",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"instruction": "Search the installed recipe and action catalogs, inspect exact schemas, and select the closest recipe before writing anything new.",
|
|
27
27
|
"details": [
|
|
28
28
|
"A bundled recipe, including a Core recipe, is strong composable vocabulary, not authority for every future product version.",
|
|
29
|
-
"Prefer an existing recipe, then existing actions, then read-only CDP or controller inspection. Add shared vocabulary only after repeated need is proven."
|
|
29
|
+
"Prefer an existing recipe, then existing actions, then read-only CDP or controller inspection. Add shared vocabulary only after repeated need is proven.",
|
|
30
|
+
"Before declaring a capability unsupported, inspect both catalogs and the closest recipe's composition. A missing dedicated action may already be composed from UI actions. Read the returned schema before filtering fields; do not treat guessed field names as empty capabilities."
|
|
30
31
|
],
|
|
31
32
|
"commands": [
|
|
32
33
|
"mm-harness actions --json",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"instruction": "Probe each unfamiliar action with call or a tiny action-to-end recipe. Use read-only CDP inspection only when declared actions cannot reveal the needed authoring detail, then consolidate the final proof from manifest-declared actions.",
|
|
62
63
|
"details": [
|
|
63
64
|
"Plan after every structural change and preserve the first failing trace.",
|
|
65
|
+
"A plan validates structure only. Probe a reused setup boundary against the current build with a short explicit timeout before a full journey; a bundled journey timeout is not a discovery budget.",
|
|
64
66
|
"A CDP observation can help find the current route or selector; it is not final evidence unless the recipe records an allowed action and assertion for that signal."
|
|
65
67
|
],
|
|
66
68
|
"commands": [
|
|
@@ -84,6 +86,7 @@
|
|
|
84
86
|
"instruction": "Run the complete graph once into an explicit artifact directory, then apply the built-in quality bar before assigning coverage or a verdict.",
|
|
85
87
|
"details": [
|
|
86
88
|
"Coverage: every criterion needs an executable path and assertion; manual, untestable, and environment-dependent gaps stay explicit.",
|
|
89
|
+
"A blocked required runtime claim is incomplete proof. Passing static checks or closing a checklist cannot make that claim validated or fixed.",
|
|
87
90
|
"Graph: no unconditional pass, generic intent, hidden start state, or opaque node that collapses preparation, action, assertion, evidence, and teardown.",
|
|
88
91
|
"Evidence: inspect the recipe resolution, summary, trace, manifest, logs, and actual visual evidence. A filename or passing node is not visual proof.",
|
|
89
92
|
"Flake risk: wait on observable state instead of sleeping, keep device and runtime identity explicit, and never overwrite a prior run's artifacts.",
|
|
@@ -101,6 +104,8 @@
|
|
|
101
104
|
"instruction": "When a shared recipe fails, reproduce once and classify product regression, recipe drift, fixture/runtime drift, harness defect, or live dependency before changing anything.",
|
|
102
105
|
"details": [
|
|
103
106
|
"The graph and evidence contract can be deterministic while the product, provider, account, or network outcome is not. Honest external rejection remains a valid result, not a reason to seek a green retry.",
|
|
107
|
+
"Trace the first failure through the exact action source and test the smallest causal explanation before filing or fixing a harness issue. A selector name does not establish its cause. If a proposed fix outgrows the confirmed cause, preserve it separately and return to the minimal fix.",
|
|
108
|
+
"For blocked runtime proof, retain the target identity, build/app state, and first failing command and error. Reconcile later status snapshots with that evidence; they cannot establish that an earlier device was absent.",
|
|
104
109
|
"For product-version drift, preserve the failed trace, copy the composed recipe into the task, keep working nodes, and repair the smallest owning node against the tested build.",
|
|
105
110
|
"Prove the changed node in isolation, re-plan, then rerun the full task-local recipe and affected callers. Never weaken an assertion or change product code merely to turn stale proof green.",
|
|
106
111
|
"Record the source recipe digest, tested product identity, repair, and fresh evidence. If the defect belongs to reusable harness code or bundled vocabulary, prepare a focused experimental-metamask-harness issue and open it when authorized.",
|