@deeeed/metamask-harness 0.62.0 → 0.64.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 +26 -0
- package/README.md +15 -0
- package/adapters/extension/build-lavamoat.sh +6 -1
- package/adapters/extension/live.sh +1 -1
- package/adapters/extension/start-watch.sh +2 -2
- package/dist/adapters/extension/runtime-decision.js +17 -2
- package/dist/cli-commands.js +1 -1
- package/dist/command-contract.js +1 -0
- package/dist/commands/launch/extension.js +12 -1
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/prepare.js +30 -2
- package/dist/commands/run-engine.js +11 -2
- package/dist/commands/run.js +6 -0
- package/dist/commands/runtime-launch.js +19 -0
- package/dist/commands/status-watch.js +43 -7
- package/dist/commands/status.js +8 -1
- package/dist/devices.js +4 -2
- package/dist/mm-harness-cli.js +5 -2
- package/dist/paths.js +2 -0
- package/dist/runtime-proof.js +57 -21
- package/dist/task-view.js +45 -2
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +18 -0
- package/library/actions/extension/wallet/read_state.mjs +11 -3
- package/library/actions/extension/wallet/state.mjs +25 -1
- package/library/manifests/extension.action-manifest.json +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.64.0 - 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Preflight behavioral proof bindings with `run --proof`, including `--plan`, before launching QA recipes. Executed outcomes and evidence still require runtime validation.
|
|
10
|
+
- Read source-development Extension pending approval IDs and types with `metamask.wallet.read_state scope=pending_approvals`, without mutating requests or exposing payloads.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Gate all Extension source launch paths on current dependencies and reject bundles compiled against stale or changed installed resolutions. Successful watch and LavaMoat builds record their dependency baseline; verified release artifacts retain their independent launch path.
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- Clarify how reusable recipes support daily domain and release QA through the scope-owning skill.
|
|
19
|
+
|
|
20
|
+
## 0.63.0 - 2026-09-23
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Show the task's acceptance criteria and recorded verdicts in `status`, `status --watch`, and `status --task`, using the Farmslot handoff and acceptance ledger.
|
|
25
|
+
- Show the active preparation step and elapsed time in `status --watch` and `status --task` even when the agent buffers preparation output.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Allow 30 seconds for iOS simulator discovery so a cold CoreSimulator startup does not fail at five seconds.
|
|
30
|
+
|
|
5
31
|
## 0.62.0 - 2026-09-22
|
|
6
32
|
|
|
7
33
|
### Added
|
package/README.md
CHANGED
|
@@ -10,6 +10,21 @@ and runtime paths are detected automatically.
|
|
|
10
10
|
The generic graph engine and evidence schemas live in [Farmslot](https://farmslot.io) packages.
|
|
11
11
|
`mm-harness` owns MetaMask runtime control and domain capabilities.
|
|
12
12
|
|
|
13
|
+
## Daily and release QA
|
|
14
|
+
|
|
15
|
+
The same recipes can validate yesterday's domain changes together or a release
|
|
16
|
+
that is behind `main`. The `mms-recipe-qa` skill freezes the change scope and
|
|
17
|
+
criteria; the team library supplies smoke and reusable journeys; this CLI runs
|
|
18
|
+
them on the selected app and retains assertions, traces and captures.
|
|
19
|
+
|
|
20
|
+
Use the installed skill's `references/domain-qa.md` for window/range inputs and
|
|
21
|
+
report policy. Scope selection and scheduling are outside the harness. Discover
|
|
22
|
+
recipes through `run --list` and `run <recipe> --describe`, then follow the
|
|
23
|
+
locked version's `help --json` for composition and drift diagnosis. Adapt a
|
|
24
|
+
recipe to the tested release without changing the release or weakening its
|
|
25
|
+
criteria. An internal action that needs development instrumentation cannot
|
|
26
|
+
prove behavior on an unmodified release artifact.
|
|
27
|
+
|
|
13
28
|
## Getting started from zero
|
|
14
29
|
|
|
15
30
|
No checkouts yet? `mm-harness setup-base` clones the MetaMask product repos into one
|
|
@@ -4,11 +4,13 @@ set -euo pipefail
|
|
|
4
4
|
|
|
5
5
|
TARGET="$PWD"
|
|
6
6
|
RUNTIME_DIR="temp/recipe/runtime"
|
|
7
|
+
RUNNER_BIN=""
|
|
7
8
|
while [ "$#" -gt 0 ]; do
|
|
8
9
|
case "$1" in
|
|
9
10
|
--target) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
|
|
10
11
|
--runtime-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; RUNTIME_DIR="$2"; shift 2 ;;
|
|
11
|
-
-
|
|
12
|
+
--runner-bin) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; RUNNER_BIN="$2"; shift 2 ;;
|
|
13
|
+
-h|--help) echo "Usage: build-lavamoat.sh [--target <metamask-extension>] [--runtime-dir <relative path>] [--runner-bin <path>]"; exit 0 ;;
|
|
12
14
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
13
15
|
esac
|
|
14
16
|
done
|
|
@@ -55,4 +57,7 @@ test -f dist/chrome/manifest.json || {
|
|
|
55
57
|
echo "build-lavamoat: build completed without dist/chrome/manifest.json" >&2
|
|
56
58
|
exit 1
|
|
57
59
|
}
|
|
60
|
+
if [ -n "$RUNNER_BIN" ]; then
|
|
61
|
+
"$RUNNER_BIN" runtime-decision --adapter extension --target "$TARGET" --record-baseline --json >/dev/null
|
|
62
|
+
fi
|
|
58
63
|
echo "[recipe-harness] non-test LavaMoat build completed"
|
|
@@ -308,7 +308,7 @@ NODE
|
|
|
308
308
|
prepare_parts+=("bash ${quoted_start_watch} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
|
|
309
309
|
fi
|
|
310
310
|
if $BUILD_LAVAMOAT; then
|
|
311
|
-
prepare_parts+=("bash ${quoted_build_lavamoat} --target ${quoted_target} --runtime-dir ${quoted_runtime_dir}")
|
|
311
|
+
prepare_parts+=("bash ${quoted_build_lavamoat} --target ${quoted_target} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
|
|
312
312
|
fi
|
|
313
313
|
if ! $EXTERNAL_ARTIFACT; then
|
|
314
314
|
prepare_parts+=("node ${quoted_check_infura} --target ${quoted_target} --runtime-dist ${quoted_dist}")
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# --runtime-dir <rel> (default from adapters/shared path-defaults)
|
|
13
13
|
# --watcher-port <port> (default env WATCHER_PORT/RECIPE_WATCHER_PORT,
|
|
14
14
|
# runtime json, or slot suffix e.g. metamask-extension-1 => 9011)
|
|
15
|
-
# --runner-bin <path> (optional; records deps/cache baseline
|
|
15
|
+
# --runner-bin <path> (optional; records deps/cache baseline)
|
|
16
16
|
# --summary <file> (optional; writes the standard summary.json shape)
|
|
17
17
|
#
|
|
18
18
|
# Outputs:
|
|
@@ -352,7 +352,7 @@ if ! write_canonical_watch_pid; then
|
|
|
352
352
|
fi
|
|
353
353
|
echo '[recipe-harness] yarn start compiled successfully'
|
|
354
354
|
if [ -n "$RUNNER_BIN" ]; then
|
|
355
|
-
"$RUNNER_BIN" runtime-decision --adapter extension --target . --record-baseline --json >/dev/null
|
|
355
|
+
"$RUNNER_BIN" runtime-decision --adapter extension --target . --record-baseline --json >/dev/null
|
|
356
356
|
echo '[recipe-harness] recorded webpack cache baseline (runtime-decision --record-baseline)'
|
|
357
357
|
fi
|
|
358
358
|
status=pass
|
|
@@ -56,6 +56,13 @@ function webpackFingerprint(target) {
|
|
|
56
56
|
for (const dir of WEBPACK_RECURSIVE_INPUTS) walk(hash, target, dir);
|
|
57
57
|
return { gitHead: git(target, ["rev-parse", "HEAD"]) ?? "unknown", fingerprint: hash.digest("hex") };
|
|
58
58
|
}
|
|
59
|
+
function installedDepsFingerprint(target) {
|
|
60
|
+
const hash = crypto.createHash("sha256");
|
|
61
|
+
for (const rel of INSTALL_MARKERS) {
|
|
62
|
+
if (fs.existsSync(path.join(target, rel))) addFile(hash, target, rel);
|
|
63
|
+
}
|
|
64
|
+
return hash.digest("hex");
|
|
65
|
+
}
|
|
59
66
|
function stateDir(target) {
|
|
60
67
|
return recipeHarnessPath(target, "extension", "decision-state");
|
|
61
68
|
}
|
|
@@ -328,6 +335,10 @@ function distCheck(target) {
|
|
|
328
335
|
} catch {
|
|
329
336
|
return { status: "no-build" };
|
|
330
337
|
}
|
|
338
|
+
const baseline = readBaseline(target, "webpack-cache-state.json");
|
|
339
|
+
if (!baseline || !("installedDepsFingerprint" in baseline) || baseline?.depsCurrent === false || typeof baseline?.installedDepsFingerprint === "string" && baseline.installedDepsFingerprint !== installedDepsFingerprint(target)) {
|
|
340
|
+
return { status: "stale", reason: "dependencies-changed" };
|
|
341
|
+
}
|
|
331
342
|
const description = typeof manifest.description === "string" ? manifest.description : "";
|
|
332
343
|
const match = /from git id:\s*([0-9a-f]{7,40})/iu.exec(description);
|
|
333
344
|
const head = (git(target, ["rev-parse", "HEAD"]) ?? "").toLowerCase();
|
|
@@ -363,7 +374,11 @@ async function cdpCheck(target, cdpPort, pageMode) {
|
|
|
363
374
|
}
|
|
364
375
|
function recordWebpackBaseline(target) {
|
|
365
376
|
const cache = webpackFingerprint(target);
|
|
366
|
-
writeBaseline(target, "webpack-cache-state.json",
|
|
377
|
+
writeBaseline(target, "webpack-cache-state.json", {
|
|
378
|
+
...cache,
|
|
379
|
+
depsCurrent: depsCheck(target).status === "current",
|
|
380
|
+
installedDepsFingerprint: installedDepsFingerprint(target)
|
|
381
|
+
});
|
|
367
382
|
}
|
|
368
383
|
async function decideExtensionReadiness(target, options = {}) {
|
|
369
384
|
const resolved = path.resolve(target);
|
|
@@ -424,7 +439,7 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
424
439
|
when: dist.status === "stale",
|
|
425
440
|
decision: "build",
|
|
426
441
|
reasonCode: "dist-stale",
|
|
427
|
-
reasons: [dist.reason === "uncommitted-source" ? `Uncommitted source since the build (${dist.modified?.length ?? 0} file(s)); rebuild.` : `dist git id ${dist.distGitId} != HEAD ${dist.head}; rebuild.`],
|
|
442
|
+
reasons: [dist.reason === "uncommitted-source" ? `Uncommitted source since the build (${dist.modified?.length ?? 0} file(s)); rebuild.` : dist.reason === "dependencies-changed" ? "Dependency installation changed or was not current at compilation; rebuild." : `dist git id ${dist.distGitId} != HEAD ${dist.head}; rebuild.`],
|
|
428
443
|
actions: buildAction(resolved, false)
|
|
429
444
|
},
|
|
430
445
|
{
|
package/dist/cli-commands.js
CHANGED
|
@@ -25,7 +25,7 @@ const SPEC = {
|
|
|
25
25
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
26
26
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
|
|
27
27
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
28
|
-
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
28
|
+
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device", "--plan", "--proof"] },
|
|
29
29
|
{ name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
|
|
30
30
|
{ name: "recipe-quality", desc: "Build canonical quality or request optional recipe planning/advice", args: ["build", "plan", "advise", "advisor-status"], flags: ["--input", "--output", "--recipe", "--task", "--evidence", "--params", "--adapter", "--library", "--advisor", "--advisor-model", "--advisor-stages", "--json"] },
|
|
31
31
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
package/dist/command-contract.js
CHANGED
|
@@ -254,6 +254,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
254
254
|
options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, RUN_FUNDING, {
|
|
255
255
|
"--list": bool(),
|
|
256
256
|
"--plan": bool(),
|
|
257
|
+
"--proof": bool(),
|
|
257
258
|
"--describe": bool(),
|
|
258
259
|
"--hud": value(["auto", "show", "hide"])
|
|
259
260
|
}),
|
|
@@ -52,6 +52,7 @@ function extensionDepsBlock(target) {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
async function installExtensionDeps(target) {
|
|
55
|
+
stopExtensionWatcher(target);
|
|
55
56
|
const installScript = path.join(runnerDir, "adapters/shared/install-repo-deps.sh");
|
|
56
57
|
const result = await spawnScriptStreaming(installScript, ["--target", target], target);
|
|
57
58
|
if (result.status === 0) recordDepsBaseline(target);
|
|
@@ -91,6 +92,16 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
91
92
|
Next: rerun runtime-launch with the same artifact source.`
|
|
92
93
|
};
|
|
93
94
|
}
|
|
95
|
+
if (artifact.status === "none") {
|
|
96
|
+
const depsBlock = extensionDepsBlock(target);
|
|
97
|
+
if (depsBlock) {
|
|
98
|
+
return {
|
|
99
|
+
status: EXIT.infra,
|
|
100
|
+
output: `EXTENSION_DEPENDENCIES_REQUIRED: ${depsBlock.message}
|
|
101
|
+
Next: ${depsBlock.userAction}`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
94
105
|
if (!reusable) {
|
|
95
106
|
const block = extensionProductConfigBlock(target);
|
|
96
107
|
if (block) {
|
|
@@ -105,7 +116,7 @@ Next: ${block.userAction}`
|
|
|
105
116
|
restoreRequestedPorts();
|
|
106
117
|
if (wantWatch) {
|
|
107
118
|
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
108
|
-
const watchArgs = ["--target", target];
|
|
119
|
+
const watchArgs = ["--target", target, "--runner-bin", path.join(runnerDir, "bin/mm-harness")];
|
|
109
120
|
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
110
121
|
console.error(colorHumanMessage(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`));
|
|
111
122
|
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
package/dist/commands/prepare.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { color, stripAnsi } from "../cli-color.js";
|
|
8
8
|
import { runnerProvenance } from "../doctor.js";
|
|
9
9
|
import { detectAdapter } from "../harness.js";
|
|
10
|
-
import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
10
|
+
import { harnessExecutable, manifestPath, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
|
|
11
11
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
12
12
|
import { EXIT } from "./shared.js";
|
|
13
13
|
import { formatDuration } from "./status-watch.js";
|
|
@@ -336,13 +336,32 @@ async function handlePrepare(argv) {
|
|
|
336
336
|
const remoteFlagArgs = remoteFlag === void 0 ? [] : ["--remote-flag", remoteFlag];
|
|
337
337
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
338
338
|
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
339
|
+
const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
|
|
339
340
|
const chain = stepChain(platform);
|
|
340
341
|
const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
|
|
341
342
|
view?.begin();
|
|
342
343
|
const steps = [];
|
|
344
|
+
let activeStep;
|
|
345
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
346
|
+
const writeProgress = () => {
|
|
347
|
+
fs.mkdirSync(path.dirname(progressPath), { recursive: true });
|
|
348
|
+
const temporaryPath = `${progressPath}.${process.pid}.tmp`;
|
|
349
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify({
|
|
350
|
+
schemaVersion: 1,
|
|
351
|
+
startedAt,
|
|
352
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
353
|
+
total: chain.length,
|
|
354
|
+
steps: steps.filter(({ id }) => chain.includes(id)).map(({ id, status, durationMs }) => ({ id, status, durationMs })),
|
|
355
|
+
...activeStep ? { active: activeStep } : {}
|
|
356
|
+
})}
|
|
357
|
+
`);
|
|
358
|
+
fs.renameSync(temporaryPath, progressPath);
|
|
359
|
+
};
|
|
343
360
|
const failed = () => steps.some((step) => step.status === "fail");
|
|
344
361
|
const addStep = (step) => {
|
|
345
362
|
steps.push(step);
|
|
363
|
+
activeStep = void 0;
|
|
364
|
+
writeProgress();
|
|
346
365
|
view?.finish(step);
|
|
347
366
|
return step;
|
|
348
367
|
};
|
|
@@ -352,10 +371,17 @@ async function handlePrepare(argv) {
|
|
|
352
371
|
}
|
|
353
372
|
const step = await runStep(executable, target, artifactsDir, id, args, {
|
|
354
373
|
json,
|
|
355
|
-
onSpawn: (
|
|
374
|
+
onSpawn: (stepStartedAt) => {
|
|
375
|
+
activeStep = { id, startedAt: new Date(stepStartedAt).toISOString() };
|
|
376
|
+
writeProgress();
|
|
377
|
+
view?.start(id, stepStartedAt);
|
|
378
|
+
}
|
|
356
379
|
});
|
|
357
380
|
return addStep(step);
|
|
358
381
|
};
|
|
382
|
+
const heartbeat = setInterval(() => {
|
|
383
|
+
if (activeStep) writeProgress();
|
|
384
|
+
}, 1e4);
|
|
359
385
|
try {
|
|
360
386
|
await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
361
387
|
await run("status", ["status", "--json", ...targetArgs]);
|
|
@@ -384,6 +410,7 @@ async function handlePrepare(argv) {
|
|
|
384
410
|
}
|
|
385
411
|
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
386
412
|
} finally {
|
|
413
|
+
clearInterval(heartbeat);
|
|
387
414
|
view?.end();
|
|
388
415
|
}
|
|
389
416
|
const provenance = runnerProvenance(manifestPath(platform));
|
|
@@ -404,6 +431,7 @@ async function handlePrepare(argv) {
|
|
|
404
431
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
405
432
|
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
|
|
406
433
|
`);
|
|
434
|
+
fs.rmSync(progressPath, { force: true });
|
|
407
435
|
if (json) {
|
|
408
436
|
console.log(JSON.stringify(record, null, 2));
|
|
409
437
|
} else {
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { mkdtemp } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { validateRuntimeProofPlan } from "../runtime-proof.js";
|
|
5
6
|
import {
|
|
6
7
|
ensureOverlay,
|
|
7
8
|
newHealState,
|
|
@@ -46,6 +47,7 @@ import { EXIT } from "./shared.js";
|
|
|
46
47
|
import {
|
|
47
48
|
actionManifestPathOption,
|
|
48
49
|
optionString,
|
|
50
|
+
optionFlag,
|
|
49
51
|
optionStrings,
|
|
50
52
|
isRecord,
|
|
51
53
|
shellQuoteArg,
|
|
@@ -563,7 +565,7 @@ function restoreEnv(key, value) {
|
|
|
563
565
|
if (value === void 0) delete process.env[key];
|
|
564
566
|
else process.env[key] = value;
|
|
565
567
|
}
|
|
566
|
-
async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySources, rootRef, params) {
|
|
568
|
+
async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySources, rootRef, params, proof = false) {
|
|
567
569
|
const protocol = await importRecipeProtocol();
|
|
568
570
|
let externalRecipeIds;
|
|
569
571
|
let libraryResolution;
|
|
@@ -641,6 +643,12 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
|
|
|
641
643
|
}
|
|
642
644
|
};
|
|
643
645
|
validateDependencyInstances(recipe, params ?? {});
|
|
646
|
+
if (proof && !findings.some((finding) => finding.severity === "error")) {
|
|
647
|
+
findings.push(...validateRuntimeProofPlan(
|
|
648
|
+
recipe,
|
|
649
|
+
new Map([...dependencies.recipes].map(([ref, dependency]) => [ref, dependency.document]))
|
|
650
|
+
));
|
|
651
|
+
}
|
|
644
652
|
} catch (error) {
|
|
645
653
|
const resolutionError = error instanceof harness.RecipeResolutionError ? error : void 0;
|
|
646
654
|
if (!resolutionError) throw error;
|
|
@@ -919,7 +927,8 @@ async function validateRunRecipeStatic(recipeArg, adapter, options, params = {})
|
|
|
919
927
|
manifest,
|
|
920
928
|
librarySources,
|
|
921
929
|
resolved.ref,
|
|
922
|
-
effectiveParams
|
|
930
|
+
effectiveParams,
|
|
931
|
+
optionFlag(options, "proof")
|
|
923
932
|
) : {
|
|
924
933
|
status: "invalid",
|
|
925
934
|
findings: [],
|
package/dist/commands/run.js
CHANGED
|
@@ -613,6 +613,12 @@ async function handleRunPlan(recipeArg, params, options, stream) {
|
|
|
613
613
|
status: manifestOk && schemaValid ? "ok" : "error",
|
|
614
614
|
detail: "recipe document schema + action existence/platform vs the adapter manifest"
|
|
615
615
|
},
|
|
616
|
+
...optionFlag(options, "proof") ? [{
|
|
617
|
+
step: "validate.proofBindings",
|
|
618
|
+
confidence: "static",
|
|
619
|
+
status: findings.some((finding) => finding.code.startsWith("proof.")) ? "error" : errorCount ? "planned" : "ok",
|
|
620
|
+
detail: "checks possible behavioral assertion bindings only; runtime order, branches, results and proof artifact indexing remain unverified"
|
|
621
|
+
}] : [],
|
|
616
622
|
{
|
|
617
623
|
step: "fixture.file",
|
|
618
624
|
confidence: "static",
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
writeMobileReleaseArtifactState
|
|
11
11
|
} from "../adapters/mobile/release-artifact-state.js";
|
|
12
12
|
import { recipeHarnessPath } from "../paths.js";
|
|
13
|
+
import { extensionDepsBlock } from "./launch/extension.js";
|
|
14
|
+
import { isExtensionDistStale } from "../adapters/extension/runtime-decision.js";
|
|
13
15
|
import {
|
|
14
16
|
adapterOption,
|
|
15
17
|
isRecord,
|
|
@@ -152,6 +154,23 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
152
154
|
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
153
155
|
return 1;
|
|
154
156
|
}
|
|
157
|
+
if (!artifact) {
|
|
158
|
+
const depsBlock = extensionDepsBlock(target);
|
|
159
|
+
const staleDist = mode === "existing-dist" && isExtensionDistStale(target);
|
|
160
|
+
if (depsBlock || staleDist) {
|
|
161
|
+
const report2 = runtimeLaunchReport("fail", {
|
|
162
|
+
adapter,
|
|
163
|
+
target,
|
|
164
|
+
cdpPort,
|
|
165
|
+
artifactsDir,
|
|
166
|
+
reason: depsBlock ? "extension_dependencies_required" : "extension_dist_stale",
|
|
167
|
+
fix: depsBlock?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
168
|
+
command
|
|
169
|
+
});
|
|
170
|
+
printRuntimeLaunchReport(report2, optionFlag(options, "json"));
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
155
174
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
156
175
|
const jsonMode = optionFlag(options, "json");
|
|
157
176
|
if (!jsonMode) {
|
|
@@ -103,9 +103,13 @@ function renderTaskView(view, title, explicitTask = false) {
|
|
|
103
103
|
const contextLine = joinParts([
|
|
104
104
|
view.libraries.length > 0 && `libraries ${out("info", view.libraries.map(libraryLabel).join(" "))}`,
|
|
105
105
|
showTask && view.fixture && `fixture ${out("info", path.basename(view.fixture.fixture ?? "unknown"))} ${statusWord(out, view.fixture.status)}${view.fixture.walletReset ? out("warn", " reset") : ""}`,
|
|
106
|
-
showTask && view.sandbox?.ready !== void 0 && `sandbox ${view.sandbox.ready ? out("ok", "ready") : out("warn", "not ready")}`
|
|
106
|
+
showTask && (view.prepare ? `sandbox ${out("info", "preparing")}` : view.sandbox?.ready !== void 0 && `sandbox ${view.sandbox.ready ? out("ok", "ready") : out("warn", "not ready")}`)
|
|
107
107
|
]);
|
|
108
108
|
if (contextLine) lines.push(contextLine);
|
|
109
|
+
if (showTask && view.acceptance?.length) lines.push("", ...renderAcceptanceLines(view));
|
|
110
|
+
if (showTask && view.prepare) {
|
|
111
|
+
lines.push(`prepare ${out("info", view.prepare.step)} \xB7 ${view.prepare.completed}/${view.prepare.total} steps \xB7 ${formatDuration(view.prepare.elapsedMs)} elapsed`);
|
|
112
|
+
}
|
|
109
113
|
const last = view.lastCommand;
|
|
110
114
|
if (showTask && last) {
|
|
111
115
|
lines.push(joinParts([
|
|
@@ -148,6 +152,19 @@ function renderTaskView(view, title, explicitTask = false) {
|
|
|
148
152
|
}
|
|
149
153
|
return lines;
|
|
150
154
|
}
|
|
155
|
+
function renderAcceptanceLines(view) {
|
|
156
|
+
if (!view.acceptance?.length) return [];
|
|
157
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
158
|
+
const proven = view.acceptance.filter((row) => row.status?.verdict === "proven").length;
|
|
159
|
+
return [
|
|
160
|
+
`acceptance criteria ${proven}/${view.acceptance.length} proven`,
|
|
161
|
+
...view.acceptance.map((row) => {
|
|
162
|
+
const verdict = row.status?.verdict ?? "no verdict";
|
|
163
|
+
const style = verdict === "proven" ? "ok" : verdict === "weak" ? "warn" : verdict === "no verdict" ? "dim" : "err";
|
|
164
|
+
return ` ${row.id} [${out(style, verdict)}] ${row.text}`;
|
|
165
|
+
})
|
|
166
|
+
];
|
|
167
|
+
}
|
|
151
168
|
function idleReason(view) {
|
|
152
169
|
if (!view.taskDir) return "no task directory under temp/tasks";
|
|
153
170
|
const { reason, idleMs, branchCarriesTicket } = view.activity;
|
|
@@ -161,15 +178,33 @@ function idleReason(view) {
|
|
|
161
178
|
}
|
|
162
179
|
function changedLines(previous, next) {
|
|
163
180
|
if (!previous || previous.taskDir !== next.taskDir) {
|
|
164
|
-
return [
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
return [
|
|
182
|
+
joinParts([
|
|
183
|
+
path.basename(next.checkout.target),
|
|
184
|
+
next.taskDir ? path.basename(next.taskDir) : "no-task-dir",
|
|
185
|
+
`status=${next.signal?.status ?? "unknown"}`,
|
|
186
|
+
`step=${next.signal?.step ? stepLabel(next.signal.step) : "none"}`
|
|
187
|
+
], " "),
|
|
188
|
+
...(next.acceptance ?? []).map((row) => `${row.id} ${row.status?.verdict ?? "no verdict"} ${row.text}`),
|
|
189
|
+
...next.prepare ? [`prepare ${next.prepare.step} started (${next.prepare.completed}/${next.prepare.total} steps)`] : []
|
|
190
|
+
];
|
|
170
191
|
}
|
|
171
192
|
const lines = [];
|
|
193
|
+
for (const step of next.prepare?.steps ?? []) {
|
|
194
|
+
if (!previous.prepare?.steps.some((prior) => prior.id === step.id && prior.status === step.status)) {
|
|
195
|
+
lines.push(`prepare ${step.id} ${step.status} (${formatDuration(step.durationMs)})`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (next.prepare && (previous.prepare?.step !== next.prepare.step || previous.prepare.startedAt !== next.prepare.startedAt)) {
|
|
199
|
+
lines.push(`prepare ${next.prepare.step} started (${next.prepare.completed}/${next.prepare.total} steps)`);
|
|
200
|
+
}
|
|
172
201
|
const before = new Map(previous.checklist.map((row) => [row.number, row.checked]));
|
|
202
|
+
const beforeAcceptance = new Map(previous.acceptance?.map((row) => [row.id, row.status?.verdict]));
|
|
203
|
+
for (const row of next.acceptance ?? []) {
|
|
204
|
+
if (beforeAcceptance.get(row.id) !== row.status?.verdict || !beforeAcceptance.has(row.id)) {
|
|
205
|
+
lines.push(`${row.id} ${row.status?.verdict ?? "no verdict"} ${row.text}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
173
208
|
for (const row of next.checklist) {
|
|
174
209
|
if (!row.checked || before.get(row.number) === true) continue;
|
|
175
210
|
lines.push(`checked ${row.number} ${row.label}${row.durationMs !== void 0 ? ` (${formatDuration(row.durationMs)})` : ""}`);
|
|
@@ -254,6 +289,7 @@ export {
|
|
|
254
289
|
changedLines,
|
|
255
290
|
formatDuration,
|
|
256
291
|
handleStatusTaskView,
|
|
292
|
+
renderAcceptanceLines,
|
|
257
293
|
renderTaskView,
|
|
258
294
|
runStatusWatch
|
|
259
295
|
};
|
package/dist/commands/status.js
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
renderMobileLiveBlock
|
|
21
21
|
} from "./mobile-device-view.js";
|
|
22
22
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
23
|
-
import { handleStatusTaskView } from "./status-watch.js";
|
|
23
|
+
import { handleStatusTaskView, renderAcceptanceLines } from "./status-watch.js";
|
|
24
|
+
import { collectTaskView, findTaskDir } from "../task-view.js";
|
|
24
25
|
import { optionFlag, optionString, targetPath } from "./parse-args.js";
|
|
25
26
|
async function handleStatus({ options }) {
|
|
26
27
|
const target = targetPath(options);
|
|
@@ -106,6 +107,12 @@ async function handleStatus({ options }) {
|
|
|
106
107
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
107
108
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
108
109
|
console.log(renderCheckoutView(view));
|
|
110
|
+
if (!fast) {
|
|
111
|
+
const taskView = collectTaskView(target, findTaskDir(target), Date.now());
|
|
112
|
+
if (taskView.activity.ongoing && taskView.acceptance?.length) {
|
|
113
|
+
console.log(renderAcceptanceLines(taskView).join("\n"));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
109
116
|
if (adapter !== "mobile") renderFeatureFlagLine(staticFeatureFlags, out);
|
|
110
117
|
if (deviceView) renderMobileDeviceList(deviceView, out);
|
|
111
118
|
if (!fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0) {
|
package/dist/devices.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
3
|
+
const IOS_DEVICE_DISCOVERY_TIMEOUT_MS = 3e4;
|
|
3
4
|
function reportProbeFailure(errors, platform, error) {
|
|
4
5
|
const code = error?.code;
|
|
5
6
|
if (code === "ENOENT") return;
|
|
6
|
-
const
|
|
7
|
+
const timeoutSeconds = platform === "ios" ? IOS_DEVICE_DISCOVERY_TIMEOUT_MS / 1e3 : 5;
|
|
8
|
+
const reason = code === "ETIMEDOUT" ? `timed out after ${timeoutSeconds} seconds` : "failed";
|
|
7
9
|
const probeCode = code ?? error?.status;
|
|
8
10
|
errors.push({
|
|
9
11
|
platform,
|
|
@@ -61,7 +63,7 @@ function listIosSimulators(scope, errors) {
|
|
|
61
63
|
try {
|
|
62
64
|
output = execFileSync("xcrun", ["simctl", "list", "devices", scope, "-j"], {
|
|
63
65
|
encoding: "utf8",
|
|
64
|
-
timeout:
|
|
66
|
+
timeout: IOS_DEVICE_DISCOVERY_TIMEOUT_MS,
|
|
65
67
|
stdio: ["ignore", "pipe", "ignore"]
|
|
66
68
|
});
|
|
67
69
|
} catch (error) {
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -192,8 +192,10 @@ Example:
|
|
|
192
192
|
The task view reads task files only \u2014 no probe, no launch, no write \u2014 and shows
|
|
193
193
|
the same facts whether a skill or a control plane started the run: slot and
|
|
194
194
|
device isolation, template and flow, fixture and sandbox readiness, then the
|
|
195
|
-
checklist with the time each row took
|
|
196
|
-
running
|
|
195
|
+
checklist with the time each row took, how long the current row has been
|
|
196
|
+
running, and acceptance criteria with recorded verdicts. The home dashboard
|
|
197
|
+
also shows criteria for an ongoing task. --task without --watch prints it once.
|
|
198
|
+
With --json, --watch emits one
|
|
197
199
|
object per change (newline-delimited) instead of redrawing.
|
|
198
200
|
|
|
199
201
|
The checkout view (also under "view" in --json, and printed by doctor) is what
|
|
@@ -462,6 +464,7 @@ Discovery:
|
|
|
462
464
|
--list List complete recipes accepted by run for this adapter; no <recipe> needed
|
|
463
465
|
--describe Show one recipe's parameters, composed actions/recipes, and runnable command
|
|
464
466
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
467
|
+
--proof Preflight behavioral proof bindings before planning/execution; runtime evidence is still required.
|
|
465
468
|
|
|
466
469
|
Common overrides:
|
|
467
470
|
--device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
|
package/dist/paths.js
CHANGED
|
@@ -5,6 +5,7 @@ const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..
|
|
|
5
5
|
const pathDefaults = readPathDefaults();
|
|
6
6
|
const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
|
|
7
7
|
const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
|
|
8
|
+
const PREPARE_PROGRESS_ARTIFACT = "prepare/progress.json";
|
|
8
9
|
function recipeRuntimeDir() {
|
|
9
10
|
return validateRelativeRecipePath("RECIPE_RUNTIME_DIR", process.env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
|
|
10
11
|
}
|
|
@@ -171,6 +172,7 @@ function isMissingPackageError(error, packageName) {
|
|
|
171
172
|
export {
|
|
172
173
|
DEFAULT_RECIPE_HARNESS_ROOT,
|
|
173
174
|
DEFAULT_RECIPE_RUNTIME_DIR,
|
|
175
|
+
PREPARE_PROGRESS_ARTIFACT,
|
|
174
176
|
assertAdapter,
|
|
175
177
|
extensionIdPath,
|
|
176
178
|
harnessExecutable,
|
package/dist/runtime-proof.js
CHANGED
|
@@ -9,23 +9,8 @@ function validateRuntimeProof(bundle, smokeTarget) {
|
|
|
9
9
|
assert(entries.length && entries.every((entry) => entry.ok === true), "Runtime trace contains failed or unknown actions");
|
|
10
10
|
assert(entries.some((entry) => entry.action === "end" && entry.status === "pass" && !String(entry.nodeId ?? entry.id).includes("/")), "Runtime recipe did not reach its passing end");
|
|
11
11
|
const documents = new Map(recipeResolution.dependencies.map((dependency) => [normalizeRecipeRef(dependency.ref), resolvedRecipes[dependency.digest]]));
|
|
12
|
-
const nodes =
|
|
13
|
-
function collect(document, prefix = "", stack = /* @__PURE__ */ new Set()) {
|
|
14
|
-
for (const [id, node] of Object.entries(document.workflow.nodes)) {
|
|
15
|
-
nodes.set(prefix + id, node);
|
|
16
|
-
if (node.action === "call" && typeof node.ref === "string") {
|
|
17
|
-
const ref = normalizeRecipeRef(node.ref);
|
|
18
|
-
assert(!stack.has(ref), "Recursive recipe proof");
|
|
19
|
-
const nested = documents.get(ref);
|
|
20
|
-
assert(nested, `Missing resolved recipe ${ref}`);
|
|
21
|
-
collect(nested, `${prefix}${id}/`, new Set(stack).add(ref));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
collect(recipe);
|
|
12
|
+
const nodes = collectProofNodes(recipe, documents);
|
|
26
13
|
const executed = new Map(entries.map((entry, index) => [entry.nodeId ?? entry.id, { entry, index }]));
|
|
27
|
-
const assertions = /* @__PURE__ */ new Set(["assert_output", "assert_json", "ui.wait_for"]);
|
|
28
|
-
const controls = /* @__PURE__ */ new Set(["end", "wait", "manual", "switch", "call", "index_artifacts", "assert_exit_code", "assert_file", "branch", "repeat", "set", "log"]);
|
|
29
14
|
function proves(id) {
|
|
30
15
|
const node = nodes.get(id);
|
|
31
16
|
const current = executed.get(id);
|
|
@@ -33,13 +18,12 @@ function validateRuntimeProof(bundle, smokeTarget) {
|
|
|
33
18
|
if (node.action === "call") return [...nodes.keys()].some((child) => child.startsWith(`${id}/`) && proves(child));
|
|
34
19
|
if (!assertions.has(node.action) || !current.entry.output || typeof current.entry.output !== "object") return false;
|
|
35
20
|
if (node.action === "assert_output") {
|
|
36
|
-
const
|
|
37
|
-
const source = prefix + String(node.source ?? node.node ?? "");
|
|
21
|
+
const source = assertionSource(id, node);
|
|
38
22
|
const prior = executed.get(source);
|
|
39
23
|
const action = nodes.get(source)?.action;
|
|
40
|
-
return Boolean(prior && prior.index < current.index && action && action === prior.entry.action &&
|
|
24
|
+
return Boolean(prior && prior.index < current.index && action && action === prior.entry.action && isObservation(action));
|
|
41
25
|
}
|
|
42
|
-
if (node.action === "ui.wait_for") return entries.slice(0, current.index).some((entry) =>
|
|
26
|
+
if (node.action === "ui.wait_for") return entries.slice(0, current.index).some((entry) => inputs.has(entry.action));
|
|
43
27
|
return typeof node.path === "string" && manifest.artifacts.some((artifact) => artifact.path === node.path && artifact.category === "proof");
|
|
44
28
|
}
|
|
45
29
|
assert(recipe.proofTargets?.length, "Runtime recipe has no behavioral proof targets");
|
|
@@ -49,6 +33,58 @@ function validateRuntimeProof(bundle, smokeTarget) {
|
|
|
49
33
|
}
|
|
50
34
|
if (smokeTarget) assert(recipe.proofTargets.some((target) => target.id === smokeTarget), "Smoke target is absent from recipe");
|
|
51
35
|
}
|
|
36
|
+
const assertions = /* @__PURE__ */ new Set(["assert_output", "assert_json", "ui.wait_for"]);
|
|
37
|
+
const controls = /* @__PURE__ */ new Set(["end", "wait", "manual", "switch", "call", "index_artifacts", "assert_exit_code", "assert_file", "branch", "repeat", "set", "log"]);
|
|
38
|
+
const inputs = /* @__PURE__ */ new Set(["ui.press", "ui.key_press", "ui.set_input", "ui.swipe", "ui.drag", "ui.long_press"]);
|
|
39
|
+
function isObservation(action) {
|
|
40
|
+
return !controls.has(action) && !assertions.has(action);
|
|
41
|
+
}
|
|
42
|
+
function assertionSource(id, node) {
|
|
43
|
+
const prefix = id.includes("/") ? id.slice(0, id.lastIndexOf("/") + 1) : "";
|
|
44
|
+
return prefix + String(node.source ?? node.node ?? "");
|
|
45
|
+
}
|
|
46
|
+
function collectProofNodes(recipe, documents) {
|
|
47
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
48
|
+
function collect(document, prefix = "", stack = /* @__PURE__ */ new Set()) {
|
|
49
|
+
for (const [id, node] of Object.entries(document.workflow.nodes)) {
|
|
50
|
+
nodes.set(prefix + id, node);
|
|
51
|
+
if (node.action === "call" && typeof node.ref === "string") {
|
|
52
|
+
const ref = normalizeRecipeRef(node.ref);
|
|
53
|
+
assert(!stack.has(ref), "Recursive recipe proof");
|
|
54
|
+
const nested = documents.get(ref);
|
|
55
|
+
assert(nested, `Missing resolved recipe ${ref}`);
|
|
56
|
+
collect(nested, `${prefix}${id}/`, new Set(stack).add(ref));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
collect(recipe);
|
|
61
|
+
return nodes;
|
|
62
|
+
}
|
|
63
|
+
function validateRuntimeProofPlan(recipe, documents = /* @__PURE__ */ new Map()) {
|
|
64
|
+
const finding = (code, path, message) => ({ severity: "error", code, path, message });
|
|
65
|
+
if (!recipe.proofTargets?.length) return [finding("proof.targets_missing", "proofTargets", "Behavioral proof requires at least one proof target.")];
|
|
66
|
+
const nodes = collectProofNodes(recipe, documents);
|
|
67
|
+
const hasInput = [...nodes.values()].some((node) => inputs.has(node.action));
|
|
68
|
+
function eligible(id) {
|
|
69
|
+
const node = nodes.get(id);
|
|
70
|
+
if (node.action === "call") return [...nodes.keys()].some((child) => child.startsWith(`${id}/`) && eligible(child));
|
|
71
|
+
if (node.action === "assert_output") {
|
|
72
|
+
const source = nodes.get(assertionSource(id, node));
|
|
73
|
+
return Boolean(source && isObservation(source.action));
|
|
74
|
+
}
|
|
75
|
+
if (node.action === "ui.wait_for") return hasInput;
|
|
76
|
+
return node.action === "assert_json" && typeof node.path === "string";
|
|
77
|
+
}
|
|
78
|
+
return recipe.proofTargets.flatMap((target, index) => {
|
|
79
|
+
const linked = [...nodes].filter(([, node]) => node.proves?.includes(target.id));
|
|
80
|
+
return linked.some(([id]) => eligible(id)) ? [] : [finding(
|
|
81
|
+
"proof.no_runtime_assertion",
|
|
82
|
+
`proofTargets[${index}]`,
|
|
83
|
+
`Proof target ${target.id} has no potentially eligible runtime assertion. Bind proves to assert_output over an observation, ui.wait_for with an input action, assert_json over a proof artifact, or a call containing one. Runtime order, outcomes and artifact indexing remain unverified.`
|
|
84
|
+
)];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
52
87
|
export {
|
|
53
|
-
validateRuntimeProof
|
|
88
|
+
validateRuntimeProof,
|
|
89
|
+
validateRuntimeProofPlan
|
|
54
90
|
};
|
package/dist/task-view.js
CHANGED
|
@@ -2,6 +2,12 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { HANDOFF_INPUT, latestTaskDir } from "@farmslot/agent-runtime";
|
|
5
|
+
import {
|
|
6
|
+
ACCEPTANCE_STATUS_ARTIFACT,
|
|
7
|
+
acceptanceCriteriaView,
|
|
8
|
+
acceptanceCriterionId,
|
|
9
|
+
validateAcceptanceStatusLedger
|
|
10
|
+
} from "@farmslot/protocol/contracts/acceptance";
|
|
5
11
|
import {
|
|
6
12
|
checklistStepName,
|
|
7
13
|
deriveChecklistStepDurations,
|
|
@@ -17,7 +23,7 @@ import {
|
|
|
17
23
|
} from "@farmslot/protocol";
|
|
18
24
|
import { readCommandJournal } from "./command-journal.js";
|
|
19
25
|
import { resolveRuntimeContextPath } from "./harness.js";
|
|
20
|
-
import { recipeRuntimeDir } from "./paths.js";
|
|
26
|
+
import { PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "./paths.js";
|
|
21
27
|
import { parseLibraryPathEnv } from "./review/knowledge.js";
|
|
22
28
|
const CHECKLIST_LABEL_LIMIT = 70;
|
|
23
29
|
const TASK_SILENT_AFTER_MS = 10 * 60 * 1e3;
|
|
@@ -42,8 +48,22 @@ function collectTaskView(target, taskDir, now) {
|
|
|
42
48
|
const checklist = readChecklistRows(path.join(taskDir, INTERACTIVE_CHECKLIST_MARKDOWN));
|
|
43
49
|
const template = asObject(handoff?.executionTemplate);
|
|
44
50
|
const task = asObject(handoff?.task);
|
|
51
|
+
const rawCriteria = task?.acceptanceCriteria;
|
|
52
|
+
if (rawCriteria !== void 0 && !Array.isArray(rawCriteria)) {
|
|
53
|
+
throw new Error(`${path.join(taskDir, HANDOFF_INPUT)}: task.acceptanceCriteria must be an array.`);
|
|
54
|
+
}
|
|
55
|
+
const criteria = rawCriteria?.map((text, index) => ({
|
|
56
|
+
id: acceptanceCriterionId(index),
|
|
57
|
+
text: String(text)
|
|
58
|
+
})).filter((criterion) => criterion.text.trim().length > 0) ?? [];
|
|
59
|
+
const rawLedger = readJsonObject(path.join(taskDir, ACCEPTANCE_STATUS_ARTIFACT));
|
|
60
|
+
if (rawLedger) {
|
|
61
|
+
const issues = validateAcceptanceStatusLedger(rawLedger);
|
|
62
|
+
if (issues.length > 0) throw new Error(`${ACCEPTANCE_STATUS_ARTIFACT}: ${issues.join("; ")}`);
|
|
63
|
+
}
|
|
45
64
|
const startedAtIso = pickString(handoff, "startedAt");
|
|
46
65
|
const startedAt = parseTime(startedAtIso);
|
|
66
|
+
const prepare = readPrepareProgress(taskDir, now, startedAt, parseTime(pickString(sandbox, "recordedAt")));
|
|
47
67
|
const timing = applyTiming(checklist, signal, startedAtIso, now);
|
|
48
68
|
const ticket = pickString(task, "ticket");
|
|
49
69
|
const journal = readLastCommand(target, now);
|
|
@@ -56,7 +76,8 @@ function collectTaskView(target, taskDir, now) {
|
|
|
56
76
|
startedAt,
|
|
57
77
|
parseTime(pickString(signal, "timestamp")),
|
|
58
78
|
...readMarkEvents(signal).map((event) => Date.parse(event.checkedAt)),
|
|
59
|
-
commandCounts ? commandAt : void 0
|
|
79
|
+
commandCounts ? commandAt : void 0,
|
|
80
|
+
prepare ? parseTime(prepare.updatedAt) : void 0
|
|
60
81
|
],
|
|
61
82
|
branch: base.checkout.branch,
|
|
62
83
|
ticket,
|
|
@@ -79,6 +100,7 @@ function collectTaskView(target, taskDir, now) {
|
|
|
79
100
|
title: pickString(task, "title"),
|
|
80
101
|
ticket
|
|
81
102
|
}),
|
|
103
|
+
...criteria.length > 0 || rawLedger ? { acceptance: acceptanceCriteriaView(criteria, rawLedger ? rawLedger : null) } : {},
|
|
82
104
|
fixture: section({
|
|
83
105
|
fixture: pickString(fixtures, "fixture"),
|
|
84
106
|
canonicalFixture: pickString(fixtures, "canonicalFixture"),
|
|
@@ -86,6 +108,7 @@ function collectTaskView(target, taskDir, now) {
|
|
|
86
108
|
walletReset: pickBoolean(fixtures, "walletReset")
|
|
87
109
|
}),
|
|
88
110
|
sandbox: section({ ready: pickBoolean(sandbox, "ready") }),
|
|
111
|
+
...prepare ? { prepare } : {},
|
|
89
112
|
checklist,
|
|
90
113
|
subtasks: readSubtasks(taskDir),
|
|
91
114
|
signal: section({
|
|
@@ -99,6 +122,26 @@ function collectTaskView(target, taskDir, now) {
|
|
|
99
122
|
activity
|
|
100
123
|
};
|
|
101
124
|
}
|
|
125
|
+
function readPrepareProgress(taskDir, now, taskStartedAt, recordedAt) {
|
|
126
|
+
const progress = readJsonObject(path.join(taskDir, "artifacts", PREPARE_PROGRESS_ARTIFACT));
|
|
127
|
+
const active = asObject(progress?.active);
|
|
128
|
+
const step = pickString(active, "id");
|
|
129
|
+
const startedAt = pickString(active, "startedAt");
|
|
130
|
+
const stepStartedAt = parseTime(startedAt);
|
|
131
|
+
const runStartedAt = parseTime(pickString(progress, "startedAt"));
|
|
132
|
+
const updatedAtIso = pickString(progress, "updatedAt");
|
|
133
|
+
const updatedAt = parseTime(updatedAtIso);
|
|
134
|
+
const total = pickNumber(progress, "total");
|
|
135
|
+
if (!step || !["doctor", "status", "launch", "fixtures", "verify"].includes(step) || stepStartedAt === void 0 || runStartedAt === void 0 || updatedAt === void 0 || now - updatedAt > 3e4 || total === void 0 || total < 1 || taskStartedAt !== void 0 && runStartedAt < taskStartedAt || recordedAt !== void 0 && recordedAt >= runStartedAt) return void 0;
|
|
136
|
+
const steps = Array.isArray(progress?.steps) ? progress.steps.flatMap((value) => {
|
|
137
|
+
const item = asObject(value);
|
|
138
|
+
const id = pickString(item, "id");
|
|
139
|
+
const status = pickString(item, "status");
|
|
140
|
+
const durationMs = pickNumber(item, "durationMs");
|
|
141
|
+
return id && ["doctor", "status", "launch", "fixtures", "verify"].includes(id) && status && ["pass", "fail", "skipped"].includes(status) && durationMs !== void 0 ? [{ id, status, durationMs }] : [];
|
|
142
|
+
}) : [];
|
|
143
|
+
return { step, startedAt, updatedAt: updatedAtIso, elapsedMs: Math.max(0, now - stepStartedAt), completed: steps.length, total, steps };
|
|
144
|
+
}
|
|
102
145
|
function readLastCommand(target, now) {
|
|
103
146
|
const { record } = readCommandJournal(target);
|
|
104
147
|
if (!record) return void 0;
|
|
@@ -118,6 +118,24 @@ current manifests do not expose the required durable capability.
|
|
|
118
118
|
| Bridge tracking and details | Completed supported bridge route | Read-only Swap/Bridge evidence exists, but Extension cannot create the terminal bridge transaction | **MISSING** for a complete Extension bridge journey |
|
|
119
119
|
| Android multi-network visibility | Imported wallet with holdings on multiple chains | Native Assets read reports aggregate visible holdings without Metro or a development bridge | **AVAILABLE** |
|
|
120
120
|
|
|
121
|
+
## Pending approval observation
|
|
122
|
+
|
|
123
|
+
On source-dev Extension runtimes, `metamask.wallet.read_state` with
|
|
124
|
+
`scope: "pending_approvals"` reads `stateHooks.store.getState()` once. It returns
|
|
125
|
+
only `state.pendingApprovals`, an ID-sorted array of `{ id, type }`, and
|
|
126
|
+
`state.pendingApprovalCount`. It neither approves nor rejects requests and does
|
|
127
|
+
not navigate, reload, or call the background controller. Missing or malformed
|
|
128
|
+
approval state fails the read instead of reporting an empty queue.
|
|
129
|
+
|
|
130
|
+
Capture the IDs before entering a confirmation flow and again after its explicit
|
|
131
|
+
UI teardown. Check that newly created IDs are gone and pre-existing IDs remain.
|
|
132
|
+
A count alone or Back navigation does not prove cleanup. This observes pending
|
|
133
|
+
approvals only, not transaction settlement or other controller queues.
|
|
134
|
+
|
|
135
|
+
This scope requires the development store hook. It is unsupported on official
|
|
136
|
+
Extension artifacts without that hook and on Mobile. Do not patch a release or
|
|
137
|
+
mutate controller state to make the read available.
|
|
138
|
+
|
|
121
139
|
## Platform scope
|
|
122
140
|
|
|
123
141
|
The 135-case source inventory targets Extension. It must not be relabeled as an
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
// Reads wallet
|
|
1
|
+
// Reads wallet state or pending approval identities without changing requests.
|
|
2
2
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
3
|
import { readWalletStateExpression } from './state.mjs';
|
|
4
4
|
|
|
5
5
|
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const scope = input.node?.scope ?? 'wallet';
|
|
7
|
+
const state = await page.evaluate(readWalletStateExpression(scope), { awaitPromise: true });
|
|
8
|
+
return {
|
|
9
|
+
action: input.action,
|
|
10
|
+
source: scope === 'pending_approvals' ? 'extension-stateHooks-store' : 'extension-stateHooks-clean-state',
|
|
11
|
+
scope,
|
|
12
|
+
state,
|
|
13
|
+
redacted: true,
|
|
14
|
+
proofPath: 'extension-wallet-state',
|
|
15
|
+
};
|
|
8
16
|
}));
|
|
@@ -22,7 +22,31 @@ const walletStatePreamble = `
|
|
|
22
22
|
};
|
|
23
23
|
`;
|
|
24
24
|
|
|
25
|
-
export function readWalletStateExpression() {
|
|
25
|
+
export function readWalletStateExpression(scope = 'wallet') {
|
|
26
|
+
if (scope === 'pending_approvals') {
|
|
27
|
+
return `(() => {
|
|
28
|
+
const hooks = globalThis.stateHooks;
|
|
29
|
+
if (typeof hooks?.store?.getState !== 'function') {
|
|
30
|
+
throw new Error('Pending approval reads require source-dev stateHooks.store.getState; official artifacts are unsupported.');
|
|
31
|
+
}
|
|
32
|
+
const root = hooks.store.getState();
|
|
33
|
+
const pending = (root?.metamask || root)?.pendingApprovals;
|
|
34
|
+
if (!pending || typeof pending !== 'object' || Array.isArray(pending)) {
|
|
35
|
+
throw new Error('Pending approval state is unavailable; an empty queue cannot be inferred.');
|
|
36
|
+
}
|
|
37
|
+
const pendingApprovals = Object.entries(pending).map(([id, approval]) => {
|
|
38
|
+
if (!id || !approval || approval.id !== id ||
|
|
39
|
+
typeof approval.type !== 'string' || !approval.type) {
|
|
40
|
+
throw new Error('Pending approval state contains an invalid request identity or type.');
|
|
41
|
+
}
|
|
42
|
+
return { id, type: approval.type };
|
|
43
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
44
|
+
return { pendingApprovals, pendingApprovalCount: pendingApprovals.length };
|
|
45
|
+
})()`;
|
|
46
|
+
}
|
|
47
|
+
if (scope !== 'wallet') {
|
|
48
|
+
throw new Error('metamask.wallet.read_state scope must be wallet or pending_approvals.');
|
|
49
|
+
}
|
|
26
50
|
return `(async () => {
|
|
27
51
|
${walletStatePreamble}
|
|
28
52
|
const { metamask, internal } = await readWalletState();
|
|
@@ -1753,10 +1753,16 @@
|
|
|
1753
1753
|
"execution_capabilities": []
|
|
1754
1754
|
},
|
|
1755
1755
|
"metamask.wallet.read_state": {
|
|
1756
|
-
"description": "Read
|
|
1756
|
+
"description": "Read redacted wallet state, or pending approval IDs/types/count from the source-dev store without modifying requests. Pending approvals are unsupported on official artifacts without the dev store.",
|
|
1757
1757
|
"schema": {
|
|
1758
1758
|
"type": "object",
|
|
1759
|
-
"properties": {
|
|
1759
|
+
"properties": {
|
|
1760
|
+
"scope": {
|
|
1761
|
+
"type": "string",
|
|
1762
|
+
"enum": ["wallet", "pending_approvals"],
|
|
1763
|
+
"default": "wallet"
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1760
1766
|
"additionalProperties": false
|
|
1761
1767
|
},
|
|
1762
1768
|
"examples": [
|