vibe-coding-master 0.8.10 → 0.8.12
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/dist/backend/api/message-routes.js +4 -0
- package/dist/backend/cli/install-vcm-harness.js +5 -4
- package/dist/backend/server.js +3 -0
- package/dist/backend/services/code-intelligence-service.js +3 -1
- package/dist/backend/services/gate-review-service.js +53 -10
- package/dist/backend/services/harness-feedback-service.js +21 -11
- package/dist/backend/services/harness-service.js +12 -7
- package/dist/backend/services/lsp-plugin.js +39 -0
- package/dist/backend/services/message-service.js +20 -2
- package/dist/backend/services/runtime-coordinator-service.js +1 -1
- package/dist/backend/services/runtime-recovery-service.js +5 -3
- package/dist/backend/services/session-service.js +11 -3
- package/dist/backend/services/workflow-control-service.js +110 -17
- package/dist/backend/templates/harness/architect-agent.js +2 -2
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +1 -1
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +5 -0
- package/dist-frontend/assets/{index-uWFnvFv6.js → index-DqANomt1.js} +37 -37
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/run-long-check +25 -6
|
@@ -24,6 +24,10 @@ export function registerMessageRoutes(app, deps) {
|
|
|
24
24
|
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
25
25
|
return deps.messageService.deleteMessageHistory(context);
|
|
26
26
|
});
|
|
27
|
+
app.post("/api/tasks/:taskSlug/messages/:messageId/confirm-delivered", async (request) => {
|
|
28
|
+
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
29
|
+
return deps.messageService.confirmDeliveredDispatch(context, request.params.messageId);
|
|
30
|
+
});
|
|
27
31
|
app.get("/api/tasks/:taskSlug/orchestration", async (request) => {
|
|
28
32
|
return withOpenFileLimitFallback(async () => {
|
|
29
33
|
const context = await getRouteContext(deps, request.params.taskSlug);
|
|
@@ -51,21 +51,22 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
51
51
|
".claude/skills/vcm-codex-review-gate",
|
|
52
52
|
".ai/tools/request-codex-review"
|
|
53
53
|
];
|
|
54
|
-
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
54
|
+
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
55
55
|
const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
56
56
|
const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
|
|
57
57
|
const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
|
|
58
58
|
const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; python3 "$guard"'`;
|
|
59
59
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
60
60
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
61
|
+
const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
|
|
61
62
|
const VCM_HOOK_DEFINITIONS = [
|
|
62
63
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
63
64
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
65
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
|
-
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout:
|
|
66
|
+
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
|
|
66
67
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
67
|
-
{ eventName: "StopFailure", command:
|
|
68
|
-
{ eventName: "PostCompact", command:
|
|
68
|
+
{ eventName: "StopFailure", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
|
+
{ eventName: "PostCompact", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
70
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
70
71
|
];
|
|
71
72
|
const AGENT_FRONTMATTER = {
|
package/dist/backend/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createJobGuardService } from "./services/job-guard-service.js";
|
|
|
33
33
|
import { createProjectService } from "./services/project-service.js";
|
|
34
34
|
import { createSessionRegistry } from "./runtime/session-registry.js";
|
|
35
35
|
import { createSessionService } from "./services/session-service.js";
|
|
36
|
+
import { createHarnessCodeIntelligenceDetector } from "./services/code-intelligence-service.js";
|
|
36
37
|
import { createMessageService } from "./services/message-service.js";
|
|
37
38
|
import { createRoundService } from "./services/round-service.js";
|
|
38
39
|
import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
|
|
@@ -250,6 +251,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
250
251
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
251
252
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
252
253
|
const taskWorkflowService = createTaskWorkflowService({ fs });
|
|
254
|
+
const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(fs, { runner });
|
|
253
255
|
const sessionService = createSessionService({
|
|
254
256
|
fs,
|
|
255
257
|
runtime,
|
|
@@ -259,6 +261,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
259
261
|
projectService,
|
|
260
262
|
taskService,
|
|
261
263
|
taskWorkflowService,
|
|
264
|
+
codeIntelligenceDetector,
|
|
262
265
|
codexBridgeIntegration,
|
|
263
266
|
apiUrl: options.apiUrl
|
|
264
267
|
});
|
|
@@ -64,7 +64,9 @@ export function createHarnessCodeIntelligenceDetector(fs, options = {}) {
|
|
|
64
64
|
const now = options.now ?? Date.now;
|
|
65
65
|
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
66
66
|
return {
|
|
67
|
-
async detect(repoRoot) {
|
|
67
|
+
async detect(repoRoot, detectOptions) {
|
|
68
|
+
if (detectOptions?.refresh)
|
|
69
|
+
probeCache.clear();
|
|
68
70
|
return detectCodeIntelligence(fs, repoRoot, options, async (definition, executablePath) => {
|
|
69
71
|
const cacheKey = `${executablePath}\u0000${definition.probeArgs.join("\u0000")}`;
|
|
70
72
|
const cached = probeCache.get(cacheKey);
|
|
@@ -148,7 +148,15 @@ export function createGateReviewService(deps) {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
if (gate === "code-diff") {
|
|
151
|
-
|
|
151
|
+
let dirtyStatus;
|
|
152
|
+
try {
|
|
153
|
+
dirtyStatus = await getDirtyCodeDiffStatus(deps.runner, context.taskRepoRoot);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = errorMessage(error);
|
|
157
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message);
|
|
158
|
+
return { status: "failed_to_start", gate, record: index.gates[gate], message };
|
|
159
|
+
}
|
|
152
160
|
if (dirtyStatus) {
|
|
153
161
|
const message = `code-diff requires committed inputs; commit or clean these changes first: ${dirtyStatus}`;
|
|
154
162
|
index = applyGateState(index, gate, {
|
|
@@ -319,9 +327,17 @@ export function createGateReviewService(deps) {
|
|
|
319
327
|
};
|
|
320
328
|
}
|
|
321
329
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
let codeDiffInput;
|
|
331
|
+
if (gate === "code-diff") {
|
|
332
|
+
try {
|
|
333
|
+
codeDiffInput = await resolveCodeDiffInput(deps, context, record);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const message = errorMessage(error);
|
|
337
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message);
|
|
338
|
+
return { status: "failed_to_start", gate, record: index.gates[gate], message };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
325
341
|
if (gate === "code-diff" && (!codeDiffInput || codeDiffInput.commits.length === 0)) {
|
|
326
342
|
index = applyGateState(index, gate, {
|
|
327
343
|
status: "not_required",
|
|
@@ -1157,7 +1173,7 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1157
1173
|
"merge-base",
|
|
1158
1174
|
"HEAD",
|
|
1159
1175
|
rootBranch
|
|
1160
|
-
])).trim();
|
|
1176
|
+
], { expectedFailure: "no-common-ancestor" })).trim();
|
|
1161
1177
|
if (mergeBase
|
|
1162
1178
|
&& mergeBase !== headCommit
|
|
1163
1179
|
&& await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
|
|
@@ -1169,13 +1185,13 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1169
1185
|
"--abbrev-ref",
|
|
1170
1186
|
"--symbolic-full-name",
|
|
1171
1187
|
"@{upstream}"
|
|
1172
|
-
])).trim();
|
|
1188
|
+
], { expectedFailure: "missing-upstream" })).trim();
|
|
1173
1189
|
if (upstream) {
|
|
1174
1190
|
const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1175
1191
|
"merge-base",
|
|
1176
1192
|
"HEAD",
|
|
1177
1193
|
upstream
|
|
1178
|
-
])).trim();
|
|
1194
|
+
], { expectedFailure: "no-common-ancestor" })).trim();
|
|
1179
1195
|
if (mergeBase
|
|
1180
1196
|
&& mergeBase !== headCommit
|
|
1181
1197
|
&& await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
|
|
@@ -1185,7 +1201,9 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1185
1201
|
return headCommit;
|
|
1186
1202
|
}
|
|
1187
1203
|
async function isAncestor(runner, cwd, ancestor, descendant) {
|
|
1188
|
-
const result = await runner
|
|
1204
|
+
const result = await readGateGit(runner, cwd, ["merge-base", "--is-ancestor", ancestor, descendant], {
|
|
1205
|
+
expectedFailure: "not-ancestor"
|
|
1206
|
+
});
|
|
1189
1207
|
return result.exitCode === 0;
|
|
1190
1208
|
}
|
|
1191
1209
|
async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDiffSources) {
|
|
@@ -1395,10 +1413,35 @@ async function readCodeDiffSourceArtifactError(fs, taskRepoRoot, source) {
|
|
|
1395
1413
|
}
|
|
1396
1414
|
return undefined;
|
|
1397
1415
|
}
|
|
1398
|
-
async function commandStdout(runner, cwd, args) {
|
|
1399
|
-
const result = await runner
|
|
1416
|
+
async function commandStdout(runner, cwd, args, options = {}) {
|
|
1417
|
+
const result = await readGateGit(runner, cwd, args, options);
|
|
1400
1418
|
return result.exitCode === 0 ? result.stdout : "";
|
|
1401
1419
|
}
|
|
1420
|
+
async function readGateGit(runner, cwd, args, options = {}) {
|
|
1421
|
+
const retryDelays = [500, 1000, 2000];
|
|
1422
|
+
for (let attempt = 0;; attempt += 1) {
|
|
1423
|
+
const result = await runner.run("git", ["-c", `safe.directory=${path.resolve(cwd)}`, ...args], { cwd });
|
|
1424
|
+
if (result.exitCode === 0 || isExpectedGitFailure(result, options.expectedFailure))
|
|
1425
|
+
return result;
|
|
1426
|
+
if (attempt < retryDelays.length) {
|
|
1427
|
+
await delay(retryDelays[attempt]);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "Git did not provide an error message.";
|
|
1431
|
+
throw new VcmError({
|
|
1432
|
+
code: "GATE_REVIEW_GIT_COMMAND_FAILED",
|
|
1433
|
+
message: `Git ${args.join(" ")} failed in ${cwd} (exit ${result.exitCode}): ${detail}`,
|
|
1434
|
+
statusCode: 409
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function isExpectedGitFailure(result, expected) {
|
|
1439
|
+
if (expected === "not-ancestor" || expected === "no-common-ancestor") {
|
|
1440
|
+
return result.exitCode === 1 && !result.stderr.trim() && !result.stdout.trim();
|
|
1441
|
+
}
|
|
1442
|
+
return expected === "missing-upstream" && result.exitCode === 128
|
|
1443
|
+
&& /^fatal: (no upstream configured for branch |HEAD does not point to a branch)/.test(result.stderr.trim());
|
|
1444
|
+
}
|
|
1402
1445
|
function splitLines(value) {
|
|
1403
1446
|
return value
|
|
1404
1447
|
.split(/\r?\n/)
|
|
@@ -52,15 +52,6 @@ export function createHarnessFeedbackService(deps) {
|
|
|
52
52
|
statusCode: 409
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
56
|
-
if (existingMarker && existingMarker.status !== "failed") {
|
|
57
|
-
throw new VcmError({
|
|
58
|
-
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
59
|
-
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
60
|
-
statusCode: 409,
|
|
61
|
-
hint: "Review the existing retrospective result instead of starting another retrospective for the same task."
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
55
|
const finalAcceptancePath = path.posix.join(input.handoffDir, "final-acceptance.md");
|
|
65
56
|
const finalAcceptanceAbsolutePath = resolveRepoPath(input.taskRepoRoot, finalAcceptancePath);
|
|
66
57
|
const finalAcceptanceContent = await readAbsoluteOptionalText(finalAcceptanceAbsolutePath);
|
|
@@ -78,10 +69,25 @@ export function createHarnessFeedbackService(deps) {
|
|
|
78
69
|
hint: `${finalAcceptancePath} must pass the final-acceptance artifact check before Task Harness Retrospective can start.`
|
|
79
70
|
});
|
|
80
71
|
}
|
|
72
|
+
const finalAcceptanceHash = `sha256:${sha256(finalAcceptanceContent)}`;
|
|
73
|
+
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
74
|
+
if (existingMarker && (existingMarker.status === "running"
|
|
75
|
+
|| existingMarker.status === "triggered"
|
|
76
|
+
|| existingMarker.status === "waiting-docs"
|
|
77
|
+
|| (existingMarker.finalAcceptanceHash === finalAcceptanceHash && existingMarker.status !== "failed"))) {
|
|
78
|
+
throw new VcmError({
|
|
79
|
+
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
80
|
+
message: `Task Harness Retrospective has already been triggered for this acceptance or is still active: ${taskSlug}`,
|
|
81
|
+
statusCode: 409,
|
|
82
|
+
hint: "Wait for the active retrospective, or review its result before retrying the same acceptance."
|
|
83
|
+
});
|
|
84
|
+
}
|
|
81
85
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
82
86
|
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
83
87
|
const timestamp = now();
|
|
84
|
-
const analysisPath =
|
|
88
|
+
const analysisPath = existingMarker && existingMarker.finalAcceptanceHash !== finalAcceptanceHash
|
|
89
|
+
? `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.${finalAcceptanceHash.slice(7)}.md`
|
|
90
|
+
: existingMarker?.analysisPath ?? `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
85
91
|
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
86
92
|
const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
|
|
87
93
|
const marker = {
|
|
@@ -90,13 +96,17 @@ export function createHarnessFeedbackService(deps) {
|
|
|
90
96
|
trigger: input.trigger,
|
|
91
97
|
status: "running",
|
|
92
98
|
analysisPath,
|
|
93
|
-
finalAcceptanceHash
|
|
99
|
+
finalAcceptanceHash,
|
|
94
100
|
pendingFeedbackPaths: pendingFeedback.map((item) => item.path),
|
|
95
101
|
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
96
102
|
createdAt: timestamp,
|
|
97
103
|
updatedAt: timestamp
|
|
98
104
|
};
|
|
99
105
|
try {
|
|
106
|
+
if (existingMarker && existingMarker.finalAcceptanceHash !== finalAcceptanceHash) {
|
|
107
|
+
const archivePath = resolveRepoPath(repoRoot, `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.${existingMarker.finalAcceptanceHash.slice(7)}.json`);
|
|
108
|
+
await deps.fs.writeJsonAtomic(archivePath, existingMarker);
|
|
109
|
+
}
|
|
100
110
|
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
101
111
|
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
102
112
|
}
|
|
@@ -37,6 +37,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
|
37
37
|
import { VcmError } from "../errors.js";
|
|
38
38
|
import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
|
|
39
39
|
import { createHarnessCodeIntelligenceDetector } from "./code-intelligence-service.js";
|
|
40
|
+
import { lspUnavailableWarning } from "./lsp-plugin.js";
|
|
40
41
|
const execFileAsync = promisify(execFile);
|
|
41
42
|
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
42
43
|
const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
@@ -51,21 +52,22 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
51
52
|
".claude/skills/vcm-codex-review-gate",
|
|
52
53
|
".ai/tools/request-codex-review"
|
|
53
54
|
];
|
|
54
|
-
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
55
|
+
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
55
56
|
const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
56
57
|
const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
|
|
57
58
|
const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
|
|
58
59
|
const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; python3 "$guard"'`;
|
|
59
60
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
60
61
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
62
|
+
const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
|
|
61
63
|
const VCM_HOOK_DEFINITIONS = [
|
|
62
64
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
63
65
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
66
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
|
-
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout:
|
|
67
|
+
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
|
|
66
68
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
67
|
-
{ eventName: "StopFailure", command:
|
|
68
|
-
{ eventName: "PostCompact", command:
|
|
69
|
+
{ eventName: "StopFailure", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
70
|
+
{ eventName: "PostCompact", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
71
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
70
72
|
];
|
|
71
73
|
const HARNESS_FILES = [
|
|
@@ -1405,9 +1407,12 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
|
|
|
1405
1407
|
needsApply: plannedChanges.length > 0,
|
|
1406
1408
|
plannedChanges,
|
|
1407
1409
|
codeIntelligence,
|
|
1408
|
-
warnings:
|
|
1409
|
-
? ["Review and commit VCM Harness changes before starting long-running work."]
|
|
1410
|
-
|
|
1410
|
+
warnings: [
|
|
1411
|
+
...(plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []),
|
|
1412
|
+
...((codeIntelligence?.languages ?? [])
|
|
1413
|
+
.filter((language) => language.state !== "server_runnable")
|
|
1414
|
+
.map(lspUnavailableWarning))
|
|
1415
|
+
]
|
|
1411
1416
|
};
|
|
1412
1417
|
}
|
|
1413
1418
|
async function readHarnessRevisionValue(fs, repoRoot) {
|
|
@@ -4,3 +4,42 @@ 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 async function prepareTaskLspPlugin(fs, taskRepoRoot, status) {
|
|
8
|
+
const runnable = status.languages.filter((language) => language.state === "server_runnable");
|
|
9
|
+
if (runnable.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const manifest = await fs.readJson(VCM_LSP_PLUGIN_MANIFEST);
|
|
12
|
+
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 pluginDir = path.join(taskRepoRoot, ".ai/vcm/lsp-plugin");
|
|
15
|
+
const pluginManifestPath = path.join(pluginDir, ".claude-plugin/plugin.json");
|
|
16
|
+
await fs.ensureDir(path.dirname(pluginManifestPath));
|
|
17
|
+
await fs.writeJsonAtomic(pluginManifestPath, { ...manifest, lspServers });
|
|
18
|
+
return [pluginDir];
|
|
19
|
+
}
|
|
20
|
+
export function lspUnavailableWarning(language) {
|
|
21
|
+
const installCommands = {
|
|
22
|
+
"rust-analyzer": "rustup component add rust-analyzer",
|
|
23
|
+
"typescript-language-server": "npm install -g typescript typescript-language-server",
|
|
24
|
+
"pyright-langserver": "npm install -g pyright",
|
|
25
|
+
"gopls": "go install golang.org/x/tools/gopls@latest"
|
|
26
|
+
};
|
|
27
|
+
const recovery = language.state === "plugin_missing"
|
|
28
|
+
? "Refresh the VCM Harness to restore the LSP plugin."
|
|
29
|
+
: `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.`;
|
|
31
|
+
}
|
|
32
|
+
export function renderArchitectLspNotice(status) {
|
|
33
|
+
if (status.languages.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const available = status.languages.filter((language) => language.state === "server_runnable");
|
|
36
|
+
const unavailable = status.languages.filter((language) => language.state !== "server_runnable");
|
|
37
|
+
return [
|
|
38
|
+
"[VCM Architect LSP Availability]",
|
|
39
|
+
`Registered language servers: ${available.map((language) => `${language.label} (${language.serverCommand})`).join(", ") || "none"}.`,
|
|
40
|
+
...unavailable.map(lspUnavailableWarning),
|
|
41
|
+
...(unavailable.length > 0 ? [
|
|
42
|
+
"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."
|
|
43
|
+
] : [])
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
@@ -11,7 +11,7 @@ const DEFAULT_PRE_DISPATCH_SWITCH_DELAY_MS = 500;
|
|
|
11
11
|
const DEFAULT_AUTO_DISPATCH_ENTER_DELAY_MS = 500;
|
|
12
12
|
const DEFAULT_DISPATCH_CONFIRMATION_RETRY_DELAYS_MS = [1500, 3000];
|
|
13
13
|
const DEFAULT_DISPATCH_CONFIRMATION_FAILURE_DELAY_MS = 3000;
|
|
14
|
-
const DISPATCH_NOT_CONFIRMED_REASON = "Auto orchestration pasted the message, but Claude Code did not confirm submission.
|
|
14
|
+
const DISPATCH_NOT_CONFIRMED_REASON = "Auto orchestration pasted the message, but Claude Code did not confirm submission. Inspect the target terminal: press Enter if the prompt was not submitted, or use Confirm Executed if the role already executed it.";
|
|
15
15
|
const INVALID_ROUTE_REASON = "Invalid route: non-PM roles must route through project-manager.";
|
|
16
16
|
export function createMessageService(deps) {
|
|
17
17
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
@@ -193,6 +193,8 @@ export function createMessageService(deps) {
|
|
|
193
193
|
if (!message) {
|
|
194
194
|
return undefined;
|
|
195
195
|
}
|
|
196
|
+
if (message.acceptedAt)
|
|
197
|
+
return message;
|
|
196
198
|
const accepted = {
|
|
197
199
|
...message,
|
|
198
200
|
acceptedAt: timestamp,
|
|
@@ -206,6 +208,22 @@ export function createMessageService(deps) {
|
|
|
206
208
|
return accepted;
|
|
207
209
|
});
|
|
208
210
|
},
|
|
211
|
+
async confirmDeliveredDispatch(input, messageId) {
|
|
212
|
+
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
213
|
+
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
214
|
+
const message = messages.find((entry) => entry.id === messageId);
|
|
215
|
+
if (!message || message.fromRole !== PM_ROLE || !message.deliveredAt || !deps.workflowControlService) {
|
|
216
|
+
throw new VcmError({ code: "MESSAGE_NOT_DELIVERED", message: `Message ${messageId} is not a delivered PM workflow dispatch.`, statusCode: 409 });
|
|
217
|
+
}
|
|
218
|
+
if (message.acceptedAt)
|
|
219
|
+
return message;
|
|
220
|
+
await deps.workflowControlService.confirmDispatch(toWorkflowContext(input), messageId, "manual");
|
|
221
|
+
const accepted = { ...message, acceptedAt: now(), confirmationSource: "manual", failureReason: undefined };
|
|
222
|
+
await appendMessageSnapshot(deps.fs, input, accepted);
|
|
223
|
+
await clearRouteFileIfStillMatchesMessage(deps.fs, input, accepted);
|
|
224
|
+
return accepted;
|
|
225
|
+
});
|
|
226
|
+
},
|
|
209
227
|
async markAllDone(input) {
|
|
210
228
|
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
211
229
|
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
@@ -353,7 +371,7 @@ export function createMessageService(deps) {
|
|
|
353
371
|
return;
|
|
354
372
|
}
|
|
355
373
|
if (current.fromRole === PM_ROLE) {
|
|
356
|
-
await deps.workflowControlService?.
|
|
374
|
+
await deps.workflowControlService?.markDispatchUnconfirmed(toWorkflowContext(input), messageId, failureReason);
|
|
357
375
|
}
|
|
358
376
|
await appendMessageSnapshot(deps.fs, input, {
|
|
359
377
|
...current,
|
|
@@ -192,7 +192,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
|
-
if (error instanceof VcmError &&
|
|
195
|
+
if (error instanceof VcmError && EXPECTED_AUTO_RETROSPECTIVE_SKIP_CODES.has(error.code)) {
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
throw error;
|
|
@@ -193,10 +193,12 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
193
193
|
if (message.dispatchingAt && !message.acceptedAt) {
|
|
194
194
|
const next = {
|
|
195
195
|
...message,
|
|
196
|
-
failureReason:
|
|
196
|
+
failureReason: message.deliveredAt
|
|
197
|
+
? "VCM restarted before this delivered dispatch was confirmed. Inspect the target session before retrying."
|
|
198
|
+
: "VCM restarted before this dispatch was delivered. Resend the message if it is still needed."
|
|
197
199
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
if (!message.deliveredAt)
|
|
201
|
+
delete next.dispatchingAt;
|
|
200
202
|
changed = true;
|
|
201
203
|
return next;
|
|
202
204
|
}
|
|
@@ -8,7 +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
|
+
import { roleUsesLsp, prepareTaskLspPlugin, renderArchitectLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
|
|
12
12
|
const TRANSLATOR_ROLE = "translator";
|
|
13
13
|
const HARNESS_ENGINEER_ROLE = "harness-engineer";
|
|
14
14
|
const TRANSLATION_DIR = ".ai/vcm/translations";
|
|
@@ -82,8 +82,16 @@ export function createSessionService(deps) {
|
|
|
82
82
|
: resumeClaudeSessionId
|
|
83
83
|
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
84
84
|
: undefined;
|
|
85
|
+
const lspStatus = roleUsesLsp(role) && deps.codeIntelligenceDetector
|
|
86
|
+
? await deps.codeIntelligenceDetector.detect(taskRepoRoot, { refresh: true })
|
|
87
|
+
: undefined;
|
|
88
|
+
const lspPluginDirs = roleUsesLsp(role)
|
|
89
|
+
? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
|
|
90
|
+
: [];
|
|
91
|
+
const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderArchitectLspNotice(lspStatus)]
|
|
92
|
+
.filter(Boolean).join("\n\n") || undefined;
|
|
85
93
|
const startCommand = {
|
|
86
|
-
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride,
|
|
94
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt, lspPluginDirs),
|
|
87
95
|
cwd: taskRepoRoot
|
|
88
96
|
};
|
|
89
97
|
const runtimeSessionToken = randomUUID();
|
|
@@ -104,7 +112,7 @@ export function createSessionService(deps) {
|
|
|
104
112
|
VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
|
|
105
113
|
}, modelEnvironment, {
|
|
106
114
|
...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
|
|
107
|
-
...(
|
|
115
|
+
...(lspPluginDirs.length > 0
|
|
108
116
|
? {
|
|
109
117
|
ENABLE_LSP_TOOL: "true",
|
|
110
118
|
ENABLE_TOOL_SEARCH: "false"
|