skalpel 4.0.52 → 4.0.54
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/install.mjs +85 -18
- package/package.json +1 -1
- package/postinstall.mjs +13 -2
- package/skalpel-hook.mjs +26 -6
- package/skalpel-statusline.mjs +44 -7
package/install.mjs
CHANGED
|
@@ -195,9 +195,32 @@ function readJson(p) {
|
|
|
195
195
|
return null; // signal "do not write" to callers
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
// Abbreviate a home-rooted path to `~/…` for the user-facing repair hint (so it reads `~/.claude`,
|
|
199
|
+
// not a long absolute path).
|
|
200
|
+
function homeTilde(p) {
|
|
201
|
+
const h = homedir();
|
|
202
|
+
return p === h || p.startsWith(h + "/") || p.startsWith(h + "\\") ? "~" + p.slice(h.length) : p;
|
|
203
|
+
}
|
|
204
|
+
// Write a config file DEFENSIVELY. A read-only / permission-denied target — a root-owned ~/.claude
|
|
205
|
+
// left behind by an earlier `sudo` install, an MDM-locked home — must NOT throw and abort the whole
|
|
206
|
+
// installer (that would leave ZERO working hooks). Instead surface ONE clear repair line and return
|
|
207
|
+
// the OS error code so the caller can report `failed:<code>` and STILL attempt every other target.
|
|
208
|
+
// Returns null on success. mkdir is inside the try so a read-only PARENT dir is caught too, never
|
|
209
|
+
// left as an uncaught throw.
|
|
210
|
+
function safeWrite(p, data) {
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
213
|
+
writeFileSync(p, data);
|
|
214
|
+
return null;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error(
|
|
217
|
+
`skalpel: could not write ${homeTilde(p)}: ${e.message} — run: sudo chown -R $(whoami) ${homeTilde(dirname(p))}`,
|
|
218
|
+
);
|
|
219
|
+
return e.code || "write-failed";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
198
222
|
function writeJson(p, o) {
|
|
199
|
-
|
|
200
|
-
writeFileSync(p, JSON.stringify(o, null, 2));
|
|
223
|
+
return safeWrite(p, JSON.stringify(o, null, 2));
|
|
201
224
|
}
|
|
202
225
|
function isOurs(c, marker) {
|
|
203
226
|
if (typeof c !== "string") return false;
|
|
@@ -284,7 +307,10 @@ function claude() {
|
|
|
284
307
|
// — when there was nothing to do: an `uninstall` on a machine that never had our hooks leaves the
|
|
285
308
|
// file exactly as it was (or absent). Install always changes something here, so this only suppresses
|
|
286
309
|
// the no-op write.
|
|
287
|
-
if (installed || refreshed || removed)
|
|
310
|
+
if (installed || refreshed || removed) {
|
|
311
|
+
const err = writeJson(CLAUDE, d);
|
|
312
|
+
if (err) return `failed:${err}`; // read-only ~/.claude — reported, never thrown
|
|
313
|
+
}
|
|
288
314
|
if (uninstall) return removed ? "removed" : "absent";
|
|
289
315
|
return installed ? "installed" : refreshed ? "refreshed" : "absent";
|
|
290
316
|
}
|
|
@@ -404,14 +430,18 @@ function codexToml() {
|
|
|
404
430
|
// Only rewrite a config.toml that already existed — never CREATE one during an uninstall. A user
|
|
405
431
|
// with ~/.codex but no config.toml has nothing of ours to strip, so writing `next + "\n"` here
|
|
406
432
|
// would leave a stray 1-byte file behind an operation that's supposed to clean up.
|
|
407
|
-
if (hadToml)
|
|
433
|
+
if (hadToml) {
|
|
434
|
+
const err = safeWrite(CODEX_TOML, next + "\n");
|
|
435
|
+
if (err) return `failed:${err}`;
|
|
436
|
+
}
|
|
408
437
|
return stripped.removed ? "removed" : "absent";
|
|
409
438
|
}
|
|
410
439
|
mkdirSync(CODEX_DIR, { recursive: true });
|
|
411
440
|
for (const [event, marker, command] of HOOKS) {
|
|
412
441
|
next += "\n" + tomlBlock(event, marker, command);
|
|
413
442
|
}
|
|
414
|
-
|
|
443
|
+
const err = safeWrite(CODEX_TOML, next + "\n");
|
|
444
|
+
if (err) return `failed:${err}`;
|
|
415
445
|
return stripped.removed ? "refreshed" : "installed";
|
|
416
446
|
}
|
|
417
447
|
|
|
@@ -445,10 +475,14 @@ function codexJson() {
|
|
|
445
475
|
// one of our blocks from an existing file (mirrors codexToml's hadToml guard). A machine whose
|
|
446
476
|
// ~/.codex has no hooks.json (or none of ours) has nothing to clean up, so writing here would drop
|
|
447
477
|
// a stray file behind an operation that's supposed to leave the machine clean.
|
|
448
|
-
if (removed)
|
|
478
|
+
if (removed) {
|
|
479
|
+
const err = writeJson(CODEX, d);
|
|
480
|
+
if (err) return `failed:${err}`;
|
|
481
|
+
}
|
|
449
482
|
return removed ? "removed" : "absent";
|
|
450
483
|
}
|
|
451
|
-
writeJson(CODEX, d);
|
|
484
|
+
const err = writeJson(CODEX, d);
|
|
485
|
+
if (err) return `failed:${err}`;
|
|
452
486
|
return installed ? "installed" : refreshed ? "refreshed" : "absent";
|
|
453
487
|
}
|
|
454
488
|
|
|
@@ -505,14 +539,17 @@ function claudeMd() {
|
|
|
505
539
|
}
|
|
506
540
|
if (uninstall) {
|
|
507
541
|
if (!had) return "absent";
|
|
508
|
-
|
|
542
|
+
const err = safeWrite(CLAUDE_MD, stripped.trimEnd() + "\n");
|
|
543
|
+
if (err) return `failed:${err}`;
|
|
509
544
|
return "removed";
|
|
510
545
|
}
|
|
511
546
|
const next = stripped.trimEnd()
|
|
512
547
|
? `${stripped.trimEnd()}\n\n${CLAUDE_MD_BLOCK}\n`
|
|
513
548
|
: `${CLAUDE_MD_BLOCK}\n`;
|
|
514
|
-
|
|
515
|
-
|
|
549
|
+
// safeWrite mkdirs dirname(CLAUDE_MD) inside its try, so a read-only ~/.claude is caught here too
|
|
550
|
+
// (a bare mkdirSync would throw uncaught before the write).
|
|
551
|
+
const err = safeWrite(CLAUDE_MD, next);
|
|
552
|
+
if (err) return `failed:${err}`;
|
|
516
553
|
return had ? "refreshed" : "installed";
|
|
517
554
|
}
|
|
518
555
|
|
|
@@ -590,17 +627,33 @@ function cleanupLocalData() {
|
|
|
590
627
|
return cleaned;
|
|
591
628
|
}
|
|
592
629
|
|
|
630
|
+
// A target that could not be written returns `failed:<code>` (see safeWrite). Detect it so we never
|
|
631
|
+
// print a clean success over a partial wire.
|
|
632
|
+
const isFailure = (s) => typeof s === "string" && s.startsWith("failed:");
|
|
593
633
|
const claudeResult = claude();
|
|
594
634
|
if (statuslineOnly) {
|
|
595
635
|
console.log(`skalpel statusline (${STATUSLINE_CMD}) → claude: ${claudeResult}`);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
636
|
+
if (isFailure(claudeResult)) {
|
|
637
|
+
// Read-only ~/.claude (root-owned after a sudo install, MDM lock). Don't claim the status line
|
|
638
|
+
// installed; exit non-zero so postinstall's staging check surfaces its WARNING + repair path.
|
|
639
|
+
console.log(
|
|
640
|
+
"status line NOT installed — could not write ~/.claude/settings.json (see the error above). " +
|
|
641
|
+
"Fix the permissions shown, then run `skalpel setup`.",
|
|
642
|
+
);
|
|
643
|
+
process.exitCode = 1;
|
|
644
|
+
} else {
|
|
645
|
+
console.log(
|
|
646
|
+
claudeResult === "preserved"
|
|
647
|
+
? "custom Claude status line preserved."
|
|
648
|
+
: "installed. Behavior hooks activate automatically after `skalpel login`.",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
601
651
|
} else {
|
|
652
|
+
const claudeMdResult = claudeMd();
|
|
653
|
+
const codexTomlResult = codexToml();
|
|
654
|
+
const codexJsonResult = codexJson();
|
|
602
655
|
console.log(
|
|
603
|
-
`skalpel hook (${uninstall ? "uninstall" : CMD}) → claude: ${claudeResult} · claude.md: ${
|
|
656
|
+
`skalpel hook (${uninstall ? "uninstall" : CMD}) → claude: ${claudeResult} · claude.md: ${claudeMdResult} · codex(config.toml): ${codexTomlResult} · codex(hooks.json): ${codexJsonResult}`,
|
|
604
657
|
);
|
|
605
658
|
if (uninstall) {
|
|
606
659
|
const cleaned = cleanupLocalData();
|
|
@@ -614,8 +667,22 @@ if (statuslineOnly) {
|
|
|
614
667
|
}
|
|
615
668
|
console.log("uninstalled.");
|
|
616
669
|
} else {
|
|
617
|
-
|
|
618
|
-
|
|
670
|
+
// Downgrade the success summary if ANY target could not be written (read-only/permission-denied
|
|
671
|
+
// ~/.claude). One failed target never aborts the others — but never print a clean "installed."
|
|
672
|
+
// over a partial wire. When nothing failed, the success line below is byte-identical to before.
|
|
673
|
+
const failed = [claudeResult, claudeMdResult, codexTomlResult, codexJsonResult].filter(
|
|
674
|
+
isFailure,
|
|
619
675
|
);
|
|
676
|
+
if (failed.length) {
|
|
677
|
+
console.log(
|
|
678
|
+
`install incomplete — ${failed.length} target(s) could not be written (see the error line(s) above). ` +
|
|
679
|
+
"Fix the permissions shown, then run `skalpel setup`.",
|
|
680
|
+
);
|
|
681
|
+
process.exitCode = 1;
|
|
682
|
+
} else {
|
|
683
|
+
console.log(
|
|
684
|
+
`installed. Hooks run from ${HOOKS_DIR} and use the canonical skalpel Cognito login.`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
620
687
|
}
|
|
621
688
|
}
|
package/package.json
CHANGED
package/postinstall.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the user runs `skalpel` (login → full install), so a bare `npm i -g` never silently starts
|
|
7
7
|
// reading prompts.
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { rmSync } from "node:fs";
|
|
9
|
+
import { rmSync, existsSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join, dirname } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
@@ -93,7 +93,18 @@ function fixSudoOwnership() {
|
|
|
93
93
|
const uid = process.env.SUDO_UID;
|
|
94
94
|
if (!uid) return; // genuinely root (not via sudo) — don't touch their files
|
|
95
95
|
const gid = process.env.SUDO_GID || uid;
|
|
96
|
-
|
|
96
|
+
// Chown back EVERYTHING the sudo install may have created/written as root in the invoking user's
|
|
97
|
+
// HOME — not just ~/.skalpel. The `install --statusline-only` run above writes ~/.claude
|
|
98
|
+
// (settings.json / CLAUDE.md) and can touch ~/.codex; if those stay root-owned, the next non-root
|
|
99
|
+
// `skalpel` (or Claude Code itself) dies EACCES on them. Guard each with existsSync so we only
|
|
100
|
+
// chown what actually exists.
|
|
101
|
+
for (const dir of [
|
|
102
|
+
join(homedir(), ".skalpel"),
|
|
103
|
+
join(homedir(), ".claude"),
|
|
104
|
+
join(homedir(), ".codex"),
|
|
105
|
+
]) {
|
|
106
|
+
if (existsSync(dir)) spawnSync("chown", ["-R", `${uid}:${gid}`, dir], q);
|
|
107
|
+
}
|
|
97
108
|
} catch {
|
|
98
109
|
/* best-effort — worst case the user runs `sudo chown -R $(whoami) ~/.skalpel` themselves */
|
|
99
110
|
}
|
package/skalpel-hook.mjs
CHANGED
|
@@ -93,8 +93,18 @@ function isRealPrompt(q) {
|
|
|
93
93
|
// interrupt markers + rapid corrections ("no", "still wrong", "that's not it"). The felt "you've cut
|
|
94
94
|
// me off twice, let's reset" — the one steer you can't self-catch, and it's purely local + cheap.
|
|
95
95
|
const _CORRECTION =
|
|
96
|
-
/^(no\b|nope|that'?s not|not what|still\b|again\b|wrong\b|i said|i meant|wtf|tf\b|
|
|
97
|
-
|
|
96
|
+
/^(no\b|nope|that'?s not|not what|still\b|again\b|wrong\b|i said|i meant|wtf|tf\b|ugh|c'?mon|cmon|bruh)/i;
|
|
97
|
+
// A ^-anchored _CORRECTION match is only a REAL interrupt signal when the turn is a SHORT standalone
|
|
98
|
+
// reaction ("no", "nope wrong", "still broken") — NOT an ordinary debugging prompt whose prose merely
|
|
99
|
+
// OPENS with one of those words ("still seeing the error after the refresh because…", "why is the auth
|
|
100
|
+
// middleware rejecting valid tokens?"). Gating on shape (<= 4 words OR < 30 chars) is what stops the
|
|
101
|
+
// cascade interrupt from false-firing on healthy debugging turns. Explicit "[Request interrupted by
|
|
102
|
+
// user]" markers stay counted unconditionally (a hard signal) — this gate is only for prose corrections.
|
|
103
|
+
// NOTE: kept BYTE-IDENTICAL with skalpel-statusline.mjs's CORRECTION gate so the two never drift.
|
|
104
|
+
function isShortReaction(trimmed) {
|
|
105
|
+
return trimmed.length < 30 || trimmed.split(/\s+/).length <= 4;
|
|
106
|
+
}
|
|
107
|
+
export function detectCascade(payload) {
|
|
98
108
|
try {
|
|
99
109
|
if (!payload.transcript_path) return 0;
|
|
100
110
|
// Bounded tail read (256 KiB) — this runs EVERY turn, BEFORE the fetch deadline is armed, so an
|
|
@@ -123,7 +133,10 @@ function detectCascade(payload) {
|
|
|
123
133
|
if (/\[Request interrupted by user\]/i.test(text)) signals++; // you cut it off
|
|
124
134
|
if (e.type === "user" || e.role === "user") {
|
|
125
135
|
userTurns++;
|
|
126
|
-
|
|
136
|
+
const trimmed = text.trim();
|
|
137
|
+
// A correction counts only as a SHORT standalone reaction — not a long prompt that merely opens
|
|
138
|
+
// with "still"/"again"/"no"/… (ordinary debugging phrasing). See isShortReaction above.
|
|
139
|
+
if (isShortReaction(trimmed) && _CORRECTION.test(trimmed)) signals++; // you re-corrected
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
return signals;
|
|
@@ -376,7 +389,7 @@ export function updatePrefs(mutate) {
|
|
|
376
389
|
// captured natively through the prompt (no button UI needed; this IS the harness wrapping). Returns
|
|
377
390
|
// {action} or null. Deliberately narrow so normal prose never trips it: the word "skalpel", or a
|
|
378
391
|
// short standalone reaction right after a steer fired.
|
|
379
|
-
function parseControl(q) {
|
|
392
|
+
export function parseControl(q) {
|
|
380
393
|
const t = (q || "").trim().toLowerCase();
|
|
381
394
|
if (t.length > 60) return null; // a real coding prompt, not a reaction
|
|
382
395
|
const mentionsUs = /\bskalpel\b/.test(t);
|
|
@@ -407,12 +420,19 @@ function parseControl(q) {
|
|
|
407
420
|
pure("(?:fewer|less|stop) (?:of )?(?:these|this|that|like this|steers?)|fewer like this"))
|
|
408
421
|
)
|
|
409
422
|
return { action: "fewer" };
|
|
410
|
-
if (
|
|
423
|
+
if (
|
|
424
|
+
/\b(tell me more|more detail|expand|go deeper|more on this|explain the trap)\b/.test(t) &&
|
|
425
|
+
(mentionsUs || pure("tell me more|more detail|expand|go deeper|more on this|explain the trap"))
|
|
426
|
+
)
|
|
411
427
|
return { action: "more" };
|
|
412
428
|
if (
|
|
413
429
|
/\b(that('?s| was)? (helpful|useful|good|great)|nice catch|good (call|catch)|helped|love (this|that)|👍)\b/.test(
|
|
414
430
|
t,
|
|
415
|
-
)
|
|
431
|
+
) &&
|
|
432
|
+
(mentionsUs ||
|
|
433
|
+
pure(
|
|
434
|
+
"that(?:'?s| was)? (?:helpful|useful|good|great)|nice catch|good (?:call|catch)|helped|love (?:this|that)|👍",
|
|
435
|
+
))
|
|
416
436
|
)
|
|
417
437
|
return { action: "up" };
|
|
418
438
|
if (/\b(not (helpful|useful)|wrong|useless|👎|off base|missed)\b/.test(t) && mentionsUs)
|
package/skalpel-statusline.mjs
CHANGED
|
@@ -85,7 +85,17 @@ function readPayload() {
|
|
|
85
85
|
// "still wrong", "that's not it"). Mirrors the per-turn hook's own cascade detector so the number the
|
|
86
86
|
// bar shows is the same one the hook acts on — and it is a COUNT of real events, not a time estimate.
|
|
87
87
|
const CORRECTION =
|
|
88
|
-
/^(no\b|nope|that'?s not|not what|still\b|again\b|wrong\b|i said|i meant|wtf|tf\b|
|
|
88
|
+
/^(no\b|nope|that'?s not|not what|still\b|again\b|wrong\b|i said|i meant|wtf|tf\b|ugh|c'?mon|cmon|bruh)/i;
|
|
89
|
+
// A ^-anchored CORRECTION match is only a REAL interrupt signal when the turn is a SHORT standalone
|
|
90
|
+
// reaction ("no", "nope wrong", "still broken") — NOT an ordinary debugging prompt whose prose merely
|
|
91
|
+
// OPENS with one of those words ("still seeing the error after the refresh because…", "why is the auth
|
|
92
|
+
// middleware rejecting valid tokens?"). Gating on shape (<= 4 words OR < 30 chars) is what stops the
|
|
93
|
+
// re-ask count from false-firing on healthy debugging turns. Explicit "[Request interrupted by
|
|
94
|
+
// user]" markers stay counted unconditionally (a hard signal) — this gate is only for prose corrections.
|
|
95
|
+
// NOTE: kept BYTE-IDENTICAL with skalpel-hook.mjs's _CORRECTION gate so the two never drift.
|
|
96
|
+
function isShortReaction(trimmed) {
|
|
97
|
+
return trimmed.length < 30 || trimmed.split(/\s+/).length <= 4;
|
|
98
|
+
}
|
|
89
99
|
function reaskCount(transcriptPath) {
|
|
90
100
|
if (!transcriptPath) return 0;
|
|
91
101
|
const lines = tailLines(transcriptPath, 256 * 1024);
|
|
@@ -112,7 +122,10 @@ function reaskCount(transcriptPath) {
|
|
|
112
122
|
if (/\[Request interrupted by user\]/i.test(text)) signals++;
|
|
113
123
|
if (e.type === "user" || e.role === "user") {
|
|
114
124
|
userTurns++;
|
|
115
|
-
|
|
125
|
+
const trimmed = text.trim();
|
|
126
|
+
// A correction counts only as a SHORT standalone reaction — not a long prompt that merely opens
|
|
127
|
+
// with "still"/"again"/"no"/… (ordinary debugging phrasing). See isShortReaction above.
|
|
128
|
+
if (isShortReaction(trimmed) && CORRECTION.test(trimmed)) signals++;
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
return signals;
|
|
@@ -332,10 +345,10 @@ function compactMeasured(ms) {
|
|
|
332
345
|
// This always-on aggregate counter is an EXPLICIT PRODUCT DECISION by the owner: it is skalpel's terminal
|
|
333
346
|
// presence and must never be gated/hidden ("never remove it from the terminal otherwise skalpel is
|
|
334
347
|
// invisible"). Aggregate-only + no fabrication is what keeps that always-on surface honest.
|
|
335
|
-
function
|
|
348
|
+
function shadowTally() {
|
|
336
349
|
try {
|
|
337
350
|
const lines = tailLines(VERIFY_LOG_PATH, SHADOW_READ_BYTES);
|
|
338
|
-
if (!lines.length) return 0;
|
|
351
|
+
if (!lines.length) return { totalMs: 0, checked: 0, lies: 0 };
|
|
339
352
|
const rows = [];
|
|
340
353
|
for (const l of lines) {
|
|
341
354
|
try {
|
|
@@ -349,14 +362,25 @@ function measuredCaughtTotalMs() {
|
|
|
349
362
|
// O(falseCount × rows) that re-scanned every row for each catch on EVERY bar render.
|
|
350
363
|
const fixDeltaFor = buildFixDeltas(rows);
|
|
351
364
|
let total = 0;
|
|
365
|
+
// The HEARTBEAT counts (same single pass — zero extra I/O on the bar's hot path):
|
|
366
|
+
// checked — claims the shadow got a GENUINE verdict for (its own proof/probe really ran: PASS /
|
|
367
|
+
// GENUINE_FAIL / SHIP_OK / SHIP_FAIL). HARNESS_ERROR rows are logged but NOT counted —
|
|
368
|
+
// identical to the ledger's "unverifiable" rule: we never claim a check we can't stand
|
|
369
|
+
// behind. So a fresh install (or harness-noise-only log) keeps checked = 0.
|
|
370
|
+
// lies — the caught-false subset (mismatch === true), the money metric.
|
|
371
|
+
const CHECKED = new Set(["PASS", "GENUINE_FAIL", "SHIP_OK", "SHIP_FAIL"]);
|
|
372
|
+
let checked = 0;
|
|
373
|
+
let lies = 0;
|
|
352
374
|
for (const r of rows) {
|
|
375
|
+
if (CHECKED.has(r.outcome)) checked += 1;
|
|
353
376
|
if (r.mismatch !== true) continue; // only a caught false "done" (verify-shadow's money metric)
|
|
377
|
+
lies += 1;
|
|
354
378
|
const d = fixDeltaFor(r); // exact ledger.mjs logic; null-session rows excluded (Finding 3)
|
|
355
379
|
if (d && Number.isFinite(d.ms) && d.ms > 0) total += d.ms; // real measured delta only; else +0
|
|
356
380
|
}
|
|
357
|
-
return total;
|
|
381
|
+
return { totalMs: total, checked, lies };
|
|
358
382
|
} catch {
|
|
359
|
-
return 0
|
|
383
|
+
return { totalMs: 0, checked: 0, lies: 0 }; // fail-open: never break or slow the bar
|
|
360
384
|
}
|
|
361
385
|
}
|
|
362
386
|
|
|
@@ -422,10 +446,23 @@ function main() {
|
|
|
422
446
|
// • Raahil's `~`-prefixed saved ESTIMATE ("· ~7m saved") — shown only when saved_min > 0 (its data is
|
|
423
447
|
// the user's own steers.ndjson credit, not the shadow log, so it keeps his always-on #609 behavior).
|
|
424
448
|
const { steers, savedMin } = readState();
|
|
449
|
+
const tally = SHOW_MEASURED ? shadowTally() : { totalMs: 0, checked: 0, lies: 0 };
|
|
425
450
|
let line = `${BOLD}skalpel${RESET} · ${CYAN}${steers} steers${RESET}`;
|
|
426
451
|
if (SHOW_MEASURED) {
|
|
427
|
-
const caught = compactMeasured(
|
|
452
|
+
const caught = compactMeasured(tally.totalMs); // null when < 1s of real measured catches
|
|
428
453
|
if (caught) line += ` · ${CYAN}${caught} caught${RESET}`;
|
|
454
|
+
// THE HEARTBEAT (founder: "I only see 0 steers — the product is invisible while it works").
|
|
455
|
+
// Once the armed shadow has genuinely CHECKED at least one claim, the line SHOWS the work:
|
|
456
|
+
// clean so far → "· 3 claims verified" (the earned-silence receipt — real verdicts only)
|
|
457
|
+
// lies caught → "· 31 checked · 2 false" (the honest split; the measured clause carries the time)
|
|
458
|
+
// Real counts from the same single log pass. A fresh install (checked = 0) emits EXACTLY the
|
|
459
|
+
// baseline line — byte-identical, nothing invented.
|
|
460
|
+
if (tally.checked > 0) {
|
|
461
|
+
line +=
|
|
462
|
+
tally.lies > 0
|
|
463
|
+
? ` · ${CYAN}${tally.checked} checked · ${tally.lies} false${RESET}`
|
|
464
|
+
: ` · ${CYAN}${tally.checked} claim${tally.checked === 1 ? "" : "s"} verified${RESET}`;
|
|
465
|
+
}
|
|
429
466
|
}
|
|
430
467
|
if (SHOW_SAVED) {
|
|
431
468
|
const phrase = savedPhrase(savedMin);
|