csift 0.12.2

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
//! agents lifecycle: frozen lanes, returned messages, trigger-time truth.

use crate::harness::*;

#[test]
fn agents_frozen_lane_reports_escalation_blocked_not_completed() {
    // A background built-in subagent whose teardown Bash (dangerous `rm` of `$VAR/$f`) CC HOISTED
    // to a human approval prompt EVEN under bypass - its transcript freezes at the unreturned
    // tool_use, PRECEDED by assistant text (the L629→L630 shape that made the old walk-back
    // mis-report `completed`). csift must report it running + escalation-blocked, then NOT pending
    // once the result lands (Yes clicked). Mirrors the real fixture agent-ab8a4c5868015a8be.
    let enc = "-Users-testuser-Projects-frozen";
    let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
    let hex = "ab8a4c5868015a8be";
    let frozen = concat!(
        r#"{"type":"user","isSidechain":true,"agentId":"ab8a4c5868015a8be","timestamp":"2026-06-26T10:40:00.000Z","message":{"role":"user","content":"teardown"}}"#,
        "\n",
        r#"{"type":"assistant","timestamp":"2026-06-26T10:42:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Removing the transient credential files."}]}}"#,
        "\n",
        r#"{"type":"assistant","timestamp":"2026-06-26T10:43:31.906Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_0137gHdLDnXKsa94qGmmnbqV","name":"Bash","input":{"command":"for f in a.txt b.txt; do [ -f \"$SCRATCH/$f\" ] && rm -f \"$SCRATCH/$f\"; done"}}]}}"#,
        "\n",
    );
    let h = Home::new();
    h.write(
        &format!("{enc}/{sess}.jsonl"),
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T09:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"go\"}}\n",
    );
    h.write(&format!("{enc}/{sess}/subagents/agent-{hex}.jsonl"), frozen);

    let find = |stdout: &str| -> serde_json::Value {
        json_rows(stdout, "agent")
            .into_iter()
            .find(|n| n["agent_id"] == hex)
            .expect("the subagent row")
    };

    // FROZEN: running + escalation-blocked, NOT completed.
    let out = h.run(&["agents", &format!("@{sess}"), "--format", "json"]);
    assert!(out.success, "stderr: {}", out.stderr);
    let node = find(&out.stdout);
    assert_eq!(
        node["status"], "running",
        "a frozen lane must not be completed: {node}"
    );
    assert_eq!(node["pending_classification"], "escalation-blocked");
    assert_eq!(node["pending_tool_name"], "Bash");
    assert_eq!(
        node["pending_tool_use_id"],
        "toolu_0137gHdLDnXKsa94qGmmnbqV"
    );
    // completed_* is STATUS-GATED: a frozen lane carries no completion instant (the naive
    // `if completed_utc: done` consumer must not false-positive); its tail ts lives in
    // last_activity_* and equals the freeze instant.
    assert!(
        node["completed_utc"].is_null() && node["completed_local"].is_null(),
        "a frozen lane must not carry a completion instant: {node}"
    );
    assert!(
        node["duration"].is_null(),
        "no duration while frozen: {node}"
    );
    assert_eq!(node["last_activity_utc"], "2026-06-26T10:43:31.906Z");
    assert_eq!(node["last_activity_utc"], node["pending_since_utc"]);
    // Text surfaces the disambiguation prominently - and no "completed"/"last-seen" line
    // (the PENDING line already carries the freeze instant).
    let txt = h.run(&["agents", &format!("@{sess}")]);
    assert!(
        txt.stdout.contains("PENDING") && txt.stdout.contains("escalation-blocked"),
        "no pending line: {}",
        txt.stdout
    );
    // Mutation pins: the PENDING detail line sits one indent level UNDER its node head,
    // and the escalation class (and only that class) carries the dangerous-rm explainer.
    assert!(
        txt.stdout.contains("\n    PENDING"),
        "PENDING must be an indented detail line: {}",
        txt.stdout
    );
    assert!(
        txt.stdout.contains("HOISTS"),
        "escalation-blocked carries the hoist explainer: {}",
        txt.stdout
    );
    assert!(
        !txt.stdout.contains("completed  2026") && !txt.stdout.contains("last-seen"),
        "frozen lane must not print a terminal-instant line: {}",
        txt.stdout
    );

    // RESOLVED (Yes clicked → tool_result + closing text) → completed, no pending.
    let resolved = format!(
        "{frozen}{}{}",
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T11:20:13.911Z\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_0137gHdLDnXKsa94qGmmnbqV\",\"content\":\"shredded\"}]}}\n",
        "{\"type\":\"assistant\",\"timestamp\":\"2026-06-26T11:21:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Teardown complete.\"}]}}\n"
    );
    h.write(
        &format!("{enc}/{sess}/subagents/agent-{hex}.jsonl"),
        &resolved,
    );
    let out2 = h.run(&["agents", &format!("@{sess}"), "--format", "json"]);
    let node2 = find(&out2.stdout);
    assert_eq!(node2["status"], "completed");
    assert!(
        node2["pending_classification"].is_null(),
        "resolved lane must not be pending: {node2}"
    );
    // Completed lane: completion instant present and == last activity.
    assert_eq!(node2["completed_utc"], "2026-06-26T11:21:00.000Z");
    assert_eq!(node2["last_activity_utc"], node2["completed_utc"]);
}

#[test]
fn agents_returned_message_on_open_lane_carries_history_caution() {
    // R8: a frozen teammate's newest returned message read like a clean finale ("work is
    // complete, confirming shutdown") and nearly fooled a real reader. On a NON-completed
    // lane the text render brands the message as history inline; a completed lane stays
    // unbranded.
    let enc = "-Users-testuser-Projects-rmcaution";
    let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
    let hex = "ab8a4c5868015a8be";
    let frozen = concat!(
        r#"{"type":"user","isSidechain":true,"agentId":"ab8a4c5868015a8be","timestamp":"2026-06-26T10:40:00.000Z","message":{"role":"user","content":"teardown"}}"#,
        "\n",
        r#"{"type":"assistant","timestamp":"2026-06-26T10:42:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Work is complete — confirming the shutdown request."}]}}"#,
        "\n",
        r#"{"type":"assistant","timestamp":"2026-06-26T10:43:31.906Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_0137gHdLDnXKsa94qGmmnbqV","name":"Bash","input":{"command":"echo wait"}}]}}"#,
        "\n",
    );
    let h = Home::new();
    h.write(
        &format!("{enc}/{sess}.jsonl"),
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T09:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"go\"}}\n",
    );
    h.write(&format!("{enc}/{sess}/subagents/agent-{hex}.jsonl"), frozen);

    // FROZEN lane: the returned line must carry the inline history caution.
    let out = h.run(&["agents", &format!("@{sess}"), "--returned-message"]);
    assert!(out.success, "stderr: {}", out.stderr);
    assert!(
        out.stdout
            .contains("history — predates the still-open lane, NOT the outcome"),
        "open-lane returned message must be branded as history: {}",
        out.stdout
    );

    // RESOLVED (tool_result + closing text) → completed lane, no caution.
    let resolved = format!(
        "{frozen}{}{}",
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T11:20:13.911Z\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_0137gHdLDnXKsa94qGmmnbqV\",\"content\":\"ok\"}]}}\n",
        "{\"type\":\"assistant\",\"timestamp\":\"2026-06-26T11:21:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Teardown complete.\"}]}}\n"
    );
    h.write(
        &format!("{enc}/{sess}/subagents/agent-{hex}.jsonl"),
        &resolved,
    );
    let out2 = h.run(&["agents", &format!("@{sess}"), "--returned-message"]);
    assert!(
        out2.stdout.contains("returned") && !out2.stdout.contains("predates the still-open lane"),
        "a completed lane's returned message must stay unbranded: {}",
        out2.stdout
    );
}

#[test]
fn agents_running_not_frozen_prints_last_seen_not_completed() {
    // A lane whose NEWEST meaningful record is a returned tool_result with NO closing
    // assistant text: not frozen (nothing pending), not completed (no terminal message) -
    // the honest middle. Its tail instant must surface as last_activity/"last-seen",
    // NEVER as a fabricated completion.
    let enc = "-Users-testuser-Projects-midflight";
    let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
    let hex = "beef4c5868015a8be";
    let h = Home::new();
    h.write(
        &format!("{enc}/{sess}.jsonl"),
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T09:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"go\"}}\n",
    );
    h.write(
        &format!("{enc}/{sess}/subagents/agent-{hex}.jsonl"),
        concat!(
            r#"{"type":"user","isSidechain":true,"agentId":"beef4c5868015a8be","timestamp":"2026-06-26T10:00:00.000Z","message":{"role":"user","content":"scan"}}"#,
            "\n",
            r#"{"type":"assistant","timestamp":"2026-06-26T10:01:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_mid1","name":"Bash","input":{"command":"ls"}}]}}"#,
            "\n",
            r#"{"type":"user","timestamp":"2026-06-26T10:02:00.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_mid1","content":"ok"}]}}"#,
            "\n",
        ),
    );
    let out = h.run(&["agents", &format!("@{sess}"), "--format", "json"]);
    assert!(out.success, "stderr: {}", out.stderr);
    let node = json_rows(&out.stdout, "agent")
        .into_iter()
        .find(|n| n["agent_id"] == hex)
        .expect("the subagent row");
    assert_eq!(node["status"], "running", "honest middle: {node}");
    assert!(node["pending_classification"].is_null(), "{node}");
    assert!(
        node["completed_utc"].is_null() && node["duration"].is_null(),
        "running lane must not claim completion: {node}"
    );
    assert_eq!(node["last_activity_utc"], "2026-06-26T10:02:00.000Z");
    let txt = h.run(&["agents", &format!("@{sess}")]);
    assert!(
        txt.stdout.contains("last-seen") && !txt.stdout.contains("completed  2026"),
        "text must print last-seen, not completed: {}",
        txt.stdout
    );
}

#[test]
fn agents_row_without_timestamps_omits_duration() {
    // A subagent whose transcript records carry NO timestamps → started/completed are
    // both absent → `duration_label` returns None (the `if let Some(dur)` FALSE arm),
    // so NO "duration" line is rendered for that row; status is `unknown`.
    let h = Home::new();
    h.write(
        &format!("{ENC}/{SESS}.jsonl"),
        "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
    );
    h.write(
        &format!("{ENC}/{SESS}/subagents/agent-nots99.jsonl"),
        concat!(
            r#"{"type":"user","isSidechain":true,"agentId":"nots99","message":{"role":"user","content":"start, no timestamp"}}"#, "\n",
            r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"Bash","input":{}}]}}"#, "\n",
        ),
    );
    let out = h.run(&["agents", at(SESS).as_str()]);
    assert!(out.success, "stderr: {}", out.stderr);
    assert!(
        out.stdout.contains("unknown"),
        "status unknown: {}",
        out.stdout
    );
    assert!(
        !out.stdout.contains("duration"),
        "no duration line: {}",
        out.stdout
    );
}

#[test]
fn agents_true_trigger_time_is_the_parent_tool_use_ts() {
    // The default axis is TRIGGER: the built-in's `trigger_utc` is the parent Agent
    // tool_use ts (04:59:58), which DIVERGES from its child-head `started_utc`
    // (05:00:00) - proving the topology recovered the true spawn instant.
    let h = topology_home();
    let out = h.run(&["agents", at(SESS).as_str(), "--format", "json"]);
    assert!(out.success, "stderr: {}", out.stderr);
    // v0.5 flat rows: the built-in topo11 is its own `kind:"agent"` row.
    let rows = json_rows(&out.stdout, "agent");
    let builtin = rows
        .iter()
        .find(|v| v.get("agent_id").and_then(|a| a.as_str()) == Some("topo11"))
        .expect("the built-in topo11 row");
    assert_eq!(builtin["trigger_utc"], "2026-06-07T04:59:58.000Z");
    assert_eq!(builtin["started_utc"], "2026-06-07T05:00:00.000Z");
    assert_ne!(builtin["trigger_utc"], builtin["started_utc"]);
    assert_eq!(builtin["spawn_tool"], "Agent");
    assert_eq!(builtin["spawn_tool_use_id"], "toolu_x");
}

#[test]
fn agents_default_axis_is_trigger_not_start() {
    // A bound BETWEEN the trigger (04:59:58) and the start (05:00:00): the DEFAULT
    // (trigger) axis EXCLUDES the built-in (triggered before the bound); `--order-by start`
    // INCLUDES it (started after the bound). Proves the default flipped to trigger.
    let h = topology_home();
    let default_axis = h.run(&[
        "agents",
        at(SESS).as_str(),
        "--since",
        "2026-06-07T04:59:59Z",
        "--shape",
        "builtin-task",
        "--format",
        "json",
    ]);
    assert!(default_axis.success, "stderr: {}", default_axis.stderr);
    let default_has_topo11 = default_axis.stdout.contains("topo11");
    assert!(
        !default_has_topo11,
        "default (trigger) axis must EXCLUDE topo11 triggered before the bound: {}",
        default_axis.stdout
    );
    let by_start = h.run(&[
        "agents",
        at(SESS).as_str(),
        "--since",
        "2026-06-07T04:59:59Z",
        "--order-by",
        "start",
        "--shape",
        "builtin-task",
        "--format",
        "json",
    ]);
    assert!(by_start.success, "stderr: {}", by_start.stderr);
    assert!(
        by_start.stdout.contains("topo11"),
        "--order-by start must INCLUDE topo11 started after the bound: {}",
        by_start.stdout
    );
    // The footer reflects the default axis.
    let footer = h.run(&["agents", at(SESS).as_str()]);
    assert!(footer.stdout.contains("window-axis=trigger"));
}

#[test]
fn agents_returned_message_three_way_resolution() {
    let h = topology_home();
    let out = h.run(&[
        "agents",
        at(SESS).as_str(),
        "--returned-message",
        "--format",
        "json",
    ]);
    assert!(out.success, "stderr: {}", out.stderr);
    // v0.5 flat rows: builtin + workflow agents are both `kind:"agent"` rows; the run is
    // its own `kind:"run"` row (workflow_id joins them).
    let rows = json_rows(&out.stdout, "agent");
    let builtin = rows
        .iter()
        .find(|v| v["agent_id"] == "topo11")
        .expect("topo11");
    // SYNC built-in → parent tool_result text.
    assert_eq!(
        builtin["returned_message"],
        "SYNC-RETURN: the built-in carry answer"
    );
    assert_eq!(builtin["returned_message_source"], "sync-tool-result");
    let wf = rows
        .iter()
        .find(|v| v["agent_id"] == "topo22")
        .expect("topo22");
    // WORKFLOW → journal result payload.
    assert_eq!(wf["returned_message"], "WF-RETURN: journal payload");
    assert_eq!(wf["returned_message_source"], "workflow-journal");
    // The run row precedes its member agent row and carries the run metadata.
    let runs = json_rows(&out.stdout, "run");
    assert_eq!(runs.len(), 1, "{}", out.stdout);
    assert_eq!(runs[0]["run_id"], "wf_topo");
    assert_eq!(runs[0]["workflow_name"], "carry-wf");
    assert_eq!(wf["workflow_id"], "wf_topo", "join key intact: {wf}");
}

#[test]
fn agents_returned_message_omitted_by_default() {
    // Without --returned-message (and without --agent), the returned message is NOT in
    // the JSON - keeping a plain listing compact.
    let h = topology_home();
    let out = h.run(&["agents", at(SESS).as_str(), "--format", "json"]);
    assert!(out.success, "stderr: {}", out.stderr);
    let rows = json_rows(&out.stdout, "agent");
    assert!(!rows.is_empty());
    assert!(
        rows.iter().all(|n| n.get("returned_message").is_none()),
        "returned_message must be omitted by default: {rows:?}"
    );
}

/// One frozen lane fixture: a seed record plus an unreturned Bash `tool_use`.
fn removal_lane(hex: &str, version: &str, command: &str) -> String {
    let ver = if version.is_empty() {
        String::new()
    } else {
        format!("\"version\":\"{version}\",")
    };
    format!(
        concat!(
            r#"{{"type":"user","isSidechain":true,"agentId":"{hex}","timestamp":"2026-06-26T10:40:00.000Z","message":{{"role":"user","content":"work"}}}}"#,
            "\n",
            r#"{{"type":"assistant",{ver}"timestamp":"2026-06-26T10:43:00.000Z","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"toolu_{hex}","name":"Bash","input":{{"command":"{command}"}}}}]}}}}"#,
            "\n"
        ),
        hex = hex,
        ver = ver,
        command = command
    )
}

#[test]
fn agents_pending_reason_names_the_checker_and_the_generation() {
    // Five frozen lanes, one per shape the ported decision has to separate. A lane is
    // `escalation-blocked` only where the harness of THAT lane's own Claude Code version
    // would hold the call for a human even under bypass permissions.
    let enc = "-Users-testuser-Projects-removals";
    let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
    let h = Home::new();
    h.write(
        &format!("{enc}/{sess}.jsonl"),
        "{\"type\":\"user\",\"timestamp\":\"2026-06-26T09:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"go\"}}\n",
    );
    // A too-complex parse whose `&&` clause the lexical classifier reaches.
    h.write(
        &format!("{enc}/{sess}/subagents/agent-aaa1111111111111.jsonl"),
        &removal_lane(
            "aaa1111111111111",
            "2.1.258",
            "for f in a; do [ -f \\\"$S/$f\\\" ] && rm -f \\\"$S/$f\\\"; done",
        ),
    );
    // A clean parse whose target's dirname is the filesystem root.
    h.write(
        &format!("{enc}/{sess}/subagents/agent-bbb2222222222222.jsonl"),
        &removal_lane("bbb2222222222222", "2.1.258", "rm -rf /tmp"),
    );
    // A clean parse with no dangerous shape at all.
    h.write(
        &format!("{enc}/{sess}/subagents/agent-ccc3333333333333.jsonl"),
        &removal_lane("ccc3333333333333", "2.1.258", "rm file.txt"),
    );
    // A prefix command inside a too-complex body: only the rewritten classifier of
    // 2.1.261 and later walks past `then` and `sudo` to the target.
    h.write(
        &format!("{enc}/{sess}/subagents/agent-ddd4444444444444.jsonl"),
        &removal_lane(
            "ddd4444444444444",
            "2.1.268",
            "if true; then sudo rm -rf $D/*; fi",
        ),
    );
    // The same command on a record carrying NO version: the port falls to the generation
    // of the build the introspection ledger is verified against, and discloses it.
    h.write(
        &format!("{enc}/{sess}/subagents/agent-eee5555555555555.jsonl"),
        &removal_lane("eee5555555555555", "", "if true; then sudo rm -rf $D/*; fi"),
    );

    let out = h.run(&["agents", &format!("@{sess}"), "--format", "json"]);
    assert!(out.success, "stderr: {}", out.stderr);
    let rows = json_rows(&out.stdout, "agent");
    let node = |hex: &str| -> serde_json::Value {
        rows.iter()
            .find(|n| n["agent_id"] == hex)
            .unwrap_or_else(|| panic!("row {hex} in {rows:?}"))
            .clone()
    };

    let lexical = node("aaa1111111111111");
    assert_eq!(lexical["pending_classification"], "escalation-blocked");
    assert_eq!(
        lexical["pending_reason"], "on possibly-empty variable path: \"$S/$f\"",
        "{lexical}"
    );
    assert!(
        lexical["pending_checker"]
            .as_str()
            .unwrap_or_default()
            .contains("checker: lexical"),
        "{lexical}"
    );

    let critical = node("bbb2222222222222");
    assert_eq!(critical["pending_classification"], "escalation-blocked");
    assert_eq!(critical["pending_reason"], "on critical path: /tmp");
    assert_eq!(
        critical["pending_checker"],
        "path: structured · checker: structured · gen2"
    );

    let plain = node("ccc3333333333333");
    assert_eq!(plain["pending_classification"], "awaiting-execution");
    assert_eq!(
        plain["pending_reason"], "removal target needs the filesystem state at the time",
        "the workspace arm reads the filesystem, so csift declines to decide: {plain}"
    );

    let gen3 = node("ddd4444444444444");
    assert_eq!(
        gen3["pending_classification"], "escalation-blocked",
        "the 2.1.261 token walk reaches a prefixed removal: {gen3}"
    );
    assert!(
        gen3["pending_checker"]
            .as_str()
            .unwrap_or_default()
            .contains("gen3"),
        "{gen3}"
    );

    let assumed = node("eee5555555555555");
    assert_eq!(
        assumed["pending_classification"], "awaiting-execution",
        "with no version the assumed generation cannot reach it: {assumed}"
    );
    assert!(
        assumed["pending_checker"]
            .as_str()
            .unwrap_or_default()
            .contains("gen2 assumed"),
        "an assumed generation says so: {assumed}"
    );

    // The text tree carries the same two facts under the PENDING line.
    let txt = h.run(&["agents", &format!("@{sess}")]);
    assert!(
        txt.stdout.contains("reason: on critical path: /tmp"),
        "{}",
        txt.stdout
    );
    assert!(txt.stdout.contains("checker: structured"), "{}", txt.stdout);
}