@rulvar/cli 1.16.2 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as runCli, t as processIo } from "./io-lNmGU5Lv.js";
2
+ import { r as runCli, t as processIo } from "./io-Dp2LeOi7.js";
3
3
  import { inspect } from "node:util";
4
4
  //#region src/cli.ts
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ interface CliIo {
16
16
  declare function processIo(): CliIo;
17
17
  //#endregion
18
18
  //#region src/cli-main.d.ts
19
- declare const HELP = "rulvar: durable multi-agent workflows (https://docs.rulvar.com)\n\n rulvar run <file|name> [--args JSON] [--store PATH] [--budget-usd N]\n rulvar resume <runId> [--store PATH]\n rulvar runs ls [--store PATH]\n rulvar inspect <runId> [--store PATH]\n rulvar plan \"<goal>\" [--dry-run]\n rulvar kb <list | inbox | gate | sweep>\n\nEngine assembly: adapters, defaults, and the workflow registry come from\nrulvar.config.mjs in the working directory (default export\n{ engineOptions?, workflows? }) or from the workflow module's named\nexports. --store selects the JsonlFileStore directory (default .rulvar).\nplan asks the planner model (role plan) to write a workflow script,\nlints and self-repairs it, then runs it in the worker sandbox; --dry-run\nprints the accepted script without running. Requires @rulvar/planner\ninstalled. kb list shows the per-project claim store\n(./rulvar.models.json) with full provenance. kb sweep runs the\nfalsification matrix from the kbSweep section of rulvar.config.mjs\n(fixed pool UNIONED with every model carrying an active negative claim\nplus the re-measure queue; optional canary probes flip drifted claims\nstale first; requires @rulvar/evals installed). kb inbox aggregates\nkb_propose proposals from finished runs (14 day TTL); kb gate turns one\ninbox proposal into a committed claim behind a human attestation\n(--approver and --ruled-out are mandatory). inbox and gate require\n@rulvar/plan installed.";
19
+ declare const HELP: string;
20
20
  declare function runCli(argv: string[], options: {
21
21
  cwd: string;
22
22
  io: CliIo;
@@ -76,6 +76,30 @@ interface KbSweepCliConfig {
76
76
  model: `${string}:${string}`;
77
77
  effort?: string;
78
78
  }) => unknown;
79
+ /**
80
+ * Immutable per-run ceilings and the aggregate debit-only envelope
81
+ * (v1.16.2 review P1-2). A sweep multiplies paid runs: pool members
82
+ * times cases for targets, one judge run per judge-grader call, one
83
+ * canary run per probe per member, and the falsification union can
84
+ * grow the pool past the configured models. Per-run ceilings alone do
85
+ * not bound that product, so maxTotalUsd is the hard aggregate ceiling
86
+ * every target, judge, and canary run authorizes against BEFORE it
87
+ * starts. Required unless allowUnbounded is set: a sweep is never
88
+ * silently unbounded.
89
+ */
90
+ budgets?: {
91
+ /** Immutable ceiling B0 of every eval target run. */targetUsd: number; /** Immutable ceiling of every judge run. */
92
+ judgeUsd: number; /** Immutable ceiling of every canary probe run. */
93
+ canaryUsd: number; /** The debit-only envelope over the WHOLE sweep (targets, judges, canary). */
94
+ maxTotalUsd: number;
95
+ };
96
+ /**
97
+ * Explicitly waive the ceilings and run every target, judge, and
98
+ * canary run unbounded (the pre-v1.16.2 behavior). A sweep with
99
+ * neither budgets nor this flag set fails loudly: an unbounded paid
100
+ * matrix is never the silent default.
101
+ */
102
+ allowUnbounded?: boolean;
79
103
  }
80
104
  /** Loads `rulvar.config.mjs`/`.js` from cwd; absent config is fine. */
81
105
  declare function loadCliConfig(cwd: string): Promise<CliConfig>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-lNmGU5Lv.js";
1
+ import { a as resumeCommand, c as driveRun, d as renderEventLine, f as DEFAULT_STORE_DIR, g as looksLikeFile, h as loadWorkflowModule, i as inspectCommand, l as reportOutcome, m as loadCliConfig, n as HELP, o as runCommand, p as assembleEngine, r as runCli, s as runsLsCommand, t as processIo, u as attachProgress } from "./io-Dp2LeOi7.js";
2
2
  import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
3
3
  //#region src/server.ts
4
4
  /**
@@ -1,8 +1,8 @@
1
1
  import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, parseModelRef, priceUsdOf, proposalStatement, remeasureQueue, resolvePricing, runProfile } from "@rulvar/core";
2
- import { parseArgs } from "node:util";
3
2
  import { join, resolve } from "node:path";
4
3
  import { existsSync, statSync } from "node:fs";
5
4
  import { pathToFileURL } from "node:url";
5
+ import { parseArgs } from "node:util";
6
6
  import { createInterface } from "node:readline";
7
7
  //#region src/config.ts
8
8
  /**
@@ -302,6 +302,254 @@ function reportOutcome(outcome, io) {
302
302
  }
303
303
  }
304
304
  //#endregion
305
+ //#region src/grammar.ts
306
+ /**
307
+ * The canonical CLI grammar as data (v1.16.2 review P3-1): one
308
+ * structure generates the help block, every per-command usage line,
309
+ * and the docs contract test, so the three surfaces can never drift
310
+ * apart again. Parsing goes through parseCommand, which converts
311
+ * node:util parseArgs failures into the fail-loud ConfigError grammar
312
+ * the CLI documents (v1.16.2 review P2-1): nothing is ignored; unknown
313
+ * flags, duplicate value flags, mutually exclusive pairs, and wrong
314
+ * positional arity all fail naming the canonical usage.
315
+ */
316
+ const ARGS = {
317
+ name: "args",
318
+ placeholder: "JSON"
319
+ };
320
+ const STORE = {
321
+ name: "store",
322
+ placeholder: "PATH"
323
+ };
324
+ /**
325
+ * Every command of the canonical grammar (no aliases in v1). The help
326
+ * block, the per-command usage errors, and the docs grammar block in
327
+ * docs/guide/cli.md all derive from this table; the docs contract test
328
+ * compares them literally.
329
+ */
330
+ const GRAMMAR = {
331
+ run: {
332
+ command: "run",
333
+ positionals: ["<file|name>"],
334
+ flags: [
335
+ ARGS,
336
+ STORE,
337
+ {
338
+ name: "budget-usd",
339
+ placeholder: "N"
340
+ },
341
+ {
342
+ name: "profile",
343
+ placeholder: "NAME"
344
+ }
345
+ ]
346
+ },
347
+ resume: {
348
+ command: "resume",
349
+ positionals: ["<runId>"],
350
+ flags: [ARGS, STORE]
351
+ },
352
+ "runs ls": {
353
+ command: "runs ls",
354
+ positionals: [],
355
+ flags: [STORE],
356
+ note: "(no aliases in v1)"
357
+ },
358
+ inspect: {
359
+ command: "inspect",
360
+ positionals: ["<runId>"],
361
+ flags: [STORE]
362
+ },
363
+ plan: {
364
+ command: "plan",
365
+ positionals: ["\"<goal>\""],
366
+ flags: [
367
+ {
368
+ name: "planning-budget-usd",
369
+ placeholder: "N"
370
+ },
371
+ {
372
+ name: "budget-usd",
373
+ placeholder: "N"
374
+ },
375
+ { name: "allow-unbounded" },
376
+ { name: "dry-run" }
377
+ ]
378
+ },
379
+ "kb list": {
380
+ command: "kb list",
381
+ positionals: [],
382
+ flags: []
383
+ },
384
+ "kb inbox": {
385
+ command: "kb inbox",
386
+ positionals: [],
387
+ flags: [STORE]
388
+ },
389
+ "kb gate": {
390
+ command: "kb gate",
391
+ positionals: ["<runId>", "<entryRef>"],
392
+ flags: [
393
+ {
394
+ name: "approver",
395
+ placeholder: "NAME",
396
+ required: true
397
+ },
398
+ {
399
+ name: "ruled-out",
400
+ placeholder: "a,b,c",
401
+ required: true
402
+ },
403
+ {
404
+ name: "contrast-run",
405
+ placeholder: "runId#seq",
406
+ exclusiveGroup: "contrast"
407
+ },
408
+ {
409
+ name: "contrast-eval",
410
+ placeholder: "reportId:caseId[,caseId...]",
411
+ exclusiveGroup: "contrast"
412
+ },
413
+ {
414
+ name: "confidence",
415
+ placeholder: "high|medium|low"
416
+ },
417
+ STORE
418
+ ]
419
+ },
420
+ "kb sweep": {
421
+ command: "kb sweep",
422
+ positionals: [],
423
+ flags: [],
424
+ note: "(configuration lives in rulvar.config.mjs)"
425
+ }
426
+ };
427
+ /** The kb dispatch line: subcommands carry their own grammar entries. */
428
+ const KB_FAMILY_USAGE = "usage: rulvar kb <list | inbox | gate | sweep> (no aliases in v1)";
429
+ function renderFlags(flags) {
430
+ const tokens = [];
431
+ const renderedGroups = /* @__PURE__ */ new Set();
432
+ for (const flag of flags) {
433
+ if (flag.exclusiveGroup !== void 0) {
434
+ if (renderedGroups.has(flag.exclusiveGroup)) continue;
435
+ renderedGroups.add(flag.exclusiveGroup);
436
+ const members = flags.filter((candidate) => candidate.exclusiveGroup === flag.exclusiveGroup).map((member) => `--${member.name} ${member.placeholder ?? ""}`.trim());
437
+ tokens.push(`[${members.join(" | ")}]`);
438
+ continue;
439
+ }
440
+ const body = flag.placeholder === void 0 ? `--${flag.name}` : `--${flag.name} ${flag.placeholder}`;
441
+ tokens.push(flag.required === true ? body : `[${body}]`);
442
+ }
443
+ return tokens;
444
+ }
445
+ /** The full invocation shape, e.g. `rulvar run <file|name> [--args JSON] ...`. */
446
+ function invocationOf(grammar) {
447
+ const parts = [
448
+ `rulvar ${grammar.command}`,
449
+ ...grammar.positionals,
450
+ ...renderFlags(grammar.flags)
451
+ ];
452
+ if (grammar.note !== void 0) parts.push(grammar.note);
453
+ return parts.join(" ");
454
+ }
455
+ /** The canonical usage error line for a command. */
456
+ function usageOf(grammar) {
457
+ return `usage: ${invocationOf(grammar)}`;
458
+ }
459
+ /**
460
+ * The command lines of the help block: every top-level command plus the
461
+ * kb family line, aligned on the flag column. The docs grammar block is
462
+ * the same list verbatim (docs contract test).
463
+ */
464
+ function helpCommandLines() {
465
+ const top = [
466
+ GRAMMAR.run,
467
+ GRAMMAR.resume,
468
+ GRAMMAR["runs ls"],
469
+ GRAMMAR.inspect,
470
+ GRAMMAR.plan
471
+ ];
472
+ const heads = top.map((grammar) => [
473
+ "rulvar",
474
+ grammar.command,
475
+ ...grammar.positionals
476
+ ].join(" "));
477
+ const kbHead = "rulvar kb <list | inbox | gate | sweep>";
478
+ const width = Math.max(...heads.map((head) => head.length), 39);
479
+ const lines = top.map((grammar, index) => {
480
+ const tail = renderFlags(grammar.flags).join(" ");
481
+ return tail === "" ? heads[index] : `${heads[index].padEnd(width)} ${tail}`;
482
+ });
483
+ lines.push(kbHead);
484
+ return lines;
485
+ }
486
+ function isParseArgsError(error) {
487
+ if (!(error instanceof Error)) return false;
488
+ const code = error.code;
489
+ return typeof code === "string" && code.startsWith("ERR_PARSE_ARGS");
490
+ }
491
+ /**
492
+ * Parses argv against one grammar entry. Everything the grammar does
493
+ * not name is an error: unknown options (raised by parseArgs), value
494
+ * flags given twice, both members of an exclusive group, and any
495
+ * positional beyond the exact arity. Every rejection names the
496
+ * canonical usage and happens before configs, stores, or adapters load.
497
+ */
498
+ function parseCommand(grammar, argv) {
499
+ const options = {};
500
+ for (const flag of grammar.flags) options[flag.name] = {
501
+ type: flag.placeholder === void 0 ? "boolean" : "string",
502
+ multiple: true
503
+ };
504
+ let raw;
505
+ try {
506
+ raw = parseArgs({
507
+ args: argv,
508
+ allowPositionals: true,
509
+ options
510
+ });
511
+ } catch (error) {
512
+ if (isParseArgsError(error)) {
513
+ const summary = error.message.split(".")[0];
514
+ throw new ConfigError(`${summary}; ${usageOf(grammar)}`);
515
+ }
516
+ throw error;
517
+ }
518
+ if (raw.positionals.length < grammar.positionals.length) throw new ConfigError(usageOf(grammar));
519
+ if (raw.positionals.length > grammar.positionals.length) {
520
+ const extra = raw.positionals[grammar.positionals.length];
521
+ throw new ConfigError(`unexpected extra argument '${extra}'; ${usageOf(grammar)}`);
522
+ }
523
+ const values = {};
524
+ const groupMembers = /* @__PURE__ */ new Map();
525
+ for (const flag of grammar.flags) {
526
+ const entry = raw.values[flag.name];
527
+ if (entry === void 0) continue;
528
+ if (flag.placeholder !== void 0 && entry.length > 1) throw new ConfigError(`--${flag.name} may appear at most once; ${usageOf(grammar)}`);
529
+ values[flag.name] = flag.placeholder === void 0 ? true : entry[entry.length - 1];
530
+ if (flag.exclusiveGroup !== void 0) {
531
+ const members = groupMembers.get(flag.exclusiveGroup) ?? [];
532
+ members.push(`--${flag.name}`);
533
+ groupMembers.set(flag.exclusiveGroup, members);
534
+ }
535
+ }
536
+ for (const members of groupMembers.values()) if (members.length > 1) throw new ConfigError(`${members.join(" and ")} are mutually exclusive; ${usageOf(grammar)}`);
537
+ return {
538
+ positionals: raw.positionals,
539
+ values
540
+ };
541
+ }
542
+ /**
543
+ * Validates a `--...-usd` flag value: a finite dollar amount strictly
544
+ * above zero (0, NaN, Infinity, negatives, and non-numbers all fail),
545
+ * checked at parse time, before any provider work.
546
+ */
547
+ function parseBudgetValue(flagName, value) {
548
+ const budget = Number(value);
549
+ if (!Number.isFinite(budget) || budget <= 0) throw new ConfigError(`--${flagName} must be a positive number, got '${value}'`);
550
+ return budget;
551
+ }
552
+ //#endregion
305
553
  //#region src/commands.ts
306
554
  /**
307
555
  * The four M5 commands of the canonical CLI grammar (no aliases in v1):
@@ -340,61 +588,34 @@ async function loadCompanion(loading, specifier, command, missingMessage) {
340
588
  throw new Error(`${command}: ${specifier} is installed but failed to load; the cause below is a defect in the installed package or its dependencies, not a missing install`, { cause: error });
341
589
  }
342
590
  }
343
- function parseRunFlags(argv) {
344
- const { values, positionals } = parseArgs({
345
- args: argv,
346
- allowPositionals: true,
347
- options: {
348
- args: { type: "string" },
349
- store: { type: "string" },
350
- "budget-usd": { type: "string" },
351
- profile: { type: "string" }
352
- }
353
- });
354
- const parsed = { positionals };
355
- if (values.store !== void 0) parsed.store = values.store;
356
- if (values.profile !== void 0) parsed.profile = values.profile;
357
- if (values.args !== void 0) parsed.args = values.args;
358
- if (values["budget-usd"] !== void 0) {
359
- const budget = Number(values["budget-usd"]);
360
- if (!Number.isFinite(budget) || budget <= 0) throw new ConfigError(`--budget-usd must be a positive number, got '${values["budget-usd"]}'`);
361
- parsed.budgetUsd = budget;
362
- }
363
- return parsed;
364
- }
365
- function parseCommonFlags(argv) {
366
- const { values, positionals } = parseArgs({
367
- args: argv,
368
- allowPositionals: true,
369
- options: { store: { type: "string" } }
370
- });
371
- return {
372
- positionals,
373
- ...values.store === void 0 ? {} : { store: values.store }
374
- };
591
+ /** Parses --args JSON into workflow arguments; undefined when absent. */
592
+ function parseArgsJson(raw) {
593
+ if (raw === void 0) return;
594
+ try {
595
+ return JSON.parse(raw);
596
+ } catch {
597
+ throw new ConfigError(`--args is not valid JSON: ${raw}`);
598
+ }
375
599
  }
376
600
  async function runCommand(argv, context) {
377
- const flags = parseRunFlags(argv);
378
- const target = flags.positionals[0];
379
- if (target === void 0) throw new ConfigError("usage: rulvar run <file|name> [--args JSON] [--store PATH] [--budget-usd N]");
601
+ const parsed = parseCommand(GRAMMAR.run, argv);
602
+ const target = parsed.positionals[0];
603
+ const store = parsed.values.store;
604
+ const profile = parsed.values.profile;
605
+ const budgetUsd = parsed.values["budget-usd"] === void 0 ? void 0 : parseBudgetValue("budget-usd", parsed.values["budget-usd"]);
606
+ const args = parseArgsJson(parsed.values.args);
380
607
  const config = await loadCliConfig(context.cwd);
381
608
  const module = looksLikeFile(target) ? await loadWorkflowModule(target, context.cwd) : void 0;
382
609
  const assembled = assembleEngine({
383
610
  config,
384
611
  ...module === void 0 ? {} : { module },
385
- ...flags.store === void 0 ? {} : { storePath: flags.store },
386
- ...flags.profile === void 0 ? {} : { profile: flags.profile },
612
+ ...store === void 0 ? {} : { storePath: store },
613
+ ...profile === void 0 ? {} : { profile },
387
614
  cwd: context.cwd
388
615
  });
389
616
  const workflow = module?.workflow ?? assembled.workflows[target];
390
617
  if (workflow === void 0) throw new ConfigError(looksLikeFile(target) ? `${target} exports no workflow (default export or named 'workflow')` : `no workflow named '${target}' in the registry; register it in rulvar.config.mjs`);
391
- let args;
392
- if (flags.args !== void 0) try {
393
- args = JSON.parse(flags.args);
394
- } catch {
395
- throw new ConfigError(`--args is not valid JSON: ${flags.args}`);
396
- }
397
- const runOptions = { ...flags.budgetUsd === void 0 ? {} : { budgetUsd: flags.budgetUsd } };
618
+ const runOptions = { ...budgetUsd === void 0 ? {} : { budgetUsd } };
398
619
  const first = assembled.engine.run(workflow, args, runOptions);
399
620
  context.io.err(`runId: ${first.runId}`);
400
621
  return reportOutcome(await driveRun({
@@ -406,18 +627,13 @@ async function runCommand(argv, context) {
406
627
  }), context.io);
407
628
  }
408
629
  async function resumeCommand(argv, context) {
409
- const flags = parseRunFlags(argv);
410
- const runId = flags.positionals[0];
411
- if (runId === void 0) throw new ConfigError("usage: rulvar resume <runId> [--args JSON] [--store PATH]");
412
- let args;
413
- if (flags.args !== void 0) try {
414
- args = JSON.parse(flags.args);
415
- } catch {
416
- throw new ConfigError(`--args is not valid JSON: ${flags.args}`);
417
- }
630
+ const parsed = parseCommand(GRAMMAR.resume, argv);
631
+ const runId = parsed.positionals[0];
632
+ const args = parseArgsJson(parsed.values.args);
633
+ const store = parsed.values.store;
418
634
  const assembled = assembleEngine({
419
635
  config: await loadCliConfig(context.cwd),
420
- ...flags.store === void 0 ? {} : { storePath: flags.store },
636
+ ...store === void 0 ? {} : { storePath: store },
421
637
  cwd: context.cwd
422
638
  });
423
639
  const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
@@ -435,10 +651,10 @@ async function resumeCommand(argv, context) {
435
651
  }), context.io);
436
652
  }
437
653
  async function runsLsCommand(argv, context) {
438
- const flags = parseCommonFlags(argv);
654
+ const store = parseCommand(GRAMMAR["runs ls"], argv).values.store;
439
655
  const metas = await assembleEngine({
440
656
  config: await loadCliConfig(context.cwd),
441
- ...flags.store === void 0 ? {} : { storePath: flags.store },
657
+ ...store === void 0 ? {} : { storePath: store },
442
658
  cwd: context.cwd
443
659
  }).store.listRuns();
444
660
  if (metas.length === 0) {
@@ -453,12 +669,12 @@ async function runsLsCommand(argv, context) {
453
669
  return 0;
454
670
  }
455
671
  async function inspectCommand(argv, context) {
456
- const flags = parseCommonFlags(argv);
457
- const runId = flags.positionals[0];
458
- if (runId === void 0) throw new ConfigError("usage: rulvar inspect <runId> [--store PATH]");
672
+ const parsed = parseCommand(GRAMMAR.inspect, argv);
673
+ const runId = parsed.positionals[0];
674
+ const store = parsed.values.store;
459
675
  const assembled = assembleEngine({
460
676
  config: await loadCliConfig(context.cwd),
461
- ...flags.store === void 0 ? {} : { storePath: flags.store },
677
+ ...store === void 0 ? {} : { storePath: store },
462
678
  cwd: context.cwd
463
679
  });
464
680
  const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
@@ -491,34 +707,44 @@ async function inspectCommand(argv, context) {
491
707
  return 0;
492
708
  }
493
709
  /**
494
- * rulvar plan "<goal>" [--dry-run] (M6-T11): plans a
495
- * workflow script through @rulvar/planner (loaded dynamically: the CLI's
496
- * static dependency stays @rulvar/core only),
497
- * prints the accepted script and its advisories, and runs it in the
498
- * worker sandbox unless --dry-run.
710
+ * rulvar plan (M6-T11; grammar in grammar.ts): plans a workflow script
711
+ * through @rulvar/planner (loaded dynamically: the CLI's static
712
+ * dependency stays @rulvar/core only), prints the accepted script and
713
+ * its advisories, and runs it in the worker sandbox unless --dry-run.
714
+ *
715
+ * Both stages are paid runs with their OWN immutable ceilings (the
716
+ * v1.16.2 review P1-1): --planning-budget-usd caps the planning run
717
+ * (PlanOptions.run.budgetUsd, frozen at the planning journal's
718
+ * genesis), --budget-usd caps the execution run (RunOptions.budgetUsd,
719
+ * consistent with rulvar run). A machine-written workflow never runs
720
+ * unbounded silently: missing ceilings fail loudly unless
721
+ * --allow-unbounded waives them explicitly, and an execution ceiling
722
+ * beside --dry-run is a contradiction, not an ignorable leftover.
499
723
  */
500
724
  async function planCommand(argv, context) {
501
- const parsed = parseArgs({
502
- args: argv,
503
- allowPositionals: true,
504
- options: { "dry-run": { type: "boolean" } }
505
- });
725
+ const parsed = parseCommand(GRAMMAR.plan, argv);
506
726
  const goal = parsed.positionals[0];
507
- if (goal === void 0 || parsed.positionals.length > 1) throw new ConfigError("usage: rulvar plan \"<goal>\" [--dry-run]");
727
+ const dryRun = parsed.values["dry-run"] === true;
728
+ const allowUnbounded = parsed.values["allow-unbounded"] === true;
729
+ const planningBudgetUsd = parsed.values["planning-budget-usd"] === void 0 ? void 0 : parseBudgetValue("planning-budget-usd", parsed.values["planning-budget-usd"]);
730
+ const executionBudgetUsd = parsed.values["budget-usd"] === void 0 ? void 0 : parseBudgetValue("budget-usd", parsed.values["budget-usd"]);
731
+ if (dryRun && executionBudgetUsd !== void 0) throw new ConfigError("--dry-run never executes the planned workflow, so --budget-usd (the execution ceiling) has nothing to bound; drop one of the two");
732
+ if (!allowUnbounded && planningBudgetUsd === void 0) throw new ConfigError("rulvar plan runs the planner model as a paid run; set --planning-budget-usd N (its immutable ceiling) or waive it explicitly with --allow-unbounded");
733
+ if (!allowUnbounded && !dryRun && executionBudgetUsd === void 0) throw new ConfigError("executing the planned workflow is a second paid run; set --budget-usd N (its immutable ceiling) or waive it explicitly with --allow-unbounded");
508
734
  const plannerModule = await loadCompanion(import("@rulvar/planner"), "@rulvar/planner", "rulvar plan", "rulvar plan requires @rulvar/planner (the plan agent, compileScript, and the worker sandbox live there); install it next to the CLI");
509
735
  const assembled = assembleEngine({
510
736
  config: await loadCliConfig(context.cwd),
511
737
  cwd: context.cwd
512
738
  });
513
- const planned = await plannerModule.plan(assembled.engine, goal);
739
+ const planned = await plannerModule.plan(assembled.engine, goal, planningBudgetUsd === void 0 ? void 0 : { run: { budgetUsd: planningBudgetUsd } });
514
740
  context.io.err(`plan: accepted with ${String(planned.lint.length)} advisory diagnostic(s)`);
515
741
  for (const diagnostic of planned.lint) context.io.err(` ${diagnostic.ruleId}: ${diagnostic.message}`);
516
- if (parsed.values["dry-run"] === true) {
742
+ if (dryRun) {
517
743
  context.io.out(planned.source);
518
744
  return 0;
519
745
  }
520
746
  const workflow = planned.workflow;
521
- const first = assembled.engine.run(workflow, null);
747
+ const first = assembled.engine.run(workflow, null, executionBudgetUsd === void 0 ? {} : { budgetUsd: executionBudgetUsd });
522
748
  context.io.err(`runId: ${first.runId}`);
523
749
  return reportOutcome(await driveRun({
524
750
  engine: assembled.engine,
@@ -540,7 +766,8 @@ async function kbCommand(argv, context) {
540
766
  if (sub === "inbox") return await kbInboxCommand(rest, context);
541
767
  if (sub === "gate") return await kbGateCommand(rest, context);
542
768
  if (sub === "sweep") return await kbSweepCommand(rest, context);
543
- if (sub !== "list" || rest.length > 0) throw new ConfigError("usage: rulvar kb <list | inbox | gate | sweep> (no aliases in v1)");
769
+ if (sub !== "list") throw new ConfigError(KB_FAMILY_USAGE);
770
+ parseCommand(GRAMMAR["kb list"], rest);
544
771
  const snapshot = await new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") }).current();
545
772
  context.io.out(`knowledge store: rulvar.models.json (version ${String(snapshot.version)}, ${String(snapshot.claims.length)} claim${snapshot.claims.length === 1 ? "" : "s"})`);
546
773
  renderKbList(snapshot, context);
@@ -574,8 +801,7 @@ function renderKbList(snapshot, context) {
574
801
  * concrete model names render here VERBATIM, exactly like kb list.
575
802
  */
576
803
  async function kbInboxCommand(argv, context) {
577
- const flags = parseCommonFlags(argv);
578
- if (flags.positionals.length > 0) throw new ConfigError("usage: rulvar kb inbox [--store PATH]");
804
+ const flags = { store: parseCommand(GRAMMAR["kb inbox"], argv).values.store };
579
805
  const plan = await loadCompanion(import("@rulvar/plan"), "@rulvar/plan", "rulvar kb inbox", "rulvar kb inbox requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
580
806
  const assembled = assembleEngine({
581
807
  config: await loadCliConfig(context.cwd),
@@ -662,22 +888,11 @@ const RULED_OUT_VOCABULARY = [
662
888
  * per-project file store, whose git review is the authenticating gate.
663
889
  */
664
890
  async function kbGateCommand(argv, context) {
665
- const { values, positionals } = parseArgs({
666
- args: argv,
667
- allowPositionals: true,
668
- options: {
669
- store: { type: "string" },
670
- approver: { type: "string" },
671
- "ruled-out": { type: "string" },
672
- "contrast-run": { type: "string" },
673
- "contrast-eval": { type: "string" },
674
- confidence: { type: "string" }
675
- }
676
- });
677
- const usage = "usage: rulvar kb gate <runId> <entryRef> --approver NAME --ruled-out a,b,c [--contrast-run runId#seq | --contrast-eval reportId:caseId[,caseId...]] [--confidence high|medium|low] [--store PATH]";
678
- const runId = positionals[0];
679
- const entryRefRaw = positionals[1];
680
- if (runId === void 0 || entryRefRaw === void 0 || positionals.length > 2) throw new ConfigError(usage);
891
+ const parsed = parseCommand(GRAMMAR["kb gate"], argv);
892
+ const values = parsed.values;
893
+ const usage = usageOf(GRAMMAR["kb gate"]);
894
+ const runId = parsed.positionals[0];
895
+ const entryRefRaw = parsed.positionals[1];
681
896
  const entryRef = Number(entryRefRaw);
682
897
  if (!Number.isInteger(entryRef) || entryRef < 1) throw new ConfigError(`entryRef must be a positive integer entry seq, got '${entryRefRaw}'`);
683
898
  const approver = values.approver;
@@ -686,7 +901,6 @@ async function kbGateCommand(argv, context) {
686
901
  if (ruledOutRaw === void 0 || ruledOutRaw === "") throw new ConfigError(`--ruled-out is required: the attribution attestation lists the alternative causes you ruled out (${RULED_OUT_VOCABULARY.join(", ")}). ${usage}`);
687
902
  const ruledOut = ruledOutRaw.split(",").map((entry) => entry.trim());
688
903
  for (const entry of ruledOut) if (!RULED_OUT_VOCABULARY.includes(entry)) throw new ConfigError(`--ruled-out '${entry}' is not in the attestation vocabulary (${RULED_OUT_VOCABULARY.join(", ")})`);
689
- if (values["contrast-run"] !== void 0 && values["contrast-eval"] !== void 0) throw new ConfigError("--contrast-run and --contrast-eval are mutually exclusive");
690
904
  let contrastEvidence;
691
905
  if (values["contrast-run"] !== void 0) {
692
906
  const [contrastRun, seqRaw, ...tail] = values["contrast-run"].split("#");
@@ -804,10 +1018,21 @@ async function kbGateCommand(argv, context) {
804
1018
  * sweep re-measures.
805
1019
  */
806
1020
  async function kbSweepCommand(argv, context) {
807
- if (argv.length > 0) throw new ConfigError("usage: rulvar kb sweep (configuration lives in rulvar.config.mjs)");
1021
+ parseCommand(GRAMMAR["kb sweep"], argv);
808
1022
  const config = await loadCliConfig(context.cwd);
809
1023
  const sweep = config.kbSweep;
810
1024
  if (sweep === void 0) throw new ConfigError("rulvar kb sweep requires a kbSweep section in rulvar.config.mjs ({ committerId, models, cases })");
1025
+ const budgets = sweep.budgets;
1026
+ if (budgets === void 0 && sweep.allowUnbounded !== true) throw new ConfigError("rulvar kb sweep runs paid target, judge, and canary runs; set kbSweep.budgets ({ targetUsd, judgeUsd, canaryUsd, maxTotalUsd }) so every run carries an immutable ceiling and the whole sweep stays under maxTotalUsd, or waive the ceilings explicitly with kbSweep.allowUnbounded: true");
1027
+ if (budgets !== void 0) for (const field of [
1028
+ "targetUsd",
1029
+ "judgeUsd",
1030
+ "canaryUsd",
1031
+ "maxTotalUsd"
1032
+ ]) {
1033
+ const value = budgets[field];
1034
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new ConfigError(`kbSweep.budgets.${field} must be a positive finite number, got ${String(value)}`);
1035
+ }
811
1036
  const evals = await loadCompanion(import("@rulvar/evals"), "@rulvar/evals", "rulvar kb sweep", "rulvar kb sweep requires @rulvar/evals (matrix sweeps, the eval-committer identity, and the canary live there); install it next to the CLI");
812
1037
  const store = new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") });
813
1038
  const snapshot = await store.current();
@@ -849,15 +1074,39 @@ async function kbSweepCommand(argv, context) {
849
1074
  }
850
1075
  }
851
1076
  }));
1077
+ const usd = (amount) => `$${String(Math.round(amount * 1e6) / 1e6)}`;
1078
+ let envelope;
1079
+ if (budgets !== void 0) {
1080
+ envelope = new evals.SpendEnvelope(budgets.maxTotalUsd);
1081
+ const canaryRuns = (sweep.canary?.prompts.length ?? 0) * pool.size;
1082
+ const targetRuns = sweep.cases.length * pool.size;
1083
+ context.io.out(`sweep budget: ${usd(budgets.maxTotalUsd)} maxTotalUsd hard ceiling; authorizes up to ${usd(canaryRuns * budgets.canaryUsd + targetRuns * budgets.targetUsd)} for ${String(canaryRuns)} canary + ${String(targetRuns)} target run(s) before judges (each judge run up to ${usd(budgets.judgeUsd)} draws from the same envelope at grade time)`);
1084
+ } else context.io.err("kb sweep: running UNBOUNDED (kbSweep.allowUnbounded); no target, judge, or canary run carries a ceiling");
852
1085
  for (const { member, origin } of pool.values()) {
853
1086
  const effort = member.effort === void 0 ? "" : ` effort=${member.effort}`;
854
1087
  context.io.out(`pool: ${member.model}${effort} [${origin}]`);
855
1088
  }
856
1089
  if (sweep.canary !== void 0) for (const { member } of pool.values()) {
857
1090
  const engine = await engineFor(member);
858
- const fingerprint = await evals.canaryFingerprint(engine, sweep.canary);
859
- const drift = await evals.flipStaleOnCanaryDrift(store, member.model, fingerprint);
860
- context.io.out(`canary ${member.model}: ${fingerprint.slice(0, 12)}...` + (drift.flipped.length === 0 ? " no drift" : ` DRIFT, ${String(drift.flipped.length)} claim(s) flipped stale`));
1091
+ let canary;
1092
+ try {
1093
+ canary = await evals.runCanary(engine, sweep.canary, { ...budgets === void 0 ? {} : {
1094
+ budgetUsd: budgets.canaryUsd,
1095
+ envelope
1096
+ } });
1097
+ } catch (error) {
1098
+ if (error instanceof Error && error.name === "SweepBudgetError") {
1099
+ context.io.out(`canary ${member.model}: envelope exhausted, skipped`);
1100
+ continue;
1101
+ }
1102
+ throw error;
1103
+ }
1104
+ if (!canary.allOk) {
1105
+ context.io.out(`canary ${member.model}: ${canary.fingerprint.slice(0, 12)}... incomplete (a probe did not settle ok); NOT flipping claims`);
1106
+ continue;
1107
+ }
1108
+ const drift = await evals.flipStaleOnCanaryDrift(store, member.model, canary.fingerprint);
1109
+ context.io.out(`canary ${member.model}: ${canary.fingerprint.slice(0, 12)}...` + (drift.flipped.length === 0 ? " no drift" : ` DRIFT, ${String(drift.flipped.length)} claim(s) flipped stale`));
861
1110
  }
862
1111
  const report = await evals.runSweepMatrix({
863
1112
  models: [...pool.values()].map((entry) => entry.member),
@@ -868,11 +1117,26 @@ async function kbSweepCommand(argv, context) {
868
1117
  observedAt,
869
1118
  engineFor,
870
1119
  store,
871
- ...sweep.thresholds === void 0 ? {} : { thresholds: sweep.thresholds }
1120
+ ...sweep.thresholds === void 0 ? {} : { thresholds: sweep.thresholds },
1121
+ ...budgets === void 0 ? {} : {
1122
+ suite: {
1123
+ budgetUsd: budgets.targetUsd,
1124
+ judgeBudgetUsd: budgets.judgeUsd
1125
+ },
1126
+ envelope
1127
+ }
872
1128
  });
873
- for (const cell of report.cells) context.io.out(`cell ${cell.model} :: ${cell.taskClass}: passRate ${cell.passRate.toFixed(2)} over ${String(cell.n)} case${cell.n === 1 ? "" : "s"}`);
1129
+ for (const cell of report.cells) {
1130
+ if (cell.envelopeExhausted === true) {
1131
+ context.io.out(`cell ${cell.model} :: ${cell.taskClass}: envelope exhausted, not measured (no claim)`);
1132
+ continue;
1133
+ }
1134
+ const exhausted = cell.exhaustedRuns === void 0 ? "" : ` (${String(cell.exhaustedRuns)} run(s) hit their own ceiling; no claim)`;
1135
+ context.io.out(`cell ${cell.model} :: ${cell.taskClass}: passRate ${cell.passRate.toFixed(2)} over ${String(cell.n)} case${cell.n === 1 ? "" : "s"}${exhausted}`);
1136
+ }
874
1137
  for (const claim of report.claims) context.io.out(`claim ${claim.id}: ${claim.taskClass} ${claim.polarity}`);
875
1138
  context.io.out(report.committedVersion === void 0 ? "no claims crossed a threshold; nothing committed" : `committed ${String(report.claims.length)} claim(s) as store version ${String(report.committedVersion)} (report ${report.reportId})`);
1139
+ if (envelope !== void 0) context.io.out(`sweep budget: authorized ${usd(envelope.authorizedUsd)} of ${usd(envelope.maxTotalUsd)}`);
876
1140
  return 0;
877
1141
  }
878
1142
  //#endregion
@@ -883,12 +1147,7 @@ async function kbSweepCommand(argv, context) {
883
1147
  */
884
1148
  const HELP = `rulvar: durable multi-agent workflows (https://docs.rulvar.com)
885
1149
 
886
- rulvar run <file|name> [--args JSON] [--store PATH] [--budget-usd N]
887
- rulvar resume <runId> [--store PATH]
888
- rulvar runs ls [--store PATH]
889
- rulvar inspect <runId> [--store PATH]
890
- rulvar plan "<goal>" [--dry-run]
891
- rulvar kb <list | inbox | gate | sweep>
1150
+ ${helpCommandLines().map((line) => ` ${line}`).join("\n")}
892
1151
 
893
1152
  Engine assembly: adapters, defaults, and the workflow registry come from
894
1153
  rulvar.config.mjs in the working directory (default export
@@ -896,13 +1155,18 @@ rulvar.config.mjs in the working directory (default export
896
1155
  exports. --store selects the JsonlFileStore directory (default .rulvar).
897
1156
  plan asks the planner model (role plan) to write a workflow script,
898
1157
  lints and self-repairs it, then runs it in the worker sandbox; --dry-run
899
- prints the accepted script without running. Requires @rulvar/planner
900
- installed. kb list shows the per-project claim store
1158
+ prints the accepted script without running. Both stages are paid runs
1159
+ with their own immutable ceilings: --planning-budget-usd caps the
1160
+ planning run, --budget-usd caps the execution run, and a missing
1161
+ ceiling fails loudly unless --allow-unbounded waives it explicitly.
1162
+ Requires @rulvar/planner installed. kb list shows the per-project claim store
901
1163
  (./rulvar.models.json) with full provenance. kb sweep runs the
902
1164
  falsification matrix from the kbSweep section of rulvar.config.mjs
903
1165
  (fixed pool UNIONED with every model carrying an active negative claim
904
1166
  plus the re-measure queue; optional canary probes flip drifted claims
905
- stale first; requires @rulvar/evals installed). kb inbox aggregates
1167
+ stale first; kbSweep.budgets sets per-run ceilings plus the maxTotalUsd
1168
+ envelope, required unless allowUnbounded waives it; requires
1169
+ @rulvar/evals installed). kb inbox aggregates
906
1170
  kb_propose proposals from finished runs (14 day TTL); kb gate turns one
907
1171
  inbox proposal into a committed claim behind a human attestation
908
1172
  (--approver and --ruled-out are mandatory). inbox and gate require
@@ -919,7 +1183,7 @@ async function runCli(argv, options) {
919
1183
  case "resume": return await resumeCommand(rest, context);
920
1184
  case "runs": {
921
1185
  const [sub, ...subRest] = rest;
922
- if (sub !== "ls") throw new ConfigError("usage: rulvar runs ls [--store PATH] (no aliases in v1)");
1186
+ if (sub !== "ls") throw new ConfigError(usageOf(GRAMMAR["runs ls"]));
923
1187
  return await runsLsCommand(subRest, context);
924
1188
  }
925
1189
  case "inspect": return await inspectCommand(rest, context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/cli",
3
- "version": "1.16.2",
3
+ "version": "1.17.0",
4
4
  "description": "Rulvar shell: run/resume/runs/inspect/plan/kb commands, TUI progress, createServer, createWorker, OTel exporter.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,17 +22,17 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.16.2"
25
+ "@rulvar/core": "1.17.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",
29
29
  "tsdown": "^0.22.3",
30
30
  "typescript": "~6.0.3",
31
- "@rulvar/testing": "1.16.2",
32
- "@rulvar/planner": "1.16.2",
33
- "@rulvar/store-sqlite": "1.16.2",
34
- "@rulvar/plan": "1.16.2",
35
- "@rulvar/evals": "1.16.2"
31
+ "@rulvar/testing": "1.17.0",
32
+ "@rulvar/store-sqlite": "1.17.0",
33
+ "@rulvar/plan": "1.17.0",
34
+ "@rulvar/evals": "1.17.0",
35
+ "@rulvar/planner": "1.17.0"
36
36
  },
37
37
  "bin": {
38
38
  "rulvar": "./dist/cli.js"