@deeeed/metamask-harness 0.92.22 → 0.92.23
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 +37 -0
- package/dist/adapters/core/surface.js +2 -3
- package/dist/adapters/extension/surface.js +1 -2
- package/dist/adapters/mobile/devices.js +0 -1
- package/dist/adapters/mobile/overlay.js +3 -2
- package/dist/adapters/mobile/prepare-target.js +13 -0
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/commands/doctor.js +3 -423
- package/dist/commands/review.js +5 -66
- package/dist/doctor.js +0 -11
- package/dist/mm-harness-cli.js +50 -20
- package/dist/paths.js +0 -2
- package/dist/provider-doctor.js +3 -1
- package/dist/provider-helpers.js +143 -1
- package/dist/provider-lifecycle.js +43 -1
- package/dist/review/knowledge.js +0 -119
- package/docs/CONTRIBUTING.md +3 -2
- package/package.json +3 -3
- package/dist/commands/checklist.js +0 -242
- package/dist/commands/domain.js +0 -57
- package/dist/commands/execution-template.js +0 -28
- package/dist/commands/logs.js +0 -99
- package/dist/commands/prepare.js +0 -433
- package/dist/commands/recipe-quality.js +0 -103
- package/dist/commands/status-watch.js +0 -317
- package/dist/commands/status.js +0 -146
- package/dist/commands/task-init.js +0 -49
- package/dist/task-view.js +0 -332
package/dist/task-view.js
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { readOperations, processIdentity } from "@farmslot/recipe-runner/runtime/operation";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { HANDOFF_INPUT, latestTaskDir } from "@farmslot/agent-runtime";
|
|
6
|
-
import {
|
|
7
|
-
ACCEPTANCE_STATUS_ARTIFACT,
|
|
8
|
-
acceptanceCriteriaView,
|
|
9
|
-
acceptanceCriterionId,
|
|
10
|
-
validateAcceptanceStatusLedger
|
|
11
|
-
} from "@farmslot/protocol/contracts/acceptance";
|
|
12
|
-
import {
|
|
13
|
-
checklistStepName,
|
|
14
|
-
deriveChecklistStepDurations,
|
|
15
|
-
enumerateChecklistCheckboxes,
|
|
16
|
-
INTERACTIVE_CHECKLIST_MARKDOWN,
|
|
17
|
-
isSettledSubtaskStatus,
|
|
18
|
-
isTerminalWorkerSignalStatus,
|
|
19
|
-
READINESS_RECORD,
|
|
20
|
-
SUBTASK_INDEX_FILE,
|
|
21
|
-
SUBTASKS_DIR,
|
|
22
|
-
subtaskPaths,
|
|
23
|
-
WORKER_SIGNAL_FILE
|
|
24
|
-
} from "@farmslot/protocol";
|
|
25
|
-
import { readCommandJournal, recipeRuntimeDir, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
|
|
26
|
-
import { PREPARE_PROGRESS_ARTIFACT } from "./paths.js";
|
|
27
|
-
import { parseLibraryPathEnv } from "./review/knowledge.js";
|
|
28
|
-
const CHECKLIST_LABEL_LIMIT = 70;
|
|
29
|
-
const TASK_SILENT_AFTER_MS = 10 * 60 * 1e3;
|
|
30
|
-
function findTaskDir(target) {
|
|
31
|
-
return latestTaskDir(path.join(target, "temp", "tasks"));
|
|
32
|
-
}
|
|
33
|
-
function collectTaskView(target, taskDir, now) {
|
|
34
|
-
target = fs.existsSync(target) ? fs.realpathSync(target) : path.resolve(target);
|
|
35
|
-
taskDir = taskDir && (fs.existsSync(taskDir) ? fs.realpathSync(taskDir) : path.resolve(taskDir));
|
|
36
|
-
const taskIsRunning = taskDir && readJsonObject(path.join(taskDir, WORKER_SIGNAL_FILE))?.status === "running";
|
|
37
|
-
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 }));
|
|
38
|
-
const base = {
|
|
39
|
-
operations,
|
|
40
|
-
checkout: readCheckout(target),
|
|
41
|
-
isolation: readIsolation(target),
|
|
42
|
-
libraries: parseLibraryPathEnv(process.env.RECIPE_LIBRARY_PATH).map(
|
|
43
|
-
({ name, root }) => ({ ...name ? { name } : {}, path: root })
|
|
44
|
-
)
|
|
45
|
-
};
|
|
46
|
-
if (!taskDir) {
|
|
47
|
-
return { ...base, checklist: [], subtasks: [], activity: { ongoing: false, reason: "no-task" } };
|
|
48
|
-
}
|
|
49
|
-
const handoff = readJsonObject(path.join(taskDir, HANDOFF_INPUT));
|
|
50
|
-
const fixtures = readJsonObject(path.join(taskDir, "artifacts", "prepare", "fixtures.json"));
|
|
51
|
-
const sandbox = readJsonObject(path.join(taskDir, "artifacts", READINESS_RECORD)) ?? readJsonObject(path.join(target, recipeRuntimeDir(), READINESS_RECORD));
|
|
52
|
-
const signal = readJsonObject(path.join(taskDir, WORKER_SIGNAL_FILE));
|
|
53
|
-
const checklist = readChecklistRows(path.join(taskDir, INTERACTIVE_CHECKLIST_MARKDOWN));
|
|
54
|
-
const template = asObject(handoff?.executionTemplate);
|
|
55
|
-
const task = asObject(handoff?.task);
|
|
56
|
-
const rawCriteria = task?.acceptanceCriteria;
|
|
57
|
-
if (rawCriteria !== void 0 && !Array.isArray(rawCriteria)) {
|
|
58
|
-
throw new Error(`${path.join(taskDir, HANDOFF_INPUT)}: task.acceptanceCriteria must be an array.`);
|
|
59
|
-
}
|
|
60
|
-
const criteria = rawCriteria?.map((text, index) => ({
|
|
61
|
-
id: acceptanceCriterionId(index),
|
|
62
|
-
text: String(text)
|
|
63
|
-
})).filter((criterion) => criterion.text.trim().length > 0) ?? [];
|
|
64
|
-
const rawLedger = readJsonObject(path.join(taskDir, ACCEPTANCE_STATUS_ARTIFACT));
|
|
65
|
-
if (rawLedger) {
|
|
66
|
-
const issues = validateAcceptanceStatusLedger(rawLedger);
|
|
67
|
-
if (issues.length > 0) throw new Error(`${ACCEPTANCE_STATUS_ARTIFACT}: ${issues.join("; ")}`);
|
|
68
|
-
}
|
|
69
|
-
const startedAtIso = pickString(handoff, "startedAt");
|
|
70
|
-
const startedAt = parseTime(startedAtIso);
|
|
71
|
-
const prepare = readPrepareProgress(taskDir, now, startedAt, parseTime(pickString(sandbox, "recordedAt")));
|
|
72
|
-
const timing = applyTiming(checklist, signal, startedAtIso, now);
|
|
73
|
-
const ticket = pickString(task, "ticket");
|
|
74
|
-
const journal = readLastCommand(target, now);
|
|
75
|
-
const commandAt = parseTime(journal?.at);
|
|
76
|
-
const commandCounts = commandAt !== void 0 && (startedAt === void 0 || commandAt >= startedAt);
|
|
77
|
-
const lastCommand = commandCounts ? journal : void 0;
|
|
78
|
-
const activity = taskActivity({
|
|
79
|
-
status: pickString(signal, "status"),
|
|
80
|
-
lastWrites: [
|
|
81
|
-
startedAt,
|
|
82
|
-
parseTime(pickString(signal, "timestamp")),
|
|
83
|
-
...readMarkEvents(signal).map((event) => Date.parse(event.checkedAt)),
|
|
84
|
-
commandCounts ? commandAt : void 0,
|
|
85
|
-
prepare ? parseTime(prepare.updatedAt) : void 0,
|
|
86
|
-
...operations.flatMap((operation) => [parseTime(operation.lastOutputAt), parseTime(operation.stageStartedAt), parseTime(operation.startedAt)])
|
|
87
|
-
],
|
|
88
|
-
branch: base.checkout.branch,
|
|
89
|
-
ticket,
|
|
90
|
-
now
|
|
91
|
-
});
|
|
92
|
-
return {
|
|
93
|
-
...base,
|
|
94
|
-
taskDir,
|
|
95
|
-
// The readiness record is the only place a harness records itself for a task.
|
|
96
|
-
harness: section({
|
|
97
|
-
version: pickString(asObject(sandbox?.harness), "version"),
|
|
98
|
-
sourceKind: pickString(asObject(sandbox?.harness), "source")
|
|
99
|
-
}),
|
|
100
|
-
template: section({
|
|
101
|
-
id: pickString(template, "id"),
|
|
102
|
-
sourceId: pickString(template, "sourceId"),
|
|
103
|
-
flow: pickString(handoff, "flow"),
|
|
104
|
-
domain: pickString(handoff, "domain"),
|
|
105
|
-
surface: pickString(handoff, "surface"),
|
|
106
|
-
title: pickString(task, "title"),
|
|
107
|
-
ticket
|
|
108
|
-
}),
|
|
109
|
-
...criteria.length > 0 || rawLedger ? { acceptance: acceptanceCriteriaView(criteria, rawLedger ? rawLedger : null) } : {},
|
|
110
|
-
fixture: section({
|
|
111
|
-
fixture: pickString(fixtures, "fixture"),
|
|
112
|
-
canonicalFixture: pickString(fixtures, "canonicalFixture"),
|
|
113
|
-
status: pickString(fixtures, "status"),
|
|
114
|
-
walletReset: pickBoolean(fixtures, "walletReset")
|
|
115
|
-
}),
|
|
116
|
-
sandbox: section({ ready: pickBoolean(sandbox, "ready") }),
|
|
117
|
-
...prepare ? { prepare } : {},
|
|
118
|
-
checklist,
|
|
119
|
-
subtasks: readSubtasks(taskDir),
|
|
120
|
-
signal: section({
|
|
121
|
-
status: pickString(signal, "status"),
|
|
122
|
-
step: pickString(signal, "step"),
|
|
123
|
-
timestamp: pickString(signal, "timestamp"),
|
|
124
|
-
attemptId: pickString(signal, "attemptId")
|
|
125
|
-
}),
|
|
126
|
-
...lastCommand ? { lastCommand } : {},
|
|
127
|
-
...timing,
|
|
128
|
-
activity
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function readPrepareProgress(taskDir, now, taskStartedAt, recordedAt) {
|
|
132
|
-
const progress = readJsonObject(path.join(taskDir, "artifacts", PREPARE_PROGRESS_ARTIFACT));
|
|
133
|
-
const active = asObject(progress?.active);
|
|
134
|
-
const step = pickString(active, "id");
|
|
135
|
-
const startedAt = pickString(active, "startedAt");
|
|
136
|
-
const stepStartedAt = parseTime(startedAt);
|
|
137
|
-
const runStartedAt = parseTime(pickString(progress, "startedAt"));
|
|
138
|
-
const updatedAtIso = pickString(progress, "updatedAt");
|
|
139
|
-
const updatedAt = parseTime(updatedAtIso);
|
|
140
|
-
const total = pickNumber(progress, "total");
|
|
141
|
-
if (!step || !["doctor", "status", "launch", "fixtures", "verify"].includes(step) || stepStartedAt === void 0 || runStartedAt === void 0 || updatedAt === void 0 || now - updatedAt > 3e4 || total === void 0 || total < 1 || taskStartedAt !== void 0 && runStartedAt < taskStartedAt || recordedAt !== void 0 && recordedAt >= runStartedAt) return void 0;
|
|
142
|
-
const steps = Array.isArray(progress?.steps) ? progress.steps.flatMap((value) => {
|
|
143
|
-
const item = asObject(value);
|
|
144
|
-
const id = pickString(item, "id");
|
|
145
|
-
const status = pickString(item, "status");
|
|
146
|
-
const durationMs = pickNumber(item, "durationMs");
|
|
147
|
-
return id && ["doctor", "status", "launch", "fixtures", "verify"].includes(id) && status && ["pass", "fail", "skipped"].includes(status) && durationMs !== void 0 ? [{ id, status, durationMs }] : [];
|
|
148
|
-
}) : [];
|
|
149
|
-
return { step, startedAt, updatedAt: updatedAtIso, elapsedMs: Math.max(0, now - stepStartedAt), completed: steps.length, total, steps };
|
|
150
|
-
}
|
|
151
|
-
function readLastCommand(target, now) {
|
|
152
|
-
const { record } = readCommandJournal(target);
|
|
153
|
-
if (!record) return void 0;
|
|
154
|
-
const first = record.args[0];
|
|
155
|
-
const subject = first !== void 0 && !first.startsWith("-") ? first : void 0;
|
|
156
|
-
const at = record.finishedAt || record.startedAt;
|
|
157
|
-
const atMs = parseTime(at);
|
|
158
|
-
return {
|
|
159
|
-
command: record.command,
|
|
160
|
-
...subject ? { subject } : {},
|
|
161
|
-
verdict: record.verdict,
|
|
162
|
-
...at ? { at } : {},
|
|
163
|
-
...atMs !== void 0 ? { ageMs: Math.max(0, now - atMs) } : {}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function taskActivity(input) {
|
|
167
|
-
const writes = input.lastWrites.filter((value) => value !== void 0);
|
|
168
|
-
const idleMs = writes.length > 0 ? input.now - Math.max(...writes) : void 0;
|
|
169
|
-
const branchCarriesTicket = input.ticket && input.branch ? input.branch.includes(input.ticket) : void 0;
|
|
170
|
-
const extra = {
|
|
171
|
-
...idleMs !== void 0 ? { idleMs } : {},
|
|
172
|
-
...branchCarriesTicket !== void 0 ? { branchCarriesTicket } : {}
|
|
173
|
-
};
|
|
174
|
-
if (input.status === void 0 && writes.length === 0) return { ongoing: false, reason: "no-task", ...extra };
|
|
175
|
-
if (isTerminalWorkerSignalStatus(input.status)) return { ongoing: false, reason: "finished", ...extra };
|
|
176
|
-
if (idleMs !== void 0 && idleMs > TASK_SILENT_AFTER_MS) return { ongoing: false, reason: "silent", ...extra };
|
|
177
|
-
return { ongoing: true, ...extra };
|
|
178
|
-
}
|
|
179
|
-
function applyTiming(rows, signal, startedAtIso, now) {
|
|
180
|
-
const events = readMarkEvents(signal);
|
|
181
|
-
const durations = new Map(
|
|
182
|
-
deriveChecklistStepDurations({ schemaVersion: 1, events }, startedAtIso).map((step) => [
|
|
183
|
-
step.stepNumber,
|
|
184
|
-
step.durationMs
|
|
185
|
-
])
|
|
186
|
-
);
|
|
187
|
-
const marks = new Map(events.map((event) => [event.stepNumber, Date.parse(event.checkedAt)]));
|
|
188
|
-
for (const row of rows) {
|
|
189
|
-
const durationMs = durations.get(row.number);
|
|
190
|
-
if (durationMs !== void 0) row.durationMs = durationMs;
|
|
191
|
-
}
|
|
192
|
-
const instants = [...marks.values()];
|
|
193
|
-
const lastAt = instants.length > 0 ? Math.max(...instants) : void 0;
|
|
194
|
-
const firstAt = instants.length > 0 ? Math.min(...instants) : void 0;
|
|
195
|
-
const current = rows.find((row) => !row.checked);
|
|
196
|
-
if (current && lastAt !== void 0) current.runningMs = now - lastAt;
|
|
197
|
-
const origin = parseTime(startedAtIso) ?? firstAt;
|
|
198
|
-
return {
|
|
199
|
-
...current ? { currentStep: current.number } : {},
|
|
200
|
-
...origin !== void 0 ? { elapsedMs: now - origin } : {}
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function readMarkEvents(signal) {
|
|
204
|
-
const events = asObject(signal?.checklistTiming)?.events;
|
|
205
|
-
if (!Array.isArray(events)) return [];
|
|
206
|
-
const marks = [];
|
|
207
|
-
for (const entry of events) {
|
|
208
|
-
const event = asObject(entry);
|
|
209
|
-
const stepNumber = pickNumber(event, "stepNumber");
|
|
210
|
-
const checkedAt = pickString(event, "checkedAt");
|
|
211
|
-
if (stepNumber === void 0 || parseTime(checkedAt) === void 0) continue;
|
|
212
|
-
marks.push({ stepNumber, label: "", checkedAt });
|
|
213
|
-
}
|
|
214
|
-
return marks;
|
|
215
|
-
}
|
|
216
|
-
function readChecklistRows(file) {
|
|
217
|
-
const text = readTextFile(file);
|
|
218
|
-
if (text === void 0) return [];
|
|
219
|
-
return enumerateChecklistCheckboxes(text).map((item) => ({
|
|
220
|
-
...item.phase ? { phase: item.phase } : {},
|
|
221
|
-
number: item.stepNumber,
|
|
222
|
-
label: trimLabel(item.rawLabel),
|
|
223
|
-
checked: item.checked
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
function readSubtasks(taskDir) {
|
|
227
|
-
const index = readJsonObject(path.join(taskDir, SUBTASKS_DIR, SUBTASK_INDEX_FILE));
|
|
228
|
-
if (!index) return [];
|
|
229
|
-
const units = index.units;
|
|
230
|
-
if (!Array.isArray(units)) return [];
|
|
231
|
-
const views = [];
|
|
232
|
-
for (const entry of units) {
|
|
233
|
-
const unit = asObject(entry);
|
|
234
|
-
const id = pickString(unit, "id");
|
|
235
|
-
const parentStep = pickNumber(asObject(unit?.parent), "stepNumber");
|
|
236
|
-
if (id === void 0 || parentStep === void 0) continue;
|
|
237
|
-
const paths = subtaskPaths(id);
|
|
238
|
-
const signal = readJsonObject(path.join(taskDir, paths.signal));
|
|
239
|
-
const status = pickString(signal, "status");
|
|
240
|
-
const source = asObject(unit?.source);
|
|
241
|
-
views.push({
|
|
242
|
-
id,
|
|
243
|
-
parentStep,
|
|
244
|
-
...status !== void 0 ? { status } : {},
|
|
245
|
-
settled: isSettledSubtaskStatus(status),
|
|
246
|
-
rows: readChecklistRows(path.join(taskDir, paths.checklist)),
|
|
247
|
-
...section({ kind: pickString(source, "kind"), ref: pickString(source, "ref") }) ? { source: section({ kind: pickString(source, "kind"), ref: pickString(source, "ref") }) } : {}
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
return views;
|
|
251
|
-
}
|
|
252
|
-
function trimLabel(text) {
|
|
253
|
-
const plain = checklistStepName(text).replace(/\*\*/gu, "").replace(/^\d+\.\s*/u, "").replace(/\s+/gu, " ").trim();
|
|
254
|
-
if (plain.length <= CHECKLIST_LABEL_LIMIT) return plain;
|
|
255
|
-
return `${plain.slice(0, CHECKLIST_LABEL_LIMIT).trimEnd()}\u2026`;
|
|
256
|
-
}
|
|
257
|
-
function readCheckout(target) {
|
|
258
|
-
const branch = gitRef(target, "--abbrev-ref");
|
|
259
|
-
const head = gitRef(target, "--short");
|
|
260
|
-
return { target, ...branch ? { branch } : {}, ...head ? { head } : {} };
|
|
261
|
-
}
|
|
262
|
-
function gitRef(target, mode) {
|
|
263
|
-
const result = spawnSync("git", ["-C", target, "rev-parse", mode, "HEAD"], { encoding: "utf8" });
|
|
264
|
-
if (result.status !== 0) return void 0;
|
|
265
|
-
return result.stdout.trim() || void 0;
|
|
266
|
-
}
|
|
267
|
-
function readIsolation(target) {
|
|
268
|
-
const context = readJsonObject(resolveRuntimeContextPath(target));
|
|
269
|
-
if (!context) return void 0;
|
|
270
|
-
return section({
|
|
271
|
-
slotId: pickString(context, "slotId"),
|
|
272
|
-
platform: pickString(context, "platform"),
|
|
273
|
-
metroPort: pickNumber(context, "metroPort"),
|
|
274
|
-
watcherPort: pickNumber(context, "watcherPort"),
|
|
275
|
-
devServerPort: pickNumber(context, "devServerPort"),
|
|
276
|
-
simulator: pickString(context, "simulator"),
|
|
277
|
-
simulatorUdid: pickString(context, "simulatorUdid"),
|
|
278
|
-
cdpPort: pickNumber(context, "cdpPort"),
|
|
279
|
-
extensionId: pickString(context, "extensionId")
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
function readTextFile(file) {
|
|
283
|
-
try {
|
|
284
|
-
return fs.readFileSync(file, "utf8");
|
|
285
|
-
} catch (error) {
|
|
286
|
-
if (error.code === "ENOENT") return void 0;
|
|
287
|
-
throw error;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function readJsonObject(file) {
|
|
291
|
-
const raw = readTextFile(file);
|
|
292
|
-
if (raw === void 0) return void 0;
|
|
293
|
-
let parsed;
|
|
294
|
-
try {
|
|
295
|
-
parsed = JSON.parse(raw);
|
|
296
|
-
} catch (error) {
|
|
297
|
-
throw new Error(`${file} is not valid JSON: ${error.message}`);
|
|
298
|
-
}
|
|
299
|
-
const object = asObject(parsed);
|
|
300
|
-
if (!object) throw new Error(`${file} must contain a JSON object.`);
|
|
301
|
-
return object;
|
|
302
|
-
}
|
|
303
|
-
function asObject(value) {
|
|
304
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
305
|
-
return value;
|
|
306
|
-
}
|
|
307
|
-
function pickString(source, key) {
|
|
308
|
-
const value = source?.[key];
|
|
309
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
310
|
-
}
|
|
311
|
-
function pickNumber(source, key) {
|
|
312
|
-
const value = source?.[key];
|
|
313
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
314
|
-
}
|
|
315
|
-
function pickBoolean(source, key) {
|
|
316
|
-
const value = source?.[key];
|
|
317
|
-
return typeof value === "boolean" ? value : void 0;
|
|
318
|
-
}
|
|
319
|
-
function parseTime(value) {
|
|
320
|
-
if (value === void 0) return void 0;
|
|
321
|
-
const parsed = Date.parse(value);
|
|
322
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
323
|
-
}
|
|
324
|
-
function section(value) {
|
|
325
|
-
return Object.values(value).some((entry) => entry !== void 0) ? value : void 0;
|
|
326
|
-
}
|
|
327
|
-
export {
|
|
328
|
-
CHECKLIST_LABEL_LIMIT,
|
|
329
|
-
TASK_SILENT_AFTER_MS,
|
|
330
|
-
collectTaskView,
|
|
331
|
-
findTaskDir
|
|
332
|
-
};
|