@rulvar/cli 1.16.1 → 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 +3 -2
- package/dist/index.d.ts +25 -1
- package/dist/index.js +1 -1
- package/dist/{io-Bex4Du1t.js → io-Dp2LeOi7.js} +404 -134
- package/package.json +7 -7
- package/dist/dist-A5zLp_kr.js +0 -6993
- package/dist/dist-CdhvWszV.js +0 -626
- package/dist/dist-D9g28MUP.js +0 -95398
- package/dist/src-pgjrRW_Q.js +0 -1174
|
@@ -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):
|
|
@@ -314,61 +562,60 @@ function reportOutcome(outcome, io) {
|
|
|
314
562
|
* `plan` and `kb` land with M6+/M10. Every command builds strictly from
|
|
315
563
|
* the public @rulvar/core API.
|
|
316
564
|
*/
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
565
|
+
/**
|
|
566
|
+
* True exactly when a companion dynamic import failed because THAT
|
|
567
|
+
* package is not installed (the v1.16.1 review P1): the Node code must
|
|
568
|
+
* be ERR_MODULE_NOT_FOUND and the quoted missing specifier must be the
|
|
569
|
+
* requested companion itself. A transitive miss inside a found
|
|
570
|
+
* companion (same code, different quoted specifier) or any throw during
|
|
571
|
+
* module evaluation is that package's own defect, never install advice.
|
|
572
|
+
*/
|
|
573
|
+
function isCompanionMissing(error, specifier) {
|
|
574
|
+
return error instanceof Error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes(`'${specifier}'`);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Awaits a command-local companion import. Call sites keep the literal
|
|
578
|
+
* `import('@rulvar/...')` so the specifier stays analyzable and the
|
|
579
|
+
* tsdown external rule preserves it in dist. Missing package: the
|
|
580
|
+
* friendly ConfigError install hint. Anything else: the original error
|
|
581
|
+
* survives as `cause` under a command-prefixed message.
|
|
582
|
+
*/
|
|
583
|
+
async function loadCompanion(loading, specifier, command, missingMessage) {
|
|
584
|
+
try {
|
|
585
|
+
return await loading;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (isCompanionMissing(error, specifier)) throw new ConfigError(missingMessage);
|
|
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 });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
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
|
+
}
|
|
349
599
|
}
|
|
350
600
|
async function runCommand(argv, context) {
|
|
351
|
-
const
|
|
352
|
-
const target =
|
|
353
|
-
|
|
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);
|
|
354
607
|
const config = await loadCliConfig(context.cwd);
|
|
355
608
|
const module = looksLikeFile(target) ? await loadWorkflowModule(target, context.cwd) : void 0;
|
|
356
609
|
const assembled = assembleEngine({
|
|
357
610
|
config,
|
|
358
611
|
...module === void 0 ? {} : { module },
|
|
359
|
-
...
|
|
360
|
-
...
|
|
612
|
+
...store === void 0 ? {} : { storePath: store },
|
|
613
|
+
...profile === void 0 ? {} : { profile },
|
|
361
614
|
cwd: context.cwd
|
|
362
615
|
});
|
|
363
616
|
const workflow = module?.workflow ?? assembled.workflows[target];
|
|
364
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`);
|
|
365
|
-
|
|
366
|
-
if (flags.args !== void 0) try {
|
|
367
|
-
args = JSON.parse(flags.args);
|
|
368
|
-
} catch {
|
|
369
|
-
throw new ConfigError(`--args is not valid JSON: ${flags.args}`);
|
|
370
|
-
}
|
|
371
|
-
const runOptions = { ...flags.budgetUsd === void 0 ? {} : { budgetUsd: flags.budgetUsd } };
|
|
618
|
+
const runOptions = { ...budgetUsd === void 0 ? {} : { budgetUsd } };
|
|
372
619
|
const first = assembled.engine.run(workflow, args, runOptions);
|
|
373
620
|
context.io.err(`runId: ${first.runId}`);
|
|
374
621
|
return reportOutcome(await driveRun({
|
|
@@ -380,18 +627,13 @@ async function runCommand(argv, context) {
|
|
|
380
627
|
}), context.io);
|
|
381
628
|
}
|
|
382
629
|
async function resumeCommand(argv, context) {
|
|
383
|
-
const
|
|
384
|
-
const runId =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (flags.args !== void 0) try {
|
|
388
|
-
args = JSON.parse(flags.args);
|
|
389
|
-
} catch {
|
|
390
|
-
throw new ConfigError(`--args is not valid JSON: ${flags.args}`);
|
|
391
|
-
}
|
|
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;
|
|
392
634
|
const assembled = assembleEngine({
|
|
393
635
|
config: await loadCliConfig(context.cwd),
|
|
394
|
-
...
|
|
636
|
+
...store === void 0 ? {} : { storePath: store },
|
|
395
637
|
cwd: context.cwd
|
|
396
638
|
});
|
|
397
639
|
const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
|
|
@@ -409,10 +651,10 @@ async function resumeCommand(argv, context) {
|
|
|
409
651
|
}), context.io);
|
|
410
652
|
}
|
|
411
653
|
async function runsLsCommand(argv, context) {
|
|
412
|
-
const
|
|
654
|
+
const store = parseCommand(GRAMMAR["runs ls"], argv).values.store;
|
|
413
655
|
const metas = await assembleEngine({
|
|
414
656
|
config: await loadCliConfig(context.cwd),
|
|
415
|
-
...
|
|
657
|
+
...store === void 0 ? {} : { storePath: store },
|
|
416
658
|
cwd: context.cwd
|
|
417
659
|
}).store.listRuns();
|
|
418
660
|
if (metas.length === 0) {
|
|
@@ -427,12 +669,12 @@ async function runsLsCommand(argv, context) {
|
|
|
427
669
|
return 0;
|
|
428
670
|
}
|
|
429
671
|
async function inspectCommand(argv, context) {
|
|
430
|
-
const
|
|
431
|
-
const runId =
|
|
432
|
-
|
|
672
|
+
const parsed = parseCommand(GRAMMAR.inspect, argv);
|
|
673
|
+
const runId = parsed.positionals[0];
|
|
674
|
+
const store = parsed.values.store;
|
|
433
675
|
const assembled = assembleEngine({
|
|
434
676
|
config: await loadCliConfig(context.cwd),
|
|
435
|
-
...
|
|
677
|
+
...store === void 0 ? {} : { storePath: store },
|
|
436
678
|
cwd: context.cwd
|
|
437
679
|
});
|
|
438
680
|
const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
|
|
@@ -465,39 +707,44 @@ async function inspectCommand(argv, context) {
|
|
|
465
707
|
return 0;
|
|
466
708
|
}
|
|
467
709
|
/**
|
|
468
|
-
* rulvar plan
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
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.
|
|
473
723
|
*/
|
|
474
724
|
async function planCommand(argv, context) {
|
|
475
|
-
const parsed =
|
|
476
|
-
args: argv,
|
|
477
|
-
allowPositionals: true,
|
|
478
|
-
options: { "dry-run": { type: "boolean" } }
|
|
479
|
-
});
|
|
725
|
+
const parsed = parseCommand(GRAMMAR.plan, argv);
|
|
480
726
|
const goal = parsed.positionals[0];
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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");
|
|
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");
|
|
488
735
|
const assembled = assembleEngine({
|
|
489
736
|
config: await loadCliConfig(context.cwd),
|
|
490
737
|
cwd: context.cwd
|
|
491
738
|
});
|
|
492
|
-
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 } });
|
|
493
740
|
context.io.err(`plan: accepted with ${String(planned.lint.length)} advisory diagnostic(s)`);
|
|
494
741
|
for (const diagnostic of planned.lint) context.io.err(` ${diagnostic.ruleId}: ${diagnostic.message}`);
|
|
495
|
-
if (
|
|
742
|
+
if (dryRun) {
|
|
496
743
|
context.io.out(planned.source);
|
|
497
744
|
return 0;
|
|
498
745
|
}
|
|
499
746
|
const workflow = planned.workflow;
|
|
500
|
-
const first = assembled.engine.run(workflow, null);
|
|
747
|
+
const first = assembled.engine.run(workflow, null, executionBudgetUsd === void 0 ? {} : { budgetUsd: executionBudgetUsd });
|
|
501
748
|
context.io.err(`runId: ${first.runId}`);
|
|
502
749
|
return reportOutcome(await driveRun({
|
|
503
750
|
engine: assembled.engine,
|
|
@@ -519,7 +766,8 @@ async function kbCommand(argv, context) {
|
|
|
519
766
|
if (sub === "inbox") return await kbInboxCommand(rest, context);
|
|
520
767
|
if (sub === "gate") return await kbGateCommand(rest, context);
|
|
521
768
|
if (sub === "sweep") return await kbSweepCommand(rest, context);
|
|
522
|
-
if (sub !== "list"
|
|
769
|
+
if (sub !== "list") throw new ConfigError(KB_FAMILY_USAGE);
|
|
770
|
+
parseCommand(GRAMMAR["kb list"], rest);
|
|
523
771
|
const snapshot = await new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") }).current();
|
|
524
772
|
context.io.out(`knowledge store: rulvar.models.json (version ${String(snapshot.version)}, ${String(snapshot.claims.length)} claim${snapshot.claims.length === 1 ? "" : "s"})`);
|
|
525
773
|
renderKbList(snapshot, context);
|
|
@@ -553,14 +801,8 @@ function renderKbList(snapshot, context) {
|
|
|
553
801
|
* concrete model names render here VERBATIM, exactly like kb list.
|
|
554
802
|
*/
|
|
555
803
|
async function kbInboxCommand(argv, context) {
|
|
556
|
-
const flags =
|
|
557
|
-
|
|
558
|
-
let plan;
|
|
559
|
-
try {
|
|
560
|
-
plan = await import("./dist-A5zLp_kr.js");
|
|
561
|
-
} catch {
|
|
562
|
-
throw new ConfigError("rulvar kb inbox requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
|
|
563
|
-
}
|
|
804
|
+
const flags = { store: parseCommand(GRAMMAR["kb inbox"], argv).values.store };
|
|
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)");
|
|
564
806
|
const assembled = assembleEngine({
|
|
565
807
|
config: await loadCliConfig(context.cwd),
|
|
566
808
|
...flags.store === void 0 ? {} : { storePath: flags.store },
|
|
@@ -646,22 +888,11 @@ const RULED_OUT_VOCABULARY = [
|
|
|
646
888
|
* per-project file store, whose git review is the authenticating gate.
|
|
647
889
|
*/
|
|
648
890
|
async function kbGateCommand(argv, context) {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
approver: { type: "string" },
|
|
655
|
-
"ruled-out": { type: "string" },
|
|
656
|
-
"contrast-run": { type: "string" },
|
|
657
|
-
"contrast-eval": { type: "string" },
|
|
658
|
-
confidence: { type: "string" }
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
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]";
|
|
662
|
-
const runId = positionals[0];
|
|
663
|
-
const entryRefRaw = positionals[1];
|
|
664
|
-
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];
|
|
665
896
|
const entryRef = Number(entryRefRaw);
|
|
666
897
|
if (!Number.isInteger(entryRef) || entryRef < 1) throw new ConfigError(`entryRef must be a positive integer entry seq, got '${entryRefRaw}'`);
|
|
667
898
|
const approver = values.approver;
|
|
@@ -670,7 +901,6 @@ async function kbGateCommand(argv, context) {
|
|
|
670
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}`);
|
|
671
902
|
const ruledOut = ruledOutRaw.split(",").map((entry) => entry.trim());
|
|
672
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(", ")})`);
|
|
673
|
-
if (values["contrast-run"] !== void 0 && values["contrast-eval"] !== void 0) throw new ConfigError("--contrast-run and --contrast-eval are mutually exclusive");
|
|
674
904
|
let contrastEvidence;
|
|
675
905
|
if (values["contrast-run"] !== void 0) {
|
|
676
906
|
const [contrastRun, seqRaw, ...tail] = values["contrast-run"].split("#");
|
|
@@ -698,12 +928,7 @@ async function kbGateCommand(argv, context) {
|
|
|
698
928
|
"medium",
|
|
699
929
|
"low"
|
|
700
930
|
].includes(confidence)) throw new ConfigError(`--confidence must be high, medium or low, got '${String(values.confidence)}'`);
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
plan = await import("./dist-A5zLp_kr.js");
|
|
704
|
-
} catch {
|
|
705
|
-
throw new ConfigError("rulvar kb gate requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
|
|
706
|
-
}
|
|
931
|
+
const plan = await loadCompanion(import("@rulvar/plan"), "@rulvar/plan", "rulvar kb gate", "rulvar kb gate requires @rulvar/plan (the RunLedger fold behind the LedgerExport seam)");
|
|
707
932
|
const assembled = assembleEngine({
|
|
708
933
|
config: await loadCliConfig(context.cwd),
|
|
709
934
|
...values.store === void 0 ? {} : { storePath: values.store },
|
|
@@ -793,16 +1018,22 @@ async function kbGateCommand(argv, context) {
|
|
|
793
1018
|
* sweep re-measures.
|
|
794
1019
|
*/
|
|
795
1020
|
async function kbSweepCommand(argv, context) {
|
|
796
|
-
|
|
1021
|
+
parseCommand(GRAMMAR["kb sweep"], argv);
|
|
797
1022
|
const config = await loadCliConfig(context.cwd);
|
|
798
1023
|
const sweep = config.kbSweep;
|
|
799
1024
|
if (sweep === void 0) throw new ConfigError("rulvar kb sweep requires a kbSweep section in rulvar.config.mjs ({ committerId, models, cases })");
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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)}`);
|
|
805
1035
|
}
|
|
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");
|
|
806
1037
|
const store = new FileModelKnowledgeStore({ path: join(context.cwd, "rulvar.models.json") });
|
|
807
1038
|
const snapshot = await store.current();
|
|
808
1039
|
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -843,15 +1074,39 @@ async function kbSweepCommand(argv, context) {
|
|
|
843
1074
|
}
|
|
844
1075
|
}
|
|
845
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");
|
|
846
1085
|
for (const { member, origin } of pool.values()) {
|
|
847
1086
|
const effort = member.effort === void 0 ? "" : ` effort=${member.effort}`;
|
|
848
1087
|
context.io.out(`pool: ${member.model}${effort} [${origin}]`);
|
|
849
1088
|
}
|
|
850
1089
|
if (sweep.canary !== void 0) for (const { member } of pool.values()) {
|
|
851
1090
|
const engine = await engineFor(member);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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`));
|
|
855
1110
|
}
|
|
856
1111
|
const report = await evals.runSweepMatrix({
|
|
857
1112
|
models: [...pool.values()].map((entry) => entry.member),
|
|
@@ -862,11 +1117,26 @@ async function kbSweepCommand(argv, context) {
|
|
|
862
1117
|
observedAt,
|
|
863
1118
|
engineFor,
|
|
864
1119
|
store,
|
|
865
|
-
...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
|
+
}
|
|
866
1128
|
});
|
|
867
|
-
for (const cell of report.cells)
|
|
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
|
+
}
|
|
868
1137
|
for (const claim of report.claims) context.io.out(`claim ${claim.id}: ${claim.taskClass} ${claim.polarity}`);
|
|
869
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)}`);
|
|
870
1140
|
return 0;
|
|
871
1141
|
}
|
|
872
1142
|
//#endregion
|
|
@@ -877,12 +1147,7 @@ async function kbSweepCommand(argv, context) {
|
|
|
877
1147
|
*/
|
|
878
1148
|
const HELP = `rulvar: durable multi-agent workflows (https://docs.rulvar.com)
|
|
879
1149
|
|
|
880
|
-
|
|
881
|
-
rulvar resume <runId> [--store PATH]
|
|
882
|
-
rulvar runs ls [--store PATH]
|
|
883
|
-
rulvar inspect <runId> [--store PATH]
|
|
884
|
-
rulvar plan "<goal>" [--dry-run]
|
|
885
|
-
rulvar kb <list | inbox | gate | sweep>
|
|
1150
|
+
${helpCommandLines().map((line) => ` ${line}`).join("\n")}
|
|
886
1151
|
|
|
887
1152
|
Engine assembly: adapters, defaults, and the workflow registry come from
|
|
888
1153
|
rulvar.config.mjs in the working directory (default export
|
|
@@ -890,13 +1155,18 @@ rulvar.config.mjs in the working directory (default export
|
|
|
890
1155
|
exports. --store selects the JsonlFileStore directory (default .rulvar).
|
|
891
1156
|
plan asks the planner model (role plan) to write a workflow script,
|
|
892
1157
|
lints and self-repairs it, then runs it in the worker sandbox; --dry-run
|
|
893
|
-
prints the accepted script without running.
|
|
894
|
-
|
|
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
|
|
895
1163
|
(./rulvar.models.json) with full provenance. kb sweep runs the
|
|
896
1164
|
falsification matrix from the kbSweep section of rulvar.config.mjs
|
|
897
1165
|
(fixed pool UNIONED with every model carrying an active negative claim
|
|
898
1166
|
plus the re-measure queue; optional canary probes flip drifted claims
|
|
899
|
-
stale first;
|
|
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
|
|
900
1170
|
kb_propose proposals from finished runs (14 day TTL); kb gate turns one
|
|
901
1171
|
inbox proposal into a committed claim behind a human attestation
|
|
902
1172
|
(--approver and --ruled-out are mandatory). inbox and gate require
|
|
@@ -913,7 +1183,7 @@ async function runCli(argv, options) {
|
|
|
913
1183
|
case "resume": return await resumeCommand(rest, context);
|
|
914
1184
|
case "runs": {
|
|
915
1185
|
const [sub, ...subRest] = rest;
|
|
916
|
-
if (sub !== "ls") throw new ConfigError("
|
|
1186
|
+
if (sub !== "ls") throw new ConfigError(usageOf(GRAMMAR["runs ls"]));
|
|
917
1187
|
return await runsLsCommand(subRest, context);
|
|
918
1188
|
}
|
|
919
1189
|
case "inspect": return await inspectCommand(rest, context);
|