Commit 079d432
committed
v0.2.3: feat: pathkit slug disambig + csb resume cd/TTY fix
Finalizes the v0.2.3 epic. Closes the last two issues in the bundle
(#23 pathkit multi-candidate slug disambiguation, #24 csb resume cd
plus Windows TTY-handoff fix), folds in a small bug-fix surfaced by
the tester agent during the v0.2.3 checklist run, and converts the
CHANGELOG `[Unreleased]` section to `[0.2.3] -- 2026-05-06`.
Version files were bumped from 0.2.2 -> 0.2.3 in the prior commit
(442cec7); this commit does not bump them again, but cuts the actual
release section in CHANGELOG and updates the version-comparison
footer link.
# pathkit slug disambiguation (#23)
When a project-dir slug like `C--code-New--Project` happens to decode
to two real folders on disk (e.g., a literal `New--Project` folder
AND a sibling `New\.Project` folder, both of which sanitize to the
same slug), pathkit now picks the right one via a three-tier fallback
chain in `_disambiguate`:
Tier 1 (definitive, O(N)): if the JSONL's `first_cwd` matches any
candidate exactly OR as a prefix-with-separator, return that
candidate. This is the canonical "session-open cwd" oracle.
Tier 2 (full histogram, O(N*M)): if no candidate matches `first_cwd`
but `folder_usage` is provided, find the candidate with the
highest sum of matching cwd-counts. Subdirectory cwds count
toward their parent candidate.
Tier 3 (no signal): fall back to the encoded-length heuristic
(current candidate ordering from `_collect_candidates`), which
preserves #19's first-match return for callers without JSONL
info. Zero perf change for the unambiguous case.
The signature change is additive and backward-compatible:
decode_project_slug(slug, first_cwd=None, folder_usage=None)
derive_start_at(jsonl_path, first_cwd=None, folder_usage=None)
`timeline._resolve_start_at` builds the `folder_usage` dict from the
existing `Session` object (no new indexer fields needed) and threads
both signals through.
Path comparisons go through a `_path_matches` helper using stdlib
`os.path.normcase` + `os.path.normpath` for case + separator +
trailing-slash insensitivity. The original AC suggested
`dazzle_filekit.paths.normalize_path_no_resolve`; we used the stdlib
equivalent for now and noted in a code comment that filekit is a
drop-in replacement when the broader path-utils consolidation pass
happens (separate issue, not blocking this release).
Old `_decode_under` is kept as a thin backward-compat wrapper that
calls `_collect_candidates` + `_disambiguate(..., None, None)` --
the existing 33 #19-era pathkit tests pass unchanged.
# csb resume cd + Windows TTY handoff (#24)
Three layers of fix for what was originally filed as one bug:
Layer 1 -- cmd_resume now invokes claude with cwd=target rather
than printing `cd` decoratively and exec'ing claude in the
user's terminal cwd. Pre-fix, `claude --resume <uuid>` ran from
the wrong cwd and failed with "No conversation found."
Layer 2 -- target is derived from `pathkit.derive_start_at(
jsonl_path, first_cwd, folder_usage)` rather than
`session['start_folder']`. Per the upstream-source audit (#25),
the slug-decoded path is the only cwd whose slug equals the
JSONL's parent directory -- which is the only cwd from which
`claude --resume` will find the file. Falls back to start_folder
for legacy session rows that lack `jsonl_path`.
Layer 3 -- launch via `subprocess.run([...], cwd=target)` rather
than `os.chdir + os.execvp`. On Windows, Python's `os.execvp` is
`_spawnv(P_OVERLAY, ...)`: the parent exits and a child spawns,
but the controlling-TTY relationship doesn't transfer cleanly.
Symptom: claude TUI rendered to stdout but stdin keystrokes went
into the void. subprocess.run inherits the parent's
stdin/stdout/stderr handles so the TUI works correctly. The
python parent stays alive (~30MB) while claude runs and
propagates claude's exit code.
The original AC for #24 specified `os.chdir + os.execvp`; the
subprocess.run approach is a strict improvement that addresses
Layer 3 (which the original AC didn't anticipate). FileNotFoundError
from subprocess.run is disambiguated between "target folder deleted"
and "claude not in PATH" via `os.path.isdir(target)`.
The TTY-handoff regression was caught by the tester agent flagging
HV.1 as MANUAL during the v0.2.3 checklist run, and confirmed by
the user manually running `csb resume <uuid>` from a foreign cwd
and observing claude not accept keystrokes. Fixed before commit.
# 2-positional scan grammar fix (regression from #20)
The tester agent also found that `csb scan ./amdead my-paper` was
rejected by argparse with "unrecognized arguments: my-paper" --
the parser had a single `term` positional that consumed `./amdead`,
leaving no slot for `my-paper`. The dot-prefix-shortcut help text
documented this combined form but the parser didn't accept it.
Fix: added `term2` as a second optional positional. cmd_scan
validates that two positionals are only allowed when the first is
a `./<dir>` / `.\<dir>` / bare `.` shortcut; otherwise rejects with
a clear error suggesting `csb scan -d <dir> <term>`.
# CHANGELOG converted to [0.2.3]
`[Unreleased]` -> `[0.2.3] -- 2026-05-06`. Release preamble
summarizes the full v0.2.3 epic (#19, #21, display_top_folders,
#20, #23, #24; #25 closed by source audit). Comparison-link footer
updated.
Added:
- claude_session_backup/pathkit.py: `_normalize_path`,
`_path_matches`, `_collect_candidates`, `_disambiguate`. New kwargs
on `decode_project_slug` and `derive_start_at`. Module docstring
updated to remove the "documented limitation" caveat from #19.
- claude_session_backup/timeline.py: `_resolve_start_at` threads
`start_folder` and the `folders` histogram through to
`derive_start_at` for disambiguation.
- claude_session_backup/commands.py: `cmd_resume` rewrite using
`subprocess.run(cwd=target)` with three-layer target derivation
(slug-decoded -> start_folder -> None). `cmd_scan` validates the
2-positional form requires a dot-prefix first arg.
- claude_session_backup/cli.py: scan parser gains `term2` second
optional positional.
- tests/test_pathkit.py: 20 new tests for `_collect_candidates`,
three-tier `_disambiguate` (Tier 1 exact, prefix, both directions,
fall-through; Tier 2 highest-count, subdirectory-rollup,
fall-through; Tier 3 single-candidate short-circuit, encoded-length
heuristic, empty list); path-comparison normalization (case,
separators, trailing slash); signature-compat smoke tests for the
new kwargs.
- tests/test_commands.py: 9 new `cmd_resume` tests covering the
subprocess.run launch with cwd=target, returncode propagation,
FileNotFoundError disambiguation between deleted-target and
missing-claude, session-not-found early exit, Layer 2 slug-decoded
preference over start_folder, fallback on `<unresolved:>` sentinel,
and fallback when session row lacks jsonl_path. 1 new `cmd_scan`
test for the 2-positional rejection without dot-prefix.
- tests/test_cli.py: 4 new parser tests for the `term2` positional
(dot-prefix + term combo, single-positional case, three-positional
rejection).
- tests/checklists/v0.2.3__Feature__csb-scan-disambiguation.md:
restructured per the test-checklist skill template (header block,
High-Value Verification, public/private planning lineage,
cross-shell prerequisites, automated-test coverage table, expanded
"what mocks don't cover", reset/cleanup sections, reporting
guidance). Added Section 11 (#24 cd + TTY) and Section 12 (#23
pathkit) detail.
- tests/one-offs/check_resume_target.py: validation script left by
the tester agent during checklist run (kept per CLAUDE.md's
"include test code" rule).
Changed:
- CHANGELOG: `[Unreleased]` content moved to `[0.2.3] -- 2026-05-06`
with epic-summary preamble. Comparison-link footer updated.
- Implementation deviation from #23 AC #7 (path normalization): used
stdlib `os.path.normcase`/`normpath` instead of `dazzle_filekit.
paths.normalize_path_no_resolve`. Functionally equivalent;
filekit consolidation deferred to #26.
- Implementation deviation from #24 ACs #1, #4, #5 (chdir/execvp):
used `subprocess.run(cwd=target)` for Windows TTY-handoff
correctness. Functionally satisfies the original ACs and addresses
Layer 3 the original spec didn't anticipate.
226 tests passing (was 192; +34 net for #23 + #24 + scan 2-positional).
Closes #23
Closes #24
Refs #20 (closed in 442cec7; this completes the 0.2.3 wave)
Refs #21 (top-N infrastructure consumed by #23's Tier 2 gating)
Refs #19 (pathkit foundation extended)
Refs #25 (closed; upstream-source audit grounds #23 algorithm and
#24 Layer 2 justification)
Refs #26 (tracks the deferred dazzle_filekit consolidation that
#23 ACs #7 and #15 originally specified)
Design:
2026-05-06__15-21-24__senior-eng-claude-code-source-audit-slug-mechanisms.md
2026-05-06__11-17-07__senior-eng-investigation-pathkit-slug-vs-first-cwd.md
2026-05-05__19-00-30__csb-scan-term-vs-folder-disambiguation.md
2026-05-06__16-30-00__both_csb-resume-tty-handoff-broken-on-windows.md1 parent 442cec7 commit 079d432
10 files changed
Lines changed: 1084 additions & 78 deletions
File tree
- claude_session_backup
- tests
- checklists
- one-offs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
15 | 19 | | |
| 20 | + | |
| 21 | + | |
16 | 22 | | |
17 | 23 | | |
18 | 24 | | |
| |||
21 | 27 | | |
22 | 28 | | |
23 | 29 | | |
24 | | - | |
| 30 | + | |
25 | 31 | | |
26 | 32 | | |
27 | 33 | | |
| 34 | + | |
28 | 35 | | |
29 | 36 | | |
30 | 37 | | |
| |||
63 | 70 | | |
64 | 71 | | |
65 | 72 | | |
66 | | - | |
| 73 | + | |
| 74 | + | |
67 | 75 | | |
68 | 76 | | |
69 | 77 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
204 | 211 | | |
205 | 212 | | |
206 | 213 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
433 | 433 | | |
434 | 434 | | |
435 | 435 | | |
| 436 | + | |
| 437 | + | |
436 | 438 | | |
437 | 439 | | |
438 | 440 | | |
| |||
445 | 447 | | |
446 | 448 | | |
447 | 449 | | |
448 | | - | |
449 | 450 | | |
450 | 451 | | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
451 | 469 | | |
452 | 470 | | |
453 | | - | |
454 | | - | |
| 471 | + | |
| 472 | + | |
455 | 473 | | |
456 | 474 | | |
457 | 475 | | |
458 | | - | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
459 | 488 | | |
460 | | - | |
461 | | - | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
462 | 505 | | |
463 | 506 | | |
464 | 507 | | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
465 | 512 | | |
466 | 513 | | |
467 | 514 | | |
| |||
574 | 621 | | |
575 | 622 | | |
576 | 623 | | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
577 | 642 | | |
578 | 643 | | |
579 | 644 | | |
580 | 645 | | |
581 | 646 | | |
582 | 647 | | |
583 | 648 | | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
584 | 653 | | |
585 | 654 | | |
586 | 655 | | |
| |||
0 commit comments