@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 +1 -1
- package/dist/index.js +10 -1
- package/dist/{io-DHbOOiYv.js → io-DK9Ji_H2.js} +110 -1
- package/package.json +7 -7
package/dist/cli.js
CHANGED
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-
|
|
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.
|
|
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.
|
|
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.
|
|
32
|
-
"@rulvar/planner": "1.
|
|
33
|
-
"@rulvar/
|
|
34
|
-
"@rulvar/evals": "1.
|
|
35
|
-
"@rulvar/
|
|
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"
|