skalpel 4.0.34 → 4.0.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.34",
3
+ "version": "4.0.35",
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-hook.mjs CHANGED
@@ -146,6 +146,43 @@ function writePending(obj) {
146
146
  }
147
147
  }
148
148
 
149
+ // ALWAYS-ON PRESENCE tally — the honest lifetime record the statusline reads for its baseline line.
150
+ // Concept is Raahil's (#609): steers = count of steers ACTUALLY applied (fired AND not suppressed) across
151
+ // all sessions; saved_min = Σ, over those applied steers, of the user's OWN median cost of one loop of
152
+ // that steer's work-type (the server's `median_loop_min`, from the causal graph's sink/loop stats). It is
153
+ // NEVER a server wall-clock estimate; when the server has no median (null) we credit NOTHING.
154
+ //
155
+ // RACE-FREE by construction (audit Finding 2 — a read-modify-write single-counter loses increments under
156
+ // concurrent hook double-fires; metrics.mjs's own comment warns the same). So this is an APPEND-ONLY log:
157
+ // ONE atomic `appendFileSync` line per applied steer. POSIX guarantees a single small append is atomic, so
158
+ // N concurrent applied-steer writes yield exactly N lines (0 loss). No read, no tmp, no rename, no mutated
159
+ // counter. Same idiom as metrics.ndjson / insights.ndjson.
160
+ //
161
+ // ── STEERS SOURCE CONTRACT (the source of truth for the statusline baseline AND #620's analytics TUI) ──
162
+ // file : ~/.skalpel/steers.ndjson (append-only; one line per APPLIED steer; never rewritten/trimmed)
163
+ // line : {"ts": <epoch ms>, "saved_min_delta": <number ≥ 0>}
164
+ // ts = Date.now() when the steer was applied.
165
+ // saved_min_delta = the median_loop_min credited for THIS steer (≥ 0; 0 when the server sent
166
+ // no positive median — never fabricated).
167
+ // derive: steers = count of valid JSON lines (one line == one applied steer).
168
+ // saved_min = Σ saved_min_delta over valid lines (round to 1 dp; garbage/partial lines skipped).
169
+ // Readers MUST fail open (unreadable/absent → {steers:0, saved_min:0}) and MUST NOT mutate this file.
170
+ const STATE_LOG_PATH = join(homedir(), ".skalpel", "steers.ndjson");
171
+ function bumpState(medianMin) {
172
+ try {
173
+ // credit ONLY a real, positive median from the server; null/NaN/<=0 → 0 credit (no fabrication).
174
+ const savedMinDelta = Number.isFinite(medianMin) && medianMin > 0 ? medianMin : 0;
175
+ mkdirSync(join(homedir(), ".skalpel"), { recursive: true });
176
+ // ONE atomic append == ONE applied steer. Race-free (never a read-modify-write of a counter).
177
+ appendFileSync(
178
+ STATE_LOG_PATH,
179
+ JSON.stringify({ ts: Date.now(), saved_min_delta: savedMinDelta }) + "\n",
180
+ );
181
+ } catch {
182
+ /* the presence tally is a bonus; never block the hook */
183
+ }
184
+ }
185
+
149
186
  // STEER label — the statusline reads this so the user SEES skalpel steering LIVE, every turn a steer
150
187
  // injects, INDEPENDENT of whether the model surfaces a 🔬 line (the deterministic awareness channel).
151
188
  // A one-glance 2-word label derived from the top injected steer type. Overwritten each turn.
@@ -559,6 +596,7 @@ async function main() {
559
596
  let steerType = null; // raw top-steer type (TELL/SINK/…) — for the feedback loop + session log
560
597
  let steerWt = null; // the work-type the steer is about (refine/debug/…)
561
598
  let brokeOut = false; // behavioral-only: did expected-waste-ahead drop after last turn's steer (adherence)
599
+ let medianLoopMin = null; // server's per-work-type median loop cost (min) — the ONLY input to "saved"
562
600
 
563
601
  if (!authNote) {
564
602
  const remaining = deadline - Date.now();
@@ -596,6 +634,14 @@ async function main() {
596
634
  steerLabel = j?.steer_label; // surfaced on the statusline (written once below, with any credit)
597
635
  steerType = j?.steer_type || null;
598
636
  steerWt = j?.steer_wt || (j?.entry ? String(j.entry).split("/")[0] : null);
637
+ // the user's OWN median cost of one loop of this steer's work-type (from the graph's sink stats).
638
+ // The ONLY quantity we ever turn into "saved" minutes, and only for an APPLIED steer (below).
639
+ medianLoopMin =
640
+ typeof j?.median_loop_min === "number" &&
641
+ isFinite(j.median_loop_min) &&
642
+ j.median_loop_min > 0
643
+ ? j.median_loop_min
644
+ : null;
599
645
  if (j?.entry) pushTraj(String(j.entry).split("/")[0]); // remember this turn's work-type
600
646
  dbg(` -> sim=${j?.top_sim ?? "?"} ${injection ? "INJECT" : "silent"}`);
601
647
 
@@ -693,6 +739,9 @@ async function main() {
693
739
  const steerId = injection ? String(Date.now()) : null;
694
740
  writeSteer(steerLabel, steerType, steerWt, steerId);
695
741
  if (injection) logSessionEvent({ type: steerType, wt: steerWt }); // feed the end-of-session recap
742
+ // ALWAYS-ON presence tally: a steer was APPLIED this turn (fired + survived suppression). Count it,
743
+ // and credit its honest median-loop cost ONLY when the server sent a real number (null -> +0).
744
+ if (injection) bumpState(medianLoopMin);
696
745
 
697
746
  // LOCAL INSIGHT RECORDS (~/.skalpel/insights.ndjson) — one plain-English row per note kind that
698
747
  // fired this turn, post-suppression (a muted/suppressed steer writes nothing). recordInsight
@@ -1,12 +1,31 @@
1
1
  #!/usr/bin/env node
2
- // skalpel statusline — COUNT-OR-NOTHING. Prints ONLY facts that reproduce from local state, never an
3
- // estimate dressed up as a measurement. There is no "~Xh saved" and no "+min this turn": those were a
4
- // SERVER estimate of expected-waste (never a measured clock), so they are gone. What can honestly show:
2
+ // skalpel statusline — HONEST-OR-NOTHING. Prints only facts grounded in local state, never a server
3
+ // estimate dressed up as a measurement. In priority order (highest first):
4
+ // the AHA CATCH reveal agent claimed done, its OWN proof re-run GENUINELY failed (opt-in,
5
+ // SKALPEL_VERIFY_REVEAL; dark by default → byte-identical to the always-on baseline when off); else
6
+ // • the green "✓ verified" stamp — that same proof re-run really PASSED (same opt-in flag); else
5
7
  // • the live re-ask/interrupt COUNT for THIS session, read straight from the transcript Claude Code
6
8
  // hands us on stdin — the exact material `skalpel autopsy` re-derives from disk, a count of things
7
- // that verifiably happened, or
8
- // • the current steer's plain-English label (what skalpel is doing now) — a description, not a number.
9
- // A clean session prints NOTHING. Fail-open: any error → print nothing, exit 0. Never blocks the bar.
9
+ // that verifiably happened; else
10
+ // • the current steer's plain-English label (what skalpel is doing now) — a description, not a number;
11
+ // else
12
+ // • the ALWAYS-ON presence line, "skalpel · N steers[ · <time clause>]".
13
+ // N = steers ACTUALLY applied (fired + not suppressed), counted from the append-only
14
+ // ~/.skalpel/steers.ndjson — a fresh install reads "0 steers", present, never blank. The optional
15
+ // TIME clause is chosen by BASELINE_TIME_MODE (Ryan flips one constant):
16
+ // · <Xm> caught — the CUMULATIVE LIFETIME sum of every real MEASURED red→green delta (each caught
17
+ // false "done" and the moment that SAME proof, that SAME session, was later logged PASS — real
18
+ // timestamps; reuses ledger.mjs's measureFixDelta logic). ALWAYS-ON once real catches exist (it is
19
+ // skalpel's terminal presence — never gated/hidden). A measured total, NEVER an estimate; a catch
20
+ // with no later PASS adds 0. Cumulative framing (not present-tense) keeps it honest for old data.
21
+ // · ~Xm saved — Raahil's #609 estimate: (applied steers) × (the user's OWN median loop cost from
22
+ // their causal graph). A `~`-prefixed ESTIMATE grounded in their own data, shown only when
23
+ // saved_min > 0. Never a server wall-clock number; never "skalpel saved you X" as a fact.
24
+ // Every value here is a real count or a real measured timestamp delta (or a clearly `~`-marked estimate).
25
+ // NOTE: this always-on presence line is a DELIBERATE new behavior — origin/main's clean-idle bar emitted
26
+ // ZERO bytes; this emits "skalpel · 0 steers" (Ryan: always-on presence, non-negotiable). Byte-identical
27
+ // to origin/main is preserved ONLY for the untouched reveal / verdict / re-ask surfaces, not clean-idle.
28
+ // Fail-open: any error → print nothing (or just the presence line), exit 0. Never blocks the bar.
10
29
  import { readFileSync, rmSync } from "node:fs";
11
30
  import { homedir } from "node:os";
12
31
  import { join } from "node:path";
@@ -19,16 +38,34 @@ const BOLD = "\x1b[1m";
19
38
  const DIM = "\x1b[2m";
20
39
  const RESET = "\x1b[0m";
21
40
 
41
+ // ── RENDER MODE — the ONE constant Ryan flips. Governs the optional TIME clause on the always-on
42
+ // `skalpel · N steers` baseline. The steers count + Raahil's #609 BOLD/CYAN styling are ALWAYS present,
43
+ // unchanged, in every mode. Nothing else on the bar (reveal / verdict / re-ask / steer label) is affected.
44
+ // "steers" → skalpel · N steers
45
+ // "measured" → skalpel · N steers · 38m caught (DEFAULT — cumulative lifetime MEASURED red→green)
46
+ // "saved" → skalpel · N steers · ~7m saved (Raahil's own-graph ESTIMATE, from steers.ndjson)
47
+ // "both" → skalpel · N steers · 38m caught · ~7m saved
48
+ const BASELINE_TIME_MODE = "measured";
49
+ const SHOW_MEASURED = BASELINE_TIME_MODE === "measured" || BASELINE_TIME_MODE === "both";
50
+ const SHOW_SAVED = BASELINE_TIME_MODE === "saved" || BASELINE_TIME_MODE === "both";
51
+
22
52
  // AHA CATCH REVEAL — how long a fresh catch stays on the bar before it auto-clears (so it never
23
53
  // wallpapers). verify-shadow.mjs writes the reveal the moment a GENUINE_FAIL mismatch is re-run; it also
24
54
  // retracts it if a later proof re-run PASSES. This TTL covers the abandoned case (user moved on).
25
55
  const REVEAL_TTL_MS = 90_000;
26
56
  // LIVE VERDICT green stamp — the earned "✓ verified" written on a clean PASS. DELIBERATELY short-lived:
27
57
  // 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.
58
+ // never competes with the loud red catch. Short TTL is the whole point — a green that lingers is clutter.
30
59
  const VERDICT_TTL_MS = 20_000;
31
60
 
61
+ // The verify-shadow log the measured clause reads. Defined LOCALLY (not imported from verify-shadow.mjs)
62
+ // on purpose: this runs on EVERY bar render, so it must not pull that module's heavy (child_process /
63
+ // probe) import graph onto the hot path — it only READS the file verify-shadow.mjs owns. verify-shadow.mjs
64
+ // self-trims the log to ~1MB/500 lines, so this bounded tail reads the whole retained log in practice
65
+ // while staying safe against a pathological huge file (a bounded read can only UNDERCOUNT, never overstate).
66
+ const VERIFY_LOG_PATH = join(homedir(), ".skalpel", "verify-shadow.log");
67
+ const SHADOW_READ_BYTES = 1024 * 1024;
68
+
32
69
  // Claude Code pipes a JSON status payload on stdin (transcript_path, session_id, …). Read it ONLY when
33
70
  // stdin is actually piped — never block on a TTY (manual invocation) where reading fd 0 would hang.
34
71
  function readPayload() {
@@ -101,7 +138,7 @@ function steerLabel() {
101
138
  // The opt-in AHA CATCH: read the reveal state file verify-shadow.mjs wrote. Returns the plain reveal
102
139
  // line (fresh + honest — the statusline never invents it, it only renders what the re-run recorded) or
103
140
  // null. Auto-clears when stale (past the TTL) so an old catch can't wallpaper the bar. GATED entirely on
104
- // SKALPEL_VERIFY_REVEAL: when off, this is never called and the bar is byte-identical to the original.
141
+ // SKALPEL_VERIFY_REVEAL: when off, this is never called and the bar is byte-identical to the baseline.
105
142
  function revealNote() {
106
143
  try {
107
144
  const r = JSON.parse(readFileSync(REVEAL_PATH, "utf8"));
@@ -125,7 +162,7 @@ function revealNote() {
125
162
  // The LIVE VERDICT green stamp: read the verdict state verify-shadow.mjs wrote on a clean PASS. Returns
126
163
  // the plain "✓ verified" line (fresh + honest — the statusline never invents it, it only renders what the
127
164
  // 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.
165
+ // on SKALPEL_VERIFY_REVEAL: when off, this is never called and the bar is byte-identical to the baseline.
129
166
  function verdictNote() {
130
167
  try {
131
168
  const r = JSON.parse(readFileSync(VERDICT_PATH, "utf8"));
@@ -146,21 +183,148 @@ function verdictNote() {
146
183
  }
147
184
  }
148
185
 
149
- async function main() {
186
+ // The always-on presence tally — derived from ~/.skalpel/steers.ndjson, the APPEND-ONLY log the hook
187
+ // writes (one atomic line per APPLIED steer; see the STEERS SOURCE CONTRACT in skalpel-hook.mjs). This is
188
+ // the RACE-FREE replacement for the old mutated state.json counter (audit Finding 2): steers = the count
189
+ // of valid lines, saved_min = Σ saved_min_delta. Defaults to zeros so the baseline line is never blank;
190
+ // garbage/partial lines are skipped; the sum only ever adds ≥0 deltas, so it is honest and never negative.
191
+ function readState() {
192
+ try {
193
+ const raw = readFileSync(join(homedir(), ".skalpel", "steers.ndjson"), "utf8");
194
+ let steers = 0;
195
+ let savedMin = 0;
196
+ for (const line of raw.split("\n")) {
197
+ const l = line.trim();
198
+ if (!l) continue;
199
+ let rec;
200
+ try {
201
+ rec = JSON.parse(l);
202
+ } catch {
203
+ continue; // a partial/garbage line — skip it (fail-open), keep counting the rest
204
+ }
205
+ if (!rec || typeof rec !== "object") continue;
206
+ steers += 1; // one valid line == one applied steer (append-only ⇒ exact, race-free count)
207
+ if (Number.isFinite(rec.saved_min_delta) && rec.saved_min_delta > 0)
208
+ savedMin += rec.saved_min_delta;
209
+ }
210
+ return { steers, savedMin: Math.round(savedMin * 10) / 10 };
211
+ } catch {
212
+ return { steers: 0, savedMin: 0 }; // absent/unreadable → honest zero-state (fail-open)
213
+ }
214
+ }
215
+
216
+ // The `~`-prefixed saved ESTIMATE, or null when there's no real credit (never show a fake/zero number).
217
+ // Minutes below an hour render as ~Xm; an hour or more as ~Xh (one decimal, trimmed of a trailing .0).
218
+ // [Raahil #609 — preserved verbatim so the "saved"/"both" modes match his line byte-for-byte.]
219
+ function savedPhrase(min) {
220
+ if (!(min > 0)) return null;
221
+ if (min >= 60) {
222
+ const h = min / 60;
223
+ const s = h >= 10 ? String(Math.round(h)) : h.toFixed(1).replace(/\.0$/, "");
224
+ return `~${s}h saved`;
225
+ }
226
+ return `~${Math.round(min)}m saved`;
227
+ }
228
+
229
+ // ── MEASURED red→green delta (real timestamps only — the honest replacement for the banned "~Xh saved").
230
+ // tsMs + measureFixDelta are the EXACT logic from ledger.mjs, replicated here (not imported) so the hot
231
+ // statusline path never pulls verify-shadow.mjs's heavy graph. Parse an ISO ts to epoch ms, or null.
232
+ function tsMs(ts) {
233
+ if (typeof ts !== "string") return null;
234
+ const n = Date.parse(ts);
235
+ return Number.isFinite(n) ? n : null;
236
+ }
237
+
238
+ // measureFixDelta(falseRow, rows) → { ms, atTs } for the EARLIEST later row that is, in the SAME session,
239
+ // the SAME proof_command, and a genuine PASS whose timestamp is strictly after the caught-false claim.
240
+ // That is the real moment the false "done" became a true "done": the exact proof that failed later passed.
241
+ // Returns null when no such row exists (then we show NO time — never an invented minute). [ledger.mjs]
242
+ function measureFixDelta(falseRow, rows) {
243
+ const t0 = tsMs(falseRow.ts);
244
+ if (t0 == null) return null;
245
+ const session = falseRow.session ?? null;
246
+ // Finding 3: a REAL session id is required to tie a fix to a claim. With a null session, the
247
+ // `(r.session ?? null) !== session` guard below would treat every OTHER null-session row as
248
+ // "same session" and cross-attribute a bogus delta — so refuse to measure when session is null.
249
+ if (session == null) return null;
250
+ const proof = falseRow.proof_command || null;
251
+ if (!proof) return null; // can't tie a fix to a claim with no proof — count only
252
+ let best = null;
253
+ for (const r of rows) {
254
+ if (r === falseRow) continue;
255
+ if ((r.session ?? null) !== session) continue;
256
+ if ((r.proof_command || null) !== proof) continue;
257
+ if (r.outcome !== "PASS") continue; // the SAME proof genuinely passing = the real done
258
+ const t1 = tsMs(r.ts);
259
+ if (t1 == null || t1 <= t0) continue; // must be strictly LATER than the false claim
260
+ if (best == null || t1 < best) best = t1;
261
+ }
262
+ return best == null ? null : { ms: best - t0, atTs: new Date(best).toISOString() };
263
+ }
264
+
265
+ // Compact one-line duration for the bar. Input is a real (t_pass − t_false) MEASURED delta; this only
266
+ // formats it. FLOORS every unit so it can never round UP into a number larger than the real measurement.
267
+ // Sub-second deltas → null (nothing worth a clause). Seconds < 1m; whole minutes < 1h; else hours.
268
+ function compactMeasured(ms) {
269
+ if (!Number.isFinite(ms) || ms < 1000) return null;
270
+ const s = Math.floor(ms / 1000);
271
+ if (s < 60) return `${s}s`;
272
+ const m = Math.floor(s / 60);
273
+ if (m < 60) return `${m}m`;
274
+ const h = s / 3600;
275
+ return h >= 10
276
+ ? `${Math.floor(h)}h`
277
+ : `${(Math.floor(h * 10) / 10).toFixed(1).replace(/\.0$/, "")}h`;
278
+ }
279
+
280
+ // measuredCaughtTotalMs() → the CUMULATIVE LIFETIME sum, in ms, of EVERY real measured red→green delta:
281
+ // each caught false "done" (mismatch row) that has a later same-session same-proof PASS contributes that
282
+ // one measured (t_pass − t_false) delta; a catch with no later PASS contributes 0 (never estimated). This
283
+ // is an AGGREGATE — only the total number is ever rendered, never any per-catch claim text or proof (those
284
+ // stay in the opt-in reveal + the analytics TUI). Framed on the bar as a cumulative "Xm caught", NOT a
285
+ // present-tense "just caught", so it stays honest for old catches — the TOTAL genuinely happened.
286
+ //
287
+ // This always-on aggregate counter is an EXPLICIT PRODUCT DECISION by the owner: it is skalpel's terminal
288
+ // presence and must never be gated/hidden ("never remove it from the terminal otherwise skalpel is
289
+ // invisible"). Aggregate-only + no fabrication is what keeps that always-on surface honest.
290
+ function measuredCaughtTotalMs() {
291
+ try {
292
+ const lines = tailLines(VERIFY_LOG_PATH, SHADOW_READ_BYTES);
293
+ if (!lines.length) return 0;
294
+ const rows = [];
295
+ for (const l of lines) {
296
+ try {
297
+ const r = JSON.parse(l);
298
+ if (r && typeof r === "object") rows.push(r);
299
+ } catch {
300
+ /* partial / garbage line — skip it, keep the rest (fail-open) */
301
+ }
302
+ }
303
+ let total = 0;
304
+ for (const r of rows) {
305
+ if (r.mismatch !== true) continue; // only a caught false "done" (verify-shadow's money metric)
306
+ const d = measureFixDelta(r, rows); // exact ledger.mjs logic; null-session rows excluded (Finding 3)
307
+ if (d && Number.isFinite(d.ms) && d.ms > 0) total += d.ms; // real measured delta only; else +0
308
+ }
309
+ return total;
310
+ } catch {
311
+ return 0; // any error → no measured total (fail-open: never break or slow the bar)
312
+ }
313
+ }
314
+
315
+ function main() {
150
316
  const payload = readPayload();
151
317
 
152
- // AHA CATCH REVEAL (opt-in). The single highest-priority line: the agent claimed done but its OWN proof,
153
- // re-run, GENUINELY failed. Fully gated on SKALPEL_VERIFY_REVEAL when off, this branch never runs and
154
- // the statusline output is byte-identical to the count-or-nothing original.
318
+ // AHA CATCH REVEAL / green ✓ verified (opt-in). The highest-priority surfaces: the agent claimed done
319
+ // and its OWN proof, re-run, GENUINELY failed (red)or really passed (green). Fully gated on
320
+ // SKALPEL_VERIFY_REVEAL — when off (the default) this branch never runs and the bar is byte-identical to
321
+ // the always-on baseline below.
155
322
  if (revealEnabled(process.env)) {
156
323
  const reveal = revealNote();
157
324
  if (reveal) {
158
325
  process.stdout.write(`${BOLD}${reveal}${RESET}`);
159
326
  return;
160
327
  }
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
328
  const verdict = verdictNote();
165
329
  if (verdict) {
166
330
  process.stdout.write(`${DIM}${GREEN}${verdict}${RESET}`);
@@ -178,17 +342,40 @@ async function main() {
178
342
  return;
179
343
  }
180
344
 
181
- // Otherwise, if a steer is live, name what it's doing — a description, no number. Else: print NOTHING.
345
+ // Otherwise, if a steer is live, name what it's doing — a description, no number.
182
346
  const label = steerLabel();
183
347
  if (label) {
184
348
  process.stdout.write(`${BOLD}skalpel${RESET} · ${CYAN}${label}${RESET}`);
185
349
  return;
186
350
  }
187
- // Clean session → COUNT-OR-NOTHING resolves to nothing. Emit no bytes.
351
+
352
+ // ALWAYS-ON baseline presence line — skalpel's terminal presence (owner product decision: never gated,
353
+ // never hidden). `N steers` is ALWAYS shown (fresh install reads "0 steers" — honest, present, never
354
+ // blank; the user's own applied-steer count). Then the ONE optional time clause selected by
355
+ // BASELINE_TIME_MODE, each an AGGREGATE number only (never per-catch claim text / proof — those live in
356
+ // the opt-in reveal + the TUI), shown only when its real value exists:
357
+ // • MEASURED "· Xm caught" — the CUMULATIVE LIFETIME sum of every real measured red→green delta
358
+ // (measuredCaughtTotalMs). Always-on once real catches exist; a MEASURED wall-clock, never an
359
+ // estimate. Cumulative framing (not present-tense "just caught"), so it stays honest for old catches.
360
+ // • Raahil's `~`-prefixed saved ESTIMATE ("· ~7m saved") — shown only when saved_min > 0 (its data is
361
+ // the user's own steers.ndjson credit, not the shadow log, so it keeps his always-on #609 behavior).
362
+ const { steers, savedMin } = readState();
363
+ let line = `${BOLD}skalpel${RESET} · ${CYAN}${steers} steers${RESET}`;
364
+ if (SHOW_MEASURED) {
365
+ const caught = compactMeasured(measuredCaughtTotalMs()); // null when < 1s of real measured catches
366
+ if (caught) line += ` · ${CYAN}${caught} caught${RESET}`;
367
+ }
368
+ if (SHOW_SAVED) {
369
+ const phrase = savedPhrase(savedMin);
370
+ if (phrase) line += ` · ${CYAN}${phrase}${RESET}`;
371
+ }
372
+ process.stdout.write(line);
188
373
  }
189
374
 
190
- // Fail-open, matching the other three hooks (skalpel-hook / -session / -session-end): any error
191
- // print nothing exit 0, so a thrown error can never spill a stack trace into the live status bar.
192
- main()
193
- .then(() => process.exit(0))
194
- .catch(() => process.exit(0));
375
+ // Fail-open: wrap the whole render so a thrown error can never spill a stack trace into the live status
376
+ // bar. Every helper is already individually try/caught; this is the last-line guard. Exit 0 regardless.
377
+ try {
378
+ main();
379
+ } catch {
380
+ /* never let the statusline break or slow the bar */
381
+ }