@deeeed/metamask-harness 0.67.1 → 0.69.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 +21 -0
- package/adapters/mobile/metro-config.cjs +4 -0
- package/adapters/mobile/start-metro.sh +14 -3
- package/dist/adapters/mobile/prepare.js +12 -7
- package/dist/adapters/mobile/runtime-decision.js +11 -11
- package/dist/adapters/mobile/verify.js +56 -102
- package/dist/checkout-lock.js +62 -10
- package/dist/command-contract.js +8 -0
- package/dist/command-journal.js +52 -7
- package/dist/commands/call.js +3 -0
- package/dist/commands/checklist.js +13 -2
- package/dist/commands/fixtures.js +4 -5
- package/dist/commands/launch/index.js +19 -0
- package/dist/commands/list-executables.js +62 -11
- package/dist/commands/manifest.js +11 -4
- package/dist/commands/prepare.js +25 -4
- package/dist/commands/run-engine.js +8 -2
- package/dist/commands/run.js +3 -0
- package/dist/commands/runtime-launch.js +19 -7
- package/dist/commands/shared.js +48 -21
- package/dist/commands/status-watch.js +23 -0
- package/dist/execution-provenance.js +1 -1
- package/dist/harness.js +7 -11
- package/dist/json-stream.js +3 -0
- package/dist/live-adapter-contract.js +5 -0
- package/dist/mm-harness-cli.js +10 -1
- package/dist/task-view.js +8 -1
- package/docs/CONTRIBUTING.md +37 -0
- package/docs/RECIPES.md +10 -3
- package/library/actions/mobile/ui/native-navigation.mjs +18 -6
- package/package.json +5 -5
- package/scripts/completions.sh +2 -2
package/dist/commands/shared.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { trackCheckoutChild } from "../checkout-lock.js";
|
|
2
|
+
import { recordCommandOutput, recordCommandStage } from "../command-journal.js";
|
|
1
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { OperationOutputTail } from "@farmslot/recipe-harness/runtime/operation";
|
|
2
6
|
import path from "node:path";
|
|
3
7
|
import { detectAdapter } from "../harness.js";
|
|
4
8
|
import { colorHumanMessage } from "../cli-color.js";
|
|
@@ -81,6 +85,7 @@ function spawnScript(script, args, cwd, json, env) {
|
|
|
81
85
|
return { status: 1, output: message };
|
|
82
86
|
}
|
|
83
87
|
const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
|
|
88
|
+
recordCommandStage(path.basename(isNodeScript ? args[0] : script));
|
|
84
89
|
const result = spawnSync(invokeBin, spawnArgs, {
|
|
85
90
|
cwd,
|
|
86
91
|
encoding: "utf8",
|
|
@@ -97,6 +102,7 @@ function spawnScript(script, args, cwd, json, env) {
|
|
|
97
102
|
return { status: 1, output: message };
|
|
98
103
|
}
|
|
99
104
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
105
|
+
recordCommandOutput(output);
|
|
100
106
|
if (!json && output) process.stderr.write(output);
|
|
101
107
|
return { status: result.status ?? 1, output };
|
|
102
108
|
}
|
|
@@ -122,7 +128,7 @@ function spawnInherit(script, args, cwd) {
|
|
|
122
128
|
return result.signal === "SIGINT" || result.signal === "SIGTERM" ? 0 : 1;
|
|
123
129
|
}
|
|
124
130
|
function spawnScriptStreaming(script, args, cwd, options) {
|
|
125
|
-
const spawnOptions = options && ("env" in options || "timeoutMs" in options) ? options : { env: options };
|
|
131
|
+
const spawnOptions = options && ("env" in options || "timeoutMs" in options || "forward" in options || "capture" in options || "stdin" in options || "stage" in options) ? options : { env: options };
|
|
126
132
|
const isNodeScript = script === process.execPath && args.length > 0;
|
|
127
133
|
const stem = isNodeScript ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase() : path.basename(script).replace(/[^A-Za-z0-9]/gu, "_").toUpperCase();
|
|
128
134
|
const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
|
|
@@ -131,26 +137,41 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
131
137
|
if (shellLeafMissing(bin)) {
|
|
132
138
|
const message = `leaf could not start: ${path.basename(bin)} (ENOENT)
|
|
133
139
|
Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the shell leaf is missing or not executable`;
|
|
134
|
-
process.stderr.write(`${message}
|
|
140
|
+
if (spawnOptions.forward !== "none") process.stderr.write(`${message}
|
|
135
141
|
`);
|
|
136
142
|
return Promise.resolve({ status: 1, output: message });
|
|
137
143
|
}
|
|
138
144
|
const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
recordCommandStage(spawnOptions.stage ?? path.basename(isNodeScript ? args[0] : script));
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const ownsProcessGroup = process.platform !== "win32";
|
|
141
148
|
const child = spawn(invokeBin, spawnArgs, {
|
|
142
149
|
cwd,
|
|
143
150
|
env: spawnOptions.env ? { ...process.env, ...spawnOptions.env } : process.env,
|
|
144
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
151
|
+
stdio: [spawnOptions.stdin ?? "ignore", "pipe", "pipe"],
|
|
145
152
|
detached: ownsProcessGroup
|
|
146
153
|
});
|
|
147
|
-
let
|
|
154
|
+
let untrack;
|
|
155
|
+
try {
|
|
156
|
+
untrack = child.pid ? trackCheckoutChild(cwd, child.pid, ownsProcessGroup) : void 0;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
child.once("error", reject);
|
|
159
|
+
child.once("close", () => reject(error));
|
|
160
|
+
try {
|
|
161
|
+
if (ownsProcessGroup && child.pid) process.kill(-child.pid, "SIGKILL");
|
|
162
|
+
else child.kill("SIGKILL");
|
|
163
|
+
} catch (killError) {
|
|
164
|
+
if (killError.code !== "ESRCH") reject(killError);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const captured = new OperationOutputTail();
|
|
148
169
|
let settled = false;
|
|
149
170
|
let didTimeout = false;
|
|
150
171
|
let escalation;
|
|
151
172
|
let forwardedParentSignal;
|
|
152
173
|
let parentSignalFallback;
|
|
153
|
-
const parentSignals = ["SIGINT", "SIGTERM"];
|
|
174
|
+
const parentSignals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
154
175
|
const removeParentSignalHandlers = () => {
|
|
155
176
|
for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
|
|
156
177
|
};
|
|
@@ -182,7 +203,7 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
182
203
|
forwardedParentSignal = signal;
|
|
183
204
|
signalChildTree(signal);
|
|
184
205
|
removeParentSignalHandlers();
|
|
185
|
-
parentSignalFallback = setTimeout(() => {
|
|
206
|
+
if (Number(spawnOptions.timeoutMs) > 0) parentSignalFallback = setTimeout(() => {
|
|
186
207
|
signalChildTree("SIGKILL");
|
|
187
208
|
exitFromForwardedSignal();
|
|
188
209
|
}, 1e3);
|
|
@@ -194,31 +215,35 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
194
215
|
const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
|
|
195
216
|
didTimeout = true;
|
|
196
217
|
const message = `leaf timed out after ${String(timeoutMs)}ms: ${path.basename(script)}`;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
218
|
+
captured.append(`
|
|
219
|
+
${message}
|
|
220
|
+
`);
|
|
221
|
+
if (spawnOptions.forward !== "none") process.stderr.write(`${message}
|
|
200
222
|
`);
|
|
201
223
|
signalChildTree("SIGTERM");
|
|
202
224
|
escalation = setTimeout(() => {
|
|
203
225
|
signalChildTree("SIGKILL");
|
|
204
|
-
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
226
|
+
finish({ status: 1, output: captured.toString(), timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
205
227
|
}, 1e3);
|
|
206
228
|
}, Number(timeoutMs)) : void 0;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
229
|
+
const decoders = [new StringDecoder("utf8"), new StringDecoder("utf8")];
|
|
230
|
+
const tee = (chunk, stdout = false) => {
|
|
231
|
+
if (spawnOptions.capture !== false) captured.append(decoders[stdout ? 0 : 1].write(chunk));
|
|
232
|
+
recordCommandOutput(chunk);
|
|
233
|
+
if (spawnOptions.forward !== "none") {
|
|
234
|
+
(stdout && spawnOptions.forward === "original" ? process.stdout : process.stderr).write(chunk);
|
|
235
|
+
}
|
|
211
236
|
};
|
|
212
|
-
child.stdout?.on("data", tee);
|
|
237
|
+
child.stdout?.on("data", (chunk) => tee(chunk, true));
|
|
213
238
|
child.stderr?.on("data", tee);
|
|
214
239
|
child.on("error", (error) => {
|
|
215
240
|
const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
|
|
216
241
|
const code = error.code ?? "ESPAWN";
|
|
217
242
|
const message = `leaf could not start: ${leaf} (${code})
|
|
218
243
|
Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) \u2014 the shell leaf is missing or not executable`;
|
|
219
|
-
process.stderr.write(`${message}
|
|
244
|
+
if (spawnOptions.forward !== "none") process.stderr.write(`${message}
|
|
220
245
|
`);
|
|
221
|
-
finish({ status: 1, output: message });
|
|
246
|
+
finish({ status: 1, output: message, error });
|
|
222
247
|
});
|
|
223
248
|
child.on("exit", () => {
|
|
224
249
|
if (forwardedParentSignal) {
|
|
@@ -227,13 +252,15 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
227
252
|
}
|
|
228
253
|
if (!didTimeout) return;
|
|
229
254
|
signalChildTree("SIGKILL");
|
|
230
|
-
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
255
|
+
finish({ status: 1, output: captured.toString(), timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
231
256
|
});
|
|
232
257
|
child.on("close", (status) => {
|
|
258
|
+
if (status !== null) untrack?.();
|
|
259
|
+
if (spawnOptions.capture !== false) captured.append(decoders.map((decoder) => decoder.end()).join(""));
|
|
233
260
|
if (didTimeout) signalChildTree("SIGKILL");
|
|
234
261
|
finish({
|
|
235
262
|
status: didTimeout ? 1 : status ?? 1,
|
|
236
|
-
output,
|
|
263
|
+
output: captured.toString(),
|
|
237
264
|
...didTimeout ? { timedOut: true, timeoutMs: Number(timeoutMs) } : {}
|
|
238
265
|
});
|
|
239
266
|
});
|
|
@@ -118,6 +118,7 @@ function renderTaskView(view, title, explicitTask = false) {
|
|
|
118
118
|
last.ageMs !== void 0 && `${formatDuration(last.ageMs)} ago`
|
|
119
119
|
]));
|
|
120
120
|
}
|
|
121
|
+
lines.push(...renderOperations(view));
|
|
121
122
|
lines.push("");
|
|
122
123
|
if (!view.activity.ongoing) {
|
|
123
124
|
lines.push(out("warn", "no ongoing task detected"));
|
|
@@ -190,6 +191,12 @@ function changedLines(previous, next) {
|
|
|
190
191
|
];
|
|
191
192
|
}
|
|
192
193
|
const lines = [];
|
|
194
|
+
for (const operation of next.operations) {
|
|
195
|
+
const before2 = previous.operations.find((entry) => entry.id === operation.id);
|
|
196
|
+
if (!before2 || before2.status !== operation.status || before2.stage !== operation.stage || before2.ownerAlive !== operation.ownerAlive) {
|
|
197
|
+
lines.push(`operation ${operation.command} ${operation.status}${operation.stage ? ` \xB7 ${operation.stage}` : ""} \xB7 log ${operation.logPath}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
193
200
|
for (const step of next.prepare?.steps ?? []) {
|
|
194
201
|
if (!previous.prepare?.steps.some((prior) => prior.id === step.id && prior.status === step.status)) {
|
|
195
202
|
lines.push(`prepare ${step.id} ${step.status} (${formatDuration(step.durationMs)})`);
|
|
@@ -285,6 +292,22 @@ function shortUdid(udid) {
|
|
|
285
292
|
function joinParts(parts, separator = " \xB7 ") {
|
|
286
293
|
return parts.filter((part) => Boolean(part)).join(separator);
|
|
287
294
|
}
|
|
295
|
+
function renderOperations(view) {
|
|
296
|
+
const active = view.operations.filter((operation) => operation.status === "running" && operation.ownerAlive);
|
|
297
|
+
const selected = active.length ? active : view.operations.slice(-1);
|
|
298
|
+
return selected.flatMap((operation) => {
|
|
299
|
+
const elapsed = Date.parse(operation.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString()) - Date.parse(operation.startedAt);
|
|
300
|
+
const state = operation.status === "running" ? operation.ownerAlive ? "owner alive" : "owner exited; outcome unknown" : operation.status;
|
|
301
|
+
const age = operation.lastOutputAt ? formatDuration(Date.now() - Date.parse(operation.lastOutputAt)) + " ago" : "none recorded";
|
|
302
|
+
const quoted = "'" + operation.logPath.replace(/'/g, "'\\''") + "'";
|
|
303
|
+
return [
|
|
304
|
+
`${operation.parentId ? " " : ""}operation ${operation.command} \xB7 ${state} \xB7 ${formatDuration(elapsed)} elapsed`,
|
|
305
|
+
...operation.stage ? [` stage ${operation.stage}`] : [],
|
|
306
|
+
` last output ${age}`,
|
|
307
|
+
` follow: tail -n 50 -F ${quoted}`
|
|
308
|
+
];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
288
311
|
export {
|
|
289
312
|
changedLines,
|
|
290
313
|
formatDuration,
|
|
@@ -215,7 +215,7 @@ function gitSourceFingerprint(topLevel, pathspecs, head) {
|
|
|
215
215
|
}
|
|
216
216
|
function sourcePathspecs(git, excludedRoots, includedRoots) {
|
|
217
217
|
const exclusions = excludedRoots.flatMap((root) => {
|
|
218
|
-
const relative = path.relative(git.topLevel, path.resolve(root));
|
|
218
|
+
const relative = path.relative(git.topLevel, fs.existsSync(root) ? fs.realpathSync(root) : path.resolve(root));
|
|
219
219
|
return relative !== ".." && !relative.startsWith(`..${path.sep}`) ? [`:(exclude)${relative}`] : [];
|
|
220
220
|
});
|
|
221
221
|
const inclusions = includedRoots.length > 0 ? includedRoots.map((root) => path.join(git.pathspec === "." ? "" : git.pathspec, root)) : [git.pathspec];
|
package/dist/harness.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnScriptStreaming } from "./commands/shared.js";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
5
6
|
import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
|
|
6
7
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
7
8
|
import { colorHumanMessage } from "./cli-color.js";
|
|
@@ -367,15 +368,10 @@ async function handleHarness(argv) {
|
|
|
367
368
|
}
|
|
368
369
|
return 1;
|
|
369
370
|
}
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const result = spawnSync(dispatchBin, dispatchArgs, {
|
|
375
|
-
// Default: stream child output verbatim (byte-identical to the skill path).
|
|
376
|
-
// --json: route child output to our stderr so stdout carries only the summary.
|
|
377
|
-
stdio: quiet ? "ignore" : json ? ["inherit", 2, "inherit"] : "inherit",
|
|
378
|
-
env: process.env
|
|
371
|
+
const result = await spawnScriptStreaming(dispatch.command, [...dispatch.prefixArgs, ...forwardArgs], target, {
|
|
372
|
+
forward: quiet ? "none" : json ? "stderr" : "original",
|
|
373
|
+
stdin: quiet ? "ignore" : "inherit",
|
|
374
|
+
capture: false
|
|
379
375
|
});
|
|
380
376
|
if (result.error) {
|
|
381
377
|
if (quiet) {
|
package/dist/json-stream.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordCommandStage } from "./command-journal.js";
|
|
1
2
|
class JsonStreamWriter {
|
|
2
3
|
enabled;
|
|
3
4
|
command;
|
|
@@ -32,9 +33,11 @@ class JsonStreamWriter {
|
|
|
32
33
|
}));
|
|
33
34
|
}
|
|
34
35
|
phase(phase, fields = {}) {
|
|
36
|
+
recordCommandStage(phase);
|
|
35
37
|
this.emit("phase", { phase, ...fields });
|
|
36
38
|
}
|
|
37
39
|
node(nodeId, action, status) {
|
|
40
|
+
if (status === "running") recordCommandStage(`${nodeId}: ${action}`);
|
|
38
41
|
this.emit("node", { nodeId, action, status });
|
|
39
42
|
}
|
|
40
43
|
mutation(mutation) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { recordCommandOutput } from "./command-journal.js";
|
|
2
|
+
import { trackCheckoutChild } from "./checkout-lock.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
4
|
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -95,6 +97,7 @@ function runProcess(command, args, options) {
|
|
|
95
97
|
env: options.env,
|
|
96
98
|
stdio: [options.stdin == null ? "ignore" : "pipe", "pipe", "pipe"]
|
|
97
99
|
});
|
|
100
|
+
if (child.pid) trackCheckoutChild(options.cwd, child.pid);
|
|
98
101
|
if (options.stdin != null) {
|
|
99
102
|
child.stdin.on("error", (error) => {
|
|
100
103
|
if (error.code === "EPIPE" || settled) return;
|
|
@@ -107,9 +110,11 @@ function runProcess(command, args, options) {
|
|
|
107
110
|
let stderr = "";
|
|
108
111
|
child.stdout.on("data", (chunk) => {
|
|
109
112
|
stdout += chunk;
|
|
113
|
+
recordCommandOutput(chunk);
|
|
110
114
|
});
|
|
111
115
|
child.stderr.on("data", (chunk) => {
|
|
112
116
|
stderr += chunk;
|
|
117
|
+
recordCommandOutput(chunk);
|
|
113
118
|
});
|
|
114
119
|
const timeout = options.timeoutMs ? setTimeout(() => {
|
|
115
120
|
if (settled) return;
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -245,6 +245,8 @@ Example:
|
|
|
245
245
|
Example:
|
|
246
246
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> start
|
|
247
247
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
|
|
248
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> sub start qa-cases --step 2 --from inputs/qa-cases.md
|
|
249
|
+
mm-harness checklist mark temp/tasks/recipe-cook/<task> sub qa-cases 1
|
|
248
250
|
mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
|
|
249
251
|
mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
|
|
250
252
|
},
|
|
@@ -296,6 +298,7 @@ Example:
|
|
|
296
298
|
Task:
|
|
297
299
|
--title <t> Task title (required)
|
|
298
300
|
--task-text <s> | --task-file <p> Task description, inline or from a file
|
|
301
|
+
--acceptance <criterion> Authored acceptance criterion (repeatable, preserved in order)
|
|
299
302
|
--ticket <key> Ticket key
|
|
300
303
|
--source-ref <url> Where the task came from
|
|
301
304
|
|
|
@@ -428,6 +431,9 @@ Example:
|
|
|
428
431
|
mm-harness actions.
|
|
429
432
|
|
|
430
433
|
--list List actions accepted by call for this adapter; no <action> needed
|
|
434
|
+
--domain <name> With --list: filter by action domain, e.g. perps or swap-bridge
|
|
435
|
+
--source <name> With --list: filter by origin, e.g. metamask, perps, or official
|
|
436
|
+
--sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
|
|
431
437
|
--arg k=v Action field value (repeatable; equivalent to key=value shorthand)
|
|
432
438
|
--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.
|
|
433
439
|
--cdp-port <port> Extension only: bind the action to this live browser runtime.
|
|
@@ -462,6 +468,9 @@ Remote feature flags (mobile applies at runtime; extension pins at launch):
|
|
|
462
468
|
|
|
463
469
|
Discovery:
|
|
464
470
|
--list List complete recipes accepted by run for this adapter; no <recipe> needed
|
|
471
|
+
--domain <name> With --list: filter by recipe domain, e.g. perps or swap-bridge
|
|
472
|
+
--source <name> With --list: filter by library origin, e.g. metamask or perps
|
|
473
|
+
--sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
|
|
465
474
|
--describe Show one recipe's parameters, composed actions/recipes, and runnable command
|
|
466
475
|
--plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
|
|
467
476
|
--proof Preflight behavioral proof bindings before planning/execution; runtime evidence is still required.
|
|
@@ -506,7 +515,7 @@ Example:
|
|
|
506
515
|
replace it, and an interrupted process remains recorded as verdict=running.
|
|
507
516
|
|
|
508
517
|
--target <path> Checkout path (default: cwd)
|
|
509
|
-
--runtime-dir <dir> Runtime dir containing
|
|
518
|
+
--runtime-dir <dir> Runtime dir containing command operation records
|
|
510
519
|
--json Machine-readable envelope
|
|
511
520
|
|
|
512
521
|
Example:
|
package/dist/task-view.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readOperations, processIdentity } from "@farmslot/recipe-harness/runtime/operation";
|
|
1
2
|
import { spawnSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -31,7 +32,12 @@ function findTaskDir(target) {
|
|
|
31
32
|
return latestTaskDir(path.join(target, "temp", "tasks"));
|
|
32
33
|
}
|
|
33
34
|
function collectTaskView(target, taskDir, now) {
|
|
35
|
+
target = fs.existsSync(target) ? fs.realpathSync(target) : path.resolve(target);
|
|
36
|
+
taskDir = taskDir && (fs.existsSync(taskDir) ? fs.realpathSync(taskDir) : path.resolve(taskDir));
|
|
37
|
+
const taskIsRunning = taskDir && readJsonObject(path.join(taskDir, WORKER_SIGNAL_FILE))?.status === "running";
|
|
38
|
+
const operations = readOperations(path.join(target, recipeRuntimeDir(), "operations")).filter((operation) => !taskIsRunning || path.resolve(operation.logPath).startsWith(path.resolve(taskDir, "artifacts", "operations") + path.sep)).map((operation) => ({ ...operation, ownerAlive: operation.status === "running" && processIdentity(operation.pid) === operation.processStartedAt }));
|
|
34
39
|
const base = {
|
|
40
|
+
operations,
|
|
35
41
|
checkout: readCheckout(target),
|
|
36
42
|
isolation: readIsolation(target),
|
|
37
43
|
libraries: parseLibraryPathEnv(process.env.RECIPE_LIBRARY_PATH).map(
|
|
@@ -77,7 +83,8 @@ function collectTaskView(target, taskDir, now) {
|
|
|
77
83
|
parseTime(pickString(signal, "timestamp")),
|
|
78
84
|
...readMarkEvents(signal).map((event) => Date.parse(event.checkedAt)),
|
|
79
85
|
commandCounts ? commandAt : void 0,
|
|
80
|
-
prepare ? parseTime(prepare.updatedAt) : void 0
|
|
86
|
+
prepare ? parseTime(prepare.updatedAt) : void 0,
|
|
87
|
+
...operations.flatMap((operation) => [parseTime(operation.lastOutputAt), parseTime(operation.stageStartedAt), parseTime(operation.startedAt)])
|
|
81
88
|
],
|
|
82
89
|
branch: base.checkout.branch,
|
|
83
90
|
ticket,
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -149,6 +149,43 @@ bash tests/contract/run.sh
|
|
|
149
149
|
Runtime changes also require real affected-platform QA and a packed installation
|
|
150
150
|
test. Update `CHANGELOG.md` under `Unreleased` for user-visible behavior.
|
|
151
151
|
|
|
152
|
+
When upgrading the protocol or runtime, align their transitive consumers before
|
|
153
|
+
pinning them here. Farmslot's `workspace:*` dependencies become exact versions
|
|
154
|
+
when packed, so `agent-runtime` and `expo-recipe` can retain older copies until
|
|
155
|
+
republished. Verify the complete install with
|
|
156
|
+
`bash tests/contract/packaging-dist-install.test.sh` before releasing the harness.
|
|
157
|
+
|
|
152
158
|
Docs describe only current behavior. History belongs in the changelog and Git;
|
|
153
159
|
do not add handovers, audits, speculative roadmaps, or duplicate command guides
|
|
154
160
|
under `docs/`.
|
|
161
|
+
|
|
162
|
+
## Long command observations
|
|
163
|
+
|
|
164
|
+
`mm-harness status --watch` shows command ownership, stage, elapsed time, last
|
|
165
|
+
output, and a `tail -n 50 -F` command for each active operation. Records and logs
|
|
166
|
+
live under `temp/recipe/runtime/operations/`. During a running task, records and
|
|
167
|
+
logs also appear under its `artifacts/operations/` directory for Command Center.
|
|
168
|
+
|
|
169
|
+
Use `mm-harness last --json` to resolve the current journal path. Each invocation
|
|
170
|
+
writes its own journal; new commands no longer write `last-command.json`. The
|
|
171
|
+
reader still accepts that legacy file when no operation records exist. A running
|
|
172
|
+
parent remains visible while its child or a rejected competing command finishes.
|
|
173
|
+
|
|
174
|
+
An owner process being alive, recent log output, and a stage change are separate
|
|
175
|
+
observations. None proves application readiness. Reading status or following logs
|
|
176
|
+
does not refresh activity or take the checkout mutation lock.
|
|
177
|
+
|
|
178
|
+
The checkout keeps the latest 100 completed invocation records. Task-local mirrors
|
|
179
|
+
remain with their task artifacts. Streaming leaves keep full output on disk;
|
|
180
|
+
commands that need captured output for classification keep a bounded tail.
|
|
181
|
+
`fixtures sync --json` streams progress to stderr while reserving stdout for JSON.
|
|
182
|
+
Interactive harness leaves inherit stdin; their stdout/stderr are piped for logs,
|
|
183
|
+
so child TTY formatting may differ from a direct launch.
|
|
184
|
+
|
|
185
|
+
Streaming leaves run in tracked process groups. Ordinary interrupt, termination,
|
|
186
|
+
hangup and quit signals are forwarded. Non-timed leaves may finish their cleanup
|
|
187
|
+
without a new forced-kill deadline. SIGKILL cannot be forwarded: if the caller's
|
|
188
|
+
group is forcibly killed, its detached build may survive and the checkout remains
|
|
189
|
+
reserved until that recorded group exits. Inspect the recorded group before
|
|
190
|
+
manual cleanup. Group-ID reuse can conservatively retain an old crash lock; the
|
|
191
|
+
lock's recovery hint describes what to inspect.
|
package/docs/RECIPES.md
CHANGED
|
@@ -542,12 +542,19 @@ selected deterministically from `recipes/<adapter>/<domain>/`. Identical
|
|
|
542
542
|
cross-adapter graphs live once in `recipes/shared/<domain>/`; an adapter-specific
|
|
543
543
|
variant takes precedence. Both resolve as `<domain>.<recipe>`, without a platform
|
|
544
544
|
or `shared` prefix. Keep domains below `core`, `extension`, `mobile`, or `shared`.
|
|
545
|
-
|
|
546
|
-
`*.<adapter>.recipe.json` paths remain readable during migration, but new
|
|
547
|
-
migrated libraries use the adapter-first layout. A file cannot declare its
|
|
545
|
+
|
|
546
|
+
Legacy `*.<adapter>.recipe.json` paths remain readable during migration, but new
|
|
547
|
+
and migrated libraries use the adapter-first layout. A file cannot declare its
|
|
548
548
|
adapter through both forms. Every run records the root recipe, exact resolved
|
|
549
549
|
dependency documents, their digests, call edges, selected sources, and shadows.
|
|
550
550
|
|
|
551
|
+
`mm-harness run --list` and `mm-harness call --list` group recipes/actions by domain
|
|
552
|
+
and show their library origins separately.
|
|
553
|
+
Use `--domain swap-bridge` or `--source perps` to filter, and `--sort name|domain|library`
|
|
554
|
+
to change the order. These options also apply to `--json` output, which keeps name
|
|
555
|
+
ordering unless `--sort` is supplied. Domain filters accept hyphens or underscores,
|
|
556
|
+
so `--domain swap-bridge` includes `metamask.swap_bridge.*` actions.
|
|
557
|
+
|
|
551
558
|
## Direct product access
|
|
552
559
|
|
|
553
560
|
The bundled library is intentionally small. A task may use a verified controller
|
|
@@ -159,6 +159,18 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Release builds may omit the mode toggle when Pro is disabled. The distinct
|
|
163
|
+
// visible market screen still proves its mode; a chooser or conflicting screen does not.
|
|
164
|
+
export function visiblePerpsModeScreen(ids, mode) {
|
|
165
|
+
const expectedScreen = mode === 'pro' ? IDS.proMarket : IDS.market;
|
|
166
|
+
const oppositeScreen = mode === 'pro' ? IDS.market : IDS.proMarket;
|
|
167
|
+
const expectedToggle = mode === 'pro' ? IDS.modeTogglePro : IDS.modeToggleLite;
|
|
168
|
+
const oppositeToggle = mode === 'pro' ? IDS.modeToggleLite : IDS.modeTogglePro;
|
|
169
|
+
if (ids.has(IDS.modeLite) || ids.has(IDS.modePro) || ids.has(oppositeScreen) || ids.has(oppositeToggle)) return undefined;
|
|
170
|
+
if (ids.has(expectedScreen)) return expectedScreen;
|
|
171
|
+
return ids.has(expectedToggle) ? expectedToggle : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
export async function ensureVisibleAndroidPerpsMode(input, perpsMode) {
|
|
163
175
|
if (!['lite', 'pro'].includes(perpsMode)) {
|
|
164
176
|
throw new Error('Opaque Mobile Perps mode must be lite or pro.');
|
|
@@ -218,7 +230,6 @@ export async function ensureVisibleAndroidPerpsMode(input, perpsMode) {
|
|
|
218
230
|
|
|
219
231
|
try {
|
|
220
232
|
const deadline = Date.now() + Number(input.node?.timeout_ms ?? 30_000);
|
|
221
|
-
const expectedToggle = perpsMode === 'pro' ? IDS.modeTogglePro : IDS.modeToggleLite;
|
|
222
233
|
const currentToggle = perpsMode === 'pro' ? IDS.modeToggleLite : IDS.modeTogglePro;
|
|
223
234
|
const option = perpsMode === 'pro' ? IDS.modePro : IDS.modeLite;
|
|
224
235
|
let expectedStreak = 0;
|
|
@@ -240,12 +251,13 @@ export async function ensureVisibleAndroidPerpsMode(input, perpsMode) {
|
|
|
240
251
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
241
252
|
continue;
|
|
242
253
|
}
|
|
243
|
-
|
|
254
|
+
const observedScreen = visiblePerpsModeScreen(ids, perpsMode);
|
|
255
|
+
if (observedScreen) {
|
|
244
256
|
expectedStreak += 1;
|
|
245
257
|
if (expectedStreak >= 2) {
|
|
246
258
|
return {
|
|
247
259
|
mode: perpsMode,
|
|
248
|
-
observedScreen
|
|
260
|
+
observedScreen,
|
|
249
261
|
provider: 'android-native-accessibility',
|
|
250
262
|
proofPath: 'visible-native-navigation',
|
|
251
263
|
};
|
|
@@ -294,7 +306,6 @@ export async function ensureVisibleIosPerpsMode(input, perpsMode) {
|
|
|
294
306
|
if (!['lite', 'pro'].includes(perpsMode)) {
|
|
295
307
|
throw new Error('iOS Perps mode must be lite or pro.');
|
|
296
308
|
}
|
|
297
|
-
const expectedToggle = perpsMode === 'pro' ? IDS.modeTogglePro : IDS.modeToggleLite;
|
|
298
309
|
const currentToggle = perpsMode === 'pro' ? IDS.modeToggleLite : IDS.modeTogglePro;
|
|
299
310
|
const option = perpsMode === 'pro' ? IDS.modePro : IDS.modeLite;
|
|
300
311
|
const deadline = Date.now() + Number(input.node?.timeout_ms ?? 30_000);
|
|
@@ -324,12 +335,13 @@ export async function ensureVisibleIosPerpsMode(input, perpsMode) {
|
|
|
324
335
|
const items = observed.observations?.['ui.visible']?.items ?? [];
|
|
325
336
|
const ids = new Set(items.map((item) => String(item.test_id ?? '')));
|
|
326
337
|
lastObserved = [...ids].filter(Boolean).slice(0, 20);
|
|
327
|
-
|
|
338
|
+
const observedScreen = visiblePerpsModeScreen(ids, perpsMode);
|
|
339
|
+
if (observedScreen) {
|
|
328
340
|
expectedStreak += 1;
|
|
329
341
|
if (expectedStreak >= 2) {
|
|
330
342
|
return {
|
|
331
343
|
mode: perpsMode,
|
|
332
|
-
observedScreen
|
|
344
|
+
observedScreen,
|
|
333
345
|
provider: 'ios-native-accessibility',
|
|
334
346
|
proofPath: 'visible-native-navigation',
|
|
335
347
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.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.15.
|
|
24
|
-
"@farmslot/expo-recipe": "0.12.
|
|
23
|
+
"@farmslot/agent-runtime": "0.15.1",
|
|
24
|
+
"@farmslot/expo-recipe": "0.12.2",
|
|
25
25
|
"@farmslot/handoff": "^0.3.1",
|
|
26
|
-
"@farmslot/protocol": "0.
|
|
27
|
-
"@farmslot/recipe-harness": "0.
|
|
26
|
+
"@farmslot/protocol": "0.33.0",
|
|
27
|
+
"@farmslot/recipe-harness": "0.21.1",
|
|
28
28
|
"agent-device": "0.19.3",
|
|
29
29
|
"commander": "^12.0.0",
|
|
30
30
|
"es-module-lexer": "2.3.1",
|
package/scripts/completions.sh
CHANGED
|
@@ -44,8 +44,8 @@ _mmh_flags_for() {
|
|
|
44
44
|
execution-template) printf '%s' "--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" ;;
|
|
45
45
|
recipe-quality) printf '%s' "--input --output --recipe --task --evidence --params --adapter --library --advisor --advisor-model --advisor-stages --json" ;;
|
|
46
46
|
actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
|
|
47
|
-
call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
48
|
-
run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
47
|
+
call) printf '%s' "--list --domain --source --sort --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
48
|
+
run) printf '%s' "--list --domain --source --sort --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
49
49
|
doctor) printf '%s' "--adapter --target --fix --json" ;;
|
|
50
50
|
install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
|
|
51
51
|
*) printf '%s' "" ;;
|