skalpel 4.0.31 → 4.0.32

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/ledger.mjs ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ // ledger.mjs — the PERSONAL TRUTH LEDGER: `skalpel ledger`.
3
+ //
4
+ // THE PROBLEM: your agent is both the executor AND the reporter of its own work. Every "done / tests
5
+ // pass / fixed" it emits is a CLAIM. The claim-verification SHADOW (verify-shadow.mjs) already re-runs
6
+ // the session's OWN proof on each such claim and appends one honest row per claim to
7
+ // ~/.skalpel/verify-shadow.log. That file is already an APPEND-ONLY, LOCAL history of every
8
+ // claim -> verdict. What was missing was a VIEW: the running personal tally of how many "done"s were
9
+ // checked and how many were caught FALSE, with the receipts.
10
+ //
11
+ // THIS is that view. It DERIVES entirely from verify-shadow.log — it NEVER re-runs a proof, NEVER writes
12
+ // a second copy of the history (no double-write), NEVER touches the network, and NEVER touches the
13
+ // per-turn hook. It is a user-invoked, read-only subcommand: opt-in by construction, dark by default,
14
+ // byte-identical to before when not invoked.
15
+ //
16
+ // HARD RED LINES (why this is safe + honest):
17
+ // • DERIVED, NOT DOUBLE-WRITTEN. The append-only ledger IS verify-shadow.log (written by the shadow).
18
+ // This module only READS it and renders a view. It writes no ledger file of its own.
19
+ // • READ-ONLY + LOCAL. Imports only node: core + local sibling modules. Opens no socket. Re-runs nothing
20
+ // (the pass/fail is the verdict the shadow already recorded).
21
+ // • NO FABRICATED / ESTIMATED NUMBERS. Every count is a real tally over real logged rows. The ONLY time
22
+ // figure shown is a MEASURED wall-clock delta between two REAL logged timestamps — the moment a "done"
23
+ // was caught FALSE (mismatch row) and the moment that SAME proof, in that SAME session, was later
24
+ // logged PASS. If no such later PASS exists, we show the count ONLY — never an estimate, never a
25
+ // "minutes saved / lost" tally (that metric stays DEAD).
26
+ // • FAIL-OPEN. Any read/parse error yields an honest zero-state, never a throw at the user.
27
+ import { readFileSync } from "node:fs";
28
+ import { fileURLToPath } from "node:url";
29
+ import { realpathSync } from "node:fs";
30
+ // READ-ONLY reuse of the shadow's single-sourced log path + its honest fail-count extractor. Importing
31
+ // verify-shadow.mjs runs no side effects (its CLI block is guarded on isMain) and modifies nothing there.
32
+ import { VERIFY_LOG_PATH, extractFailCount } from "./verify-shadow.mjs";
33
+
34
+ // ---------- (1) read the append-only history (verify-shadow.log) ----------
35
+ // One NDJSON row per logged claim:
36
+ // { ts, session, claim_text, proof_command|null, pass_fail: "PASS"|"FAIL"|"HARNESS_ERROR"|null,
37
+ // outcome: "PASS"|"GENUINE_FAIL"|"HARNESS_ERROR", mismatch: boolean, evidence }
38
+ // The file is size-capped by the shadow (newest rows kept), so a plain read is safe.
39
+ export function readLedgerRows(path = VERIFY_LOG_PATH) {
40
+ let raw;
41
+ try {
42
+ raw = readFileSync(path, "utf8");
43
+ } catch {
44
+ return null; // no shadow log yet → honest zero-state (distinct from "log exists but empty")
45
+ }
46
+ const rows = [];
47
+ for (const line of raw.split("\n")) {
48
+ const l = line.trim();
49
+ if (!l) continue;
50
+ try {
51
+ const r = JSON.parse(l);
52
+ if (r && typeof r === "object") rows.push(r);
53
+ } catch {
54
+ /* a partial/garbage line — skip it (fail-open) */
55
+ }
56
+ }
57
+ return rows;
58
+ }
59
+
60
+ // ---------- (2) the MEASURED wall-clock delta (real timestamps only) ----------
61
+ // Parse an ISO timestamp to epoch ms, or null if it isn't a real parseable time.
62
+ function tsMs(ts) {
63
+ if (typeof ts !== "string") return null;
64
+ const n = Date.parse(ts);
65
+ return Number.isFinite(n) ? n : null;
66
+ }
67
+
68
+ // measureFixDelta(falseRow, rows) → { ms, atTs } for the EARLIEST later row that is, in the SAME session,
69
+ // the SAME proof_command, and a genuine PASS whose timestamp is strictly after the caught-false claim.
70
+ // That is the real moment the false "done" became a true "done": the exact proof that failed later passed.
71
+ // Returns null when no such row exists (then the ledger shows the count only — never an invented minute).
72
+ export function measureFixDelta(falseRow, rows) {
73
+ const t0 = tsMs(falseRow.ts);
74
+ if (t0 == null) return null;
75
+ const session = falseRow.session ?? null;
76
+ const proof = falseRow.proof_command || null;
77
+ if (!proof) return null; // can't tie a fix to a claim with no proof — count only
78
+ let best = null;
79
+ for (const r of rows) {
80
+ if (r === falseRow) continue;
81
+ if ((r.session ?? null) !== session) continue;
82
+ if ((r.proof_command || null) !== proof) continue;
83
+ if (r.outcome !== "PASS") continue; // the SAME proof genuinely passing = the real done
84
+ const t1 = tsMs(r.ts);
85
+ if (t1 == null || t1 <= t0) continue; // must be strictly LATER than the false claim
86
+ if (best == null || t1 < best) best = t1;
87
+ }
88
+ return best == null ? null : { ms: best - t0, atTs: new Date(best).toISOString() };
89
+ }
90
+
91
+ // ---------- (3) build the ledger view from the rows ----------
92
+ // verdict per row:
93
+ // "FALSE" — mismatch true: claimed done, its OWN proof re-run GENUINELY failed (a caught lie)
94
+ // "verified" — pass_fail PASS: claimed done, its OWN proof re-run really passed (a true done)
95
+ // "unverifiable" — no re-runnable proof, or HARNESS_ERROR (timeout/missing-script/wrong-cwd) — NEVER a lie
96
+ export function buildLedger(rows) {
97
+ const entries = (rows || []).map((r) => {
98
+ let verdict;
99
+ if (r.mismatch === true) verdict = "FALSE";
100
+ else if (r.pass_fail === "PASS") verdict = "verified";
101
+ else verdict = "unverifiable";
102
+ return {
103
+ ts: r.ts || null,
104
+ session: r.session ?? null,
105
+ claim_text: r.claim_text || "",
106
+ // Ship-status rows (category:"ship") record the check under probe_command, not proof_command —
107
+ // fall back so a caught ship-lie shows its real probe command, not an empty one.
108
+ proof_command: r.proof_command || r.probe_command || null,
109
+ evidence: r.evidence || "",
110
+ fail_count: extractFailCount(r.evidence),
111
+ verdict,
112
+ _row: r,
113
+ };
114
+ });
115
+
116
+ // "Checked" = claims we got a genuine re-run verdict for (a true PASS or a caught FALSE). Unverifiable
117
+ // rows (no proof / harness noise) are logged but NOT counted as checked — we can't stand behind them.
118
+ const checked = entries.filter((e) => e.verdict === "FALSE" || e.verdict === "verified").length;
119
+ const falseEntries = entries.filter((e) => e.verdict === "FALSE");
120
+ // Attach the MEASURED fix delta where — and only where — a real later same-proof PASS exists.
121
+ for (const e of falseEntries) {
122
+ const d = measureFixDelta(e._row, rows || []);
123
+ e.fix_delta_ms = d ? d.ms : null;
124
+ e.fixed_at = d ? d.atTs : null;
125
+ }
126
+ return {
127
+ total: entries.length,
128
+ checked,
129
+ falseCount: falseEntries.length,
130
+ verifiedCount: entries.filter((e) => e.verdict === "verified").length,
131
+ unverifiableCount: entries.filter((e) => e.verdict === "unverifiable").length,
132
+ entries,
133
+ falseEntries,
134
+ };
135
+ }
136
+
137
+ // ---------- (4) render ----------
138
+ function clip(s, n) {
139
+ const t = String(s || "")
140
+ .replace(/\s+/g, " ")
141
+ .trim();
142
+ return t.length > n ? t.slice(0, n - 1) + "…" : t;
143
+ }
144
+
145
+ // Format a MEASURED millisecond delta as a human duration. Input is a real (t_pass - t_false) delta; this
146
+ // only formats it — it never rounds up into an invented number. Sub-second deltas read as "<1s".
147
+ export function formatDuration(ms) {
148
+ if (!Number.isFinite(ms) || ms < 0) return null;
149
+ const s = Math.floor(ms / 1000);
150
+ if (s < 1) return "<1s";
151
+ const h = Math.floor(s / 3600);
152
+ const m = Math.floor((s % 3600) / 60);
153
+ const sec = s % 60;
154
+ if (h > 0) return `${h}h ${m}m`;
155
+ if (m > 0) return `${m}m ${sec}s`;
156
+ return `${sec}s`;
157
+ }
158
+
159
+ const MAX_RECEIPTS = 5;
160
+
161
+ export function renderLedger(ledger, { logPath = VERIFY_LOG_PATH } = {}) {
162
+ const L = [];
163
+ L.push("");
164
+ L.push(
165
+ ' 🔬 skalpel — personal truth ledger (every "done" your agent claimed, checked by its own proof)',
166
+ );
167
+ L.push(" " + "─".repeat(74));
168
+
169
+ if (ledger == null) {
170
+ L.push("");
171
+ L.push(" No claim-verification history yet on this machine.");
172
+ L.push(
173
+ " Enable the shadow with SKALPEL_VERIFY_SHADOW=1, then keep coding — every turn your agent",
174
+ );
175
+ L.push(' claims "done / tests pass" gets its own proof re-run and appended to the ledger.');
176
+ L.push("");
177
+ return L.join("\n");
178
+ }
179
+
180
+ const { checked, falseCount } = ledger;
181
+ L.push("");
182
+ // THE HEADLINE — both numbers are real counts over the real append-only log.
183
+ L.push(
184
+ ` ${checked} done-claim${checked === 1 ? "" : "s"} checked, ${falseCount} verified FALSE.`,
185
+ );
186
+
187
+ if (ledger.total === 0) {
188
+ L.push("");
189
+ L.push(" The ledger is empty — no completion claims logged yet. I'll append every one live.");
190
+ L.push("");
191
+ return L.join("\n");
192
+ }
193
+
194
+ if (falseCount === 0) {
195
+ L.push("");
196
+ L.push(' No caught lies — every checked "done" held up against its own proof. Clean ledger.');
197
+ if (ledger.unverifiableCount > 0) {
198
+ L.push(
199
+ ` (${ledger.unverifiableCount} claim(s) logged but unverifiable — no re-runnable proof or harness noise; not counted.)`,
200
+ );
201
+ }
202
+ L.push("");
203
+ return L.join("\n");
204
+ }
205
+
206
+ L.push("");
207
+ L.push(
208
+ " Every FALSE below is real — the agent's own proof, re-run, did NOT pass at claim time:",
209
+ );
210
+ L.push("");
211
+
212
+ const top = ledger.falseEntries.slice(-MAX_RECEIPTS).reverse();
213
+ top.forEach((e, i) => {
214
+ const failN = e.fail_count != null ? ` (${e.fail_count} failing)` : "";
215
+ L.push(` ${i + 1}. your agent said:`);
216
+ L.push(` "${clip(e.claim_text, 72)}"`);
217
+ L.push(` but its own \`${clip(e.proof_command, 64)}\` had just FAILED${failN}:`);
218
+ if (e.evidence) L.push(` ${clip(e.evidence, 72)}`);
219
+ // MEASURED time — shown ONLY when a real later same-proof PASS exists in the log. Never estimated.
220
+ if (e.fix_delta_ms != null) {
221
+ const dur = formatDuration(e.fix_delta_ms);
222
+ if (dur)
223
+ L.push(
224
+ ` ⏱ ${dur} later, that same proof was logged PASS (measured, real timestamps).`,
225
+ );
226
+ } else {
227
+ L.push(" ⏱ no later PASS logged for this proof yet — count only, no time estimated.");
228
+ }
229
+ L.push("");
230
+ });
231
+
232
+ if (falseCount > top.length)
233
+ L.push(` …and ${falseCount - top.length} more FALSE in the ledger.`);
234
+ if (ledger.unverifiableCount > 0) {
235
+ L.push(
236
+ ` (${ledger.unverifiableCount} claim(s) logged but unverifiable — no re-runnable proof or harness noise; not counted as checked.)`,
237
+ );
238
+ }
239
+ L.push(" " + "─".repeat(74));
240
+ L.push(` Append-only local history: ${logPath}`);
241
+ L.push("");
242
+ return L.join("\n");
243
+ }
244
+
245
+ // ---------- CLI ----------
246
+ const isMain = (() => {
247
+ try {
248
+ return (
249
+ Boolean(process.argv[1]) &&
250
+ realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
251
+ );
252
+ } catch {
253
+ return false;
254
+ }
255
+ })();
256
+
257
+ if (isMain) {
258
+ try {
259
+ // Optional override so the verifier can point the ledger at a crafted log (tests / self-check).
260
+ const logPath = process.env.SKALPEL_LEDGER_LOG || VERIFY_LOG_PATH;
261
+ const rows = readLedgerRows(logPath);
262
+ const ledger = rows == null ? null : buildLedger(rows);
263
+ process.stdout.write(renderLedger(ledger, { logPath }) + "\n");
264
+ } catch {
265
+ // READ-ONLY + fail-open: never throw at the user.
266
+ process.stdout.write(
267
+ "\n 🔬 skalpel — could not read your claim-verification ledger on this machine.\n\n",
268
+ );
269
+ }
270
+ process.exit(0);
271
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.31",
3
+ "version": "4.0.32",
4
4
  "description": "Behavioral graph for AI-assisted coding — learns how you work and steers Claude Code + Codex in real time.",
5
5
  "type": "module",
6
6
  "bin": {
package/skalpel-setup.mjs CHANGED
@@ -63,6 +63,7 @@ const KNOWN_SUBS = new Set([
63
63
  "xray",
64
64
  "catches",
65
65
  "card",
66
+ "ledger",
66
67
  "__build",
67
68
  "__verify-report",
68
69
  ]);
@@ -79,6 +80,7 @@ usage:
79
80
  skalpel xray re-run your last session's own proof — did "done" hold up?
80
81
  skalpel catches scan your own history for "done" claims over failing tests
81
82
  skalpel card [--last|--id <id>] [--md] share one verified catch: the claim + its own failing proof
83
+ skalpel ledger your running tally: N "done"s checked, M verified FALSE
82
84
  skalpel uninstall [--purge] remove the hooks + local data from this machine
83
85
 
84
86
  options:
@@ -896,6 +898,14 @@ async function main() {
896
898
  spawnSync("node", [join(__dir, "card.mjs"), ...argv.slice(1)], { stdio: "inherit" });
897
899
  return;
898
900
  }
901
+ // `skalpel ledger` — the PERSONAL TRUTH LEDGER: a LOCAL, READ-ONLY view DERIVED from the append-only
902
+ // verify-shadow.log — your running tally of "done" claims checked and how many were verified FALSE,
903
+ // with the receipts and (only where a real red->green timestamp delta exists) the measured time.
904
+ // Never re-runs a proof, never touches the network, never estimates a number.
905
+ if (sub === "ledger") {
906
+ spawnSync("node", [join(__dir, "ledger.mjs"), ...argv.slice(1)], { stdio: "inherit" });
907
+ return;
908
+ }
899
909
  // `skalpel __verify-report` — the claim-verification SHADOW instrument: reads the local
900
910
  // verify-shadow.log and prints the three numbers (claim fire-rate, proof-reconstruction rate, and the
901
911
  // headline MISMATCH rate = how often the agent claimed done but its OWN proof, re-run, failed).