From f78d811f84b1dbf9a7d8fecd51d6a7f87f086dcc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Mar 2026 11:09:13 +0900 Subject: [PATCH 1/3] fix(lsp): include file paths in directory diagnostics output --- src/tools/lsp/directory-diagnostics.test.ts | 72 ++++++++++++++++++++- src/tools/lsp/directory-diagnostics.ts | 18 ++++-- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/tools/lsp/directory-diagnostics.test.ts b/src/tools/lsp/directory-diagnostics.test.ts index 4c932e036..ca3f2d3a7 100644 --- a/src/tools/lsp/directory-diagnostics.test.ts +++ b/src/tools/lsp/directory-diagnostics.test.ts @@ -1,12 +1,56 @@ -import { describe, expect, it } from "bun:test" +import { beforeEach, describe, expect, it, mock } from "bun:test" import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs" import { join } from "path" import os from "os" -import { isDirectoryPath } from "./lsp-client-wrapper" -import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics" +import type { Diagnostic } from "./types" + +const diagnosticsMock = mock(async (_filePath: string) => ({ items: [] as Diagnostic[] })) +const getClientMock = mock(async () => ({ diagnostics: diagnosticsMock })) +const releaseClientMock = mock(() => {}) + +mock.module("./config", () => ({ + findServerForExtension: (extension: string) => ({ + status: "found" as const, + server: { + id: "test-server", + command: ["test-server"], + extensions: [extension], + priority: 1, + }, + }), + getLanguageId: () => "typescript", +})) + +mock.module("./lsp-server", () => ({ + lspManager: { + getClient: getClientMock, + releaseClient: releaseClientMock, + }, +})) + +const { isDirectoryPath } = await import("./lsp-client-wrapper") +const { aggregateDiagnosticsForDirectory } = await import("./directory-diagnostics") + +function createDiagnostic(message: string): Diagnostic { + return { + message, + severity: 1, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 1 }, + }, + } +} describe("directory diagnostics", () => { + beforeEach(() => { + diagnosticsMock.mockReset() + diagnosticsMock.mockImplementation(async (_filePath: string) => ({ items: [] })) + getClientMock.mockClear() + releaseClientMock.mockClear() + }) + describe("isDirectoryPath", () => { it("returns true for existing directory", () => { const tmp = mkdtempSync(join(os.tmpdir(), "omo-isdir-")) @@ -52,5 +96,27 @@ describe("directory diagnostics", () => { "Directory does not exist" ) }) + + it("#given diagnostics from multiple files #when aggregating directory diagnostics #then each entry includes the source file path", async () => { + const tmp = mkdtempSync(join(os.tmpdir(), "omo-aggr-files-")) + try { + const firstFile = join(tmp, "first.ts") + const secondFile = join(tmp, "second.ts") + + writeFileSync(firstFile, "export const first = true\n") + writeFileSync(secondFile, "export const second = true\n") + + diagnosticsMock.mockImplementation(async (filePath: string) => ({ + items: [createDiagnostic(`problem in ${filePath}`)], + })) + + const result = await aggregateDiagnosticsForDirectory(tmp, ".ts") + + expect(result).toContain(`${firstFile}: error at 1:0: problem in ${firstFile}`) + expect(result).toContain(`${secondFile}: error at 1:0: problem in ${secondFile}`) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } + }) }) }) diff --git a/src/tools/lsp/directory-diagnostics.ts b/src/tools/lsp/directory-diagnostics.ts index a99450fa8..b3dd96106 100644 --- a/src/tools/lsp/directory-diagnostics.ts +++ b/src/tools/lsp/directory-diagnostics.ts @@ -11,6 +11,11 @@ import type { Diagnostic } from "./types" const SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]) +type FileDiagnostic = { + filePath: string + diagnostic: Diagnostic +} + function collectFilesWithExtension(dir: string, extension: string, maxFiles: number): string[] { const files: string[] = [] @@ -95,7 +100,7 @@ export async function aggregateDiagnosticsForDirectory( const root = findWorkspaceRoot(absDir) - const allDiagnostics: Diagnostic[] = [] + const allDiagnostics: FileDiagnostic[] = [] const fileErrors: { file: string; error: string }[] = [] let client: LSPClient @@ -106,7 +111,12 @@ export async function aggregateDiagnosticsForDirectory( try { const result = await client.diagnostics(file) const filtered = filterDiagnosticsBySeverity(result.items, severity) - allDiagnostics.push(...filtered) + allDiagnostics.push( + ...filtered.map((diagnostic) => ({ + filePath: file, + diagnostic, + })) + ) } catch (e) { fileErrors.push({ file, @@ -138,8 +148,8 @@ export async function aggregateDiagnosticsForDirectory( if (displayDiagnostics.length > 0) { lines.push("") - for (const diag of displayDiagnostics) { - lines.push(formatDiagnostic(diag)) + for (const { filePath, diagnostic } of displayDiagnostics) { + lines.push(`${filePath}: ${formatDiagnostic(diagnostic)}`) } if (wasDiagCapped) { lines.push( From 4517699d5e360e3b61ea35983bc4781127d05f68 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Mar 2026 11:09:13 +0900 Subject: [PATCH 2/3] fix(atlas): clarify capped directory diagnostics guidance --- src/agents/atlas/default.ts | 8 ++++---- src/agents/atlas/gemini.ts | 6 +++--- src/agents/atlas/gpt.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/agents/atlas/default.ts b/src/agents/atlas/default.ts index 380606752..8bc360795 100644 --- a/src/agents/atlas/default.ts +++ b/src/agents/atlas/default.ts @@ -184,7 +184,7 @@ task( After EVERY delegation, complete ALL of these steps — no shortcuts: #### A. Automated Verification -1. 'lsp_diagnostics(filePath=".", extension=".ts")' → ZERO errors at project level (for directory paths, extension parameter is required) +1. 'lsp_diagnostics(filePath=".", extension=".ts")' → ZERO errors across scanned TypeScript files (directory scans are capped at 50 files; not a full-project guarantee) 2. \`bun run build\` or \`bun run typecheck\` → exit code 0 3. \`bun test\` → ALL tests pass @@ -346,7 +346,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING. **After each delegation — BOTH automated AND manual verification are MANDATORY:** -1. 'lsp_diagnostics(filePath=".", extension=".ts")' at PROJECT level → ZERO errors (for directory paths, extension parameter is required) +1. 'lsp_diagnostics(filePath=".", extension=".ts")' across scanned TypeScript files → ZERO errors (directory scans are capped at 50 files; not a full-project guarantee) 2. Run build command → exit 0 3. Run test suite → ALL pass 4. **\`Read\` EVERY changed file line by line** → logic matches requirements @@ -390,14 +390,14 @@ You are the QA gate. Subagents lie. Verify EVERYTHING. - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics after delegation (use 'filePath=".", extension=".ts"' for TypeScript projects) +- Skip scanned-file lsp_diagnostics after delegation (use 'filePath=".", extension=".ts"' for TypeScript projects; directory scans are capped at 50 files) - Batch multiple tasks in one delegation - Start fresh session for failures/follow-ups - use \`resume\` instead **ALWAYS**: - Include ALL 6 sections in delegation prompts - Read notepad before every delegation -- Run project-level QA after every delegation +- Run scanned-file QA after every delegation - Pass inherited wisdom to every subagent - Parallelize independent tasks - Verify with your own tools diff --git a/src/agents/atlas/gemini.ts b/src/agents/atlas/gemini.ts index 73048975e..48c5b1763 100644 --- a/src/agents/atlas/gemini.ts +++ b/src/agents/atlas/gemini.ts @@ -361,14 +361,14 @@ Subagents CLAIM "done" when: - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects) +- Skip scanned-file lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects; directory scans are capped at 50 files) - Batch multiple tasks in one delegation - Start fresh session for failures (use session_id) **ALWAYS**: - Include ALL 6 sections in delegation prompts - Read notepad before every delegation -- Run project-level QA after every delegation +- Run scanned-file QA after every delegation - Pass inherited wisdom to every subagent - Parallelize independent tasks - Store and reuse session_id for retries @@ -392,4 +392,4 @@ This ensures accurate progress tracking. Skip this and you lose visibility into export function getGeminiAtlasPrompt(): string { return ATLAS_GEMINI_SYSTEM_PROMPT -} \ No newline at end of file +} diff --git a/src/agents/atlas/gpt.ts b/src/agents/atlas/gpt.ts index 2a0fe8c80..2549075a5 100644 --- a/src/agents/atlas/gpt.ts +++ b/src/agents/atlas/gpt.ts @@ -55,7 +55,7 @@ Implementation tasks are the means. Final Wave approval is the goal. - Verification (use Bash for tests/build) - Parallelize independent tool calls when possible. - After ANY delegation, verify with your own tool calls: - 1. 'lsp_diagnostics(filePath=".", extension=".ts")' at project level (for directory paths, extension parameter is required) + 1. 'lsp_diagnostics(filePath=".", extension=".ts")' across scanned TypeScript files (directory scans are capped at 50 files; not a full-project guarantee) 2. \`Bash\` for build/test commands 3. \`Read\` for changed files @@ -364,14 +364,14 @@ Your job is to CATCH THEM. Assume every claim is false until YOU personally veri - Trust subagent claims without verification - Use run_in_background=true for task execution - Send prompts under 30 lines -- Skip project-level lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects) +- Skip scanned-file lsp_diagnostics (use 'filePath=".", extension=".ts"' for TypeScript projects; directory scans are capped at 50 files) - Batch multiple tasks in one delegation - Start fresh session for failures (use session_id) **ALWAYS**: - Include ALL 6 sections in delegation prompts - Read notepad before every delegation -- Run project-level QA after every delegation +- Run scanned-file QA after every delegation - Pass inherited wisdom to every subagent - Parallelize independent tasks - Store and reuse session_id for retries From dc23e63fa61a01e65b653825812358b36e466a15 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 9 Mar 2026 11:16:38 +0900 Subject: [PATCH 3/3] test(lsp): avoid leaking directory diagnostics mocks across tests --- src/tools/lsp/directory-diagnostics.test.ts | 45 ++++++++++----------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/tools/lsp/directory-diagnostics.test.ts b/src/tools/lsp/directory-diagnostics.test.ts index ca3f2d3a7..b46875a70 100644 --- a/src/tools/lsp/directory-diagnostics.test.ts +++ b/src/tools/lsp/directory-diagnostics.test.ts @@ -1,37 +1,18 @@ -import { beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs" import { join } from "path" import os from "os" +import * as configModule from "./config" +import { lspManager } from "./lsp-server" +import { isDirectoryPath } from "./lsp-client-wrapper" +import { aggregateDiagnosticsForDirectory } from "./directory-diagnostics" import type { Diagnostic } from "./types" const diagnosticsMock = mock(async (_filePath: string) => ({ items: [] as Diagnostic[] })) const getClientMock = mock(async () => ({ diagnostics: diagnosticsMock })) const releaseClientMock = mock(() => {}) -mock.module("./config", () => ({ - findServerForExtension: (extension: string) => ({ - status: "found" as const, - server: { - id: "test-server", - command: ["test-server"], - extensions: [extension], - priority: 1, - }, - }), - getLanguageId: () => "typescript", -})) - -mock.module("./lsp-server", () => ({ - lspManager: { - getClient: getClientMock, - releaseClient: releaseClientMock, - }, -})) - -const { isDirectoryPath } = await import("./lsp-client-wrapper") -const { aggregateDiagnosticsForDirectory } = await import("./directory-diagnostics") - function createDiagnostic(message: string): Diagnostic { return { message, @@ -49,6 +30,22 @@ describe("directory diagnostics", () => { diagnosticsMock.mockImplementation(async (_filePath: string) => ({ items: [] })) getClientMock.mockClear() releaseClientMock.mockClear() + + spyOn(configModule, "findServerForExtension").mockReturnValue({ + status: "found", + server: { + id: "test-server", + command: ["test-server"], + extensions: [".ts"], + priority: 1, + }, + }) + spyOn(lspManager, "getClient").mockImplementation(getClientMock) + spyOn(lspManager, "releaseClient").mockImplementation(releaseClientMock) + }) + + afterEach(() => { + mock.restore() }) describe("isDirectoryPath", () => {