Skip to content

Commit d76134d

Browse files
jpheinclaude
andauthored
fix(hooks): preserve dashed project names + route through normalize_wing_name (#10)
Two findings from Copilot review on #9 (already merged): 1. **Last-dash-token rule lost hyphenated project names.** A project directory named ``realm-watch`` is encoded by Claude Code as ``-home-jp-Projects-realm-watch``, and the previous primary regex's ``rsplit('-', 1)[-1]`` returned ``watch`` — collapsing the project name. Reorder the resolution: try the explicit ``-Projects-<name>`` segment FIRST (preserves dashes), fall back to the last-dash-token only when the path is in a non-Projects layout (``~/dev/<parent>/<project>``). 2. **Inconsistent normalization vs operator mines.** Hook used ``.lower().replace(' ', '_')`` (no hyphen handling) while ``mempalace mine`` runs the dirname through ``normalize_wing_name`` (lowercases, dashes/spaces → underscores). Same project mined two ways produced two different wings. Route the hook through ``normalize_wing_name`` for parity. Net behavior: ``-Projects-realm-watch`` → ``realm_watch`` (matches operator-mine output). Existing tests for non-dashed projects still pass; 4 new tests cover dashed-project + uppercase-mixed cases + explicit operator-mine convergence assertion. Tests: 1551 pass, 1 skipped. Ruff lint + format clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86d4700 commit d76134d

2 files changed

Lines changed: 53 additions & 17 deletions

File tree

mempalace/hooks_cli.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -522,30 +522,38 @@ def _wing_from_transcript_path(transcript_path: str) -> str:
522522
~/.claude/projects/-home-<user>-dev-<parent>-<project>/session.jsonl
523523
~/.claude/projects/-Users-<user>-<folder>-<project>/session.jsonl
524524
525-
Returns the project directory's basename, lowercased, with spaces
526-
collapsed to underscores. Falls back to ``"sessions"`` for paths
527-
that don't match the standard Claude Code projects layout.
528-
529-
The earlier shape returned ``wing_<project>``, which silently split
530-
content between hook-derived ``wing_<project>`` wings and
531-
operator-mined bare-name wings. The bare project name converges
532-
them.
525+
Returns the project's basename run through
526+
:func:`mempalace.config.normalize_wing_name` (lowercases, replaces
527+
spaces and hyphens with underscores) so hook-derived wings match
528+
operator-mined wing names exactly. Falls back to ``"sessions"`` for
529+
paths that don't match the standard Claude Code projects layout.
530+
531+
Resolution order:
532+
533+
1. ``-Projects-<name>`` segment when present — preserves project
534+
names containing dashes (``realm-watch`` resolves to
535+
``realm_watch`` instead of collapsing to ``watch``). Closes
536+
Copilot finding on jphein/mempalace#9.
537+
2. Last dash-separated token of ``/.claude/projects/-...`` — covers
538+
non-Projects layouts (``-Users-<user>-<folder>-<project>``).
539+
3. ``"sessions"`` fallback.
533540
"""
534-
# Normalize path separators for cross-platform (Windows backslashes)
541+
from .config import normalize_wing_name
542+
535543
normalized = transcript_path.replace("\\", "/")
536-
# Primary: pull the encoded project folder out of ``.claude/projects/``
537-
# and take its last dash-separated token.
544+
# Primary: explicit ``-Projects-<name>`` segment. Preferred because
545+
# it preserves project names that themselves contain dashes.
546+
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
547+
if match:
548+
return normalize_wing_name(match.group(1))
549+
# Secondary: last dash-separated token of ``/.claude/projects/-...``.
550+
# Used for projects under non-Projects parents (e.g. ~/dev, ~/code).
538551
match = re.search(r"/\.claude/projects/-([^/]+)", normalized)
539552
if match:
540553
encoded = match.group(1)
541554
project = encoded.rsplit("-", 1)[-1]
542555
if project:
543-
return project.lower().replace(" ", "_")
544-
# Legacy fallback: explicit ``-Projects-<name>`` segment, useful for
545-
# transcripts not under the standard Claude Code projects dir.
546-
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
547-
if match:
548-
return match.group(1).lower().replace(" ", "_")
556+
return normalize_wing_name(project)
549557
return "sessions"
550558

551559

tests/test_hooks_cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,34 @@ def test_wing_from_transcript_path_nested_deep():
347347
assert _wing_from_transcript_path(path) == "frontend"
348348

349349

350+
def test_wing_from_transcript_path_dashed_project():
351+
"""Project names containing dashes (realm-watch) survive intact via
352+
normalize_wing_name — dashes become underscores. Closes Copilot
353+
finding on jphein/mempalace#9: previously the last-dash-token rule
354+
collapsed ``realm-watch`` to ``watch``.
355+
"""
356+
path = "/home/jp/.claude/projects/-home-jp-Projects-realm-watch/session.jsonl"
357+
assert _wing_from_transcript_path(path) == "realm_watch"
358+
359+
360+
def test_wing_from_transcript_path_dashed_project_uppercase():
361+
"""Combined: dashes preserved AND lowercased."""
362+
path = "/home/jp/.claude/projects/-home-jp-Projects-Realm-Watch/session.jsonl"
363+
assert _wing_from_transcript_path(path) == "realm_watch"
364+
365+
366+
def test_wing_from_transcript_path_matches_operator_mine():
367+
"""The wing this returns matches what `mempalace mine ~/Projects/X`
368+
would produce when --wing is omitted (convo_miner.normalize_wing_name
369+
over the dir basename). This is the convergence the bare-name shape
370+
is supposed to deliver."""
371+
from mempalace.config import normalize_wing_name
372+
373+
path = "/home/jp/.claude/projects/-home-jp-Projects-realm-watch/session.jsonl"
374+
operator_wing = normalize_wing_name("realm-watch")
375+
assert _wing_from_transcript_path(path) == operator_wing
376+
377+
350378
# --- _log ---
351379

352380

0 commit comments

Comments
 (0)