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 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 only for Architect sessions,
514
- including Codex Bridge launches. The project environment must still
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
- Other roles use generated context and ordinary source reads. Harness Studio
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
- tools: "Read, Grep, Glob, Write"
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
- const LSP_ROLE_NAMES = new Set(["architect"]);
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.", { tools: "Read, Grep, Glob, Write" }),
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).filter(([, server]) => commands.has(server.command)));
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 `Architect LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`} ${recovery} Restart Architect after fixing the environment so VCM can detect and register the server again.`;
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 renderArchitectLspNotice(status) {
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 Architect LSP Availability]",
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."
@@ -25,6 +25,7 @@ export const GATE_ANALYSIS_FIELDS = {
25
25
  "Code Reality",
26
26
  "Invalidated Assumptions",
27
27
  "Existing-Class Completeness",
28
+ "Relationship Evidence",
28
29
  "Ownership",
29
30
  "Data Flow",
30
31
  "Lifecycle",
@@ -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, renderArchitectLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
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 && renderArchitectLspNotice(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
- - Do not claim a complete semantic relationship from text matches. Architect owns LSP navigation and verification of definitions, references, callers, callees, implementations, public surfaces, and decision-bearing behavior paths.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [
@@ -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",