claude_storage 1.0.0

CLI tool for exploring Claude Code filesystem storage
Documentation
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
# Test Suite Organization

## Overview

The claude_storage test suite uses automated tests for parameter validation and integration testing, with manual tests for exploratory and user experience validation.

## Test Structure

```
tests/
├── readme.md                              # This file - test suite organization
├── common/                                 # Shared test utilities
│   └── mod.rs                             # Pre-compiled binary helper (cargo_bin!)
├── behavior/                               # Behavior hypothesis invalidation tests (B1..B11)
│   ├── mod.rs                             # Test binary root; shared helpers for real ~/.claude/ inspection
│   ├── b01_default_continues.rs           # B1 — default invocation continues most recent session
│   ├── b02_new_session.rs                 # B2 — --new-session creates separate .jsonl
│   ├── b03_print_flag.rs                  # B3 — -p is output mode, not session flag
│   ├── b04_continue_flag.rs              # B4 — -c aliases default continuation
│   ├── b05_mtime_selection.rs             # B5 — current session selected by mtime
│   ├── b06_session_accumulation.rs        # B6 — sessions accumulate as separate files
│   ├── b07_agent_sessions.rs              # B7 — agent sessions are agent-*.jsonl siblings
│   ├── b08_zero_byte_init.rs              # B8 — 0-byte .jsonl created as placeholder on startup
│   ├── b09_storage_path.rs                # B9 — project path uses /→- encoding
│   ├── b10_entry_threading.rs             # B10 — entries linked via parentUuid
│   └── b11_auto_continue.rs              # B11 — CLAUDE_CODE_AUTO_CONTINUE env var
├── manual/                                 # Manual testing plans and results
│   └── readme.md                          # Manual testing plan for this crate
├── cli_commands.rs                        # CLI command storage operations
├── cli_sanity.rs                          # CLI binary build and run verification
├── command_version_consistency_test.rs    # Command version consistency tests
├── content_display_integration_test.rs    # Content display behavior tests
├── count_command_bug_fix.rs               # .count context-awareness bug fix (Bug #003)
├── export_command_test.rs                 # .export parameter validation tests (Phase 1C)
├── lib_test.rs                            # Library API smoke tests
├── list_command_test.rs                   # .list parameter bounds and combinations
├── list_smart_session_display.rs          # .list smart session display tests
├── parameter_validation_test.rs           # Multi-command parameter validation tests
├── path_resolution_integration_test.rs    # Path resolution tests
├── path_resolution_test.rs                # Path resolution unit tests
├── project_parameter_bug_fix.rs           # Project parameter parsing tests
├── project_parameter_multi_command_bug.rs # Project parameter across commands (#012)
├── project_parameter_relative_path_bug.rs # Relative path resolution (#013)
├── search_command_test.rs                 # .search parameter validation tests (Phase 1B)
├── search_session_partial_uuid_bug.rs     # .search session partial UUID fix (issue-020)
├── search_special_characters_bug.rs       # Special character handling (Bug #006, #007)
├── session_path_command_test.rs           # .path/.exists/.session.dir/.session.ensure lifecycle commands
├── projects_command_test.rs               # .projects scope-aware listing, family tree display, mode boundaries, parameter validation (issues 024/029/031/032, IT-47)
├── projects_output_format_test.rs         # .projects output format: path headers, agent collapse (IT-17..IT-22); project-centric redesign (IT-50..IT-53)
├── projects_zero_byte_count_bug.rs        # .projects zero-byte session exclusion from header count (issue-034, IT-54..IT-56)
├── smart_show_command.rs                  # .show smart parameter detection tests
├── status_path_test.rs                    # .status path parameter tests (Phase 1D)
└── truncate_utf8_bug.rs                   # Truncation safety on multibyte UTF-8 (issue-018)
```

## Responsibility Table

| File | Responsibility |
|------|----------------|
| `lib_test.rs` | Library API: `COMMANDS_YAML` exists, `register_commands()` callable |
| `common/mod.rs` | Pre-compiled binary helper for integration tests |
| `behavior/mod.rs` | Behavior test binary root; shared helpers for real `~/.claude/` inspection |
| `behavior/b01_default_continues.rs` | B1: resumable session exists in real storage |
| `behavior/b02_new_session.rs` | B2: `--new-session` documented in `claude --help` |
| `behavior/b03_print_flag.rs` | B3: `-p`/`--print` flag documented in `claude --help` |
| `behavior/b04_continue_flag.rs` | B4: `-c`/`--continue` flag documented in `claude --help` |
| `behavior/b05_mtime_selection.rs` | B5: multiple sessions have distinct observable mtimes |
| `behavior/b06_session_accumulation.rs` | B6: sessions accumulate as separate `.jsonl` files |
| `behavior/b07_agent_sessions.rs` | B7: agent sessions are `agent-*.jsonl` siblings with `isSidechain:true` |
| `behavior/b08_zero_byte_init.rs` | B8: 0-byte `.jsonl` placeholder files exist in real storage |
| `behavior/b09_storage_path.rs` | B9: project dir names follow `/``-` encoding convention |
| `behavior/b10_entry_threading.rs` | B10: conversation entries linked via `parentUuid` (null root, non-null chain) |
| `behavior/b11_auto_continue.rs` | B11: `CLAUDE_CODE_AUTO_CONTINUE` env var recognized by `claude` |
| `cli_commands.rs` | Test CLI command storage operations |
| `cli_sanity.rs` | Verify CLI binary builds and runs |
| `command_version_consistency_test.rs` | Validate version annotation consistency |
| `content_display_integration_test.rs` | Test content-first display (REQ-011) |
| `count_command_bug_fix.rs` | Test .count context-awareness and path projects |
| `export_command_test.rs` | Validate .export command parameters |
| `list_command_test.rs` | Validate .list command parameter bounds and combinations |
| `list_smart_session_display.rs` | Test smart session display in .list |
| `parameter_validation_test.rs` | Validate CLI parameter handling |
| `path_resolution_integration_test.rs` | Test path resolution in .list command |
| `path_resolution_test.rs` | Test path:: parameter smart detection |
| `project_parameter_bug_fix.rs` | Test project parameter ID resolution |
| `project_parameter_multi_command_bug.rs` | Test project parameter across commands |
| `project_parameter_relative_path_bug.rs` | Test relative path resolution (Finding #013) |
| `search_command_test.rs` | Validate .search command parameters |
| `search_session_partial_uuid_bug.rs` | Test partial UUID matching in .search session filter |
| `search_special_characters_bug.rs` | Test special character handling in queries |
| `session_path_command_test.rs` | Test .path/.exists/.session.dir/.session.ensure lifecycle commands |
| `projects_command_test.rs` | Test .projects scope filtering, family tree, mode boundaries, parameter validation |
| `projects_output_format_test.rs` | Test .projects output format: path headers, agent collapse (IT-17..22); project-centric redesign (IT-50..53) |
| `projects_zero_byte_count_bug.rs` | Test zero-byte session exclusion from .projects list-mode header count (issue-034) |
| `smart_show_command.rs` | Test location-aware .show command |
| `status_path_test.rs` | Test path parameter in .status command |
| `truncate_utf8_bug.rs` | Test truncation safety on multibyte UTF-8 (issue-018) |

## Test Documentation Standards

### Feature Tests (New Commands/Parameters)

Use 4-section Purpose format:

```rust
/// Test {command} {parameter} {validation_type}
///
/// ## Purpose
/// {What this test validates and why it matters}
///
/// ## Coverage
/// {Specific corner case or requirement being tested}
///
/// ## Validation Strategy
/// {How the test verifies behavior - assertions used}
///
/// ## Related Requirements
/// {REQ-NNN or doc instance (docs/feature/NNN_name.md) that this test validates}
#[test]
fn test_{command}_{parameter}_{case}()
```

**Examples**:
- `tests/search_command_test.rs::test_search_query_required`
- `tests/export_command_test.rs::test_export_session_id_required`
- `tests/status_path_test.rs::test_status_custom_path`

### Bug Fix Tests (Finding #NNN)

Use 5-section Root Cause format with Fix comment in source:

```rust
/// Test {command} {parameter} {issue} (Finding #NNN)
///
/// ## Root Cause
/// {Technical explanation of why bug occurred}
///
/// ## Why Not Caught
/// {Gap in existing tests that allowed bug}
///
/// ## Fix Applied
/// {What validation was added}
///
/// ## Prevention
/// {Policy to prevent similar bugs}
///
/// ## Pitfall
/// {Anti-pattern that caused bug}
#[test]
fn test_{command}_{parameter}_{issue}()
```

**Source Code Fix Comment** (3 required fields):
```rust
// Fix(issue-NNN): {One-line description}
//
// Root cause: {Why bug occurred}
//
// Pitfall: {Anti-pattern to avoid}
```

**Example**:
- Test: `tests/search_command_test.rs::test_search_verbosity_invalid`
- Fix comment: `src/cli/mod.rs:1183-1200` (Finding #010)

## Integration Test Strategy

Tests that depend on real storage state or external resources should be marked `#[ignore]`:

```rust
#[test]
#[ignore = "Integration test - depends on actual ~/.claude/ storage state"]
fn test_status_default_path()
```

**Why**:
- Prevents test failures due to environmental factors (corrupted sessions, missing directories)
- Allows tests to be run selectively with `cargo test -- --ignored`
- Separates unit/validation tests from integration tests

**Examples**:
- `tests/status_path_test.rs::test_status_default_path` - depends on ~/.claude/ state
- `tests/search_command_test.rs::test_search_entry_type_valid` - requires real session data
- `tests/export_command_test.rs::test_export_format_valid` - requires real session data

## Test Naming Conventions

```
test_{command}_{parameter}_{scenario}
```

**Examples**:
- `test_search_query_required` - .search command, query parameter, required validation
- `test_export_format_invalid` - .export command, format parameter, invalid value rejection
- `test_status_path_with_verbosity` - .status command, path+verbosity parameters, interaction

## Test Organization Principles

### Command-Specific Files

Each command gets its own test file for parameter validation:
- `search_command_test.rs` - .search parameter validation
- `export_command_test.rs` - .export parameter validation
- `status_path_test.rs` - .status path parameter tests

### Shared Validation Files

Cross-command tests in shared files:
- `parameter_validation_test.rs` - Multi-command parameter validation tests

### Integration Test Files

Feature-specific integration tests:
- `content_display_integration_test.rs` - Content display behavior
- `list_smart_session_display.rs` - Smart session display auto-enable
- `path_resolution_integration_test.rs` - Path resolution with real filesystem

## Test Quality Standards

### Documentation Quality

Test documentation must be:
- **Specific**: Technical details, not generic statements ("Fixed bug" → "search_routine missing verbosity validation")
- **Actionable**: Clear prevention steps ("Don't assume defaults prevent invalid input")
- **Traceable**: Links to requirements (REQ-012), issues (Finding #010), source locations
- **Concise**: Essential information only, no redundancy

### Test Coverage

All parameters must have validation tests:
- Required parameters → test missing parameter error
- Optional parameters → test default value behavior
- Enumerated values → test invalid value rejection
- Ranges → test boundary values and out-of-range rejection
- Booleans → test invalid value rejection (not 0 or 1)

### No Mocking

Tests must use real implementations or be marked `#[ignore]`:
- ✅ Use `TempDir` for real filesystem operations
- ✅ Mark tests requiring real storage as `#[ignore]`
- ❌ Don't mock Storage, Command, or core functionality

## Test Execution Architecture

Integration tests use a pre-compiled binary helper (`common::claude_storage_cmd()`)
instead of `cargo run` to avoid compilation during test execution.

**Why**: Each `cargo run` inside a test triggers a full cargo compilation cycle
(300s+). Under workspace-wide nextest runs, this exceeds the 300s timeout.

**Fix**: `assert_cmd::cargo::cargo_bin!("claude_storage")` resolves to the binary
path built by nextest BEFORE test execution. No recompilation at test time.

**Pattern**: All test files declare `mod common;` and use `common::claude_storage_cmd()`
instead of `Command::new("cargo").args(["run", ...])`.

**Test Isolation with `CLAUDE_STORAGE_ROOT`**:

Tests that write fixture data use the `CLAUDE_STORAGE_ROOT` env var to redirect storage
to a `TempDir`, so they never touch real `~/.claude/` state and run safely in parallel:

```rust
let dir = tempfile::TempDir::new().unwrap();
// write fixture data under dir.path()...
let output = common::claude_storage_cmd()
  .env("CLAUDE_STORAGE_ROOT", dir.path())
  .args([".list"])
  .output()
  .unwrap();
```

Set the env var on the **subprocess** (`cmd.env(…)`), NOT via `std::env::set_var()`,
which is process-wide and causes nextest parallel-test race conditions.

## Test Verification Commands

```bash
# Run all effective tests (excludes ignored tests)
w3 .test l::3           # Default (recommended)
ctest3                  # Alias for w3 .test l::3

# Run specific test file
cargo nextest run --test search_command_test --all-features

# Run ignored tests only
cargo nextest run --all-features -- --ignored

# Run all tests including ignored
cargo nextest run --all-features -- --include-ignored
```

## Test Count Tracking

**Current Status**: 284 tests, 0 ignored
- Effective tests: 284 (all tests run fully)
- Ignored tests: 0 (target met — all tests use `CLAUDE_STORAGE_ROOT` + `TempDir` isolation)

## Known Findings

### Finding #009: .count target parameter validation
- **Issue**: Missing validation for target parameter (accepted invalid values)
- **Tests**: 4 tests added in `parameter_validation_test.rs`
- **Fix**: Added validation at `src/cli/mod.rs:1151-1157`
- **Documentation**: Fix(issue-009) comment in source

### Finding #010: .search verbosity parameter validation
- **Issue**: search_routine missing verbosity range validation (0-5), inconsistent with other commands
- **Test**: `test_search_verbosity_invalid` in `search_command_test.rs`
- **Fix**: Added validation at `src/cli/mod.rs:1183-1200`
- **Root Cause**: Assumed default values prevent invalid input (they don't)
- **Documentation**: Fix(issue-010) comment in source + 5-section test documentation

### Finding #013: Relative path resolution in project parameter
- **Issue**: parse_project_parameter does not resolve ".", "..", "~" as paths
- **Tests**: 4 tests in `project_parameter_relative_path_bug.rs`
- **Fix**: Added relative path detection before UUID default case
- **Root Cause**: Only handled absolute paths, path-encoded, and UUID; missed relative paths
- **Documentation**: Fix(issue-013) comment in source + 5-section test documentation

### Finding #014: Path resolution in status_routine
- **Issue**: status_routine does not resolve ".", "..", "~" in path parameter
- **Tests**: 2 tests in `status_path_test.rs` (test_status_path_dot_resolves_to_cwd, test_status_path_tilde_resolves_to_home)
- **Fix**: Added resolve_path_parameter() call before Storage::with_root()
- **Root Cause**: status_routine passed path directly without resolving, unlike list_routine
- **Documentation**: Fix(issue-014) comment in source + 5-section test documentation

### Finding #015: list_routine missing verbosity range validation ✅ Fixed
- **Issue**: `list_routine` did not validate verbosity 0-5 range; `-1` or `6` were silently accepted
- **Tests**: 5 verbosity tests in `list_command_test.rs` (N: -1, 6; P: 0, 3, 5)
- **Fix**: Added `if !(0..=5).contains(&verbosity)` check in `list_routine` after get_integer call
- **Root Cause**: Verbosity extracted without bounds check, unlike `status_routine` and `search_routine`
- **Documentation**: Fix(issue-015) comment in `src/cli/mod.rs` + 5-section test documentation

### Finding #016: show_project_routine missing verbosity range validation ✅ Fixed (command removed in task-013)
- **Issue**: `show_project_routine` did not validate verbosity 0-5 range; same gap as Finding #015
- **Tests**: 4 verbosity tests existed — test file deleted with command removal (task-013)
- **Fix**: Added `if !(0..=5).contains(&verbosity)` check in `show_project_routine` after get_integer call
- **Root Cause**: Verbosity passed unvalidated to impl functions; invalid values produced garbled output
- **Note**: `.show.project` command removed in task-013; pattern applies to any routine that accepts verbosity

### issue-015: .status performance — global_stats() O(total JSONL bytes)
- **Issue**: `.status` took >2 minutes with 1903 projects / 7 GB JSONL
- **Tests**: `status_global_stats_fast_bug.rs` in `claude_storage_core/tests/`
- **Fix**: Added `global_stats_fast()` (filesystem-only); `status_routine` uses it for verbosity 0-1
- **Root Cause**: `global_stats()` parsed all session JSONL to count entries/tokens — O(total JSONL bytes)
- **Documentation**: Fix(issue-015) in `storage.rs` + `status_global_stats_fast_bug.rs`

### issue-016: count_entries() counted all JSONL lines, not conversation entries
- **Issue**: `.count target::entries` returned 2135 while `.show` "Total Entries" showed 2034 (101 discrepancy)
- **Tests**: `count_entries_bug.rs` in `claude_storage_core/tests/`
- **Fix**: Changed `count_entries()` to parse `"type"` field and count only `"user"`/`"assistant"` entries
- **Root Cause**: `content.lines().count()` counted every non-empty JSONL line including metadata
- **Documentation**: Fix(issue-016) in `session.rs` + `count_entries_bug.rs`

### issue-017: .count crashes on IO errors in sessions instead of skipping with warning
- **Issue**: `.count` from a project with any session causing an IO error (e.g., unreadable file) failed entirely (exit 1)
- **Test**: `test_count_skips_unreadable_sessions` in `count_command_bug_fix.rs`
- **Fix**: Changed `?` to `match` + `eprintln!` warning in context-aware loop in `count_routine()`
- **Root Cause**: `?` propagated `count_entries()` IO errors from individual sessions to entire command
- **Note**: Truncated JSONL does NOT trigger this — `count_entries()` uses byte-level search and succeeds on partial lines; only IO errors (e.g., permission denied) cause failure
- **Documentation**: Fix(issue-017) in `cli/mod.rs` + 5-section test doc in `count_command_bug_fix.rs`

### issue-018: `truncate_if_needed` panics on multibyte UTF-8 text
- **Issue**: `&text[..len]` slices by byte offset, panicking when `len` lands inside a multibyte UTF-8 sequence (emoji, CJK, accented characters)
- **Tests**: 7 tests in `truncate_utf8_bug.rs` (tc001-tc007: emoji, CJK, boundary, zero-length)
- **Fix**: Walk backwards from `len` using `is_char_boundary()` to find nearest valid boundary
- **Root Cause**: `str::len()` returns bytes, not characters; using it directly as a slice bound on user-supplied text panics on non-ASCII content
- **Documentation**: Fix(issue-018) in `cli/mod.rs` + 5-section test doc in `truncate_utf8_bug.rs`

### issue-025: Singular/plural mismatch in "Found N X:" output headers
- **Issue**: `.list`, `.search`, and `.projects` all output "Found 1 projects:", "Found 1 matches:", "Found 1 sessions:" — incorrect plural when count == 1
- **Tests**: 7 tests across 3 files (IT-14..IT-16 in `projects_command_test.rs`; 2 in `list_command_test.rs`; 2 in `search_command_test.rs`)
- **Fix**: Derive noun ("project"/"projects", "match"/"matches", "session"/"sessions") from count before formatting header; zero uses plural
- **Root Cause**: `writeln!(output, "Found {} noun:\n", count)` used a hardcoded plural noun string regardless of count
- **Documentation**: 5-section doc block at issue-025 comment in each test file; source changes are minimal inline fixes

### issue-027: list_routine verbosity 1 per-project session count uses wrong plural
- **Issue**: `.list sessions::1` showed `Uuid("proj") (1 sessions)` — should be `(1 session)` (singular)
- **Tests**: `test_list_session_count_singular_when_one_session`, `test_list_session_count_plural_when_multiple_sessions` in `list_command_test.rs`
- **Fix**: Derive `noun` from `session_count` before format string, same pattern as issue-025 header fix
- **Root Cause**: `writeln!(output, "{:?} ({} sessions)", ...)` used hardcoded plural — sibling of the issue-025 bug in a different format string in the same routine
- **Documentation**: Fix(issue-027) in `cli/mod.rs` + 5-section test doc in `list_command_test.rs`

### issue-026: export_session_to_file uses bare `?` losing path context in IO errors
- **Issue**: `.export output::/nonexistent/dir/file.md` produced "I/O error during unknown operation: No such file or directory" with no indication of which path failed
- **Test**: `test_export_output_path_in_error_message` in `export_command_test.rs`
- **Fix**: Changed `File::create(output_path)?` to `.map_err(|e| Error::io(e, format!("create output file '{}'", ...)))` in `export_session_to_file`
- **Root Cause**: Blanket `From<io::Error> for Error` always sets context to "unknown operation". Any `?` on an IO operation silently loses path/operation context.
- **Documentation**: Fix(issue-026) in `export.rs` + 5-section test doc in `export_command_test.rs`

### plan-004: projects_routine output format redesign

- **Issue**: `.projects` output was a flat list of session IDs with opaque encoded project labels (e.g. `"-home-user1-pro"`); no project grouping, no readable paths, no agent collapse at scale
- **Tests**: 6 tests IT-17..IT-22 in `projects_output_format_test.rs` (IT-23 covers display fix issue-029)
- **Fix**: Redesigned `projects_routine` to group sessions by `BTreeMap<String, Vec<Session>>` keyed by decoded project path; added `decode_project_display()` helper; agent sessions collapsed at v1 with no `agent::` filter; entry counts shown per session at v2+; blank line between project groups
- **Root Cause**: Original design used flat `Vec<(label, id)>` with labels from `format!("{:?}", project.id())` — debug-format encoded strings, not human-readable paths
- **Pitfalls**:
  1. `decode_path()` requires input starting with `-`; UUID project dirs don't → must guard with `starts_with('-')` before calling decode
  2. Topic suffix `--topic` must be stripped (`find("--")`) before calling `decode_path`; otherwise it becomes a phantom path component
  3. Blank line between project groups was not in the initial implementation despite being in the design algorithm and the docs example — always verify format output against docs examples
- **Note**: Originally tagged as issue-026 internally; relabeled to plan-004 because issue-026 was already assigned to the export path-in-error-message bug

### issue-029: decode_project_display splits underscore dirs as path separators

- **Issue**: `.sessions scope::under` (and all verbosity ≥ 1 scopes) displayed project path headers with underscore-named directories split on `/` — e.g., `~/wip_core/myproject:` shown as `~/wip/core/myproject:`
- **Test**: `IT-23` (`test_sessions_under_display_preserves_underscores`) in `projects_command_test.rs`; marked `bug_reproducer(issue-029)`
- **Fix**: Added `decode_path_via_fs()` + `walk_fs()` in `cli/mod.rs`; `decode_project_display` now tries the heuristic result first — if it doesn't exist on disk, falls back to FS-guided DFS that chooses `/` vs `_` at each boundary by calling `is_dir()` on candidate prefixes; final fallback is the heuristic result (handles deleted/remote projects)
- **Root Cause**: `encode_path` maps both `_` (underscore) and `/` (path separator) to `-`; `decode_component` heuristic defaulted to `/` for all unrecognized `-` boundaries, so `wip-core` always decoded to `wip/core` regardless of whether a real `wip_core` directory exists
- **Documentation**: Fix(issue-029) + 3-field source comment in `cli/mod.rs`; 5-section test doc block in `projects_command_test.rs`

### issue-031: scope::under includes sibling modules with underscore-suffix names

- **Issue**: `scope::under path::claude_storage` incorrectly included sessions from `claude_storage_core` — a sibling module at the same directory level
- **Test**: `IT-25` (`it_25_scope_under_excludes_underscore_named_sibling`) in `projects_command_test.rs`; marked `bug_reproducer(issue-031)`
- **Fix**: Two-stage predicate in the `"under"` arm of `project_matches` in `projects_routine`. String prefix is fast-reject only; `decode_path_via_fs` + `Path::starts_with` (component-wise) verifies ambiguous candidates. `--topic` suffix stripped before filesystem walk.
- **Root Cause**: `encode_path` maps both `_` and `/` to `-`; string `starts_with` on encoded forms cannot distinguish `base/sub` (encoded `base-sub`) from `base_extra` (encoded `base-extra`) — both share the `base-` prefix
- **Documentation**: Fix(issue-031) + 3-field source comment in `cli/mod.rs`; 5-section test doc block in `projects_command_test.rs`

### issue-032: scope::relevant includes sibling projects with underscore-suffix names

- **Issue**: `scope::relevant path::base_extra` incorrectly included sessions from the sibling project `base` — not an ancestor of `base_extra` despite passing the string prefix check
- **Test**: `IT-26` (`it_26_scope_relevant_excludes_underscore_named_sibling`) in `projects_command_test.rs`; marked `bug_reproducer(issue-032)`
- **Fix**: Two-stage predicate in the `"relevant"` arm of `project_matches`. `is_relevant_encoded` is fast-reject only; `decode_path_via_fs` + `base_path.starts_with(decoded_path)` (component-wise) verifies prefix-match candidates.
- **Root Cause**: `is_relevant_encoded` used `encoded_base.starts_with(dir_name + "-")` which cannot distinguish ancestor `base` (child path `base/sub``base-sub`) from sibling `base` (when base_path is `base_extra``base-extra`) — same underscore/slash encoding ambiguity as issue-031
- **Documentation**: Fix(issue-032) + 3-field source comment in `cli/mod.rs`; 5-section test doc block in `projects_command_test.rs`

### issue-033: `.exists` stderr output violated spec ("no sessions" vs multi-level wrapped error)

- **Issue**: `execute_oneshot` printed `"Error: Execution error: Execution Error: no sessions"` but the spec requires exactly `"no sessions"` on stderr for exit-1 case
- **Test**: `it_exists_stderr_exact_when_no_history` in `session_path_command_test.rs`; marked `bug_reproducer(issue-033)`
- **Fix**: Added `extract_user_message()` in `cli_main.rs` that strips `"Execution error: Execution Error: "` prefix before printing in one-shot mode
- **Root Cause**: `execute_oneshot` used `eprintln!("Error: {error}")` where the unilang pipeline had already double-wrapped the message. `ErrorData::Display` uses `writeln!` (adds `\n`) → `Error::Execution` adds `"Execution Error: "` → pipeline adds `"Execution error: "``execute_oneshot` adds `"Error: "` = four layers
- **Documentation**: Fix(issue-033) comment in `cli/mod.rs` exists_routine + 5-section test doc in `session_path_command_test.rs`

### issue-034: .projects list mode header count includes zero-byte placeholder sessions

- **Issue**: `clg .projects scope::local` showed `"(2 sessions)"` in the header but rendered 0 session lines when a project had 2 zero-byte placeholder sessions. Same bug in flat display branch (agent:: filter active) and in summary mode (zero-byte file could become the "best session" showing "(no text content)").
- **Tests**: 3 tests IT-54..IT-56 in `projects_zero_byte_count_bug.rs` (use_families branch, flat branch, zero-byte-only project)
- **Fix**: 3-site fix in `src/cli/mod.rs`: (1) `aggregate_projects` skips zero-byte in best-selection and uses `!is_zero_byte_session` in session_count; (2) `root_count` in use_families branch filters to non-zero-byte roots; (3) flat branch computes `displayable` before `group_count`
- **Root Cause**: Count expressions used unfiltered `sessions.len()` / `families.len()` while the render layer had separate `is_zero_byte_session` filtering. Count and render were not derived from the same source
- **Documentation**: Fix(issue-034) 3-field comment at all three source sites + 5-section test docs in `projects_zero_byte_count_bug.rs`

### issue-028: "1 entries" — hardcoded plural "entries" in session header and project session list
- **Issue**: (a) `.show session_id::abc` produced "Session: abc (1 entries)" — wrong plural in header; (b) `.show.project verbosity::1` with 1-entry session showed "(1 entries, last: ...)" — same root cause
- **Tests**: `test_show_session_single_entry_header_says_entry_not_entries`, `test_show_session_multi_entry_header_still_says_entries` in `smart_show_command.rs`; `.show.project` tests deleted with command removal (task-013)
- **Fix**: Added `entry_noun`/`e_noun` variables derived from count (1 → "entry", else "entries") in `show_session_routine` in `cli/mod.rs`; same fix was in `show_project_routine` (now removed)
- **Root Cause**: Format strings hardcoded "entries" regardless of count — same pattern as issue-025/027 but for the irregular noun "entry"/"entries"
- **Documentation**: Fix(issue-028) in `cli/mod.rs` (two locations) + 5-section test doc in both test files

## Manual Testing

See `tests/manual/readme.md` for manual testing plan and procedures.

## Related Documentation

- **Documentation**: `docs/entities.md` - Command specifications and behavioral requirements index
- **Code Design**: See applicable rulebooks via `clm .rulebooks.list`
- **Test Organization**: `test_organization.rulebook.md` - Test documentation format standards
- **Codebase Hygiene**: `codebase_hygiene.rulebook.md` - Quality standards for documentation