From 19208e656068ea84893d7f32c34277adfb77b6e7 Mon Sep 17 00:00:00 2001 From: Mohammad Alsayyedan Date: Sun, 20 Sep 2026 21:58:13 -0500 Subject: [PATCH] Resolve Claude workspace roots so Context Health can scan them Every Claude workspace is silently dropped from the Context Health page, so the page reports "No workspaces have context instruction files" and an agentic readiness of 0 even when the projects are well configured. resolveWorkspaceRoot routes claude-* ids to resolveClaudeRoot, which expects ~/.claude/projects/ and reads cwd out of the first .jsonl line. But ws.path is already the project root, exactly as it is for codex-* and opencode-* ids. readdirSync finds no .jsonl there, the helper returns null, and buildWorkspaceHealth discards the workspace. Only Claude is affected: the other CLI harnesses take the existsSync branch and resolve correctly. Use ws.path when it is a real project directory, keeping the session file lookup as a fallback for paths that do still point at a Claude session store. Both branches now reject paths that do not exist. On a real log set this takes Context Health from 1 usable workspace to 7, the overall score from 7/100 to 34/100, agentic readiness from 0 to 30, and correctly reports 4 of 7 workspaces as having instruction files. --- src/core/config-health-helpers.test.ts | 32 ++++++++++++++++++++++++++ src/core/config-health-helpers.ts | 17 +++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/core/config-health-helpers.test.ts b/src/core/config-health-helpers.test.ts index cb25d71b..e1954a27 100644 --- a/src/core/config-health-helpers.test.ts +++ b/src/core/config-health-helpers.test.ts @@ -329,3 +329,35 @@ describe('readSnippet', () => { expect(snippet.length).toBe(50); }); }); + +describe('resolveWorkspaceRoot', () => { + it('uses a Claude workspace path that is already the project root', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-root-')); + try { + const resolved = resolveWorkspaceRoot('claude--home-me-proj', { id: 'claude--home-me-proj', name: 'proj', path: root }); + expect(resolved).toBe(root); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('still reads cwd from a session file when the path is a .claude/projects store', () => { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-store-')); + const projectRoot = path.join(base, 'real-project'); + const store = path.join(base, '.claude', 'projects', '-tmp-real-project'); + fs.mkdirSync(projectRoot, { recursive: true }); + fs.mkdirSync(store, { recursive: true }); + fs.writeFileSync(path.join(store, 's.jsonl'), JSON.stringify({ cwd: projectRoot }) + '\n'); + try { + expect(resolveWorkspaceRoot('claude-x', { id: 'claude-x', name: 'x', path: store })).toBe(projectRoot); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } + }); + + it('returns null for a Claude workspace whose path no longer exists', () => { + const gone = path.join(os.tmpdir(), 'cfg-claude-missing-' + Date.now()); + expect(resolveWorkspaceRoot('claude-gone', { id: 'claude-gone', name: 'gone', path: gone })).toBeNull(); + }); +}); + diff --git a/src/core/config-health-helpers.ts b/src/core/config-health-helpers.ts index ff2598f9..9d693f38 100644 --- a/src/core/config-health-helpers.ts +++ b/src/core/config-health-helpers.ts @@ -74,14 +74,29 @@ function firstStringProperty(value: Record, ...keys: string[]): export function resolveWorkspaceRoot(id: string, ws: Workspace): string | null { if (id.startsWith('claude-')) { + // `ws.path` is normally the project root already, as it is for the other + // CLI harnesses. Fall back to reading `cwd` out of a session file for + // workspaces whose path still points at ~/.claude/projects/. + if (isProjectRoot(ws.path)) return ws.path; return resolveClaudeRoot(ws.path); } if (id.startsWith('codex-') || id.startsWith('opencode-')) { - return fs.existsSync(ws.path) ? ws.path : null; + return isProjectRoot(ws.path) ? ws.path : null; } return resolveVsCodeRoot(ws.path) ?? resolveCLIRoot(ws.path); } +/** True when the path is an existing directory that is not a Claude session + * store (those hold .jsonl transcripts, not the user's project files). */ +function isProjectRoot(p: string): boolean { + try { + if (!fs.statSync(p).isDirectory()) return false; + } catch { + return false; + } + return path.basename(path.dirname(p)) !== 'projects' || !path.dirname(p).includes('.claude'); +} + function resolveVsCodeRoot(storagePath: string): string | null { const wsJson = path.join(storagePath, 'workspace.json'); const data = readJsonFile(wsJson);