diff --git a/.brv/context-tree/facts/conventions/rlm_curate_single_pass_workflow.md b/.brv/context-tree/facts/conventions/rlm_curate_single_pass_workflow.md index b94f964..c7ccb5f 100644 --- a/.brv/context-tree/facts/conventions/rlm_curate_single_pass_workflow.md +++ b/.brv/context-tree/facts/conventions/rlm_curate_single_pass_workflow.md @@ -1,38 +1,44 @@ --- title: RLM Curate Single-Pass Workflow -summary: Curation workflow guidance for single-pass RLM processing, including recon-aware execution and verification. +summary: 'Single-pass RLM curation workflow: use recon first, skip chunking for small contexts, curate directly with UPSERT, and verify via result.applied[].filePath.' tags: [] -related: [] +related: [facts/project/current_runtime_timestamp.md, facts/conventions/rlm_curate_workflow.md] keywords: [] createdAt: '2026-05-26T18:08:33.384Z' -updatedAt: '2026-05-26T18:08:33.384Z' +updatedAt: '2026-05-26T21:53:06.950Z' --- ## Reason -Preserve the curation workflow requirement inferred from the task instructions. +Document the single-pass RLM curation workflow requirements and execution guidance ## Raw Concept **Task:** -Document the single-pass RLM curation workflow and verification rule. +Document the RLM curate single-pass workflow and execution requirements. **Changes:** - Used precomputed recon output to proceed directly - Captured the requirement to verify via applied file paths +- Captured the single-pass recommendation for the current context +- Recorded the no-chunking rule for small contexts +- Recorded verification requirements for curate results **Flow:** -recon -> single-pass extraction -> curate -> verify applied paths +recon -> inspect suggestedMode -> skip chunking when single-pass -> curate directly -> verify applied file paths -**Timestamp:** 2026-05-26T18:08:21.401Z +**Timestamp:** 2026-05-26T21:52:59.866Z ## Narrative ### Structure -This entry records the operational guidance for handling small contexts in one pass. +This knowledge entry captures the execution path for single-pass RLM curation, including the expected control flow and verification approach. ### Dependencies -Relies on precomputed recon metadata and the curated result summary. +Depends on precomputed recon metadata and the curate result object; mapExtract is optional and only relevant when chunking is needed. ### Highlights -Emphasizes efficiency by skipping chunking when the context is already small enough. +The workflow emphasizes minimal steps for small contexts, direct UPSERT curation, and strict verification through applied file paths. ## Facts -- **curation_mode**: The recon result already computed suggests single-pass processing. [convention] -- **verification_method**: Verification should use applied file paths rather than readFile reads. [convention] +- **rlm_suggested_mode**: Recon was already computed and suggestedMode is single-pass for this context. [convention] +- **rlm_chunking_policy**: For single-pass contexts, chunking should be skipped entirely. [convention] +- **rlm_single_pass_steps**: When a context is small, it should be curated in two code_exec calls: recon and curate. [convention] +- **mapextract_timeout**: If mapExtract is used, code_exec must set timeout: 300000 on the tool call itself. [convention] +- **verification_method**: Verification for curation should use result.applied[].filePath and should not call readFile for verification. [convention] diff --git a/.brv/context-tree/facts/conventions/rlm_curation_single_pass_workflow.md b/.brv/context-tree/facts/conventions/rlm_curation_single_pass_workflow.md index 62ca558..86ad161 100644 --- a/.brv/context-tree/facts/conventions/rlm_curation_single_pass_workflow.md +++ b/.brv/context-tree/facts/conventions/rlm_curation_single_pass_workflow.md @@ -1,45 +1,50 @@ --- -title: RLM Curation Single-pass Workflow -summary: Single-pass RLM curation workflow uses recon data, direct extraction, dedup/grouping, UPSERT curation, and verification via applied file paths. +title: RLM Curation Single-Pass Workflow +summary: RLM curation workflow uses recon results to proceed directly to single-pass curation when suggestedMode is single-pass, with verification via applied file paths and mandatory status reporting. tags: [] related: [facts/project/rlm_curate_execution_guidance.md, facts/conventions/rlm_curate_single_pass_workflow.md] keywords: [] createdAt: '2026-05-26T18:31:24.268Z' -updatedAt: '2026-05-26T18:31:24.268Z' +updatedAt: '2026-05-26T22:27:38.282Z' --- ## Reason -Curate the single-pass RLM workflow guidance from the provided context +Document the single-pass RLM curation workflow and requirements from the provided context. ## Raw Concept **Task:** -Document the RLM curation approach for a single-pass context. +Document the RLM single-pass curation workflow and execution requirements. **Changes:** - Use pre-computed recon output to avoid redundant analysis - Proceed directly to extraction when suggestedMode is single-pass - Organize extracted facts with deduplication and grouping - Curate via UPSERT and verify through applied file paths +- Use recon output to choose single-pass mode when suggestedMode is single-pass +- Skip chunking entirely for small contexts +- Curate directly without additional extraction steps **Flow:** -pre-computed recon -> extraction -> dedup/group -> curate -> verify +recon -> single-pass decision -> curate -> verify applied file paths -> report status -**Timestamp:** 2026-05-26T18:31:12.025Z +**Timestamp:** 2026-05-26T22:27:31.244Z **Author:** ByteRover context engineer ## Narrative ### Structure -The guidance is an execution recipe for small RLM curation jobs: rely on the existing recon result, skip chunking, and complete the operation in one pass. +This context describes the small-context RLM path where recon already determined the mode and no chunking or map extraction is needed. ### Dependencies -Depends on the pre-computed recon result, the curation history variable, the metadata variable, and the taskId variable supplied by the caller. +Depends on precomputed recon metadata and the curation result object for verification. ### Highlights -Emphasizes direct extraction, use of tools.curation.dedup() and tools.curation.groupBySubject(), and final verification through applied file paths. +The workflow is optimized for concise contexts by skipping extraction overhead and relying on direct UPSERT curation. + +### Rules +Do not call tools.curation.recon when recon is already precomputed. Do not print raw context. Verify via result.applied[].filePath. ## Facts -- **curation_mode**: The context was small enough for single-pass curation. [convention] -- **recon_status**: Recon had already been computed before curation. [convention] -- **recon_usage**: The task requires proceeding directly to extraction without recomputing recon. [convention] -- **mapextract_task_id**: Map extraction should pass the taskId as a bare variable when used. [convention] -- **verification_method**: Verification should use result.applied[].filePath and not readFile. [convention] +- **rlm_single_pass_mode**: When recon suggests single-pass, chunking is skipped entirely. [convention] +- **rlm_single_pass_calls**: For single-pass contexts, the workflow should proceed in 2 code_exec calls: recon and curate. [convention] +- **rlm_verification_method**: Verification should use result.applied[].filePath rather than readFile. [convention] +- **mapextract_timeout_ms**: Any code_exec call containing mapExtract must use timeout 300000 on the tool call itself. [convention] diff --git a/.brv/context-tree/facts/conventions/rlm_curation_workflow_guidance.md b/.brv/context-tree/facts/conventions/rlm_curation_workflow_guidance.md index 44d47a3..c8b083a 100644 --- a/.brv/context-tree/facts/conventions/rlm_curation_workflow_guidance.md +++ b/.brv/context-tree/facts/conventions/rlm_curation_workflow_guidance.md @@ -1,18 +1,18 @@ --- title: RLM Curation Workflow Guidance -summary: RLM curation workflow guidance covering recon, single-pass handling, mapExtract timeouts, and verification rules. +summary: RLM curation uses single-pass processing when suggested, prefers UPSERT, and verifies applied file paths rather than rereading files. tags: [] related: [] keywords: [] createdAt: '2026-05-26T10:51:08.735Z' -updatedAt: '2026-05-26T14:26:07.876Z' +updatedAt: '2026-05-26T19:32:46.985Z' --- ## Reason -Capture curation workflow guidance and verification rules from the provided context. +Curate the current RLM curation instructions and verification conventions from the provided context ## Raw Concept **Task:** -Document the RLM curation workflow guidance for context-driven knowledge curation. +Curate the provided context using the RLM approach **Changes:** - Use precomputed recon results when available @@ -23,31 +23,36 @@ Document the RLM curation workflow guidance for context-driven knowledge curatio - Confirmed single-pass processing for the current context. - Captured the no-rerun recon rule for precomputed contexts. - Captured the mapExtract timeout requirement and verification rule. +- Use single-pass RLM processing when recon suggests single-pass +- Prefer UPSERT for curation operations +- Verify curation through result.applied[].filePath + +**Files:** +- .brv/context-tree/facts/conventions/context.md **Flow:** -recon precomputed -> extract context -> curate knowledge -> verify applied file paths +recon already computed -> extract key facts -> dedup/group -> curate -> verify applied file paths -**Timestamp:** 2026-05-26T14:25:50.483Z +**Timestamp:** 2026-05-26T19:32:39.707Z **Author:** ByteRover context engineer ## Narrative ### Structure -This guidance defines how to process curated context when recon is already available and when to switch to mapExtract for chunked inputs. +This guidance documents the curation workflow conventions used in the project context tree. ### Dependencies -Relies on precomputed recon metadata, task-scoped context variables, and curate result inspection. +Depends on the RLM flow and the curated knowledge store under .brv/context-tree. ### Highlights -The workflow emphasizes avoiding redundant recon calls, preserving raw context privacy, enforcing the 300000 ms timeout for mapExtract, and verifying via applied file paths. +The context emphasizes single-pass processing for small contexts and a no-readback verification pattern. ### Rules -Do NOT print raw context. Do NOT call tools.curation.recon when recon has already been precomputed. For chunked extraction use tools.curation.mapExtract() with timeout: 300000 on the code_exec call itself. Use tools.curation.groupBySubject() and tools.curation.dedup() to organize extractions. Verify via result.applied[].filePath — do NOT call readFile for verification. +Do NOT print raw context. Do NOT call tools.curation.recon when recon has already been computed. Proceed directly to extraction. Verify via result.applied[].filePath and do NOT call readFile for verification. ## Facts -- **rlm_curation_workflow**: The curation workflow uses the RLM approach with precomputed recon, single-pass extraction for small contexts, and mapExtract for chunked contexts. [convention] -- **single_pass_recon_rule**: For single-pass contexts, recon is already computed and should not be called again before curating. [convention] -- **mapextract_timeout**: When mapExtract is used, code_exec must set timeout: 300000 on the tool call itself. [convention] -- **verification_rule**: Verification after curation must use result.applied[].filePath and must not use readFile for verification. [convention] -- **no_raw_context_printing**: Context variables should not be printed raw during curation. [convention] -- **current_task_id**: The current curation task references task ID __taskId_ed34ca7d_1074_42ed_966f_0e77b7807351. [project] +- **context_tree**: The project uses a centralized curated context tree under .brv/context-tree for durable knowledge. [project] +- **rlm_mode**: Curation workflows in this project prefer RLM single-pass processing when recon suggests single-pass. [convention] +- **curate_operation_preference**: Curation operations should use UPSERT by default rather than ADD or UPDATE. [convention] +- **verification_method**: The current curation guidance emphasizes verifying results via result.applied[].filePath instead of reading files back for verification. [convention] +- **current_task**: The active task is to curate the provided context via the RLM approach. [other] diff --git a/.brv/context-tree/facts/project/current_runtime_timestamp.md b/.brv/context-tree/facts/project/current_runtime_timestamp.md index ef91c50..58a1846 100644 --- a/.brv/context-tree/facts/project/current_runtime_timestamp.md +++ b/.brv/context-tree/facts/project/current_runtime_timestamp.md @@ -34,16 +34,25 @@ timestamp supplied -> normalize as ISO 8601 fact -> curate into facts/project ## Narrative ### Structure -A single timestamp fact stored as a project-level knowledge entry for session reference. +A timestamp fact stored as a project-level knowledge entry for session reference, with chronological context preserved rather than overwritten. ### Dependencies Depends on the injected curate context, history, metadata, and task ID variables supplied by the caller. +### Chronology +1. Historical runtime reference: `current_runtime_timestamp.md` captured 2026-05-26T10:04:55.754Z as the session runtime timestamp. +2. Later planning snapshot: `context_and_plan_execution_notes.md` records the 2026-05-26T09:22:02.646Z planning context for the LSP MCP server workstream. + +### Current State +The runtime timestamp remains a historical session fact. For current planning context, prefer `context_and_plan_execution_notes.md`; that newer planning note supersedes this runtime-only reference when deciding what project plan, execution guidance, or workstream context is current. + ### Highlights -Preserves the exact runtime timestamp for recall and temporal reasoning. +Preserves the exact runtime timestamp for recall and temporal reasoning while distinguishing historical timestamp context from current planning state. ## Facts - **current_runtime_timestamp**: Current date and time is 2026-05-26T10:04:55.754Z [project] +- **historical_context**: This file preserves a runtime timestamp as historical session context rather than current planning guidance. [project] +- **current_planning_context**: Use context_and_plan_execution_notes.md as the current planning-context reference for the LSP MCP workstream. [project] --- diff --git a/.brv/context-tree/facts/project/lsp_caplet_smoke_test_findings.md b/.brv/context-tree/facts/project/lsp_caplet_smoke_test_findings.md new file mode 100644 index 0000000..1228e7f --- /dev/null +++ b/.brv/context-tree/facts/project/lsp_caplet_smoke_test_findings.md @@ -0,0 +1,59 @@ +--- +title: LSP Caplet Smoke Test Findings +summary: Broad smoke test across 49 LSP caplet tools succeeded end-to-end; unsupported TypeScript capabilities failed structurally, and edit-tool success schemas were fixed in commit 66e22fa. +tags: [] +related: [facts/project/rlm_curate_single_pass_workflow.md] +keywords: [] +createdAt: '2026-05-26T22:53:58.838Z' +updatedAt: '2026-05-26T22:53:58.838Z' +--- +## Reason +Curate durable findings from a broad smoke test of LSP caplet tools, including success, expected failures, and fixes. + +## Raw Concept +**Task:** +Document the results and fixes from a comprehensive LSP caplet tool smoke test. + +**Changes:** +- Validated all 49 caplet tools through the CLI +- Confirmed unsupported TypeScript-language features fail structurally rather than crashing +- Fixed edit-tool success output schemas and added a regression test + +**Files:** +- tests/fixtures/fake-lsp-server.ts + +**Flow:** +smoke test -> observe structured failures/successes -> fix schema issue -> add regression test -> restart stale backend -> confirm success + +**Timestamp:** 2026-05-26T22:53:39.920Z + +**Author:** assistant + +## Narrative +### Structure +The smoke test covered server/control, raw request/notify, diagnostics/edit, and navigation/query tool groups. The outcome separated true transport/schema regressions from expected capability gaps in the TypeScript LSP. + +### Dependencies +Relies on the caplet CLI, TypeScript LSP capability behavior, and the edit-tool schema validation path that was corrected in commit 66e22fa. + +### Highlights +This run confirmed end-to-end tool plumbing and surfaced a stale-backend issue that masked the schema fix until the backend was restarted. + +### Rules +Safe edit tools were tested with apply: false. Stop tools were tested at the end. + +### Examples +Examples of working control tools include list_servers and server_status; examples of expected failures include declaration and document_colors. + +## Facts +- **caplet_tool_count**: Broad smoke test completed across all 49 LSP caplet tools. [project] +- **caplet_cli_result**: 49/49 tools returned through the caplet CLI without downstream transport/schema failure. [project] +- **unsupported_tools_behavior**: Tools unsupported by the TypeScript LSP returned structured per-server failures instead of crashing. [project] +- **safe_edit_test_mode**: Safe edit tools were tested with apply set to false. [project] +- **stop_tools_tested**: Stop tools were tested at the end of the smoke test. [project] +- **working_tool_examples**: The following tools worked as key examples: list_servers, server_status, search_servers, stop_server, stop_workspace, request, notify, diagnostics, rename, format_document, format_range, code_actions, hover, completion, definition, references, document_symbols, folding_ranges, semantic_tokens_*, and inlay_hints. [project] +- **expected_structured_failures**: Expected structured failures were observed for declaration, document_links, document_colors, call_hierarchy_*, type_hierarchy_*, monikers, inline_values, format_on_type, and some resolve tools with synthetic test items. [project] +- **format_document_schema_fix**: A schema issue was fixed for format_document success results through the caplet wrapper. [project] +- **edit_tool_schema_fix**: Edit-tool success output schemas were fixed and a regression test was added. [project] +- **commit_reference**: The fix was committed as 66e22fa with message: fix: validate edit tool success outputs. [project] +- **backend_restart_effect**: After restarting the stale caplet backend, format_document returned structured success correctly. [project] diff --git a/.brv/context-tree/facts/project/lsp_mcp_interface_cleanup.md b/.brv/context-tree/facts/project/lsp_mcp_interface_cleanup.md index 01d74ac..1a8e169 100644 --- a/.brv/context-tree/facts/project/lsp_mcp_interface_cleanup.md +++ b/.brv/context-tree/facts/project/lsp_mcp_interface_cleanup.md @@ -1,18 +1,18 @@ --- title: LSP MCP Interface Cleanup -summary: Documents the LSP MCP interface cleanup work, including CLI/MCP consolidation, command policy handling, tool registration behavior, and verification outcomes. +summary: LSP MCP interface cleanup work covering tool naming consistency, output schema handling, server discovery, and verification updates. tags: [] related: [] keywords: [] createdAt: '2026-05-26T14:39:23.671Z' -updatedAt: '2026-05-26T14:55:55.578Z' +updatedAt: '2026-05-26T22:44:47.552Z' --- ## Reason -Curate project guidance about cleaning up the LSP MCP interface and related implementation details +Curate RLM context about cleanup of the LSP MCP interface and related tooling ## Raw Concept **Task:** -Document the LSP MCP interface cleanup effort and its implementation notes. +Document the LSP MCP interface cleanup work captured in the curated context. **Changes:** - Standardize tool naming and method wiring @@ -20,24 +20,26 @@ Document the LSP MCP interface cleanup effort and its implementation notes. - Record cleanup and implementation notes in durable knowledge - Captured interface cleanup guidance - Recorded implementation details and verification outcomes +- Captured cleanup-related implementation and review notes +- Recorded interface, schema, discovery, and verification concerns **Flow:** -identify interface cleanup concerns -> document implementation details -> verify readiness +cleanup work -> review findings -> verification updates -> curated knowledge -**Timestamp:** 2026-05-26 +**Timestamp:** 2026-05-26T22:44:41.174Z **Author:** ByteRover context engineer ## Narrative ### Structure -This knowledge entry captures the interface cleanup task and the related implementation/verification notes. +The knowledge captures a compact project-facing record of interface cleanup efforts for the LSP MCP codebase. ### Dependencies -Relates to LSP server implementation, MCP server behavior, tool registration, and command policy handling. +This context is tied to review, tooling, schema, and verification materials in the project tree. ### Highlights -Preserves the cleanup scope and the associated project notes for future recall. +The preserved knowledge is intended to support future recall of interface cleanup decisions and associated implementation notes. ## Facts -- **lsp_mcp_interface_cleanup**: The project has a cleanup task focused on the LSP MCP interface. [project] -- **lsp_mcp_interface_boundaries**: The cleanup work involves consolidating or clarifying LSP and MCP interface boundaries. [project] +- **lsp_mcp_interface_cleanup**: The context documents cleanup work for the LSP MCP interface. [project] +- **lsp_mcp_interface_cleanup_scope**: The project includes tool naming, output schema handling, server discovery, and verification updates. [project] diff --git a/.brv/context-tree/facts/project/pr_branch_commit_and_verification.md b/.brv/context-tree/facts/project/pr_branch_commit_and_verification.md new file mode 100644 index 0000000..60d1ce4 --- /dev/null +++ b/.brv/context-tree/facts/project/pr_branch_commit_and_verification.md @@ -0,0 +1,46 @@ +--- +title: PR Branch Commit and Verification +summary: Committed and pushed branch opencode/nimble-wizard with commit 682877c; hooks passed format, lint, typecheck, tests, and build. +tags: [] +related: [] +keywords: [] +createdAt: '2026-05-26T18:56:03.037Z' +updatedAt: '2026-05-26T18:56:03.037Z' +--- +## Reason +Preserve the durable outcome of the commit, push, and passing verification checks from the conversation. + +## Raw Concept +**Task:** +Document the durable outcome of a commit and push workflow along with verification results. + +**Changes:** +- Committed the PR branch changes +- Pushed to the remote origin branch +- Confirmed all push hooks passed successfully + +**Flow:** +commit -> push -> run hooks -> confirm format, lint, typecheck, tests, and build success + +**Timestamp:** 2026-05-26T18:55:48.941Z + +## Narrative +### Structure +This note captures the final state after the assistant reported a successful commit and push sequence. + +### Dependencies +Relies on the repository hooks and test/build pipeline for verification. + +### Highlights +The push completed successfully and the verification suite passed, including 27 files and 327 tests. + +### Rules +The context-tree files were intentionally excluded from the commit. + +## Facts +- **branch_name**: The branch is opencode/nimble-wizard [project] +- **commit**: The commit was 682877c fix: normalize stop server alias responses [project] +- **push_target**: The push target was origin/opencode/nimble-wizard [project] +- **verification_checks**: Push hooks passed format check, lint, typecheck, tests, and build [project] +- **test_results**: The test run reported 27 files passed and 327 tests passed [project] +- **context_tree_status**: .brv/context-tree files remained uncommitted and untracked and were intentionally excluded from the commit [project] diff --git a/.brv/context-tree/facts/project/rlm_curate_execution_requirements.md b/.brv/context-tree/facts/project/rlm_curate_execution_requirements.md new file mode 100644 index 0000000..fc4a8dc --- /dev/null +++ b/.brv/context-tree/facts/project/rlm_curate_execution_requirements.md @@ -0,0 +1,47 @@ +--- +title: RLM Curate Execution Requirements +summary: RLM curation requires recon first, single-pass handling for small contexts, quiet previewing, extraction of facts, and verification of curate results before reporting completion. +tags: [] +related: [] +keywords: [] +createdAt: '2026-05-26T22:44:04.240Z' +updatedAt: '2026-05-26T22:44:04.240Z' +--- +## Reason +Capture concise RLM curation requirements from the provided context + +## Raw Concept +**Task:** +Document the execution requirements and workflow constraints for RLM curation in this session + +**Changes:** +- Confirmed precomputed recon guidance for a single-pass context +- Captured the required extraction and verification workflow +- Preserved instructions about not printing raw context and not rereading files for verification + +**Flow:** +recon -> direct extraction -> curate -> verify applied file paths -> report status + +**Timestamp:** 2026-05-26T22:43:57.998Z + +**Patterns:** +- `^__taskId_` - Task ID variable naming convention + +## Narrative +### Structure +This knowledge captures the operational constraints for RLM-based curation when the context is small and recon has already been computed. + +### Dependencies +Depends on precomputed recon metadata, the injected context/history/metadata variables, and curate result verification. + +### Highlights +The workflow emphasizes single-pass processing, no raw context printing, no recon rerun, and post-curation verification via applied file paths. + +### Rules +Do not print raw context. Do not call tools.curation.recon when recon is already precomputed. For verification, use result.applied[].filePath and do not call readFile. + +## Facts +- **rlm_approach**: The context was processed with the RLM curation approach. [other] +- **curation_mode**: The provided context was small enough for single-pass handling. [convention] +- **recon_step**: Recon was already computed before extraction. [convention] +- **verification_method**: Verification should use result.applied[].filePath and should not rely on readFile. [convention] diff --git a/.brv/context-tree/facts/project/rlm_curation_execution_context.md b/.brv/context-tree/facts/project/rlm_curation_execution_context.md new file mode 100644 index 0000000..5d5cca2 --- /dev/null +++ b/.brv/context-tree/facts/project/rlm_curation_execution_context.md @@ -0,0 +1,45 @@ +--- +title: RLM Curation Execution Context +summary: RLM curation execution context describing single-pass workflow, task variables, and verification expectations for curated inputs. +tags: [] +related: [] +keywords: [] +createdAt: '2026-05-26T22:41:10.094Z' +updatedAt: '2026-05-26T22:41:10.094Z' +--- +## Reason +Persist execution guidance and curation context from the provided RLM input + +## Raw Concept +**Task:** +Document RLM curation execution instructions for this session + +**Changes:** +- Captured single-pass RLM curation requirements +- Recorded task, history, metadata, and task ID variable handling +- Preserved verification expectations for curate results + +**Flow:** +receive curated context -> extract facts -> upsert knowledge -> verify applied file paths + +**Timestamp:** 2026-05-26T22:41:03.781Z + +**Author:** ByteRover context engineer + +## Narrative +### Structure +This context defines how to process a compact curation input using the RLM approach with precomputed reconnaissance and single-pass execution. + +### Dependencies +Depends on the provided context, history, metadata, and task ID variables, plus tools.curate for persistence. + +### Highlights +Suggested mode is single-pass with one chunk; verification should use result.applied[].filePath rather than readFile. + +## Facts +- **context_line_1**: The following is a conversation between a user and an AI assistant. [project] +- **context_line_2**: Curate only information with lasting value: facts, decisions, technical details, preferences, or notable outcomes. [project] +- **context_line_3**: Skip trivial messages such as greetings, acknowledgments ("ok", "thanks", "sure", "got it"), one-word replies, anything with no substantive content. [project] +- **context_line_4**: Conversation: [project] +- **context_line_5**: --- [project] +- **context_line_6**: [user]: try using the lsp caplet now that we have these fixes [project] diff --git a/.changeset/fuzzy-vans-glow.md b/.changeset/fuzzy-vans-glow.md new file mode 100644 index 0000000..999cd90 --- /dev/null +++ b/.changeset/fuzzy-vans-glow.md @@ -0,0 +1,5 @@ +--- +"language-server-mcp": patch +--- + +Fix list and search server tools diff --git a/src/mcp/server.ts b/src/mcp/server.ts index f68a52d..9a67fd8 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,4 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { z } from "zod"; import { createToolRegistry, @@ -32,13 +33,14 @@ export function createMcpServer( } function registerTool(server: McpServer, tool: RegisteredTool): void { + const outputSchema = schemaHasObjectRoot(tool.outputSchema) ? tool.outputSchema : undefined; server.registerTool( tool.name, { title: tool.title, description: tool.description, ...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}), - ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), + ...(outputSchema ? { outputSchema } : {}), }, async (input, extra) => { const result = await tool.handler(input, { signal: extra.signal }); @@ -54,3 +56,21 @@ function registerTool(server: McpServer, tool: RegisteredTool): void { function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } + +function schemaHasObjectRoot(schema: z.ZodType | undefined): boolean { + if (!schema) { + return false; + } + const maybeSchema = schema as z.ZodType & { + _zod?: { def?: { type?: string; shape?: unknown } }; + _def?: { typeName?: string; type?: string; shape?: unknown }; + shape?: unknown; + }; + return ( + maybeSchema._zod?.def?.type === "object" || + maybeSchema._zod?.def?.shape !== undefined || + maybeSchema._def?.typeName === "ZodObject" || + maybeSchema._def?.type === "object" || + maybeSchema.shape !== undefined + ); +} diff --git a/src/tools/diagnosticTools.ts b/src/tools/diagnosticTools.ts index dd0668e..b6639e9 100644 --- a/src/tools/diagnosticTools.ts +++ b/src/tools/diagnosticTools.ts @@ -191,35 +191,29 @@ async function collectFileDiagnostics( serverExtensions: acquired.extensions, }); if (acquired.session.capabilities?.diagnosticProvider) { - const response = await acquired.session.sendRequest( - "textDocument/diagnostic", - { - textDocument: { uri }, - }, - { signal: context?.signal }, - ); - return { - ok: true, - mode: "pull", - uri, - filePath, - diagnostics: normalizeDiagnostics(extractPullDiagnostics(response), uri), - }; - } - - const cached = options.diagnosticStore.get(acquired.serverId, uri); - if (cached) { - return pushResult("push-cache", cached.uri, cached.diagnostics); + try { + const response = await acquired.session.sendRequest( + "textDocument/diagnostic", + { + textDocument: { uri }, + }, + { signal: context?.signal }, + ); + return { + ok: true, + mode: "pull", + uri, + filePath, + diagnostics: normalizeDiagnostics(extractPullDiagnostics(response), uri), + }; + } catch (error) { + if (!isMethodUnhandledError(error)) { + throw error; + } + } } - const waited = await options.diagnosticStore.waitFor( - acquired.serverId, - uri, - options.diagnosticsWaitMs ?? - options.config?.sessions?.diagnosticsWaitMs ?? - DEFAULT_DIAGNOSTICS_WAIT_MS, - ); - return pushResult("push-wait", uri, waited?.diagnostics ?? []); + return collectPushDiagnostics(options, acquired.serverId, uri); } async function collectWorkspaceDiagnostics( @@ -243,6 +237,26 @@ async function collectWorkspaceDiagnostics( }; } +async function collectPushDiagnostics( + options: DiagnosticsToolHandlerOptions, + serverId: string, + uri: string, +): Promise { + const cached = options.diagnosticStore.get(serverId, uri); + if (cached) { + return pushResult("push-cache", cached.uri, cached.diagnostics); + } + + const waited = await options.diagnosticStore.waitFor( + serverId, + uri, + options.diagnosticsWaitMs ?? + options.config?.sessions?.diagnosticsWaitMs ?? + DEFAULT_DIAGNOSTICS_WAIT_MS, + ); + return pushResult("push-wait", uri, waited?.diagnostics ?? []); +} + function pushResult( mode: "push-cache" | "push-wait", uri: string, @@ -264,6 +278,15 @@ function extractPullDiagnostics(response: unknown): unknown[] { return []; } +function isMethodUnhandledError(error: unknown): boolean { + if (!isRecord(error)) { + return false; + } + return ( + error.code === -32601 || error.code === "MethodNotFound" || error.code === "METHOD_NOT_FOUND" + ); +} + function normalizeWorkspaceDiagnostics(response: unknown): unknown[] { if (!isRecord(response) || !Array.isArray(response.items)) { return []; diff --git a/src/tools/outputSchemas.ts b/src/tools/outputSchemas.ts index 8c82361..c3bd9fd 100644 --- a/src/tools/outputSchemas.ts +++ b/src/tools/outputSchemas.ts @@ -235,7 +235,12 @@ export const diagnosticsOutputSchema = z.object({ error: z.string().optional(), }); -const editSuccessBaseSchema = z.object({ ok: z.literal(true), applied: z.boolean() }); +const editSuccessBaseSchema = z.object({ + ok: z.literal(true), + applied: z.boolean(), + message: z.string().optional(), + changedFiles: z.array(changedFileSchema).optional(), +}); export const editToolOutputSchemas = { lsp_rename: editOutputSchema(z.object({ edit: workspaceEditSchema.optional() })), @@ -336,26 +341,17 @@ export const stopWorkspaceOutputSchema = z.object({ ), }); -function editOutputSchema(extraSuccessFields: z.ZodType): z.ZodType { +function editOutputSchema(extraSuccessFields: z.ZodObject): z.ZodType { return z.object({ ok: z.boolean(), results: z.record( z.string(), z.union([ - editSuccessBaseSchema - .and( - z.object({ - message: z.string().optional(), - changedFiles: z.array(changedFileSchema).optional(), - }), - ) - .and(extraSuccessFields), - structuredErrorSchema.and( - z.object({ - applied: z.boolean().optional(), - changedFiles: z.array(changedFileSchema).optional(), - }), - ), + editSuccessBaseSchema.extend(extraSuccessFields.shape), + structuredErrorSchema.extend({ + applied: z.boolean().optional(), + changedFiles: z.array(changedFileSchema).optional(), + }), ]), ), error: z.string().optional(), diff --git a/src/tools/standardTools.ts b/src/tools/standardTools.ts index 83199a7..ad5d9ff 100644 --- a/src/tools/standardTools.ts +++ b/src/tools/standardTools.ts @@ -101,11 +101,14 @@ export function createStandardToolHandler(options: StandardToolHandlerOptions) { const rawResult = await acquired.session.sendRequest(entry.lspMethod, requestParams, { signal: context?.signal, }); + const normalizedResult = normalizeLspResult(rawResult, { + workspaceRoot: acquired.workspaceRoot, + }); return { serverId: acquired.serverId, result: { ok: true, - result: normalizeLspResult(rawResult, { workspaceRoot: acquired.workspaceRoot }), + result: postProcessResult(entry, parsed, normalizedResult), }, } satisfies PerServerResult; } catch (error) { @@ -262,6 +265,77 @@ function buildRequestParams(entry: MethodRegistryEntry, input: Record, + result: unknown, +): unknown { + if (entry.toolName !== "completion" || input.includeAll === true) { + return result; + } + return limitCompletionResult(result, optionalString(input.query), completionLimit(input.limit)); +} + +function completionLimit(value: unknown): number { + return typeof value === "number" ? value : 100; +} + +function limitCompletionResult(result: unknown, query: string | undefined, limit: number): unknown { + if (Array.isArray(result)) { + const filteredItems = filterCompletionItems(result, query); + return { + isIncomplete: false, + items: filteredItems.slice(0, limit), + lspMcpMeta: completionMeta(result.length, filteredItems.length, limit), + }; + } + if (!isRecord(result) || !Array.isArray(result.items)) { + return result; + } + const filteredItems = filterCompletionItems(result.items, query); + return { + ...result, + items: filteredItems.slice(0, limit), + lspMcpMeta: completionMeta(result.items.length, filteredItems.length, limit), + }; +} + +function completionMeta(totalItems: number, matchedItems: number, limit: number) { + return { + totalItems, + matchedItems, + returnedItems: Math.min(matchedItems, limit), + truncated: matchedItems > limit, + }; +} + +function filterCompletionItems(items: unknown[], query: string | undefined): unknown[] { + if (!query) { + return items; + } + const normalizedQuery = query.toLowerCase(); + return items.filter((item) => completionSearchText(item).includes(normalizedQuery)); +} + +function completionSearchText(item: unknown): string { + if (!isRecord(item)) { + return ""; + } + return [item.label, item.detail, item.documentation, labelDetailsText(item.labelDetails)] + .filter((value): value is string => typeof value === "string") + .join("\n") + .toLowerCase(); +} + +function labelDetailsText(value: unknown): string | undefined { + if (!isRecord(value)) { + return undefined; + } + return [value.detail, value.description] + .filter((item): item is string => typeof item === "string") + .join(" "); +} + function range(input: Record) { return { start: mcpPositionToLspPosition(position(input.startLine, input.startCharacter)), @@ -303,3 +377,7 @@ function position(line: unknown, character: unknown): McpPosition { function optionalString(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/src/tools/toolSchemas.ts b/src/tools/toolSchemas.ts index deecbe6..aa5b1f9 100644 --- a/src/tools/toolSchemas.ts +++ b/src/tools/toolSchemas.ts @@ -8,6 +8,9 @@ export const positionInputSchema = z.object({ strict: z.boolean().optional(), line: z.number().int().positive(), character: z.number().int().positive(), + query: z.string().optional(), + limit: z.number().int().positive().optional(), + includeAll: z.boolean().optional(), }); export const fileInputSchema = z.object({ diff --git a/tests/integration/diagnostics.test.ts b/tests/integration/diagnostics.test.ts index 0176acf..c63ffce 100644 --- a/tests/integration/diagnostics.test.ts +++ b/tests/integration/diagnostics.test.ts @@ -233,6 +233,57 @@ describe("diagnostics", () => { expect(session.requestOptions[0]).toEqual({ signal }); }); + it("falls back to push diagnostics when advertised pull diagnostics are unhandled", async () => { + const { workspaceRoot, filePath } = await createWorkspaceFile(); + const session = createSession({ + capabilities: { diagnosticProvider: true }, + requestErrors: { + "textDocument/diagnostic": Object.assign( + new Error("Unhandled method textDocument/diagnostic"), + { code: -32601 }, + ), + }, + }); + const handler = createHandler([acquired("ts", session, workspaceRoot)], 5); + + const result = await handler({ workspaceRoot, filePath }); + + expect(session.requests).toEqual([ + { + method: "textDocument/diagnostic", + params: { textDocument: { uri: filePathToUri(filePath) } }, + }, + ]); + expect(result.results.ts).toMatchObject({ + ok: true, + mode: "push-wait", + uri: filePathToUri(filePath), + filePath, + diagnostics: [], + }); + }); + + it("does not fallback when pull diagnostics fail with an internal unhandled error", async () => { + const { workspaceRoot, filePath } = await createWorkspaceFile(); + const session = createSession({ + capabilities: { diagnosticProvider: true }, + requestErrors: { + "textDocument/diagnostic": Object.assign(new Error("Unhandled exception in server"), { + code: -32603, + }), + }, + }); + const handler = createHandler([acquired("ts", session, workspaceRoot)], 5); + + const result = await handler({ workspaceRoot, filePath }); + + expect(result.results.ts).toMatchObject({ + ok: false, + error: "Unhandled exception in server", + code: -32603, + }); + }); + it("preserves structured timeout errors", async () => { const { workspaceRoot, filePath } = await createWorkspaceFile(); const session = createSession({ diff --git a/tests/integration/standardTools.test.ts b/tests/integration/standardTools.test.ts index 4fc70c7..e99830b 100644 --- a/tests/integration/standardTools.test.ts +++ b/tests/integration/standardTools.test.ts @@ -961,6 +961,86 @@ describe("standard tool forwarding", () => { }); }); + it("limits completion results by default and supports filtering", async () => { + const { workspaceRoot, filePath } = await createWorkspaceFile(); + const items = Array.from({ length: 120 }, (_, index) => ({ + label: index === 119 ? "goalPlugin" : `global${index}`, + detail: index === 119 ? "exported plugin" : "global symbol", + })); + const session = createSession( + { "textDocument/completion": { isIncomplete: false, items } }, + { completionProvider: true }, + ); + const handler = createStandardToolHandler({ + sessionManager: { + getSessionsForFile: vi.fn(async () => [acquired("ts", session, workspaceRoot)]), + getSessionsForWorkspace: vi.fn(async () => []), + }, + documentStore: new DocumentStore(), + }); + + const defaultResult = await handler("completion", { + workspaceRoot, + filePath, + line: 1, + character: 1, + }); + const filteredResult = await handler("completion", { + workspaceRoot, + filePath, + line: 1, + character: 1, + query: "goal", + }); + + expect(defaultResult.results.ts).toMatchObject({ + ok: true, + result: { + isIncomplete: false, + items: expect.any(Array), + lspMcpMeta: { totalItems: 120, returnedItems: 100, truncated: true }, + }, + }); + expect( + (defaultResult.results.ts as { ok: true; result: { items: unknown[] } }).result.items, + ).toHaveLength(100); + expect(filteredResult.results.ts).toMatchObject({ + ok: true, + result: { + items: [{ label: "goalPlugin", detail: "exported plugin" }], + lspMcpMeta: { totalItems: 120, matchedItems: 1, returnedItems: 1, truncated: false }, + }, + }); + }); + + it("returns serializable completion metadata for completion item arrays", async () => { + const { workspaceRoot, filePath } = await createWorkspaceFile(); + const items = Array.from({ length: 101 }, (_, index) => ({ label: `global${index}` })); + const session = createSession( + { "textDocument/completion": items }, + { completionProvider: true }, + ); + const handler = createStandardToolHandler({ + sessionManager: { + getSessionsForFile: vi.fn(async () => [acquired("ts", session, workspaceRoot)]), + getSessionsForWorkspace: vi.fn(async () => []), + }, + documentStore: new DocumentStore(), + }); + + const result = await handler("completion", { workspaceRoot, filePath, line: 1, character: 1 }); + const serialized = JSON.parse(JSON.stringify(result)) as typeof result; + + expect(serialized.results.ts).toMatchObject({ + ok: true, + result: { + isIncomplete: false, + items: expect.any(Array), + lspMcpMeta: { totalItems: 101, returnedItems: 100, truncated: true }, + }, + }); + }); + it("requires serverId for server-specific resolve and hierarchy item methods", async () => { const { workspaceRoot } = await createWorkspaceFile(); const session = createSession({}, { completionProvider: { resolveProvider: true } }); diff --git a/tests/mcp/server.test.ts b/tests/mcp/server.test.ts index 04990c5..821fc0e 100644 --- a/tests/mcp/server.test.ts +++ b/tests/mcp/server.test.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { describe, expect, it, vi } from "vitest"; import { createMcpServer, listLspServers } from "../../src/mcp/server.js"; +import { editToolOutputSchemas } from "../../src/tools/outputSchemas.js"; import type { ToolHandlerContext, ToolRegistry } from "../../src/tools/registerTools.js"; describe("list_servers", () => { @@ -42,6 +43,97 @@ describe("createMcpServer", () => { await handle.close(); }); + it("handles union output schemas through the MCP SDK path", async () => { + const payload = { ok: true, value: 42 }; + const handle = createMcpServer( + testRegistry([ + { + name: "union_output", + outputSchema: z.union([ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), error: z.string() }), + ]), + handler: () => payload, + }, + ]), + ); + const client = await connectClient(handle); + + const tools = await client.listTools(); + const unionTool = tools.tools.find((tool) => tool.name === "union_output"); + const result = await client.callTool({ name: "union_output", arguments: {} }); + + expect(unionTool?.outputSchema).toBeUndefined(); + expect(result.structuredContent).toEqual(payload); + expect(result.content).toEqual([{ type: "text", text: JSON.stringify(payload) }]); + await client.close(); + await handle.close(); + }); + + it("returns edit-tool failures through MCP structured output validation", async () => { + const payload = { + ok: false, + results: { + ts: { + ok: false, + error: "format failed", + code: -32603, + method: "textDocument/formatting", + }, + }, + }; + const handle = createMcpServer( + testRegistry([ + { + name: "format_document", + outputSchema: editToolOutputSchemas.lsp_format_document, + handler: () => payload, + }, + ]), + ); + const client = await connectClient(handle); + + await client.listTools(); + const result = await client.callTool({ name: "format_document", arguments: {} }); + + expect(result.structuredContent).toEqual(payload); + expect(result.content).toEqual([{ type: "text", text: JSON.stringify(payload) }]); + await client.close(); + await handle.close(); + }); + + it("returns edit-tool successes through MCP structured output validation", async () => { + const payload = { + ok: true, + results: { + ts: { + ok: true, + applied: false, + message: "Edits were returned but not applied.", + edits: [], + }, + }, + }; + const handle = createMcpServer( + testRegistry([ + { + name: "format_document", + outputSchema: editToolOutputSchemas.lsp_format_document, + handler: () => payload, + }, + ]), + ); + const client = await connectClient(handle); + + await client.listTools(); + const result = await client.callTool({ name: "format_document", arguments: {} }); + + expect(result.structuredContent).toEqual(payload); + expect(result.content).toEqual([{ type: "text", text: JSON.stringify(payload) }]); + await client.close(); + await handle.close(); + }); + it("wraps object handler results as structured content and JSON text content", async () => { const payload = { ok: true, value: 42 }; const handle = createMcpServer(