@testmuai/rook 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/dist/cli.js +1084 -732
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -2
package/dist/cli.js CHANGED
@@ -499,7 +499,7 @@ var VERSION, SHA, VERSION_LABEL, CLIENT_NAME, PUBLIC_DOCS_URL, PUBLIC_UPDATE_COM
499
499
  var init_constants = __esm({
500
500
  "src/constants.ts"() {
501
501
  "use strict";
502
- VERSION = true ? "0.1.2" : "0.0.0-dev";
502
+ VERSION = true ? "0.1.3" : "0.0.0-dev";
503
503
  SHA = /^[0-9a-f]{40}$/;
504
504
  VERSION_LABEL = SHA.test(VERSION) ? VERSION.slice(0, 7) : VERSION.includes("-") ? VERSION : `${VERSION}-alpha`;
505
505
  CLIENT_NAME = "ROOK";
@@ -19494,8 +19494,26 @@ var init_drift = __esm({
19494
19494
  });
19495
19495
 
19496
19496
  // src/run/materialise.ts
19497
- import { copyFileSync, cpSync, existsSync as existsSync31, mkdirSync as mkdirSync25, readFileSync as readFileSync34, readdirSync as readdirSync19, realpathSync as realpathSync8, writeFileSync as writeFileSync23 } from "fs";
19498
- import { basename as basename8, dirname as dirname21, isAbsolute as isAbsolute8, join as join36, relative as relative14, resolve as resolve15, sep as sep15 } from "path";
19497
+ import {
19498
+ cpSync,
19499
+ existsSync as existsSync31,
19500
+ mkdirSync as mkdirSync25,
19501
+ readFileSync as readFileSync34,
19502
+ readdirSync as readdirSync19,
19503
+ realpathSync as realpathSync8,
19504
+ rmSync as rmSync9,
19505
+ statSync as statSync16,
19506
+ writeFileSync as writeFileSync23
19507
+ } from "fs";
19508
+ import {
19509
+ basename as basename8,
19510
+ dirname as dirname21,
19511
+ isAbsolute as isAbsolute8,
19512
+ join as join36,
19513
+ relative as relative14,
19514
+ resolve as resolve15,
19515
+ sep as sep15
19516
+ } from "path";
19499
19517
  import yaml10 from "js-yaml";
19500
19518
  function scenarioDir(root, agentId, runId, scenarioId) {
19501
19519
  return join36(runDir(root, agentId, runId), "scenarios", scenarioId);
@@ -19531,7 +19549,10 @@ function materialiseContext(root, agentId, runId, context) {
19531
19549
  );
19532
19550
  write2("agent.yaml", context.spec);
19533
19551
  write2("features.yaml", context.features);
19534
- write2("profile.yaml", readableProfile(root, resolve15(dir2, "..", ".."), context.profile));
19552
+ write2(
19553
+ "profile.yaml",
19554
+ readableProfile(root, resolve15(dir2, "..", ".."), context.profile)
19555
+ );
19535
19556
  }
19536
19557
  function priorWork(root, agentId, runId) {
19537
19558
  const dir2 = join36(runDir(root, agentId, runId), "scenarios");
@@ -19551,34 +19572,99 @@ function priorWork(root, agentId, runId) {
19551
19572
  }
19552
19573
  return { decided, ungraded };
19553
19574
  }
19554
- function carryForward(root, agentId, from, to) {
19575
+ function carryForward(root, agentId, from, to, opts = {}) {
19555
19576
  const src = scenarioDir(root, agentId, from.runId, from.scenarioId);
19556
19577
  const dst = scenarioDir(root, agentId, to.runId, to.scenarioId);
19557
19578
  if (!existsSync31(src)) return "nothing";
19558
- mkdirSync25(dst, { recursive: true });
19559
- const copy = (name) => {
19560
- const at = join36(src, name);
19561
- if (!existsSync31(at)) return false;
19562
- copyFileSync(at, join36(dst, name));
19563
- return true;
19564
- };
19565
- const copyTree = (name) => {
19566
- const at = join36(src, name);
19567
- if (!existsSync31(at)) return false;
19568
- cpSync(at, join36(dst, name), { recursive: true });
19569
- return true;
19579
+ const carried = () => {
19580
+ const take = (name) => {
19581
+ try {
19582
+ return readFileSync34(join36(src, name), "utf-8");
19583
+ } catch {
19584
+ return null;
19585
+ }
19586
+ };
19587
+ const put = (name, body) => {
19588
+ mkdirSync25(dst, { recursive: true });
19589
+ writeFileSync23(join36(dst, name), body, "utf-8");
19590
+ };
19591
+ const move2 = (name) => {
19592
+ const body = take(name);
19593
+ if (body === null) return false;
19594
+ put(name, body);
19595
+ return true;
19596
+ };
19597
+ const record = (body, parse2) => {
19598
+ if (body === null) return null;
19599
+ let parsed;
19600
+ try {
19601
+ parsed = parse2(body);
19602
+ } catch {
19603
+ return null;
19604
+ }
19605
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
19606
+ return null;
19607
+ return parsed;
19608
+ };
19609
+ const moveTree = (name, skip) => {
19610
+ const at = join36(src, name);
19611
+ if (!existsSync31(at)) return false;
19612
+ mkdirSync25(dst, { recursive: true });
19613
+ cpSync(at, join36(dst, name), {
19614
+ recursive: true,
19615
+ ...skip ? { filter: (f) => basename8(f) !== skip } : {}
19616
+ });
19617
+ return true;
19618
+ };
19619
+ const response = record(take("response.json"), JSON.parse);
19620
+ if (!response || !judgeable(response, from.scenarioId)) return "nothing";
19621
+ move2("request.json");
19622
+ move2("output.txt");
19623
+ move2("hooks.json");
19624
+ moveTree("artifacts");
19625
+ const within2 = (base, file) => {
19626
+ const path = relative14(base, file);
19627
+ return path !== "" && path !== ".." && !path.startsWith(`..${sep15}`) && !isAbsolute8(path);
19628
+ };
19629
+ const artifacts = response.artifacts.map((name) => {
19630
+ const source = resolve15(root, name);
19631
+ if (!within2(join36(src, "artifacts"), source) || !within2(realpathSync8(src), realpathSync8(source)))
19632
+ throw new Error("artifact is outside the prior scenario's artifacts directory");
19633
+ const destination2 = join36(dst, relative14(src, source));
19634
+ if (!within2(realpathSync8(dst), realpathSync8(destination2)) || !statSync16(destination2).isFile()) throw new Error("copied artifact is missing");
19635
+ return relative14(root, destination2);
19636
+ });
19637
+ moveTree("hook-state");
19638
+ const verdict3 = opts.verdict === false ? null : record(take("verdict.yaml"), (t) => yaml10.load(t));
19639
+ const carriesVerdict = verdict3 !== null && completeVerdict(verdict3, from.scenarioId);
19640
+ moveTree("evidence", carriesVerdict ? void 0 : JUDGE_WORKING);
19641
+ if (carriesVerdict) {
19642
+ put(
19643
+ "verdict.yaml",
19644
+ yaml10.dump(verdict3, { lineWidth: 100, noRefs: true, sortKeys: false })
19645
+ );
19646
+ }
19647
+ put("response.json", JSON.stringify({ ...response, artifacts }, null, 2));
19648
+ return carriesVerdict ? "verdict" : "response";
19570
19649
  };
19571
- const hasResponse = copy("response.json");
19572
- if (hasResponse) {
19573
- copy("request.json");
19574
- copy("output.txt");
19575
- copy("hooks.json");
19576
- copyTree("evidence");
19577
- copyTree("hook-state");
19578
- if (copy("verdict.yaml")) return "verdict";
19579
- return "response";
19580
- }
19581
- return "nothing";
19650
+ try {
19651
+ return carried();
19652
+ } catch {
19653
+ for (const name of CARRIED_FILES) {
19654
+ try {
19655
+ rmSync9(join36(dst, name), { recursive: true, force: true });
19656
+ } catch {
19657
+ }
19658
+ }
19659
+ return "nothing";
19660
+ }
19661
+ }
19662
+ function completeVerdict(value, scenarioId) {
19663
+ const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
19664
+ return value.scenario_id === scenarioId && typeof value.run_id === "string" && value.run_id.length > 0 && DECIDED.includes(value.status) && typeof value.summary === "string" && ["pass_count", "fail_count", "unable_to_verify_count", "compliance_percentage", "latency_ms", "turns"].every((key2) => typeof value[key2] === "number" && Number.isFinite(value[key2])) && Array.isArray(value.forbidden_hits) && value.forbidden_hits.every((hit) => typeof hit === "string") && record(value.metrics) && Object.values(value.metrics).every((metric) => typeof metric === "number" && Number.isFinite(metric)) && Array.isArray(value.criteria) && value.criteria.every((row) => record(row) && ["criterion_id", "criterion", "expected", "achieved", "evidence"].every((key2) => typeof row[key2] === "string") && DECIDED.includes(row.status) && ["High", "Medium", "Low"].includes(row.confidence));
19665
+ }
19666
+ function judgeable(response, scenarioId) {
19667
+ return response.scenario_id === scenarioId && typeof response.status === "string" && typeof response.output === "string" && Array.isArray(response.transcript) && Array.isArray(response.artifacts) && response.artifacts.every((path) => typeof path === "string" && path.length > 0);
19582
19668
  }
19583
19669
  function phasesRunIn(root, agentId, runId) {
19584
19670
  const dir2 = join36(runDir(root, agentId, runId), "scenarios");
@@ -19603,7 +19689,10 @@ function phasesRunIn(root, agentId, runId) {
19603
19689
  return [...seen];
19604
19690
  }
19605
19691
  function loadSnapshot(root, agentId, runId, scenarioId) {
19606
- const at = join36(scenarioDir(root, agentId, runId, scenarioId), "snapshot.yaml");
19692
+ const at = join36(
19693
+ scenarioDir(root, agentId, runId, scenarioId),
19694
+ "snapshot.yaml"
19695
+ );
19607
19696
  let parsed;
19608
19697
  try {
19609
19698
  parsed = yaml10.load(readFileSync34(at, "utf-8"));
@@ -19648,12 +19737,25 @@ function real(p) {
19648
19737
  return parent === p ? p : join36(real(parent), basename8(p));
19649
19738
  }
19650
19739
  }
19740
+ var CARRIED_FILES, DECIDED, JUDGE_WORKING;
19651
19741
  var init_materialise = __esm({
19652
19742
  "src/run/materialise.ts"() {
19653
19743
  "use strict";
19654
19744
  init_spec2();
19655
19745
  init_plan_file();
19656
19746
  init_types3();
19747
+ CARRIED_FILES = [
19748
+ "response.json",
19749
+ "request.json",
19750
+ "output.txt",
19751
+ "hooks.json",
19752
+ "verdict.yaml",
19753
+ "artifacts",
19754
+ "evidence",
19755
+ "hook-state"
19756
+ ];
19757
+ DECIDED = ["Pass", "Fail", "Unable to Verify"];
19758
+ JUDGE_WORKING = "judge-working.json";
19657
19759
  }
19658
19760
  });
19659
19761
 
@@ -20092,6 +20194,522 @@ var init_hook_scripts = __esm({
20092
20194
  }
20093
20195
  });
20094
20196
 
20197
+ // src/run/plan.ts
20198
+ import yaml13 from "js-yaml";
20199
+ function staticPlan(input) {
20200
+ const { partition: partition2 } = input;
20201
+ const included = partition2.candidates.map((c) => ({
20202
+ scenario_id: c.local_id,
20203
+ why: "a candidate, and nothing narrowed this run further",
20204
+ confidence: "high"
20205
+ }));
20206
+ const judge_only = [];
20207
+ const carried = [];
20208
+ const skipped = [
20209
+ ...partition2.excluded.map((e) => ({
20210
+ scenario_id: e.scenario_id,
20211
+ why: `excluded by the flags: ${e.why}`,
20212
+ confidence: "high"
20213
+ })),
20214
+ ...partition2.filtered.map((f) => ({
20215
+ scenario_id: f.scenario_id,
20216
+ why: `left out by the tree: ${f.why}`,
20217
+ confidence: "high"
20218
+ }))
20219
+ ];
20220
+ const counts = [
20221
+ included.length ? `${included.length} to execute` : "",
20222
+ // SAID, not implied. Somebody who typed `--resume` expects work to be
20223
+ // reused, and a plan that silently re-ran everything would look like one
20224
+ // that had reused nothing to reuse.
20225
+ input.resumeHint ? `nothing reused from ${input.resumeHint}` : ""
20226
+ ].filter(Boolean);
20227
+ return {
20228
+ included,
20229
+ skipped,
20230
+ judge_only,
20231
+ carried,
20232
+ /*
20233
+ * `--rca` and nothing else. There is no sentence for anything to read an
20234
+ * intent out of, and RCA costs a model call per failure cluster — turning
20235
+ * it on because a run happens to be large would spend on an explanation
20236
+ * nobody asked for.
20237
+ */
20238
+ rca: Boolean(input.rca),
20239
+ summary: counts.length ? `no instruction given \u2014 ${counts.join(", ")}. Every candidate the flags and the tree left.` : "no instruction given, and nothing is left to run.",
20240
+ ...input.resumeHint ? { resume_from: input.resumeHint } : {},
20241
+ /*
20242
+ * No `name` and no `concurrency`. Both are proposals a planner makes from
20243
+ * a sentence, and `applyPlanned` already lets an explicit flag win — so
20244
+ * omitting them leaves the flag, or the default, exactly as typed.
20245
+ */
20246
+ // Nothing was spent. A non-zero cost here would bill a run for a decision
20247
+ // no model was asked to make.
20248
+ credits: 0
20249
+ };
20250
+ }
20251
+ async function planRun(input, deps) {
20252
+ const { partition: partition2 } = input;
20253
+ const flagsAsked = input.flags ?? [];
20254
+ const task4 = [
20255
+ input.instruction ? `## WHAT THE USER ASKED FOR
20256
+
20257
+ ${input.instruction}
20258
+ ` : "",
20259
+ input.resumeHint ? `## THEY NAMED A RUN
20260
+
20261
+ ${input.resumeHint} \u2014 look it up before planning.
20262
+ ` : "",
20263
+ /**
20264
+ * The same rendering of the agent every other planner gets.
20265
+ *
20266
+ * From the same function, so a planner deciding what to SPEND cannot be
20267
+ * reasoning about a different agent than the writers described.
20268
+ */
20269
+ agentContext({
20270
+ spec: input.spec,
20271
+ features: input.features,
20272
+ ...input.findings?.length ? { findings: input.findings } : {},
20273
+ ...input.callers?.length ? { callers: input.callers } : {},
20274
+ ...input.callees?.length ? { callees: input.callees } : {}
20275
+ }),
20276
+ "",
20277
+ /**
20278
+ * How the agent is reached, and what rook can SEE while it runs.
20279
+ *
20280
+ * The second half is what decides whether a scenario is checkable at all,
20281
+ * and the unrunnable list below is computed from it — so the planner is
20282
+ * shown the reason as well as the verdict.
20283
+ */
20284
+ "## THE PROFILE THIS RUN WILL USE",
20285
+ "",
20286
+ "```yaml",
20287
+ yaml13.dump(
20288
+ {
20289
+ id: input.profile.id,
20290
+ /*
20291
+ * WHICH PHASES, not which transport.
20292
+ *
20293
+ * `kind` and `mode` were here and told the planner the agent was
20294
+ * reached over http, synchronously — neither of which changes what
20295
+ * to run. The phases do: a profile with a `collect` can be run now
20296
+ * and collected later, and one without cannot, so a planner asked
20297
+ * about staging a run needs this and had no way to see it.
20298
+ */
20299
+ phases: definedPhases(input.profile),
20300
+ multi_turn: Boolean(input.profile.capabilities?.multi_turn),
20301
+ /*
20302
+ * What the profile REPORTS, and it must not contradict the gap block
20303
+ * below it.
20304
+ *
20305
+ * This was hardcoded `false` upstream, because on the old schema
20306
+ * nothing observed a call — `observe.mcp: proxy` was a declaration
20307
+ * with no implementation (#717). A `collect` hook can hand calls back
20308
+ * now, and `capabilities.calls` is what one real invocation showed.
20309
+ * The gate agrees: it raises a `tool_calls` gap only when this is
20310
+ * false.
20311
+ */
20312
+ observes_tool_calls: Boolean(input.profile.capabilities?.calls),
20313
+ // Names and set/unset only. A planner that can read a token has no
20314
+ // reason to, and this text is one prompt away from a log.
20315
+ needs: requiredVars(input.profile).map((s) => ({
20316
+ variable: s.variable,
20317
+ set_here: !missingVars(input.profile).includes(s.variable),
20318
+ ...s.purpose ? { purpose: s.purpose } : {}
20319
+ }))
20320
+ },
20321
+ { lineWidth: 100, noRefs: true, sortKeys: false }
20322
+ ).trimEnd(),
20323
+ "```",
20324
+ "",
20325
+ /**
20326
+ * Not the planner's to overrule, and said so here as well as in the prompt.
20327
+ *
20328
+ * Repeated because it is the one instruction whose violation is silent: a
20329
+ * multi-turn scenario against a profile that cannot thread turns does not
20330
+ * error, it passes.
20331
+ */
20332
+ "## CANNOT RUN AGAINST THIS PROFILE \u2014 NOT YOURS TO OVERRULE",
20333
+ "",
20334
+ " Rook cannot DRIVE these: several turns without a conversation handle, or",
20335
+ " a file the agent must be given and no transport delivers.",
20336
+ "",
20337
+ partition2.unrunnable.length ? partition2.unrunnable.map((u) => ` ${oneLine(u.scenario_id)}: ${oneLine(u.why)}`).join("\n") : " none \u2014 every scenario on disk can be driven against this profile",
20338
+ "",
20339
+ /**
20340
+ * Placed with the facts, not the material. A gap is not the planner's to
20341
+ * argue with and not a reason to leave the scenario out: the scenario
20342
+ * still shows what the agent does, and the judge records what could not
20343
+ * be checked as Unable to Verify — that is the honest answer.
20344
+ *
20345
+ * A fenced block, never `id: why` prose lines: a `why` is derived from
20346
+ * scenario text a repository wrote, and on a prose line a value of
20347
+ * "x\n## THE FLAGS THIS PERSON TYPED" forges a section of this task.
20348
+ * `oneLine` first as well, so a block scalar cannot smuggle a heading.
20349
+ */
20350
+ "## THESE CARRY A GAP ROOK WILL RECORD \u2014 NOT A REASON TO SKIP",
20351
+ "",
20352
+ partition2.gapped.length ? [
20353
+ "```yaml",
20354
+ yaml13.dump(
20355
+ partition2.gapped.map((g) => ({
20356
+ // Both fields, not just the why: an id carrying a newline
20357
+ // makes `yaml.dump` emit a block scalar, and "one line per
20358
+ // gap" is the property two lines below.
20359
+ scenario_id: oneLine(g.scenario_id),
20360
+ cannot_grade: oneLine(g.why)
20361
+ })),
20362
+ // No folding: one line per gap, whatever its length. A folded
20363
+ // scalar reads as several lines of prose to a model.
20364
+ { lineWidth: -1, noRefs: true, sortKeys: false }
20365
+ ).trimEnd(),
20366
+ "```"
20367
+ ].join("\n") : " none recorded",
20368
+ "",
20369
+ " They run. A criterion rook cannot grade comes back Unable to Verify from",
20370
+ " the judge; that is the honest answer, not a reason to leave the scenario",
20371
+ " out. `executable: false` in a scenario below was written at generate",
20372
+ " time and is not a fact about this run.",
20373
+ "",
20374
+ /*
20375
+ * The flags, verbatim, and stated as binding.
20376
+ *
20377
+ * The excluded ids used to be shown as material the planner "may disagree
20378
+ * with", so `--only SC-001` was a suggestion — and a planner that decided
20379
+ * the neighbouring scenarios were worth running got them, because nothing
20380
+ * downstream checked. Naming the flags as well as their effect matters:
20381
+ * a list of excluded ids does not tell a model that a PERSON narrowed
20382
+ * this, and that is the fact that decides whether it may argue.
20383
+ */
20384
+ ...flagsAsked.length ? [
20385
+ "## THE FLAGS THIS PERSON TYPED \u2014 BINDING, NOT YOURS TO OVERRULE",
20386
+ "",
20387
+ ...flagsAsked.map((f) => ` ${f}`),
20388
+ "",
20389
+ " They narrowed this run deliberately. Every scenario those flags",
20390
+ " excluded is listed below and is OUT \u2014 naming one in included,",
20391
+ " judge_only or carried is an error, and rook drops it. If the flags",
20392
+ " look wrong for what they asked, say so in summary rather than",
20393
+ " planning around them.",
20394
+ "",
20395
+ "## WHAT THOSE FLAGS EXCLUDED \u2014 out of this run",
20396
+ "",
20397
+ partition2.excluded.length ? partition2.excluded.map((f) => ` ${f.scenario_id}: ${f.why}`).join("\n") : " nothing \u2014 every scenario on disk matched them",
20398
+ ""
20399
+ ] : [],
20400
+ "## WHAT THE TREE LEFT OUT \u2014 material, and you may disagree",
20401
+ "",
20402
+ partition2.filtered.length ? partition2.filtered.map((f) => ` ${f.scenario_id}: ${f.why}`).join("\n") : " nothing \u2014 no scenario was dropped for the state it is in",
20403
+ "",
20404
+ "## YOUR CANDIDATES \u2014 everything you may choose from",
20405
+ "",
20406
+ partition2.candidates.length ? partition2.candidates.map((c) => ` ${c.local_id} ${c.title}`).join("\n") : " nothing \u2014 the flags excluded everything runnable",
20407
+ "",
20408
+ ...input.stale.length ? [
20409
+ "## THESE PIN OLDER FEATURE TEXT",
20410
+ "",
20411
+ ` ${input.stale.join(", ")}`,
20412
+ "",
20413
+ "They still run \u2014 an outdated test that passes is information. What must",
20414
+ "not happen is a plan describing them as though they were written",
20415
+ "against the feature as it stands.",
20416
+ ""
20417
+ ] : [],
20418
+ /**
20419
+ * Every scenario, in full, and LAST — the state block.
20420
+ *
20421
+ * Titles and counts cannot tell six behaviours from six rewordings, and
20422
+ * that difference is exactly what decides whether a run is worth its cost.
20423
+ * Last because it is the part that changes every run, which is what prompt
20424
+ * caching wants.
20425
+ */
20426
+ "## EVERY SCENARIO ON DISK",
20427
+ "",
20428
+ "```yaml",
20429
+ yaml13.dump(input.scenarios, { lineWidth: 100, noRefs: true, sortKeys: false }).trimEnd(),
20430
+ "```",
20431
+ ""
20432
+ ].filter(Boolean).join("\n");
20433
+ const child = await spawnSubagent(
20434
+ {
20435
+ role: "run_planner",
20436
+ label: `${partition2.candidates.length} candidate(s)`,
20437
+ task: task4,
20438
+ // The BUDGET phase, not the command. `plan_scenarios` bills to
20439
+ // "generate" for the same reason: a planner is charged to the work it is
20440
+ // planning, so one bucket covers deciding and doing.
20441
+ phase: "execute",
20442
+ responseSchema: PLAN_SCHEMA2,
20443
+ tools: toolsForRole("run_planner", { root: input.root, mcpAgent: null })
20444
+ },
20445
+ deps
20446
+ );
20447
+ if (!child.ok || child.data == null)
20448
+ throw new Error(roleFailure("run_planner", child));
20449
+ const forbidden = /* @__PURE__ */ new Set([
20450
+ ...partition2.unrunnable.map((u) => u.scenario_id),
20451
+ ...partition2.excluded.map((e) => e.scenario_id)
20452
+ ]);
20453
+ const known = new Set(input.scenarios.map((s) => s.local_id));
20454
+ const included = (child.data.included ?? []).filter(
20455
+ (e) => e?.scenario_id && known.has(e.scenario_id) && !forbidden.has(e.scenario_id)
20456
+ );
20457
+ const skipped = (child.data.skipped ?? []).filter(
20458
+ (e) => e?.scenario_id && known.has(e.scenario_id)
20459
+ );
20460
+ const canJudge = new Set(input.ungraded ?? []);
20461
+ const canCarry = new Set(input.decided ?? []);
20462
+ const eligible = (entries, allowed) => {
20463
+ const kept = [];
20464
+ const demoted = [];
20465
+ for (const e of entries ?? []) {
20466
+ if (!e?.scenario_id || !known.has(e.scenario_id)) continue;
20467
+ if (forbidden.has(e.scenario_id)) continue;
20468
+ (allowed.has(e.scenario_id) ? kept : demoted).push(e);
20469
+ }
20470
+ return { kept, demoted };
20471
+ };
20472
+ const judgeOnly = eligible(child.data.judge_only, canJudge);
20473
+ const carried = eligible(child.data.carried, canCarry);
20474
+ const claimed = /* @__PURE__ */ new Set();
20475
+ const takeOnce = (entries) => entries.filter(
20476
+ (e) => !claimed.has(e.scenario_id) && claimed.add(e.scenario_id)
20477
+ );
20478
+ const finalIncluded = takeOnce([
20479
+ ...included,
20480
+ ...judgeOnly.demoted,
20481
+ ...carried.demoted
20482
+ ]);
20483
+ const finalJudgeOnly = takeOnce(judgeOnly.kept);
20484
+ const finalCarried = takeOnce(carried.kept);
20485
+ return {
20486
+ included: finalIncluded,
20487
+ skipped,
20488
+ judge_only: finalJudgeOnly,
20489
+ carried: finalCarried,
20490
+ /**
20491
+ * The flag forces it ON and never off.
20492
+ *
20493
+ * A planner that declined costs nothing to overrule, and somebody who typed
20494
+ * `--rca` has asked plainly. The reverse — a flag that silences a planner
20495
+ * which decided an explanation was needed — would hide the reasoning at the
20496
+ * moment it is most wanted.
20497
+ */
20498
+ rca: Boolean(child.data.rca) || Boolean(input.rca),
20499
+ summary: child.data.summary ?? "",
20500
+ ...child.data.resume_from ? { resume_from: child.data.resume_from } : {},
20501
+ ...child.data.name ? { name: child.data.name } : {},
20502
+ ...child.data.concurrency ? { concurrency: child.data.concurrency } : {},
20503
+ credits: child.credits
20504
+ };
20505
+ }
20506
+ function applyPlanned(plan2, flags) {
20507
+ const proposed = plan2.concurrency ?? 1;
20508
+ return {
20509
+ ...flags.name ?? plan2.name ? { name: flags.name ?? plan2.name } : {},
20510
+ concurrency: flags.concurrency ?? Math.min(Math.max(1, Math.trunc(proposed)), MAX_PLANNED_CONCURRENCY)
20511
+ };
20512
+ }
20513
+ function renderRunPlan(plan2, view) {
20514
+ const gap = new Map(view.gapped.map((g) => [g.scenario_id, g.why]));
20515
+ const excluded = new Set(view.excluded.map((e) => e.scenario_id));
20516
+ const gapLine = (id) => {
20517
+ const why2 = gap.get(id);
20518
+ return why2 === void 0 ? [] : [` cannot grade: ${oneLine(why2)}`];
20519
+ };
20520
+ const lines = [`plan: ${plan2.included.length} scenario(s) to run`];
20521
+ for (const e of plan2.included) {
20522
+ const sure = e.confidence && e.confidence !== "high" ? ` (${e.confidence} confidence)` : "";
20523
+ lines.push(` ${oneLine(e.scenario_id)}${sure}`);
20524
+ lines.push(` ${oneLine(e.why)}`);
20525
+ lines.push(...gapLine(e.scenario_id));
20526
+ }
20527
+ for (const e of plan2.judge_only) {
20528
+ lines.push(` ${oneLine(e.scenario_id)} judge only \u2014 ${oneLine(e.why)}`);
20529
+ lines.push(...gapLine(e.scenario_id));
20530
+ }
20531
+ for (const e of plan2.carried) {
20532
+ lines.push(` ${oneLine(e.scenario_id)} carried \u2014 ${oneLine(e.why)}`);
20533
+ lines.push(...gapLine(e.scenario_id));
20534
+ }
20535
+ for (const e of plan2.skipped) {
20536
+ lines.push(` ${oneLine(e.scenario_id)} skipped \u2014 ${oneLine(e.why)}`);
20537
+ const why2 = gap.get(e.scenario_id);
20538
+ if (why2 === void 0) continue;
20539
+ lines.push(
20540
+ excluded.has(e.scenario_id) ? ` carries a gap: ${oneLine(why2)}` : ` carries a gap, which is not a reason to skip: ${oneLine(why2)}`
20541
+ );
20542
+ }
20543
+ if (view.unrunnable.length)
20544
+ lines.push(` ${view.unrunnable.length} cannot run against this profile`);
20545
+ if (plan2.resume_from)
20546
+ lines.push(` carrying finished work from ${plan2.resume_from}`);
20547
+ return lines;
20548
+ }
20549
+ function decidedElsewhere(plan2) {
20550
+ return new Set(
20551
+ [...plan2.judge_only ?? [], ...plan2.carried ?? []].map(
20552
+ (e) => e.scenario_id
20553
+ )
20554
+ );
20555
+ }
20556
+ function scenariosOf(plan2) {
20557
+ return [
20558
+ ...plan2.included,
20559
+ ...plan2.judge_only ?? [],
20560
+ ...plan2.carried ?? []
20561
+ ];
20562
+ }
20563
+ function judgedHere(plan2) {
20564
+ return [...plan2.included, ...plan2.judge_only ?? []];
20565
+ }
20566
+ function gapLines(view, plan2) {
20567
+ const does = /* @__PURE__ */ new Map();
20568
+ for (const e of plan2.included) does.set(e.scenario_id, "runs");
20569
+ for (const e of plan2.judge_only ?? []) does.set(e.scenario_id, "is judged");
20570
+ return view.gapped.filter((g) => does.has(g.scenario_id)).map(
20571
+ (g) => `${oneLine(g.scenario_id)}: ${does.get(g.scenario_id)} \u2014 cannot grade: ${oneLine(g.why)}`
20572
+ );
20573
+ }
20574
+ var PLAN_SCHEMA2, MAX_PLANNED_CONCURRENCY;
20575
+ var init_plan2 = __esm({
20576
+ "src/run/plan.ts"() {
20577
+ "use strict";
20578
+ init_subagent();
20579
+ init_role_failure();
20580
+ init_registry4();
20581
+ init_context();
20582
+ init_types3();
20583
+ init_env();
20584
+ init_render();
20585
+ PLAN_SCHEMA2 = {
20586
+ type: "object",
20587
+ /*
20588
+ * `rca` is NOT required, and requiring it failed real runs.
20589
+ *
20590
+ * "run_planner: the reply does not match the shape asked for — rca:
20591
+ * required, and absent" is the whole of `/run` refusing because a boolean
20592
+ * with an obvious default was left out. The schema's own description says
20593
+ * "false is the ordinary answer", and `planRun` already reads it as
20594
+ * `Boolean(child.data.rca) || Boolean(input.rca)` — so absence was always
20595
+ * handled everywhere except the validator that rejected it first.
20596
+ *
20597
+ * The three that remain are the answer itself: what to run, what was left
20598
+ * out, and the sentence somebody reads before spending. A reply missing any
20599
+ * of those is not a plan.
20600
+ */
20601
+ required: ["included", "skipped", "summary"],
20602
+ properties: {
20603
+ /**
20604
+ * The four lists are a PARTITION of the candidates.
20605
+ *
20606
+ * Every candidate lands in exactly one, and rook enforces it — a scenario
20607
+ * in two is ambiguous about whether it costs an agent call, and one in none
20608
+ * is silently dropped. `judge_only` and `carried` are the resume half: rook
20609
+ * works out which scenarios are ELIGIBLE for each from the files, and the
20610
+ * planner decides which of them the request actually wants that done to.
20611
+ */
20612
+ included: {
20613
+ type: "array",
20614
+ description: "Execute these: call the agent, then judge. One entry per scenario.",
20615
+ items: {
20616
+ type: "object",
20617
+ required: ["scenario_id", "why", "confidence"],
20618
+ properties: {
20619
+ scenario_id: { type: "string" },
20620
+ why: {
20621
+ type: "string",
20622
+ description: "What this one establishes that the others do not. Not 'important coverage', which is true of everything and decides nothing."
20623
+ },
20624
+ confidence: {
20625
+ type: "string",
20626
+ enum: ["high", "medium", "low"],
20627
+ description: "`low` says you are unsure it earns its cost \u2014 exactly what somebody about to approve wants to know."
20628
+ }
20629
+ }
20630
+ }
20631
+ },
20632
+ judge_only: {
20633
+ type: "array",
20634
+ description: "Already executed by the prior run and never judged. A judge call and NO agent call. Only ids listed as ungraded below are eligible.",
20635
+ items: {
20636
+ type: "object",
20637
+ required: ["scenario_id", "why"],
20638
+ properties: {
20639
+ scenario_id: { type: "string" },
20640
+ why: { type: "string" },
20641
+ confidence: { type: "string", enum: ["high", "medium", "low"] }
20642
+ }
20643
+ }
20644
+ },
20645
+ carried: {
20646
+ type: "array",
20647
+ description: "Already decided by the prior run. Copy the verdict forward \u2014 no agent call, no judge call. Only ids listed as decided below are eligible.",
20648
+ items: {
20649
+ type: "object",
20650
+ required: ["scenario_id", "why"],
20651
+ properties: {
20652
+ scenario_id: { type: "string" },
20653
+ why: { type: "string" },
20654
+ confidence: { type: "string", enum: ["high", "medium", "low"] }
20655
+ }
20656
+ }
20657
+ },
20658
+ rca: {
20659
+ type: "boolean",
20660
+ description: "Explain the failure clusters when this run ends. True when the request asks WHY something fails, or when a run this large failing would leave somebody with nowhere to start. It costs one model call per cluster, so false is the ordinary answer."
20661
+ },
20662
+ skipped: {
20663
+ type: "array",
20664
+ description: "One entry per scenario you deliberately left out.",
20665
+ items: {
20666
+ type: "object",
20667
+ required: ["scenario_id", "why", "confidence"],
20668
+ properties: {
20669
+ scenario_id: { type: "string" },
20670
+ why: {
20671
+ type: "string",
20672
+ description: "Why it is not worth running now. A scenario nobody considered and one deliberately excluded look identical afterwards."
20673
+ },
20674
+ confidence: { type: "string", enum: ["high", "medium", "low"] }
20675
+ }
20676
+ }
20677
+ },
20678
+ summary: {
20679
+ type: "string",
20680
+ description: "What this run will establish, and what it will not. The sentence a person reads before deciding to spend."
20681
+ },
20682
+ resume_from: {
20683
+ type: "string",
20684
+ description: "A prior run to carry finished work from. Omit unless proposing one."
20685
+ },
20686
+ /**
20687
+ * Two fields of `run.yaml` the planner may set, and only these two.
20688
+ *
20689
+ * Both are things a person expresses in words — "call this the threshold
20690
+ * sweep", "run them one at a time" — and neither changes what a run MEANS.
20691
+ *
20692
+ * `test_mode` is deliberately NOT here. It decides whether the run is
20693
+ * pinned upstream at all, so a model able to set it could turn a real run
20694
+ * into one that records nothing, or a smoke test into a version-pinned
20695
+ * result. That is a flag, and flags are for people.
20696
+ *
20697
+ * An explicit flag always wins over both — see `applyPlanned`.
20698
+ */
20699
+ name: {
20700
+ type: "string",
20701
+ description: "A short name for this run, when the request suggests one. Omit rather than inventing a label nobody used."
20702
+ },
20703
+ concurrency: {
20704
+ type: "integer",
20705
+ description: "How many scenarios to run at once, when the request says. One is the default and the right answer for anything hitting a rate-limited API."
20706
+ }
20707
+ }
20708
+ };
20709
+ MAX_PLANNED_CONCURRENCY = 8;
20710
+ }
20711
+ });
20712
+
20095
20713
  // src/report/cluster.ts
20096
20714
  function shapeOf2(verdict3) {
20097
20715
  if (verdict3.compromised) return `compromised:${verdict3.impact ?? "unrated"}`;
@@ -20555,9 +21173,38 @@ function resolveRun(root, agentId, runId) {
20555
21173
  const plans = readdirSync21(dir2).map((id) => loadPlan(root, agentId, id)).filter((p) => Boolean(p?.id)).sort((a, b) => (b.created ?? "").localeCompare(a.created ?? ""));
20556
21174
  return plans[0] ?? null;
20557
21175
  }
21176
+ function snapshotsOf(root, agentId, plan2) {
21177
+ return judgedHere(plan2).map((e) => {
21178
+ const at = join44(
21179
+ scenarioDir(root, agentId, plan2.id, e.scenario_id),
21180
+ "snapshot.yaml"
21181
+ );
21182
+ try {
21183
+ return yaml15.load(readFileSync40(at, "utf-8"));
21184
+ } catch {
21185
+ return null;
21186
+ }
21187
+ }).filter((s) => Boolean(s?.local_id));
21188
+ }
21189
+ function fallbackTotals(plan2, verdicts) {
21190
+ const planned = judgedHere(plan2).length;
21191
+ return {
21192
+ planned,
21193
+ executed: verdicts.length,
21194
+ passed: verdicts.filter((v) => v.status === "Pass").length,
21195
+ failed: verdicts.filter((v) => v.status === "Fail").length,
21196
+ unverifiable: verdicts.filter((v) => v.status === "Unable to Verify").length,
21197
+ unjudged: 0,
21198
+ not_run: Math.max(0, planned - verdicts.length),
21199
+ unrunnable: plan2.unrunnable.length,
21200
+ decided: verdicts.filter((v) => v.status !== "Unable to Verify").length,
21201
+ pass_rate: null,
21202
+ carried_forward: (plan2.carried ?? []).length
21203
+ };
21204
+ }
20558
21205
  function runVerdicts(root, agentId, plan2) {
20559
21206
  const out = [];
20560
- for (const entry of plan2.included) {
21207
+ for (const entry of judgedHere(plan2)) {
20561
21208
  const at = join44(
20562
21209
  scenarioDir(root, agentId, plan2.id, entry.scenario_id),
20563
21210
  "verdict.yaml"
@@ -20622,19 +21269,7 @@ async function explainRun(input, deps) {
20622
21269
  );
20623
21270
  return null;
20624
21271
  }
20625
- const scenarios2 = plan2.included.map((e) => {
20626
- const at = join44(
20627
- scenarioDir(input.root, input.agentId, plan2.id, e.scenario_id),
20628
- "snapshot.yaml"
20629
- );
20630
- try {
20631
- return yaml15.load(readFileSync40(at, "utf-8"));
20632
- } catch {
20633
- return null;
20634
- }
20635
- }).filter(
20636
- (s) => Boolean(s?.local_id)
20637
- );
21272
+ const scenarios2 = snapshotsOf(input.root, input.agentId, plan2);
20638
21273
  const prior = (() => {
20639
21274
  try {
20640
21275
  return yaml15.load(
@@ -20683,19 +21318,7 @@ async function explainRun(input, deps) {
20683
21318
  // Kept from the run that produced them. Recomputing totals here would
20684
21319
  // count only the scenarios that left a verdict file, silently dropping
20685
21320
  // everything the run never reached.
20686
- totals: prior?.totals ?? {
20687
- planned: plan2.included.length,
20688
- executed: verdicts.length,
20689
- passed: verdicts.filter((v) => v.status === "Pass").length,
20690
- failed: verdicts.filter((v) => v.status === "Fail").length,
20691
- unverifiable: verdicts.filter((v) => v.status === "Unable to Verify").length,
20692
- unjudged: 0,
20693
- not_run: Math.max(0, plan2.included.length - verdicts.length),
20694
- unrunnable: plan2.unrunnable.length,
20695
- decided: verdicts.filter((v) => v.status !== "Unable to Verify").length,
20696
- pass_rate: null,
20697
- carried_forward: 0
20698
- },
21321
+ totals: prior?.totals ?? fallbackTotals(plan2, verdicts),
20699
21322
  ...prior?.metrics ? { metrics: prior.metrics } : {},
20700
21323
  say: input.say,
20701
21324
  rca: true
@@ -20714,6 +21337,7 @@ var init_command = __esm({
20714
21337
  init_store3();
20715
21338
  init_plan_file();
20716
21339
  init_materialise();
21340
+ init_plan2();
20717
21341
  init_report();
20718
21342
  init_hook_scripts();
20719
21343
  }
@@ -20819,6 +21443,59 @@ var init_output = __esm({
20819
21443
  }
20820
21444
  });
20821
21445
 
21446
+ // src/command-arguments.ts
21447
+ function argumentPlaceholder(arg) {
21448
+ return arg.required ? `<${arg.name()}>` : `[${arg.name()}]`;
21449
+ }
21450
+ function declaredChoices(arg) {
21451
+ return arg.argChoices?.length ? `(choices: ${arg.argChoices.join(", ")})` : "";
21452
+ }
21453
+ function argumentDescription(arg) {
21454
+ return [arg.description, declaredChoices(arg)].filter(Boolean).join(" ");
21455
+ }
21456
+ function argumentSummary(arg) {
21457
+ return [argumentPlaceholder(arg), declaredChoices(arg)].filter(Boolean).join(" ");
21458
+ }
21459
+ function commandWords(line) {
21460
+ const out = [];
21461
+ let current2 = "";
21462
+ let quote2 = null;
21463
+ let started = false;
21464
+ for (const ch of line) {
21465
+ if (quote2) {
21466
+ if (ch === quote2) quote2 = null;
21467
+ else current2 += ch;
21468
+ continue;
21469
+ }
21470
+ if (ch === '"' || ch === "'") {
21471
+ quote2 = ch;
21472
+ started = true;
21473
+ continue;
21474
+ }
21475
+ if (/\s/.test(ch)) {
21476
+ if (current2 || started) out.push(current2);
21477
+ current2 = "";
21478
+ started = false;
21479
+ continue;
21480
+ }
21481
+ current2 += ch;
21482
+ }
21483
+ if (current2 || started) out.push(current2);
21484
+ return {
21485
+ words: out,
21486
+ complete: !started && current2 === "",
21487
+ openQuote: quote2 !== null
21488
+ };
21489
+ }
21490
+ function split2(line) {
21491
+ return commandWords(line).words;
21492
+ }
21493
+ var init_command_arguments = __esm({
21494
+ "src/command-arguments.ts"() {
21495
+ "use strict";
21496
+ }
21497
+ });
21498
+
20822
21499
  // src/classify/material.ts
20823
21500
  function nutshell(root, projectId2) {
20824
21501
  const active2 = activeAgentId(root);
@@ -20850,12 +21527,12 @@ function commandForm(surface, name) {
20850
21527
  function commandCatalogue(program2, surface = "cli") {
20851
21528
  const lines = [];
20852
21529
  for (const cmd of program2.commands) {
20853
- const args = cmd.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
21530
+ const args = cmd.registeredArguments.map(argumentSummary).join(" ");
20854
21531
  lines.push(
20855
21532
  ` ${commandForm(surface, cmd.name())}${args ? ` ${args}` : ""} \u2014 ${cmd.description()}`
20856
21533
  );
20857
21534
  for (const sub of cmd.commands) {
20858
- const subArgs = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
21535
+ const subArgs = sub.registeredArguments.map(argumentSummary).join(" ");
20859
21536
  lines.push(
20860
21537
  ` ${sub.name()}${subArgs ? ` ${subArgs}` : ""} \u2014 ${sub.description()}`
20861
21538
  );
@@ -20872,6 +21549,7 @@ function roleCatalogue() {
20872
21549
  var init_material = __esm({
20873
21550
  "src/classify/material.ts"() {
20874
21551
  "use strict";
21552
+ init_command_arguments();
20875
21553
  init_dist();
20876
21554
  init_registry2();
20877
21555
  init_state();
@@ -20884,7 +21562,7 @@ var init_material = __esm({
20884
21562
 
20885
21563
  // src/help.ts
20886
21564
  function usageOf(cmd, surface = "cli") {
20887
- const args = cmd.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
21565
+ const args = cmd.registeredArguments.map(argumentPlaceholder).join(" ");
20888
21566
  const subs = cmd.commands.length > 0 ? " <subcommand>" : "";
20889
21567
  const name = surface === "tui" ? `/${cmd.name()}` : cmd.name();
20890
21568
  return `${name}${subs}${args ? ` ${args}` : ""}`;
@@ -20955,12 +21633,33 @@ function renderCommandHelp(program2, name, surface = "cli") {
20955
21633
  lines.push(
20956
21634
  ` ${usageOf(sub).padEnd(w)} ${sub.description()}${isDefault ? " (default)" : ""}`
20957
21635
  );
21636
+ for (const arg of sub.registeredArguments) {
21637
+ if (!argumentDescription(arg)) continue;
21638
+ lines.push(
21639
+ ` ${argumentPlaceholder(arg)} ${argumentDescription(arg)}`
21640
+ );
21641
+ }
20958
21642
  for (const opt of sub.options) {
20959
21643
  lines.push(` ${opt.flags.padEnd(fw)} ${opt.description}`);
20960
21644
  }
20961
21645
  }
20962
21646
  lines.push("");
20963
21647
  }
21648
+ const describedArguments = cmd.registeredArguments.filter(
21649
+ (a) => argumentDescription(a)
21650
+ );
21651
+ if (describedArguments.length > 0) {
21652
+ lines.push("ARGUMENTS", "");
21653
+ const width = Math.max(
21654
+ ...describedArguments.map((a) => argumentPlaceholder(a).length)
21655
+ );
21656
+ for (const arg of describedArguments) {
21657
+ lines.push(
21658
+ ` ${argumentPlaceholder(arg).padEnd(width)} ${argumentDescription(arg)}`
21659
+ );
21660
+ }
21661
+ lines.push("");
21662
+ }
20964
21663
  if (cmd.options.length > 0) {
20965
21664
  lines.push("OPTIONS", "");
20966
21665
  const w = Math.max(...cmd.options.map((o) => o.flags.length));
@@ -21045,6 +21744,7 @@ var ORDER2, GROUPS;
21045
21744
  var init_help = __esm({
21046
21745
  "src/help.ts"() {
21047
21746
  "use strict";
21747
+ init_command_arguments();
21048
21748
  init_material();
21049
21749
  ORDER2 = [
21050
21750
  {
@@ -21904,6 +22604,7 @@ var init_Choice = __esm({
21904
22604
  });
21905
22605
 
21906
22606
  // src/app/commands.ts
22607
+ import { Command, Option } from "commander";
21907
22608
  function slashCommands(program2) {
21908
22609
  return program2.commands.map((c) => ({ name: `/${c.name()}`, description: c.description() })).sort((a, b) => a.name.localeCompare(b.name));
21909
22610
  }
@@ -21925,23 +22626,33 @@ function completionsFor(program2, input) {
21925
22626
  return finishedTyping ? [] : slashCommands(program2).filter((c) => c.name.startsWith(head));
21926
22627
  }
21927
22628
  if (!finishedTyping) return [];
21928
- const typed = parts.slice(1);
21929
- const partial = typed.at(-1) ?? "";
21930
- const given = new Set(typed.slice(0, -1));
21931
- const chosen = command.commands.find((sc) => given.has(sc.name()));
21932
- if (chosen) return flagsOf(chosen, partial, given);
21933
- const subs = command.commands.filter((sc) => sc.name().startsWith(partial)).map((sc) => ({
22629
+ const parsed = commandWords(trimmed);
22630
+ if (parsed.openQuote) return [];
22631
+ const typed = parsed.words.slice(1);
22632
+ const partial = parsed.complete ? "" : typed.pop() ?? "";
22633
+ const state4 = completionState(command, typed, partial);
22634
+ const cmd = state4.command;
22635
+ if (state4.unknown || state4.pending && (state4.requiresValue || partial === "" || !partial.startsWith("-")))
22636
+ return [];
22637
+ const positional = cmd.registeredArguments[state4.position];
22638
+ const values = positional && !positional.variadic ? (positional.argChoices ?? []).filter((value) => value.startsWith(partial)).map((value) => ({
22639
+ name: value,
22640
+ description: positional.description
22641
+ })) : [];
22642
+ if (state4.literal) return values;
22643
+ const subs = state4.position === 0 ? cmd.commands.filter((sc) => sc.name().startsWith(partial)).map((sc) => ({
21934
22644
  name: [sc.name(), placeholders(sc)].filter(Boolean).join(" "),
21935
22645
  description: sc.description()
21936
- }));
22646
+ })) : [];
21937
22647
  return [
21938
22648
  ...subs,
21939
- ...flagsOf(command, partial, given),
21940
- ...instruction(command, partial, given)
22649
+ ...values,
22650
+ ...flagsOf(cmd, partial, state4.given),
22651
+ ...cmd === command ? instruction(cmd, partial) : []
21941
22652
  ];
21942
22653
  }
21943
22654
  function placeholders(cmd) {
21944
- return cmd.registeredArguments.filter((a) => !a.variadic).map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
22655
+ return cmd.registeredArguments.filter((a) => !a.variadic).map(argumentPlaceholder).join(" ");
21945
22656
  }
21946
22657
  function flagsOf(cmd, partial, given) {
21947
22658
  return cmd.options.filter((o) => !o.hidden).map((o) => ({ flag: longest(o.flags), option: o })).filter(({ flag }) => flag !== null && !given.has(flag)).filter(({ flag }) => flag.startsWith(partial)).map(({ flag, option }) => ({
@@ -21955,14 +22666,109 @@ function longest(flags) {
21955
22666
  const match = flags.match(/--[a-z][\w-]*/);
21956
22667
  return match ? match[0] : null;
21957
22668
  }
21958
- function instruction(cmd, partial, given) {
22669
+ function instruction(cmd, partial) {
21959
22670
  const variadic = cmd.registeredArguments.find((a) => a.variadic);
21960
- if (!variadic || given.has("--") || !"--".startsWith(partial)) return [];
22671
+ if (!variadic || !"--".startsWith(partial)) return [];
21961
22672
  return [{ name: "-- <text>", description: variadic.description }];
21962
22673
  }
22674
+ function completionState(command, words, partial) {
22675
+ let position = 0;
22676
+ let pending2;
22677
+ let requiresValue = false;
22678
+ let literal = false;
22679
+ const initial = withoutAncestorOptions(command, words, partial);
22680
+ let unknown = initial === void 0;
22681
+ const given = /* @__PURE__ */ new Set();
22682
+ const tokens = initial ?? [];
22683
+ while (tokens.length) {
22684
+ const word = tokens.shift();
22685
+ if (pending2) {
22686
+ if (requiresValue || !word.startsWith("-") || word === "-") {
22687
+ requiresValue = false;
22688
+ pending2 = pending2.variadic ? pending2 : void 0;
22689
+ continue;
22690
+ }
22691
+ pending2 = void 0;
22692
+ }
22693
+ if (!literal && word === "--") {
22694
+ literal = true;
22695
+ given.add(word);
22696
+ continue;
22697
+ }
22698
+ if (!literal && word.startsWith("-") && word !== "-") {
22699
+ const equals = word.startsWith("--") ? word.indexOf("=") : -1;
22700
+ const flag = equals >= 0 ? word.slice(0, equals) : word;
22701
+ const options = command.options;
22702
+ let option = options.find((o) => o.short === flag || o.long === flag);
22703
+ let inline = equals >= 0;
22704
+ if (!option && !word.startsWith("--") && word.length > 2) {
22705
+ option = options.find((o) => o.short === word.slice(0, 2));
22706
+ if (option) {
22707
+ inline = option.required || option.optional;
22708
+ if (!inline) tokens.unshift(`-${word.slice(2)}`);
22709
+ }
22710
+ }
22711
+ if (!option || inline && word.startsWith("--") && !option.required && !option.optional) {
22712
+ unknown = true;
22713
+ break;
22714
+ }
22715
+ given.add(option.long ?? option.short);
22716
+ if (!inline && (option.required || option.optional)) {
22717
+ pending2 = option;
22718
+ requiresValue = option.required;
22719
+ }
22720
+ continue;
22721
+ }
22722
+ const sub = !literal && position === 0 ? command.commands.find(
22723
+ (c) => c.name() === word || c.aliases().includes(word)
22724
+ ) : void 0;
22725
+ if (sub) {
22726
+ command = sub;
22727
+ const remaining = withoutAncestorOptions(command, tokens, partial);
22728
+ if (!remaining) {
22729
+ unknown = true;
22730
+ break;
22731
+ }
22732
+ tokens.splice(0, tokens.length, ...remaining);
22733
+ given.clear();
22734
+ } else {
22735
+ position++;
22736
+ }
22737
+ }
22738
+ return { command, position, given, pending: pending2, requiresValue, literal, unknown };
22739
+ }
22740
+ function withoutAncestorOptions(command, words, partial) {
22741
+ const ancestors = [];
22742
+ for (let scope = command.parent; scope; scope = scope.parent)
22743
+ ancestors.unshift(scope);
22744
+ let remaining = [...words];
22745
+ for (const scope of ancestors) {
22746
+ if (scope.options.length === 0) continue;
22747
+ const parser = new Command().exitOverride().configureOutput({ writeOut: () => {
22748
+ }, writeErr: () => {
22749
+ } });
22750
+ for (const option of scope.options)
22751
+ parser.addOption(new Option(option.flags));
22752
+ let boundary = "--__rook_completion_boundary";
22753
+ while (remaining.includes(boundary) || scope.options.some((o) => o.long === boundary))
22754
+ boundary += "_";
22755
+ let cursor = partial.length > 1 && partial.startsWith("-") ? "--__rook_cursor" : "__rook_cursor";
22756
+ while (remaining.includes(cursor) || scope.options.some((o) => o.long === cursor))
22757
+ cursor += "_";
22758
+ try {
22759
+ const parsed = parser.parseOptions([boundary, ...remaining, cursor]);
22760
+ if (!parsed.unknown.includes(cursor)) return void 0;
22761
+ remaining = parsed.unknown.slice(1, -1);
22762
+ } catch {
22763
+ return void 0;
22764
+ }
22765
+ }
22766
+ return remaining;
22767
+ }
21963
22768
  var init_commands = __esm({
21964
22769
  "src/app/commands.ts"() {
21965
22770
  "use strict";
22771
+ init_command_arguments();
21966
22772
  init_help();
21967
22773
  }
21968
22774
  });
@@ -22021,10 +22827,8 @@ function InputPrompt({
22021
22827
  };
22022
22828
  const accept = (name) => {
22023
22829
  const word = name.split(" ")[0];
22024
- const trailing = /\s$/.test(value);
22025
- const parts = value.split(/\s+/).filter(Boolean);
22026
- if (!trailing && parts.length > 0) parts.pop();
22027
- const next = [...parts, word].join(" ") + " ";
22830
+ const prefix = value.replace(/\S*$/, "");
22831
+ const next = `${prefix}${word} `;
22028
22832
  edit(next, next.length);
22029
22833
  };
22030
22834
  const wordStart = (text, at) => {
@@ -23506,33 +24310,6 @@ var init_ThinkingLine = __esm({
23506
24310
 
23507
24311
  // src/app/dispatch.ts
23508
24312
  import { CommanderError } from "commander";
23509
- function split3(line) {
23510
- const out = [];
23511
- let current2 = "";
23512
- let quote2 = null;
23513
- let started = false;
23514
- for (const ch of line) {
23515
- if (quote2) {
23516
- if (ch === quote2) quote2 = null;
23517
- else current2 += ch;
23518
- continue;
23519
- }
23520
- if (ch === '"' || ch === "'") {
23521
- quote2 = ch;
23522
- started = true;
23523
- continue;
23524
- }
23525
- if (/\s/.test(ch)) {
23526
- if (current2 || started) out.push(current2);
23527
- current2 = "";
23528
- started = false;
23529
- continue;
23530
- }
23531
- current2 += ch;
23532
- }
23533
- if (current2 || started) out.push(current2);
23534
- return out;
23535
- }
23536
24313
  function joinLists(command, argv) {
23537
24314
  const out = [];
23538
24315
  let cmd = command;
@@ -23683,7 +24460,7 @@ function resetOptions(cmd) {
23683
24460
  for (const sub of cmd.commands) resetOptions(sub);
23684
24461
  }
23685
24462
  async function dispatch(deps) {
23686
- const argv = split3(deps.line.trim());
24463
+ const argv = split2(deps.line.trim());
23687
24464
  const head = argv[0] ?? "";
23688
24465
  const name = head.startsWith("/") ? head.slice(1) : head;
23689
24466
  if (!name) return { kind: "unknown", name: "" };
@@ -23731,6 +24508,8 @@ function withExitOverride(program2) {
23731
24508
  var init_dispatch = __esm({
23732
24509
  "src/app/dispatch.ts"() {
23733
24510
  "use strict";
24511
+ init_command_arguments();
24512
+ init_command_arguments();
23734
24513
  init_commands();
23735
24514
  init_flags();
23736
24515
  }
@@ -25157,7 +25936,7 @@ function fatalExit(err, context = "command") {
25157
25936
  // src/index.ts
25158
25937
  init_flags();
25159
25938
  init_client();
25160
- import { Command } from "commander";
25939
+ import { Argument, Command as Command2 } from "commander";
25161
25940
 
25162
25941
  // src/auth/account.ts
25163
25942
  function accountView(identity, credits) {
@@ -26847,7 +27626,7 @@ import { join as join42 } from "path";
26847
27626
 
26848
27627
  // src/judge/judge.ts
26849
27628
  init_envelope();
26850
- import { copyFileSync as copyFileSync2, mkdirSync as mkdirSync27, statSync as statSync16, writeFileSync as writeFileSync25 } from "fs";
27629
+ import { copyFileSync, mkdirSync as mkdirSync27, statSync as statSync17, writeFileSync as writeFileSync25 } from "fs";
26851
27630
  import { basename as basename9, join as join39, relative as relative15 } from "path";
26852
27631
  import yaml12 from "js-yaml";
26853
27632
 
@@ -27808,7 +28587,7 @@ function evidencePack(scope, scenarioLocalId) {
27808
28587
  record(absolutePath) {
27809
28588
  if (seen.has(absolutePath)) return;
27810
28589
  try {
27811
- const size = statSync16(absolutePath).size;
28590
+ const size = statSync17(absolutePath).size;
27812
28591
  mkdirSync27(dir2, { recursive: true });
27813
28592
  const name = `${seen.size + 1}-${basename9(absolutePath)}`;
27814
28593
  if (size > PACK_FILE_BYTES_MAX) {
@@ -27818,7 +28597,7 @@ function evidencePack(scope, scenarioLocalId) {
27818
28597
  );
27819
28598
  return;
27820
28599
  }
27821
- copyFileSync2(absolutePath, join39(dir2, name));
28600
+ copyFileSync(absolutePath, join39(dir2, name));
27822
28601
  seen.set(absolutePath, name);
27823
28602
  } catch (err) {
27824
28603
  seen.set(absolutePath, `(not copied \u2014 ${err.message})`);
@@ -28497,14 +29276,14 @@ import {
28497
29276
  mkdirSync as mkdirSync29,
28498
29277
  readFileSync as readFileSync38,
28499
29278
  readdirSync as readdirSync20,
28500
- statSync as statSync18,
29279
+ statSync as statSync19,
28501
29280
  writeFileSync as writeFileSync27
28502
29281
  } from "fs";
28503
29282
  import { join as join41, relative as relative16 } from "path";
28504
29283
 
28505
29284
  // src/run/capture.ts
28506
29285
  init_constants();
28507
- import { copyFileSync as copyFileSync3, existsSync as existsSync35, mkdirSync as mkdirSync28, statSync as statSync17, writeFileSync as writeFileSync26 } from "fs";
29286
+ import { copyFileSync as copyFileSync2, existsSync as existsSync35, mkdirSync as mkdirSync28, statSync as statSync18, writeFileSync as writeFileSync26 } from "fs";
28508
29287
  import { basename as basename10, extname as extname8, join as join40 } from "path";
28509
29288
  var MAX_BYTES = 8 * 1024 * 1024;
28510
29289
  var MAX_ARTEFACTS = 12;
@@ -28620,7 +29399,7 @@ async function captureArtifacts(scenarioDir2, data) {
28620
29399
  continue;
28621
29400
  }
28622
29401
  try {
28623
- const stat = statSync17(source);
29402
+ const stat = statSync18(source);
28624
29403
  if (!stat.isFile()) {
28625
29404
  result2.missed.push({ source, reason: "not a file" });
28626
29405
  continue;
@@ -28636,7 +29415,7 @@ async function captureArtifacts(scenarioDir2, data) {
28636
29415
  });
28637
29416
  continue;
28638
29417
  }
28639
- copyFileSync3(source, target);
29418
+ copyFileSync2(source, target);
28640
29419
  bytes += stat.size;
28641
29420
  result2.files.push(target);
28642
29421
  } catch {
@@ -29030,7 +29809,7 @@ function capturedFiles(scenarioDirectory) {
29030
29809
  const dir2 = join41(scenarioDirectory, "artifacts");
29031
29810
  if (!existsSync36(dir2)) return [];
29032
29811
  try {
29033
- return readdirSync20(dir2).map((name) => join41(dir2, name)).filter((at) => statSync18(at).isFile());
29812
+ return readdirSync20(dir2).map((name) => join41(dir2, name)).filter((at) => statSync19(at).isFile());
29034
29813
  } catch {
29035
29814
  return [];
29036
29815
  }
@@ -29251,6 +30030,7 @@ async function executeRun(opts, deps) {
29251
30030
  }
29252
30031
  }
29253
30032
  const total = opts.scenarios.length;
30033
+ const planned = total + (opts.judgeOnly?.length ?? 0);
29254
30034
  const reachAtStart = resolveReach(opts.root);
29255
30035
  const verdicts = [];
29256
30036
  const outcomes = /* @__PURE__ */ new Map();
@@ -29259,6 +30039,7 @@ async function executeRun(opts, deps) {
29259
30039
  let haltReason;
29260
30040
  let next = 0;
29261
30041
  let consecutiveUnreached = 0;
30042
+ let missingEvidence = false;
29262
30043
  const stop = (why2) => {
29263
30044
  if (!halted2) haltReason = why2;
29264
30045
  halted2 = true;
@@ -29268,25 +30049,29 @@ async function executeRun(opts, deps) {
29268
30049
  const index = next++;
29269
30050
  return { scenario: opts.scenarios[index], index };
29270
30051
  };
30052
+ const stopDue = () => {
30053
+ if (opts.signal.aborted) {
30054
+ stop("interrupted");
30055
+ return true;
30056
+ }
30057
+ if (!deps.budget.canStart()) {
30058
+ opts.onProgress?.(
30059
+ `${stoppedBecause("budget_halt")} \u2014 stopping before the next scenario`
30060
+ );
30061
+ stop("budget");
30062
+ return true;
30063
+ }
30064
+ const halt = halted();
30065
+ if (halt) {
30066
+ opts.onProgress?.(halt.message);
30067
+ stop(halt.kind);
30068
+ return true;
30069
+ }
30070
+ return false;
30071
+ };
29271
30072
  const worker = async () => {
29272
30073
  for (; ; ) {
29273
- if (opts.signal.aborted) {
29274
- stop("interrupted");
29275
- return;
29276
- }
29277
- if (!deps.budget.canStart()) {
29278
- opts.onProgress?.(
29279
- `${stoppedBecause("budget_halt")} \u2014 stopping before the next scenario`
29280
- );
29281
- stop("budget");
29282
- return;
29283
- }
29284
- const halt = halted();
29285
- if (halt) {
29286
- opts.onProgress?.(halt.message);
29287
- stop(halt.kind);
29288
- return;
29289
- }
30074
+ if (stopDue()) return;
29290
30075
  const item = takeOne();
29291
30076
  if (!item) return;
29292
30077
  const { scenario: scenario2, index } = item;
@@ -29367,36 +30152,56 @@ async function executeRun(opts, deps) {
29367
30152
  }
29368
30153
  };
29369
30154
  for (const scenario2 of opts.judgeOnly ?? []) {
29370
- if (opts.signal.aborted) break;
29371
- const execution = readCarriedResponse(opts, scenario2);
29372
- if (!execution) continue;
30155
+ const execution = opts.unavailableEvidence?.has(scenario2.local_id) ? null : readCarriedResponse(opts, scenario2);
30156
+ if (!execution) {
30157
+ opts.onProgress?.(
30158
+ "carried evidence could not be read \u2014 restore it and start a newly approved run",
30159
+ scenario2.local_id
30160
+ );
30161
+ missingEvidence = true;
30162
+ continue;
30163
+ }
30164
+ if (!wants("judge")) {
30165
+ opts.onProgress?.(
30166
+ "not judged \u2014 rerun with --phases judge when you want a verdict",
30167
+ scenario2.local_id
30168
+ );
30169
+ outcomes.set(scenario2.local_id, "unjudged");
30170
+ continue;
30171
+ }
30172
+ if (stopDue()) break;
29373
30173
  opts.onProgress?.("judging what the last run executed", scenario2.local_id);
29374
- const judged = await judgeScenario(
29375
- { root: opts.root, agentId: opts.agentId, runId: opts.runId },
29376
- scenario2,
29377
- judgeContext(
29378
- opts,
30174
+ const judgeDeps = {
30175
+ ...deps,
30176
+ ctx: {
30177
+ ...deps.ctx,
30178
+ runId: opts.upstream?.run_id,
30179
+ runScenarioId: opts.upstream?.scenario_ids[planKey(scenario2.local_id, 1)]
30180
+ }
30181
+ };
30182
+ try {
30183
+ const judged = await judgeScenario(
30184
+ { root: opts.root, agentId: opts.agentId, runId: opts.runId },
29379
30185
  scenario2,
29380
- /*
29381
- * THIS run executes nothing for a carried scenario, so it observes no
29382
- * filesystem — said as its own fact rather than as a concurrency
29383
- * number, which would print a "re-run at --concurrency 1" remedy to
29384
- * somebody already at 1 and would suppress the earlier walk's note.
29385
- *
29386
- * Reading the requirement's fate out of the carried `response.json`
29387
- * instead is reading a file the target can write: deleting `fs_note`
29388
- * from it removed the row and turned the verdict Pass.
29389
- */
29390
- unmetNow(opts, scenario2, 1, reachAtStart, false)
29391
- ),
29392
- execution,
29393
- deps,
29394
- (m) => opts.onProgress?.(m, scenario2.local_id)
29395
- );
29396
- credits += judged.credits;
29397
- verdicts.push(judged.verdict);
29398
- outcomes.set(scenario2.local_id, outcomeOf(judged.verdict));
29399
- await opts.onVerdict?.(judged.verdict, scenario2, 1);
30186
+ judgeContext(
30187
+ opts,
30188
+ scenario2,
30189
+ unmetNow(opts, scenario2, 1, reachAtStart, false)
30190
+ ),
30191
+ execution,
30192
+ judgeDeps,
30193
+ (m) => opts.onProgress?.(m, scenario2.local_id)
30194
+ );
30195
+ credits += judged.credits;
30196
+ verdicts.push(judged.verdict);
30197
+ outcomes.set(scenario2.local_id, outcomeOf(judged.verdict));
30198
+ await opts.onVerdict?.(judged.verdict, scenario2, 1);
30199
+ } catch (err) {
30200
+ opts.onProgress?.(
30201
+ `judge failed: ${err?.message ?? String(err)}`,
30202
+ scenario2.local_id
30203
+ );
30204
+ }
29400
30205
  }
29401
30206
  const results = await Promise.allSettled(
29402
30207
  Array.from(
@@ -29412,6 +30217,7 @@ async function executeRun(opts, deps) {
29412
30217
  stop("runner_error");
29413
30218
  }
29414
30219
  }
30220
+ if (missingEvidence) stop("runner_error");
29415
30221
  const order = new Map(opts.scenarios.map((s, i) => [s.local_id, i]));
29416
30222
  verdicts.sort((a, b) => {
29417
30223
  const byScenario = (order.get(a.scenario_id) ?? 1e9) - (order.get(b.scenario_id) ?? 1e9);
@@ -29419,7 +30225,7 @@ async function executeRun(opts, deps) {
29419
30225
  });
29420
30226
  return {
29421
30227
  verdicts,
29422
- totals: tally(outcomes, { planned: total, unrunnable: opts.unrunnable }),
30228
+ totals: tally(outcomes, { planned, unrunnable: opts.unrunnable }),
29423
30229
  credits,
29424
30230
  halted: halted2,
29425
30231
  haltReason
@@ -29561,508 +30367,7 @@ init_manifest();
29561
30367
  init_sync2();
29562
30368
  init_materialise();
29563
30369
  init_plan_file();
29564
-
29565
- // src/run/plan.ts
29566
- init_subagent();
29567
- init_role_failure();
29568
- init_registry4();
29569
- init_context();
29570
- init_types3();
29571
- init_env();
29572
- init_render();
29573
- import yaml13 from "js-yaml";
29574
- var PLAN_SCHEMA2 = {
29575
- type: "object",
29576
- /*
29577
- * `rca` is NOT required, and requiring it failed real runs.
29578
- *
29579
- * "run_planner: the reply does not match the shape asked for — rca:
29580
- * required, and absent" is the whole of `/run` refusing because a boolean
29581
- * with an obvious default was left out. The schema's own description says
29582
- * "false is the ordinary answer", and `planRun` already reads it as
29583
- * `Boolean(child.data.rca) || Boolean(input.rca)` — so absence was always
29584
- * handled everywhere except the validator that rejected it first.
29585
- *
29586
- * The three that remain are the answer itself: what to run, what was left
29587
- * out, and the sentence somebody reads before spending. A reply missing any
29588
- * of those is not a plan.
29589
- */
29590
- required: ["included", "skipped", "summary"],
29591
- properties: {
29592
- /**
29593
- * The four lists are a PARTITION of the candidates.
29594
- *
29595
- * Every candidate lands in exactly one, and rook enforces it — a scenario
29596
- * in two is ambiguous about whether it costs an agent call, and one in none
29597
- * is silently dropped. `judge_only` and `carried` are the resume half: rook
29598
- * works out which scenarios are ELIGIBLE for each from the files, and the
29599
- * planner decides which of them the request actually wants that done to.
29600
- */
29601
- included: {
29602
- type: "array",
29603
- description: "Execute these: call the agent, then judge. One entry per scenario.",
29604
- items: {
29605
- type: "object",
29606
- required: ["scenario_id", "why", "confidence"],
29607
- properties: {
29608
- scenario_id: { type: "string" },
29609
- why: {
29610
- type: "string",
29611
- description: "What this one establishes that the others do not. Not 'important coverage', which is true of everything and decides nothing."
29612
- },
29613
- confidence: {
29614
- type: "string",
29615
- enum: ["high", "medium", "low"],
29616
- description: "`low` says you are unsure it earns its cost \u2014 exactly what somebody about to approve wants to know."
29617
- }
29618
- }
29619
- }
29620
- },
29621
- judge_only: {
29622
- type: "array",
29623
- description: "Already executed by the prior run and never judged. A judge call and NO agent call. Only ids listed as ungraded below are eligible.",
29624
- items: {
29625
- type: "object",
29626
- required: ["scenario_id", "why"],
29627
- properties: {
29628
- scenario_id: { type: "string" },
29629
- why: { type: "string" },
29630
- confidence: { type: "string", enum: ["high", "medium", "low"] }
29631
- }
29632
- }
29633
- },
29634
- carried: {
29635
- type: "array",
29636
- description: "Already decided by the prior run. Copy the verdict forward \u2014 no agent call, no judge call. Only ids listed as decided below are eligible.",
29637
- items: {
29638
- type: "object",
29639
- required: ["scenario_id", "why"],
29640
- properties: {
29641
- scenario_id: { type: "string" },
29642
- why: { type: "string" },
29643
- confidence: { type: "string", enum: ["high", "medium", "low"] }
29644
- }
29645
- }
29646
- },
29647
- rca: {
29648
- type: "boolean",
29649
- description: "Explain the failure clusters when this run ends. True when the request asks WHY something fails, or when a run this large failing would leave somebody with nowhere to start. It costs one model call per cluster, so false is the ordinary answer."
29650
- },
29651
- skipped: {
29652
- type: "array",
29653
- description: "One entry per scenario you deliberately left out.",
29654
- items: {
29655
- type: "object",
29656
- required: ["scenario_id", "why", "confidence"],
29657
- properties: {
29658
- scenario_id: { type: "string" },
29659
- why: {
29660
- type: "string",
29661
- description: "Why it is not worth running now. A scenario nobody considered and one deliberately excluded look identical afterwards."
29662
- },
29663
- confidence: { type: "string", enum: ["high", "medium", "low"] }
29664
- }
29665
- }
29666
- },
29667
- summary: {
29668
- type: "string",
29669
- description: "What this run will establish, and what it will not. The sentence a person reads before deciding to spend."
29670
- },
29671
- resume_from: {
29672
- type: "string",
29673
- description: "A prior run to carry finished work from. Omit unless proposing one."
29674
- },
29675
- /**
29676
- * Two fields of `run.yaml` the planner may set, and only these two.
29677
- *
29678
- * Both are things a person expresses in words — "call this the threshold
29679
- * sweep", "run them one at a time" — and neither changes what a run MEANS.
29680
- *
29681
- * `test_mode` is deliberately NOT here. It decides whether the run is
29682
- * pinned upstream at all, so a model able to set it could turn a real run
29683
- * into one that records nothing, or a smoke test into a version-pinned
29684
- * result. That is a flag, and flags are for people.
29685
- *
29686
- * An explicit flag always wins over both — see `applyPlanned`.
29687
- */
29688
- name: {
29689
- type: "string",
29690
- description: "A short name for this run, when the request suggests one. Omit rather than inventing a label nobody used."
29691
- },
29692
- concurrency: {
29693
- type: "integer",
29694
- description: "How many scenarios to run at once, when the request says. One is the default and the right answer for anything hitting a rate-limited API."
29695
- }
29696
- }
29697
- };
29698
- function staticPlan(input) {
29699
- const { partition: partition2 } = input;
29700
- const included = partition2.candidates.map((c) => ({
29701
- scenario_id: c.local_id,
29702
- why: "a candidate, and nothing narrowed this run further",
29703
- confidence: "high"
29704
- }));
29705
- const judge_only = [];
29706
- const carried = [];
29707
- const skipped = [
29708
- ...partition2.excluded.map((e) => ({
29709
- scenario_id: e.scenario_id,
29710
- why: `excluded by the flags: ${e.why}`,
29711
- confidence: "high"
29712
- })),
29713
- ...partition2.filtered.map((f) => ({
29714
- scenario_id: f.scenario_id,
29715
- why: `left out by the tree: ${f.why}`,
29716
- confidence: "high"
29717
- }))
29718
- ];
29719
- const counts = [
29720
- included.length ? `${included.length} to execute` : "",
29721
- // SAID, not implied. Somebody who typed `--resume` expects work to be
29722
- // reused, and a plan that silently re-ran everything would look like one
29723
- // that had reused nothing to reuse.
29724
- input.resumeHint ? `nothing reused from ${input.resumeHint}` : ""
29725
- ].filter(Boolean);
29726
- return {
29727
- included,
29728
- skipped,
29729
- judge_only,
29730
- carried,
29731
- /*
29732
- * `--rca` and nothing else. There is no sentence for anything to read an
29733
- * intent out of, and RCA costs a model call per failure cluster — turning
29734
- * it on because a run happens to be large would spend on an explanation
29735
- * nobody asked for.
29736
- */
29737
- rca: Boolean(input.rca),
29738
- summary: counts.length ? `no instruction given \u2014 ${counts.join(", ")}. Every candidate the flags and the tree left.` : "no instruction given, and nothing is left to run.",
29739
- ...input.resumeHint ? { resume_from: input.resumeHint } : {},
29740
- /*
29741
- * No `name` and no `concurrency`. Both are proposals a planner makes from
29742
- * a sentence, and `applyPlanned` already lets an explicit flag win — so
29743
- * omitting them leaves the flag, or the default, exactly as typed.
29744
- */
29745
- // Nothing was spent. A non-zero cost here would bill a run for a decision
29746
- // no model was asked to make.
29747
- credits: 0
29748
- };
29749
- }
29750
- async function planRun(input, deps) {
29751
- const { partition: partition2 } = input;
29752
- const flagsAsked = input.flags ?? [];
29753
- const task4 = [
29754
- input.instruction ? `## WHAT THE USER ASKED FOR
29755
-
29756
- ${input.instruction}
29757
- ` : "",
29758
- input.resumeHint ? `## THEY NAMED A RUN
29759
-
29760
- ${input.resumeHint} \u2014 look it up before planning.
29761
- ` : "",
29762
- /**
29763
- * The same rendering of the agent every other planner gets.
29764
- *
29765
- * From the same function, so a planner deciding what to SPEND cannot be
29766
- * reasoning about a different agent than the writers described.
29767
- */
29768
- agentContext({
29769
- spec: input.spec,
29770
- features: input.features,
29771
- ...input.findings?.length ? { findings: input.findings } : {},
29772
- ...input.callers?.length ? { callers: input.callers } : {},
29773
- ...input.callees?.length ? { callees: input.callees } : {}
29774
- }),
29775
- "",
29776
- /**
29777
- * How the agent is reached, and what rook can SEE while it runs.
29778
- *
29779
- * The second half is what decides whether a scenario is checkable at all,
29780
- * and the unrunnable list below is computed from it — so the planner is
29781
- * shown the reason as well as the verdict.
29782
- */
29783
- "## THE PROFILE THIS RUN WILL USE",
29784
- "",
29785
- "```yaml",
29786
- yaml13.dump(
29787
- {
29788
- id: input.profile.id,
29789
- /*
29790
- * WHICH PHASES, not which transport.
29791
- *
29792
- * `kind` and `mode` were here and told the planner the agent was
29793
- * reached over http, synchronously — neither of which changes what
29794
- * to run. The phases do: a profile with a `collect` can be run now
29795
- * and collected later, and one without cannot, so a planner asked
29796
- * about staging a run needs this and had no way to see it.
29797
- */
29798
- phases: definedPhases(input.profile),
29799
- multi_turn: Boolean(input.profile.capabilities?.multi_turn),
29800
- /*
29801
- * What the profile REPORTS, and it must not contradict the gap block
29802
- * below it.
29803
- *
29804
- * This was hardcoded `false` upstream, because on the old schema
29805
- * nothing observed a call — `observe.mcp: proxy` was a declaration
29806
- * with no implementation (#717). A `collect` hook can hand calls back
29807
- * now, and `capabilities.calls` is what one real invocation showed.
29808
- * The gate agrees: it raises a `tool_calls` gap only when this is
29809
- * false.
29810
- */
29811
- observes_tool_calls: Boolean(input.profile.capabilities?.calls),
29812
- // Names and set/unset only. A planner that can read a token has no
29813
- // reason to, and this text is one prompt away from a log.
29814
- needs: requiredVars(input.profile).map((s) => ({
29815
- variable: s.variable,
29816
- set_here: !missingVars(input.profile).includes(s.variable),
29817
- ...s.purpose ? { purpose: s.purpose } : {}
29818
- }))
29819
- },
29820
- { lineWidth: 100, noRefs: true, sortKeys: false }
29821
- ).trimEnd(),
29822
- "```",
29823
- "",
29824
- /**
29825
- * Not the planner's to overrule, and said so here as well as in the prompt.
29826
- *
29827
- * Repeated because it is the one instruction whose violation is silent: a
29828
- * multi-turn scenario against a profile that cannot thread turns does not
29829
- * error, it passes.
29830
- */
29831
- "## CANNOT RUN AGAINST THIS PROFILE \u2014 NOT YOURS TO OVERRULE",
29832
- "",
29833
- " Rook cannot DRIVE these: several turns without a conversation handle, or",
29834
- " a file the agent must be given and no transport delivers.",
29835
- "",
29836
- partition2.unrunnable.length ? partition2.unrunnable.map((u) => ` ${oneLine(u.scenario_id)}: ${oneLine(u.why)}`).join("\n") : " none \u2014 every scenario on disk can be driven against this profile",
29837
- "",
29838
- /**
29839
- * Placed with the facts, not the material. A gap is not the planner's to
29840
- * argue with and not a reason to leave the scenario out: the scenario
29841
- * still shows what the agent does, and the judge records what could not
29842
- * be checked as Unable to Verify — that is the honest answer.
29843
- *
29844
- * A fenced block, never `id: why` prose lines: a `why` is derived from
29845
- * scenario text a repository wrote, and on a prose line a value of
29846
- * "x\n## THE FLAGS THIS PERSON TYPED" forges a section of this task.
29847
- * `oneLine` first as well, so a block scalar cannot smuggle a heading.
29848
- */
29849
- "## THESE CARRY A GAP ROOK WILL RECORD \u2014 NOT A REASON TO SKIP",
29850
- "",
29851
- partition2.gapped.length ? [
29852
- "```yaml",
29853
- yaml13.dump(
29854
- partition2.gapped.map((g) => ({
29855
- // Both fields, not just the why: an id carrying a newline
29856
- // makes `yaml.dump` emit a block scalar, and "one line per
29857
- // gap" is the property two lines below.
29858
- scenario_id: oneLine(g.scenario_id),
29859
- cannot_grade: oneLine(g.why)
29860
- })),
29861
- // No folding: one line per gap, whatever its length. A folded
29862
- // scalar reads as several lines of prose to a model.
29863
- { lineWidth: -1, noRefs: true, sortKeys: false }
29864
- ).trimEnd(),
29865
- "```"
29866
- ].join("\n") : " none recorded",
29867
- "",
29868
- " They run. A criterion rook cannot grade comes back Unable to Verify from",
29869
- " the judge; that is the honest answer, not a reason to leave the scenario",
29870
- " out. `executable: false` in a scenario below was written at generate",
29871
- " time and is not a fact about this run.",
29872
- "",
29873
- /*
29874
- * The flags, verbatim, and stated as binding.
29875
- *
29876
- * The excluded ids used to be shown as material the planner "may disagree
29877
- * with", so `--only SC-001` was a suggestion — and a planner that decided
29878
- * the neighbouring scenarios were worth running got them, because nothing
29879
- * downstream checked. Naming the flags as well as their effect matters:
29880
- * a list of excluded ids does not tell a model that a PERSON narrowed
29881
- * this, and that is the fact that decides whether it may argue.
29882
- */
29883
- ...flagsAsked.length ? [
29884
- "## THE FLAGS THIS PERSON TYPED \u2014 BINDING, NOT YOURS TO OVERRULE",
29885
- "",
29886
- ...flagsAsked.map((f) => ` ${f}`),
29887
- "",
29888
- " They narrowed this run deliberately. Every scenario those flags",
29889
- " excluded is listed below and is OUT \u2014 naming one in included,",
29890
- " judge_only or carried is an error, and rook drops it. If the flags",
29891
- " look wrong for what they asked, say so in summary rather than",
29892
- " planning around them.",
29893
- "",
29894
- "## WHAT THOSE FLAGS EXCLUDED \u2014 out of this run",
29895
- "",
29896
- partition2.excluded.length ? partition2.excluded.map((f) => ` ${f.scenario_id}: ${f.why}`).join("\n") : " nothing \u2014 every scenario on disk matched them",
29897
- ""
29898
- ] : [],
29899
- "## WHAT THE TREE LEFT OUT \u2014 material, and you may disagree",
29900
- "",
29901
- partition2.filtered.length ? partition2.filtered.map((f) => ` ${f.scenario_id}: ${f.why}`).join("\n") : " nothing \u2014 no scenario was dropped for the state it is in",
29902
- "",
29903
- "## YOUR CANDIDATES \u2014 everything you may choose from",
29904
- "",
29905
- partition2.candidates.length ? partition2.candidates.map((c) => ` ${c.local_id} ${c.title}`).join("\n") : " nothing \u2014 the flags excluded everything runnable",
29906
- "",
29907
- ...input.stale.length ? [
29908
- "## THESE PIN OLDER FEATURE TEXT",
29909
- "",
29910
- ` ${input.stale.join(", ")}`,
29911
- "",
29912
- "They still run \u2014 an outdated test that passes is information. What must",
29913
- "not happen is a plan describing them as though they were written",
29914
- "against the feature as it stands.",
29915
- ""
29916
- ] : [],
29917
- /**
29918
- * Every scenario, in full, and LAST — the state block.
29919
- *
29920
- * Titles and counts cannot tell six behaviours from six rewordings, and
29921
- * that difference is exactly what decides whether a run is worth its cost.
29922
- * Last because it is the part that changes every run, which is what prompt
29923
- * caching wants.
29924
- */
29925
- "## EVERY SCENARIO ON DISK",
29926
- "",
29927
- "```yaml",
29928
- yaml13.dump(input.scenarios, { lineWidth: 100, noRefs: true, sortKeys: false }).trimEnd(),
29929
- "```",
29930
- ""
29931
- ].filter(Boolean).join("\n");
29932
- const child = await spawnSubagent(
29933
- {
29934
- role: "run_planner",
29935
- label: `${partition2.candidates.length} candidate(s)`,
29936
- task: task4,
29937
- // The BUDGET phase, not the command. `plan_scenarios` bills to
29938
- // "generate" for the same reason: a planner is charged to the work it is
29939
- // planning, so one bucket covers deciding and doing.
29940
- phase: "execute",
29941
- responseSchema: PLAN_SCHEMA2,
29942
- tools: toolsForRole("run_planner", { root: input.root, mcpAgent: null })
29943
- },
29944
- deps
29945
- );
29946
- if (!child.ok || child.data == null)
29947
- throw new Error(roleFailure("run_planner", child));
29948
- const forbidden = /* @__PURE__ */ new Set([
29949
- ...partition2.unrunnable.map((u) => u.scenario_id),
29950
- ...partition2.excluded.map((e) => e.scenario_id)
29951
- ]);
29952
- const known = new Set(input.scenarios.map((s) => s.local_id));
29953
- const included = (child.data.included ?? []).filter(
29954
- (e) => e?.scenario_id && known.has(e.scenario_id) && !forbidden.has(e.scenario_id)
29955
- );
29956
- const skipped = (child.data.skipped ?? []).filter(
29957
- (e) => e?.scenario_id && known.has(e.scenario_id)
29958
- );
29959
- const canJudge = new Set(input.ungraded ?? []);
29960
- const canCarry = new Set(input.decided ?? []);
29961
- const eligible = (entries, allowed) => {
29962
- const kept = [];
29963
- const demoted = [];
29964
- for (const e of entries ?? []) {
29965
- if (!e?.scenario_id || !known.has(e.scenario_id)) continue;
29966
- if (forbidden.has(e.scenario_id)) continue;
29967
- (allowed.has(e.scenario_id) ? kept : demoted).push(e);
29968
- }
29969
- return { kept, demoted };
29970
- };
29971
- const judgeOnly = eligible(child.data.judge_only, canJudge);
29972
- const carried = eligible(child.data.carried, canCarry);
29973
- const claimed = /* @__PURE__ */ new Set();
29974
- const takeOnce = (entries) => entries.filter(
29975
- (e) => !claimed.has(e.scenario_id) && claimed.add(e.scenario_id)
29976
- );
29977
- const finalIncluded = takeOnce([
29978
- ...included,
29979
- ...judgeOnly.demoted,
29980
- ...carried.demoted
29981
- ]);
29982
- const finalJudgeOnly = takeOnce(judgeOnly.kept);
29983
- const finalCarried = takeOnce(carried.kept);
29984
- return {
29985
- included: finalIncluded,
29986
- skipped,
29987
- judge_only: finalJudgeOnly,
29988
- carried: finalCarried,
29989
- /**
29990
- * The flag forces it ON and never off.
29991
- *
29992
- * A planner that declined costs nothing to overrule, and somebody who typed
29993
- * `--rca` has asked plainly. The reverse — a flag that silences a planner
29994
- * which decided an explanation was needed — would hide the reasoning at the
29995
- * moment it is most wanted.
29996
- */
29997
- rca: Boolean(child.data.rca) || Boolean(input.rca),
29998
- summary: child.data.summary ?? "",
29999
- ...child.data.resume_from ? { resume_from: child.data.resume_from } : {},
30000
- ...child.data.name ? { name: child.data.name } : {},
30001
- ...child.data.concurrency ? { concurrency: child.data.concurrency } : {},
30002
- credits: child.credits
30003
- };
30004
- }
30005
- var MAX_PLANNED_CONCURRENCY = 8;
30006
- function applyPlanned(plan2, flags) {
30007
- const proposed = plan2.concurrency ?? 1;
30008
- return {
30009
- ...flags.name ?? plan2.name ? { name: flags.name ?? plan2.name } : {},
30010
- concurrency: flags.concurrency ?? Math.min(Math.max(1, Math.trunc(proposed)), MAX_PLANNED_CONCURRENCY)
30011
- };
30012
- }
30013
- function renderRunPlan(plan2, view) {
30014
- const gap = new Map(view.gapped.map((g) => [g.scenario_id, g.why]));
30015
- const excluded = new Set(view.excluded.map((e) => e.scenario_id));
30016
- const gapLine = (id) => {
30017
- const why2 = gap.get(id);
30018
- return why2 === void 0 ? [] : [` cannot grade: ${oneLine(why2)}`];
30019
- };
30020
- const lines = [`plan: ${plan2.included.length} scenario(s) to run`];
30021
- for (const e of plan2.included) {
30022
- const sure = e.confidence && e.confidence !== "high" ? ` (${e.confidence} confidence)` : "";
30023
- lines.push(` ${oneLine(e.scenario_id)}${sure}`);
30024
- lines.push(` ${oneLine(e.why)}`);
30025
- lines.push(...gapLine(e.scenario_id));
30026
- }
30027
- for (const e of plan2.judge_only) {
30028
- lines.push(` ${oneLine(e.scenario_id)} judge only \u2014 ${oneLine(e.why)}`);
30029
- lines.push(...gapLine(e.scenario_id));
30030
- }
30031
- for (const e of plan2.carried) {
30032
- lines.push(` ${oneLine(e.scenario_id)} carried \u2014 ${oneLine(e.why)}`);
30033
- lines.push(...gapLine(e.scenario_id));
30034
- }
30035
- for (const e of plan2.skipped) {
30036
- lines.push(` ${oneLine(e.scenario_id)} skipped \u2014 ${oneLine(e.why)}`);
30037
- const why2 = gap.get(e.scenario_id);
30038
- if (why2 === void 0) continue;
30039
- lines.push(
30040
- excluded.has(e.scenario_id) ? ` carries a gap: ${oneLine(why2)}` : ` carries a gap, which is not a reason to skip: ${oneLine(why2)}`
30041
- );
30042
- }
30043
- if (view.unrunnable.length)
30044
- lines.push(` ${view.unrunnable.length} cannot run against this profile`);
30045
- if (plan2.resume_from)
30046
- lines.push(` carrying finished work from ${plan2.resume_from}`);
30047
- return lines;
30048
- }
30049
- function decidedElsewhere(plan2) {
30050
- return new Set(
30051
- [...plan2.judge_only ?? [], ...plan2.carried ?? []].map(
30052
- (e) => e.scenario_id
30053
- )
30054
- );
30055
- }
30056
- function gapLines(view, plan2) {
30057
- const does = /* @__PURE__ */ new Map();
30058
- for (const e of plan2.included) does.set(e.scenario_id, "runs");
30059
- for (const e of plan2.judge_only ?? []) does.set(e.scenario_id, "is judged");
30060
- return view.gapped.filter((g) => does.has(g.scenario_id)).map(
30061
- (g) => `${oneLine(g.scenario_id)}: ${does.get(g.scenario_id)} \u2014 cannot grade: ${oneLine(g.why)}`
30062
- );
30063
- }
30064
-
30065
- // src/run/run.ts
30370
+ init_plan2();
30066
30371
  init_render();
30067
30372
  init_flags();
30068
30373
  init_tally();
@@ -30079,7 +30384,7 @@ import {
30079
30384
  mkdirSync as mkdirSync31,
30080
30385
  readFileSync as readFileSync41,
30081
30386
  readdirSync as readdirSync22,
30082
- statSync as statSync19,
30387
+ statSync as statSync20,
30083
30388
  writeFileSync as writeFileSync29
30084
30389
  } from "fs";
30085
30390
  import { dirname as dirname23, join as join45 } from "path";
@@ -30182,7 +30487,7 @@ async function uploadDir(scope, dir2, kind, scenarioId, prefix) {
30182
30487
  for (const name of names) {
30183
30488
  const at = join45(dir2, name);
30184
30489
  try {
30185
- if (!statSync19(at).isFile()) continue;
30490
+ if (!statSync20(at).isFile()) continue;
30186
30491
  } catch {
30187
30492
  continue;
30188
30493
  }
@@ -30287,6 +30592,7 @@ async function uploadReport(ctx, upstreamRunId, root, agentId, runId, report2) {
30287
30592
  // src/run/upstream.ts
30288
30593
  init_client();
30289
30594
  init_materialise();
30595
+ init_plan2();
30290
30596
  init_plan_file();
30291
30597
  import { readFileSync as readFileSync42 } from "fs";
30292
30598
  import { join as join46 } from "path";
@@ -30449,7 +30755,7 @@ async function reconcileRuns(ctx, root, agentId, say2) {
30449
30755
  if (!runId) continue;
30450
30756
  const reported = new Set(plan2.upstream?.reported ?? []);
30451
30757
  const owed = [];
30452
- for (const entry of plan2.included) {
30758
+ for (const entry of judgedHere(plan2)) {
30453
30759
  const attempts = 8;
30454
30760
  for (let attempt = 1; attempt <= attempts; attempt++) {
30455
30761
  const key2 = planKey(entry.scenario_id, attempt);
@@ -30802,7 +31108,7 @@ async function run3(opts, deps) {
30802
31108
  why: r.why ?? `continuing ${opts.continueRun}`
30803
31109
  }));
30804
31110
  plan2 = {
30805
- included: asEntries(prior.included ?? []),
31111
+ included: asEntries(judgedHere(prior)),
30806
31112
  skipped: asEntries(prior.skipped ?? []),
30807
31113
  /*
30808
31114
  * Both empty, and neither is a resume.
@@ -30852,12 +31158,6 @@ async function run3(opts, deps) {
30852
31158
  partition: split4,
30853
31159
  ...opts.resumeHint ? { resumeHint: opts.resumeHint } : {},
30854
31160
  ...opts.rca ? { rca: true } : {}
30855
- /*
30856
- * `eligible` is NOT passed, and `staticPlan`'s header says why: the
30857
- * lists it would fill are dropped a hundred lines below, where
30858
- * `included` is built from `plan.included` alone. Handing them over
30859
- * would settle scenarios that then get no folder and no verdict.
30860
- */
30861
31161
  });
30862
31162
  credits += planned.credits;
30863
31163
  plan2 = planned;
@@ -30952,48 +31252,102 @@ async function run3(opts, deps) {
30952
31252
  reason: "no plan"
30953
31253
  };
30954
31254
  const byId = new Map(scenarios2.map((s) => [s.local_id, s]));
30955
- const included = plan2.included.map(
31255
+ const acted = scenariosOf(plan2).map(
30956
31256
  (e) => (opts.continueRun ? loadSnapshot(root, agentId, runId, e.scenario_id) : null) ?? byId.get(e.scenario_id)
30957
31257
  ).filter((s) => Boolean(s));
31258
+ const actedById = new Map(acted.map((s) => [s.local_id, s]));
30958
31259
  if (!opts.continueRun) {
30959
- materialise(root, agentId, runId, included);
31260
+ materialise(root, agentId, runId, acted);
30960
31261
  materialiseContext(root, agentId, runId, { spec, features, profile });
30961
31262
  }
30962
- const carriedIds = new Set(plan2.carried.map((e) => e.scenario_id));
30963
- const judgeOnlyIds = new Set(plan2.judge_only.map((e) => e.scenario_id));
31263
+ const carriedIds = /* @__PURE__ */ new Set();
30964
31264
  let carried = 0;
30965
31265
  const toJudgeOnly = [];
30966
- if (resumeFrom && (carriedIds.size > 0 || judgeOnlyIds.size > 0)) {
30967
- for (const sc of included) {
30968
- if (!carriedIds.has(sc.local_id) && !judgeOnlyIds.has(sc.local_id))
30969
- continue;
31266
+ const unavailableEvidence = /* @__PURE__ */ new Set();
31267
+ const planFailure = (err) => {
31268
+ const reason = `cannot read or update ${planPath(root, agentId, runId)}: ${err?.message ?? String(err)}`;
31269
+ say2("error", `${reason} \u2014 restore access and create a new run; no target-agent work started`);
31270
+ return { runId, credits, halted: true, reason };
31271
+ };
31272
+ const moved = [];
31273
+ if (resumeFrom) {
31274
+ const wanted = [
31275
+ ...plan2.carried.map((entry) => ({ entry, as: "carried" })),
31276
+ ...plan2.judge_only.map((entry) => ({ entry, as: "judge_only" }))
31277
+ ];
31278
+ for (const { entry, as } of wanted) {
31279
+ const sc = actedById.get(entry.scenario_id);
31280
+ if (!sc) continue;
30970
31281
  const what = carryForward(
30971
31282
  root,
30972
31283
  agentId,
30973
31284
  { runId: resumeFrom, scenarioId: sc.local_id },
30974
- { runId, scenarioId: sc.local_id }
31285
+ { runId, scenarioId: sc.local_id },
31286
+ { verdict: as === "carried" }
30975
31287
  );
30976
- if (carriedIds.has(sc.local_id) && what === "verdict") {
31288
+ if (as === "carried" && what === "verdict") {
30977
31289
  carried++;
31290
+ carriedIds.add(sc.local_id);
30978
31291
  continue;
30979
31292
  }
30980
- if (what === "response" || what === "verdict") toJudgeOnly.push(sc);
31293
+ if (what === "response" || what === "verdict") {
31294
+ toJudgeOnly.push(sc);
31295
+ if (as === "carried")
31296
+ moved.push({
31297
+ entry,
31298
+ why: `${resumeFrom} no longer holds a readable verdict.yaml \u2014 judged here instead`
31299
+ });
31300
+ continue;
31301
+ }
31302
+ unavailableEvidence.add(sc.local_id);
31303
+ toJudgeOnly.push(sc);
31304
+ moved.push({
31305
+ entry,
31306
+ why: `${resumeFrom} no longer holds a readable response.json \u2014 nothing to judge, and this run was not approved to execute it`
31307
+ });
30981
31308
  }
30982
31309
  if (carried > 0) {
30983
31310
  say2("system", `carried ${carried} verdict(s) forward from ${resumeFrom}`);
30984
31311
  }
30985
- if (toJudgeOnly.length > 0) {
31312
+ if (toJudgeOnly.length > unavailableEvidence.size && selection.phases.includes("judge")) {
30986
31313
  say2(
30987
31314
  "system",
30988
- `${toJudgeOnly.length} already executed \u2014 judging without calling the agent`
31315
+ `${toJudgeOnly.length - unavailableEvidence.size} already executed \u2014 judging without calling the agent`
30989
31316
  );
30990
31317
  }
31318
+ if (moved.length > 0) {
31319
+ for (const m of moved) say2("warn", `${m.entry.scenario_id}: ${m.why}`);
31320
+ try {
31321
+ const before = loadPlanOrThrow(root, agentId, runId);
31322
+ const movedIds = new Set(moved.map((m) => m.entry.scenario_id));
31323
+ const row = (m) => ({
31324
+ scenario_id: m.entry.scenario_id,
31325
+ why: m.why,
31326
+ confidence: "high"
31327
+ });
31328
+ savePlan(root, agentId, {
31329
+ ...before,
31330
+ judge_only: [
31331
+ ...(before.judge_only ?? []).filter(
31332
+ (e) => !movedIds.has(e.scenario_id)
31333
+ ),
31334
+ ...moved.map(row)
31335
+ ],
31336
+ carried: (before.carried ?? []).filter(
31337
+ (e) => !movedIds.has(e.scenario_id)
31338
+ )
31339
+ });
31340
+ } catch (err) {
31341
+ return planFailure(err);
31342
+ }
31343
+ }
30991
31344
  }
30992
31345
  const settled = /* @__PURE__ */ new Set([
30993
- ...plan2.carried.map((e) => e.scenario_id),
31346
+ ...carriedIds,
30994
31347
  ...toJudgeOnly.map((s) => s.local_id)
30995
31348
  ]);
30996
- const toExecute = included.filter((s) => !settled.has(s.local_id));
31349
+ const toExecute = acted.filter((s) => !settled.has(s.local_id));
31350
+ const own = acted.filter((s) => !carriedIds.has(s.local_id));
30997
31351
  const upstreamCtx = {
30998
31352
  api: opts.api,
30999
31353
  projectId: opts.projectId,
@@ -31001,7 +31355,12 @@ async function run3(opts, deps) {
31001
31355
  testMode: Boolean(opts.testMode)
31002
31356
  };
31003
31357
  const revisionOf = state4?.revisions?.scenarios ?? {};
31004
- const saved = loadPlanOrThrow(root, agentId, runId);
31358
+ let saved;
31359
+ try {
31360
+ saved = loadPlanOrThrow(root, agentId, runId);
31361
+ } catch (err) {
31362
+ return planFailure(err);
31363
+ }
31005
31364
  let upstream = null;
31006
31365
  try {
31007
31366
  upstream = await startRunUpstream(
@@ -31025,7 +31384,7 @@ async function run3(opts, deps) {
31025
31384
  // One or the other, never one field meaning both.
31026
31385
  ...state4?.profile_ids?.[profile.id] ? { profileId: state4.profile_ids[profile.id] } : { profileLocalId: profile.id }
31027
31386
  },
31028
- toExecute,
31387
+ own,
31029
31388
  revisionOf
31030
31389
  );
31031
31390
  } catch (err) {
@@ -31066,6 +31425,7 @@ async function run3(opts, deps) {
31066
31425
  profile,
31067
31426
  scenarios: toExecute,
31068
31427
  ...toJudgeOnly.length > 0 ? { judgeOnly: toJudgeOnly } : {},
31428
+ unavailableEvidence,
31069
31429
  unrunnable: split4.unrunnable.length,
31070
31430
  // The agent, once, for every judge this run spawns.
31071
31431
  spec,
@@ -31166,10 +31526,6 @@ async function run3(opts, deps) {
31166
31526
  }
31167
31527
  const totals = {
31168
31528
  ...result2.totals,
31169
- // Judged-without-executing counts as PLANNED work this run did; carried
31170
- // verdicts did not. A resume that judged two and inherited nine did not
31171
- // run eleven, and did not run two either.
31172
- planned: toExecute.length + toJudgeOnly.length,
31173
31529
  carried_forward: carried
31174
31530
  };
31175
31531
  await verdicts.flush();
@@ -31182,7 +31538,9 @@ async function run3(opts, deps) {
31182
31538
  const reported = opts.continueRun ? tallyFromVerdicts(allVerdicts, {
31183
31539
  planned: totals.planned,
31184
31540
  unrunnable: split4.unrunnable.length,
31185
- carriedForward: carried
31541
+ // Nothing is carried on a continuation; what the resume carried is on
31542
+ // its plan, and a later pass still inherited exactly that much.
31543
+ carriedForward: (saved.carried ?? []).length
31186
31544
  }) : totals;
31187
31545
  const invokedHooks = selection.phases.some(
31188
31546
  (p) => HOOK_PHASES.includes(p)
@@ -31201,7 +31559,7 @@ async function run3(opts, deps) {
31201
31559
  spec,
31202
31560
  features,
31203
31561
  verdicts: allVerdicts,
31204
- scenarios: included,
31562
+ scenarios: own,
31205
31563
  totals: reported,
31206
31564
  metrics,
31207
31565
  say: say2,
@@ -31404,8 +31762,8 @@ import {
31404
31762
  mkdirSync as mkdirSync32,
31405
31763
  mkdtempSync,
31406
31764
  readdirSync as readdirSync23,
31407
- rmSync as rmSync9,
31408
- statSync as statSync20,
31765
+ rmSync as rmSync10,
31766
+ statSync as statSync21,
31409
31767
  writeFileSync as writeFileSync31
31410
31768
  } from "fs";
31411
31769
  import { tmpdir } from "os";
@@ -31479,10 +31837,10 @@ function exportLogs(opts) {
31479
31837
  const out = destination(opts.out, name, tool.ext);
31480
31838
  mkdirSync32(dirname24(out), { recursive: true });
31481
31839
  build(tool.cmd, staging, name, out);
31482
- const bytes = existsSync40(out) ? statSync20(out).size : 0;
31840
+ const bytes = existsSync40(out) ? statSync21(out).size : 0;
31483
31841
  return { path: out, included, bytes };
31484
31842
  } finally {
31485
- rmSync9(staging, { recursive: true, force: true });
31843
+ rmSync10(staging, { recursive: true, force: true });
31486
31844
  }
31487
31845
  }
31488
31846
  function notALink(src) {
@@ -31497,11 +31855,11 @@ function scrub(box) {
31497
31855
  for (const entry of readdirSync23(dir2, { withFileTypes: true })) {
31498
31856
  const at = join47(dir2, entry.name);
31499
31857
  if (entry.isSymbolicLink()) {
31500
- rmSync9(at, { force: true, recursive: true });
31858
+ rmSync10(at, { force: true, recursive: true });
31501
31859
  } else if (entry.isDirectory()) {
31502
31860
  walk4(at);
31503
31861
  } else if (NEVER.includes(entry.name)) {
31504
- rmSync9(at, { force: true });
31862
+ rmSync10(at, { force: true });
31505
31863
  }
31506
31864
  }
31507
31865
  };
@@ -31597,7 +31955,7 @@ function terminal() {
31597
31955
  };
31598
31956
  }
31599
31957
  function runCommand(line) {
31600
- const parts = split2(line);
31958
+ const parts = split3(line);
31601
31959
  const args = parts[0] === "rook" ? parts.slice(1) : parts;
31602
31960
  if (args.length === 0) return Promise.resolve(1);
31603
31961
  return new Promise((done) => {
@@ -31610,7 +31968,7 @@ function runCommand(line) {
31610
31968
  child.on("close", (code) => done(code ?? 1));
31611
31969
  });
31612
31970
  }
31613
- function split2(line) {
31971
+ function split3(line) {
31614
31972
  const out = [];
31615
31973
  let current2 = "";
31616
31974
  let quote2 = null;
@@ -32091,7 +32449,7 @@ import {
32091
32449
  mkdtempSync as mkdtempSync2,
32092
32450
  readFileSync as readFileSync44,
32093
32451
  realpathSync as realpathSync10,
32094
- rmSync as rmSync10,
32452
+ rmSync as rmSync11,
32095
32453
  writeFileSync as writeFileSync32
32096
32454
  } from "fs";
32097
32455
  import { tmpdir as tmpdir2 } from "os";
@@ -32257,9 +32615,9 @@ function readPackageName(pkgRoot) {
32257
32615
  // src/updater.ts
32258
32616
  init_version_check();
32259
32617
  var CURL_INSTALL_COMMAND = "curl -fsSL https://raw.githubusercontent.com/LambdaTest/rook/main/install.sh | bash";
32260
- var SCOPE_REGISTRY_FLAG = `--${PUBLISHED_SCOPE}:registry`;
32618
+ var SCOPE_REGISTRY_ARG = `--${PUBLISHED_SCOPE}:registry=${NPM_REGISTRY_BASE}`;
32261
32619
  function npmInstallCommand(latest) {
32262
- return `npm install -g --registry ${NPM_REGISTRY_BASE} ${SCOPE_REGISTRY_FLAG} ${NPM_REGISTRY_BASE} ${PUBLISHED_PACKAGE_NAME}@${latest}`;
32620
+ return `npm install -g --registry ${NPM_REGISTRY_BASE} ${SCOPE_REGISTRY_ARG} ${PUBLISHED_PACKAGE_NAME}@${latest}`;
32263
32621
  }
32264
32622
  function candidateCommands(latest) {
32265
32623
  return [npmInstallCommand(latest), CURL_INSTALL_COMMAND, "brew upgrade rook"];
@@ -32322,7 +32680,7 @@ async function runUpdate(deps) {
32322
32680
  case "npm-print": {
32323
32681
  const cause = provenance.reason === "layout" ? "not a global install \u2014 the layout above it is an npx cache, or a project-local node_modules, not an npm prefix" : "a global npm install, but the running copy could not be confirmed to be the one <prefix>/bin/rook points at";
32324
32682
  deps.out(
32325
- `this is the npm package, but ${cause}. If a global install exists, run: npm install -g --registry ${NPM_REGISTRY_BASE} ${SCOPE_REGISTRY_FLAG} ${NPM_REGISTRY_BASE} ${shellQuote(`${PUBLISHED_PACKAGE_NAME}@${latest}`)}`
32683
+ `this is the npm package, but ${cause}. If a global install exists, run: npm install -g --registry ${NPM_REGISTRY_BASE} ${SCOPE_REGISTRY_ARG} ${shellQuote(`${PUBLISHED_PACKAGE_NAME}@${latest}`)}`
32326
32684
  );
32327
32685
  return 1;
32328
32686
  }
@@ -32361,8 +32719,7 @@ async function runUpdate(deps) {
32361
32719
  provenance.prefix,
32362
32720
  "--registry",
32363
32721
  NPM_REGISTRY_BASE,
32364
- SCOPE_REGISTRY_FLAG,
32365
- NPM_REGISTRY_BASE,
32722
+ SCOPE_REGISTRY_ARG,
32366
32723
  `${PUBLISHED_PACKAGE_NAME}@${latest}`
32367
32724
  ];
32368
32725
  return dispatchAndConfirm(
@@ -32413,7 +32770,7 @@ async function runUpdate(deps) {
32413
32770
  );
32414
32771
  return 1;
32415
32772
  } finally {
32416
- rmSync10(tmpDir, { recursive: true, force: true });
32773
+ rmSync11(tmpDir, { recursive: true, force: true });
32417
32774
  }
32418
32775
  }
32419
32776
  }
@@ -32606,7 +32963,13 @@ function announceUpdateIfAny() {
32606
32963
  writeErr(`Update available: ${VERSION} \u2192 ${updateInfo.latest}
32607
32964
  `);
32608
32965
  }
32609
- var program = new Command();
32966
+ var program = new Command2().configureOutput({
32967
+ writeOut,
32968
+ writeErr,
32969
+ outputError: (text, write2) => {
32970
+ if (currentClient() === "cli") write2(text);
32971
+ }
32972
+ });
32610
32973
  program.name("rook").description("Agent Assurance by Testmu AI").on("option:version", () => announceUpdateIfAny()).version(VERSION, "-v, --version");
32611
32974
  async function admit(command) {
32612
32975
  const auth3 = await resolveBootAuth();
@@ -34476,7 +34839,12 @@ program.command("docs").description("the public repo, printed and opened").optio
34476
34839
  `);
34477
34840
  await announceOpen(PUBLIC_DOCS_URL, flags.open);
34478
34841
  });
34479
- program.command("update").description("check for a newer rook, and how to get it").argument("[auto]", "`auto` re-enables the notice after 'never ask again'").option("--json", "machine-readable output").action(async (sub, flags) => {
34842
+ program.command("update").description("check for a newer rook, and how to get it").addArgument(
34843
+ new Argument(
34844
+ "[auto]",
34845
+ "`auto` re-enables the notice after 'never ask again'"
34846
+ ).choices(["auto"])
34847
+ ).option("--json", "machine-readable output").action(async (sub, flags) => {
34480
34848
  const mode = { json: Boolean(flags.json), verbose: false };
34481
34849
  const say2 = sayFor(mode);
34482
34850
  if (sub === "auto") {
@@ -34489,11 +34857,6 @@ program.command("update").description("check for a newer rook, and how to get it
34489
34857
  if (!cleared) process.exitCode = 1;
34490
34858
  return;
34491
34859
  }
34492
- if (sub) {
34493
- say2("error", "usage: rook update [auto]");
34494
- process.exitCode = 1;
34495
- return;
34496
- }
34497
34860
  if (needsProxyReexec(process.env)) {
34498
34861
  const { spawn: spawn8 } = await import("child_process");
34499
34862
  const child = spawn8(
@@ -34626,16 +34989,10 @@ mcp.command("approve <name>").description(
34626
34989
  ).option("--json", "machine-readable output").action((name, flags) => {
34627
34990
  runMcp(approve(process.cwd(), name, flags), Boolean(flags.json));
34628
34991
  });
34629
- program.command("help").description("list commands \u2014 rook help <command> for its flags").argument("[command]", "one command, in full").action((name) => {
34992
+ var helpArgument = new Argument("[command]", "one command, in full");
34993
+ program.command("help").description("list commands \u2014 rook help <command> for its flags").addArgument(helpArgument).action((name) => {
34630
34994
  const surface = currentClient();
34631
34995
  const lines = name ? renderCommandHelp(program, name, surface) : renderHelp(program, surface);
34632
- if (!lines) {
34633
- writeErr(`no such command: ${name}
34634
- `);
34635
- writeErr("rook help lists them\n");
34636
- process.exitCode = 1;
34637
- return;
34638
- }
34639
34996
  writeOut(`${lines.join("\n")}
34640
34997
  `);
34641
34998
  });
@@ -34729,16 +35086,8 @@ ${out.ran.length} command(s) run this conversation`
34729
35086
  }
34730
35087
  }
34731
35088
  );
34732
- program.command("export").description("bundle rook's logs for a bug report").argument("<what>", "`logs` \u2014 the only kind today").requiredOption("--out <path>", "a directory, or a filename ending .zip/.tgz").option("--session <id>", "also include this session's transcript").option("--all-sessions", "include every session recorded for this project").action(
35089
+ program.command("export").description("bundle rook's logs for a bug report").addArgument(new Argument("<what>", "what to export").choices(["logs"])).requiredOption("--out <path>", "a directory, or a filename ending .zip/.tgz").option("--session <id>", "also include this session's transcript").option("--all-sessions", "include every session recorded for this project").action(
34733
35090
  async (what, flags) => {
34734
- if (what !== "logs") {
34735
- writeErr(
34736
- `rook export ${what}: not something rook exports. Try 'logs'
34737
- `
34738
- );
34739
- process.exitCode = 1;
34740
- return;
34741
- }
34742
35091
  try {
34743
35092
  const out = exportLogs({
34744
35093
  root: process.cwd(),
@@ -34831,6 +35180,9 @@ program.command("doctor").description("check the environment without starting an
34831
35180
  `);
34832
35181
  }
34833
35182
  });
35183
+ helpArgument.choices(
35184
+ program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()])
35185
+ );
34834
35186
 
34835
35187
  // src/cli.ts
34836
35188
  init_dispatch();