vibe-coding-master 0.7.36 → 0.7.38

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
@@ -455,6 +455,7 @@ Harness Studio is the UI for VCM harness maintenance.
455
455
  Use it to:
456
456
 
457
457
  - inspect fixed harness status
458
+ - inspect detected project languages and verified language-server readiness
458
459
  - run bootstrap
459
460
  - open Harness Engineer
460
461
  - review harness files
@@ -466,6 +467,16 @@ Use it to:
466
467
  - merge task harness commits back to the connected repository branch when
467
468
  appropriate
468
469
 
470
+ VCM bundles the Claude Code LSP bridge and loads it for Architect, Coder, and
471
+ Reviewer sessions, including CCR launches. The project environment must still
472
+ provide the language server for each detected language: `rust-analyzer`,
473
+ `typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
474
+ `jdtls`. Architect, Coder, and Reviewer do not receive the Grep tool, and the
475
+ Harness guard rejects shell text-search commands for those roles so unresolved
476
+ semantic relationships cannot be hidden by textual fallback. Harness Studio
477
+ reports a missing executable or failed readiness probe
478
+ with the backend diagnostic.
479
+
469
480
  Harness Engineer is task-scoped and runs from the active task worktree. The
470
481
  backend automatically starts a fresh Harness Engineer for each active task or
471
482
  resumes its saved Session. Durable memory is versioned with the project harness
@@ -18,8 +18,9 @@ export function createClaudeAdapter(runner) {
18
18
  }
19
19
  return result.stdout.trim();
20
20
  },
21
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt) {
22
- const args = ["--agent", role];
21
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt, pluginDirs = []) {
22
+ const args = pluginDirs.flatMap((pluginDir) => ["--plugin-dir", pluginDir]);
23
+ args.push("--agent", role);
23
24
  const sessionSettings = { ...settingsOverride };
24
25
  if (claudeSessionId) {
25
26
  args.push(resume ? "--resume" : "--session-id", claudeSessionId);
@@ -6,6 +6,7 @@ export function createCommandRunner() {
6
6
  const result = await execa(command, args, {
7
7
  cwd: options.cwd,
8
8
  env: options.env,
9
+ timeout: options.timeoutMs,
9
10
  reject: false
10
11
  });
11
12
  return {
@@ -20,6 +20,7 @@ import { renderProjectManagerHarnessRules } from "../templates/harness/project-m
20
20
  import { renderPullRequestTemplateHarnessRules } from "../templates/harness/pull-request-template.js";
21
21
  import { renderTesterHarnessRules } from "../templates/harness/tester-agent.js";
22
22
  import { renderVcmArchitectureInterviewSkillRules } from "../templates/harness/vcm-architecture-interview-skill.js";
23
+ import { renderVcmCodeNavigationSkillRules } from "../templates/harness/vcm-code-navigation-skill.js";
23
24
  import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
24
25
  import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
25
26
  import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
@@ -52,6 +53,7 @@ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
52
53
  const VCM_AUTO_MEMORY_ENABLED = false;
53
54
  const VCM_HOOK_DEFINITIONS = [
54
55
  { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
56
+ { eventName: "PreToolUse", matcher: "Grep", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
55
57
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
56
58
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
57
59
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
@@ -64,18 +66,18 @@ const AGENT_FRONTMATTER = {
64
66
  },
65
67
  architect: {
66
68
  description: "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.",
67
- tools: "Read, Grep, Glob, Bash, Edit, Write, Agent"
69
+ tools: "Read, Glob, Bash, Edit, Write, Agent, LSP"
68
70
  },
69
71
  coder: {
70
72
  description: "VCM implementation role for scoped code changes and focused tests.",
71
- tools: "Read, Grep, Glob, Bash, Edit, Write, Agent"
73
+ tools: "Read, Glob, Bash, Edit, Write, Agent, LSP"
72
74
  },
73
75
  tester: {
74
76
  description: "VCM testing role for validation, test adequacy, approved-scope validation, and risk findings."
75
77
  },
76
78
  reviewer: {
77
79
  description: "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.",
78
- tools: "Read, Grep, Glob, Bash, Write"
80
+ tools: "Read, Glob, Bash, Write, LSP"
79
81
  },
80
82
  translator: {
81
83
  description: "VCM task-scoped translation tool role for conversation translation, file translation, bootstrap, and memory updates."
@@ -93,6 +95,16 @@ const AGENT_FRONTMATTER = {
93
95
  effort: "xhigh"
94
96
  }
95
97
  };
98
+ const REQUIRED_AGENT_TOOLS = {
99
+ architect: ["Agent", "LSP"],
100
+ coder: ["Agent", "LSP"],
101
+ reviewer: ["LSP"]
102
+ };
103
+ const FORBIDDEN_AGENT_TOOLS = {
104
+ architect: ["Grep"],
105
+ coder: ["Grep"],
106
+ reviewer: ["Grep"]
107
+ };
96
108
  const MANAGED_FILES = [
97
109
  {
98
110
  path: "CLAUDE.md",
@@ -256,6 +268,12 @@ const WHOLE_FILES = [
256
268
  mode: 0o644,
257
269
  content: renderSkillFile("VCM Architecture Interview Skill", "vcm-architecture-interview", "Use when Architect must confirm user-owned behavior and contract decisions before architecture planning.", renderVcmArchitectureInterviewSkillRules())
258
270
  },
271
+ {
272
+ path: ".claude/skills/vcm-code-navigation/SKILL.md",
273
+ category: "skill",
274
+ mode: 0o644,
275
+ content: renderSkillFile("VCM Code Navigation Skill", "vcm-code-navigation", "Use when Architect, Coder, or Reviewer must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths.", renderVcmCodeNavigationSkillRules())
276
+ },
259
277
  {
260
278
  path: ".claude/skills/vcm-final-acceptance/SKILL.md",
261
279
  category: "skill",
@@ -657,8 +675,11 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
657
675
  if (definition.memoryBlock) {
658
676
  nextContent = ensureVcmMemoryBlock(nextContent);
659
677
  }
660
- if (definition.agentName === "architect") {
661
- nextContent = ensureAgentTool(nextContent, "Agent");
678
+ for (const requiredTool of REQUIRED_AGENT_TOOLS[definition.agentName] ?? []) {
679
+ nextContent = ensureAgentTool(nextContent, requiredTool);
680
+ }
681
+ for (const forbiddenTool of FORBIDDEN_AGENT_TOOLS[definition.agentName] ?? []) {
682
+ nextContent = removeAgentTool(nextContent, forbiddenTool);
662
683
  }
663
684
  await writeIfChanged({
664
685
  targetPath,
@@ -722,6 +743,22 @@ function ensureAgentTool(content, requiredTool) {
722
743
  const nextTools = [...tools, requiredTool].join(", ");
723
744
  return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
724
745
  }
746
+ function removeAgentTool(content, forbiddenTool) {
747
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
748
+ if (!frontmatterMatch) {
749
+ return content;
750
+ }
751
+ const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
752
+ if (!toolsMatch) {
753
+ return content;
754
+ }
755
+ const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
756
+ const nextTools = tools.filter((tool) => tool !== forbiddenTool);
757
+ if (nextTools.length === tools.length) {
758
+ return content;
759
+ }
760
+ return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools.join(", ")}`));
761
+ }
725
762
  function migrateLegacyManagedFile(definition, currentContent, block) {
726
763
  const legacyContent = definition.legacyWholeFile?.trimEnd();
727
764
  if (!legacyContent) {
@@ -246,6 +246,7 @@ export function createDefaultServerDeps(options = {}) {
246
246
  });
247
247
  const harnessService = createHarnessService({
248
248
  fs,
249
+ commandRunner: runner,
249
250
  git,
250
251
  runtime,
251
252
  harnessEngineerSessions: sessionService,
@@ -0,0 +1,253 @@
1
+ import path from "node:path";
2
+ import { VCM_LSP_PLUGIN_DIR, VCM_LSP_PLUGIN_MANIFEST, VCM_LSP_PLUGIN_NAME } from "./lsp-plugin.js";
3
+ const LANGUAGE_DEFINITIONS = [
4
+ {
5
+ language: "rust",
6
+ label: "Rust",
7
+ extensions: [".rs"],
8
+ manifests: ["Cargo.toml"],
9
+ serverCommand: "rust-analyzer",
10
+ pluginServerName: "rust-analyzer",
11
+ probeArgs: ["--version"]
12
+ },
13
+ {
14
+ language: "typescript",
15
+ label: "TypeScript / JavaScript",
16
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"],
17
+ manifests: ["package.json", "tsconfig.json", "jsconfig.json"],
18
+ serverCommand: "typescript-language-server",
19
+ pluginServerName: "typescript",
20
+ probeArgs: ["--version"]
21
+ },
22
+ {
23
+ language: "python",
24
+ label: "Python",
25
+ extensions: [".py", ".pyi"],
26
+ manifests: ["pyproject.toml", "requirements.txt", "setup.py"],
27
+ serverCommand: "pyright-langserver",
28
+ pluginServerName: "pyright",
29
+ probeArgs: ["--version"]
30
+ },
31
+ {
32
+ language: "go",
33
+ label: "Go",
34
+ extensions: [".go"],
35
+ manifests: ["go.mod"],
36
+ serverCommand: "gopls",
37
+ pluginServerName: "gopls",
38
+ probeArgs: ["version"]
39
+ },
40
+ {
41
+ language: "cpp",
42
+ label: "C / C++",
43
+ extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"],
44
+ manifests: ["CMakeLists.txt", "compile_commands.json"],
45
+ serverCommand: "clangd",
46
+ pluginServerName: "clangd",
47
+ probeArgs: ["--version"]
48
+ },
49
+ {
50
+ language: "java",
51
+ label: "Java",
52
+ extensions: [".java"],
53
+ manifests: ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
54
+ serverCommand: "jdtls",
55
+ pluginServerName: "jdtls",
56
+ probeArgs: ["--version"]
57
+ }
58
+ ];
59
+ const DEFAULT_PROBE_TIMEOUT_MS = 3_000;
60
+ const DEFAULT_CACHE_TTL_MS = 60_000;
61
+ export function createHarnessCodeIntelligenceDetector(fs, options = {}) {
62
+ const probeCache = new Map();
63
+ const probesInFlight = new Map();
64
+ const now = options.now ?? Date.now;
65
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
66
+ return {
67
+ async detect(repoRoot) {
68
+ return detectCodeIntelligence(fs, repoRoot, options, async (definition, executablePath) => {
69
+ const cacheKey = `${executablePath}\u0000${definition.probeArgs.join("\u0000")}`;
70
+ const cached = probeCache.get(cacheKey);
71
+ const currentTime = now();
72
+ if (cached && cached.expiresAt > currentTime) {
73
+ return cached.result;
74
+ }
75
+ if (!options.runner) {
76
+ return undefined;
77
+ }
78
+ const existingProbe = probesInFlight.get(cacheKey);
79
+ if (existingProbe) {
80
+ return existingProbe;
81
+ }
82
+ const probePromise = options.runner.run(executablePath, definition.probeArgs, {
83
+ cwd: repoRoot,
84
+ timeoutMs: options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
85
+ });
86
+ probesInFlight.set(cacheKey, probePromise);
87
+ const result = await probePromise.finally(() => probesInFlight.delete(cacheKey));
88
+ probeCache.set(cacheKey, {
89
+ expiresAt: currentTime + cacheTtlMs,
90
+ result
91
+ });
92
+ return result;
93
+ });
94
+ }
95
+ };
96
+ }
97
+ export async function detectHarnessCodeIntelligence(fs, repoRoot, options = {}) {
98
+ return createHarnessCodeIntelligenceDetector(fs, options).detect(repoRoot);
99
+ }
100
+ async function detectCodeIntelligence(fs, repoRoot, options, probe) {
101
+ const indexPaths = await readModuleIndexPaths(fs, repoRoot);
102
+ const pluginServers = await readPluginServers(fs, options.pluginDir ?? VCM_LSP_PLUGIN_DIR);
103
+ const detectedLanguages = await Promise.all(LANGUAGE_DEFINITIONS.map(async (definition) => {
104
+ const detectedBy = await detectLanguage(fs, repoRoot, indexPaths, definition);
105
+ if (detectedBy.length === 0) {
106
+ return undefined;
107
+ }
108
+ const pluginReady = pluginServers.get(definition.pluginServerName) === definition.serverCommand;
109
+ const executablePath = await findExecutable(fs, repoRoot, definition.serverCommand, options);
110
+ const probeResult = executablePath ? await probe(definition, executablePath) : undefined;
111
+ const serverFound = Boolean(executablePath);
112
+ const serverRunnable = probeResult?.exitCode === 0;
113
+ const state = languageState(pluginReady, serverFound, serverRunnable, Boolean(probeResult));
114
+ const status = {
115
+ language: definition.language,
116
+ label: definition.label,
117
+ serverCommand: definition.serverCommand,
118
+ pluginName: VCM_LSP_PLUGIN_NAME,
119
+ detected: true,
120
+ pluginReady,
121
+ serverFound,
122
+ serverRunnable,
123
+ state,
124
+ error: languageError(state, definition.serverCommand, probeResult),
125
+ detectedBy
126
+ };
127
+ return status;
128
+ }));
129
+ const languages = detectedLanguages.filter((language) => language !== undefined);
130
+ const readyCount = languages.filter((language) => language.state === "ready").length;
131
+ return {
132
+ state: languages.length === 0
133
+ ? "not_detected"
134
+ : readyCount === languages.length
135
+ ? "ready"
136
+ : readyCount === 0
137
+ ? "missing"
138
+ : "partial",
139
+ languages
140
+ };
141
+ }
142
+ function languageState(pluginReady, serverFound, serverRunnable, probeCompleted) {
143
+ if (!pluginReady) {
144
+ return "plugin_missing";
145
+ }
146
+ if (!serverFound) {
147
+ return "server_missing";
148
+ }
149
+ if (!probeCompleted) {
150
+ return "server_unverified";
151
+ }
152
+ return serverRunnable ? "ready" : "server_failed";
153
+ }
154
+ function languageError(state, command, probeResult) {
155
+ if (state === "plugin_missing") {
156
+ return `VCM LSP plugin manifest is missing or does not declare ${command}.`;
157
+ }
158
+ if (state === "server_missing") {
159
+ return `${command} was not found in the VCM backend PATH.`;
160
+ }
161
+ if (state === "server_unverified") {
162
+ return `${command} was found but has not been probed by this runtime.`;
163
+ }
164
+ if (state !== "server_failed") {
165
+ return undefined;
166
+ }
167
+ const detail = firstNonEmptyLine(probeResult?.stderr, probeResult?.stdout);
168
+ return detail
169
+ ? `${command} failed its startup probe. ${detail}`
170
+ : `${command} failed its startup probe with exit code ${probeResult?.exitCode ?? 1}.`;
171
+ }
172
+ async function readPluginServers(fs, pluginDir) {
173
+ const manifestPath = pluginDir === VCM_LSP_PLUGIN_DIR
174
+ ? VCM_LSP_PLUGIN_MANIFEST
175
+ : path.join(pluginDir, ".claude-plugin", "plugin.json");
176
+ if (!await fs.pathExists(manifestPath)) {
177
+ return new Map();
178
+ }
179
+ try {
180
+ const manifest = await fs.readJson(manifestPath);
181
+ return new Map(Object.entries(manifest.lspServers ?? {})
182
+ .filter((entry) => typeof entry[1]?.command === "string")
183
+ .map(([name, server]) => [name, server.command]));
184
+ }
185
+ catch {
186
+ return new Map();
187
+ }
188
+ }
189
+ async function readModuleIndexPaths(fs, repoRoot) {
190
+ const indexPath = path.join(repoRoot, ".ai/generated/module-index.json");
191
+ if (!await fs.pathExists(indexPath)) {
192
+ return [];
193
+ }
194
+ try {
195
+ const value = await fs.readJson(indexPath);
196
+ return collectPathLikeStrings(value);
197
+ }
198
+ catch {
199
+ return [];
200
+ }
201
+ }
202
+ async function detectLanguage(fs, repoRoot, indexPaths, definition) {
203
+ const evidence = new Set();
204
+ for (const manifest of definition.manifests) {
205
+ if (await fs.pathExists(path.join(repoRoot, manifest))) {
206
+ evidence.add(manifest);
207
+ }
208
+ }
209
+ for (const filePath of indexPaths) {
210
+ if (definition.extensions.includes(path.extname(filePath).toLowerCase())) {
211
+ evidence.add(".ai/generated/module-index.json");
212
+ break;
213
+ }
214
+ }
215
+ return Array.from(evidence);
216
+ }
217
+ async function findExecutable(fs, repoRoot, command, options) {
218
+ const platform = options.platform ?? process.platform;
219
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
220
+ const extensions = platform === "win32"
221
+ ? (options.pathExt ?? process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
222
+ : [""];
223
+ for (const directory of pathEnv.split(path.delimiter)) {
224
+ for (const extension of extensions) {
225
+ const candidate = path.resolve(directory || repoRoot, `${command}${extension.toLowerCase()}`);
226
+ if (await fs.pathExists(candidate)) {
227
+ return candidate;
228
+ }
229
+ }
230
+ }
231
+ return undefined;
232
+ }
233
+ function firstNonEmptyLine(...values) {
234
+ for (const value of values) {
235
+ const line = value?.split(/\r?\n/).map((part) => part.trim()).find(Boolean);
236
+ if (line) {
237
+ return line.length > 300 ? `${line.slice(0, 297)}...` : line;
238
+ }
239
+ }
240
+ return undefined;
241
+ }
242
+ function collectPathLikeStrings(value) {
243
+ if (typeof value === "string") {
244
+ return value.includes("/") || path.extname(value) ? [value] : [];
245
+ }
246
+ if (Array.isArray(value)) {
247
+ return value.flatMap(collectPathLikeStrings);
248
+ }
249
+ if (value && typeof value === "object") {
250
+ return Object.values(value).flatMap(collectPathLikeStrings);
251
+ }
252
+ return [];
253
+ }
@@ -17,6 +17,7 @@ import { renderProjectManagerHarnessRules } from "../templates/harness/project-m
17
17
  import { renderPullRequestTemplateHarnessRules } from "../templates/harness/pull-request-template.js";
18
18
  import { renderTesterHarnessRules } from "../templates/harness/tester-agent.js";
19
19
  import { renderVcmArchitectureInterviewSkillRules } from "../templates/harness/vcm-architecture-interview-skill.js";
20
+ import { renderVcmCodeNavigationSkillRules } from "../templates/harness/vcm-code-navigation-skill.js";
20
21
  import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
21
22
  import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
22
23
  import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
@@ -29,6 +30,7 @@ import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } f
29
30
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
30
31
  import { VcmError } from "../errors.js";
31
32
  import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
33
+ import { createHarnessCodeIntelligenceDetector } from "./code-intelligence-service.js";
32
34
  const execFileAsync = promisify(execFile);
33
35
  const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
34
36
  const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
@@ -51,6 +53,7 @@ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
51
53
  const VCM_AUTO_MEMORY_ENABLED = false;
52
54
  const VCM_HOOK_DEFINITIONS = [
53
55
  { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
56
+ { eventName: "PreToolUse", matcher: "Grep", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
54
57
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
55
58
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
56
59
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
@@ -110,6 +113,14 @@ const HARNESS_FILES = [
110
113
  ownership: "whole-file",
111
114
  renderRules: renderVcmArchitectureInterviewSkillRules
112
115
  },
116
+ {
117
+ kind: "skill-vcm-code-navigation",
118
+ path: ".claude/skills/vcm-code-navigation/SKILL.md",
119
+ title: "VCM Code Navigation Skill",
120
+ frontmatter: renderSkillFrontmatter("vcm-code-navigation", "Use when Architect, Coder, or Reviewer must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths."),
121
+ ownership: "whole-file",
122
+ renderRules: renderVcmCodeNavigationSkillRules
123
+ },
113
124
  {
114
125
  kind: "skill-vcm-route-message",
115
126
  path: ".claude/skills/vcm-route-message/SKILL.md",
@@ -187,7 +198,9 @@ const HARNESS_FILES = [
187
198
  path: ".claude/agents/reviewer.md",
188
199
  title: "Reviewer Agent",
189
200
  memoryBlock: true,
190
- frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { tools: "Read, Grep, Glob, Bash, Write" }),
201
+ requiredTools: ["LSP"],
202
+ forbiddenTools: ["Grep"],
203
+ frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { tools: "Read, Glob, Bash, Write, LSP" }),
191
204
  renderRules: renderReviewerAgentRules
192
205
  },
193
206
  {
@@ -260,8 +273,10 @@ const HARNESS_FILES = [
260
273
  path: ".claude/agents/architect.md",
261
274
  title: "Architect Agent",
262
275
  memoryBlock: true,
276
+ requiredTools: ["Agent", "LSP"],
277
+ forbiddenTools: ["Grep"],
263
278
  blankLineBeforeEnd: true,
264
- frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Agent" }),
279
+ frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.", { tools: "Read, Glob, Bash, Edit, Write, Agent, LSP" }),
265
280
  renderRules: renderArchitectHarnessRules
266
281
  },
267
282
  {
@@ -269,7 +284,9 @@ const HARNESS_FILES = [
269
284
  path: ".claude/agents/coder.md",
270
285
  title: "Coder Agent",
271
286
  memoryBlock: true,
272
- frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Agent" }),
287
+ requiredTools: ["Agent", "LSP"],
288
+ forbiddenTools: ["Grep"],
289
+ frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { tools: "Read, Glob, Bash, Edit, Write, Agent, LSP" }),
273
290
  renderRules: renderCoderHarnessRules
274
291
  },
275
292
  {
@@ -284,14 +301,20 @@ const HARNESS_FILES = [
284
301
  export function createHarnessService(deps) {
285
302
  const now = deps.now ?? (() => new Date().toISOString());
286
303
  const vcmVersion = deps.vcmVersion ?? "unknown";
304
+ const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(deps.fs, {
305
+ runner: deps.commandRunner
306
+ });
287
307
  return {
288
308
  async getHarnessStatus(repoRoot) {
289
- const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
309
+ const [analyses, codeIntelligence] = await Promise.all([
310
+ analyzeHarnessFiles(deps.fs, repoRoot),
311
+ codeIntelligenceDetector.detect(repoRoot)
312
+ ]);
290
313
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
291
314
  const manifestChange = deps.runFixedInstaller
292
315
  ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
293
316
  : undefined;
294
- return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange);
317
+ return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange, codeIntelligence);
295
318
  },
296
319
  async getHarnessFileContent(repoRoot, filePath) {
297
320
  return readHarnessFileContent(deps.fs, repoRoot, filePath);
@@ -320,14 +343,17 @@ export function createHarnessService(deps) {
320
343
  harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update harness file")).harnessCommit;
321
344
  }
322
345
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
323
- const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
346
+ const [analyses, codeIntelligence] = await Promise.all([
347
+ analyzeHarnessFiles(deps.fs, repoRoot),
348
+ codeIntelligenceDetector.detect(repoRoot)
349
+ ]);
324
350
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
325
351
  const manifestChange = deps.runFixedInstaller
326
352
  ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
327
353
  : undefined;
328
354
  return {
329
355
  file,
330
- status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange),
356
+ status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange, codeIntelligence),
331
357
  harnessCommit
332
358
  };
333
359
  },
@@ -1197,7 +1223,8 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1197
1223
  if (!match) {
1198
1224
  const migratedContent = migrateLegacyHarnessFile(definition, currentContent, expectedBlock);
1199
1225
  if (migratedContent) {
1200
- const nextContent = definition.memoryBlock ? ensureVcmMemoryBlock(migratedContent) : migratedContent;
1226
+ const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(migratedContent) : migratedContent;
1227
+ const nextContent = normalizeAgentTools(memoryUpdatedContent, definition.requiredTools, definition.forbiddenTools);
1201
1228
  return {
1202
1229
  definition,
1203
1230
  status: {
@@ -1216,9 +1243,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1216
1243
  };
1217
1244
  }
1218
1245
  const insertedContent = `${currentContent.trimEnd()}\n\n${expectedBlock}\n`;
1219
- const nextContent = definition.kind === "agent-architect"
1220
- ? ensureAgentTool(insertedContent, "Agent")
1221
- : insertedContent;
1246
+ const nextContent = normalizeAgentTools(insertedContent, definition.requiredTools, definition.forbiddenTools);
1222
1247
  return {
1223
1248
  definition,
1224
1249
  status: {
@@ -1240,9 +1265,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1240
1265
  const currentBlock = match[0];
1241
1266
  const blockUpdatedContent = currentContent.replace(managedBlockPattern, expectedBlock);
1242
1267
  const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(blockUpdatedContent) : blockUpdatedContent;
1243
- const nextContent = definition.kind === "agent-architect"
1244
- ? ensureAgentTool(memoryUpdatedContent, "Agent")
1245
- : memoryUpdatedContent;
1268
+ const nextContent = normalizeAgentTools(memoryUpdatedContent, definition.requiredTools, definition.forbiddenTools);
1246
1269
  const action = currentContent === nextContent ? "ok" : "update";
1247
1270
  return {
1248
1271
  definition,
@@ -1300,7 +1323,7 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
1300
1323
  }
1301
1324
  return changes;
1302
1325
  }
1303
- function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange) {
1326
+ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange, codeIntelligence) {
1304
1327
  const files = analyses.map((analysis) => analysis.status);
1305
1328
  const plannedChanges = analyses
1306
1329
  .map((analysis) => analysis.plannedChange)
@@ -1326,6 +1349,7 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
1326
1349
  files,
1327
1350
  needsApply: plannedChanges.length > 0,
1328
1351
  plannedChanges,
1352
+ codeIntelligence,
1329
1353
  warnings: plannedChanges.length > 0
1330
1354
  ? ["Review and commit VCM Harness changes before starting long-running work."]
1331
1355
  : []
@@ -1369,6 +1393,29 @@ function ensureAgentTool(content, requiredTool) {
1369
1393
  const nextTools = [...tools, requiredTool].join(", ");
1370
1394
  return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
1371
1395
  }
1396
+ function ensureAgentTools(content, requiredTools) {
1397
+ return (requiredTools ?? []).reduce(ensureAgentTool, content);
1398
+ }
1399
+ function removeAgentTool(content, forbiddenTool) {
1400
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
1401
+ if (!frontmatterMatch) {
1402
+ return content;
1403
+ }
1404
+ const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
1405
+ if (!toolsMatch) {
1406
+ return content;
1407
+ }
1408
+ const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
1409
+ const nextTools = tools.filter((tool) => tool !== forbiddenTool);
1410
+ if (nextTools.length === tools.length) {
1411
+ return content;
1412
+ }
1413
+ return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools.join(", ")}`));
1414
+ }
1415
+ function normalizeAgentTools(content, requiredTools, forbiddenTools) {
1416
+ const required = ensureAgentTools(content, requiredTools);
1417
+ return (forbiddenTools ?? []).reduce(removeAgentTool, required);
1418
+ }
1372
1419
  function migrateLegacyHarnessFile(definition, currentContent, block) {
1373
1420
  const legacyContent = definition.legacyWholeFile?.trimEnd();
1374
1421
  if (!legacyContent) {
@@ -1456,7 +1503,7 @@ function withVcmClaudeHooks(settings) {
1456
1503
  ? hooks[definition.eventName]
1457
1504
  : [];
1458
1505
  hooks[definition.eventName] = [
1459
- ...existingMatchers.filter((entry) => !isVcmHookMatcher(entry)),
1506
+ ...existingMatchers,
1460
1507
  {
1461
1508
  ...(definition.matcher ? { matcher: definition.matcher } : {}),
1462
1509
  hooks: [
@@ -0,0 +1,9 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ export const VCM_LSP_PLUGIN_NAME = "vcm-lsp-bridge";
4
+ export const VCM_LSP_PLUGIN_DIR = fileURLToPath(new URL("../../../scripts/claude-plugins/vcm-lsp-bridge", import.meta.url));
5
+ export const VCM_LSP_PLUGIN_MANIFEST = path.join(VCM_LSP_PLUGIN_DIR, ".claude-plugin", "plugin.json");
6
+ const LSP_ROLES = new Set(["architect", "coder", "reviewer"]);
7
+ export function roleUsesLsp(role) {
8
+ return LSP_ROLES.has(role);
9
+ }
@@ -8,6 +8,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
8
8
  import { claudeTranscriptPath } from "./claude-transcript-service.js";
9
9
  import { readHarnessRevisionState } from "./harness-revision.js";
10
10
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
11
+ import { roleUsesLsp, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
11
12
  const TRANSLATOR_ROLE = "translator";
12
13
  const HARNESS_ENGINEER_ROLE = "harness-engineer";
13
14
  const TRANSLATION_DIR = ".ai/vcm/translations";
@@ -82,7 +83,7 @@ export function createSessionService(deps) {
82
83
  ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
83
84
  : undefined;
84
85
  const startCommand = {
85
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
86
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt, roleUsesLsp(role) ? [VCM_LSP_PLUGIN_DIR] : []),
86
87
  cwd: taskRepoRoot
87
88
  };
88
89
  const runtimeSessionToken = randomUUID();
@@ -101,7 +102,10 @@ export function createSessionService(deps) {
101
102
  VCM_ROLE: role,
102
103
  VCM_SESSION_ID: claudeSessionId || undefined,
103
104
  VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
104
- }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
105
+ }, modelEnvironment, {
106
+ ...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
107
+ ...(roleUsesLsp(role) ? { ENABLE_LSP_TOOL: "true" } : {})
108
+ }),
105
109
  cols: input.cols,
106
110
  rows: input.rows
107
111
  });