mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 17:05:28 +02:00
fix: read project-scoped MCP registrations in gbrain detection (#2499)
Claude Code registers MCP servers at two scopes in ~/.claude.json: user scope (.mcpServers) and project scope (.projects["/abs/path"].mcpServers — what `claude mcp add` WITHOUT --scope user writes). Every gbrain detection site read only user scope, so a correctly configured project-scoped brain was invisible: brain-aware blocks suppressed, remote-mode artifacts sync never recognised, and detectEndpointHash fell through to the 'local' literal — two different project-scoped brains hashed identically, so switching between them never invalidated the cache, the exact scenario the function's docstring says it exists to catch. Nothing errored; the features just quietly were not there. Two sites fixed: - scripts/resolvers/preamble/generate-brain-sync-block.ts: the shared detection block (rendered into every tier-2+ SKILL.md) now resolves the gbrain entry ONCE into _GBRAIN_MCP_ENTRY — user scope first, then the nearest-ancestor project entry for $PWD that actually carries a gbrain server (longest matching key with a path-boundary check: /a/repo never matches /a/repo2; a nested project WITHOUT gbrain doesn't shadow its parent's registration). _GBRAIN_MCP_TYPE and _GBRAIN_HOST extract from the resolved entry, so claude.json is parsed once per skill start. All SKILL.md files regenerated in this commit; the ship golden fixtures and three carve-guard skeleton caps (plan-eng-review, plan-devex-review, office-hours; ~1.5KB rendered growth per skill) are refreshed with measured values. - bin/gstack-brain-cache detectEndpointHash: same resolution order in TS (user scope, else nearest-ancestor project entry by cwd, both path separators for Windows keys). Tests: rendered-output tests in test/gen-skill-docs.test.ts pin the regenerated block (static markers + a FUNCTIONAL run of the exact rendered lines against a fixture ~/.claude.json with only a project-scoped registration, plus an outside-cwd discriminator); detectEndpointHash unit tests in test/brain-cache-roundtrip.test.ts cover project-scope resolve, path-boundary, nearest-ancestor distinct hashes, and user-scope precedence. Root-cause analysis by @samporter-31 in #2499. Fixes #2499 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5854d122d3
commit
acc354fcfa
@@ -131,6 +131,59 @@ describe('brain-cache endpoint detection', () => {
|
||||
expect(typeof hash).toBe('string');
|
||||
expect(hash.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// #2499: project-scoped registrations (.projects["/path"].mcpServers.gbrain)
|
||||
// were never read — two different project-scoped brains both hashed to
|
||||
// 'local', so switching between them never invalidated the cache.
|
||||
test('detectEndpointHash resolves a project-scoped gbrain URL for a cwd inside the project (#2499)', async () => {
|
||||
const mod = await importCache();
|
||||
const cj = join(TMP_HOME, 'claude.json');
|
||||
writeFileSync(cj, JSON.stringify({
|
||||
projects: {
|
||||
'/w/repo': { mcpServers: { gbrain: { type: 'http', url: 'https://a.example/mcp' } } },
|
||||
},
|
||||
}));
|
||||
const inside = mod.detectEndpointHash(cj, '/w/repo/src/deep');
|
||||
expect(inside).not.toBe('local');
|
||||
expect(inside).toHaveLength(8);
|
||||
// Path-boundary check: /w/repo2 is NOT inside /w/repo.
|
||||
expect(mod.detectEndpointHash(cj, '/w/repo2')).toBe('local');
|
||||
});
|
||||
|
||||
test('detectEndpointHash prefers the nearest-ancestor project entry (#2499)', async () => {
|
||||
const mod = await importCache();
|
||||
const cj = join(TMP_HOME, 'claude.json');
|
||||
writeFileSync(cj, JSON.stringify({
|
||||
projects: {
|
||||
'/w/repo': { mcpServers: { gbrain: { url: 'https://outer.example/mcp' } } },
|
||||
'/w/repo/nested': { mcpServers: { gbrain: { url: 'https://inner.example/mcp' } } },
|
||||
},
|
||||
}));
|
||||
const inner = mod.detectEndpointHash(cj, '/w/repo/nested/sub');
|
||||
const outer = mod.detectEndpointHash(cj, '/w/repo/other');
|
||||
expect(inner).not.toBe(outer); // two brains → two hashes (the docstring scenario)
|
||||
expect(inner).not.toBe('local');
|
||||
expect(outer).not.toBe('local');
|
||||
});
|
||||
|
||||
test('detectEndpointHash still prefers user scope over project scope (#2499)', async () => {
|
||||
const mod = await importCache();
|
||||
const cj = join(TMP_HOME, 'claude.json');
|
||||
writeFileSync(cj, JSON.stringify({
|
||||
mcpServers: { gbrain: { url: 'https://user.example/mcp' } },
|
||||
projects: {
|
||||
'/w/repo': { mcpServers: { gbrain: { url: 'https://proj.example/mcp' } } },
|
||||
},
|
||||
}));
|
||||
const userScoped = mod.detectEndpointHash(cj, '/w/repo');
|
||||
// Same file minus the user-scope entry → different hash proves user scope won.
|
||||
writeFileSync(cj, JSON.stringify({
|
||||
projects: {
|
||||
'/w/repo': { mcpServers: { gbrain: { url: 'https://proj.example/mcp' } } },
|
||||
},
|
||||
}));
|
||||
expect(mod.detectEndpointHash(cj, '/w/repo')).not.toBe(userScoped);
|
||||
});
|
||||
});
|
||||
|
||||
describe('brain-cache schema mismatch behavior', () => {
|
||||
|
||||
Reference in New Issue
Block a user