reasonix 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -58,17 +58,17 @@ actually stays byte-stable.
58
58
 
59
59
  ## Validated numbers
60
60
 
61
- **τ-bench-lite** — 8 multi-turn tool-use tasks, same tools / same prompt
62
- on both sides, sole variable is prefix stability. Measured on live
63
- DeepSeek `deepseek-chat`:
61
+ **τ-bench-lite** — 8 multi-turn tool-use tasks × 3 repeats = 48 runs per
62
+ side. Same tools / same prompt / same client on both sides, sole variable
63
+ is prefix stability. Measured on live DeepSeek `deepseek-chat`:
64
64
 
65
65
  | metric | baseline (cache-hostile) | Reasonix | delta |
66
66
  |---|---:|---:|---:|
67
- | runs | 8 | 8 | — |
68
- | **cache hit** | 43.9% | **94.3%** | **+50.3pp** |
69
- | cost / task | $0.002783 | $0.001621 | **−42% (×0.58)** |
67
+ | runs | 24 | 24 | — |
68
+ | **cache hit** | 46.6% | **94.4%** | **+47.7pp** |
69
+ | cost / task | $0.002599 | $0.001579 | **−39% (×0.61)** |
70
70
  | vs Claude Sonnet 4.6 (token-count estimate) | — | — | **~96% cheaper** |
71
- | pass rate | 100% (8/8) | 88% (7/8) | 1 refusal-task predicate too strict (see [report.md][r]) |
71
+ | pass rate | 96% (23/24) | **100% (24/24)** | Reasonix held the guardrail on every run |
72
72
 
73
73
  **Verify it yourself — no API key, zero cost:**
74
74
 
@@ -86,8 +86,8 @@ stays byte-stable across every model call; baseline's prefix churns on
86
86
  every turn. The cache delta is *mechanically* attributable to log
87
87
  stability, not to a different system prompt.
88
88
 
89
- Full 16-run report: [`benchmarks/tau-bench/report.md`][r]. Reproduce
90
- with your own API key: `npx tsx benchmarks/tau-bench/runner.ts`.
89
+ Full 48-run report: [`benchmarks/tau-bench/report.md`][r]. Reproduce
90
+ with your own API key: `npx tsx benchmarks/tau-bench/runner.ts --repeats 3`.
91
91
 
92
92
  [r]: ./benchmarks/tau-bench/report.md
93
93
 
package/dist/cli/index.js CHANGED
@@ -1420,6 +1420,24 @@ function parseTranscript(raw) {
1420
1420
  }
1421
1421
 
1422
1422
  // src/replay.ts
1423
+ function groupRecordsByTurn(records) {
1424
+ const byTurn = /* @__PURE__ */ new Map();
1425
+ for (const rec of records) {
1426
+ const list = byTurn.get(rec.turn);
1427
+ if (list) list.push(rec);
1428
+ else byTurn.set(rec.turn, [rec]);
1429
+ }
1430
+ return [...byTurn.entries()].sort(([a], [b]) => a - b).map(([turn, records2]) => ({ turn, records: records2 }));
1431
+ }
1432
+ function computeCumulativeStats(pages, upToIdx) {
1433
+ if (upToIdx < 0) return computeReplayStats([]);
1434
+ const flat = [];
1435
+ for (let i = 0; i <= upToIdx && i < pages.length; i++) {
1436
+ const records = pages[i]?.records;
1437
+ if (records) flat.push(...records);
1438
+ }
1439
+ return computeReplayStats(flat);
1440
+ }
1423
1441
  function replayFromFile(path) {
1424
1442
  const parsed = readTranscript(path);
1425
1443
  return { parsed, stats: computeReplayStats(parsed.records) };
@@ -1491,6 +1509,19 @@ function round2(n, digits) {
1491
1509
  }
1492
1510
 
1493
1511
  // src/diff.ts
1512
+ function findNextDivergence(pairs, fromIdx) {
1513
+ for (let i = fromIdx + 1; i < pairs.length; i++) {
1514
+ if (pairs[i].kind !== "match") return i;
1515
+ }
1516
+ return -1;
1517
+ }
1518
+ function findPrevDivergence(pairs, fromIdx) {
1519
+ const start = Math.min(fromIdx - 1, pairs.length - 1);
1520
+ for (let i = start; i >= 0; i--) {
1521
+ if (pairs[i].kind !== "match") return i;
1522
+ }
1523
+ return -1;
1524
+ }
1494
1525
  function diffTranscripts(a, b) {
1495
1526
  const aSide = {
1496
1527
  label: a.label,
@@ -1815,7 +1846,7 @@ function redactKey(key) {
1815
1846
  }
1816
1847
 
1817
1848
  // src/index.ts
1818
- var VERSION = "0.2.0";
1849
+ var VERSION = "0.2.2";
1819
1850
 
1820
1851
  // src/cli/commands/chat.tsx
1821
1852
  import { render } from "ink";
@@ -2566,24 +2597,231 @@ async function chatCommand(opts) {
2566
2597
  // src/cli/commands/diff.ts
2567
2598
  import { writeFileSync as writeFileSync2 } from "fs";
2568
2599
  import { basename } from "path";
2569
- function diffCommand(opts) {
2600
+ import { render as render2 } from "ink";
2601
+ import React10 from "react";
2602
+
2603
+ // src/cli/ui/DiffApp.tsx
2604
+ import { Box as Box8, Static as Static2, Text as Text8, useApp as useApp3, useInput } from "ink";
2605
+ import React9, { useState as useState5 } from "react";
2606
+
2607
+ // src/cli/ui/RecordView.tsx
2608
+ import { Box as Box7, Text as Text7 } from "ink";
2609
+ import React8 from "react";
2610
+ function RecordView({ rec, compact = false }) {
2611
+ const toolArgsMax = compact ? 120 : 200;
2612
+ const toolContentMax = compact ? 200 : 400;
2613
+ if (rec.role === "user") {
2614
+ return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React8.createElement(Text7, null, rec.content));
2615
+ }
2616
+ if (rec.role === "assistant_final") {
2617
+ return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React8.createElement(CacheBadge, { usage: rec.usage }) : null), rec.content ? /* @__PURE__ */ React8.createElement(Text7, null, rec.content) : /* @__PURE__ */ React8.createElement(Text7, { dimColor: true, italic: true }, "(tool-call response only)"));
2618
+ }
2619
+ if (rec.role === "tool") {
2620
+ return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
2621
+ }
2622
+ if (rec.role === "error") {
2623
+ return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React8.createElement(Text7, { color: "red" }, rec.error ?? rec.content));
2624
+ }
2625
+ if (rec.role === "done" || rec.role === "assistant_delta") {
2626
+ return null;
2627
+ }
2628
+ return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, "[", rec.role, "] ", rec.content));
2629
+ }
2630
+ function CacheBadge({ usage }) {
2631
+ const hit = usage.prompt_cache_hit_tokens ?? 0;
2632
+ const miss = usage.prompt_cache_miss_tokens ?? 0;
2633
+ const total = hit + miss;
2634
+ if (total === 0) return null;
2635
+ const pct2 = hit / total * 100;
2636
+ const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
2637
+ return /* @__PURE__ */ React8.createElement(Text7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React8.createElement(Text7, { color }, pct2.toFixed(1), "%"));
2638
+ }
2639
+ function truncate3(s, max) {
2640
+ return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
2641
+ }
2642
+
2643
+ // src/cli/ui/DiffApp.tsx
2644
+ function DiffApp({ report }) {
2645
+ const { exit } = useApp3();
2646
+ const maxIdx = Math.max(0, report.pairs.length - 1);
2647
+ const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
2648
+ const [idx, setIdx] = useState5(Math.max(0, initialIdx));
2649
+ useInput((input, key) => {
2650
+ if (input === "q" || key.ctrl && input === "c") {
2651
+ exit();
2652
+ return;
2653
+ }
2654
+ if (input === "j" || key.downArrow || input === " " || key.return) {
2655
+ setIdx((i) => Math.min(maxIdx, i + 1));
2656
+ } else if (input === "k" || key.upArrow) {
2657
+ setIdx((i) => Math.max(0, i - 1));
2658
+ } else if (input === "g") {
2659
+ setIdx(0);
2660
+ } else if (input === "G") {
2661
+ setIdx(maxIdx);
2662
+ } else if (input === "n") {
2663
+ const next = findNextDivergence(report.pairs, idx);
2664
+ if (next !== -1) setIdx(next);
2665
+ } else if (input === "N" || input === "p") {
2666
+ const prev = findPrevDivergence(report.pairs, idx);
2667
+ if (prev !== -1) setIdx(prev);
2668
+ }
2669
+ });
2670
+ const pair = report.pairs[idx];
2671
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(DiffHeader, { report }), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React9.createElement(Text8, null, pair ? /* @__PURE__ */ React9.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React9.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React9.createElement(Text8, null, pair.divergenceNote)) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "j"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "k"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "N"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "g"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "q"), " ", "quit")));
2672
+ }
2673
+ function DiffHeader({ report }) {
2674
+ const a = report.a;
2675
+ const b = report.b;
2676
+ const cacheDelta = b.stats.cacheHitRatio - a.stats.cacheHitRatio;
2677
+ const costDelta2 = a.stats.totalCostUsd > 0 ? (b.stats.totalCostUsd - a.stats.totalCostUsd) / a.stats.totalCostUsd * 100 : 0;
2678
+ const aStable = a.stats.prefixHashes.length <= 1;
2679
+ const bStable = b.stats.prefixHashes.length <= 1;
2680
+ let prefixLine = null;
2681
+ if (aStable !== bStable) {
2682
+ const stableLabel = aStable ? report.a.label : report.b.label;
2683
+ const churnLabel = aStable ? report.b.label : report.a.label;
2684
+ const churnCount = aStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
2685
+ prefixLine = `${stableLabel} stayed byte-stable; ${churnLabel} churned ${churnCount} distinct prefixes.`;
2686
+ } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
2687
+ prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
2688
+ }
2689
+ return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box8, { justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, a.label), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " vs B="), /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, b.label)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache "), /* @__PURE__ */ React9.createElement(Text8, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cost "), /* @__PURE__ */ React9.createElement(Text8, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "model calls "), /* @__PURE__ */ React9.createElement(Text8, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, prefixLine)) : null);
2690
+ }
2691
+ function Pane({
2692
+ label,
2693
+ headerColor,
2694
+ records
2695
+ }) {
2696
+ return /* @__PURE__ */ React9.createElement(
2697
+ Box8,
2698
+ {
2699
+ flexDirection: "column",
2700
+ flexGrow: 1,
2701
+ paddingX: 1,
2702
+ borderStyle: "single",
2703
+ borderColor: headerColor
2704
+ },
2705
+ /* @__PURE__ */ React9.createElement(Text8, { color: headerColor, bold: true }, label),
2706
+ records.length === 0 ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React9.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React9.createElement(RecordView, { key, rec, compact: true }))
2707
+ );
2708
+ }
2709
+ function KindBadge({ kind }) {
2710
+ if (kind === "match") {
2711
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "green" }, "\u2713 match");
2712
+ }
2713
+ if (kind === "diverge") {
2714
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 diverge");
2715
+ }
2716
+ if (kind === "only_in_a") {
2717
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, "\u2190 only in A");
2718
+ }
2719
+ return /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, "\u2192 only in B");
2720
+ }
2721
+ function paneRecords(pair, side) {
2722
+ if (!pair) return [];
2723
+ const tools = side === "a" ? pair.aTools : pair.bTools;
2724
+ const assistant = side === "a" ? pair.aAssistant : pair.bAssistant;
2725
+ const out = [...tools];
2726
+ if (assistant) out.push(assistant);
2727
+ return out;
2728
+ }
2729
+
2730
+ // src/cli/commands/diff.ts
2731
+ async function diffCommand(opts) {
2570
2732
  const aParsed = readTranscript(opts.a);
2571
2733
  const bParsed = readTranscript(opts.b);
2572
2734
  const report = diffTranscripts(
2573
2735
  { label: opts.labelA ?? basename(opts.a), parsed: aParsed },
2574
2736
  { label: opts.labelB ?? basename(opts.b), parsed: bParsed }
2575
2737
  );
2576
- console.log(renderSummaryTable(report));
2577
- if (opts.mdPath) {
2738
+ const wantMarkdown = !!opts.mdPath;
2739
+ const wantPrint = opts.print || !process.stdout.isTTY;
2740
+ const wantTui = opts.tui || !wantPrint && !wantMarkdown;
2741
+ if (wantMarkdown) {
2742
+ console.log(renderSummaryTable(report));
2578
2743
  const md = renderMarkdown(report);
2579
2744
  writeFileSync2(opts.mdPath, md, "utf8");
2580
2745
  console.log(`
2581
2746
  markdown report written to ${opts.mdPath}`);
2747
+ return;
2748
+ }
2749
+ if (wantTui) {
2750
+ const { waitUntilExit } = render2(React10.createElement(DiffApp, { report }), {
2751
+ exitOnCtrlC: true
2752
+ });
2753
+ await waitUntilExit();
2754
+ return;
2582
2755
  }
2756
+ console.log(renderSummaryTable(report));
2583
2757
  }
2584
2758
 
2585
2759
  // src/cli/commands/replay.ts
2586
- function replayCommand(opts) {
2760
+ import { render as render3 } from "ink";
2761
+ import React12 from "react";
2762
+
2763
+ // src/cli/ui/ReplayApp.tsx
2764
+ import { Box as Box9, Static as Static3, Text as Text9, useApp as useApp4, useInput as useInput2 } from "ink";
2765
+ import React11, { useMemo as useMemo2, useState as useState6 } from "react";
2766
+ function ReplayApp({ meta, pages }) {
2767
+ const { exit } = useApp4();
2768
+ const maxIdx = Math.max(0, pages.length - 1);
2769
+ const [idx, setIdx] = useState6(maxIdx);
2770
+ useInput2((input, key) => {
2771
+ if (input === "q" || key.ctrl && input === "c") {
2772
+ exit();
2773
+ return;
2774
+ }
2775
+ if (input === "j" || key.downArrow || input === " " || key.return) {
2776
+ setIdx((i) => Math.min(maxIdx, i + 1));
2777
+ } else if (input === "k" || key.upArrow) {
2778
+ setIdx((i) => Math.max(0, i - 1));
2779
+ } else if (input === "g") {
2780
+ setIdx(0);
2781
+ } else if (input === "G") {
2782
+ setIdx(maxIdx);
2783
+ } else if (input === "h" || key.leftArrow) {
2784
+ setIdx(0);
2785
+ } else if (input === "l" || key.rightArrow) {
2786
+ setIdx(maxIdx);
2787
+ }
2788
+ });
2789
+ const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
2790
+ const summary = {
2791
+ turns: cumStats.turns,
2792
+ totalCostUsd: cumStats.totalCostUsd,
2793
+ claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
2794
+ savingsVsClaudePct: cumStats.savingsVsClaudePct,
2795
+ cacheHitRatio: cumStats.cacheHitRatio
2796
+ };
2797
+ const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
2798
+ const currentPage = pages[idx];
2799
+ const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
2800
+ return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
2801
+ StatsPanel,
2802
+ {
2803
+ summary,
2804
+ model: cumStats.models[0] ?? meta?.model ?? "?",
2805
+ prefixHash
2806
+ }
2807
+ ), /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text9, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React11.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React11.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React11.createElement(Text9, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "q"), " quit")));
2808
+ }
2809
+
2810
+ // src/cli/commands/replay.ts
2811
+ async function replayCommand(opts) {
2812
+ const wantPrint = opts.print || !process.stdout.isTTY || opts.head !== void 0 || opts.tail !== void 0;
2813
+ if (wantPrint) {
2814
+ printReplay(opts);
2815
+ return;
2816
+ }
2817
+ const { parsed } = replayFromFile(opts.path);
2818
+ const pages = groupRecordsByTurn(parsed.records);
2819
+ const { waitUntilExit } = render3(React12.createElement(ReplayApp, { meta: parsed.meta, pages }), {
2820
+ exitOnCtrlC: true
2821
+ });
2822
+ await waitUntilExit();
2823
+ }
2824
+ function printReplay(opts) {
2587
2825
  const { parsed, stats } = replayFromFile(opts.path);
2588
2826
  if (parsed.meta) {
2589
2827
  const m = parsed.meta;
@@ -2779,16 +3017,27 @@ program.command("stats <transcript>").description("Summarize a JSONL transcript
2779
3017
  statsCommand({ transcript });
2780
3018
  });
2781
3019
  program.command("replay <transcript>").description(
2782
- "Pretty-print a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
2783
- ).option("--head <n>", "Show only the first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "Show only the last N records", (v) => Number.parseInt(v, 10)).action((transcript, opts) => {
2784
- replayCommand({
3020
+ "Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
3021
+ ).option("--print", "Dump to stdout instead of mounting the TUI (auto when piped)").option("--head <n>", "stdout mode only \u2014 show first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "stdout mode only \u2014 show last N records", (v) => Number.parseInt(v, 10)).action(async (transcript, opts) => {
3022
+ await replayCommand({
2785
3023
  path: transcript,
3024
+ print: !!opts.print,
2786
3025
  head: Number.isFinite(opts.head) ? opts.head : void 0,
2787
3026
  tail: Number.isFinite(opts.tail) ? opts.tail : void 0
2788
3027
  });
2789
3028
  });
2790
- program.command("diff <a> <b>").description("Compare two transcripts: aggregate deltas + first divergence.").option("--md <path>", "Also write a markdown report (blog-ready) to this path").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action((a, b, opts) => {
2791
- diffCommand({ a, b, mdPath: opts.md, labelA: opts.labelA, labelB: opts.labelB });
3029
+ program.command("diff <a> <b>").description(
3030
+ "Compare two transcripts in a split-pane Ink TUI (default) or stdout table. Use n/N to jump across divergences."
3031
+ ).option("--md <path>", "Write a markdown report (blog-ready) to this path").option("--print", "Force stdout table instead of the TUI (auto when piped)").option("--tui", "Force the TUI even when piped (rare)").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action(async (a, b, opts) => {
3032
+ await diffCommand({
3033
+ a,
3034
+ b,
3035
+ mdPath: opts.md,
3036
+ labelA: opts.labelA,
3037
+ labelB: opts.labelB,
3038
+ print: !!opts.print,
3039
+ tui: !!opts.tui
3040
+ });
2792
3041
  });
2793
3042
  program.command("version").description("Print Reasonix version.").action(versionCommand);
2794
3043
  program.parseAsync(process.argv).catch((err) => {