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 +11 -0
- package/dist/backend/adapters/claude-adapter.js +3 -2
- package/dist/backend/adapters/command-runner.js +1 -0
- package/dist/backend/cli/install-vcm-harness.js +42 -5
- package/dist/backend/server.js +1 -0
- package/dist/backend/services/code-intelligence-service.js +253 -0
- package/dist/backend/services/harness-service.js +63 -16
- package/dist/backend/services/lsp-plugin.js +9 -0
- package/dist/backend/services/session-service.js +6 -2
- package/dist/backend/templates/harness/architect-agent.js +28 -2
- package/dist/backend/templates/harness/coder-agent.js +8 -0
- package/dist/backend/templates/harness/gate-review.js +12 -1
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +1 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +25 -0
- package/dist-frontend/assets/{index-DN9kwoOt.css → index-CXSOe-NN.css} +1 -1
- package/dist-frontend/assets/{index-DhLRhgSV.js → index-VillnFRo.js} +34 -34
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +72 -0
- package/scripts/harness-tools/vcm-bash-guard +52 -17
- package/scripts/verify-package.mjs +1 -0
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 = ["--
|
|
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);
|
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
661
|
-
nextContent = ensureAgentTool(nextContent,
|
|
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) {
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
});
|