@deeeed/metamask-harness 0.67.0 → 0.68.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/open-device.sh +53 -5
- package/adapters/mobile/start-metro.sh +14 -3
- package/adapters/mobile/wait-for-bridge.cjs +28 -81
- package/dist/adapters/mobile/prepare.js +5 -4
- package/dist/adapters/mobile/runtime-decision.js +3 -1
- package/dist/adapters/mobile/verify.js +57 -104
- package/dist/checkout-lock.js +62 -10
- package/dist/command-contract.js +1 -0
- package/dist/command-journal.js +52 -7
- package/dist/commands/checklist.js +13 -2
- package/dist/commands/fixtures.js +7 -7
- package/dist/commands/launch/index.js +20 -0
- package/dist/commands/prepare.js +25 -4
- package/dist/commands/run-engine.js +8 -2
- 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 +3 -1
- package/dist/task-view.js +8 -1
- package/docs/CONTRIBUTING.md +37 -0
- package/docs/QA.md +12 -0
- package/library/actions/mobile/ui/native-navigation.mjs +18 -6
- package/package.json +5 -5
package/dist/checkout-lock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureRuntimeDirectory, processIdentity } from "@farmslot/recipe-harness/runtime/operation";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -5,15 +6,19 @@ import { recipeRuntimeDir } from "./paths.js";
|
|
|
5
6
|
function acquireCheckoutLock(target, operation) {
|
|
6
7
|
const repoRoot = fs.realpathSync(target);
|
|
7
8
|
const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
|
|
8
|
-
|
|
9
|
+
ensureRuntimeDirectory(path.dirname(lockPath));
|
|
9
10
|
const inheritedToken = process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
10
11
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
11
12
|
try {
|
|
13
|
+
const identity = processIdentity(process.pid);
|
|
14
|
+
if (!identity) throw new Error("Cannot identify checkout lock owner process");
|
|
12
15
|
const fd = fs.openSync(lockPath, "wx", 384);
|
|
13
|
-
|
|
16
|
+
fs.fchmodSync(fd, 384);
|
|
17
|
+
const token = randomUUID();
|
|
14
18
|
const owner = {
|
|
15
19
|
schemaVersion: 1,
|
|
16
20
|
pid: process.pid,
|
|
21
|
+
processStartedAt: identity,
|
|
17
22
|
operation,
|
|
18
23
|
repoRoot,
|
|
19
24
|
token,
|
|
@@ -22,30 +27,38 @@ function acquireCheckoutLock(target, operation) {
|
|
|
22
27
|
fs.writeFileSync(fd, `${JSON.stringify(owner, null, 2)}
|
|
23
28
|
`);
|
|
24
29
|
fs.closeSync(fd);
|
|
25
|
-
|
|
30
|
+
process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN = token;
|
|
26
31
|
return {
|
|
27
32
|
path: lockPath,
|
|
28
33
|
release: () => {
|
|
29
34
|
const current = readOwner(lockPath);
|
|
30
|
-
if (current?.pid === process.pid && current.token === token
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (current?.pid === process.pid && current.token === token && !childrenAlive(current)) {
|
|
36
|
+
fs.rmSync(lockPath, { force: true });
|
|
37
|
+
fs.rmSync(path.join(path.dirname(lockPath), "lock-members", token), { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
if (process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN === token) {
|
|
40
|
+
if (inheritedToken) process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN = inheritedToken;
|
|
41
|
+
else delete process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
};
|
|
36
45
|
} catch (error) {
|
|
37
46
|
if (error.code !== "EEXIST") throw error;
|
|
38
47
|
const owner = readOwner(lockPath);
|
|
39
|
-
if (inheritedToken && owner?.token === inheritedToken && owner.repoRoot === repoRoot && typeof owner.pid === "number" &&
|
|
48
|
+
if (inheritedToken && owner?.token === inheritedToken && owner.repoRoot === repoRoot && typeof owner.pid === "number" && ownerAlive(owner)) {
|
|
49
|
+
trackCheckoutChild(repoRoot, process.pid);
|
|
40
50
|
return { path: lockPath, release: () => void 0 };
|
|
41
51
|
}
|
|
42
|
-
if (owner && typeof owner.pid === "number" &&
|
|
52
|
+
if (owner && typeof owner.pid === "number" && ownerAlive(owner)) {
|
|
43
53
|
return {
|
|
44
54
|
path: lockPath,
|
|
45
55
|
owner,
|
|
46
56
|
message: `checkout sandbox is busy with ${String(owner.operation ?? "another operation")} (pid ${owner.pid}).`
|
|
47
57
|
};
|
|
48
58
|
}
|
|
59
|
+
if (!owner || childrenAlive(owner)) {
|
|
60
|
+
return { path: lockPath, owner, message: `checkout sandbox owner is unreadable or has surviving child processes. Inspect ${lockPath} and its lock-members directory; remove the lock only after verifying all recorded processes have exited.` };
|
|
61
|
+
}
|
|
49
62
|
claimStaleLock(lockPath, owner);
|
|
50
63
|
}
|
|
51
64
|
}
|
|
@@ -61,12 +74,14 @@ function claimStaleLock(lockPath, expectedOwner) {
|
|
|
61
74
|
}
|
|
62
75
|
try {
|
|
63
76
|
const claimedOwner = readOwner(claimedPath);
|
|
64
|
-
if (!sameOwner(claimedOwner, expectedOwner) || claimedOwner && typeof claimedOwner.pid === "number" &&
|
|
77
|
+
if (!sameOwner(claimedOwner, expectedOwner) || claimedOwner && typeof claimedOwner.pid === "number" && (ownerAlive(claimedOwner) || childrenAlive(claimedOwner))) {
|
|
65
78
|
try {
|
|
66
79
|
fs.linkSync(claimedPath, lockPath);
|
|
67
80
|
} catch (error) {
|
|
68
81
|
if (error.code !== "EEXIST") throw error;
|
|
69
82
|
}
|
|
83
|
+
} else if (typeof claimedOwner?.token === "string" && /^[a-f0-9-]{36}$/.test(claimedOwner.token)) {
|
|
84
|
+
fs.rmSync(path.join(path.dirname(lockPath), "lock-members", claimedOwner.token), { recursive: true, force: true });
|
|
70
85
|
}
|
|
71
86
|
} finally {
|
|
72
87
|
fs.rmSync(claimedPath, { force: true });
|
|
@@ -84,6 +99,42 @@ function readOwner(lockPath) {
|
|
|
84
99
|
return null;
|
|
85
100
|
}
|
|
86
101
|
}
|
|
102
|
+
function ownerAlive(owner) {
|
|
103
|
+
if (typeof owner.pid !== "number") return true;
|
|
104
|
+
if (typeof owner.processStartedAt === "string") return processIdentity(owner.pid) === owner.processStartedAt;
|
|
105
|
+
return processAlive(owner.pid);
|
|
106
|
+
}
|
|
107
|
+
function childrenAlive(owner) {
|
|
108
|
+
if (typeof owner.repoRoot !== "string" || typeof owner.token !== "string" || !/^[a-f0-9-]{36}$/.test(owner.token)) return false;
|
|
109
|
+
const directory = path.join(owner.repoRoot, recipeRuntimeDir(), "lock-members", owner.token);
|
|
110
|
+
if (!fs.existsSync(directory)) return false;
|
|
111
|
+
return fs.readdirSync(directory).filter((name) => name.endsWith(".json")).some((name) => {
|
|
112
|
+
const child = readOwner(path.join(directory, name));
|
|
113
|
+
if (child && typeof child.processGroupId === "number" && child.processGroupId > 0) return processAlive(-child.processGroupId);
|
|
114
|
+
return !child || typeof child.pid !== "number" || typeof child.processStartedAt !== "string" || child.pid !== owner.pid && processIdentity(child.pid) === child.processStartedAt;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function trackCheckoutChild(target, pid, ownsProcessGroup = false) {
|
|
118
|
+
const repoRoot = fs.realpathSync(target);
|
|
119
|
+
const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
|
|
120
|
+
const owner = readOwner(lockPath);
|
|
121
|
+
if (!owner || owner.token !== process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN || typeof owner.token !== "string" || !/^[a-f0-9-]{36}$/.test(owner.token)) return () => void 0;
|
|
122
|
+
const identity = processIdentity(pid);
|
|
123
|
+
if (!identity && !ownsProcessGroup) return () => void 0;
|
|
124
|
+
const directory = path.join(repoRoot, recipeRuntimeDir(), "lock-members", owner.token);
|
|
125
|
+
ensureRuntimeDirectory(directory);
|
|
126
|
+
const file = path.join(directory, `${pid}.json`);
|
|
127
|
+
const previous = readOwner(file);
|
|
128
|
+
const group = ownsProcessGroup || previous?.processStartedAt === identity && previous?.processGroupId === pid;
|
|
129
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
130
|
+
fs.writeFileSync(temporary, JSON.stringify({ pid, processStartedAt: identity, ...group ? { processGroupId: pid } : {} }), { mode: 384 });
|
|
131
|
+
fs.chmodSync(temporary, 384);
|
|
132
|
+
fs.renameSync(temporary, file);
|
|
133
|
+
return () => {
|
|
134
|
+
const current = readOwner(file);
|
|
135
|
+
if (current?.pid === pid && current.processStartedAt === identity) fs.rmSync(file, { force: true });
|
|
136
|
+
};
|
|
137
|
+
}
|
|
87
138
|
function processAlive(pid) {
|
|
88
139
|
try {
|
|
89
140
|
process.kill(pid, 0);
|
|
@@ -93,5 +144,6 @@ function processAlive(pid) {
|
|
|
93
144
|
}
|
|
94
145
|
}
|
|
95
146
|
export {
|
|
96
|
-
acquireCheckoutLock
|
|
147
|
+
acquireCheckoutLock,
|
|
148
|
+
trackCheckoutChild
|
|
97
149
|
};
|
package/dist/command-contract.js
CHANGED
|
@@ -425,6 +425,7 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
425
425
|
}
|
|
426
426
|
const { name, contract } = resolved;
|
|
427
427
|
const tokens = argv.slice(1);
|
|
428
|
+
if (name === "checklist" && tokens[0] === "mark" && tokens[1] && !tokens[1].startsWith("-") && tokens[2] === "sub") return null;
|
|
428
429
|
const positionals = [];
|
|
429
430
|
const seenOptions = /* @__PURE__ */ new Set();
|
|
430
431
|
const validOptions = Object.keys(contract.options);
|
package/dist/command-journal.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { OperationRecord, readOperations, processIdentity, ensureRuntimeDirectory } from "@farmslot/recipe-harness/runtime/operation";
|
|
5
|
+
import { latestTaskDir } from "@farmslot/agent-runtime";
|
|
4
6
|
import { recipeRuntimeDir } from "./paths.js";
|
|
5
7
|
const COMMAND_JOURNAL_FILE = "last-command.json";
|
|
6
8
|
const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
|
|
9
|
+
"prepare",
|
|
7
10
|
"call",
|
|
8
11
|
"check",
|
|
9
12
|
"checklist",
|
|
@@ -12,6 +15,7 @@ const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
12
15
|
"fixtures",
|
|
13
16
|
"install",
|
|
14
17
|
"launch",
|
|
18
|
+
"runtime-launch",
|
|
15
19
|
"provision",
|
|
16
20
|
"recipe-quality",
|
|
17
21
|
"run",
|
|
@@ -25,6 +29,8 @@ async function withCommandJournal(command, argv, execute) {
|
|
|
25
29
|
if (!JOURNALED_COMMANDS.has(command)) return execute();
|
|
26
30
|
const handle = tryBeginCommandJournal(command, argv);
|
|
27
31
|
const previous = activeCommandJournal;
|
|
32
|
+
const parentId = process.env.MM_HARNESS_OPERATION_ID;
|
|
33
|
+
if (handle) process.env.MM_HARNESS_OPERATION_ID = handle.operation.value.id;
|
|
28
34
|
activeCommandJournal = handle;
|
|
29
35
|
try {
|
|
30
36
|
const exitCode = await execute();
|
|
@@ -35,8 +41,16 @@ async function withCommandJournal(command, argv, execute) {
|
|
|
35
41
|
throw error;
|
|
36
42
|
} finally {
|
|
37
43
|
activeCommandJournal = previous;
|
|
44
|
+
if (parentId) process.env.MM_HARNESS_OPERATION_ID = parentId;
|
|
45
|
+
else delete process.env.MM_HARNESS_OPERATION_ID;
|
|
38
46
|
}
|
|
39
47
|
}
|
|
48
|
+
function recordCommandStage(stage) {
|
|
49
|
+
activeCommandJournal?.operation.stage(stage);
|
|
50
|
+
}
|
|
51
|
+
function recordCommandOutput(chunk) {
|
|
52
|
+
activeCommandJournal?.operation.output(chunk);
|
|
53
|
+
}
|
|
40
54
|
function recordCommandEvidence(...evidence) {
|
|
41
55
|
const handle = activeCommandJournal;
|
|
42
56
|
if (!handle || evidence.length === 0) return;
|
|
@@ -58,7 +72,12 @@ function commandJournalPath(target, runtimeDir) {
|
|
|
58
72
|
return path.join(path.resolve(target), relative, COMMAND_JOURNAL_FILE);
|
|
59
73
|
}
|
|
60
74
|
function readCommandJournal(target, runtimeDir) {
|
|
61
|
-
const
|
|
75
|
+
const legacyFile = commandJournalPath(target, runtimeDir);
|
|
76
|
+
const operations = readOperations(path.join(path.dirname(legacyFile), "operations"));
|
|
77
|
+
const live = operations.filter((operation) => operation.status === "running" && processIdentity(operation.pid) === operation.processStartedAt);
|
|
78
|
+
const roots = operations.filter((operation) => !operation.parentId).sort((a, b) => (a.finishedAt ?? a.startedAt).localeCompare(b.finishedAt ?? b.startedAt));
|
|
79
|
+
const selected = live.find((operation) => !operation.parentId) ?? live.at(-1) ?? roots.at(-1) ?? operations.at(-1);
|
|
80
|
+
const file = selected ? path.join(path.dirname(legacyFile), "operations", `${selected.id}.journal.json`) : legacyFile;
|
|
62
81
|
let descriptor;
|
|
63
82
|
try {
|
|
64
83
|
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
@@ -113,12 +132,34 @@ function redactCommandArgs(argv) {
|
|
|
113
132
|
}
|
|
114
133
|
return redacted;
|
|
115
134
|
}
|
|
135
|
+
function pruneCompletedJournals(directory, operations) {
|
|
136
|
+
const completed = operations.filter((operation) => operation.status !== "running");
|
|
137
|
+
for (const operation of completed.slice(0, -100)) {
|
|
138
|
+
for (const suffix of [".json", ".journal.json", ".log"]) {
|
|
139
|
+
fs.rmSync(path.join(directory, `${operation.id}${suffix}`), { force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
116
143
|
function tryBeginCommandJournal(command, argv) {
|
|
117
144
|
try {
|
|
118
145
|
const target = invocationTarget(argv);
|
|
119
146
|
if (!fs.existsSync(target)) return void 0;
|
|
120
147
|
const runtimeDir = optionValue(argv, "--runtime-dir");
|
|
121
|
-
const
|
|
148
|
+
const legacyFile = commandJournalPath(target, runtimeDir);
|
|
149
|
+
const taskDir = latestTaskDir(path.join(target, "temp", "tasks"));
|
|
150
|
+
const signalFile = taskDir && path.join(taskDir, "SIGNAL.json");
|
|
151
|
+
const liveTask = signalFile && fs.existsSync(signalFile) && JSON.parse(fs.readFileSync(signalFile, "utf8")).status === "running";
|
|
152
|
+
const directory = path.join(path.dirname(legacyFile), "operations");
|
|
153
|
+
const operations = readOperations(directory, (error) => process.stderr.write(`mm-harness: ignoring invalid prior operation: ${error.message}
|
|
154
|
+
`));
|
|
155
|
+
pruneCompletedJournals(directory, operations);
|
|
156
|
+
const inherited = process.env.MM_HARNESS_OPERATION_ID;
|
|
157
|
+
const parent = inherited && operations.find((operation2) => operation2.id === inherited && operation2.target === target && operation2.status === "running" && processIdentity(operation2.pid) === operation2.processStartedAt);
|
|
158
|
+
const operation = new OperationRecord(directory, command, target, {
|
|
159
|
+
...parent ? { parentId: parent.id } : {},
|
|
160
|
+
...liveTask ? { mirrorDirectory: path.join(taskDir, "artifacts", "operations") } : {}
|
|
161
|
+
});
|
|
162
|
+
const file = path.join(directory, `${operation.value.id}.journal.json`);
|
|
122
163
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
123
164
|
const record = {
|
|
124
165
|
schemaVersion: 1,
|
|
@@ -132,7 +173,7 @@ function tryBeginCommandJournal(command, argv) {
|
|
|
132
173
|
finishedAt: null
|
|
133
174
|
};
|
|
134
175
|
writeAtomic(file, record);
|
|
135
|
-
return { file, record };
|
|
176
|
+
return { file, record, operation };
|
|
136
177
|
} catch (error) {
|
|
137
178
|
process.stderr.write(
|
|
138
179
|
`mm-harness: resumability journal unavailable: ${errorMessage(error)}
|
|
@@ -155,14 +196,17 @@ function tryFinishCommandJournal(handle, exitCode) {
|
|
|
155
196
|
`mm-harness: resumability journal could not finalize: ${errorMessage(error)}
|
|
156
197
|
`
|
|
157
198
|
);
|
|
199
|
+
} finally {
|
|
200
|
+
handle.operation.finish(exitCode);
|
|
158
201
|
}
|
|
159
202
|
}
|
|
160
203
|
function writeAtomic(file, record) {
|
|
161
|
-
|
|
204
|
+
ensureRuntimeDirectory(path.dirname(file));
|
|
162
205
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
163
206
|
let descriptor;
|
|
164
207
|
try {
|
|
165
208
|
descriptor = fs.openSync(temporary, "wx", 384);
|
|
209
|
+
fs.fchmodSync(descriptor, 384);
|
|
166
210
|
fs.writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}
|
|
167
211
|
`);
|
|
168
212
|
fs.fsyncSync(descriptor);
|
|
@@ -175,9 +219,8 @@ function writeAtomic(file, record) {
|
|
|
175
219
|
}
|
|
176
220
|
}
|
|
177
221
|
function invocationTarget(argv) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
);
|
|
222
|
+
const target = path.resolve(optionValue(argv, "--target") ?? process.cwd());
|
|
223
|
+
return fs.existsSync(target) ? fs.realpathSync(target) : target;
|
|
181
224
|
}
|
|
182
225
|
function evidencePaths(argv) {
|
|
183
226
|
const values = /* @__PURE__ */ new Set();
|
|
@@ -257,6 +300,8 @@ export {
|
|
|
257
300
|
isSensitiveKey,
|
|
258
301
|
readCommandJournal,
|
|
259
302
|
recordCommandEvidence,
|
|
303
|
+
recordCommandOutput,
|
|
304
|
+
recordCommandStage,
|
|
260
305
|
redactCommandArgs,
|
|
261
306
|
redactStructuredValue,
|
|
262
307
|
withCommandJournal
|
|
@@ -12,6 +12,7 @@ const MARK_SCRIPT = require2.resolve(
|
|
|
12
12
|
const { resolveChecklistPaths } = require2(
|
|
13
13
|
"@farmslot/agent-runtime/scripts/checklist-target.cjs"
|
|
14
14
|
);
|
|
15
|
+
const { readSubtaskIndex, SubtaskRefusal } = require2("@farmslot/agent-runtime/scripts/subtask-unit.cjs");
|
|
15
16
|
const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
|
|
16
17
|
const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
17
18
|
"--reason",
|
|
@@ -129,10 +130,10 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
|
129
130
|
const step = enumerateChecklistCheckboxes(checklist).find((item) => item.stepNumber === stepNumber);
|
|
130
131
|
return step !== void 0 && /mm-harness check diff/u.test(step.rawLabel);
|
|
131
132
|
}
|
|
132
|
-
function checkDiffGateReady(taskDir, step) {
|
|
133
|
+
function checkDiffGateReady(taskDir, step, parentChecklist) {
|
|
133
134
|
const stepNumber = numericMarkStep(step);
|
|
134
135
|
if (stepNumber === null) return true;
|
|
135
|
-
const
|
|
136
|
+
const taskPath = parentChecklist ? path.join(taskDir, parentChecklist) : resolveChecklistPaths(taskDir).taskPath;
|
|
136
137
|
let checklist = "";
|
|
137
138
|
try {
|
|
138
139
|
checklist = fs.readFileSync(taskPath, "utf8");
|
|
@@ -203,6 +204,16 @@ async function handleChecklist(argv) {
|
|
|
203
204
|
return EXIT.usage;
|
|
204
205
|
}
|
|
205
206
|
if (preserveTerminalSignal(resolvedTaskDir, step)) return EXIT.ok;
|
|
207
|
+
if (step === "sub" && forwarded[2] === "complete") {
|
|
208
|
+
try {
|
|
209
|
+
const unit = readSubtaskIndex(resolvedTaskDir)?.units.find((unit2) => unit2.id === forwarded[1]);
|
|
210
|
+
if (unit && !checkDiffGateReady(resolvedTaskDir, String(unit.parent.stepNumber), unit.parent.checklist)) return EXIT.runtime;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (!(error instanceof SubtaskRefusal)) throw error;
|
|
213
|
+
console.error(error.message);
|
|
214
|
+
return error.code;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
206
217
|
if (!checkDiffGateReady(resolvedTaskDir, step)) return EXIT.runtime;
|
|
207
218
|
teachRecipeCompletion(resolvedTaskDir, step);
|
|
208
219
|
const result = spawnSync(
|
|
@@ -12,6 +12,7 @@ import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec, sameFea
|
|
|
12
12
|
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
13
13
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
14
14
|
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
15
|
+
const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
|
|
15
16
|
const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
|
|
16
17
|
async function handleFixtures(argv) {
|
|
17
18
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
@@ -251,9 +252,9 @@ async function handleFixturesLocked(argv) {
|
|
|
251
252
|
const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
252
253
|
const waitResult = await spawnScriptStreaming(
|
|
253
254
|
waitForBridge,
|
|
254
|
-
["--target", target, "--port", port, "--platform", platform],
|
|
255
|
+
["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
|
|
255
256
|
target,
|
|
256
|
-
{ timeoutMs:
|
|
257
|
+
{ timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
|
|
257
258
|
);
|
|
258
259
|
if (waitResult.status !== 0) {
|
|
259
260
|
resetError = new Error("Mobile bridge did not return after app data reset.");
|
|
@@ -558,13 +559,12 @@ async function fixturesFinalize(adapter, target, options, json) {
|
|
|
558
559
|
}
|
|
559
560
|
return exitCode;
|
|
560
561
|
}
|
|
561
|
-
function fixturesSync(adapter, target, json) {
|
|
562
|
+
async function fixturesSync(adapter, target, json) {
|
|
562
563
|
const mmHarnessBin = path.join(runnerDir, "bin/mm-harness");
|
|
563
|
-
const installResult =
|
|
564
|
+
const installResult = await spawnScriptStreaming(
|
|
564
565
|
mmHarnessBin,
|
|
565
566
|
["install", "--adapter", adapter, "--target", target],
|
|
566
|
-
target
|
|
567
|
-
json
|
|
567
|
+
target
|
|
568
568
|
);
|
|
569
569
|
if (installResult.status !== 0) {
|
|
570
570
|
if (!json) {
|
|
@@ -577,7 +577,7 @@ function fixturesSync(adapter, target, json) {
|
|
|
577
577
|
const syncArgs = ["--target", target];
|
|
578
578
|
if (process.env.CDP_PORT) syncArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
579
579
|
if (process.env.RECIPE_SLOT_ID) syncArgs.push("--slot-id", process.env.RECIPE_SLOT_ID);
|
|
580
|
-
const syncResult =
|
|
580
|
+
const syncResult = await spawnScriptStreaming(syncFixtureSh, syncArgs, target);
|
|
581
581
|
if (syncResult.status !== 0 || fixtureSummary(target, adapter).status !== "ready") {
|
|
582
582
|
if (!json) console.error("\u2717 no usable wallet fixture source was found.\n Next: mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
583
583
|
return EXIT.runtime;
|
|
@@ -381,6 +381,26 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
381
381
|
originalError: attempt.output.trim() || void 0
|
|
382
382
|
});
|
|
383
383
|
}
|
|
384
|
+
if (adapter === "mobile" && attempt.output.includes("open-device: Android device is locked.")) {
|
|
385
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
386
|
+
code: "MOBILE_DEVICE_LOCKED",
|
|
387
|
+
message: "the selected Android device is locked.",
|
|
388
|
+
recoverable: false,
|
|
389
|
+
userAction: "Unlock the selected Android device, then retry the launch.",
|
|
390
|
+
exitCode: EXIT.runtime,
|
|
391
|
+
originalError: attempt.output.trim()
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (adapter === "mobile" && /open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(attempt.output)) {
|
|
395
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
396
|
+
code: "MOBILE_DEVICE_UI_UNAVAILABLE",
|
|
397
|
+
message: "the selected Android display could not be prepared.",
|
|
398
|
+
recoverable: false,
|
|
399
|
+
userAction: "Check adb access and the selected device screen, then retry the launch.",
|
|
400
|
+
exitCode: EXIT.runtime,
|
|
401
|
+
originalError: attempt.output.trim()
|
|
402
|
+
});
|
|
403
|
+
}
|
|
384
404
|
if (heal === "off") {
|
|
385
405
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
386
406
|
code: "LAUNCH_FAILED",
|
package/dist/commands/prepare.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { acquireCheckoutLock, trackCheckoutChild } from "../checkout-lock.js";
|
|
2
|
+
import { recordCommandStage } from "../command-journal.js";
|
|
1
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { OperationOutputTail } from "@farmslot/recipe-harness/runtime/operation";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import {
|
|
@@ -168,18 +171,19 @@ function spawnStep(executable, args, target) {
|
|
|
168
171
|
// stream a recorded readiness step captures.
|
|
169
172
|
env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
|
|
170
173
|
});
|
|
174
|
+
if (child.pid) trackCheckoutChild(target, child.pid);
|
|
171
175
|
let stdout = "";
|
|
172
|
-
|
|
176
|
+
const stderr = new OperationOutputTail();
|
|
173
177
|
child.stdout?.setEncoding("utf8");
|
|
174
178
|
child.stderr?.setEncoding("utf8");
|
|
175
179
|
child.stdout?.on("data", (chunk) => {
|
|
176
180
|
stdout += chunk;
|
|
177
181
|
});
|
|
178
182
|
child.stderr?.on("data", (chunk) => {
|
|
179
|
-
stderr
|
|
183
|
+
stderr.append(chunk);
|
|
180
184
|
});
|
|
181
|
-
child.on("error", (error) => resolve({ error, status: null, stdout, stderr }));
|
|
182
|
-
child.on("close", (status) => resolve({ status, stdout, stderr }));
|
|
185
|
+
child.on("error", (error) => resolve({ error, status: null, stdout, stderr: stderr.toString() }));
|
|
186
|
+
child.on("close", (status) => resolve({ status, stdout, stderr: stderr.toString() }));
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
async function runStep(executable, target, artifactsDir, id, args, { json, onSpawn }) {
|
|
@@ -305,6 +309,22 @@ function resolvePlatform(requested, target) {
|
|
|
305
309
|
);
|
|
306
310
|
}
|
|
307
311
|
async function handlePrepare(argv) {
|
|
312
|
+
const { options } = parseArgs(argv, "prepare");
|
|
313
|
+
const target = targetPath(options);
|
|
314
|
+
if (!fs.existsSync(target)) return handlePrepareLocked(argv);
|
|
315
|
+
const lock = acquireCheckoutLock(target, "prepare");
|
|
316
|
+
if ("message" in lock) {
|
|
317
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify({ status: "fail", error: { code: "SANDBOX_BUSY", message: lock.message }, exitCode: EXIT.runtime }));
|
|
318
|
+
else console.error(`${lock.message} Next: inspect the owning operation with mm-harness status --watch`);
|
|
319
|
+
return EXIT.runtime;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return await handlePrepareLocked(argv);
|
|
323
|
+
} finally {
|
|
324
|
+
lock.release();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function handlePrepareLocked(argv) {
|
|
308
328
|
const { options } = parseArgs(argv, "prepare");
|
|
309
329
|
const json = optionFlag(options, "json");
|
|
310
330
|
const target = targetPath(options);
|
|
@@ -372,6 +392,7 @@ async function handlePrepare(argv) {
|
|
|
372
392
|
const step = await runStep(executable, target, artifactsDir, id, args, {
|
|
373
393
|
json,
|
|
374
394
|
onSpawn: (stepStartedAt) => {
|
|
395
|
+
recordCommandStage(id);
|
|
375
396
|
activeStep = { id, startedAt: new Date(stepStartedAt).toISOString() };
|
|
376
397
|
writeProgress();
|
|
377
398
|
view?.start(id, stepStartedAt);
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
importRecipeHarness,
|
|
22
22
|
importRecipeProtocol,
|
|
23
|
-
runnerDir
|
|
23
|
+
runnerDir,
|
|
24
|
+
recipeRuntimePath
|
|
24
25
|
} from "../paths.js";
|
|
25
26
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
26
27
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
@@ -277,7 +278,11 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
277
278
|
recipeDocument: recipeDocument ?? recipe,
|
|
278
279
|
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : {},
|
|
279
280
|
...librarySources ? { librarySources } : {},
|
|
280
|
-
excludedProductRoots: [
|
|
281
|
+
excludedProductRoots: [
|
|
282
|
+
absoluteArtifactsDir,
|
|
283
|
+
recipeRuntimePath(projectRoot, "operations"),
|
|
284
|
+
recipeRuntimePath(projectRoot, "lock-members")
|
|
285
|
+
],
|
|
281
286
|
helperPaths: commandHelperPaths(recipeDocument ?? recipe, projectRoot)
|
|
282
287
|
};
|
|
283
288
|
const startProvenance = await captureExecutionProvenance(
|
|
@@ -416,6 +421,7 @@ function runtimeRecipeDocument(recipe, absoluteRecipePath) {
|
|
|
416
421
|
function recipeProcessEnvironment() {
|
|
417
422
|
const env = { ...process.env };
|
|
418
423
|
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
424
|
+
delete env.MM_HARNESS_OPERATION_ID;
|
|
419
425
|
return env;
|
|
420
426
|
}
|
|
421
427
|
function executionLibrarySources(sources, invocationTrust) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkoutBusyOut, spawnScriptStreaming } from "./shared.js";
|
|
2
|
+
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import {
|
|
@@ -23,7 +24,18 @@ import {
|
|
|
23
24
|
usageError
|
|
24
25
|
} from "./parse-args.js";
|
|
25
26
|
import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
|
|
26
|
-
async function handleRuntimeLaunch(
|
|
27
|
+
async function handleRuntimeLaunch(args) {
|
|
28
|
+
const target = targetPath(args.options);
|
|
29
|
+
if (optionFlag(args.options, "help") || !fs.existsSync(target)) return handleRuntimeLaunchLocked(args);
|
|
30
|
+
const lock = acquireCheckoutLock(target, "runtime-launch");
|
|
31
|
+
if ("message" in lock) return checkoutBusyOut(optionFlag(args.options, "json"), "runtime-launch", lock.message, lock.path);
|
|
32
|
+
try {
|
|
33
|
+
return await handleRuntimeLaunchLocked(args);
|
|
34
|
+
} finally {
|
|
35
|
+
lock.release();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function handleRuntimeLaunchLocked({ options }) {
|
|
27
39
|
if (optionFlag(options, "help")) {
|
|
28
40
|
console.log(`mm-harness runtime-launch --adapter <extension|mobile> --target <checkout> [flags]
|
|
29
41
|
|
|
@@ -186,11 +198,11 @@ async function handleRuntimeLaunch({ options }) {
|
|
|
186
198
|
cdpPort
|
|
187
199
|
}));
|
|
188
200
|
}
|
|
189
|
-
const result =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
201
|
+
const result = await spawnScriptStreaming(command[0], command.slice(1), target, {
|
|
202
|
+
forward: jsonMode ? "stderr" : "original",
|
|
203
|
+
stdin: jsonMode ? "ignore" : "inherit",
|
|
204
|
+
capture: false,
|
|
205
|
+
stage: mode
|
|
194
206
|
});
|
|
195
207
|
const summaryPath = path.join(artifactsDir, "summary.json");
|
|
196
208
|
const summary = readJsonIfExists(summaryPath);
|