@deeeed/metamask-harness 0.50.5 → 0.51.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 CHANGED
@@ -2,12 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.50.5 - 2026-09-10
5
+ ## 0.51.0 - 2026-09-13
6
6
 
7
- ### Fixed
7
+ ### Added
8
+
9
+ - `mm-harness task init` writes the task directory (task document, checklist, mark shim, and inputs) from a shared execution template, with the MetaMask surface, harness-gated mark command, and detected platform applied by default.
10
+ - `mm-harness prepare` runs the readiness chain for a checkout (`doctor --fix`, `status`, launch, fixtures, `verify`), stops at the first failure, and records one readiness record at `<artifacts-dir>/sandbox.json` with a per-step report.
11
+
12
+ ### Changed
8
13
 
9
- - Avoid disabling and re-enabling the Extension after a fresh browser launch, which could cause `No SW` storage errors in reused wallet profiles. Keep build handoff for updates in an already-running browser.
10
- - Keep the previously focused macOS app in front while Chrome starts, instead of restoring only after CDP is up. Do not let caffeinate declare user activity during that launch.
14
+ - Pin the getting-started iOS simulator to the checkout runtime file, and stop the setup prompt from auto-creating a disposable wallet.
15
+ - Resolve the checklist a mark targets through the agent runtime: a missing `checklist-target.json` now means the default checklist and signal instead of silently disabling the terminal-signal guard, and an invalid one raises its own error.
16
+ - Require `@farmslot/agent-runtime` 0.9.0 and `@farmslot/protocol` 0.26.0.
11
17
 
12
18
  ## 0.50.4 - 2026-09-10
13
19
 
package/README.md CHANGED
@@ -121,6 +121,37 @@ Mobile and Extension recipes can bracket explicit UI-smoothness windows with
121
121
  `app.performance_assert`. Capture is never automatic. See
122
122
  [UI smoothness capture](docs/PERFORMANCE-CAPTURE.md).
123
123
 
124
+ ## Work a task
125
+
126
+ ```bash
127
+ # Pick a checklist from the shared catalog.
128
+ mm-harness execution-template list --package-templates <catalog> --json
129
+
130
+ # Write the task directory the agent works from.
131
+ mm-harness task init temp/tasks/fix/TAT-1 \
132
+ --flow fix-bug --run-mode autonomous --template fix-bug/autonomous.mobile \
133
+ --package-templates <catalog> --title "Wrong total on the receipt" --ticket TAT-1
134
+
135
+ # Bring the checkout to a proven-ready state before the first step.
136
+ mm-harness prepare --artifacts-dir temp/tasks/fix/TAT-1/artifacts --json
137
+
138
+ # Mark progress; the task's own ./mark shim routes here.
139
+ mm-harness checklist mark temp/tasks/fix/TAT-1 start
140
+ mm-harness checklist mark temp/tasks/fix/TAT-1 1
141
+ mm-harness checklist mark temp/tasks/fix/TAT-1 complete --mark-last
142
+ ```
143
+
144
+ `task init` writes `TASK.md`, `CHECKLIST.md`, the `mark` shim, and `inputs/`.
145
+ `CHECKLIST.md` is the only file whose boxes count as steps. It writes no
146
+ `checklist-target.json`; absent means `CHECKLIST.md` + `SIGNAL.json`.
147
+
148
+ `prepare` runs `doctor --fix`, `status`, `launch --verify`, `fixtures set`, and
149
+ `verify` in order, stops at the first failure, and records one readiness record
150
+ at `<artifacts-dir>/sandbox.json`. Core skips launch and fixtures; Mobile takes
151
+ `ios`/`android` from the checkout's configured target, then from the devices
152
+ `status` reports, and asks when that is ambiguous. Exit 0 means
153
+ ready. See [the task directory](site/task-dir.html).
154
+
124
155
  ## Discover and prove
125
156
 
126
157
  ```bash
@@ -301,7 +301,7 @@ const {
301
301
  runtimeIdentityArgs,
302
302
  writeRuntimeIdentity,
303
303
  } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
304
- const { captureMacFrontmostProcess, preserveMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
304
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
305
305
 
306
306
  const SLOT_ID = '${SLOT_ID}';
307
307
  const AGENT_DIR = '${AGENT_DIR}';
@@ -345,9 +345,7 @@ const resumeWebpack = () => {
345
345
 
346
346
  const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
347
347
  const previousFrontmostPid = captureMacFrontmostProcess();
348
- const holdOperatorFocus = () => preserveMacFrontmostProcess(previousFrontmostPid);
349
348
  execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
350
- holdOperatorFocus();
351
349
  for (let i = 0; i < 60; i++) {
352
350
  try {
353
351
  const version = await new Promise((resolve, reject) => {
@@ -360,7 +358,6 @@ const resumeWebpack = () => {
360
358
  });
361
359
  if (version) break;
362
360
  } catch {}
363
- holdOperatorFocus();
364
361
  await new Promise(r => setTimeout(r, 1000));
365
362
  }
366
363
  const browser = await chromium.connectOverCDP('http://127.0.0.1:' + CDP_PORT);
@@ -501,7 +498,7 @@ const resumeWebpack = () => {
501
498
  if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
502
499
  await page.bringToFront().catch(() => {});
503
500
  } else {
504
- holdOperatorFocus();
501
+ restoreMacFrontmostProcess(previousFrontmostPid);
505
502
  }
506
503
 
507
504
  fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
@@ -42,7 +42,7 @@ const {
42
42
  validationLaunchQuarantineError,
43
43
  writeRuntimeIdentity,
44
44
  } = require('./lib/chrome-args.cjs');
45
- const { captureMacFrontmostProcess, preserveMacFrontmostProcess } = require('./lib/macos-focus.cjs');
45
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
46
46
 
47
47
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
48
48
  console.log(
@@ -165,7 +165,6 @@ const chromeArgs = [
165
165
  ];
166
166
  const logFd = fs.openSync(args['chrome-log'], 'a');
167
167
  const previousFrontmostPid = captureMacFrontmostProcess();
168
- const holdOperatorFocus = () => preserveMacFrontmostProcess(previousFrontmostPid);
169
168
  let browserPid;
170
169
  try {
171
170
  const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
@@ -175,8 +174,7 @@ try {
175
174
  env: sanitizedChildEnv(),
176
175
  stdio: ['ignore', logFd, logFd],
177
176
  });
178
- holdOperatorFocus();
179
- browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir'], holdOperatorFocus);
177
+ browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
180
178
  if (browserPid === null) {
181
179
  throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
182
180
  }
@@ -188,8 +186,7 @@ try {
188
186
  stdio: ['ignore', logFd, logFd],
189
187
  });
190
188
  child.unref();
191
- holdOperatorFocus();
192
- browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir'], holdOperatorFocus);
189
+ browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
193
190
  if (browserPid === null) {
194
191
  if (child.pid) terminatePids([child.pid]);
195
192
  throw new Error(
@@ -223,13 +220,15 @@ try {
223
220
  );
224
221
  }
225
222
  if (process.platform === 'darwin') {
226
- const wake = spawn('caffeinate', ['-dims', '-w', String(browserPid)], {
223
+ const wake = spawn('caffeinate', ['-dimsu', '-w', String(browserPid)], {
227
224
  detached: true,
228
225
  stdio: 'ignore',
229
226
  });
230
227
  wake.unref();
231
228
  }
232
- holdOperatorFocus();
229
+ if (process.env.MM_HARNESS_FOCUS_BROWSER !== '1') {
230
+ restoreMacFrontmostProcess(previousFrontmostPid);
231
+ }
233
232
 
234
233
  function sanitizedChildEnv() {
235
234
  return {
@@ -265,13 +264,12 @@ function macApplicationForExecutable(executable) {
265
264
  return null;
266
265
  }
267
266
 
268
- function waitForOwnedCdpPid(port, profile, extensionDir, onWait) {
267
+ function waitForOwnedCdpPid(port, profile, extensionDir) {
269
268
  for (let i = 0; i < 300; i += 1) {
270
269
  const pid = cdpListenerPids(port).find((candidate) =>
271
270
  processIsOwnedHarnessBrowser(candidate, profile, extensionDir),
272
271
  );
273
272
  if (pid !== undefined) return pid;
274
- if (typeof onWait === 'function' && i % 10 === 0) onWait();
275
273
  spawnSync('sleep', ['0.1']);
276
274
  }
277
275
  return null;
@@ -29,13 +29,4 @@ function restoreMacFrontmostProcess(pid) {
29
29
  }
30
30
  }
31
31
 
32
- function preserveMacFrontmostProcess(pid) {
33
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
34
- return restoreMacFrontmostProcess(pid);
35
- }
36
-
37
- module.exports = {
38
- captureMacFrontmostProcess,
39
- preserveMacFrontmostProcess,
40
- restoreMacFrontmostProcess,
41
- };
32
+ module.exports = { captureMacFrontmostProcess, restoreMacFrontmostProcess };
@@ -228,6 +228,7 @@ NODE
228
228
  quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
229
229
  quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
230
230
  quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
231
+ quoted_build_handoff="$(printf '%q' "$SCRIPT_DIR/build-handoff.cjs")"
231
232
  quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
232
233
  quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
233
234
  quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
@@ -352,6 +353,7 @@ NODE
352
353
  fi
353
354
  prepare_parts+=("$chrome_launch_cmd")
354
355
  prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
356
+ prepare_parts+=("node ${quoted_build_handoff} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --cdp-port ${CDP_PORT}")
355
357
  if [ -n "$WALLET_FIXTURE_ABS" ]; then
356
358
  prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
357
359
  fi
@@ -25,7 +25,8 @@ const SPEC = {
25
25
  { 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"] },
26
26
  { name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
27
27
  { name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
28
- { name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
28
+ { name: "prepare", desc: "Run the readiness chain and write the readiness record", flags: ["--target", "--platform", "--mobile-platform", "--artifacts-dir", "--runtime-dir", "--json"] },
29
+ { name: "task", desc: "Create a task directory from a shared execution template", args: ["init"], flags: ["--flow", "--run-mode", "--platform", "--template", "--title", "--task-text", "--task-file", "--ticket", "--source-ref", "--surface", "--project", "--repo", "--attempt-id", "--var", "--task-dir-label", "--mode-preamble", "--addendum-file", "--mark-command", "--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--domain", "--json"] },
29
30
  { name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
30
31
  ],
31
32
  extension: [
package/dist/cli.js CHANGED
@@ -22,6 +22,8 @@ import { handleStatus } from "./commands/status.js";
22
22
  import { handleCheck } from "./commands/check.js";
23
23
  import { handleChecklist } from "./commands/checklist.js";
24
24
  import { handleExecutionTemplate } from "./commands/execution-template.js";
25
+ import { handlePrepare } from "./commands/prepare.js";
26
+ import { handleTaskInit } from "./commands/task-init.js";
25
27
  import { handleLast } from "./commands/last.js";
26
28
  import { parseArgs, targetPath } from "./commands/parse-args.js";
27
29
  const COMMANDS = {
@@ -127,6 +129,8 @@ async function main(argv) {
127
129
  if (command === "check") return handleCheck(argv.slice(1));
128
130
  if (command === "checklist") return handleChecklist(argv.slice(1));
129
131
  if (command === "execution-template") return handleExecutionTemplate(argv.slice(1));
132
+ if (command === "task") return handleTaskInit(argv.slice(1));
133
+ if (command === "prepare") return handlePrepare(argv.slice(1));
130
134
  if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
131
135
  const handler = COMMANDS[command];
132
136
  if (!handler) throw new Error(`Unknown command: ${command}`);
@@ -113,6 +113,49 @@ const PUBLIC_COMMAND_CONTRACTS = {
113
113
  ],
114
114
  minimumPositionals: 2
115
115
  },
116
+ prepare: {
117
+ usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--artifacts-dir <dir>] [--json]",
118
+ options: options(HELP, JSON, TARGET, {
119
+ "--platform": value(["mobile", "extension", "core"]),
120
+ "--mobile-platform": value(["ios", "android"]),
121
+ "--artifacts-dir": value()
122
+ })
123
+ },
124
+ task: {
125
+ usage: "mm-harness task init <task-dir> --flow f --run-mode m --template id --title t [options]",
126
+ options: options(HELP, JSON, {
127
+ "--flow": value(),
128
+ "--run-mode": value(["autonomous", "interactive", "validation"]),
129
+ "--platform": value(),
130
+ "--template": value(),
131
+ "--title": value(),
132
+ "--task-text": value(),
133
+ "--task-file": value(),
134
+ "--ticket": value(),
135
+ "--source-ref": value(),
136
+ "--surface": value(),
137
+ "--project": value(),
138
+ "--repo": value(),
139
+ "--attempt-id": value(),
140
+ "--var": value(),
141
+ "--task-dir-label": value(),
142
+ "--mode-preamble": value(),
143
+ "--addendum-file": value(),
144
+ "--mark-command": value(),
145
+ "--dir": value(),
146
+ "--domain-dir": value(),
147
+ "--project-worker": value(),
148
+ "--project-name": value(),
149
+ "--package-templates": value(),
150
+ "--package-id": value(),
151
+ "--domain": value()
152
+ }),
153
+ positionals: [
154
+ { label: "action", choices: ["init"] },
155
+ { label: "task-dir" }
156
+ ],
157
+ minimumPositionals: 2
158
+ },
116
159
  "execution-template": {
117
160
  usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
118
161
  options: options(HELP, JSON, {
@@ -7,6 +7,9 @@ const require2 = createRequire(import.meta.url);
7
7
  const MARK_SCRIPT = require2.resolve(
8
8
  "@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
9
9
  );
10
+ const { resolveChecklistPaths } = require2(
11
+ "@farmslot/agent-runtime/scripts/checklist-target.cjs"
12
+ );
10
13
  const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
11
14
  const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
12
15
  "--reason",
@@ -98,16 +101,14 @@ function stageLearningPackage(taskDir, step, markArgs) {
98
101
  );
99
102
  }
100
103
  function terminalSignalStatus(taskDir) {
104
+ const { signalPath } = resolveChecklistPaths(taskDir);
105
+ let signal;
101
106
  try {
102
- const target = JSON.parse(
103
- fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
104
- );
105
- const signalPath = path.resolve(taskDir, target.signal ?? "SIGNAL.json");
106
- const signal = JSON.parse(fs.readFileSync(signalPath, "utf8"));
107
- return signal.status === "complete" || signal.status === "blocked" ? signal.status : null;
107
+ signal = JSON.parse(fs.readFileSync(signalPath, "utf8"));
108
108
  } catch {
109
109
  return null;
110
110
  }
111
+ return signal.status === "complete" || signal.status === "blocked" ? signal.status : null;
111
112
  }
112
113
  function preserveTerminalSignal(taskDir, step) {
113
114
  if (step !== "start" && numericMarkStep(step) === null) return false;
@@ -122,16 +123,6 @@ function numericMarkStep(step) {
122
123
  const stepNumber = Number(step);
123
124
  return Number.isInteger(stepNumber) && stepNumber >= 1 ? stepNumber : null;
124
125
  }
125
- function checklistPath(taskDir) {
126
- try {
127
- const target = JSON.parse(
128
- fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
129
- );
130
- return path.resolve(taskDir, target.checklist ?? "CHECKLIST.md");
131
- } catch {
132
- return path.join(taskDir, "CHECKLIST.md");
133
- }
134
- }
135
126
  function checklistStepRequiresDiffPass(checklist, stepNumber) {
136
127
  let inFence = false;
137
128
  let seen = 0;
@@ -151,9 +142,10 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
151
142
  function checkDiffGateReady(taskDir, step) {
152
143
  const stepNumber = numericMarkStep(step);
153
144
  if (stepNumber === null) return true;
145
+ const { taskPath } = resolveChecklistPaths(taskDir);
154
146
  let checklist = "";
155
147
  try {
156
- checklist = fs.readFileSync(checklistPath(taskDir), "utf8");
148
+ checklist = fs.readFileSync(taskPath, "utf8");
157
149
  } catch {
158
150
  return true;
159
151
  }
@@ -0,0 +1,236 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ READINESS_RECORD
6
+ } from "@farmslot/protocol";
7
+ import { runnerProvenance } from "../doctor.js";
8
+ import { detectAdapter } from "../harness.js";
9
+ import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
10
+ import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
11
+ import { EXIT } from "./shared.js";
12
+ const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--artifacts-dir <dir>] [--json]";
13
+ const REPORT_DIR = "prepare";
14
+ const SKIPPED_AFTER_FAILURE = "previous step failed";
15
+ function reportPathFor(id) {
16
+ return path.join(REPORT_DIR, `${id}.json`);
17
+ }
18
+ function reportedError(stdout) {
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(stdout ?? "");
22
+ } catch {
23
+ return void 0;
24
+ }
25
+ const message = parsed?.error?.message;
26
+ return typeof message === "string" && message ? message : void 0;
27
+ }
28
+ function skippedStep(id, command, reason) {
29
+ return { id, command, status: "skipped", exitCode: 0, reason };
30
+ }
31
+ const STEP_MARK = { pass: "\u2713", fail: "\u2717", skipped: "\u2013" };
32
+ function stepLine(step) {
33
+ return `${STEP_MARK[step.status]} ${step.id}: ${step.status}${step.reason ? ` \u2014 ${step.reason}` : ""}`;
34
+ }
35
+ function runStep(executable, target, artifactsDir, id, args, { json }) {
36
+ const command = [executable, ...args].join(" ");
37
+ const result = spawnSync(executable, [...args], {
38
+ cwd: target,
39
+ encoding: "utf8",
40
+ // The update nudge is an interactive courtesy; it has no place in the
41
+ // stream a recorded readiness step captures.
42
+ env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
43
+ });
44
+ if (result.error) {
45
+ return { id, command, status: "fail", exitCode: 1, reason: result.error.message };
46
+ }
47
+ const report = reportPathFor(id);
48
+ const file = path.join(artifactsDir, report);
49
+ fs.mkdirSync(path.dirname(file), { recursive: true });
50
+ fs.writeFileSync(file, result.stdout ?? "");
51
+ const exitCode = result.status ?? 1;
52
+ if (exitCode === 0) {
53
+ return { id, command, status: "pass", exitCode, reportPath: report };
54
+ }
55
+ if (!json && result.stderr) process.stderr.write(result.stderr);
56
+ return {
57
+ id,
58
+ command,
59
+ status: "fail",
60
+ exitCode,
61
+ reportPath: report,
62
+ reason: reportedError(result.stdout) ?? `exited ${exitCode}`
63
+ };
64
+ }
65
+ function readDevices(artifactsDir) {
66
+ const file = path.join(artifactsDir, reportPathFor("status"));
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
70
+ } catch {
71
+ return [];
72
+ }
73
+ const devices = parsed?.devices;
74
+ return Array.isArray(devices) ? devices : [];
75
+ }
76
+ function readRuntimeContextTarget(target) {
77
+ const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
81
+ } catch {
82
+ return {};
83
+ }
84
+ const record = parsed ?? {};
85
+ const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
86
+ return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
87
+ }
88
+ function mobilePlatformFromConfiguredTarget(env, context) {
89
+ const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
90
+ const android = Boolean(
91
+ env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
92
+ );
93
+ if (ios === android) return void 0;
94
+ return ios ? "ios" : "android";
95
+ }
96
+ function mobilePlatformFromDevices(devices) {
97
+ const selected = devices.filter((device) => device?.selected === true);
98
+ const candidates = selected.length > 0 ? selected : devices;
99
+ const platforms = [
100
+ ...new Set(
101
+ candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
102
+ )
103
+ ];
104
+ return platforms.length === 1 ? platforms[0] : void 0;
105
+ }
106
+ function mobileTargetAmbiguityError() {
107
+ const choices = [
108
+ "1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
109
+ "2. Android mm-harness prepare --platform mobile --mobile-platform android"
110
+ ];
111
+ return {
112
+ code: "PREPARE_MOBILE_TARGET_AMBIGUOUS",
113
+ message: "could not infer the mobile target from the connected devices",
114
+ userAction: `choose the mobile target, then rerun this command:
115
+ ${choices.join("\n")}`
116
+ };
117
+ }
118
+ function ambiguousMobileTarget(json) {
119
+ const { code, message, userAction } = mobileTargetAmbiguityError();
120
+ if (json) {
121
+ console.log(
122
+ JSON.stringify(
123
+ {
124
+ schemaVersion: 1,
125
+ command: "prepare",
126
+ status: "fail",
127
+ exitCode: EXIT.runtime,
128
+ error: { code, message, userAction }
129
+ },
130
+ null,
131
+ 2
132
+ )
133
+ );
134
+ } else {
135
+ console.error(`\u2717 mm-harness prepare: ${message}`);
136
+ console.error(` Next: ${userAction}`);
137
+ }
138
+ return EXIT.runtime;
139
+ }
140
+ function mobilePlatformOption(value) {
141
+ if (value === void 0) return void 0;
142
+ if (value === "ios") return "ios";
143
+ if (value === "android") return "android";
144
+ throw usageError(
145
+ `--mobile-platform must be ios or android; received '${value}'
146
+ Next: ${USAGE}`
147
+ );
148
+ }
149
+ function resolvePlatform(requested, target) {
150
+ const platform = requested ?? detectAdapter(target);
151
+ if (platform === "mobile" || platform === "extension" || platform === "core") {
152
+ return platform;
153
+ }
154
+ throw usageError(
155
+ `could not detect the platform of ${target}
156
+ Next: ${USAGE}`
157
+ );
158
+ }
159
+ async function handlePrepare(argv) {
160
+ const { options } = parseArgs(argv, "prepare");
161
+ const json = optionFlag(options, "json");
162
+ const target = targetPath(options);
163
+ const platform = resolvePlatform(optionString(options, "platform"), target);
164
+ const artifactsDir = path.resolve(
165
+ optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
166
+ );
167
+ const executable = harnessExecutable();
168
+ const targetArgs = ["--target", target];
169
+ let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
170
+ const steps = [];
171
+ const failed = () => steps.some((step) => step.status === "fail");
172
+ const addStep = (step) => {
173
+ steps.push(step);
174
+ if (!json) console.log(stepLine(step));
175
+ return step;
176
+ };
177
+ const run = (id, args) => addStep(
178
+ failed() ? skippedStep(id, [executable, ...args].join(" "), SKIPPED_AFTER_FAILURE) : runStep(executable, target, artifactsDir, id, args, { json })
179
+ );
180
+ run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
181
+ run("status", ["status", "--json", ...targetArgs]);
182
+ if (platform === "mobile" && !mobilePlatform && !failed()) {
183
+ mobilePlatform = mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(readDevices(artifactsDir));
184
+ if (!mobilePlatform) return ambiguousMobileTarget(json);
185
+ }
186
+ if (platform === "core") {
187
+ const reason = "core is headless: no app surface to launch or seed";
188
+ addStep(skippedStep("launch", `${executable} launch`, reason));
189
+ addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
190
+ } else if (platform === "mobile") {
191
+ const device = mobilePlatform ? [mobilePlatform] : [];
192
+ run("launch", ["launch", ...device, "--verify", "--json", ...targetArgs]);
193
+ run("fixtures", [
194
+ "fixtures",
195
+ "set",
196
+ ...mobilePlatform ? ["--platform", mobilePlatform] : [],
197
+ "--json",
198
+ ...targetArgs
199
+ ]);
200
+ } else {
201
+ run("launch", ["launch", "--verify", "--json", ...targetArgs]);
202
+ run("fixtures", ["fixtures", "set", "--json", ...targetArgs]);
203
+ }
204
+ run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
205
+ const provenance = runnerProvenance(manifestPath(platform));
206
+ const record = {
207
+ schemaVersion: 1,
208
+ harness: {
209
+ name: provenance.packageName,
210
+ version: provenance.version,
211
+ source: `${provenance.installKind}/${provenance.packageSource}`,
212
+ executable: provenance.executablePath
213
+ },
214
+ platform,
215
+ ...mobilePlatform ? { mobilePlatform } : {},
216
+ steps,
217
+ ready: steps.every((step) => step.status !== "fail"),
218
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
219
+ };
220
+ const recordPath = path.join(artifactsDir, READINESS_RECORD);
221
+ fs.mkdirSync(artifactsDir, { recursive: true });
222
+ fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
223
+ `);
224
+ if (json) {
225
+ console.log(JSON.stringify(record, null, 2));
226
+ } else {
227
+ console.log(`readiness record: ${recordPath}`);
228
+ }
229
+ return record.ready ? EXIT.ok : EXIT.runtime;
230
+ }
231
+ export {
232
+ handlePrepare,
233
+ mobilePlatformFromConfiguredTarget,
234
+ mobilePlatformFromDevices,
235
+ mobileTargetAmbiguityError
236
+ };
@@ -0,0 +1,51 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { detectAdapter } from "../harness.js";
4
+ import { harnessExecutable } from "../paths.js";
5
+ import { EXIT } from "./shared.js";
6
+ const require2 = createRequire(import.meta.url);
7
+ const USAGE = "mm-harness task init <task-dir> --flow f --run-mode m --template id --title t [options]";
8
+ function hasOption(argv, name) {
9
+ return argv.some((arg) => arg === name || arg.startsWith(`${name}=`));
10
+ }
11
+ function metamaskDefaults(argv) {
12
+ const defaults = [];
13
+ if (!hasOption(argv, "--surface")) defaults.push("--surface", "skill");
14
+ if (!hasOption(argv, "--mark-command")) {
15
+ defaults.push("--mark-command", `${harnessExecutable()} checklist mark`);
16
+ }
17
+ if (!hasOption(argv, "--platform")) {
18
+ const adapter = detectAdapter(process.cwd());
19
+ if (adapter) defaults.push("--platform", adapter);
20
+ }
21
+ return defaults;
22
+ }
23
+ async function handleTaskInit(argv) {
24
+ if (argv[0] !== "init") {
25
+ console.error(`usage: ${USAGE}`);
26
+ return EXIT.usage;
27
+ }
28
+ const forwarded = argv.slice(1);
29
+ let entrypoint;
30
+ try {
31
+ entrypoint = require2.resolve("@farmslot/agent-runtime/scripts/task-init-cli.mjs");
32
+ } catch (error) {
33
+ console.error(
34
+ `mm-harness task init: installed @farmslot/agent-runtime does not expose the task-directory command: ${error instanceof Error ? error.message : String(error)}`
35
+ );
36
+ return EXIT.runtime;
37
+ }
38
+ const result = spawnSync(
39
+ process.execPath,
40
+ [entrypoint, ...forwarded, ...metamaskDefaults(forwarded)],
41
+ { env: process.env, stdio: "inherit" }
42
+ );
43
+ if (result.error) {
44
+ console.error(`mm-harness task init: ${result.error.message}`);
45
+ return EXIT.runtime;
46
+ }
47
+ return result.status ?? EXIT.runtime;
48
+ }
49
+ export {
50
+ handleTaskInit
51
+ };
package/dist/doctor.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "../library/actions/mobile/platform/tool-paths.mjs";
9
9
  import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
10
10
  import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
11
- import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
11
+ import { harnessExecutable, manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
12
12
  function requiredDoctorCheckSummary(checks) {
13
13
  const required = checks.filter((check) => check.required);
14
14
  const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
@@ -246,9 +246,7 @@ function runnerProvenance(actionManifestPath) {
246
246
  if (typeof version !== "string" || !version) {
247
247
  throw new Error(`Expected package version in ${packagePath}`);
248
248
  }
249
- const executablePath = path.resolve(
250
- process.env.MM_HARNESS_EXECUTABLE ?? path.join(runnerDir, "bin/mm-harness")
251
- );
249
+ const executablePath = harnessExecutable();
252
250
  const invokedPath = path.resolve(process.env.MM_HARNESS_INVOKED_AS ?? executablePath);
253
251
  const installKind = runnerInstallKind(runnerDir, invokedPath, executablePath);
254
252
  const packageSource = runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
@@ -286,5 +284,6 @@ export {
286
284
  repoShape,
287
285
  requiredDoctorCheckSummary,
288
286
  runnerInstallKind,
287
+ runnerProvenance,
289
288
  runtimeContextSummary
290
289
  };
@@ -123,7 +123,9 @@ Example:
123
123
  mark delegates checklist state to @farmslot/agent-runtime. Successful complete
124
124
  and no-change marks stage a learning package through @farmslot/handoff.
125
125
  closeout exposes that same Handoff boundary directly; --share remains explicit.
126
- The task directory must contain CHECKLIST.md and checklist-target.json.
126
+ The task directory is the one mm-harness task init wrote. Without a
127
+ checklist-target.json it marks CHECKLIST.md and writes SIGNAL.json; only a
128
+ role switch writes that manifest, to point at its own checklist.
127
129
 
128
130
  Steps: start | 1 | 2 | ... | complete | no-change | blocked
129
131
 
@@ -133,6 +135,84 @@ Example:
133
135
  mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
134
136
  mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
135
137
  },
138
+ {
139
+ name: "prepare",
140
+ summary: "Bring this checkout to a proven-ready state and write the readiness record.",
141
+ example: "mm-harness prepare --platform extension",
142
+ helpText: `mm-harness prepare [flags]
143
+
144
+ Run the readiness chain for this checkout and record what it did as one
145
+ protocol readiness record (sandbox.json). Steps run in order and stop at the
146
+ first failure; later steps are recorded as skipped.
147
+
148
+ doctor --fix status launch --verify fixtures set verify
149
+
150
+ Core skips launch and fixtures (headless; verify is its readiness gate).
151
+ Mobile infers ios|android from the connected devices reported by status and
152
+ refuses with the numbered choice when that is ambiguous.
153
+
154
+ --target <path> Checkout path (default: cwd)
155
+ --platform <mobile|extension|core> Force the platform (auto-detected inside a checkout)
156
+ --mobile-platform <ios|android> Mobile target when status cannot decide it
157
+ --artifacts-dir <dir> Where sandbox.json and the per-step reports land
158
+ (default: <target>/temp/recipe/runtime)
159
+ --json Print the readiness record
160
+
161
+ Exit 0 when the record is ready, 1 otherwise.
162
+
163
+ Example:
164
+ mm-harness prepare
165
+ mm-harness prepare --platform mobile --mobile-platform ios --json
166
+ mm-harness prepare --artifacts-dir temp/tasks/fix/TAT-1/artifacts --json`
167
+ },
168
+ {
169
+ name: "task",
170
+ summary: "Create the task directory an agent works from, out of a shared execution template.",
171
+ example: 'mm-harness task init temp/tasks/fix/TAT-1 --flow fix-bug --run-mode autonomous --template fix-bug/autonomous.mobile --title "Wrong total"',
172
+ helpText: `mm-harness task init <task-dir> [options]
173
+
174
+ Write the task directory from a selected execution template: TASK.md, the
175
+ CHECKLIST.md whose boxes are the steps, a mark shim routed through
176
+ mm-harness checklist mark, and inputs/ (handoff.json, worker-terminal-contract.json).
177
+ checklist-target.json is NOT written \u2014 absent means CHECKLIST.md + SIGNAL.json.
178
+
179
+ Applied when you do not pass them: --surface skill, --mark-command for this
180
+ executable, and --platform from the detected checkout.
181
+
182
+ Task:
183
+ --title <t> Task title (required)
184
+ --task-text <s> | --task-file <p> Task description, inline or from a file
185
+ --ticket <key> Ticket key
186
+ --source-ref <url> Where the task came from
187
+
188
+ Selection (same catalog as execution-template):
189
+ --flow <flow> Exact flow (required)
190
+ --run-mode <mode> autonomous|interactive|validation (required)
191
+ --platform <platform> Exact platform
192
+ --template <id> Exact template id (required)
193
+ --domain <domain> Exact domain
194
+ --dir <path> Custom flow-tree source (repeatable)
195
+ --domain-dir <domain=path> Domain-scoped team source (repeatable)
196
+ --package-templates <path> Canonical package flow-tree root
197
+ --package-id <id> Source label for the package root
198
+ --project-worker <path> Project template directory
199
+ --project-name <name> Source label for project templates
200
+
201
+ Identity and rendering:
202
+ --surface <s> Handoff surface (default: skill)
203
+ --project <name> --repo <owner/name> --attempt-id <id>
204
+ --var KEY=VALUE Template placeholder value (repeatable)
205
+ --task-dir-label <label> Task directory as written into the documents
206
+ --mode-preamble <text> Run-mode preamble for TASK.md
207
+ --addendum-file <path> Extra task-document section
208
+ --mark-command <cmd> Override the recorded mark command
209
+ --json Machine-readable output
210
+
211
+ Example:
212
+ mm-harness task init temp/tasks/fix/TAT-1 --flow fix-bug --run-mode autonomous \\
213
+ --template fix-bug/autonomous.mobile --package-templates <path> \\
214
+ --title "Wrong total on the receipt" --ticket TAT-1 --json`
215
+ },
136
216
  {
137
217
  name: "execution-template",
138
218
  summary: "Discover, validate, and materialize shared agent checklists.",
@@ -698,7 +778,7 @@ const HELP_GROUPS = [
698
778
  {
699
779
  title: "DAILY LOOP",
700
780
  blurb: "what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)",
701
- commands: ["status", "launch", "stop", "logs", "debug", "reload", "fixtures"]
781
+ commands: ["status", "task", "prepare", "launch", "stop", "logs", "debug", "reload", "fixtures"]
702
782
  },
703
783
  {
704
784
  title: "DISCOVER",
package/dist/paths.js CHANGED
@@ -11,6 +11,9 @@ function recipeRuntimeDir() {
11
11
  function recipeHarnessRoot() {
12
12
  return validateRelativeRecipePath("RECIPE_HARNESS_ROOT", process.env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
13
13
  }
14
+ function harnessExecutable() {
15
+ return path.resolve(process.env.MM_HARNESS_EXECUTABLE ?? path.join(runnerDir, "bin/mm-harness"));
16
+ }
14
17
  function readPathDefaults() {
15
18
  const defaultsPath = path.join(runnerDir, "adapters/shared/path-defaults.json");
16
19
  const parsed = JSON.parse(fs.readFileSync(defaultsPath, "utf8"));
@@ -170,6 +173,7 @@ export {
170
173
  DEFAULT_RECIPE_RUNTIME_DIR,
171
174
  assertAdapter,
172
175
  extensionIdPath,
176
+ harnessExecutable,
173
177
  importMetaMaskNativeUiProvider,
174
178
  importRecipeHarness,
175
179
  importRecipeHarnessAppLifecycle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.50.5",
3
+ "version": "0.51.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -20,11 +20,11 @@
20
20
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
21
21
  },
22
22
  "dependencies": {
23
- "@farmslot/agent-runtime": "0.8.1",
24
- "@farmslot/expo-recipe": "0.9.0",
23
+ "@farmslot/agent-runtime": "0.9.0",
24
+ "@farmslot/expo-recipe": "0.9.1",
25
25
  "@farmslot/handoff": "^0.3.1",
26
- "@farmslot/protocol": "0.21.0",
27
- "@farmslot/recipe-harness": "0.16.1",
26
+ "@farmslot/protocol": "0.26.0",
27
+ "@farmslot/recipe-harness": "0.16.2",
28
28
  "agent-device": "0.19.3",
29
29
  "commander": "^12.0.0",
30
30
  "es-module-lexer": "2.3.1",