skalpel 4.0.27 → 4.0.28
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/insights.mjs +6 -0
- package/package.json +1 -1
- package/skalpel-setup.mjs +18 -0
- package/skalpel-statusline.mjs +39 -1
- package/verify-shadow.mjs +176 -1
package/insights.mjs
CHANGED
|
@@ -24,6 +24,12 @@ export const INSIGHTS_PATH = join(DIR, "insights.ndjson");
|
|
|
24
24
|
// user-visible bar). Both the flag semantics and the file path live HERE — the one light module both the
|
|
25
25
|
// writer and the reader (and the hook's spawn gate) already import — so they can never drift apart.
|
|
26
26
|
export const REVEAL_PATH = join(DIR, "verify-reveal.json");
|
|
27
|
+
// verify-verdict: the POSITIVE half of the same surface — the quiet green "✓ verified" stamp written on
|
|
28
|
+
// a PASS (the agent claimed done and its OWN proof, re-run, really passed). Same writer (verify-shadow.mjs)
|
|
29
|
+
// and reader (skalpel-statusline.mjs), same opt-in flag as the red reveal. Ambient greens are what let the
|
|
30
|
+
// rare red catch land like a thunderclap — without a stream of honest verified stamps a lone red reads as
|
|
31
|
+
// noise, not a verdict. Path single-sourced HERE alongside REVEAL_PATH so writer + reader never drift.
|
|
32
|
+
export const VERDICT_PATH = join(DIR, "verify-verdict.json");
|
|
27
33
|
// SKALPEL_VERIFY_REVEAL is OPT-IN and OFF BY DEFAULT. Unset / "" / "0" / "off" / "false" / "no" → DARK
|
|
28
34
|
// (byte-identical to the shadow-only behavior). Only an explicit truthy value arms the visible reveal.
|
|
29
35
|
export function revealEnabled(env) {
|
package/package.json
CHANGED
package/skalpel-setup.mjs
CHANGED
|
@@ -60,6 +60,7 @@ const KNOWN_SUBS = new Set([
|
|
|
60
60
|
"logout",
|
|
61
61
|
"uninstall",
|
|
62
62
|
"autopsy",
|
|
63
|
+
"xray",
|
|
63
64
|
"__build",
|
|
64
65
|
"__verify-report",
|
|
65
66
|
]);
|
|
@@ -73,6 +74,7 @@ usage:
|
|
|
73
74
|
skalpel login (re-)run the Google sign-in
|
|
74
75
|
skalpel logout clear the saved session
|
|
75
76
|
skalpel autopsy local, read-only receipt of your verified patterns
|
|
77
|
+
skalpel xray re-run your last session's own proof — did "done" hold up?
|
|
76
78
|
skalpel uninstall [--purge] remove the hooks + local data from this machine
|
|
77
79
|
|
|
78
80
|
options:
|
|
@@ -432,6 +434,13 @@ function promptLaunch(targets) {
|
|
|
432
434
|
|
|
433
435
|
async function reportLiveAndLaunch(message) {
|
|
434
436
|
console.log(message);
|
|
437
|
+
// FIRST-RUN X-RAY pointer: offer the user their own first catch — an independent re-run of the proof
|
|
438
|
+
// from the session they just had. A pointer, not an auto-run (an interactive install must never spawn a
|
|
439
|
+
// 60s test suite unasked); the command itself does the honest work when they choose to run it.
|
|
440
|
+
console.log(
|
|
441
|
+
` ${D}Want to check the session you just had? Run ${X}${B}skalpel xray${X}${D} — I'll re-run your` +
|
|
442
|
+
` agent's own proof and tell you if "done" really held up.${X}\n`,
|
|
443
|
+
);
|
|
435
444
|
await promptLaunch(detectLaunchers());
|
|
436
445
|
}
|
|
437
446
|
|
|
@@ -867,6 +876,15 @@ async function main() {
|
|
|
867
876
|
spawnSync("node", [join(__dir, "verify-shadow.mjs"), "--report"], { stdio: "inherit" });
|
|
868
877
|
return;
|
|
869
878
|
}
|
|
879
|
+
// `skalpel xray` — the FIRST-RUN X-RAY: independently re-run the session you JUST had. Finds your most
|
|
880
|
+
// recent Claude Code transcript, reconstructs your agent's OWN last proof (test/build/typecheck/lint),
|
|
881
|
+
// re-runs it once, and stamps it verified or caught. Local, read-only against ~/.claude/projects; the
|
|
882
|
+
// only thing it executes is your session's own allowlisted proof. Reports a real verdict from a real
|
|
883
|
+
// re-run — never a minutes-lost tally, never a fabricated number.
|
|
884
|
+
if (sub === "xray") {
|
|
885
|
+
spawnSync("node", [join(__dir, "verify-shadow.mjs"), "--xray"], { stdio: "inherit" });
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
870
888
|
// Persist --api/--user to ~/.skalpel/client.json so the hooks can read them without shell-env
|
|
871
889
|
// plumbing (env vars SKALPEL_API / SKALPEL_USER still win at hook runtime).
|
|
872
890
|
if (apiFlag || userFlag) {
|
package/skalpel-statusline.mjs
CHANGED
|
@@ -11,9 +11,10 @@ import { readFileSync, rmSync } from "node:fs";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { tailLines } from "./transcript.mjs";
|
|
14
|
-
import { cleanText, REVEAL_PATH, revealEnabled } from "./insights.mjs";
|
|
14
|
+
import { cleanText, REVEAL_PATH, VERDICT_PATH, revealEnabled } from "./insights.mjs";
|
|
15
15
|
|
|
16
16
|
const CYAN = "\x1b[36m";
|
|
17
|
+
const GREEN = "\x1b[32m";
|
|
17
18
|
const BOLD = "\x1b[1m";
|
|
18
19
|
const DIM = "\x1b[2m";
|
|
19
20
|
const RESET = "\x1b[0m";
|
|
@@ -22,6 +23,11 @@ const RESET = "\x1b[0m";
|
|
|
22
23
|
// wallpapers). verify-shadow.mjs writes the reveal the moment a GENUINE_FAIL mismatch is re-run; it also
|
|
23
24
|
// retracts it if a later proof re-run PASSES. This TTL covers the abandoned case (user moved on).
|
|
24
25
|
const REVEAL_TTL_MS = 90_000;
|
|
26
|
+
// LIVE VERDICT green stamp — the earned "✓ verified" written on a clean PASS. DELIBERATELY short-lived:
|
|
27
|
+
// it flashes right after the claim, then recedes so a stream of greens stays ambient (quiet trust) and
|
|
28
|
+
// never competes with the loud red catch. Short TTL is the whole point — a green that lingers is clutter;
|
|
29
|
+
// a green that blinks by, turn after turn, is what makes the rare red land like a thunderclap.
|
|
30
|
+
const VERDICT_TTL_MS = 20_000;
|
|
25
31
|
|
|
26
32
|
// Claude Code pipes a JSON status payload on stdin (transcript_path, session_id, …). Read it ONLY when
|
|
27
33
|
// stdin is actually piped — never block on a TTY (manual invocation) where reading fd 0 would hang.
|
|
@@ -116,6 +122,30 @@ function revealNote() {
|
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// The LIVE VERDICT green stamp: read the verdict state verify-shadow.mjs wrote on a clean PASS. Returns
|
|
126
|
+
// the plain "✓ verified" line (fresh + honest — the statusline never invents it, it only renders what the
|
|
127
|
+
// re-run recorded) or null. Auto-clears past its short TTL so a stale green never lingers. GATED entirely
|
|
128
|
+
// on SKALPEL_VERIFY_REVEAL: when off, this is never called and the bar is byte-identical to the original.
|
|
129
|
+
function verdictNote() {
|
|
130
|
+
try {
|
|
131
|
+
const r = JSON.parse(readFileSync(VERDICT_PATH, "utf8"));
|
|
132
|
+
if (!r || typeof r.line !== "string" || !r.line.trim()) return null;
|
|
133
|
+
const age = Date.now() - Date.parse(r.ts);
|
|
134
|
+
if (!Number.isFinite(age) || age > VERDICT_TTL_MS) {
|
|
135
|
+
try {
|
|
136
|
+
rmSync(VERDICT_PATH, { force: true }); // stale → retract so it never lingers
|
|
137
|
+
} catch {
|
|
138
|
+
/* best-effort; a leftover verdict still self-expires by TTL on the next read */
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const clean = cleanText(r.line).trim(); // last-line defense: this goes RAW to the live terminal
|
|
143
|
+
return clean || null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null; // no verdict pending, or unreadable → print nothing (fail-open)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
119
149
|
async function main() {
|
|
120
150
|
const payload = readPayload();
|
|
121
151
|
|
|
@@ -128,6 +158,14 @@ async function main() {
|
|
|
128
158
|
process.stdout.write(`${BOLD}${reveal}${RESET}`);
|
|
129
159
|
return;
|
|
130
160
|
}
|
|
161
|
+
// No red catch pending → the quiet green half. A fresh PASS stamp shows dim (ambient, not shouting)
|
|
162
|
+
// for a short beat. Lower priority than the red catch, higher than the re-ask count: it's the most
|
|
163
|
+
// recent verdict on the last claim, and it's what earns the trust the red catch spends.
|
|
164
|
+
const verdict = verdictNote();
|
|
165
|
+
if (verdict) {
|
|
166
|
+
process.stdout.write(`${DIM}${GREEN}${verdict}${RESET}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
// Acute + reproducible: the user has re-asked / interrupted repeatedly this stretch. Surface the count
|
package/verify-shadow.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import {
|
|
28
28
|
appendFileSync,
|
|
29
29
|
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
30
31
|
readFileSync,
|
|
31
32
|
writeFileSync,
|
|
32
33
|
renameSync,
|
|
@@ -42,7 +43,7 @@ import { realpathSync } from "node:fs";
|
|
|
42
43
|
import { tailLines } from "./transcript.mjs";
|
|
43
44
|
// Local sibling modules only (no sockets — RED LINE #4 holds): recordInsight logs the reveal fire for
|
|
44
45
|
// measurement; REVEAL_PATH/revealEnabled are the single-sourced state-file path + opt-in flag.
|
|
45
|
-
import { recordInsight, REVEAL_PATH, revealEnabled } from "./insights.mjs";
|
|
46
|
+
import { recordInsight, REVEAL_PATH, VERDICT_PATH, revealEnabled } from "./insights.mjs";
|
|
46
47
|
|
|
47
48
|
const DIR = join(homedir(), ".skalpel");
|
|
48
49
|
export const VERIFY_LOG_PATH = join(DIR, "verify-shadow.log");
|
|
@@ -710,12 +711,52 @@ function clearReveal() {
|
|
|
710
711
|
}
|
|
711
712
|
}
|
|
712
713
|
|
|
714
|
+
// ---- the POSITIVE half: the quiet green VERDICT STAMP (LIVE VERDICT). On a PASS (claim + the session's
|
|
715
|
+
// OWN proof re-run really exited 0) we write a dim-green "✓ verified" stamp the statusline renders for a
|
|
716
|
+
// short beat. HONESTY: every token traces to the real run — the real reconstructed proof command and the
|
|
717
|
+
// literal fact that its process exited 0. NO number is shown here (a pass has no count to invent). The
|
|
718
|
+
// ambient stream of these earned greens is what makes the rare red catch feel like a verdict, not noise.
|
|
719
|
+
|
|
720
|
+
// buildVerdictLine(row) → the plain-text green stamp (no ANSI — the statusline styles + re-sanitizes).
|
|
721
|
+
export function buildVerdictLine(row) {
|
|
722
|
+
const proof = clipText(row.proof_command, 48) || "its own proof";
|
|
723
|
+
return `✓ skalpel verified — re-ran \`${proof}\`, exit 0. It's real.`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// writeVerdict(row) — atomic write of the green-stamp state file the statusline reads. Best-effort.
|
|
727
|
+
function writeVerdict(row) {
|
|
728
|
+
try {
|
|
729
|
+
mkdirSync(DIR, { recursive: true });
|
|
730
|
+
const rec = {
|
|
731
|
+
ts: row.ts,
|
|
732
|
+
session: row.session || null,
|
|
733
|
+
proof_command: row.proof_command,
|
|
734
|
+
line: buildVerdictLine(row),
|
|
735
|
+
};
|
|
736
|
+
const tmp = `${VERDICT_PATH}.tmp`;
|
|
737
|
+
writeFileSync(tmp, JSON.stringify(rec));
|
|
738
|
+
renameSync(tmp, VERDICT_PATH); // atomic swap — a concurrent statusline never sees a half write
|
|
739
|
+
} catch {
|
|
740
|
+
/* the verdict stamp is a bonus; a write failure must never affect the shadow */
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// clearVerdict() — a fresh RED catch supersedes any lingering green (never show both at once).
|
|
745
|
+
function clearVerdict() {
|
|
746
|
+
try {
|
|
747
|
+
rmSync(VERDICT_PATH, { force: true });
|
|
748
|
+
} catch {
|
|
749
|
+
/* stale verdict self-expires via the statusline's short TTL anyway */
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
713
753
|
// maybeSurfaceReveal(row) — the single opt-in branch. OFF by default → does nothing (fully dark).
|
|
714
754
|
function maybeSurfaceReveal(row) {
|
|
715
755
|
try {
|
|
716
756
|
if (!revealEnabled(process.env)) return; // SKALPEL_VERIFY_REVEAL unset/off → dark, no-op
|
|
717
757
|
if (row.outcome === "GENUINE_FAIL" && row.mismatch === true) {
|
|
718
758
|
writeReveal(row);
|
|
759
|
+
clearVerdict(); // a real catch overrides any stale green — never show a ✓ next to a ✗
|
|
719
760
|
// MEASURABLE: one reveal_shown insight per fire → fire-rate + (later) retention, no fabricated data.
|
|
720
761
|
recordInsight({
|
|
721
762
|
kind: "reveal_shown",
|
|
@@ -725,7 +766,15 @@ function maybeSurfaceReveal(row) {
|
|
|
725
766
|
});
|
|
726
767
|
} else if (row.outcome === "PASS") {
|
|
727
768
|
clearReveal(); // resolved — don't wallpaper a catch the agent already fixed
|
|
769
|
+
writeVerdict(row); // …and stamp the earned green so the ambient trust-line keeps building
|
|
770
|
+
recordInsight({
|
|
771
|
+
kind: "verdict_shown",
|
|
772
|
+
display: buildVerdictLine(row),
|
|
773
|
+
proof: row.proof_command,
|
|
774
|
+
session: row.session || null,
|
|
775
|
+
});
|
|
728
776
|
}
|
|
777
|
+
// HARNESS_ERROR / REFUSED: neither a catch nor a clean pass — surface nothing, touch no state file.
|
|
729
778
|
} catch {
|
|
730
779
|
/* reveal surfacing is a bonus; never affect the shadow path */
|
|
731
780
|
}
|
|
@@ -883,8 +932,130 @@ export function renderVerifyReport() {
|
|
|
883
932
|
return L.join("\n");
|
|
884
933
|
}
|
|
885
934
|
|
|
935
|
+
// ======================= FIRST-RUN X-RAY: `skalpel xray` =======================
|
|
936
|
+
// A one-shot, user-INVOKED replay over the session you JUST had: find your most recent Claude Code
|
|
937
|
+
// transcript, read the agent's last completion claim, reconstruct the session's OWN proof, re-run it
|
|
938
|
+
// once, and stamp it verified or caught. Same engine as the live shadow — the only new thing is finding
|
|
939
|
+
// the newest transcript and printing a human verdict. Explicit invocation IS the opt-in; it re-runs only
|
|
940
|
+
// the session's own allowlisted proof (never anything from prompt/claim CONTENT). It reports ONLY a real
|
|
941
|
+
// verdict from a real re-run — NEVER a minutes-lost tally, never a fabricated number (the banned
|
|
942
|
+
// confession). Local, read-only against ~/.claude/projects; opens no socket. Never throws.
|
|
943
|
+
const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
944
|
+
|
|
945
|
+
// newestTranscript() → absolute path of the most-recently-modified *.jsonl under ~/.claude/projects, or
|
|
946
|
+
// null. Bounded: one level of project dirs (Claude's layout), newest mtime wins. Fail-open on any error.
|
|
947
|
+
export function newestTranscript(root = CLAUDE_PROJECTS_DIR) {
|
|
948
|
+
let best = null;
|
|
949
|
+
let bestMtime = -Infinity;
|
|
950
|
+
let dirs;
|
|
951
|
+
try {
|
|
952
|
+
dirs = readdirSync(root, { withFileTypes: true });
|
|
953
|
+
} catch {
|
|
954
|
+
return null; // no ~/.claude/projects → nothing to x-ray
|
|
955
|
+
}
|
|
956
|
+
for (const d of dirs) {
|
|
957
|
+
if (!d.isDirectory()) continue;
|
|
958
|
+
const sub = join(root, d.name);
|
|
959
|
+
let files;
|
|
960
|
+
try {
|
|
961
|
+
files = readdirSync(sub);
|
|
962
|
+
} catch {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
for (const f of files) {
|
|
966
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
967
|
+
const p = join(sub, f);
|
|
968
|
+
try {
|
|
969
|
+
const m = statSync(p).mtimeMs;
|
|
970
|
+
if (m > bestMtime) {
|
|
971
|
+
bestMtime = m;
|
|
972
|
+
best = p;
|
|
973
|
+
}
|
|
974
|
+
} catch {
|
|
975
|
+
/* vanished mid-scan — skip */
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return best;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// xrayLastSession() → runs the replay and PRINTS the verdict. Returns a small result object (also used by
|
|
983
|
+
// tests). Ansi via the same palette the statusline uses; honest by construction — no derived numbers.
|
|
984
|
+
export async function xrayLastSession({ transcriptPath } = {}) {
|
|
985
|
+
const RED = "\x1b[38;2;233;38;31m";
|
|
986
|
+
const GREEN = "\x1b[38;2;124;184;108m";
|
|
987
|
+
const DIM = "\x1b[2m";
|
|
988
|
+
const BOLD = "\x1b[1m";
|
|
989
|
+
const X = "\x1b[0m";
|
|
990
|
+
const say = (s) => process.stdout.write(s + "\n");
|
|
991
|
+
|
|
992
|
+
const tp = transcriptPath || newestTranscript();
|
|
993
|
+
if (!tp) {
|
|
994
|
+
say(
|
|
995
|
+
`\n ${DIM}No recent Claude Code session found to check (looked in ~/.claude/projects).${X}\n`,
|
|
996
|
+
);
|
|
997
|
+
return { status: "no_session" };
|
|
998
|
+
}
|
|
999
|
+
const entries = readEntries(tp);
|
|
1000
|
+
if (!entries.length) {
|
|
1001
|
+
say(`\n ${DIM}Your most recent session had nothing readable to check.${X}\n`);
|
|
1002
|
+
return { status: "empty" };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const { claim, text } = detectCompletionClaim(lastAssistantText(entries));
|
|
1006
|
+
say(`\n ${BOLD}skalpel x-ray${X} ${DIM}· replaying the session you just had${X}`);
|
|
1007
|
+
if (!claim) {
|
|
1008
|
+
// No completion claim in the last turn — nothing was asserted done, so there's nothing to catch.
|
|
1009
|
+
say(
|
|
1010
|
+
`\n ${DIM}Your agent's last turn didn't claim it was done — nothing to independently verify.${X}\n`,
|
|
1011
|
+
);
|
|
1012
|
+
return { status: "no_claim" };
|
|
1013
|
+
}
|
|
1014
|
+
say(` ${DIM}claim:${X} "${clipText(text, 72)}"`);
|
|
1015
|
+
|
|
1016
|
+
const proof = reconstructProof(entries, null);
|
|
1017
|
+
if (!proof) {
|
|
1018
|
+
// Honest limit: it claimed done but ran no test/build/typecheck we could re-run out-of-band.
|
|
1019
|
+
say(
|
|
1020
|
+
`\n ${DIM}It said done, but ran no test/build/typecheck this session — nothing skalpel can` +
|
|
1021
|
+
` independently re-run. No verdict (and no guess).${X}\n`,
|
|
1022
|
+
);
|
|
1023
|
+
return { status: "no_proof" };
|
|
1024
|
+
}
|
|
1025
|
+
const proofStr = [proof.cmd, ...proof.args].join(" ").slice(0, 120);
|
|
1026
|
+
say(` ${DIM}re-running its own proof:${X} ${proofStr}${DIM} …${X}`);
|
|
1027
|
+
|
|
1028
|
+
const result = await rerunProof(proof);
|
|
1029
|
+
const outcome = result.outcome || (result.pass ? "PASS" : "GENUINE_FAIL");
|
|
1030
|
+
if (outcome === "PASS") {
|
|
1031
|
+
say(
|
|
1032
|
+
`\n ${GREEN}✓ verified${X} — re-ran \`${proofStr}\`, exit 0. Your agent's "done" holds up.\n`,
|
|
1033
|
+
);
|
|
1034
|
+
return { status: "pass", proof: proofStr };
|
|
1035
|
+
}
|
|
1036
|
+
if (outcome === "GENUINE_FAIL") {
|
|
1037
|
+
const failN = extractFailCount(result.evidence);
|
|
1038
|
+
const count = failN != null ? ` (${failN} failing)` : "";
|
|
1039
|
+
say(
|
|
1040
|
+
`\n ${RED}${BOLD}✗ caught${X} — your agent said "${clipText(text, 60)}" but \`${proofStr}\`` +
|
|
1041
|
+
` just failed${count}. It's not done.`,
|
|
1042
|
+
);
|
|
1043
|
+
if (result.evidence) say(` ${DIM}real output:${X} ${clipText(result.evidence, 180)}`);
|
|
1044
|
+
say("");
|
|
1045
|
+
return { status: "caught", proof: proofStr, evidence: result.evidence };
|
|
1046
|
+
}
|
|
1047
|
+
// HARNESS_ERROR / REFUSED — the proof machinery broke, NOT the agent's work. Never render this as a
|
|
1048
|
+
// catch (crying wolf on a timeout / missing script / wrong cwd is exactly what kills the trust).
|
|
1049
|
+
say(
|
|
1050
|
+
`\n ${DIM}Couldn't independently verify — the proof \`${proofStr}\` errored` +
|
|
1051
|
+
`${result.evidence ? ` (${clipText(result.evidence, 120)})` : ""}. Not a catch.${X}\n`,
|
|
1052
|
+
);
|
|
1053
|
+
return { status: "harness_error", proof: proofStr };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
886
1056
|
// CLI: `node verify-shadow.mjs --run` → detached worker launched by the per-turn hook.
|
|
887
1057
|
// `node verify-shadow.mjs --report`→ print the instrument (also reachable via `skalpel __verify-report`).
|
|
1058
|
+
// `node verify-shadow.mjs --xray` → the one-shot FIRST-RUN X-RAY (reachable via `skalpel xray`).
|
|
888
1059
|
// Robust main detection (mirrors skalpel-setup.mjs): compare REAL paths, not a hand-built file:// URL —
|
|
889
1060
|
// the staged path lives under the home dir, which on macOS/Windows can contain a space that would break
|
|
890
1061
|
// `new URL("file://"+path)` and wrongly disable the CLI. Importing this module (tests) never trips it.
|
|
@@ -903,6 +1074,10 @@ if (isMain) {
|
|
|
903
1074
|
if (arg === "--report") {
|
|
904
1075
|
process.stdout.write(renderVerifyReport() + "\n");
|
|
905
1076
|
process.exit(0);
|
|
1077
|
+
} else if (arg === "--xray") {
|
|
1078
|
+
xrayLastSession({ transcriptPath: process.env.SKALPEL_VERIFY_TRANSCRIPT || null })
|
|
1079
|
+
.then(() => process.exit(0))
|
|
1080
|
+
.catch(() => process.exit(0));
|
|
906
1081
|
} else if (arg === "--run") {
|
|
907
1082
|
recordVerifyShadow({
|
|
908
1083
|
transcriptPath: process.env.SKALPEL_VERIFY_TRANSCRIPT || null,
|