Skip to content

Commit 86d4700

Browse files
jpheinclaude
andauthored
fix(hooks): drop wing_ prefix from transcript-derived wings (#9)
The fork-only _wing_from_transcript_path returned wing_<project> for hook-derived wings, but operator-mined content from `mempalace mine ~/Projects/X` lands in a bare-name wing (e.g. memorypalace, mempalace, projects). Result: every project with both manual-mined content AND hook-mined transcripts had its drawers split between "wing_X" and "X" — silently invisible to a search filtered by either name. Drop the prefix. _wing_from_transcript_path now returns the bare project name (lowercased, spaces → underscores), matching the operator-mine convention. Fallback "wing_sessions" → "sessions" (which already exists in JP's canonical palace with 2,132 drawers from earlier hook-derived ingest with hardcoded --wing sessions, so new fallback content converges with the older fallback content too). A separate one-shot script renames legacy wing_<x> drawers' metadata so they merge into <x> on the live palace. Tests: 9 wing-related assertions updated to expect bare-name shape; 1548 pass, 1 skipped, lint clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0b945e1 commit 86d4700

2 files changed

Lines changed: 24 additions & 21 deletions

File tree

mempalace/hooks_cli.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -522,10 +522,14 @@ 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-
The project directory name is the final dash-separated token of the
526-
encoded folder. Returns ``wing_<project>`` (lowercased, spaces → ``_``).
527-
Falls back to ``wing_sessions`` if the path does not match a Claude Code
528-
project-folder layout.
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.
529533
"""
530534
# Normalize path separators for cross-platform (Windows backslashes)
531535
normalized = transcript_path.replace("\\", "/")
@@ -536,14 +540,13 @@ def _wing_from_transcript_path(transcript_path: str) -> str:
536540
encoded = match.group(1)
537541
project = encoded.rsplit("-", 1)[-1]
538542
if project:
539-
return f"wing_{project.lower().replace(' ', '_')}"
543+
return project.lower().replace(" ", "_")
540544
# Legacy fallback: explicit ``-Projects-<name>`` segment, useful for
541545
# transcripts not under the standard Claude Code projects dir.
542546
match = re.search(r"-Projects-([^/]+?)(?:/|$)", normalized)
543547
if match:
544-
project = match.group(1).lower().replace(" ", "_")
545-
return f"wing_{project}"
546-
return "wing_sessions"
548+
return match.group(1).lower().replace(" ", "_")
549+
return "sessions"
547550

548551

549552
def hook_stop(data: dict, harness: str):

tests/test_hooks_cli.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ def test_stop_hook_saves_silently_at_interval(tmp_path):
236236
)
237237
# Verbatim-only: systemMessage tells the user the ingest fired; no count or themes.
238238
assert result["systemMessage"].startswith("\u2726 Transcript ingest triggered")
239-
# tmp_path has no "-Projects-" segment, so _wing_from_transcript_path falls back to "wing_sessions"
240-
assert "wing=wing_sessions" in result["systemMessage"]
239+
# tmp_path has no "-Projects-" segment, so _wing_from_transcript_path falls back to "sessions"
240+
assert "wing=sessions" in result["systemMessage"]
241241
mock_ingest.assert_called_once_with(str(transcript))
242242

243243

@@ -257,7 +257,7 @@ def test_stop_hook_derives_wing_from_transcript_path(tmp_path):
257257
{"session_id": "test", "stop_hook_active": False, "transcript_path": str(transcript)},
258258
state_dir=tmp_path,
259259
)
260-
assert "wing=wing_myproject" in result["systemMessage"]
260+
assert "wing=myproject" in result["systemMessage"]
261261
mock_ingest.assert_called_once_with(str(transcript))
262262

263263

@@ -312,39 +312,39 @@ def test_precompact_allows(tmp_path):
312312

313313
def test_wing_from_transcript_path_extracts_project():
314314
path = "/home/jp/.claude/projects/-home-jp-Projects-memorypalace/session.jsonl"
315-
assert _wing_from_transcript_path(path) == "wing_memorypalace"
315+
assert _wing_from_transcript_path(path) == "memorypalace"
316316

317317

318318
def test_wing_from_transcript_path_fallback():
319-
assert _wing_from_transcript_path("/some/random/path.jsonl") == "wing_sessions"
319+
assert _wing_from_transcript_path("/some/random/path.jsonl") == "sessions"
320320

321321

322322
def test_wing_from_transcript_path_windows_backslashes():
323323
path = "C:\\Users\\jp\\.claude\\projects\\-home-jp-Projects-myapp\\session.jsonl"
324-
assert _wing_from_transcript_path(path) == "wing_myapp"
324+
assert _wing_from_transcript_path(path) == "myapp"
325325

326326

327327
def test_wing_from_transcript_path_lowercases():
328328
path = "/home/jp/.claude/projects/-home-jp-Projects-MyProject/session.jsonl"
329-
assert _wing_from_transcript_path(path) == "wing_myproject"
329+
assert _wing_from_transcript_path(path) == "myproject"
330330

331331

332332
def test_wing_from_transcript_path_non_projects_layout():
333333
# Linux users with code under ~/dev/, ~/src/, ~/code/ — no -Projects- segment.
334334
# Project name is the final dash-separated token of the encoded folder.
335335
path = "/home/igor/.claude/projects/-home-igor-dev-MemPalace-mempalace/session.jsonl"
336-
assert _wing_from_transcript_path(path) == "wing_mempalace"
336+
assert _wing_from_transcript_path(path) == "mempalace"
337337

338338

339339
def test_wing_from_transcript_path_macos_users_layout():
340340
# macOS ~/ layout without a Projects/ segment.
341341
path = "/Users/alice/.claude/projects/-Users-alice-code-MyApp/session.jsonl"
342-
assert _wing_from_transcript_path(path) == "wing_myapp"
342+
assert _wing_from_transcript_path(path) == "myapp"
343343

344344

345345
def test_wing_from_transcript_path_nested_deep():
346346
path = "/home/bob/.claude/projects/-home-bob-work-clients-acme-frontend/session.jsonl"
347-
assert _wing_from_transcript_path(path) == "wing_frontend"
347+
assert _wing_from_transcript_path(path) == "frontend"
348348

349349

350350
# --- _log ---
@@ -611,7 +611,7 @@ def test_ingest_transcript_routes_through_daemon(tmp_path):
611611
mock_post.assert_called_once()
612612
args, kwargs = mock_post.call_args
613613
assert args[0] == str(convo_dir)
614-
assert kwargs["wing"] == "wing_myapp"
614+
assert kwargs["wing"] == "myapp"
615615
assert kwargs["mode"] == "convos"
616616

617617

@@ -978,10 +978,10 @@ def test_precompact_mines_transcript_dir(tmp_path, monkeypatch):
978978
cmd = mock_popen.call_args[0][0]
979979
# Mines the transcript's parent dir as convos. Wing is derived per-transcript;
980980
# for a path outside the standard Claude Code projects layout, _wing_from_transcript_path
981-
# falls back to "wing_sessions".
981+
# falls back to "sessions".
982982
assert str(tmp_path) in cmd
983983
assert cmd[cmd.index("--mode") + 1] == "convos"
984-
assert cmd[cmd.index("--wing") + 1] == "wing_sessions"
984+
assert cmd[cmd.index("--wing") + 1] == "sessions"
985985

986986

987987
# --- run_hook ---

0 commit comments

Comments
 (0)