@rulvar/cli 1.49.0 → 1.51.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-DHbOOiYv.js";
2
+ import { r as runCli, t as processIo } from "./io-DK9Ji_H2.js";
3
3
  import { sanitizeTerminalText } from "@rulvar/core";
4
4
  import { inspect } from "node:util";
5
5
  //#region src/cli.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as looksLikeFile, a as resumeCommand, c as driveRun, d as attachProgress, f as renderEventLine, g as loadWorkflowModule, h as loadCliConfig, i as inspectCommand, l as reportOutcome, m as assembleEngine, n as HELP, o as runCommand, p as DEFAULT_STORE_DIR, r as runCli, s as runsLsCommand, t as processIo, u as strictExitCode } from "./io-DHbOOiYv.js";
1
+ import { _ as looksLikeFile, a as resumeCommand, c as driveRun, d as attachProgress, f as renderEventLine, g as loadWorkflowModule, h as loadCliConfig, i as inspectCommand, l as reportOutcome, m as assembleEngine, n as HELP, o as runCommand, p as DEFAULT_STORE_DIR, r as runCli, s as runsLsCommand, t as processIo, u as strictExitCode } from "./io-DK9Ji_H2.js";
2
2
  import { ConfigError, InvalidResolutionError, JournalCompatibilityError, LeaseHeldError, Replayer, RulvarError, buildDeriverRegistry, costReportFromJournal, maskSecrets, normalizeEntry, readRunMeta, scanJournalCompatibility, validateSchemaSpec } from "@rulvar/core";
3
3
  //#region src/server.ts
4
4
  /**
@@ -924,6 +924,15 @@ async function toOtel(run, tracer, options = {}) {
924
924
  case "child:end":
925
925
  endSpan(event.spanId, event.ts, event.status);
926
926
  break;
927
+ case "determinism:warning":
928
+ (openBySpanId.get(event.spanId) ?? stack[stack.length - 1])?.span.addEvent("determinism:warning", {
929
+ "rulvar.entry_seq": event.seq,
930
+ "rulvar.determinism.category": event.category,
931
+ "rulvar.determinism.provenance": event.provenance,
932
+ ...event.file === void 0 ? {} : { "code.filepath": maskSecrets(event.file) },
933
+ ...event.line === void 0 ? {} : { "code.lineno": event.line }
934
+ });
935
+ break;
927
936
  default: (openBySpanId.get(event.spanId) ?? stack[stack.length - 1])?.span.addEvent(event.type, { "rulvar.entry_seq": event.seq });
928
937
  }
929
938
  }
@@ -1,4 +1,4 @@
1
- import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, LeaseHeldError, auditRuns, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, hashRunArgs, parseModelRef, priceUsdOf, proposalStatement, readRunMeta, reconcileRunMeta, remeasureQueue, resolvePricing, runProfile, sanitizeTerminalText } from "@rulvar/core";
1
+ import { ConfigError, FileModelKnowledgeStore, INBOX_PROPOSAL_TTL_DAYS, JsonlFileStore, LeaseHeldError, auditRuns, claimExpired, claimExpiry, compilePermissionPreset, costReportFromJournal, createEngine, hashRunArgs, hashRunOutput, lastRunSettle, parseModelRef, priceUsdOf, proposalStatement, readRunMeta, reconcileRunMeta, 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";
@@ -205,6 +205,10 @@ function rawEventLine(event) {
205
205
  case "tool:start": return `${replayMark}tool ${str(event.toolName)}`;
206
206
  case "tool:end": return `${replayMark}tool ${str(event.toolName)} ${str(event.outcome)} (${String(num(event.durationMs))}ms)`;
207
207
  case "approval:pending": return `approval pending: tool ${str(event.toolName)} (entry ${String(num(event.entryRef))})`;
208
+ case "determinism:warning": {
209
+ const where = str(event.file) === "" ? str(event.frame) : `at ${str(event.file)}:${String(num(event.line))}:${String(num(event.column))}`;
210
+ return `determinism ${str(event.category)} (${str(event.provenance)}) ${where}`;
211
+ }
208
212
  case "log": return event.level === "debug" ? void 0 : `${str(event.level)}: ${str(event.msg)}`;
209
213
  case "run:end": return `run ${str(event.runId)} ${str(event.status)}${event.usageApprox === true ? " (cost approx)" : ""}`;
210
214
  default: return;
@@ -229,6 +233,7 @@ function attachProgress(handle, io) {
229
233
  "tool:start",
230
234
  "tool:end",
231
235
  "approval:pending",
236
+ "determinism:warning",
232
237
  "log",
233
238
  "run:end"
234
239
  ]) detachers.push(handle.on(type, forward));
@@ -441,6 +446,16 @@ const GRAMMAR = {
441
446
  { name: "strict" }
442
447
  ]
443
448
  },
449
+ replay: {
450
+ command: "replay",
451
+ positionals: ["<runId>"],
452
+ flags: [
453
+ ARGS,
454
+ STORE,
455
+ { name: "assert-no-live" },
456
+ { name: "compare-output-hash" }
457
+ ]
458
+ },
444
459
  "runs ls": {
445
460
  command: "runs ls",
446
461
  positionals: [],
@@ -564,6 +579,7 @@ function helpCommandLines() {
564
579
  const top = [
565
580
  GRAMMAR.run,
566
581
  GRAMMAR.resume,
582
+ GRAMMAR.replay,
567
583
  GRAMMAR["runs ls"],
568
584
  GRAMMAR["runs audit"],
569
585
  GRAMMAR.inspect,
@@ -867,6 +883,92 @@ async function resumeCommand(argv, context) {
867
883
  return parsed.values.strict === true ? strictExitCode(outcome, base, context.io) : base;
868
884
  }
869
885
  /**
886
+ * `rulvar replay` (RV-209): replay-strict verification of a recorded
887
+ * run. Resumes under the engine's dry-run mode (zero journal or meta
888
+ * writes, zero adapter calls; the first would-be-live call is a typed
889
+ * JournalMissError settle), then reports the replay accounting, every
890
+ * determinism warning the re-executed body raised (with its localized
891
+ * frame), and the output digest comparison against the journaled
892
+ * settle. `--assert-no-live` exits nonzero unless the replay was pure
893
+ * (zero misses, zero reruns); `--compare-output-hash` exits nonzero
894
+ * unless the replayed result's JCS sha256 equals the recorded
895
+ * `outputHash`. Without flags the command reports and exits 0, so it
896
+ * can sit in a pipeline as a diagnostic before it gates anything.
897
+ * Args follow the resume binding exactly, but there is no
898
+ * --allow-args-change here: changed args change the logical run, and a
899
+ * verification against a different logical run proves nothing.
900
+ */
901
+ async function replayCommand(argv, context) {
902
+ const parsed = parseCommand(GRAMMAR.replay, argv);
903
+ const runId = parsed.positionals[0];
904
+ const rawArgs = parsed.values.args;
905
+ const args = parseArgsJson(rawArgs);
906
+ const argsGiven = rawArgs !== void 0;
907
+ const assertNoLive = parsed.values["assert-no-live"] === true;
908
+ const compareOutputHash = parsed.values["compare-output-hash"] === true;
909
+ const store = parsed.values.store;
910
+ const assembled = assembleEngine({
911
+ config: await loadCliConfig(context.cwd),
912
+ ...store === void 0 ? {} : { storePath: store },
913
+ cwd: context.cwd
914
+ });
915
+ const meta = await readRunMeta(assembled.store, runId);
916
+ if (meta === void 0) throw new ConfigError(`run '${runId}' not found in the store`);
917
+ enforceArgsBinding({
918
+ meta,
919
+ argsGiven,
920
+ args,
921
+ allowChange: false,
922
+ io: context.io
923
+ });
924
+ const name = meta.workflowName;
925
+ const workflow = name === void 0 ? void 0 : assembled.workflows[name];
926
+ 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 replay (replay requires the in-process workflow value)`);
927
+ const handle = assembled.engine.resume(runId, workflow, {
928
+ args,
929
+ dryRun: true
930
+ });
931
+ const warnings = [];
932
+ const consumer = (async () => {
933
+ for await (const event of handle.events) if (event.type === "determinism:warning") warnings.push(event);
934
+ })().catch(() => void 0);
935
+ const outcome = await handle.result;
936
+ const preview = await handle.preview;
937
+ await consumer;
938
+ const recorded = lastRunSettle(await assembled.store.load(runId));
939
+ const io = context.io;
940
+ io.err(`replay of '${sanitizeTerminalText(runId)}' (zero journal or meta writes, zero adapter calls):`);
941
+ io.err(` hits: ${preview.hits} misses: ${preview.misses} reruns: ${preview.reruns} skipped: ${preview.skipped}`);
942
+ io.err(recorded === void 0 ? " recorded settle: none (journal predates the settle entry)" : ` recorded settle: ${recorded.runStatus}`);
943
+ io.err(` replayed settle: ${outcome.status}`);
944
+ if (outcome.error !== void 0 && outcome.error.code !== "journal_miss") io.err(` error: ${sanitizeTerminalText(outcome.error.message)}`);
945
+ for (const warning of warnings) {
946
+ const where = warning.file === void 0 ? warning.frame : `at ${warning.file}:${warning.line ?? "?"}:${warning.column ?? "?"}`;
947
+ io.err(` determinism: ${warning.category} (${warning.provenance}) ${sanitizeTerminalText(where)}`);
948
+ }
949
+ let exit = 0;
950
+ if (assertNoLive) if (preview.misses === 0 && preview.reruns === 0 && outcome.error?.code !== "journal_miss") io.err(" assert-no-live: PASS (pure replay, zero would-be-live calls)");
951
+ else {
952
+ io.err(` assert-no-live: FAIL (misses ${preview.misses}, reruns ${preview.reruns}: a real resume would perform new paid work)`);
953
+ exit = 1;
954
+ }
955
+ if (compareOutputHash) {
956
+ const replayedHash = hashRunOutput(outcome.value);
957
+ if (recorded?.outputHash === void 0) {
958
+ io.err(" compare-output-hash: FAIL (the recorded settle carries no output hash: the run predates it, settled without a value, or the value is not JCS-serializable)");
959
+ exit = 1;
960
+ } else if (replayedHash === void 0) {
961
+ io.err(" compare-output-hash: FAIL (the replayed run produced no hashable value)");
962
+ exit = 1;
963
+ } else if (replayedHash === recorded.outputHash) io.err(` compare-output-hash: PASS (${replayedHash.slice(0, 12)})`);
964
+ else {
965
+ io.err(` compare-output-hash: FAIL (recorded ${recorded.outputHash.slice(0, 12)}, replayed ${replayedHash.slice(0, 12)}: the workflow does not reproduce its output)`);
966
+ exit = 1;
967
+ }
968
+ }
969
+ return exit;
970
+ }
971
+ /**
870
972
  * The stranded run probe and reconciler (fenced run state RFC, phase
871
973
  * 3): audits every run the catalog lists against its journal, prints
872
974
  * the divergences worker sweeps can never see, and with --repair
@@ -1434,6 +1536,12 @@ runs audit compares every run's meta row against its journal and names
1434
1536
  the divergences worker sweeps cannot see (a stranded run's terminal
1435
1537
  meta over live journal work); --repair rewrites the sound ones from
1436
1538
  the journal under a brief lease, exit 1 while any divergence remains.
1539
+ replay verifies a recorded run without paying: a dry-run resume (zero
1540
+ journal or meta writes, zero adapter calls) that reports replay
1541
+ accounting, localized determinism warnings, and the output digest;
1542
+ --assert-no-live exits 1 unless the replay is pure, and
1543
+ --compare-output-hash exits 1 unless the replayed result's digest
1544
+ equals the journaled one.
1437
1545
  plan asks the planner model (role plan) to write a workflow script,
1438
1546
  lints and self-repairs it, then runs it in the worker sandbox; --dry-run
1439
1547
  prints the accepted script without running. Both stages are paid runs
@@ -1462,6 +1570,7 @@ async function runCli(argv, options) {
1462
1570
  switch (command) {
1463
1571
  case "run": return await runCommand(rest, context);
1464
1572
  case "resume": return await resumeCommand(rest, context);
1573
+ case "replay": return await replayCommand(rest, context);
1465
1574
  case "runs": {
1466
1575
  const [sub, ...subRest] = rest;
1467
1576
  if (sub === "ls") return await runsLsCommand(subRest, context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/cli",
3
- "version": "1.49.0",
3
+ "version": "1.51.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.49.0"
25
+ "@rulvar/core": "1.51.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/store-sqlite": "1.49.0",
32
- "@rulvar/planner": "1.49.0",
33
- "@rulvar/plan": "1.49.0",
34
- "@rulvar/evals": "1.49.0",
35
- "@rulvar/testing": "1.49.0"
31
+ "@rulvar/store-sqlite": "1.51.0",
32
+ "@rulvar/planner": "1.51.0",
33
+ "@rulvar/testing": "1.51.0",
34
+ "@rulvar/evals": "1.51.0",
35
+ "@rulvar/plan": "1.51.0"
36
36
  },
37
37
  "bin": {
38
38
  "rulvar": "./dist/cli.js"