Skip to content

Complete two-root path separation (supersedes #103)#106

Merged
virtualian merged 1 commit intomainfrom
refactor/two-root-fixes
Apr 13, 2026
Merged

Complete two-root path separation (supersedes #103)#106
virtualian merged 1 commit intomainfrom
refactor/two-root-fixes

Conversation

@virtualian
Copy link
Copy Markdown
Owner

Summary

Focused refactor completing the CLAUDE_CONFIG_DIR + PAI_DIR two-root separation. This PR supersedes #103 and is intentionally narrow: 20 files, +136/-128, all in the hooks layer plus the release README and one SYSTEM doc.

Background — why this replaces #103

Issue #100's two-root split landed on main in two pieces:

  1. PR Implement CLAUDE_CONFIG_DIR + PAI_DIR two-root separation #101 squash-merged 4 commits from branch 100-claude-config-dir-pai-dir-separation. That squash silently bundled ebc8130, which also deleted ~606 lines of unrelated functionality: CorrectionMode fast-path, BehavioralSignal capture, and loadLatestSynthesis. No one noticed during review because the deletions were hidden inside a 400-file "refactor" commit.
  2. PR Restore CorrectionMode fast-path and loadLatestSynthesis (hotfix) #104 (the hotfix) restored those 606 lines as a narrow 2-file PR.
  3. PR Add CI symbol guard for critical hook files #105 added a CI symbol-presence guard to make silent deletions of those subsystems impossible in future PRs.

After #101, #104, and #105, PR #103's semantic delta collapsed from 403 files / +1712/-1658 (merge-base relative) to the 18-file correction set that genuinely differs from post-hotfix main. GitHub's PR UI couldn't show this because the merge base hadn't moved. #103 is also CONFLICTING — a naive rebase produces phantom conflicts because git's patch-ID matching cannot resolve the ebc8130 hunks against #101's squash.

This PR is the rebased outcome, produced via file-copy reconstruction + the 3 cleanup decisions documented below.

What this PR does

1. Path-separation fixes (18 files from #103's correction commits)

All of these are corrections to bugs introduced or left behind by ebc8130:

  • settings.json — revert 13 hook command paths from ${CLAUDE_CONFIG_DIR}/hooks/* back to ${PAI_DIR}/hooks/*. Hooks physically live under PAI_DIR after the split.
  • paths.tsgetHooksDir() now returns codePath('hooks') (not configPath('hooks')). Docstring rewrites for the two-root contract.
  • LoadContext.hook.ts — eliminate the paiDir parameter chain across 5 helpers. 5 raw join(paiDir, ...) calls routed through codePath(). Rename misleading loadSettings parameter. Extract sessionProgressPath.
  • UpdateCounts.tscountHooks and refreshUsageCache now read MEMORY from the PAI root. After Implement CLAUDE_CONFIG_DIR + PAI_DIR two-root separation #101's MEMORY move, the CONFIG-root reads silently returned empty data.
  • RelationshipMemory.hook.tsensureRelationshipDir fixed for the split.
  • LastResponseCache.hook.tsmkdirSync before writeFileSync (silent ENOENT on fresh install).
  • prd-utils.ts — correct .replace() prefix from configPath to codePath.
  • VoiceCompletion.hook.ts — replace hardcoded ~/.claude/settings.json with getSettingsPath().
  • VoiceNotification.ts, tab-setter.ts — drop dead configPath imports.
  • DocCrossRefIntegrity.ts, change-detection.ts, identity.ts, notifications.ts, SessionCleanup.hook.ts, KittyEnvPersist.hook.ts, WorkCompletionLearning.hook.ts — small codePath / configPath cleanups.
  • .pai-protected.json — correct stale comment.

2. Delete paiPath() alias outright

paths.ts previously kept paiPath() as a deprecated alias for codePath() to preserve backward compatibility. This PR removes it entirely.

Rationale. The semantic flip in v4.0.3 (pre-split paiPath(x)~/.claude/x, post-split paiPath(x)~/.pai/x) is exactly the class of change where a silent alias is a landmine, not a safety net. A caller that survives the grep would silently write to a different directory, and the failure mode is "files appear in an unexpected location" — one of the worst bugs to diagnose. Deleting the function forces the compiler to catch any revenant caller. Zero code callers existed when this commit was written.

SYSTEM_USER_EXTENDABILITY.md had 4 documentation examples using paiPath() — those are updated to codePath() in the same commit so the documentation matches the public API.

3. MEMORY migration release note

Releases/v4.0.3+/README.md gains a "Breaking Changes" subsection explaining the two-root split and — crucially — a required manual migration step for v4.0.2 upgraders:

if [ -d ~/.claude/MEMORY ]; then
  mkdir -p ~/.pai
  mv ~/.claude/MEMORY ~/.pai/MEMORY
fi

This is a deliberate decision to ship the migration as documentation + manual command rather than as automated installer logic. Reasoning:

  • The current user base is solo (myself) plus whoever forks from here. A release-note + explicit command is sufficient coverage without adding installer complexity under time pressure.
  • An automated migrator belongs in a focused follow-up PR that can be reviewed on its own merits (idempotency, error handling, rollback semantics) rather than bundled inside a refactor.
  • I will open a tracking issue immediately after this PR for the automator work.

What this PR does not do (deliberately deferred)

  • statusLine.command still points at ${CLAUDE_CONFIG_DIR}/statusline-command.sh. The physical script lives at ~/.claude/statusline-command.sh. Relocating it under PAI_DIR is a one-file move + one settings line + one installer template update — a valid change, but it doesn't belong inside this refactor because it's a separate decision about which root owns the statusline script. Tracked as a follow-up.
  • Automated MEMORY migrator — see above, covered by a follow-up issue.

Verification

Run against this commit locally before pushing:

  • bun build --target=bun clean on all 17 touched hook TypeScript files (paths.ts, prd-utils.ts, LoadContext.hook.ts, LastResponseCache.hook.ts, VoiceCompletion.hook.ts, RelationshipMemory.hook.ts, WorkCompletionLearning.hook.ts, KittyEnvPersist.hook.ts, SessionCleanup.hook.ts, learning-readback.ts, tab-setter.ts, change-detection.ts, identity.ts, notifications.ts, UpdateCounts.ts, VoiceNotification.ts, DocCrossRefIntegrity.ts)
  • settings.json parses as valid JSON
  • ✅ Env-var resolution test (CLAUDE_CONFIG_DIR=/tmp/ctest PAI_DIR=/tmp/ptest): 10/10 assertions passgetConfigDir(), getPaiDir(), configPath('hooks'), codePath('MEMORY'), codePath('PAI','Tools'), getSettingsPath(), getHooksDir(), getMemoryDir(), getSkillsDir() all resolve to the expected roots
  • settings.json hook-command audit: 0 references to ${CLAUDE_CONFIG_DIR}/hooks, 24 references to ${PAI_DIR}/hooks
  • ✅ MEMORY routing audit: 0 configPath('MEMORY', 45 codePath('MEMORY', 0 raw ~/.claude/MEMORY references in hook sources
  • paiPath search in Releases/v4.0.3+/: zero matches after deletion + doc substitution
  • ✅ CI symbol guard (from merged Add CI symbol guard for critical hook files #105) passes all 13 required-symbol checks against this commit

Next for the #100 series

  1. Close CLAUDE_CONFIG_DIR + PAI_DIR two-root split (v4.0.3+) #103 with a comment pointing at this PR.
  2. Open a tracking issue for the automated MEMORY migrator (will reference this PR's documentation-only approach as the temporary measure).
  3. Leave the statusLine.command relocation and the ebc8130 scope-creep post-mortem for separate follow-ups — neither belongs in this refactor.

Supersedes PR #103. Incorporates the 4 correction commits from
branch 100-claude-config-dir-pai-dir-separation (b3e4e49, 42f70b4,
39ea6b1, c7f304c) as a single focused refactor on top of the
CLAUDE_CONFIG_DIR + PAI_DIR split that landed via #101 and the
CorrectionMode + loadLatestSynthesis restore that landed via #104.

Path separation fixes
---------------------
- settings.json: revert 13 hook commands from CLAUDE_CONFIG_DIR back
  to PAI_DIR (the hook-root inversion originally introduced by ebc8130)
- paths.ts: getHooksDir() returns codePath('hooks') not configPath('hooks'),
  docstring rewrites for the two-root contract
- LoadContext.hook.ts: eliminate paiDir parameter chain across 5 helpers,
  route 5 raw join(paiDir, ...) calls through codePath(), rename
  misleading loadSettings param, extract sessionProgressPath
- UpdateCounts.ts: countHooks and refreshUsageCache now read MEMORY from
  the PAI root instead of the CONFIG root
- RelationshipMemory.hook.ts: ensureRelationshipDir fixed for the split
- LastResponseCache.hook.ts: mkdirSync before writeFileSync (silent
  ENOENT on fresh install)
- prd-utils.ts: correct replace() prefix from configPath to codePath
- VoiceCompletion.hook.ts: replace hardcoded ~/.claude/settings.json
  with getSettingsPath()
- VoiceNotification.ts, tab-setter.ts: drop dead configPath imports
- DocCrossRefIntegrity.ts, change-detection.ts, identity.ts,
  notifications.ts, SessionCleanup.hook.ts, KittyEnvPersist.hook.ts,
  WorkCompletionLearning.hook.ts: small codePath / configPath cleanups
- .pai-protected.json: correct stale comment

Deprecated alias deletion
-------------------------
- paths.ts: remove the paiPath() function entirely. The semantic flip
  in v4.0.3 (pre-split ~/.claude/x -> post-split ~/.pai/x) is exactly
  the class of change where a silent alias is a landmine, not a safety
  net. The compiler now catches any revenant caller. Zero code callers
  existed when this commit was written.
- SYSTEM_USER_EXTENDABILITY.md: update 4 code examples from paiPath()
  to codePath() so the documentation matches the public API.

MEMORY migration release note (manual upgrade path)
---------------------------------------------------
- Releases/v4.0.3+/README.md: add a "Breaking Changes" subsection
  explaining the two-root split and add the required MEMORY migration
  step (`mv ~/.claude/MEMORY ~/.pai/MEMORY`) to the "Upgrading from
  v4.0.x" recipe. An automated migrator is a follow-up - see the
  tracking issue.

Deferred (intentional)
----------------------
- statusLine.command still points at ${CLAUDE_CONFIG_DIR}/statusline-command.sh
  because the physical file currently lives at ~/.claude/statusline-command.sh.
  Relocating the script is a separate decision that does not belong in
  this refactor.

Verification
------------
- bun build --target=bun clean on all 17 touched hook TypeScript files
- settings.json parses as valid JSON
- env-var resolution tested under CLAUDE_CONFIG_DIR=/tmp/ctest PAI_DIR=/tmp/ptest:
  10/10 path primitives resolve to the expected roots
- settings.json hook-command audit: 0 references to CLAUDE_CONFIG_DIR/hooks,
  24 references to PAI_DIR/hooks
- MEMORY routing audit: 0 configPath('MEMORY'), 45 codePath('MEMORY'),
  0 raw ~/.claude/MEMORY references in hook sources
- CI symbol guard (merged in #105) passes all 13 required-symbol checks
  against this commit
@virtualian virtualian merged commit fbfa9a7 into main Apr 13, 2026
1 check passed
@virtualian virtualian deleted the refactor/two-root-fixes branch April 13, 2026 15:48
virtualian added a commit that referenced this pull request Apr 14, 2026
Two files under Releases/v4.0.3+/.claude/PAI/ still referenced the
pre-separation `.claude/PAI` path in executable code. Both now resolve
via `process.env.PAI_DIR || ~/.pai` matching the repo convention used
in Tools/algorithm.ts, Tools/FailureCapture.ts, and hooks/lib/paths.ts.

- RebuildPAI.ts:16 — introduce PAI_ROOT const; PAI_DIR now = PAI_ROOT/PAI
- runner.v2.ts:33 — resolve PAI_ROOT inside createLocalLLM()

Out of scope and intentionally untouched:
- `~/.claude/settings.json` reference in RebuildPAI.ts:21 (correct per
  two-root architecture — settings live in CLAUDE_CONFIG_DIR)
- The 102-line Algorithm-spec gap in v3.7.0.md (separate problem)
- 4 residual hits in `.claude/skills/` tree (filed as separate issue)

Per Plans/2026-04-14-two-root-separation-followups.md the plan listed
10 files but 8 were already clean on this branch between #101 and #106.

Refs #108

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
virtualian added a commit that referenced this pull request Apr 14, 2026
Two files under Releases/v4.0.3+/.claude/PAI/ still referenced the
pre-separation `.claude/PAI` path in executable code. Both now resolve
via `process.env.PAI_DIR || ~/.pai` matching the repo convention used
in Tools/algorithm.ts, Tools/FailureCapture.ts, and hooks/lib/paths.ts.

- RebuildPAI.ts:16 — introduce PAI_ROOT const; PAI_DIR now = PAI_ROOT/PAI
- runner.v2.ts:33 — resolve PAI_ROOT inside createLocalLLM()

Out of scope and intentionally untouched:
- `~/.claude/settings.json` reference in RebuildPAI.ts:21 (correct per
  two-root architecture — settings live in CLAUDE_CONFIG_DIR)
- The 102-line Algorithm-spec gap in v3.7.0.md (separate problem)
- 4 residual hits in `.claude/skills/` tree (filed as separate issue)

Per Plans/2026-04-14-two-root-separation-followups.md the plan listed
10 files but 8 were already clean on this branch between #101 and #106.

Refs #108

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant