@deeeed/metamask-harness 0.61.1 → 0.63.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 +25 -0
- package/README.md +7 -0
- package/adapters/mobile/open-device.sh +63 -35
- package/dist/advisor-policy.js +40 -0
- package/dist/assessment-providers.js +12 -0
- package/dist/cli-commands.js +2 -2
- package/dist/command-contract.js +11 -3
- package/dist/commands/config.js +14 -9
- package/dist/commands/prepare.js +30 -2
- package/dist/commands/recipe-advice.js +76 -0
- package/dist/commands/recipe-quality.js +8 -1
- package/dist/commands/run-engine.js +1 -0
- package/dist/commands/status-watch.js +43 -7
- package/dist/commands/status.js +8 -1
- package/dist/devices.js +4 -2
- package/dist/mm-harness-cli.js +42 -8
- package/dist/paths.js +2 -0
- package/dist/recipe-assistance-context.js +149 -0
- package/dist/recipe-assistance-rubric.js +46 -0
- package/dist/recipe-assistance.js +68 -0
- package/dist/review/knowledge.js +13 -0
- package/dist/semantic-assessment.js +0 -0
- package/dist/task-view.js +45 -2
- package/dist/typesafe-client.js +57 -0
- package/docs/QA.md +2 -1
- package/docs/RECIPES.md +79 -0
- package/docs/SECURITY.md +8 -0
- package/package.json +1 -1
- package/scripts/completions.sh +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.63.0 - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Show the task's acceptance criteria and recorded verdicts in `status`, `status --watch`, and `status --task`, using the Farmslot handoff and acceptance ledger.
|
|
10
|
+
- Show the active preparation step and elapsed time in `status --watch` and `status --task` even when the agent buffers preparation output.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Allow 30 seconds for iOS simulator discovery so a cold CoreSimulator startup does not fail at five seconds.
|
|
15
|
+
|
|
16
|
+
## 0.62.0 - 2026-09-22
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `MM_HARNESS_SIMULATOR_APP` can pin a standalone Simulator from another installed Xcode without changing build tools; an invalid preference fails instead of selecting a different viewer.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- iOS launch opens the assigned simulator window through Device Hub on Xcode 27, including when Device Hub is already running. Older Xcode versions keep using Simulator. GUI launch errors stay visible instead of requiring a headless workaround. Explicit foreground opening no longer needs AppleScript permission, and fails the launch if it cannot open the UI.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `recipe-quality plan` maps acceptance criteria to existing adapter-supported actions and composable library recipes before authoring; `recipe-quality advise` reviews drafts and supplied evidence. Both offer opt-in semantic assessment through `--advisor <provider>` with configurable `--advisor-model`; TypeSafe is the first provider. Engineer settings and CLI/environment overrides share one resolver exposed by `recipe-quality advisor-status`, defaulting to disabled and planning only. They record provider/model provenance, write separate advisory results, and skip missing keys or report provider unavailability without changing normal execution or canonical quality.
|
|
29
|
+
|
|
5
30
|
## 0.61.1 - 2026-09-21
|
|
6
31
|
|
|
7
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -237,6 +237,13 @@ mm-harness run --list
|
|
|
237
237
|
Shared libraries hold durable actions and composable recipes. Task acceptance
|
|
238
238
|
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
239
239
|
|
|
240
|
+
For optional model advice matching acceptance criteria to library flows and assessing
|
|
241
|
+
drafts or evidence, see [Optional recipe advisor](docs/RECIPES.md#optional-recipe-advisor).
|
|
242
|
+
Select a provider with `--advisor typesafe` or save `advisor.enabled` and
|
|
243
|
+
`advisor.provider` through `mm-harness config`. Inspect the effective policy with
|
|
244
|
+
`mm-harness recipe-quality advisor-status --json`; use `--advisor off` per task.
|
|
245
|
+
Ordinary recipe execution needs no advisor or API key.
|
|
246
|
+
|
|
240
247
|
Exporting is not required: a library registered with `mm-harness config set
|
|
241
248
|
libraries.<name> <path>`, or checked out beside the product checkout, reaches
|
|
242
249
|
`run`, `actions`, and template discovery too. `mm-harness status` lists what this
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
# --adb-serial <serial> Android device serial (default: ADB_SERIAL env)
|
|
15
15
|
# --preflight-mode fast|auto|rebuild-native|clean (default: fast; env MOBILE_PREFLIGHT_MODE)
|
|
16
16
|
# --restart terminate the selected app before opening it
|
|
17
|
-
# --ui-only iOS only:
|
|
17
|
+
# --ui-only iOS only: show the assigned simulator window without relaunching the app
|
|
18
|
+
# MM_HARNESS_SIMULATOR_APP absolute Simulator.app or DeviceHub.app path; GUI only
|
|
19
|
+
# MM_HARNESS_SHOW_SIMULATOR=0 opt out of automatic GUI opening
|
|
20
|
+
# MM_HARNESS_FOCUS_SIMULATOR=1 explicitly open/activate the GUI, even with SHOW_SIMULATOR=0
|
|
18
21
|
# Outputs:
|
|
19
22
|
# Progress on stderr; exit 0 pass; 1 open failed; 2 bad args.
|
|
20
23
|
#
|
|
@@ -273,47 +276,71 @@ boot_simulator_if_needed() {
|
|
|
273
276
|
xcrun simctl bootstatus "$udid" -b >/dev/null
|
|
274
277
|
}
|
|
275
278
|
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
resolve_simulator_ui_app() {
|
|
280
|
+
local preferred_app="${MM_HARNESS_SIMULATOR_APP:-}"
|
|
281
|
+
if [ -n "$preferred_app" ]; then
|
|
282
|
+
preferred_app="${preferred_app%/}"
|
|
283
|
+
case "$preferred_app" in
|
|
284
|
+
/*/Simulator.app|/*/DeviceHub.app)
|
|
285
|
+
if [ -d "$preferred_app" ]; then
|
|
286
|
+
printf '%s\n' "$preferred_app"
|
|
287
|
+
return 0
|
|
288
|
+
fi
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
printf 'open-device: MM_HARNESS_SIMULATOR_APP must name an existing absolute Simulator.app or DeviceHub.app path.\nNext: ls -d %q\n' "$preferred_app" >&2
|
|
292
|
+
return 1
|
|
293
|
+
fi
|
|
294
|
+
local developer_dir="${DEVELOPER_DIR:-}"
|
|
295
|
+
if [ -z "$developer_dir" ]; then
|
|
296
|
+
developer_dir="$(xcode-select -p)" || return 1
|
|
297
|
+
fi
|
|
298
|
+
developer_dir="${developer_dir%/}"
|
|
299
|
+
case "$developer_dir" in *.app) developer_dir="$developer_dir/Contents/Developer" ;; esac
|
|
300
|
+
if [ -d "$developer_dir/../Applications/DeviceHub.app" ]; then
|
|
301
|
+
printf '%s\n' "$developer_dir/../Applications/DeviceHub.app"
|
|
302
|
+
elif [ -d "$developer_dir/Applications/Simulator.app" ]; then
|
|
303
|
+
printf '%s\n' "$developer_dir/Applications/Simulator.app"
|
|
304
|
+
else
|
|
305
|
+
printf 'open-device: no simulator UI found in the selected Xcode.\nNext: xcode-select -p\n' >&2
|
|
306
|
+
return 1
|
|
307
|
+
fi
|
|
278
308
|
}
|
|
279
309
|
|
|
280
|
-
|
|
281
|
-
local target="$1"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
open_simulator_ui() {
|
|
311
|
+
local target="$1" app="$2" background="$3"
|
|
312
|
+
local open_args=(-a "$app")
|
|
313
|
+
[ "$background" != "true" ] || open_args=(-g "${open_args[@]}")
|
|
314
|
+
if [[ "$app" == */DeviceHub.app ]]; then
|
|
315
|
+
# Device Hub routes device windows through URLs, including when already open.
|
|
316
|
+
# Simulator's CurrentDeviceUDID launch argument does not select its window.
|
|
317
|
+
open "${open_args[@]}" "devices://device/open?id=$target" >&2 || {
|
|
318
|
+
printf 'open-device: failed to open Device Hub for %s.\nNext: open -a %q %q\n' "$target" "$app" "devices://device/open?id=$target" >&2
|
|
288
319
|
return 1
|
|
289
320
|
}
|
|
290
321
|
else
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return 1
|
|
296
|
-
}
|
|
297
|
-
else
|
|
298
|
-
open -g -a Simulator >/dev/null 2>&1 || {
|
|
299
|
-
printf 'open-device: failed to open Simulator UI.\n' >&2
|
|
300
|
-
return 1
|
|
301
|
-
}
|
|
302
|
-
fi
|
|
322
|
+
open "${open_args[@]}" --args -CurrentDeviceUDID "$target" >&2 || {
|
|
323
|
+
printf 'open-device: failed to open Simulator for %s.\nNext: open -a %q --args -CurrentDeviceUDID %q\n' "$target" "$app" "$target" >&2
|
|
324
|
+
return 1
|
|
325
|
+
}
|
|
303
326
|
fi
|
|
304
327
|
}
|
|
305
328
|
|
|
306
|
-
|
|
307
|
-
local target="$1"
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
|
|
313
|
-
else
|
|
314
|
-
open -a Simulator >/dev/null 2>&1 || true
|
|
329
|
+
open_simulator_in_background_if_needed() {
|
|
330
|
+
local target="$1" app=""
|
|
331
|
+
[ "${MM_HARNESS_SHOW_SIMULATOR:-1}" != "0" ] || return 0
|
|
332
|
+
app="$(resolve_simulator_ui_app)" || return 1
|
|
333
|
+
if [[ "$app" == */Simulator.app ]] && [ -z "${MM_HARNESS_SIMULATOR_APP:-}" ] && pgrep -x Simulator >/dev/null 2>&1; then
|
|
334
|
+
return 0
|
|
315
335
|
fi
|
|
316
|
-
|
|
336
|
+
printf 'Opening %s for %s in the background\n' "$(basename "$app" .app)" "$target" >&2
|
|
337
|
+
open_simulator_ui "$target" "$app" true
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
focus_simulator() {
|
|
341
|
+
local app=""
|
|
342
|
+
app="$(resolve_simulator_ui_app)" || return 1
|
|
343
|
+
open_simulator_ui "$1" "$app" false
|
|
317
344
|
}
|
|
318
345
|
|
|
319
346
|
# --- Android helpers ----------------------------------------------------------
|
|
@@ -507,9 +534,10 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
507
534
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
508
535
|
|
|
509
536
|
boot_simulator_if_needed "$SIM_TARGET" || exit 1
|
|
510
|
-
open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
|
|
511
537
|
if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
|
|
512
|
-
focus_simulator "$SIM_TARGET"
|
|
538
|
+
focus_simulator "$SIM_TARGET" || exit 1
|
|
539
|
+
else
|
|
540
|
+
open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
|
|
513
541
|
fi
|
|
514
542
|
if $UI_ONLY; then
|
|
515
543
|
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { assessmentProvider } from "./assessment-providers.js";
|
|
2
|
+
import { optionString, usageError } from "./commands/parse-args.js";
|
|
3
|
+
import { configPath, parseAdvisorSettings, readConfig } from "./review/knowledge.js";
|
|
4
|
+
function resolveAdvisorPolicy(options, env = process.env, config) {
|
|
5
|
+
const saved = (config ?? readConfig(configPath(env))).advisor ?? {};
|
|
6
|
+
const cliAdvisor = optionString(options, "advisor");
|
|
7
|
+
const configuredAdvisor = cliAdvisor ?? env.MM_HARNESS_ADVISOR;
|
|
8
|
+
if (configuredAdvisor !== void 0 && !configuredAdvisor.trim()) throw usageError("Advisor override must name a provider or off. Next: unset MM_HARNESS_ADVISOR or select --advisor off.");
|
|
9
|
+
const enabled = configuredAdvisor !== void 0 ? configuredAdvisor !== "off" : saved.enabled === true;
|
|
10
|
+
const source = cliAdvisor !== void 0 ? "cli" : env.MM_HARNESS_ADVISOR !== void 0 ? "env" : saved.enabled !== void 0 ? "config" : "default";
|
|
11
|
+
const providerName = configuredAdvisor && configuredAdvisor !== "off" ? configuredAdvisor : saved.provider;
|
|
12
|
+
const cliModel = optionString(options, "advisorModel");
|
|
13
|
+
const overrideStages = optionString(options, "advisorStages") ?? env.MM_HARNESS_ADVISOR_STAGES;
|
|
14
|
+
const stages = enabled && overrideStages !== void 0 ? parseAdvisorSettings({ stages: overrideStages.split(",").map((stage) => stage.trim()) }).stages : saved.stages ?? ["plan"];
|
|
15
|
+
const sources = {
|
|
16
|
+
enabled: source,
|
|
17
|
+
provider: configuredAdvisor && configuredAdvisor !== "off" ? source : saved.provider ? "config" : "default",
|
|
18
|
+
model: cliModel !== void 0 ? "cli" : env.MM_HARNESS_ADVISOR_MODEL !== void 0 ? "env" : saved.model ? "config" : "default",
|
|
19
|
+
stages: enabled && options.advisorStages !== void 0 ? "cli" : enabled && env.MM_HARNESS_ADVISOR_STAGES !== void 0 ? "env" : saved.stages ? "config" : "default"
|
|
20
|
+
};
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
if (cliModel !== void 0 && configuredAdvisor !== "off") throw usageError("A model alone does not enable advice. Next: select --advisor <provider> or enable advisor settings.");
|
|
23
|
+
return { policy: { enabled: false, status: "disabled", stages, sources }, selection: void 0 };
|
|
24
|
+
}
|
|
25
|
+
const provider = providerName ? assessmentProvider(providerName) : void 0;
|
|
26
|
+
if (!provider) throw usageError("Enabled advice requires a supported provider. Next: use --advisor typesafe or --advisor off.");
|
|
27
|
+
const savedModel = !configuredAdvisor || configuredAdvisor === saved.provider ? saved.model : void 0;
|
|
28
|
+
const model = cliModel ?? env.MM_HARNESS_ADVISOR_MODEL ?? savedModel ?? provider.defaultModel;
|
|
29
|
+
if (!model.trim()) throw usageError("Advisor model must be non-empty. Next: unset the model override to use the provider default.");
|
|
30
|
+
if (!cliModel && env.MM_HARNESS_ADVISOR_MODEL === void 0 && !savedModel) sources.model = "default";
|
|
31
|
+
const keyAvailable = Boolean(env[provider.apiKeyEnv]?.trim());
|
|
32
|
+
const selection = { provider, model: model.trim() };
|
|
33
|
+
return {
|
|
34
|
+
policy: { enabled: true, status: keyAvailable ? "ready" : "skipped", ...keyAvailable ? {} : { reason: "missing_key" }, provider: provider.id, model: model.trim(), stages, keyAvailable, sources },
|
|
35
|
+
selection
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
resolveAdvisorPolicy
|
|
40
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const providers = [{
|
|
2
|
+
id: "typesafe",
|
|
3
|
+
apiKeyEnv: "TYPESAFE_API_KEY",
|
|
4
|
+
defaultModel: "jev-1.13.0",
|
|
5
|
+
assess: async (request, options) => (await import("./typesafe-client.js")).requestTypeSafe(request, options)
|
|
6
|
+
}];
|
|
7
|
+
function assessmentProvider(name) {
|
|
8
|
+
return providers.find((provider) => provider.id === name);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
assessmentProvider
|
|
12
|
+
};
|
package/dist/cli-commands.js
CHANGED
|
@@ -12,7 +12,7 @@ const SPEC = {
|
|
|
12
12
|
{ name: "review", desc: "Materialize a review checklist composed from the base review and a team library", args: ["checklist"], flags: ["--domain", "--since", "--base", "--out", "--adapter", "--target", "--json"] },
|
|
13
13
|
{ name: "domain", desc: "Which team library owns the change (declared value, else owned-paths.json)", flags: ["--domain", "--base", "--adapter", "--target", "--json"] },
|
|
14
14
|
{ name: "pr-body", desc: "Render the publishable PR body: pr-description.md plus the recipe and run sections built from artifacts", args: ["render", "<task-dir>"], flags: ["--command", "--out", "--json"] },
|
|
15
|
-
{ name: "config", desc: "Per-engineer locations
|
|
15
|
+
{ name: "config", desc: "Per-engineer locations and optional advisor settings", args: ["list", "path", "get", "set", "unset"], flags: ["--json"] },
|
|
16
16
|
{ name: "tutorial", desc: "Open the visual recipe tutorial", flags: ["--json", "--no-open"] },
|
|
17
17
|
{ name: "setup-base", desc: "Bootstrap numbered product checkouts", flags: ["--dir", "--counts", "--only", "--dry-run", "--force", "--json", "--show-config", "--reset-config", "--skip-harness-update"] },
|
|
18
18
|
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json", "--fast", "--watch", "--task"] },
|
|
@@ -27,7 +27,7 @@ const SPEC = {
|
|
|
27
27
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
28
28
|
{ name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
|
|
29
29
|
{ name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
|
|
30
|
-
{ name: "recipe-quality", desc: "Build
|
|
30
|
+
{ name: "recipe-quality", desc: "Build canonical quality or request optional recipe planning/advice", args: ["build", "plan", "advise", "advisor-status"], flags: ["--input", "--output", "--recipe", "--task", "--evidence", "--params", "--adapter", "--library", "--advisor", "--advisor-model", "--advisor-stages", "--json"] },
|
|
31
31
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
32
32
|
{ name: "prepare", desc: "Run the readiness chain and write the readiness record", flags: ["--target", "--platform", "--mobile-platform", "--artifacts-dir", "--runtime-dir", "--json"] },
|
|
33
33
|
{ 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"] },
|
package/dist/command-contract.js
CHANGED
|
@@ -289,11 +289,19 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
289
289
|
minimumPositionals: 1
|
|
290
290
|
},
|
|
291
291
|
"recipe-quality": {
|
|
292
|
-
options: options(HELP, JSON, {
|
|
292
|
+
options: options(HELP, JSON, ADAPTER, {
|
|
293
293
|
"--input": value(),
|
|
294
|
-
"--output": value()
|
|
294
|
+
"--output": value(),
|
|
295
|
+
"--recipe": value(),
|
|
296
|
+
"--task": value(),
|
|
297
|
+
"--evidence": value(),
|
|
298
|
+
"--params": value(),
|
|
299
|
+
"--library": value(),
|
|
300
|
+
"--advisor": value(),
|
|
301
|
+
"--advisor-model": value(),
|
|
302
|
+
"--advisor-stages": value()
|
|
295
303
|
}),
|
|
296
|
-
positionals: [{ label: "action", choices: ["build"] }]
|
|
304
|
+
positionals: [{ label: "action", choices: ["build", "plan", "advise", "advisor-status"] }]
|
|
297
305
|
},
|
|
298
306
|
provision: {
|
|
299
307
|
options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
|
package/dist/commands/config.js
CHANGED
|
@@ -2,11 +2,12 @@ import path from "node:path";
|
|
|
2
2
|
import {
|
|
3
3
|
configPath,
|
|
4
4
|
readConfig,
|
|
5
|
+
parseAdvisorSettings,
|
|
5
6
|
REVIEW_ADAPTERS,
|
|
6
7
|
writeConfig
|
|
7
8
|
} from "../review/knowledge.js";
|
|
8
9
|
import { optionFlag, parseArgs, usageError } from "./parse-args.js";
|
|
9
|
-
const USAGE = "Usage: mm-harness config <list|path|get <key>|set <key> <
|
|
10
|
+
const USAGE = "Usage: mm-harness config <list|path|get <key>|set <key> <value>|unset <key>> [--json]\n keys: libraries.<name>, references.<mobile|extension|core>, advisor.<enabled|provider|model|stages>";
|
|
10
11
|
function parseKey(raw) {
|
|
11
12
|
if (!raw) throw usageError(USAGE);
|
|
12
13
|
const dot = raw.indexOf(".");
|
|
@@ -14,21 +15,23 @@ function parseKey(raw) {
|
|
|
14
15
|
const name = dot === -1 ? "" : raw.slice(dot + 1);
|
|
15
16
|
if (!name) throw usageError(`Config key must be libraries.<name> or references.<adapter> (got ${raw}).`);
|
|
16
17
|
if (section === "libraries") return { section, name };
|
|
18
|
+
if (section === "advisor" && ["enabled", "provider", "model", "stages"].includes(name)) return { section, name };
|
|
17
19
|
if (section === "references") {
|
|
18
20
|
if (!REVIEW_ADAPTERS.includes(name)) {
|
|
19
21
|
throw usageError(`references.<adapter> must be one of ${REVIEW_ADAPTERS.join(", ")} (got ${name}).`);
|
|
20
22
|
}
|
|
21
23
|
return { section, name };
|
|
22
24
|
}
|
|
23
|
-
throw usageError(`
|
|
25
|
+
throw usageError(`Unknown config key ${raw}. Next: use libraries.<name>, references.<adapter> or advisor.<enabled|provider|model|stages>.`);
|
|
24
26
|
}
|
|
25
27
|
function getValue(config, key) {
|
|
26
|
-
return
|
|
28
|
+
return config[key.section]?.[key.name];
|
|
27
29
|
}
|
|
28
30
|
function setValue(config, key, value) {
|
|
29
31
|
const section = config[key.section] ?? {};
|
|
30
32
|
if (value === void 0) delete section[key.name];
|
|
31
33
|
else section[key.name] = value;
|
|
34
|
+
if (key.section === "advisor") parseAdvisorSettings(section);
|
|
32
35
|
if (Object.keys(section).length === 0) delete config[key.section];
|
|
33
36
|
else config[key.section] = section;
|
|
34
37
|
}
|
|
@@ -56,7 +59,8 @@ async function handleConfig(argv) {
|
|
|
56
59
|
}
|
|
57
60
|
const lines = [
|
|
58
61
|
...Object.entries(config.libraries ?? {}).map(([name, root]) => `libraries.${name}=${root}`),
|
|
59
|
-
...Object.entries(config.references ?? {}).map(([name, root]) => `references.${name}=${root}`)
|
|
62
|
+
...Object.entries(config.references ?? {}).map(([name, root]) => `references.${name}=${root}`),
|
|
63
|
+
...Object.entries(config.advisor ?? {}).map(([name, value]) => `advisor.${name}=${Array.isArray(value) ? value.join(",") : value}`)
|
|
60
64
|
];
|
|
61
65
|
process.stdout.write(lines.length > 0 ? `${lines.join("\n")}
|
|
62
66
|
` : `(empty) ${file}
|
|
@@ -71,17 +75,18 @@ async function handleConfig(argv) {
|
|
|
71
75
|
return 0;
|
|
72
76
|
}
|
|
73
77
|
if (value === void 0) return 1;
|
|
74
|
-
process.stdout.write(`${value}
|
|
78
|
+
process.stdout.write(`${Array.isArray(value) ? value.join(",") : value}
|
|
75
79
|
`);
|
|
76
80
|
return 0;
|
|
77
81
|
}
|
|
78
82
|
if (action === "set") {
|
|
79
83
|
const key = parseKey(rawKey);
|
|
80
|
-
if (!rawValue) throw usageError(`mm-harness config set ${rawKey} <
|
|
81
|
-
|
|
84
|
+
if (!rawValue) throw usageError(`mm-harness config set ${rawKey} <value>`);
|
|
85
|
+
const value = key.section !== "advisor" ? path.resolve(rawValue) : key.name === "enabled" ? rawValue === "true" ? true : rawValue === "false" ? false : rawValue : key.name === "stages" ? rawValue.split(",").map((stage) => stage.trim()) : rawValue.trim();
|
|
86
|
+
setValue(config, key, value);
|
|
82
87
|
writeConfig(file, config);
|
|
83
|
-
process.stdout.write(json ? `${JSON.stringify({ command: "config", key: rawKey, value
|
|
84
|
-
` : `${rawKey}=${
|
|
88
|
+
process.stdout.write(json ? `${JSON.stringify({ command: "config", key: rawKey, value, path: file })}
|
|
89
|
+
` : `${rawKey}=${Array.isArray(value) ? value.join(",") : value}
|
|
85
90
|
`);
|
|
86
91
|
return 0;
|
|
87
92
|
}
|
package/dist/commands/prepare.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { color, stripAnsi } from "../cli-color.js";
|
|
8
8
|
import { runnerProvenance } from "../doctor.js";
|
|
9
9
|
import { detectAdapter } from "../harness.js";
|
|
10
|
-
import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
10
|
+
import { harnessExecutable, manifestPath, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
|
|
11
11
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
12
12
|
import { EXIT } from "./shared.js";
|
|
13
13
|
import { formatDuration } from "./status-watch.js";
|
|
@@ -336,13 +336,32 @@ async function handlePrepare(argv) {
|
|
|
336
336
|
const remoteFlagArgs = remoteFlag === void 0 ? [] : ["--remote-flag", remoteFlag];
|
|
337
337
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
338
338
|
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
339
|
+
const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
|
|
339
340
|
const chain = stepChain(platform);
|
|
340
341
|
const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
|
|
341
342
|
view?.begin();
|
|
342
343
|
const steps = [];
|
|
344
|
+
let activeStep;
|
|
345
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
346
|
+
const writeProgress = () => {
|
|
347
|
+
fs.mkdirSync(path.dirname(progressPath), { recursive: true });
|
|
348
|
+
const temporaryPath = `${progressPath}.${process.pid}.tmp`;
|
|
349
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify({
|
|
350
|
+
schemaVersion: 1,
|
|
351
|
+
startedAt,
|
|
352
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
353
|
+
total: chain.length,
|
|
354
|
+
steps: steps.filter(({ id }) => chain.includes(id)).map(({ id, status, durationMs }) => ({ id, status, durationMs })),
|
|
355
|
+
...activeStep ? { active: activeStep } : {}
|
|
356
|
+
})}
|
|
357
|
+
`);
|
|
358
|
+
fs.renameSync(temporaryPath, progressPath);
|
|
359
|
+
};
|
|
343
360
|
const failed = () => steps.some((step) => step.status === "fail");
|
|
344
361
|
const addStep = (step) => {
|
|
345
362
|
steps.push(step);
|
|
363
|
+
activeStep = void 0;
|
|
364
|
+
writeProgress();
|
|
346
365
|
view?.finish(step);
|
|
347
366
|
return step;
|
|
348
367
|
};
|
|
@@ -352,10 +371,17 @@ async function handlePrepare(argv) {
|
|
|
352
371
|
}
|
|
353
372
|
const step = await runStep(executable, target, artifactsDir, id, args, {
|
|
354
373
|
json,
|
|
355
|
-
onSpawn: (
|
|
374
|
+
onSpawn: (stepStartedAt) => {
|
|
375
|
+
activeStep = { id, startedAt: new Date(stepStartedAt).toISOString() };
|
|
376
|
+
writeProgress();
|
|
377
|
+
view?.start(id, stepStartedAt);
|
|
378
|
+
}
|
|
356
379
|
});
|
|
357
380
|
return addStep(step);
|
|
358
381
|
};
|
|
382
|
+
const heartbeat = setInterval(() => {
|
|
383
|
+
if (activeStep) writeProgress();
|
|
384
|
+
}, 1e4);
|
|
359
385
|
try {
|
|
360
386
|
await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
361
387
|
await run("status", ["status", "--json", ...targetArgs]);
|
|
@@ -384,6 +410,7 @@ async function handlePrepare(argv) {
|
|
|
384
410
|
}
|
|
385
411
|
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
386
412
|
} finally {
|
|
413
|
+
clearInterval(heartbeat);
|
|
387
414
|
view?.end();
|
|
388
415
|
}
|
|
389
416
|
const provenance = runnerProvenance(manifestPath(platform));
|
|
@@ -404,6 +431,7 @@ async function handlePrepare(argv) {
|
|
|
404
431
|
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
405
432
|
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
|
|
406
433
|
`);
|
|
434
|
+
fs.rmSync(progressPath, { force: true });
|
|
407
435
|
if (json) {
|
|
408
436
|
console.log(JSON.stringify(record, null, 2));
|
|
409
437
|
} else {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { adviseRecipe } from "../recipe-assistance.js";
|
|
4
|
+
import { resolveAdvisorPolicy } from "../advisor-policy.js";
|
|
5
|
+
import { RECIPE_ADVICE_RUBRIC, RECIPE_PLANNING_RUBRIC } from "../recipe-assistance-rubric.js";
|
|
6
|
+
import { CliError, isRecord, optionFlag, optionString, usageError } from "./parse-args.js";
|
|
7
|
+
import { EXIT } from "./shared.js";
|
|
8
|
+
async function handleRecipeAdvice(options, mode = "review") {
|
|
9
|
+
try {
|
|
10
|
+
return await runRecipeAdvice(options, mode);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (!(error instanceof CliError)) throw error;
|
|
13
|
+
const userAction = "mm-harness recipe-quality --help";
|
|
14
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify({ schemaVersion: 1, command: "recipe-quality", action: mode === "status" ? "advisor-status" : mode === "plan" ? "plan" : "advise", status: "fail", exitCode: error.exitCode, error: { code: error.exitCode === EXIT.validation ? "RECIPE_ADVICE_INVALID" : "RECIPE_ADVICE_USAGE", message: error.message, userAction } }, null, 2));
|
|
15
|
+
else console.error(`\u2717 mm-harness recipe-quality: ${error.message}
|
|
16
|
+
Next: ${userAction}`);
|
|
17
|
+
return error.exitCode;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function runRecipeAdvice(options, mode) {
|
|
21
|
+
const { policy, selection } = resolveAdvisorPolicy(options);
|
|
22
|
+
if (mode === "status") {
|
|
23
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify({ schemaVersion: 1, command: "recipe-quality", action: "advisor-status", ...policy }, null, 2));
|
|
24
|
+
else console.log(`Recipe advisor: ${policy.status}${policy.enabled ? ` (${policy.provider}, ${policy.model}; stages: ${policy.stages.join(", ")})` : ""}.`);
|
|
25
|
+
return EXIT.ok;
|
|
26
|
+
}
|
|
27
|
+
const recipe = optionString(options, "recipe");
|
|
28
|
+
const task = optionString(options, "task");
|
|
29
|
+
const adapter = optionString(options, "adapter");
|
|
30
|
+
const output = optionString(options, "output");
|
|
31
|
+
const evidence = optionString(options, "evidence");
|
|
32
|
+
if (mode === "review" && !recipe || !task || !["mobile", "extension", "core"].includes(adapter ?? "")) {
|
|
33
|
+
throw usageError("Use recipe-quality plan --task <task.json>, or advise --recipe <recipe.json> --task <TASK.md>, with --adapter <adapter> [--advisor <provider>].");
|
|
34
|
+
}
|
|
35
|
+
if (mode === "plan" && (recipe || evidence || optionString(options, "params"))) throw usageError("Planning runs before a recipe exists. Next: use advise to review a draft or evidence.");
|
|
36
|
+
if (optionString(options, "input")) throw usageError("--input belongs to recipe-quality build. Next: use --recipe and --task for advise.");
|
|
37
|
+
let params;
|
|
38
|
+
if (optionString(options, "params")) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(optionString(options, "params"));
|
|
41
|
+
if (!isRecord(parsed)) throw usageError("--params must be a JSON object.");
|
|
42
|
+
params = parsed;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof SyntaxError) throw usageError("--params must be a JSON object.");
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (output) {
|
|
49
|
+
const resolved = path.resolve(output);
|
|
50
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) throw usageError("Advice output must be a file. Next: use artifacts/recipe-advice.json.");
|
|
51
|
+
const canonical = fs.existsSync(resolved) ? fs.realpathSync(resolved) : resolved;
|
|
52
|
+
const inputs = [recipe, task, evidence].filter((file) => Boolean(file)).map((file) => fs.existsSync(file) ? fs.realpathSync(file) : path.resolve(file));
|
|
53
|
+
if ([resolved, canonical].some((file) => /^recipe(?:-quality(?:-input)?)?\.json$/iu.test(path.basename(file))) || inputs.includes(canonical) || inputs.includes(resolved)) {
|
|
54
|
+
throw usageError("Advice output must be separate from recipes, task inputs, and recipe-quality.json. Next: use artifacts/recipe-advice.json.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const stage = mode === "plan" ? "plan" : evidence ? "evidence" : "draft";
|
|
58
|
+
const explicitCall = options.advisor !== void 0 && options.advisor !== "off" && options.advisorStages === void 0;
|
|
59
|
+
const stageDisabled = policy.enabled && !explicitCall && !policy.stages.includes(stage);
|
|
60
|
+
const artifact = stageDisabled ? { schemaVersion: 1, advisory: true, rubric: mode === "plan" ? RECIPE_PLANNING_RUBRIC : RECIPE_ADVICE_RUBRIC, status: "skipped", reason: "stage_disabled" } : await adviseRecipe({ recipe, task, mode, adapter, evidence, params, options }, selection);
|
|
61
|
+
if (output && artifact.status !== "disabled") {
|
|
62
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
63
|
+
fs.writeFileSync(path.resolve(output), `${JSON.stringify(artifact, null, 2)}
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
if (optionFlag(options, "json")) console.log(JSON.stringify({ schemaVersion: 1, command: "recipe-quality", action: mode === "plan" ? "plan" : "advise", ...artifact, policy, ...output && artifact.status !== "disabled" ? { outputPath: output } : {} }, null, 2));
|
|
67
|
+
else {
|
|
68
|
+
console.log(`Recipe advice: ${artifact.status}${"reason" in artifact ? ` (${artifact.reason})` : ""}.`);
|
|
69
|
+
if (artifact.status === "completed" && "checks" in artifact) for (const check of artifact.checks) console.log(` ${check.id}: ${check.outcome}${check.suggestion ? `. ${check.suggestion}` : ""}`);
|
|
70
|
+
if (artifact.status === "completed" && "assessments" in artifact) for (const assessment of artifact.assessments) console.log(` ${assessment.id}: ${assessment.classification}${assessment.candidate ? ` (${assessment.candidate.ref})` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
return EXIT.ok;
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
handleRecipeAdvice
|
|
76
|
+
};
|
|
@@ -37,11 +37,18 @@ async function handleRecipeQuality(argv) {
|
|
|
37
37
|
const { positional, options } = parseArgs(argv, "recipe-quality");
|
|
38
38
|
const json = optionFlag(options, "json");
|
|
39
39
|
const action = positional[0];
|
|
40
|
+
if (action === "advise" || action === "plan" || action === "advisor-status") {
|
|
41
|
+
const { handleRecipeAdvice } = await import("./recipe-advice.js");
|
|
42
|
+
return handleRecipeAdvice(options, action === "advisor-status" ? "status" : action === "plan" ? "plan" : "review");
|
|
43
|
+
}
|
|
44
|
+
if (["advisor", "advisorModel", "advisorStages", "recipe", "task", "evidence", "adapter", "library", "params"].some((key) => options[key] !== void 0)) {
|
|
45
|
+
return usageOut(json, "recipe-quality", "advice options require the advise subcommand", "mm-harness recipe-quality advise --recipe <recipe.json> --task <TASK.md> --adapter <adapter> --advisor <provider>");
|
|
46
|
+
}
|
|
40
47
|
if (!action && (optionString(options, "input") || optionString(options, "output"))) {
|
|
41
48
|
return buildArtifact(options, json);
|
|
42
49
|
}
|
|
43
50
|
if (action !== "build") {
|
|
44
|
-
const message = action ? `unknown action '${action}'` : "missing action: use build, or
|
|
51
|
+
const message = action ? `unknown action '${action}'` : "missing action: use build, plan, or advise";
|
|
45
52
|
return usageOut(json, "recipe-quality", message, SHORTHAND_USAGE);
|
|
46
53
|
}
|
|
47
54
|
return buildArtifact(options, json);
|
|
@@ -103,9 +103,13 @@ function renderTaskView(view, title, explicitTask = false) {
|
|
|
103
103
|
const contextLine = joinParts([
|
|
104
104
|
view.libraries.length > 0 && `libraries ${out("info", view.libraries.map(libraryLabel).join(" "))}`,
|
|
105
105
|
showTask && view.fixture && `fixture ${out("info", path.basename(view.fixture.fixture ?? "unknown"))} ${statusWord(out, view.fixture.status)}${view.fixture.walletReset ? out("warn", " reset") : ""}`,
|
|
106
|
-
showTask && view.sandbox?.ready !== void 0 && `sandbox ${view.sandbox.ready ? out("ok", "ready") : out("warn", "not ready")}`
|
|
106
|
+
showTask && (view.prepare ? `sandbox ${out("info", "preparing")}` : view.sandbox?.ready !== void 0 && `sandbox ${view.sandbox.ready ? out("ok", "ready") : out("warn", "not ready")}`)
|
|
107
107
|
]);
|
|
108
108
|
if (contextLine) lines.push(contextLine);
|
|
109
|
+
if (showTask && view.acceptance?.length) lines.push("", ...renderAcceptanceLines(view));
|
|
110
|
+
if (showTask && view.prepare) {
|
|
111
|
+
lines.push(`prepare ${out("info", view.prepare.step)} \xB7 ${view.prepare.completed}/${view.prepare.total} steps \xB7 ${formatDuration(view.prepare.elapsedMs)} elapsed`);
|
|
112
|
+
}
|
|
109
113
|
const last = view.lastCommand;
|
|
110
114
|
if (showTask && last) {
|
|
111
115
|
lines.push(joinParts([
|
|
@@ -148,6 +152,19 @@ function renderTaskView(view, title, explicitTask = false) {
|
|
|
148
152
|
}
|
|
149
153
|
return lines;
|
|
150
154
|
}
|
|
155
|
+
function renderAcceptanceLines(view) {
|
|
156
|
+
if (!view.acceptance?.length) return [];
|
|
157
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
158
|
+
const proven = view.acceptance.filter((row) => row.status?.verdict === "proven").length;
|
|
159
|
+
return [
|
|
160
|
+
`acceptance criteria ${proven}/${view.acceptance.length} proven`,
|
|
161
|
+
...view.acceptance.map((row) => {
|
|
162
|
+
const verdict = row.status?.verdict ?? "no verdict";
|
|
163
|
+
const style = verdict === "proven" ? "ok" : verdict === "weak" ? "warn" : verdict === "no verdict" ? "dim" : "err";
|
|
164
|
+
return ` ${row.id} [${out(style, verdict)}] ${row.text}`;
|
|
165
|
+
})
|
|
166
|
+
];
|
|
167
|
+
}
|
|
151
168
|
function idleReason(view) {
|
|
152
169
|
if (!view.taskDir) return "no task directory under temp/tasks";
|
|
153
170
|
const { reason, idleMs, branchCarriesTicket } = view.activity;
|
|
@@ -161,15 +178,33 @@ function idleReason(view) {
|
|
|
161
178
|
}
|
|
162
179
|
function changedLines(previous, next) {
|
|
163
180
|
if (!previous || previous.taskDir !== next.taskDir) {
|
|
164
|
-
return [
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
return [
|
|
182
|
+
joinParts([
|
|
183
|
+
path.basename(next.checkout.target),
|
|
184
|
+
next.taskDir ? path.basename(next.taskDir) : "no-task-dir",
|
|
185
|
+
`status=${next.signal?.status ?? "unknown"}`,
|
|
186
|
+
`step=${next.signal?.step ? stepLabel(next.signal.step) : "none"}`
|
|
187
|
+
], " "),
|
|
188
|
+
...(next.acceptance ?? []).map((row) => `${row.id} ${row.status?.verdict ?? "no verdict"} ${row.text}`),
|
|
189
|
+
...next.prepare ? [`prepare ${next.prepare.step} started (${next.prepare.completed}/${next.prepare.total} steps)`] : []
|
|
190
|
+
];
|
|
170
191
|
}
|
|
171
192
|
const lines = [];
|
|
193
|
+
for (const step of next.prepare?.steps ?? []) {
|
|
194
|
+
if (!previous.prepare?.steps.some((prior) => prior.id === step.id && prior.status === step.status)) {
|
|
195
|
+
lines.push(`prepare ${step.id} ${step.status} (${formatDuration(step.durationMs)})`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (next.prepare && (previous.prepare?.step !== next.prepare.step || previous.prepare.startedAt !== next.prepare.startedAt)) {
|
|
199
|
+
lines.push(`prepare ${next.prepare.step} started (${next.prepare.completed}/${next.prepare.total} steps)`);
|
|
200
|
+
}
|
|
172
201
|
const before = new Map(previous.checklist.map((row) => [row.number, row.checked]));
|
|
202
|
+
const beforeAcceptance = new Map(previous.acceptance?.map((row) => [row.id, row.status?.verdict]));
|
|
203
|
+
for (const row of next.acceptance ?? []) {
|
|
204
|
+
if (beforeAcceptance.get(row.id) !== row.status?.verdict || !beforeAcceptance.has(row.id)) {
|
|
205
|
+
lines.push(`${row.id} ${row.status?.verdict ?? "no verdict"} ${row.text}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
173
208
|
for (const row of next.checklist) {
|
|
174
209
|
if (!row.checked || before.get(row.number) === true) continue;
|
|
175
210
|
lines.push(`checked ${row.number} ${row.label}${row.durationMs !== void 0 ? ` (${formatDuration(row.durationMs)})` : ""}`);
|
|
@@ -254,6 +289,7 @@ export {
|
|
|
254
289
|
changedLines,
|
|
255
290
|
formatDuration,
|
|
256
291
|
handleStatusTaskView,
|
|
292
|
+
renderAcceptanceLines,
|
|
257
293
|
renderTaskView,
|
|
258
294
|
runStatusWatch
|
|
259
295
|
};
|