Stable project identity from git remote + default remember to project scope#738
Stable project identity from git remote + default remember to project scope#738devon3000 wants to merge 3 commits into
Conversation
resolveProject() derived project scope from the git toplevel basename (or cwd basename), so the same repo checked out under different directory names — or two unrelated repos sharing a directory name — could not be distinguished or unified reliably across machines. Add an opt-in AGENTMEMORY_PROJECT_FROM_REMOTE flag: when set, resolveProject derives a stable "host/org/repo" identity from remote.origin.url, normalizing scp-style SSH, ssh://, git://, https://, and credentialed URLs. Falls back to the existing git-toplevel/cwd basename behavior when there is no remote, so default behavior is unchanged. AGENTMEMORY_PROJECT_NAME still overrides. Shared resolver, so all nine hooks pick this up; server stores the value verbatim. Adds normalizeGitRemote unit tests and remote-mode coverage. Refs: rohitg00#733
Consolidate fragmented legacy `project` tags (full-path / basename) onto a single canonical identity. Re-tags sessions/summaries/memories/lessons/actions and renames+merges profile keys (kept newest on collision). Dry-run by default; --apply writes a timestamped backup first. Companion to the AGENTMEMORY_PROJECT_FROM_REMOTE fix. See rohitg00#733. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
memory_save previously stored a memory with a project only when the caller passed one explicitly — which Claude almost never does — so remembered facts landed unscoped (project=null) while sessions and observations (written by the session-start hook) carried the resolved project. That asymmetry silently excluded remembered facts from project-scoped surfaces (mem::context, the rolling profile). Resolve project the same way the hook does: an explicit project always wins; scope:"global" stores unscoped for genuinely cross-project facts; otherwise default to resolveProject() so memories unify with the session's project. Applied to both MCP entry points — the standalone client proxy/local paths (the runtime npx surface, which has the repo cwd) and the in-container SDK handler. Companion to the git-remote project identity work (rohitg00#733). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@devon3000 is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR implements optional git remote-based project identity resolution across the codebase. When ChangesGit Remote-Based Project Identity
Sequence Diagram(s)flowchart TD
Start([resolveProject called]) --> CheckEnv{AGENTMEMORY_PROJECT_NAME set?}
CheckEnv -->|yes| ReturnEnv["return env override"]
CheckEnv -->|no| CheckRemoteFlag{AGENTMEMORY_PROJECT_FROM_REMOTE enabled?}
CheckRemoteFlag -->|yes| ReadRemote["git config --get remote.origin.url"]
CheckRemoteFlag -->|no| CheckGitToplevel
ReadRemote --> NormalizeURL["normalizeGitRemote: host/org/repo"]
NormalizeURL --> HasRemote{normalized?}
HasRemote -->|yes| ReturnRemote["return remote identity"]
HasRemote -->|no| CheckGitToplevel["git rev-parse --show-toplevel"]
CheckGitToplevel --> HasToplevel{success?}
HasToplevel -->|yes| ReturnToplevel["return basename"]
HasToplevel -->|no| ReturnCwd["return basename(cwd)"]
ReturnEnv --> End([return identifier])
ReturnRemote --> End
ReturnToplevel --> End
ReturnCwd --> End
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/hook-project.test.ts (1)
57-60:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
basename(dir)in these assertions.
dir.split("/").pop()is POSIX-only, so these tests will fail on Windows and no longer mirror the implementation they're checking.Suggested change
-import { join } from "node:path"; +import { basename, join } from "node:path"; @@ - expect(resolveProject(dir)).toBe(dir.split("/").pop()); + expect(resolveProject(dir)).toBe(basename(dir)); @@ - expect(resolveProject(dir)).toBe(dir.split("/").pop()); + expect(resolveProject(dir)).toBe(basename(dir));Also applies to: 99-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/hook-project.test.ts` around lines 57 - 60, Replace POSIX-only dir.split("/").pop() with path.basename(dir) in the tests that assert resolveProject(dir) falls back to the cwd basename; import or use the existing basename from the path module and update both the assertion at the resolveProject(dir) case (around the block using mkdtempSync in hook-project.test.ts) and the similar assertions in the second block (lines ~99-104) so tests are platform-independent and mirror the implementation.
🧹 Nitpick comments (2)
src/mcp/standalone.ts (1)
126-128: ⚡ Quick winDrop the WHAT-comment in
memory_savevalidation.The code is already readable enough here, and the added prose violates the repo's no-WHAT-comments rule for
srcfiles.As per coding guidelines, "
src/**/*.{ts,js}: No code comments explaining WHAT — use clear naming instead".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mcp/standalone.ts` around lines 126 - 128, Remove the WHAT-style explanatory comment in the memory_save validation block in src/mcp/standalone.ts; locate the comment near the memory_save validation logic (the block that starts with "Explicit project wins; scope:\"global\" stores unscoped; otherwise default to the current project...") and delete that prose comment so the code adheres to the repo rule forbidding WHAT-comments in src files while keeping the existing variable and function names (memory_save validation code) intact.src/hooks/_project.ts (1)
19-21: ⚡ Quick winRemove the WHAT-comments from this helper.
These blocks restate behavior the function names and tests already cover, and they violate the repo rule for
srcfiles.As per coding guidelines, "
src/**/*.{ts,js}: No code comments explaining WHAT — use clear naming instead".Also applies to: 73-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/_project.ts` around lines 19 - 21, Remove the explanatory "WHAT" comment blocks that restate behavior for the helper that normalizes any git remote URL (the top comment describing scp-style SSH, ssh://, https://, git:// and credential-carrying URLs) and the similar comment block later in the file (the second block covering scp/ssh/https/gitea-style examples). Replace them with either no comment or a single-line descriptive summary (e.g., "Normalize git remote URLs to host/org/repo") if you want a short identifier, and do not add any additional behavioral explanation — leave the function name and tests to convey the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plugin/scripts/post-tool-use.mjs`:
- Around line 22-41: normalizeGitRemote currently treats local filesystem paths
like "C:/work/repo.git" as valid remotes; update normalizeGitRemote to detect
and reject local-path remotes by returning null so resolveProject() can fall
back to repo top-level. Before parsing/scp handling, add a guard that returns
null when raw matches local path patterns (e.g., starts with "/", "./", "../",
"~/" or Windows drive-letter patterns like /^[A-Za-z]:[\\/]/, or contains
backslashes without a protocol/user@host), then continue the existing
scp/noCreds logic; reference the normalizeGitRemote function and its local
variables raw, scp, noCreds, host, and path when making the change.
In `@src/mcp/server.ts`:
- Around line 182-188: The code currently falls back to resolveProject() when
args.project is omitted, which wrongly binds direct MCP saves to the server's
startup repo; change the project resolution so project is set only from
explicitProject or left undefined (respecting isGlobal) and remove the
resolveProject() fallback call (i.e., update the logic using explicitProject,
isGlobal, and project to not call or use resolveProject()).
---
Outside diff comments:
In `@test/hook-project.test.ts`:
- Around line 57-60: Replace POSIX-only dir.split("/").pop() with
path.basename(dir) in the tests that assert resolveProject(dir) falls back to
the cwd basename; import or use the existing basename from the path module and
update both the assertion at the resolveProject(dir) case (around the block
using mkdtempSync in hook-project.test.ts) and the similar assertions in the
second block (lines ~99-104) so tests are platform-independent and mirror the
implementation.
---
Nitpick comments:
In `@src/hooks/_project.ts`:
- Around line 19-21: Remove the explanatory "WHAT" comment blocks that restate
behavior for the helper that normalizes any git remote URL (the top comment
describing scp-style SSH, ssh://, https://, git:// and credential-carrying URLs)
and the similar comment block later in the file (the second block covering
scp/ssh/https/gitea-style examples). Replace them with either no comment or a
single-line descriptive summary (e.g., "Normalize git remote URLs to
host/org/repo") if you want a short identifier, and do not add any additional
behavioral explanation — leave the function name and tests to convey the
behavior.
In `@src/mcp/standalone.ts`:
- Around line 126-128: Remove the WHAT-style explanatory comment in the
memory_save validation block in src/mcp/standalone.ts; locate the comment near
the memory_save validation logic (the block that starts with "Explicit project
wins; scope:\"global\" stores unscoped; otherwise default to the current
project...") and delete that prose comment so the code adheres to the repo rule
forbidding WHAT-comments in src files while keeping the existing variable and
function names (memory_save validation code) intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6344e0a4-7b83-4b92-b3a6-407cca2e47af
📒 Files selected for processing (16)
plugin/scripts/notification.mjsplugin/scripts/post-tool-failure.mjsplugin/scripts/post-tool-use.mjsplugin/scripts/pre-compact.mjsplugin/scripts/prompt-submit.mjsplugin/scripts/session-start.mjsplugin/scripts/subagent-start.mjsplugin/scripts/subagent-stop.mjsplugin/scripts/task-completed.mjsscripts/backfill-project-identity.mjssrc/hooks/_project.tssrc/mcp/server.tssrc/mcp/standalone.tssrc/mcp/tools-registry.tstest/hook-project.test.tstest/mcp-standalone.test.ts
| function normalizeGitRemote(url) { | ||
| const raw = (url ?? "").trim(); | ||
| if (!raw) return null; | ||
| let host = ""; | ||
| let path = ""; | ||
| const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/); | ||
| if (scp) { | ||
| host = scp[1]; | ||
| path = scp[2]; | ||
| } else { | ||
| const noCreds = raw.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").replace(/^[^@/]*@/, ""); | ||
| const slash = noCreds.indexOf("/"); | ||
| if (slash === -1) return null; | ||
| host = noCreds.slice(0, slash); | ||
| path = noCreds.slice(slash + 1); | ||
| } | ||
| host = host.toLowerCase().replace(/:\d+$/, ""); | ||
| path = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, ""); | ||
| if (!host || !path) return null; | ||
| return `${host}/${path}`; |
There was a problem hiding this comment.
Return null for local-path remotes instead of treating them as canonical IDs.
With AGENTMEMORY_PROJECT_FROM_REMOTE=1, a local origin like C:/work/repo.git currently normalizes to c:/work/repo, which is machine-specific and reintroduces the fragmentation this feature is meant to eliminate. These cases should fail normalization so resolveProject() falls back to the git toplevel/cwd basename.
Suggested fix
function normalizeGitRemote(url) {
const raw = (url ?? "").trim();
if (!raw) return null;
+ if (/^(file:|\/|\.{1,2}[\\/]|[A-Za-z]:[\\/]|\\\\)/.test(raw)) return null;
let host = "";
let path = "";
const scp = raw.match(/^[^`@/`]+@([^:/]+):(.+)$/);
if (scp) {
host = scp[1];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function normalizeGitRemote(url) { | |
| const raw = (url ?? "").trim(); | |
| if (!raw) return null; | |
| let host = ""; | |
| let path = ""; | |
| const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/); | |
| if (scp) { | |
| host = scp[1]; | |
| path = scp[2]; | |
| } else { | |
| const noCreds = raw.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").replace(/^[^@/]*@/, ""); | |
| const slash = noCreds.indexOf("/"); | |
| if (slash === -1) return null; | |
| host = noCreds.slice(0, slash); | |
| path = noCreds.slice(slash + 1); | |
| } | |
| host = host.toLowerCase().replace(/:\d+$/, ""); | |
| path = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, ""); | |
| if (!host || !path) return null; | |
| return `${host}/${path}`; | |
| function normalizeGitRemote(url) { | |
| const raw = (url ?? "").trim(); | |
| if (!raw) return null; | |
| if (/^(file:|\/|\.{1,2}[\\/]|[A-Za-z]:[\\/]|\\\\)/.test(raw)) return null; | |
| let host = ""; | |
| let path = ""; | |
| const scp = raw.match(/^[^`@/`]+@([^:/]+):(.+)$/); | |
| if (scp) { | |
| host = scp[1]; | |
| path = scp[2]; | |
| } else { | |
| const noCreds = raw.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").replace(/^[^`@/`]*`@/`, ""); | |
| const slash = noCreds.indexOf("/"); | |
| if (slash === -1) return null; | |
| host = noCreds.slice(0, slash); | |
| path = noCreds.slice(slash + 1); | |
| } | |
| host = host.toLowerCase().replace(/:\d+$/, ""); | |
| path = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, ""); | |
| if (!host || !path) return null; | |
| return `${host}/${path}`; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugin/scripts/post-tool-use.mjs` around lines 22 - 41, normalizeGitRemote
currently treats local filesystem paths like "C:/work/repo.git" as valid
remotes; update normalizeGitRemote to detect and reject local-path remotes by
returning null so resolveProject() can fall back to repo top-level. Before
parsing/scp handling, add a guard that returns null when raw matches local path
patterns (e.g., starts with "/", "./", "../", "~/" or Windows drive-letter
patterns like /^[A-Za-z]:[\\/]/, or contains backslashes without a
protocol/user@host), then continue the existing scp/noCreds logic; reference the
normalizeGitRemote function and its local variables raw, scp, noCreds, host, and
path when making the change.
| const explicitProject = | ||
| typeof args.project === "string" && args.project.trim().length > 0 | ||
| ? args.project.trim() | ||
| : undefined; | ||
| const isGlobal = | ||
| typeof args.scope === "string" && args.scope.trim().toLowerCase() === "global"; | ||
| const project = explicitProject ?? (isGlobal ? undefined : resolveProject()); |
There was a problem hiding this comment.
Don't default direct MCP saves to the server process project.
This handler has no caller cwd/project context, so resolveProject() here binds omitted memory_save.project values to whatever repo the MCP server was started in. Direct MCP clients will silently write memories into the wrong project bucket unless they always pass project explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/mcp/server.ts` around lines 182 - 188, The code currently falls back to
resolveProject() when args.project is omitted, which wrongly binds direct MCP
saves to the server's startup repo; change the project resolution so project is
set only from explicitProject or left undefined (respecting isGlobal) and remove
the resolveProject() fallback call (i.e., update the logic using
explicitProject, isGlobal, and project to not call or use resolveProject()).
Summary
Fixes #733. Project identity was the git-toplevel basename, so the same repo checked out on two machines/paths (or two different repos sharing a name) collided or fragmented across project-scoped surfaces (session lists, the rolling profile, session-start auto-context). This adds an opt-in, stable identity and makes the
rememberpath participate in project scoping the same way sessions do.Three commits:
feat(project)— opt-in stable identity from git remote. NewAGENTMEMORY_PROJECT_FROM_REMOTE=1makesresolveProject()derive ahost/org/repoidentity fromremote.origin.url(vianormalizeGitRemote, handling scp/ssh/https/git URLs, credentials, ports, nested groups). Off by default — behavior is unchanged unless the flag is set. Resolution order:AGENTMEMORY_PROJECT_NAME→ git remote (when enabled) → git-toplevel basename → cwd basename.feat(scripts)— one-time backfill.scripts/backfill-project-identity.mjsconsolidates legacy fragmentedprojecttags onto a canonical identity in the standalone JSON store. Dry-run by default;--applywrites a timestamped.bakfirst.feat(remember)— defaultmemory_saveto project scope. Previouslymemory_saveonly set a project when the caller passed one explicitly (which the model rarely does), so remembered facts landed unscoped (project=null) while hook-written sessions/observations carried the resolved project. That asymmetry silently excluded remembered facts frommem::contextand the profile. Now: explicitprojectwins;scope:"global"stores unscoped for genuinely cross-project facts; otherwise default toresolveProject(). Applied to both MCP entry points (the standalone client proxy/local paths and the in-container SDK handler).Notes
scope/default only affect memories that previously had no project at all.normalizeGitRemote+resolveProjectcovered bytest/hook-project.test.ts(21 cases); the newmemory_savescoping bytest/mcp-standalone.test.ts(explicit-wins / global-unscoped / default-to-project).Test plan
AGENTMEMORY_PROJECT_FROM_REMOTE=1yieldshost/org/repo; unset preserves basename behavior.memory_savewith no project defaults to the current project;scope:"global"stays unscoped; explicit project wins.--applywrites a.bak.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests