csift 0.10.5

ripgrep for Claude Code session transcripts: fast regex list/search over ~/.claude/projects/**/*.jsonl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
use super::home::*;

/// A session whose ONLY event for the target is an edit with no preceding read → the edit
/// is un-anchorable, leaving an empty buffer and no seen_total (events non-empty, but the
/// reconstruction is empty). Used to drive the at-mode "nothing known" skip in both
/// renderers.
pub(crate) fn recover_empty_reconstruction_home() -> Home {
    let h = Home::new();
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        concat!(
            r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"go"}}"#, "\n",
            // An Edit result for /p/e.rs with NO prior read → un-anchorable, no known lines.
            r#"{"type":"user","uuid":"c0","timestamp":"2026-06-07T05:00:01.000Z","toolUseResult":{"filePath":"/p/e.rs","oldString":"x","newString":"y","structuredPatch":[{"oldStart":1,"oldLines":1,"newStart":1,"newLines":1,"lines":["-x","+y"]}]},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"ed0","content":"ok"}]}}"#, "\n",
        ),
    );
    h
}

/// Build the `turns` integration fixture: a realistic multi-compaction transcript
/// authored with LOCALE-NEUTRAL tokens only (accented-Latin + emoji - the
/// same house charset style as the recover fixtures).
///
/// Shape (all on the SINGLE top-level session jsonl so the spanning walk is exercised
/// without subagent noise; tests pass `--no-subagents`):
///   • 3 genuine round-trip turns, then a compaction SUMMARY #1 (its §6 quotes turn 0's
///     user verbatim + §9 quotes the last assistant - drives the dedup test),
///   • 3 more round-trips, then SUMMARY #2,
///   • 3 more round-trips, then SUMMARY #3,
///   • a final live block: one HUGE round-trip (user > 600 chars, assistant > 900 chars
///     → drives the role-asymmetric ellipsis test) and an assistant-heavy tail (a turn
///     whose assistant side is enormous + a pure tool-call turn) → drives the 50% floor.
/// Plus one malformed line for the skipped-line accounting.
/// A tiny jsonl builder for the turns fixture: free methods on a struct so there is no
/// closure-capture conflict (the earlier closure form double-borrowed the buffer).
pub(crate) struct TurnsBuilder {
    pub(crate) out: String,
    pub(crate) ts: u64,
}

impl TurnsBuilder {
    pub(crate) fn new() -> Self {
        TurnsBuilder {
            out: String::new(),
            ts: 0,
        }
    }

    pub(crate) fn next_ts(&mut self) -> String {
        self.ts += 1;
        let t = self.ts;
        format!(
            "2026-06-07T{:02}:{:02}:{:02}.000Z",
            t / 3600,
            (t / 60) % 60,
            t % 60
        )
    }

    pub(crate) fn line(&mut self, s: &str) {
        self.out.push_str(s);
        self.out.push('\n');
    }

    /// A round-trip turn: user opener (+ optional N tool calls) + assistant EOT text.
    pub(crate) fn round_trip(&mut self, user: &str, asst: &str, tools: usize) {
        let ts = self.next_ts();
        self.line(&format!(
            r#"{{"type":"user","timestamp":"{ts}","message":{{"role":"user","content":"{user}"}}}}"#
        ));
        if tools > 0 {
            let blocks: Vec<String> = (0..tools)
                .map(|i| format!(r#"{{"type":"tool_use","id":"t{i}","name":"Bash","input":{{}}}}"#))
                .collect();
            let ts = self.next_ts();
            self.line(&format!(
                r#"{{"type":"assistant","timestamp":"{ts}","message":{{"role":"assistant","content":[{}]}}}}"#,
                blocks.join(",")
            ));
        }
        let ts = self.next_ts();
        self.line(&format!(
            r#"{{"type":"assistant","timestamp":"{ts}","message":{{"role":"assistant","content":[{{"type":"text","text":"{asst}"}}]}}}}"#
        ));
    }

    /// Emit one assistant text record (one agent message in a turn's run).
    pub(crate) fn agent_text(&mut self, text: &str) {
        let ts = self.next_ts();
        self.line(&format!(
            r#"{{"type":"assistant","timestamp":"{ts}","message":{{"role":"assistant","content":[{{"type":"text","text":"{text}"}}]}}}}"#
        ));
    }

    /// Emit one assistant tool_use record (drives the per-message attribution span).
    pub(crate) fn tool_use(&mut self) {
        let ts = self.next_ts();
        self.line(&format!(
            r#"{{"type":"assistant","timestamp":"{ts}","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"tx","name":"Bash","input":{{}}}}]}}}}"#
        ));
    }

    /// A turn with a LONG agent-message run: a user opener, then the ordered list of
    /// agent messages (each preceded by one tool_use so the placeholder Y is non-zero),
    /// to drive the richness selection. The LAST entry is the EOT.
    pub(crate) fn long_agent_run(&mut self, user: &str, agent_msgs: &[&str]) {
        let ts = self.next_ts();
        self.line(&format!(
            r#"{{"type":"user","timestamp":"{ts}","message":{{"role":"user","content":"{user}"}}}}"#
        ));
        for m in agent_msgs {
            self.tool_use();
            self.agent_text(m);
        }
    }
}

pub(crate) fn turns_fixture_jsonl() -> String {
    let mut b = TurnsBuilder::new();

    // ── Block A: 3 round-trips, then SUMMARY #1 ──
    b.round_trip(
        "the very first ask about the café budget walk logic",
        "first reply café🛠",
        2,
    );
    b.round_trip(
        "second ask explain the panic path now please",
        "second reply",
        2,
    );
    b.round_trip("third ask about the carry boundary", "third reply", 0);
    // SUMMARY #1: §6 quotes turn 0's user verbatim (dedup target), §9 quotes an assistant.
    b.line(
        r#"{"type":"user","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"message":{"role":"user","content":"This session is being continued from a previous conversation.\n6. All user messages:\n   - \"the very first ask about the café budget walk logic\"\n   - \"second ask explain ...\"\n9. Optional Next Step:\n   The assistant said \"third reply\" before compaction."}}"#,
    );

    // ── Block B: 3 round-trips, then SUMMARY #2 ──
    b.round_trip(
        "fourth ask after the first compaction boundary",
        "fourth reply",
        1,
    );
    b.round_trip("fifth ask keep going", "fifth reply", 3);
    b.round_trip("sixth ask about détours", "sixth reply", 0);
    b.line(
        r#"{"type":"user","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"message":{"role":"user","content":"This session is being continued.\n6. All user messages:\n   - \"fourth ask after the first compaction boundary\"\n9. Optional Next Step:\n   The assistant said \"sixth reply\"."}}"#,
    );

    // ── Block C: 3 round-trips, then SUMMARY #3 ──
    b.round_trip("seventh ask post second boundary", "seventh reply", 2);
    b.round_trip("eighth ask almost there", "eighth reply", 1);
    b.round_trip("ninth ask wrap it up", "ninth reply", 0);
    b.line(
        r#"{"type":"user","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"message":{"role":"user","content":"This session is being continued.\n6. All user messages:\n   - \"seventh ask post second boundary\"\n9. Optional Next Step:\n   The assistant said \"ninth reply\"."}}"#,
    );

    // ── Block D (live region, after the newest summary) ──
    // A LONG agent-message run in ONE turn (8 agent messages > the default >6 threshold):
    // a rich first, pure-declaration middles, a sudden rich middle, a FUSED finding+decl
    // body, and the EOT - drives the richness selection + placeholder integration tests.
    b.long_agent_run(
        "kick off the long debugging chain",
        &[
            "found the AGENTRICHFIRST root cause already", // first - rich (lexeme) → kept
            "let me try the LETMEDECL one next",           // middle decl → collapse
            "now i will check LETMEDECL another",          // middle decl → collapse
            // A declaration with a digit adjacent to multi-byte chars (the exact
            // shape that once panicked the ±16-byte number-of-substance window): a
            // signal-less intent-verb opener → collapses, and must NOT panic.
            "let me LETMEDECL look at the 🤖 07:40 log", // middle decl → collapse
            "AGENTRICHMID 12 passed 3 failed in src/x.rs:9", // sudden rich middle → kept
            "let me write LETMEDECL it up",              // middle decl → collapse
            "now let me LETMEDECL finalize",             // middle decl → collapse
            "root cause confirmed in src/y.rs:42 — now let me FUSEDTAIL write the fix", // fused → kept
            "the AGENTEOT final committed answer", // last - always kept
        ],
    );
    // A HUGE round-trip: user > 600 chars, assistant > 900 chars → role-asymmetric ellipsis.
    let huge_user = format!("HEADuser {} TAILuser", "u".repeat(800));
    let huge_asst = format!("HEADasst {} TAILasst", "a".repeat(1100));
    b.round_trip(&huge_user, &huge_asst, 5);
    // An assistant-heavy tail: a complete turn whose assistant side is enormous.
    let big_asst = format!("big {} end", "b".repeat(3000));
    b.round_trip("short live ask", &big_asst, 2);
    // A pure tool-call turn (no assistant EOT text) → partial turn.
    let ts = b.next_ts();
    b.line(&format!(
        r#"{{"type":"user","timestamp":"{ts}","message":{{"role":"user","content":"do the final thing"}}}}"#
    ));
    let ts = b.next_ts();
    b.line(&format!(
        r#"{{"type":"assistant","timestamp":"{ts}","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"z","name":"Bash","input":{{}}}}]}}}}"#
    ));
    // A malformed line that survives the prefilter (carries "role":"user") → counted.
    b.line(r#"{"type":"user","role":"user" broken json after marker}"#);

    b.out
}

pub(crate) fn turns_home() -> Home {
    let h = Home::new();
    h.write(&format!("{ENC}/{SESS}.jsonl"), &turns_fixture_jsonl());
    h
}

/// Parse the JSON-lines stdout into a vector of serde_json::Value objects.
pub(crate) fn json_lines(stdout: &str) -> Vec<serde_json::Value> {
    stdout
        .lines()
        .filter(|l| l.trim_start().starts_with('{'))
        .filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
        .collect()
}

/// Strip the OPERATIONAL trailer lines the text renderer prints to stdout but that are
/// NOT part of the reconstruction DOCUMENT (per SPEC §6.8 the document
/// is: doc-header-block + unit headers + bodies + ellipsis markers + boundary banners).
/// The `(skipped N malformed …)` diagnostic and the `(wrote full reconstruction …)`
/// notice are stdout-only chrome, never written to `--out`; everything else stays.
pub(crate) fn turns_document_text(stdout: &str) -> String {
    let kept: Vec<&str> = stdout
        .lines()
        .filter(|l| {
            let t = l.trim_start();
            !t.starts_with("(skipped ") && !t.starts_with("(wrote ")
        })
        .collect();
    // Re-join with the same '\n' the renderer used; trailing newline normalized away by
    // the line split, so this is the exact emitted-document char basis.
    kept.join("\n")
}

/// A fixture whose NEWEST summary quotes a LIVE-region turn verbatim → exercises the
/// live-region dedup demote-and-flag path (the main fixture's live turns are unique).
pub(crate) fn turns_dedup_home() -> Home {
    let h = Home::new();
    let mut s = String::new();
    // turn 0 (pre-boundary): user + assistant.
    s.push_str(r#"{"type":"user","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"pre-boundary ask kept verbatim here"}}"#);
    s.push('\n');
    s.push_str(r#"{"type":"assistant","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"pre reply"}]}}"#);
    s.push('\n');
    // SUMMARY whose §6 quotes a LIVE turn that comes AFTER it (the "live duplicate ask").
    s.push_str(r#"{"type":"user","isCompactSummary":true,"message":{"role":"user","content":"6. All user messages:\n   - \"the live duplicate ask that the summary already holds verbatim\"\n9. Optional Next Step:\n   The assistant said \"pre reply\"."}}"#);
    s.push('\n');
    // turn 1 (LIVE region) whose user text matches the summary's §6 bullet → deduped.
    s.push_str(r#"{"type":"user","timestamp":"2026-06-07T06:00:00.000Z","message":{"role":"user","content":"the live duplicate ask that the summary already holds verbatim"}}"#);
    s.push('\n');
    s.push_str(r#"{"type":"assistant","timestamp":"2026-06-07T06:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"live reply"}]}}"#);
    s.push('\n');
    // turn 2 (LIVE region) unique.
    s.push_str(r#"{"type":"user","timestamp":"2026-06-07T07:00:00.000Z","message":{"role":"user","content":"a unique live follow-up question"}}"#);
    s.push('\n');
    s.push_str(r#"{"type":"assistant","timestamp":"2026-06-07T07:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"follow-up reply"}]}}"#);
    s.push('\n');
    h.write(&format!("{ENC}/{SESS}.jsonl"), &s);
    h
}

/// A SECOND clean session (no malformed lines) under the same project - exercises the
/// multi-session render path (blank separator) + the no-skipped-lines branch.
pub(crate) fn turns_two_sessions_home() -> Home {
    let h = Home::new();
    // Session 1: the main multi-summary fixture (has a malformed line).
    h.write(&format!("{ENC}/{SESS}.jsonl"), &turns_fixture_jsonl());
    // Session 2: a small CLEAN session (no malformed line, no summary).
    let sess2 = "00000000-0000-4000-8000-000000000002";
    let mut s = String::new();
    s.push_str(r#"{"type":"user","timestamp":"2026-06-07T08:00:00.000Z","message":{"role":"user","content":"clean session ask"}}"#);
    s.push('\n');
    s.push_str(r#"{"type":"assistant","timestamp":"2026-06-07T08:00:01.000Z","message":{"role":"assistant","content":[{"type":"text","text":"clean session reply"}]}}"#);
    s.push('\n');
    h.write(&format!("{ENC}/{sess2}.jsonl"), &s);
    h
}

/// The captured pre-feature baseline: the EXACT single-EOT stdout the `turns` tool emitted
/// on the §fixture BEFORE the multi-agent-message richness feature (one agent EOT message
/// per turn, no `△ L…–L…` collapsed-agents placeholder, no intermediate rich members). The
/// DEFAULT now keeps the LONGEST agent message + the first-if-substantive + the rich
/// middles, so this baseline is reproduced by the `--agent-msgs eot-only` ESCAPE, not by
/// the implicit default. Captured under `TZ=UTC` so the system-local timestamp render is
/// deterministic across machines. Re-capture (ONLY on an INTENDED eot-only-output change):
///   cargo test --test cli_integration recapture_turns_pre_feature_baseline -- --ignored
/// then review the baseline diff like any code change.
pub(crate) const TURNS_PRE_FEATURE_BASELINE: &str =
    include_str!("../../turns_pre_feature_baseline.txt");

/// A session whose ONLY genuine human opener is "start the work", followed by an
/// AskUserQuestion exchange (Q+options+multi-byte answer) and an ExitPlanMode plan that the
/// user REJECTS with a typed message, plus an interrupt marker that must NOT split a
/// turn.
pub(crate) fn holes_home() -> Home {
    let h = Home::new();
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        concat!(
            // turn 0: genuine human opener.
            r#"{"type":"user","uuid":"u0","sessionId":"0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d","cwd":"/Users/testuser/Projects/foo","version":"2.1.0","gitBranch":"main","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"start the work"}}"#, "\n",
            // assistant asks (member of turn 0). Options carry per-option descriptions.
            r#"{"type":"assistant","uuid":"a0","parentUuid":"u0","timestamp":"2026-06-07T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"q1","name":"AskUserQuestion","input":{"questions":[{"question":"which option for step two?","header":"STEP TWO","options":[{"label":"option A (recommended)","description":"the conservative path that reuses existing state"},{"label":"option B","description":"the full path that rebuilds from scratch"}]}]}}]}}"#, "\n",
            // turn 1: the AUQ ANSWER opens a turn (the behavior change). The carrier echoes
            // the options WITH descriptions and carries free-text `annotations.notes`.
            r#"{"type":"user","uuid":"ans","parentUuid":"a0","timestamp":"2026-06-07T05:10:00.000Z","toolUseResult":{"questions":[{"question":"which option for step two?","header":"STEP TWO","options":[{"label":"option A (recommended)","description":"the conservative path that reuses existing state"},{"label":"option B","description":"the full path that rebuilds from scratch"}]}],"answers":{"which option for step two?":"option A is fine, the scope is broader than stated"},"annotations":{"which option for step two?":{"notes":"go with option A but budget for the edge cases, it is more involved than a quick tweak"}}},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"q1","content":"Your questions have been answered: \"which option for step two?\"=\"option A is fine, the scope is broader than stated\"."}]}}"#, "\n",
            // assistant proposes a plan (member of turn 1).
            r#"{"type":"assistant","uuid":"a1","parentUuid":"ans","timestamp":"2026-06-07T05:11:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_PLAN1","name":"ExitPlanMode","input":{"plan":"the plan body here","planFilePath":"/Users/testuser/.claude/plans/elegant-scribbling-dream.md"}}]}}"#, "\n",
            // turn 2: the user REJECTS the plan with a typed message → boundary + pointer.
            r#"{"type":"user","uuid":"rej","parentUuid":"a1","timestamp":"2026-06-07T05:20:00.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_PLAN1","is_error":true,"content":"The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\nplease run the smoke tests once before calling it done"}]}}"#, "\n",
            // an interrupt marker - a turn MEMBER of turn 2, NOT a new boundary.
            r#"{"type":"user","uuid":"int","parentUuid":"rej","timestamp":"2026-06-07T05:20:30.000Z","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user]"}]}}"#, "\n",
            r#"{"type":"assistant","uuid":"a2","parentUuid":"int","timestamp":"2026-06-07T05:21:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok, adding the smoke-test check"}]}}"#, "\n",
        ),
    );
    h
}

/// Extract the line texts (in order) from a `recover --at … --format json` snapshot.
pub(crate) fn recon_lines_from_at_json(stdout: &str) -> Vec<String> {
    let snap = stdout
        .lines()
        .filter(|l| !l.trim().is_empty())
        .map(|l| serde_json::from_str::<serde_json::Value>(l).unwrap())
        .find(|v| v.get("kind").and_then(|t| t.as_str()) == Some("snapshot"))
        .unwrap_or_else(|| panic!("no snapshot object in:\n{stdout}"));
    let mut recon: std::collections::BTreeMap<usize, String> = std::collections::BTreeMap::new();
    for l in snap.get("lines").and_then(|v| v.as_array()).unwrap() {
        recon.insert(
            l.get("n").and_then(|v| v.as_u64()).unwrap() as usize,
            l.get("text").and_then(|v| v.as_str()).unwrap().to_string(),
        );
    }
    recon.into_values().collect()
}

/// Build a top-level transcript that (a) binds a plan via a `plan_mode` attachment, (b) also
/// Edits a DIFFERENT plan file (which must NOT be mistaken for the bound one), and (c) has a
/// Write+Edit history of the bound plan (so `@plan` recover has content to rebuild).
pub(crate) fn write_planning_session(h: &Home, sess: &str, bound_abs: &str, other_abs: &str) {
    let jsonl = concat!(
        r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"plan it"}}"#, "\n",
        // plan_mode attachment → the AUTHORITATIVE binding.
        r#"{"type":"attachment","isSidechain":false,"attachment":{"type":"plan_mode","reminderType":"full","isSubAgent":false,"planFilePath":"__BOUND__","planExists":true},"uuid":"att0","timestamp":"2026-06-07T05:00:01.000Z","userType":"external","entrypoint":"cli","cwd":"/p"}"#, "\n",
        // An Edit of SOMEONE ELSE's plan file - a red herring for the resolver.
        r#"{"type":"assistant","uuid":"ax","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"ex","name":"Edit","input":{"file_path":"__OTHER__","old_string":"x","new_string":"y"}}]}}"#, "\n",
        r#"{"type":"user","uuid":"cx","timestamp":"2026-06-07T05:00:02.500Z","toolUseResult":{"filePath":"__OTHER__","oldString":"x","newString":"y","originalFile":null,"replaceAll":false,"structuredPatch":[{"oldStart":1,"oldLines":1,"newStart":1,"newLines":1,"lines":["-x","+y"]}]},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"ex","content":"ok"}]}}"#, "\n",
        // The bound plan's own Write + Edit history.
        r#"{"type":"assistant","uuid":"aw","timestamp":"2026-06-07T05:00:03.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"pw","name":"Write","input":{"file_path":"__BOUND__","content":"P1\nP2\nP3\n"}}]}}"#, "\n",
        r#"{"type":"user","uuid":"cw","timestamp":"2026-06-07T05:00:03.500Z","toolUseResult":{"type":"create","filePath":"__BOUND__","content":"P1\nP2\nP3\n"},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"pw","content":"ok"}]}}"#, "\n",
        r#"{"type":"assistant","uuid":"ap","timestamp":"2026-06-07T05:00:04.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"pe","name":"Edit","input":{"file_path":"__BOUND__","old_string":"P2","new_string":"P2-revised"}}]}}"#, "\n",
        r#"{"type":"user","uuid":"cp","timestamp":"2026-06-07T05:00:04.500Z","toolUseResult":{"filePath":"__BOUND__","oldString":"P2","newString":"P2-revised","originalFile":null,"replaceAll":false,"structuredPatch":[{"oldStart":2,"oldLines":1,"newStart":2,"newLines":1,"lines":["-P2","+P2-revised"]}]},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"pe","content":"ok"}]}}"#, "\n",
    )
    .replace("__BOUND__", &jpath(bound_abs))
    .replace("__OTHER__", &jpath(other_abs));
    h.write(&format!("{ENC}/{sess}.jsonl"), &jsonl);
}

/// A synthetic 1×1 transparent PNG (base64) - a REAL valid PNG (correct chunk CRCs, so the
/// strict `image` decoder accepts it for `--as` transcoding, not just the magic-byte checks).
pub(crate) const PNG_1X1: &str =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=";

/// Three more DISTINCT valid 1×1 PNGs (red / green / blue) - distinct content fingerprints, so
/// the listing's content-dedup treats them as separate screenshots (not one re-injected image).
pub(crate) const PNG_RED: &str =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";

pub(crate) const PNG_GREEN: &str =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNg+M/wHwAEAQH/cetH5QAAAABJRU5ErkJggg==";

pub(crate) const PNG_BLUE: &str =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYPj/HwADAgH/5ncLrgAAAABJRU5ErkJggg==";

/// A real 3-frame (red/green/blue, 0.5s each = 1.5s) 4×4 animated GIF89a, for the
/// "convert an animated GIF to a still format → first frame + warning" path.
pub(crate) const ANIM_GIF_3F: &str = "R0lGODlhBAAEAPAAAP8AAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQAMgAAACwAAAAABAAEAAACBISPCQUAIfkEADIAAAAsAAAAAAQABACAAIAAAAAAAgSEjwkFACH5BAAyAAAALAAAAAAEAAQAgAAA/wAAAAIEhI8JBQA7";

pub(crate) fn img_block(media: &str, data: &str) -> serde_json::Value {
    serde_json::json!({"type":"image","source":{"type":"base64","media_type":media,"data":data}})
}

pub(crate) fn image_home() -> Home {
    let h = Home::new();
    let r0 = serde_json::json!({
        "type":"user","uuid":"u0","sessionId":SESS,"cwd":"/Users/testuser/Projects/foo",
        "version":"2.1.0","gitBranch":"main","timestamp":"2026-06-07T05:00:00.000Z",
        "message":{"role":"user","content":[{"type":"text","text":"first screenshot"}, img_block("image/png", PNG_1X1)]}
    });
    let r1 = serde_json::json!({
        "type":"assistant","uuid":"a0","parentUuid":"u0","timestamp":"2026-06-07T05:00:05.000Z",
        "message":{"role":"assistant","content":[{"type":"text","text":"got it"}]}
    });
    let r2 = serde_json::json!({
        "type":"user","uuid":"u1","timestamp":"2026-06-07T06:00:00.000Z",
        "message":{"role":"user","content":[{"type":"text","text":"two more"}, img_block("image/jpeg", PNG_RED), img_block("image/png", PNG_GREEN)]}
    });
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        &format!("{r0}\n{r1}\n{r2}\n"),
    );
    h
}

/// Fixture: a session where `#1` names TWO different images (CC reuses `#N` per prompt). r0
/// (turn 0) carries #1=transparent + #2=red; a later r2 (turn 1) reuses #1 for a different
/// image (blue). Returns the home.
pub(crate) fn ambiguous_hash_home() -> Home {
    let h = Home::new();
    let r0 = serde_json::json!({
        "type":"user","uuid":"u0","sessionId":SESS,"cwd":"/Users/testuser/Projects/foo",
        "timestamp":"2026-06-07T05:00:00.000Z",
        "message":{"role":"user","content":[
            {"type":"text","text":"look at [Image #1] and [Image #2]"},
            img_block("image/png", PNG_1X1), img_block("image/png", PNG_RED)]}
    });
    let r1 = serde_json::json!({
        "type":"assistant","uuid":"a0","parentUuid":"u0","timestamp":"2026-06-07T05:00:05.000Z",
        "message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}
    });
    let r2 = serde_json::json!({
        "type":"user","uuid":"u1deadbeef","timestamp":"2026-06-07T06:00:00.000Z",
        "message":{"role":"user","content":[
            {"type":"text","text":"now re-sharing [Image #1]"},
            img_block("image/png", PNG_BLUE)]}
    });
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        &format!("{r0}\n{r1}\n{r2}\n"),
    );
    h
}

/// A minimal top-level session jsonl (one genuine-user + one assistant turn) for the
/// elicitation-sidecar tests. Each test drops its own `<SESS>/elicitations.jsonl` content.
pub(crate) fn sidecar_session_home() -> Home {
    let h = Home::new();
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        concat!(
            r#"{"type":"user","uuid":"u0","sessionId":"0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d","cwd":"/Users/testuser/Projects/foo","version":"2.1.0","gitBranch":"main","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"start the work"}}"#, "\n",
            r#"{"type":"assistant","uuid":"a0","parentUuid":"u0","timestamp":"2026-06-07T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"working on it"}]}}"#, "\n",
        ),
    );
    h
}

/// An unresolved AskUserQuestion pending sidecar record (native-shaped assistant tool_use +
/// csift marker fields).
pub(crate) fn auq_pending_line(key: &str, ts: &str, question: &str) -> String {
    format!(
        r#"{{"type":"assistant","uuid":"u-{key}","timestamp":"{ts}","sessionId":"{SESS}","cwd":"/Users/testuser/Projects/foo","isSidechain":false,"message":{{"role":"assistant","stop_reason":"tool_use","content":[{{"type":"tool_use","id":"{key}","name":"AskUserQuestion","input":{{"questions":[{{"question":"{question}"}}]}}}}]}},"csift":"elicitation-marker-v1","csiftPhase":"pending","csiftKind":"AskUserQuestion","csiftKey":"{key}","csiftHookEvent":"PreToolUse","hookInput":{{}}}}"#
    )
}

pub(crate) fn resolved_line(key: &str, ts: &str) -> String {
    format!(
        r#"{{"type":"csift-elicitation-resolved","uuid":"r-{key}","timestamp":"{ts}","sessionId":"{SESS}","csift":"elicitation-marker-v1","csiftPhase":"resolved","csiftKind":"AskUserQuestion","csiftKey":"{key}"}}"#
    )
}

pub(crate) fn mcp_pending_line(key: &str, ts: &str, server: &str, message: &str) -> String {
    format!(
        r#"{{"type":"system","subtype":"mcp_elicitation","uuid":"m-{key}","timestamp":"{ts}","sessionId":"{SESS}","isSidechain":false,"content":"MCP elicitation [{server}] (url): {message}","csift":"elicitation-marker-v1","csiftPhase":"pending","csiftKind":"mcp-elicitation","csiftKey":"{key}","csiftMcpServer":"{server}","hookInput":{{}}}}"#
    )
}

pub(crate) const ACC_ENC: &str = "-Users-x-acc";

pub(crate) const ACC_SESS: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";

pub(crate) const ACC_SUB: &str = "c0ffeec0ffeec0ff";

/// A `$HOME` whose single top-level transcript carries one record per §A–J disk-shape (each with a
/// unique `zz<name>` token so a search targets it precisely), plus one subagent transcript whose
/// opener seed exercises C9 (parent ⇨ self).
pub(crate) fn acceptance_home() -> Home {
    let h = Home::new();
    h.write(
        &format!("{ACC_ENC}/{ACC_SESS}.jsonl"),
        concat!(
            r#"{"type":"user","uuid":"u0","sessionId":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","cwd":"/Users/x/acc","version":"2.1.0","gitBranch":"main","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"zzgenuine human prose here"}}"#, "\n",
            r#"{"type":"user","uuid":"u0b","parentUuid":"u0","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"zzblocktext genuine prose in a text block"}]}}"#, "\n",
            r#"{"type":"assistant","uuid":"a0","parentUuid":"u0b","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"zzagentmsg the visible reply"},{"type":"thinking","thinking":"zzthink hidden reasoning"}]}}"#, "\n",
            r#"{"type":"assistant","uuid":"a1","parentUuid":"a0","timestamp":"2026-06-07T05:00:03.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"spawn1","name":"Agent","input":{"subagent_type":"general-purpose","name":"audit-x","prompt":"zzspawn go audit the thing"}}]}}"#, "\n",
            r#"{"type":"assistant","uuid":"a2","parentUuid":"a1","timestamp":"2026-06-07T05:00:04.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"sm1","name":"SendMessage","input":{"to":"GraftBoard","type":"message","message":"zzsent ship the fix"}}]}}"#, "\n",
            r#"{"type":"assistant","uuid":"a3","parentUuid":"a2","timestamp":"2026-06-07T05:00:05.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"sm2","name":"SendMessage","input":{"to":"GraftBoard","type":"shutdown_request","message":{"type":"shutdown_request","reason":"zzshutdownreq stop now"}}}]}}"#, "\n",
            r#"{"type":"user","uuid":"sig1","timestamp":"2026-06-07T05:00:06.000Z","message":{"role":"user","content":"<teammate-message teammate_id=\"SOurDnd\" color=\"green\">\n{\"type\":\"idle_notification\",\"from\":\"SOurDnd\",\"idleReason\":\"zzidle available\"}\n</teammate-message>"}}"#, "\n",
            r#"{"type":"user","uuid":"sig2","timestamp":"2026-06-07T05:00:07.000Z","message":{"role":"user","content":"<teammate-message teammate_id=\"system\">\n{\"type\":\"teammate_terminated\",\"message\":\"zzterminated B38 has shut down.\"}\n</teammate-message>"}}"#, "\n",
            r#"{"type":"user","uuid":"sig3","timestamp":"2026-06-07T05:00:08.000Z","message":{"role":"user","content":"<teammate-message teammate_id=\"B38\">\n{\"type\":\"shutdown_approved\",\"from\":\"B38\",\"reason\":\"zzapproved ok done\"}\n</teammate-message>"}}"#, "\n",
            r#"{"type":"user","uuid":"tm2","timestamp":"2026-06-07T05:00:09.000Z","message":{"role":"user","content":"<teammate-message teammate_id=\"team-lead\" color=\"blue\">\nzzbareinbox please check the rate limit\n</teammate-message>"}}"#, "\n",
            r#"{"type":"user","uuid":"n1","timestamp":"2026-06-07T05:00:10.000Z","message":{"role":"user","content":"<task-notification>\n<task-id>mon1</task-id>\n<status>completed</status>\n<event>tick</event>\n<summary>Monitor \"zzmonitor liveness\" fired</summary>\n</task-notification>"}}"#, "\n",
            r#"{"type":"user","uuid":"cmd1","timestamp":"2026-06-07T05:00:11.000Z","message":{"role":"user","content":"<command-name>/deploy</command-name>\n<command-message>deploy</command-message>\n<command-args>zzcmdargs to staging now</command-args>"}}"#, "\n",
            r#"{"type":"user","uuid":"out1","timestamp":"2026-06-07T05:00:12.000Z","message":{"role":"user","content":"<local-command-stdout>zzstdout Login successful</local-command-stdout>"}}"#, "\n",
            r#"{"type":"user","uuid":"int1","timestamp":"2026-06-07T05:00:13.000Z","message":{"role":"user","content":"[Request interrupted by user]"}}"#, "\n",
            r#"{"type":"user","uuid":"int2","timestamp":"2026-06-07T05:00:14.000Z","message":{"role":"user","content":"[Request interrupted by user for tool use]"}}"#, "\n",
            r##"{"type":"user","uuid":"wake1","isMeta":true,"timestamp":"2026-06-07T05:00:15.000Z","message":{"role":"user","content":"# Autonomous loop check\n\nYou're being invoked on a timer while the user is away. zzwakeup"}}"##, "\n",
            r#"{"type":"user","uuid":"cont1","isMeta":true,"timestamp":"2026-06-07T05:00:16.000Z","message":{"role":"user","content":[{"type":"text","text":"Continue from where you left off."}]}}"#, "\n",
            r#"{"type":"user","uuid":"hook1","isMeta":true,"timestamp":"2026-06-07T05:00:17.000Z","message":{"role":"user","content":"Stop hook feedback:\nThe last Edit failed. zzhook retry"}}"#, "\n",
            r##"{"type":"user","uuid":"loop1","isMeta":true,"timestamp":"2026-06-07T05:00:18.000Z","message":{"role":"user","content":"# Autonomous loop tick (dynamic pacing)\nzzloop driver"}}"##, "\n",
            r#"{"type":"user","uuid":"meta0","isMeta":true,"timestamp":"2026-06-07T05:00:19.000Z","message":{"role":"user","content":"zzunmarked novel hook wrapper text"}}"#, "\n",
            r#"{"type":"user","uuid":"sum1","isCompactSummary":true,"isVisibleInTranscriptOnly":true,"timestamp":"2026-06-07T05:00:20.000Z","message":{"role":"user","content":"This session is being continued. zzsummary prior context preserved"}}"#, "\n",
            r#"{"type":"system","subtype":"compact_boundary","uuid":"bnd1","timestamp":"2026-06-07T05:00:21.000Z","content":"Conversation compacted zzboundary","compactMetadata":{"trigger":"auto","preTokens":1000,"postTokens":200,"durationMs":50}}"#, "\n",
            r#"{"type":"attachment","uuid":"att1","timestamp":"2026-06-07T05:00:22.000Z","attachment":{"type":"hook_success","note":"zzattach should not surface"}}"#, "\n",
        ),
    );
    // C9: a subagent transcript whose opener seed is the delivered spawn prompt (parent ⇨ self).
    h.write(
        &format!("{ACC_ENC}/{ACC_SESS}/subagents/agent-{ACC_SUB}.jsonl"),
        concat!(
            r#"{"type":"user","isSidechain":true,"agentId":"c0ffeec0ffeec0ff","uuid":"so0","timestamp":"2026-06-07T05:00:03.500Z","message":{"role":"user","content":"zzopener do the delegated work please"}}"#, "\n",
            r#"{"type":"assistant","uuid":"sa0","parentUuid":"so0","timestamp":"2026-06-07T05:00:03.700Z","message":{"role":"assistant","content":[{"type":"text","text":"zzsubreply on it"}]}}"#, "\n",
        ),
    );
    h
}

/// `search <pattern> -t <selector>` over the top-level acceptance transcript only.
pub(crate) fn acc(h: &Home, pattern: &str, selector: &str) -> Output {
    h.run(&[
        "search",
        pattern,
        "-t",
        selector,
        &at(ACC_SESS),
        "--no-subagents",
    ])
}

pub(crate) const HOOKCTX_SESS: &str = "5c1d9e02-4b7a-4f3c-9d21-6e8a0b4c7d15";

/// One session: a genuine user turn, a `hook_additional_context` attachment record
/// (content ARRAY - two blocks, joined with `\n`), and the agent's reply.
pub(crate) fn hook_context_scenario(h: &Home) {
    let enc = "-Users-dev-example-project";
    let body = concat!(
        r#"{"type":"user","uuid":"u1","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"start the beacon work"}}"#,
        "\n",
        r#"{"parentUuid":"u1","attachment":{"type":"hook_additional_context","content":["<project-memory-context>\nquartzlantern rules apply\n</project-memory-context>","second block harborlight note"],"hookName":"SessionStart","hookEvent":"SessionStart"},"type":"attachment","uuid":"att1","timestamp":"2026-06-07T05:00:01.000Z"}"#,
        "\n",
        r#"{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"done with the beacon work"}]}}"#,
        "\n",
    );
    h.write(&format!("{enc}/{HOOKCTX_SESS}.jsonl"), body);
}

/// A session carrying GENERIC `type:"attachment"` records (edited_text_file +
/// compact_file_reference) plus one hook-context payload and a plain exchange - the
/// `search --attachments` gate / `--count-by attachment` census scenarios.
pub(crate) const ATTACH_SESS: &str = "7e2f4a19-8c3b-4d5e-a671-2b9d0c8e4f33";

pub(crate) fn attachment_scenario(h: &Home) {
    let enc = "-Users-dev-example-project";
    let body = concat!(
        r#"{"type":"user","uuid":"u1","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"start the beacon work"}}"#,
        "\n",
        r#"{"type":"attachment","uuid":"att1","parentUuid":"u1","timestamp":"2026-06-07T05:00:01.000Z","attachment":{"type":"edited_text_file","filePath":"/Users/dev/example-project/src/beacon.rs","snippet":"fn glowlantern() {}"}}"#,
        "\n",
        r#"{"type":"attachment","uuid":"att2","parentUuid":"u1","timestamp":"2026-06-07T05:00:02.000Z","attachment":{"type":"compact_file_reference","path":"/Users/dev/example-project/notes/harbor.md"}}"#,
        "\n",
        r#"{"type":"attachment","uuid":"att3","parentUuid":"u1","timestamp":"2026-06-07T05:00:03.000Z","attachment":{"type":"hook_additional_context","content":["mistgate token applies"],"hookEvent":"SessionStart"}}"#,
        "\n",
        r#"{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-07T05:00:04.000Z","message":{"role":"assistant","content":[{"type":"text","text":"done with the beacon work"}]}}"#,
        "\n",
    );
    h.write(&format!("{enc}/{ATTACH_SESS}.jsonl"), body);
}