@rulvar/cli 1.23.0 → 1.24.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-Dh-Fa5Nf.js";
2
+ import { r as runCli, t as processIo } from "./io-CL4wldEQ.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-CL4wldEQ.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",
@@ -645,10 +679,52 @@ async function runCommand(argv, context) {
645
679
  args
646
680
  }), context.io);
647
681
  }
682
+ /**
683
+ * The resume args safety gate (the v1.23.0 review): a run's logical
684
+ * identity includes its genesis args, so a resume that silently drops,
685
+ * adds, or changes them is refused BEFORE the engine starts (zero
686
+ * provider calls, zero journal writes). `--allow-args-change` is the
687
+ * explicit override for every divergence class; legacy runs recorded
688
+ * before the binding existed require it (or explicit `--args`) because
689
+ * nothing can be verified against them.
690
+ */
691
+ function enforceArgsBinding(input) {
692
+ const { meta, argsGiven, args, allowChange, io } = input;
693
+ if (meta.argsProvided === void 0) {
694
+ 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)}`);
695
+ if (argsGiven) io.err(`warning: run '${meta.runId}' predates the args binding; the supplied --args cannot be verified against the original invocation`);
696
+ return;
697
+ }
698
+ if (meta.argsProvided) {
699
+ if (!argsGiven) {
700
+ 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)}`);
701
+ io.err(`warning: resuming '${meta.runId}' without its genesis args (--allow-args-change)`);
702
+ return;
703
+ }
704
+ if (meta.argsHash === void 0) {
705
+ io.err(`warning: run '${meta.runId}' recorded args presence but no hash (the genesis args were not JCS-serializable); the supplied --args cannot be verified`);
706
+ return;
707
+ }
708
+ const supplied = hashRunArgs(args);
709
+ if (supplied !== meta.argsHash) {
710
+ 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)}`);
711
+ io.err(`warning: resuming '${meta.runId}' with changed args (--allow-args-change)`);
712
+ }
713
+ return;
714
+ }
715
+ if (argsGiven) {
716
+ 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)}`);
717
+ io.err(`warning: resuming no-args run '${meta.runId}' with args (--allow-args-change)`);
718
+ }
719
+ }
648
720
  async function resumeCommand(argv, context) {
649
721
  const parsed = parseCommand(GRAMMAR.resume, argv);
650
722
  const runId = parsed.positionals[0];
651
- const args = parseArgsJson(parsed.values.args);
723
+ const rawArgs = parsed.values.args;
724
+ const args = parseArgsJson(rawArgs);
725
+ const argsGiven = rawArgs !== void 0;
726
+ const dryRun = parsed.values["dry-run"] === true;
727
+ const allowChange = parsed.values["allow-args-change"] === true;
652
728
  const store = parsed.values.store;
653
729
  const assembled = assembleEngine({
654
730
  config: await loadCliConfig(context.cwd),
@@ -657,10 +733,21 @@ async function resumeCommand(argv, context) {
657
733
  });
658
734
  const meta = (await assembled.store.listRuns()).find((m) => m.runId === runId);
659
735
  if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
736
+ enforceArgsBinding({
737
+ meta,
738
+ argsGiven,
739
+ args,
740
+ allowChange,
741
+ io: context.io
742
+ });
660
743
  const name = meta.workflowName;
661
744
  const workflow = name === void 0 ? void 0 : assembled.workflows[name];
662
745
  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 });
746
+ const first = assembled.engine.resume(runId, workflow, {
747
+ args,
748
+ ...dryRun ? { dryRun: true } : {}
749
+ });
750
+ if (dryRun) return await reportDryRun(first, context.io);
664
751
  return reportOutcome(await driveRun({
665
752
  engine: assembled.engine,
666
753
  workflow,
@@ -701,6 +788,7 @@ async function inspectCommand(argv, context) {
701
788
  const entries = await assembled.store.load(runId);
702
789
  context.io.out(`run ${meta.runId}: ${meta.status} (updated ${meta.updatedAt})`);
703
790
  if (meta.workflowName !== void 0) context.io.out(`workflow: ${meta.workflowName}`);
791
+ 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
792
  const byKind = /* @__PURE__ */ new Map();
705
793
  let openSuspensions = 0;
706
794
  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.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.23.0"
25
+ "@rulvar/core": "1.24.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.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/testing": "1.24.0",
32
+ "@rulvar/store-sqlite": "1.24.0",
33
+ "@rulvar/planner": "1.24.0",
34
+ "@rulvar/evals": "1.24.0",
35
+ "@rulvar/plan": "1.24.0"
36
36
  },
37
37
  "bin": {
38
38
  "rulvar": "./dist/cli.js"