fix(project-name): use parent/worktree composite so observations don't cross worktrees

Revert of #1820 behavior. Each worktree now gets its own bucket:
- In a worktree, primary = `parent/worktree` (e.g. `claude-mem/dar-es-salaam`)
- In a main repo, primary = basename (unchanged)
- allProjects is always `[primary]` — strict isolation at query time

Includes a one-off maintenance script (scripts/worktree-remap.ts) that
retroactively reattributes past sessions to their worktree using path
signals in observations and user prompts. Two-rule inference keeps the
remap high-confidence:
  1. The worktree basename in the path matches the session's current
     plain project name (pre-#1820 era; trusted).
  2. Or all worktree path signals converge on a single (parent, worktree)
     across the session.
Ambiguous sessions are skipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-16 15:40:44 -07:00
parent 53622b59e9
commit 040729beef
5 changed files with 196 additions and 28 deletions
+10 -9
View File
@@ -58,21 +58,24 @@ export function getProjectName(cwd: string | null | undefined): string {
* Project context with worktree awareness
*/
export interface ProjectContext {
/** Canonical project name for writes/queries (parent repo in worktrees) */
/** Canonical project name for writes/queries; `parent/worktree` when in a worktree */
primary: string;
/** Parent project name if in a worktree, null otherwise */
parent: string | null;
/** True if currently in a worktree */
isWorktree: boolean;
/** All projects to query: [primary] for main repo, [parentRepo, worktreeName] for worktree */
/** Projects to query — always `[primary]` so observations don't cross worktrees */
allProjects: string[];
}
/**
* Get project context with worktree detection.
*
* When in a worktree, returns both the worktree project name and parent project name
* for unified timeline queries.
* Each worktree is its own bucket. When in a worktree, `primary` is the
* composite `parent/worktree` (e.g. `claude-mem/dar-es-salaam`) so worktrees
* are uniquely identified and grouped under their parent project without
* mixing observations across them. In the main repo, `primary` is just the
* project basename.
*
* @param cwd - Current working directory (absolute path)
* @returns ProjectContext with worktree info
@@ -88,14 +91,12 @@ export function getProjectContext(cwd: string | null | undefined): ProjectContex
const worktreeInfo = detectWorktree(expandedCwd);
if (worktreeInfo.isWorktree && worktreeInfo.parentProjectName) {
// In a worktree: use parent project name as primary so observations
// are stored under the same project as the main repo (#1081, #1500, #1819)
const allProjects = Array.from(new Set([worktreeInfo.parentProjectName, cwdProjectName]));
const composite = `${worktreeInfo.parentProjectName}/${cwdProjectName}`;
return {
primary: worktreeInfo.parentProjectName,
primary: composite,
parent: worktreeInfo.parentProjectName,
isWorktree: true,
allProjects
allProjects: [composite]
};
}