vibe-coding-master 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/backend/cli/install-vcm-harness.js +3 -2
- package/dist/backend/role-tool-policy.js +10 -1
- package/dist/backend/services/harness-service.js +3 -2
- package/dist/backend/services/lsp-plugin.js +19 -4
- package/dist/backend/services/managed-artifact-validation.js +1 -0
- package/dist/backend/services/session-service.js +2 -2
- package/dist/backend/templates/harness/architect-agent.js +1 -1
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +6 -3
- package/dist/backend/templates/harness/gate-review.js +28 -1
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +269 -0
- package/scripts/verify-package.mjs +1 -0
package/README.md
CHANGED
|
@@ -510,15 +510,20 @@ Use it to:
|
|
|
510
510
|
- merge task harness commits back to the connected repository branch when
|
|
511
511
|
appropriate
|
|
512
512
|
|
|
513
|
-
VCM bundles the Claude Code LSP bridge and loads it
|
|
514
|
-
including Codex Bridge launches
|
|
513
|
+
VCM bundles the Claude Code LSP bridge and loads it for Architect and Reviewer
|
|
514
|
+
sessions, including Codex Bridge launches; Architect's evidence worker can use it
|
|
515
|
+
too. While a language server is still indexing, VCM holds LSP queries until it
|
|
516
|
+
is ready and fails them explicitly if indexing does not finish, instead of
|
|
517
|
+
returning partial results. The project environment must still
|
|
515
518
|
provide the language server for each detected language: `rust-analyzer`,
|
|
516
519
|
`typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
|
|
517
520
|
`jdtls`. Architect preloads `vcm-code-navigation` and uses LSP for semantic
|
|
518
521
|
relationships. Accessors, backing fields, trait items, implementations, wrappers,
|
|
519
522
|
and aliases are queried as separate symbols; a bounded source-text fallback may
|
|
520
523
|
only locate candidates after the correct LSP query is demonstrably partial.
|
|
521
|
-
|
|
524
|
+
At the architecture-plan gate, Reviewer checks that every decision-bearing
|
|
525
|
+
relationship cites its evidence source and re-queries it with LSP. Other roles
|
|
526
|
+
use generated context and ordinary source reads. Harness Studio
|
|
522
527
|
reports whether the server executable and plugin can run; the role Session
|
|
523
528
|
performs the real workspace indexing and semantic query retries.
|
|
524
529
|
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
9
|
-
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
9
|
+
import { CODE_ROLE_DISALLOWED_TOOLS, EVIDENCE_WORKER_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
10
10
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
11
11
|
import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
|
|
12
12
|
import { renderArchitectEvidenceWorkerHarnessRules } from "../templates/harness/architect-evidence-worker-agent.js";
|
|
@@ -108,7 +108,7 @@ const AGENT_FRONTMATTER = {
|
|
|
108
108
|
},
|
|
109
109
|
"vcm-architect-evidence-worker": {
|
|
110
110
|
description: "Foreground Architect worker for bounded project evidence collection.",
|
|
111
|
-
|
|
111
|
+
disallowedTools: EVIDENCE_WORKER_DISALLOWED_TOOLS.join(", ")
|
|
112
112
|
},
|
|
113
113
|
"vcm-architect-scaffold-worker": {
|
|
114
114
|
description: "Foreground Architect worker for exact scaffold execution and scaffold validation."
|
|
@@ -119,6 +119,7 @@ const AGENT_FRONTMATTER = {
|
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
const REQUIRED_AGENT_DISALLOWED_TOOLS = {
|
|
122
|
+
"vcm-architect-evidence-worker": EVIDENCE_WORKER_DISALLOWED_TOOLS,
|
|
122
123
|
architect: CODE_ROLE_DISALLOWED_TOOLS,
|
|
123
124
|
coder: CODE_ROLE_DISALLOWED_TOOLS,
|
|
124
125
|
reviewer: REVIEWER_DISALLOWED_TOOLS
|
|
@@ -38,13 +38,22 @@ export const REVIEWER_DISALLOWED_TOOLS = [
|
|
|
38
38
|
"Edit",
|
|
39
39
|
...LSP_FRONTMATTER_DISALLOWED_TOOLS
|
|
40
40
|
];
|
|
41
|
+
// The evidence worker reads and queries LSP but never runs commands, edits code,
|
|
42
|
+
// or delegates. A deny list keeps the parent session's LSP tool available.
|
|
43
|
+
export const EVIDENCE_WORKER_DISALLOWED_TOOLS = [
|
|
44
|
+
"Agent",
|
|
45
|
+
"Bash",
|
|
46
|
+
"Edit",
|
|
47
|
+
...LSP_FRONTMATTER_DISALLOWED_TOOLS
|
|
48
|
+
];
|
|
41
49
|
export const CODE_ROLE_RUNTIME_DISALLOWED_TOOLS = [...NON_VCM_ROLE_RUNTIME_TOOLS];
|
|
42
50
|
export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
|
|
43
51
|
"Agent",
|
|
44
52
|
"Edit",
|
|
45
53
|
...NON_VCM_ROLE_RUNTIME_TOOLS
|
|
46
54
|
];
|
|
47
|
-
|
|
55
|
+
// Architect makes semantic code claims; Reviewer verifies them at Gate Review.
|
|
56
|
+
const LSP_ROLE_NAMES = new Set(["architect", "reviewer"]);
|
|
48
57
|
export function roleUsesLsp(role) {
|
|
49
58
|
return LSP_ROLE_NAMES.has(role);
|
|
50
59
|
}
|
|
@@ -4,7 +4,7 @@ import { promisify } from "node:util";
|
|
|
4
4
|
import { DEFAULT_ARCHITECT_WORKER_SETTINGS, normalizeArchitectWorkerSettings } from "../../shared/types/app-settings.js";
|
|
5
5
|
import { applyArchitectWorkerSetting } from "../templates/harness/architect-worker-frontmatter.js";
|
|
6
6
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
7
|
-
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
7
|
+
import { CODE_ROLE_DISALLOWED_TOOLS, EVIDENCE_WORKER_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
8
8
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
9
9
|
import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
|
|
10
10
|
import { renderArchitectEvidenceWorkerHarnessRules } from "../templates/harness/architect-evidence-worker-agent.js";
|
|
@@ -258,7 +258,8 @@ const HARNESS_FILES = [
|
|
|
258
258
|
kind: "agent-architect-evidence-worker",
|
|
259
259
|
path: ".claude/agents/vcm-architect-evidence-worker.md",
|
|
260
260
|
title: "VCM Architect Evidence Worker Agent",
|
|
261
|
-
frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", {
|
|
261
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { disallowedTools: EVIDENCE_WORKER_DISALLOWED_TOOLS.join(", ") }),
|
|
262
|
+
requiredDisallowedTools: EVIDENCE_WORKER_DISALLOWED_TOOLS,
|
|
262
263
|
architectWorkerType: "evidence",
|
|
263
264
|
renderRules: renderArchitectEvidenceWorkerHarnessRules
|
|
264
265
|
},
|
|
@@ -4,19 +4,31 @@ export { roleUsesLsp } from "../role-tool-policy.js";
|
|
|
4
4
|
export const VCM_LSP_PLUGIN_NAME = "vcm-lsp-bridge";
|
|
5
5
|
export const VCM_LSP_PLUGIN_DIR = fileURLToPath(new URL("../../../scripts/claude-plugins/vcm-lsp-bridge", import.meta.url));
|
|
6
6
|
export const VCM_LSP_PLUGIN_MANIFEST = path.join(VCM_LSP_PLUGIN_DIR, ".claude-plugin", "plugin.json");
|
|
7
|
+
export const VCM_LSP_PROXY_PATH = path.join(VCM_LSP_PLUGIN_DIR, "vcm-lsp-proxy.mjs");
|
|
7
8
|
export async function prepareTaskLspPlugin(fs, taskRepoRoot, status) {
|
|
8
9
|
const runnable = status.languages.filter((language) => language.state === "server_runnable");
|
|
9
10
|
if (runnable.length === 0)
|
|
10
11
|
return [];
|
|
11
12
|
const manifest = await fs.readJson(VCM_LSP_PLUGIN_MANIFEST);
|
|
12
13
|
const commands = new Set(runnable.map((language) => language.serverCommand));
|
|
13
|
-
const lspServers = Object.fromEntries(Object.entries(manifest.lspServers)
|
|
14
|
+
const lspServers = Object.fromEntries(Object.entries(manifest.lspServers)
|
|
15
|
+
.filter(([, server]) => commands.has(server.command))
|
|
16
|
+
.map(([name, server]) => [name, withReadinessProxy(server)]));
|
|
14
17
|
const pluginDir = path.join(taskRepoRoot, ".ai/vcm/lsp-plugin");
|
|
15
18
|
const pluginManifestPath = path.join(pluginDir, ".claude-plugin/plugin.json");
|
|
16
19
|
await fs.ensureDir(path.dirname(pluginManifestPath));
|
|
17
20
|
await fs.writeJsonAtomic(pluginManifestPath, { ...manifest, lspServers });
|
|
18
21
|
return [pluginDir];
|
|
19
22
|
}
|
|
23
|
+
// Runs the server behind vcm-lsp-proxy so navigation queries wait for indexing
|
|
24
|
+
// to finish, or fail explicitly, instead of returning partial or empty results.
|
|
25
|
+
function withReadinessProxy(server) {
|
|
26
|
+
return {
|
|
27
|
+
...server,
|
|
28
|
+
command: process.execPath,
|
|
29
|
+
args: [VCM_LSP_PROXY_PATH, "--", server.command, ...(server.args ?? [])]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
20
32
|
export function lspUnavailableWarning(language) {
|
|
21
33
|
const installCommands = {
|
|
22
34
|
"rust-analyzer": "rustup component add rust-analyzer",
|
|
@@ -27,16 +39,19 @@ export function lspUnavailableWarning(language) {
|
|
|
27
39
|
const recovery = language.state === "plugin_missing"
|
|
28
40
|
? "Refresh the VCM Harness to restore the LSP plugin."
|
|
29
41
|
: `Install or repair ${language.serverCommand} in the environment running the VCM backend${installCommands[language.serverCommand] ? ` (${installCommands[language.serverCommand]})` : ""} and ensure it is executable on PATH.`;
|
|
30
|
-
return `
|
|
42
|
+
return `LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`} ${recovery} Restart the LSP roles (Architect, Reviewer) after fixing the environment so VCM can detect and register the server again.`;
|
|
31
43
|
}
|
|
32
|
-
export function
|
|
44
|
+
export function renderLspNotice(status) {
|
|
33
45
|
if (status.languages.length === 0)
|
|
34
46
|
return undefined;
|
|
35
47
|
const available = status.languages.filter((language) => language.state === "server_runnable");
|
|
36
48
|
const unavailable = status.languages.filter((language) => language.state !== "server_runnable");
|
|
37
49
|
return [
|
|
38
|
-
"[VCM
|
|
50
|
+
"[VCM LSP Availability]",
|
|
39
51
|
`Registered language servers: ${available.map((language) => `${language.label} (${language.serverCommand})`).join(", ") || "none"}.`,
|
|
52
|
+
...(available.length > 0 ? [
|
|
53
|
+
"While a server is still indexing, VCM holds LSP queries until it is ready. A query that fails with \"Language server is still indexing\" did not return a result: retry it later or record the relationship as unresolved; never read the failure as an empty result."
|
|
54
|
+
] : []),
|
|
40
55
|
...unavailable.map(lspUnavailableWarning),
|
|
41
56
|
...(unavailable.length > 0 ? [
|
|
42
57
|
"If your assignment requires a semantic query in an unavailable language, stop that work and report the environment prerequisite through the current communication channel. Do not substitute source text search or claim the relationship is verified. Continue only after the server is available."
|
|
@@ -7,7 +7,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
8
8
|
import { readHarnessRevisionState } from "./harness-revision.js";
|
|
9
9
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
10
|
-
import { roleUsesLsp, prepareTaskLspPlugin,
|
|
10
|
+
import { roleUsesLsp, prepareTaskLspPlugin, renderLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
|
|
11
11
|
const TRANSLATOR_ROLE = "translator";
|
|
12
12
|
const HARNESS_ENGINEER_ROLE = "harness-engineer";
|
|
13
13
|
const TRANSLATION_DIR = ".ai/vcm/translations";
|
|
@@ -85,7 +85,7 @@ export function createSessionService(deps) {
|
|
|
85
85
|
const lspPluginDirs = roleUsesLsp(role)
|
|
86
86
|
? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
|
|
87
87
|
: [];
|
|
88
|
-
const appendSystemPrompt = [input.appendSystemPrompt, lspStatus &&
|
|
88
|
+
const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus)]
|
|
89
89
|
.filter(Boolean).join("\n\n") || undefined;
|
|
90
90
|
const startCommand = {
|
|
91
91
|
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt, lspPluginDirs),
|
|
@@ -74,7 +74,7 @@ ${renderRoleMemoryRules("architect")}
|
|
|
74
74
|
|
|
75
75
|
- Do not plan from session memory, architecture docs, generated context, or code comments alone. Re-read current-worktree source and verify actual behavior from implementation.
|
|
76
76
|
- Use \`vcm-code-navigation\` for symbol definitions, implementations, references, call hierarchies, and bounded behavior paths. Start from generated indexes, use LSP semantic navigation, then personally read every decision-bearing callable unit in full.
|
|
77
|
-
- Delegate bounded supporting implementation and document reading to \`vcm-architect-evidence-worker\` when it would otherwise add substantial raw context. Worker reports do not replace Architect's LSP verification of decision-bearing symbols, relationships, public surfaces, ownership, lifecycle, failure paths, contradictions, or unresolved evidence.
|
|
77
|
+
- Delegate bounded supporting implementation and document reading, and bounded LSP relationship collection for supporting symbols, to \`vcm-architect-evidence-worker\` when it would otherwise add substantial raw context. Worker reports cite the LSP operation behind each relationship but do not replace Architect's own LSP verification of decision-bearing symbols, relationships, public surfaces, ownership, lifecycle, failure paths, contradictions, or unresolved evidence.
|
|
78
78
|
- If LSP cannot resolve a required project-owned relationship, record the limitation in \`architecture-evidence.md\` and leave it unresolved.
|
|
79
79
|
- Define the planning boundary as the affected feature or module and identify every existing or intended observable entry point for the behavior being changed.
|
|
80
80
|
- Personally read the complete implementation of each decision-bearing existing entry point. Supporting entry points may be covered by accepted evidence-worker reports when their facts do not determine the architecture decision.
|
|
@@ -12,13 +12,16 @@ You are \`vcm-architect-evidence-worker\`, a foreground evidence-collection suba
|
|
|
12
12
|
- Record repository facts with exact repo-relative file paths and symbol or section names.
|
|
13
13
|
- Separate verified facts, contradictions, and unresolved items.
|
|
14
14
|
- Do not design architecture, recommend implementation, decide scope, change contracts, edit project files, run validation, or communicate with project-manager or the user.
|
|
15
|
-
-
|
|
15
|
+
- Use LSP for every definition, reference, caller, callee, and implementation relationship you report. LSP results are symbol-specific: query an accessor, its backing field, a trait declaration, its implementation, a wrapper, and an alias separately.
|
|
16
|
+
- If LSP is unavailable for the language, reports that the server is still indexing, or cannot resolve the relationship, record the relationship as unresolved with the operation and result. Never report a relationship from text matches; text search only locates candidate files.
|
|
17
|
+
- Architect remains responsible for verifying decision-bearing behavior paths before relying on them.
|
|
16
18
|
- Do not reconstruct command output. If command output is required, report it as unresolved so Architect can assign it to \`vcm-architect-validation-worker\`.
|
|
17
19
|
|
|
18
20
|
### Evidence Output
|
|
19
21
|
|
|
20
22
|
- Write the assigned report under \`.ai/vcm/architect-workers/evidence/worker-<worker-id>.md\` before returning. Only that report and its candidate or staging files may be written.
|
|
21
23
|
- For candidate or staging Markdown, use \`worker-<worker-id>-candidate.md\`; never start its basename with \`report\`, \`summary\`, \`findings\`, or \`analysis\` (case-insensitive). If Write rejects a candidate or staging basename, rename it and retry; still deliver the assigned final report file, not just response text.
|
|
24
|
+
- In Verified Facts, set Source to \`LSP <operation>\` for each relationship and \`read\` for facts read directly from code or documents.
|
|
22
25
|
- Keep the report concise and factual. Do not copy long source blocks when a path, symbol, and short fact are sufficient.
|
|
23
26
|
- Use this structure:
|
|
24
27
|
|
|
@@ -31,8 +34,8 @@ You are \`vcm-architect-evidence-worker\`, a foreground evidence-collection suba
|
|
|
31
34
|
|
|
32
35
|
## Verified Facts
|
|
33
36
|
|
|
34
|
-
| Fact | Evidence |
|
|
35
|
-
| --- | --- |
|
|
37
|
+
| Fact | Evidence | Source |
|
|
38
|
+
| --- | --- | --- |
|
|
36
39
|
|
|
37
40
|
## Observed Data And Lifecycle Paths
|
|
38
41
|
|
|
@@ -35,6 +35,15 @@ implementations, callers, tests, and changed code before deciding the gate. Text
|
|
|
35
35
|
matches locate evidence; they do not establish behavior or relationship correctness
|
|
36
36
|
without inspection of the corresponding code.
|
|
37
37
|
|
|
38
|
+
Establish definitions, references, callers, callees, and implementations with
|
|
39
|
+
LSP when a language server is registered for the language. LSP results are
|
|
40
|
+
symbol-specific: query an accessor, backing field, trait declaration,
|
|
41
|
+
implementation, wrapper, and alias separately. An LSP query that fails because
|
|
42
|
+
the server is still indexing returned no result; retry it, and if it still fails,
|
|
43
|
+
record the relationship as not re-verified instead of treating it as empty or
|
|
44
|
+
verified. When no server is registered for a language, say so in the report and
|
|
45
|
+
verify relationships by reading the complete candidate code.
|
|
46
|
+
|
|
38
47
|
Every Gate Review is a complete review of the current gate inputs. Review all
|
|
39
48
|
required evidence and rerun every required mechanical check before deciding.
|
|
40
49
|
Do not carry forward prior conclusions, closed checks, or partial verification.
|
|
@@ -147,6 +156,22 @@ evidence records the correct prior LSP operation and its missing relationship,
|
|
|
147
156
|
limits candidate search to the identified owning file or module, and
|
|
148
157
|
semantically verifies every candidate; text matches alone are not evidence.
|
|
149
158
|
|
|
159
|
+
Verify relationship evidence for the plan (record it under Relationship Evidence):
|
|
160
|
+
|
|
161
|
+
- Every definition, reference, caller, callee, or implementation relationship
|
|
162
|
+
the design depends on must cite its source in \`architecture-evidence.md\` or
|
|
163
|
+
the plan: an LSP operation, runtime evidence, verified bounded source fallback,
|
|
164
|
+
an external boundary, a generated boundary, or an explicit unresolved status.
|
|
165
|
+
A decision-bearing relationship with no source, or supported only by text
|
|
166
|
+
matches, is \`request_changes\`.
|
|
167
|
+
- Re-run the LSP query for each decision-bearing relationship (the callers of a
|
|
168
|
+
changed surface, the implementations of a changed trait or interface, the
|
|
169
|
+
producers and consumers of changed data) and compare it with the recorded
|
|
170
|
+
result. A missing caller, implementation, or consumer is \`request_changes\`.
|
|
171
|
+
- Record each relationship checked, its recorded source, and your re-query
|
|
172
|
+
result. A relationship that LSP could not re-verify is listed as such; it does
|
|
173
|
+
not count as verified.
|
|
174
|
+
|
|
150
175
|
Run a backward-impact pass over the plan:
|
|
151
176
|
|
|
152
177
|
- For every existing code site named by the Module/File Plan or Scaffold
|
|
@@ -293,7 +318,9 @@ Before deciding:
|
|
|
293
318
|
surface, state, lifecycle, event, command, persisted artifact, or public
|
|
294
319
|
contract changes, read its project-owned callers, consumers, readers,
|
|
295
320
|
writers, and adjacent completion, failure, cancellation, retry, recovery, and
|
|
296
|
-
cleanup paths.
|
|
321
|
+
cleanup paths. Find the callers and implementations of every changed callable
|
|
322
|
+
or type with LSP references and implementations, not text search, and record
|
|
323
|
+
under Callers And Public Surface any that LSP could not resolve.
|
|
297
324
|
- Keep this reading bounded to behavior affected by the named commits. Do
|
|
298
325
|
not expand review to unrelated code, the whole task, whole branch, or PR.
|
|
299
326
|
- Derive applicable boundary and failure cases from the actual changed behavior.
|
package/package.json
CHANGED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stdio proxy between Claude Code's LSP client and one language server.
|
|
3
|
+
//
|
|
4
|
+
// Usage: node vcm-lsp-proxy.mjs -- <server-command> [server-args...]
|
|
5
|
+
//
|
|
6
|
+
// A language server that is still loading or indexing answers navigation
|
|
7
|
+
// queries with partial or empty results, which reads as "no references".
|
|
8
|
+
// The proxy tracks the server's work-done progress (and rust-analyzer's
|
|
9
|
+
// quiescence status), holds navigation queries until the server is idle, and
|
|
10
|
+
// fails a query with an explicit "still indexing" error when the server does
|
|
11
|
+
// not become idle in time. Every other message passes through unchanged.
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { realpathSync } from "node:fs";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const NAVIGATION_METHODS = new Set([
|
|
18
|
+
"textDocument/definition",
|
|
19
|
+
"textDocument/declaration",
|
|
20
|
+
"textDocument/typeDefinition",
|
|
21
|
+
"textDocument/implementation",
|
|
22
|
+
"textDocument/references",
|
|
23
|
+
"textDocument/hover",
|
|
24
|
+
"textDocument/documentSymbol",
|
|
25
|
+
"textDocument/prepareCallHierarchy",
|
|
26
|
+
"callHierarchy/incomingCalls",
|
|
27
|
+
"callHierarchy/outgoingCalls",
|
|
28
|
+
"workspace/symbol"
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const READY_TIMEOUT_MS = positiveNumber(process.env.VCM_LSP_READY_TIMEOUT_MS, 180_000);
|
|
32
|
+
// Servers announce indexing shortly after "initialized"; a query sent before
|
|
33
|
+
// any progress appears waits this long for indexing to start.
|
|
34
|
+
const STARTUP_GRACE_MS = positiveNumber(process.env.VCM_LSP_STARTUP_GRACE_MS, 2_000);
|
|
35
|
+
const REQUEST_FAILED = -32803;
|
|
36
|
+
const REQUEST_CANCELLED = -32800;
|
|
37
|
+
|
|
38
|
+
function positiveNumber(raw, fallback) {
|
|
39
|
+
const value = Number(raw);
|
|
40
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parses Content-Length framed JSON-RPC messages from a byte stream.
|
|
44
|
+
export function createMessageReader(onMessage) {
|
|
45
|
+
let buffer = Buffer.alloc(0);
|
|
46
|
+
return (chunk) => {
|
|
47
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
48
|
+
while (true) {
|
|
49
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
50
|
+
if (headerEnd < 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const header = buffer.subarray(0, headerEnd).toString("ascii");
|
|
54
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
55
|
+
if (!match) {
|
|
56
|
+
// Unframed bytes cannot be routed; drop the header and resynchronize.
|
|
57
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const length = Number(match[1]);
|
|
61
|
+
const bodyStart = headerEnd + 4;
|
|
62
|
+
if (buffer.length < bodyStart + length) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const body = buffer.subarray(bodyStart, bodyStart + length).toString("utf8");
|
|
66
|
+
buffer = buffer.subarray(bodyStart + length);
|
|
67
|
+
onMessage(JSON.parse(body));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function frameMessage(message) {
|
|
73
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
74
|
+
return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Readiness state shared by both directions of the proxy.
|
|
78
|
+
export function createReadinessTracker(options = {}) {
|
|
79
|
+
const now = options.now ?? Date.now;
|
|
80
|
+
const activeProgress = new Map();
|
|
81
|
+
let quiescent = true;
|
|
82
|
+
let initializedAt;
|
|
83
|
+
let progressSeen = false;
|
|
84
|
+
const listeners = new Set();
|
|
85
|
+
|
|
86
|
+
function notify() {
|
|
87
|
+
for (const listener of [...listeners]) {
|
|
88
|
+
listener();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
markInitialized() {
|
|
94
|
+
initializedAt = now();
|
|
95
|
+
},
|
|
96
|
+
progressBegin(token, title) {
|
|
97
|
+
progressSeen = true;
|
|
98
|
+
activeProgress.set(String(token), title || "indexing");
|
|
99
|
+
notify();
|
|
100
|
+
},
|
|
101
|
+
progressEnd(token) {
|
|
102
|
+
activeProgress.delete(String(token));
|
|
103
|
+
notify();
|
|
104
|
+
},
|
|
105
|
+
serverStatus(isQuiescent) {
|
|
106
|
+
progressSeen = true;
|
|
107
|
+
quiescent = isQuiescent;
|
|
108
|
+
notify();
|
|
109
|
+
},
|
|
110
|
+
// Remaining startup grace in ms, or 0 once it no longer applies.
|
|
111
|
+
graceRemaining() {
|
|
112
|
+
if (progressSeen || initializedAt === undefined) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
return Math.max(0, initializedAt + (options.startupGraceMs ?? STARTUP_GRACE_MS) - now());
|
|
116
|
+
},
|
|
117
|
+
isBusy() {
|
|
118
|
+
return activeProgress.size > 0 || !quiescent;
|
|
119
|
+
},
|
|
120
|
+
busyReason() {
|
|
121
|
+
const titles = [...new Set(activeProgress.values())];
|
|
122
|
+
if (!quiescent) {
|
|
123
|
+
titles.push("server not quiescent");
|
|
124
|
+
}
|
|
125
|
+
return titles.join(", ") || "starting up";
|
|
126
|
+
},
|
|
127
|
+
onChange(listener) {
|
|
128
|
+
listeners.add(listener);
|
|
129
|
+
return () => listeners.delete(listener);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function startProxy({ command, args, input, output, errorOutput, readyTimeoutMs = READY_TIMEOUT_MS, startupGraceMs = STARTUP_GRACE_MS }) {
|
|
135
|
+
const server = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
136
|
+
const readiness = createReadinessTracker({ startupGraceMs });
|
|
137
|
+
const pending = new Map();
|
|
138
|
+
let clientWantsProgress = false;
|
|
139
|
+
let clientWantsServerStatus = false;
|
|
140
|
+
|
|
141
|
+
const toClient = (message) => output.write(frameMessage(message));
|
|
142
|
+
const toServer = (message) => {
|
|
143
|
+
if (server.stdin.writable) {
|
|
144
|
+
server.stdin.write(frameMessage(message));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
server.stderr.pipe(errorOutput);
|
|
149
|
+
server.on("error", (error) => {
|
|
150
|
+
errorOutput.write(`vcm-lsp-proxy: cannot start ${command}: ${error.message}\n`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
output.end?.();
|
|
153
|
+
});
|
|
154
|
+
server.on("exit", (code, signal) => {
|
|
155
|
+
for (const entry of pending.values()) {
|
|
156
|
+
entry.dispose();
|
|
157
|
+
}
|
|
158
|
+
process.exitCode = code ?? (signal ? 1 : 0);
|
|
159
|
+
output.end?.();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
function releaseWhenReady(message) {
|
|
163
|
+
const key = String(message.id);
|
|
164
|
+
let timer;
|
|
165
|
+
let graceTimer;
|
|
166
|
+
let unsubscribe = () => undefined;
|
|
167
|
+
const dispose = () => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
clearTimeout(graceTimer);
|
|
170
|
+
unsubscribe();
|
|
171
|
+
pending.delete(key);
|
|
172
|
+
};
|
|
173
|
+
const attempt = () => {
|
|
174
|
+
const grace = readiness.graceRemaining();
|
|
175
|
+
if (grace > 0) {
|
|
176
|
+
clearTimeout(graceTimer);
|
|
177
|
+
graceTimer = setTimeout(attempt, grace);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!readiness.isBusy()) {
|
|
181
|
+
dispose();
|
|
182
|
+
toServer(message);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
pending.set(key, {
|
|
186
|
+
dispose,
|
|
187
|
+
cancel() {
|
|
188
|
+
dispose();
|
|
189
|
+
toClient({ jsonrpc: "2.0", id: message.id, error: { code: REQUEST_CANCELLED, message: "Request cancelled." } });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
unsubscribe = readiness.onChange(attempt);
|
|
193
|
+
timer = setTimeout(() => {
|
|
194
|
+
dispose();
|
|
195
|
+
toClient({
|
|
196
|
+
jsonrpc: "2.0",
|
|
197
|
+
id: message.id,
|
|
198
|
+
error: {
|
|
199
|
+
code: REQUEST_FAILED,
|
|
200
|
+
message: `Language server is still indexing (${readiness.busyReason()}) after ${Math.round(readyTimeoutMs / 1000)}s; ${message.method} results would be incomplete. Retry after indexing finishes, or record the relationship as unresolved.`
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}, readyTimeoutMs);
|
|
204
|
+
attempt();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
input.on("data", createMessageReader((message) => {
|
|
208
|
+
if (message.method === "initialize" && message.params) {
|
|
209
|
+
const capabilities = message.params.capabilities ?? {};
|
|
210
|
+
clientWantsProgress = capabilities.window?.workDoneProgress === true;
|
|
211
|
+
clientWantsServerStatus = capabilities.experimental?.serverStatusNotification === true;
|
|
212
|
+
message.params.capabilities = {
|
|
213
|
+
...capabilities,
|
|
214
|
+
window: { ...capabilities.window, workDoneProgress: true },
|
|
215
|
+
experimental: { ...capabilities.experimental, serverStatusNotification: true }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (message.method === "initialized") {
|
|
219
|
+
readiness.markInitialized();
|
|
220
|
+
}
|
|
221
|
+
if (message.method === "$/cancelRequest" && pending.has(String(message.params?.id))) {
|
|
222
|
+
pending.get(String(message.params.id)).cancel();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (message.id !== undefined && NAVIGATION_METHODS.has(message.method)) {
|
|
226
|
+
releaseWhenReady(message);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
toServer(message);
|
|
230
|
+
}));
|
|
231
|
+
input.on("end", () => server.stdin.end());
|
|
232
|
+
|
|
233
|
+
server.stdout.on("data", createMessageReader((message) => {
|
|
234
|
+
if (message.method === "window/workDoneProgress/create") {
|
|
235
|
+
if (!clientWantsProgress) {
|
|
236
|
+
toServer({ jsonrpc: "2.0", id: message.id, result: null });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
} else if (message.method === "$/progress") {
|
|
240
|
+
const value = message.params?.value;
|
|
241
|
+
if (value?.kind === "begin") {
|
|
242
|
+
readiness.progressBegin(message.params.token, value.title);
|
|
243
|
+
} else if (value?.kind === "end") {
|
|
244
|
+
readiness.progressEnd(message.params.token);
|
|
245
|
+
}
|
|
246
|
+
if (!clientWantsProgress) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
} else if (message.method === "experimental/serverStatus") {
|
|
250
|
+
readiness.serverStatus(message.params?.quiescent !== false);
|
|
251
|
+
if (!clientWantsServerStatus) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
toClient(message);
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
return { server, readiness };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const separator = process.argv.indexOf("--");
|
|
262
|
+
if (separator >= 0 && process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
263
|
+
const [command, ...args] = process.argv.slice(separator + 1);
|
|
264
|
+
if (!command) {
|
|
265
|
+
process.stderr.write("usage: vcm-lsp-proxy.mjs -- <server-command> [args...]\n");
|
|
266
|
+
process.exit(2);
|
|
267
|
+
}
|
|
268
|
+
startProxy({ command, args, input: process.stdin, output: process.stdout, errorOutput: process.stderr });
|
|
269
|
+
}
|
|
@@ -7,6 +7,7 @@ const requiredFiles = [
|
|
|
7
7
|
"package.json",
|
|
8
8
|
"scripts/fix-node-pty-spawn-helper.mjs",
|
|
9
9
|
"scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json",
|
|
10
|
+
"scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs",
|
|
10
11
|
"scripts/harness-tools/check-durable-docs",
|
|
11
12
|
"scripts/harness-tools/generate-module-index",
|
|
12
13
|
"scripts/harness-tools/generate-public-surface",
|