@rulvar/cli 1.23.0 → 1.24.1

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-Dh-Fa5Nf.js";
2
+ import { r as runCli, t as processIo } from "./io-CV_G44A2.js";
3
3
  import { inspect } from "node:util";
4
4
  //#region src/cli.ts
5
5
  /**
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-Dh-Fa5Nf.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-CV_G44A2.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,4 +1,4 @@
1
- import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, parseModelRef, priceUsdOf, proposalStatement, remeasureQueue, resolvePricing, runProfile, sanitizeTerminalText } from "@rulvar/core";
1
+ import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, hashRunArgs, parseModelRef, priceUsdOf, proposalStatement, remeasureQueue, resolvePricing, runProfile, sanitizeTerminalText } from "@rulvar/core";
2
2
  import { join, resolve } from "node:path";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { pathToFileURL } from "node:url";
@@ -303,6 +303,35 @@ async function driveRun(options) {
303
303
  handle = options.engine.resume(handle.runId, options.workflow, { args: options.args });
304
304
  }
305
305
  }
306
+ /**
307
+ * Renders the `resume --dry-run` preview (the v1.23.0 review): the
308
+ * replay accounting from `handle.preview`, then what a real resume
309
+ * would do. The engine's replay-strict mode guarantees zero journal or
310
+ * meta writes and zero adapter calls. A preview that stops at a
311
+ * would-be-live call is a SUCCESSFUL preview (the miss IS the answer),
312
+ * so the exit code is 0 either way; only structural failures (missing
313
+ * run, unregistered workflow, args refusal) exit nonzero via their
314
+ * typed errors.
315
+ */
316
+ async function reportDryRun(handle, io) {
317
+ const outcome = await handle.result;
318
+ const preview = await handle.preview;
319
+ io.err("dry-run preview (zero journal or meta writes, zero adapter calls):");
320
+ io.err(` hits: ${preview.hits} misses: ${preview.misses} reruns: ${preview.reruns} skipped: ${preview.skipped}`);
321
+ io.err(preview.orphaned.length === 0 ? " orphaned effect roots: none" : ` orphaned effect roots (entryRefs): ${preview.orphaned.join(", ")}`);
322
+ if (preview.invalidResolutions.length === 0) io.err(" invalid resolutions: none");
323
+ else for (const invalid of preview.invalidResolutions) io.err(` invalid resolution at seq ${invalid.seq}: ${invalid.detail}`);
324
+ if (outcome.error?.code === "journal_miss") {
325
+ io.err(` stopped at the first would-be-live call: ${outcome.error.message}`);
326
+ io.err(" a real resume would perform new paid work from this point");
327
+ return 0;
328
+ }
329
+ io.err(` would settle: ${outcome.status}`);
330
+ if (outcome.error !== void 0) io.err(` error: ${outcome.error.message}`);
331
+ for (const pending of outcome.pending) io.err(` pending: ${pending.key} (entry ${pending.entryRef})`);
332
+ if (outcome.value !== void 0) io.out(JSON.stringify(outcome.value, null, 2));
333
+ return 0;
334
+ }
306
335
  /** Renders the settled outcome; returns the process exit code. */
307
336
  function reportOutcome(outcome, io) {
308
337
  io.err(`status: ${outcome.status}`);
@@ -366,7 +395,12 @@ const GRAMMAR = {
366
395
  resume: {
367
396
  command: "resume",
368
397
  positionals: ["<runId>"],
369
- flags: [ARGS, STORE]
398
+ flags: [
399
+ ARGS,
400
+ STORE,
401
+ { name: "dry-run" },
402
+ { name: "allow-args-change" }
403
+ ]
370
404
  },
371
405
  "runs ls": {
372
406
  command: "runs ls",
@@ -607,14 +641,35 @@ async function loadCompanion(loading, specifier, command, missingMessage) {
607
641
  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 });
608
642
  }
609
643
  }
610
- /** Parses --args JSON into workflow arguments; undefined when absent. */
644
+ /**
645
+ * Parses --args JSON into workflow arguments; undefined when absent.
646
+ *
647
+ * CLI args must be representable in canonical JCS, i.e. finite JSON. A
648
+ * numeric literal that overflows JavaScript's finite range parses to
649
+ * Infinity, which `hashRunArgs` cannot canonicalize, so genesis would
650
+ * record `argsProvided` WITHOUT a hash and the resume gate would soften
651
+ * to an unverifiable warning that lets changed args through (v1.24.0
652
+ * review P2-1). A CLI value always arrives as JSON text, so it can
653
+ * always be canonicalized; reject the non-finite case here, before any
654
+ * config, store, or adapter loads, instead of letting it defeat the gate
655
+ * later. In-process hosts keep the wider engine contract (functions,
656
+ * BigInt, cycles record presence without a hash); the CLI does not need
657
+ * it.
658
+ */
611
659
  function parseArgsJson(raw) {
612
660
  if (raw === void 0) return;
661
+ let parsed;
613
662
  try {
614
- return JSON.parse(raw);
663
+ parsed = JSON.parse(raw);
615
664
  } catch {
616
665
  throw new ConfigError(`--args is not valid JSON: ${raw}`);
617
666
  }
667
+ try {
668
+ hashRunArgs(parsed);
669
+ } catch {
670
+ throw new ConfigError(`--args is not representable as canonical JSON: a numeric value overflows JavaScript's finite range (e.g. 1e400 parses to Infinity). Supply finite JSON so the run's args binding can be hashed and later verified on resume: ${raw}`);
671
+ }
672
+ return parsed;
618
673
  }
619
674
  async function runCommand(argv, context) {
620
675
  const parsed = parseCommand(GRAMMAR.run, argv);
@@ -645,10 +700,58 @@ async function runCommand(argv, context) {
645
700
  args
646
701
  }), context.io);
647
702
  }
703
+ /**
704
+ * The resume args safety gate (the v1.23.0 review): a run's logical
705
+ * identity includes its genesis args, so a resume that silently drops,
706
+ * adds, or changes them is refused BEFORE the engine starts (zero
707
+ * provider calls, zero journal writes). `--allow-args-change` is the
708
+ * explicit override for every divergence class; legacy runs recorded
709
+ * before the binding existed require it (or explicit `--args`) because
710
+ * nothing can be verified against them.
711
+ */
712
+ function enforceArgsBinding(input) {
713
+ const { meta, argsGiven, args, allowChange, io } = input;
714
+ if (meta.argsProvided === void 0) {
715
+ if (!argsGiven && !allowChange) throw new ConfigError(`run '${meta.runId}' predates the args binding (rulvar < 1.24.0), so the CLI cannot tell whether it was started with --args, and resuming without them silently changes the logical run if any were used at start. Re-supply the original --args, or acknowledge explicitly with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
716
+ if (argsGiven) io.err(`warning: run '${meta.runId}' predates the args binding; the supplied --args cannot be verified against the original invocation`);
717
+ return;
718
+ }
719
+ if (meta.argsProvided) {
720
+ if (!argsGiven) {
721
+ if (!allowChange) throw new ConfigError(`run '${meta.runId}' was started WITH args, but this resume supplies none; the workflow would see undefined and every args-dependent call would become new paid work instead of a replay. Re-supply the original --args, or force the change with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
722
+ io.err(`warning: resuming '${meta.runId}' without its genesis args (--allow-args-change)`);
723
+ return;
724
+ }
725
+ if (meta.argsHash === void 0) {
726
+ if (!allowChange) throw new ConfigError(`run '${meta.runId}' started WITH args but recorded no verifiable hash (the genesis args were not JCS-serializable), so the CLI cannot confirm the supplied --args match the original; resuming risks silently changing the logical run and re-paying every args-dependent call. Force deliberately with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
727
+ io.err(`warning: run '${meta.runId}' recorded args presence but no hash (genesis args not JCS-serializable); the supplied --args cannot be verified (--allow-args-change)`);
728
+ return;
729
+ }
730
+ let supplied;
731
+ try {
732
+ supplied = hashRunArgs(args);
733
+ } catch {
734
+ throw new ConfigError(`--args cannot be canonicalized to compare against run '${meta.runId}' (a numeric value overflows the finite range, or the value is otherwise not canonical JSON); supply finite JSON, or force the resume with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
735
+ }
736
+ if (supplied !== meta.argsHash) {
737
+ if (!allowChange) throw new ConfigError(`--args does not match the args run '${meta.runId}' was started with (recorded hash ${meta.argsHash.slice(0, 12)}, supplied ${supplied?.slice(0, 12) ?? "none"}); changed args silently change the logical run and re-pay every args-dependent call. Force deliberately with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
738
+ io.err(`warning: resuming '${meta.runId}' with changed args (--allow-args-change)`);
739
+ }
740
+ return;
741
+ }
742
+ if (argsGiven) {
743
+ if (!allowChange) throw new ConfigError(`run '${meta.runId}' was started WITHOUT args, but this resume supplies some; added args silently change the logical run. Drop --args, or force the change with --allow-args-change; ${usageOf(GRAMMAR.resume)}`);
744
+ io.err(`warning: resuming no-args run '${meta.runId}' with args (--allow-args-change)`);
745
+ }
746
+ }
648
747
  async function resumeCommand(argv, context) {
649
748
  const parsed = parseCommand(GRAMMAR.resume, argv);
650
749
  const runId = parsed.positionals[0];
651
- const args = parseArgsJson(parsed.values.args);
750
+ const rawArgs = parsed.values.args;
751
+ const args = parseArgsJson(rawArgs);
752
+ const argsGiven = rawArgs !== void 0;
753
+ const dryRun = parsed.values["dry-run"] === true;
754
+ const allowChange = parsed.values["allow-args-change"] === true;
652
755
  const store = parsed.values.store;
653
756
  const assembled = assembleEngine({
654
757
  config: await loadCliConfig(context.cwd),
@@ -657,10 +760,21 @@ async function resumeCommand(argv, context) {
657
760
  });
658
761
  const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
659
762
  if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
763
+ enforceArgsBinding({
764
+ meta,
765
+ argsGiven,
766
+ args,
767
+ allowChange,
768
+ io: context.io
769
+ });
660
770
  const name = meta.workflowName;
661
771
  const workflow = name === void 0 ? void 0 : assembled.workflows[name];
662
772
  if (workflow === void 0) throw new ConfigError(`run '${runId}' was started from workflow '${name ?? "(unknown)"}'; register it under that name in rulvar.config.mjs workflows to resume (resume requires the in-process workflow value)`);
663
- const first = assembled.engine.resume(runId, workflow, { args });
773
+ const first = assembled.engine.resume(runId, workflow, {
774
+ args,
775
+ ...dryRun ? { dryRun: true } : {}
776
+ });
777
+ if (dryRun) return await reportDryRun(first, context.io);
664
778
  return reportOutcome(await driveRun({
665
779
  engine: assembled.engine,
666
780
  workflow,
@@ -701,6 +815,7 @@ async function inspectCommand(argv, context) {
701
815
  const entries = await assembled.store.load(runId);
702
816
  context.io.out(`run ${meta.runId}: ${meta.status} (updated ${meta.updatedAt})`);
703
817
  if (meta.workflowName !== void 0) context.io.out(`workflow: ${meta.workflowName}`);
818
+ if (meta.argsProvided !== void 0) context.io.out(meta.argsProvided ? `args at genesis: provided${meta.argsHash === void 0 ? " (no hash: not JCS-serializable)" : ` (hash ${meta.argsHash})`}` : "args at genesis: none");
704
819
  const byKind = /* @__PURE__ */ new Map();
705
820
  let openSuspensions = 0;
706
821
  const resolvedRefs = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/cli",
3
- "version": "1.23.0",
3
+ "version": "1.24.1",
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.23.0"
25
+ "@rulvar/core": "1.24.1"
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.23.0",
32
- "@rulvar/store-sqlite": "1.23.0",
33
- "@rulvar/planner": "1.23.0",
34
- "@rulvar/plan": "1.23.0",
35
- "@rulvar/evals": "1.23.0"
31
+ "@rulvar/store-sqlite": "1.24.1",
32
+ "@rulvar/testing": "1.24.1",
33
+ "@rulvar/planner": "1.24.1",
34
+ "@rulvar/plan": "1.24.1",
35
+ "@rulvar/evals": "1.24.1"
36
36
  },
37
37
  "bin": {
38
38
  "rulvar": "./dist/cli.js"