vibe-coding-master 0.9.5 → 0.9.7
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 +30 -18
- package/dist/backend/adapters/claude-adapter.js +4 -5
- package/dist/backend/adapters/command-runner.js +7 -2
- package/dist/backend/adapters/git-adapter.js +83 -5
- package/dist/backend/api/claude-hook-routes.js +5 -0
- package/dist/backend/role-tool-policy.js +11 -10
- package/dist/backend/runtime/node-pty-runtime.js +39 -8
- package/dist/backend/server.js +35 -7
- package/dist/backend/services/environment-check.js +245 -0
- package/dist/backend/services/gate-review-service.js +6 -2
- package/dist/backend/services/harness-service.js +17 -23
- package/dist/backend/services/message-service.js +13 -1
- package/dist/backend/services/no-proxy-environment.js +29 -0
- package/dist/backend/services/project-service.js +50 -2
- package/dist/backend/services/session-service.js +11 -19
- package/dist/backend/services/workflow-control-service.js +9 -3
- package/dist/backend/templates/harness/architect-agent.js +8 -11
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +29 -27
- package/dist/backend/templates/harness/project-manager-agent.js +2 -2
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +13 -9
- package/dist-frontend/assets/index-C6tXxGlk.css +32 -0
- package/dist-frontend/assets/{index-CjM9OqZh.js → index-Ct8NwH3l.js} +35 -35
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/verify-package.mjs +0 -2
- package/dist/backend/services/code-intelligence-service.js +0 -255
- package/dist/backend/services/lsp-plugin.js +0 -64
- package/dist-frontend/assets/index-Bs4g_1yF.css +0 -32
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +0 -88
- package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +0 -273
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { renderVcmHookProbeCommand } from "./harness-service.js";
|
|
3
|
+
import { buildVcmNoProxyEnvironment } from "./no-proxy-environment.js";
|
|
4
|
+
// Oldest Git that supports every command VCM runs (`git branch --show-current`).
|
|
5
|
+
export const MIN_GIT_VERSION = [2, 22, 0];
|
|
6
|
+
// Matches package.json engines; harness tools run with the `node` on PATH.
|
|
7
|
+
export const MIN_NODE_MAJOR = 20;
|
|
8
|
+
const CHECK_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
// A failed check is retried sooner so a fixed environment clears the warning.
|
|
10
|
+
const FAILED_CHECK_TTL_MS = 30 * 1000;
|
|
11
|
+
const COMMAND_TIMEOUT_MS = 10 * 1000;
|
|
12
|
+
// The hook pipeline retries curl twice with a 2 s limit per attempt.
|
|
13
|
+
const HOOK_PROBE_TIMEOUT_MS = 15 * 1000;
|
|
14
|
+
const HOOK_PROBE_SCOPE = "__vcm_hook_probe__";
|
|
15
|
+
const HOOK_PROBE_ROLE = "hook-probe";
|
|
16
|
+
export function createEnvironmentCheck(deps) {
|
|
17
|
+
const now = deps.now ?? Date.now;
|
|
18
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
19
|
+
const hookProbeTimeoutMs = deps.hookProbeTimeoutMs ?? HOOK_PROBE_TIMEOUT_MS;
|
|
20
|
+
const pendingProbes = new Map();
|
|
21
|
+
let cached;
|
|
22
|
+
let inFlight;
|
|
23
|
+
const repositoryCache = new Map();
|
|
24
|
+
const repositoryInFlight = new Map();
|
|
25
|
+
return {
|
|
26
|
+
async getWarnings(options = {}) {
|
|
27
|
+
if (!cached || options.refresh) {
|
|
28
|
+
return runOnce();
|
|
29
|
+
}
|
|
30
|
+
const ttl = cached.warnings.length > 0 ? FAILED_CHECK_TTL_MS : CHECK_CACHE_TTL_MS;
|
|
31
|
+
if (now() - cached.checkedAt >= ttl) {
|
|
32
|
+
void runOnce();
|
|
33
|
+
}
|
|
34
|
+
return cached.warnings;
|
|
35
|
+
},
|
|
36
|
+
async getRepositoryWarnings(repoRoot, options = {}) {
|
|
37
|
+
const current = repositoryCache.get(repoRoot);
|
|
38
|
+
if (!current || options.refresh) {
|
|
39
|
+
return runRepositoryOnce(repoRoot);
|
|
40
|
+
}
|
|
41
|
+
const ttl = current.warnings.length > 0 ? FAILED_CHECK_TTL_MS : CHECK_CACHE_TTL_MS;
|
|
42
|
+
if (now() - current.checkedAt >= ttl) {
|
|
43
|
+
void runRepositoryOnce(repoRoot);
|
|
44
|
+
}
|
|
45
|
+
return current.warnings;
|
|
46
|
+
},
|
|
47
|
+
probeHookDelivery,
|
|
48
|
+
receiveHookProbe(probeToken) {
|
|
49
|
+
if (typeof probeToken !== "string") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const resolve = pendingProbes.get(probeToken);
|
|
53
|
+
if (!resolve) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
pendingProbes.delete(probeToken);
|
|
57
|
+
resolve();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function runOnce() {
|
|
62
|
+
if (inFlight) {
|
|
63
|
+
return inFlight;
|
|
64
|
+
}
|
|
65
|
+
inFlight = (async () => {
|
|
66
|
+
try {
|
|
67
|
+
const warnings = await runChecks();
|
|
68
|
+
cached = { warnings, checkedAt: now() };
|
|
69
|
+
return warnings;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
inFlight = undefined;
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
return inFlight;
|
|
76
|
+
}
|
|
77
|
+
function runRepositoryOnce(repoRoot) {
|
|
78
|
+
const running = repositoryInFlight.get(repoRoot);
|
|
79
|
+
if (running) {
|
|
80
|
+
return running;
|
|
81
|
+
}
|
|
82
|
+
const check = (async () => {
|
|
83
|
+
try {
|
|
84
|
+
const warnings = await runRepositoryChecks(repoRoot);
|
|
85
|
+
repositoryCache.set(repoRoot, { warnings, checkedAt: now() });
|
|
86
|
+
return warnings;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
repositoryInFlight.delete(repoRoot);
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
repositoryInFlight.set(repoRoot, check);
|
|
93
|
+
return check;
|
|
94
|
+
}
|
|
95
|
+
async function runRepositoryChecks(repoRoot) {
|
|
96
|
+
if (!deps.git) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const access = await deps.git.checkRemoteAccess(repoRoot);
|
|
100
|
+
if (access.status !== "failed") {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
`Git cannot reach remote "${access.remote}": ${trimPeriod(access.detail)}. Creating a task and Pull fetch the upstream branch first, so both will fail. VCM runs Git without credential prompts; check the remote URL and set up credentials for it in the environment that runs VCM, such as an SSH key loaded in ssh-agent or a Git credential helper.`
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
async function runChecks() {
|
|
108
|
+
const [git, sh, node, curl, gh] = await Promise.all([checkGit(), checkSh(), checkNode(), checkCurl(), checkGh()]);
|
|
109
|
+
const warnings = [git, sh, node, curl, gh].filter((warning) => Boolean(warning));
|
|
110
|
+
// A missing program already explains a failed probe; only probe when the
|
|
111
|
+
// pipeline's programs are present, to catch PATH, proxy, or network faults.
|
|
112
|
+
if (!sh && !node && !curl) {
|
|
113
|
+
const hookWarning = await probeHookDelivery();
|
|
114
|
+
if (hookWarning) {
|
|
115
|
+
warnings.push(hookWarning);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return warnings;
|
|
119
|
+
}
|
|
120
|
+
async function checkGit() {
|
|
121
|
+
const result = await runCommand("git", ["--version"]);
|
|
122
|
+
if (result.exitCode !== 0) {
|
|
123
|
+
return `Git is not available: ${firstLine(result.stderr)}. VCM needs Git ${formatVersion(MIN_GIT_VERSION)} or newer on PATH to read repositories and create task worktrees.`;
|
|
124
|
+
}
|
|
125
|
+
const version = parseVersion(result.stdout);
|
|
126
|
+
if (!version) {
|
|
127
|
+
return `Git version could not be parsed from "${firstLine(result.stdout)}". VCM needs Git ${formatVersion(MIN_GIT_VERSION)} or newer.`;
|
|
128
|
+
}
|
|
129
|
+
if (compareVersions(version, MIN_GIT_VERSION) < 0) {
|
|
130
|
+
return `Git ${formatVersion(version)} is older than ${formatVersion(MIN_GIT_VERSION)}, the oldest version VCM supports. Upgrade Git.`;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
async function checkSh() {
|
|
135
|
+
const result = await runCommand("sh", ["-c", "exit 0"]);
|
|
136
|
+
return result.exitCode === 0
|
|
137
|
+
? undefined
|
|
138
|
+
: `sh is not available: ${firstLine(result.stderr)}. Claude Code runs every VCM role hook through sh.`;
|
|
139
|
+
}
|
|
140
|
+
async function checkNode() {
|
|
141
|
+
const result = await runCommand("node", ["--version"]);
|
|
142
|
+
if (result.exitCode !== 0) {
|
|
143
|
+
return `node is not available on the PATH role sessions inherit: ${firstLine(result.stderr)}. VCM role hooks, Bash and subagent guards, and .ai/tools scripts run with node; add the Node.js bin directory to PATH before starting VCM.`;
|
|
144
|
+
}
|
|
145
|
+
const version = parseVersion(result.stdout);
|
|
146
|
+
if (version && version[0] < MIN_NODE_MAJOR) {
|
|
147
|
+
return `The node on PATH is ${formatVersion(version)}, but VCM role hooks and .ai/tools scripts need Node.js ${MIN_NODE_MAJOR} or newer. Put a newer Node.js first on PATH before starting VCM.`;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
async function checkCurl() {
|
|
152
|
+
const result = await runCommand("curl", ["--version"]);
|
|
153
|
+
return result.exitCode === 0
|
|
154
|
+
? undefined
|
|
155
|
+
: `curl is not available: ${firstLine(result.stderr)}. VCM role hooks use curl to report Claude Code events; without it VCM cannot see role turns start or finish. Install curl.`;
|
|
156
|
+
}
|
|
157
|
+
// VCM never runs gh, so a missing gh is not a problem; an installed but
|
|
158
|
+
// logged-out gh breaks roles that use it for pull requests or issues.
|
|
159
|
+
async function checkGh() {
|
|
160
|
+
const version = await runCommand("gh", ["--version"]);
|
|
161
|
+
if (version.exitCode !== 0) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const status = await deps.runner.run("gh", ["auth", "status"], {
|
|
165
|
+
env: { ...baseEnv, GH_PROMPT_DISABLED: "1", NO_COLOR: "1" },
|
|
166
|
+
timeoutMs: COMMAND_TIMEOUT_MS
|
|
167
|
+
});
|
|
168
|
+
if (status.exitCode === 0) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const detail = firstLine(status.stderr) || firstLine(status.stdout) || `gh auth status exited with ${status.exitCode}`;
|
|
172
|
+
return `GitHub CLI (gh) is installed but not logged in: ${detail}. VCM does not run gh itself, but roles that use gh for pull requests or issues will fail. Run gh auth login in the environment that runs VCM.`;
|
|
173
|
+
}
|
|
174
|
+
async function probeHookDelivery() {
|
|
175
|
+
if (!deps.apiUrl) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
const probeToken = randomUUID();
|
|
179
|
+
let received = false;
|
|
180
|
+
const arrived = new Promise((resolve) => {
|
|
181
|
+
pendingProbes.set(probeToken, () => {
|
|
182
|
+
received = true;
|
|
183
|
+
resolve();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
const run = deps.runner.run("sh", ["-c", renderVcmHookProbeCommand()], {
|
|
188
|
+
env: {
|
|
189
|
+
...baseEnv,
|
|
190
|
+
...buildVcmNoProxyEnvironment(deps.apiUrl, {}, baseEnv),
|
|
191
|
+
VCM_API_URL: deps.apiUrl,
|
|
192
|
+
VCM_TASK_SLUG: HOOK_PROBE_SCOPE,
|
|
193
|
+
VCM_ROLE: HOOK_PROBE_ROLE,
|
|
194
|
+
VCM_RUNTIME_SESSION_TOKEN: probeToken
|
|
195
|
+
},
|
|
196
|
+
input: "{}",
|
|
197
|
+
timeoutMs: hookProbeTimeoutMs
|
|
198
|
+
});
|
|
199
|
+
// The endpoint records the probe before curl exits, so the command's
|
|
200
|
+
// completion bounds the wait.
|
|
201
|
+
const result = await Promise.race([run, arrived.then(() => undefined)]);
|
|
202
|
+
if (received) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const detail = firstLine(result?.stderr ?? "") || "the request did not arrive";
|
|
206
|
+
return `VCM role hooks cannot reach VCM at ${deps.apiUrl}: ${detail}. Role sessions will not report turns to VCM, so dispatches stay unconfirmed. Check that sh, node, and curl are on PATH and that any HTTP proxy excludes ${readHostname(deps.apiUrl)} through NO_PROXY.`;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
pendingProbes.delete(probeToken);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function runCommand(command, args) {
|
|
213
|
+
return deps.runner.run(command, args, { env: baseEnv, timeoutMs: COMMAND_TIMEOUT_MS });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function parseVersion(output) {
|
|
217
|
+
const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(output);
|
|
218
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)] : undefined;
|
|
219
|
+
}
|
|
220
|
+
function compareVersions(left, right) {
|
|
221
|
+
for (let index = 0; index < 3; index += 1) {
|
|
222
|
+
if (left[index] !== right[index]) {
|
|
223
|
+
return left[index] - right[index];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
function formatVersion(version) {
|
|
229
|
+
return version.join(".");
|
|
230
|
+
}
|
|
231
|
+
// Error details end the sentence they are quoted in.
|
|
232
|
+
function firstLine(value) {
|
|
233
|
+
return trimPeriod(value.trim().split(/\r?\n/)[0]?.trim() ?? "");
|
|
234
|
+
}
|
|
235
|
+
function trimPeriod(value) {
|
|
236
|
+
return value.replace(/\.+$/, "");
|
|
237
|
+
}
|
|
238
|
+
function readHostname(url) {
|
|
239
|
+
try {
|
|
240
|
+
return new URL(url).hostname;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return url;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1568,10 +1568,14 @@ async function captureGateInputSnapshots(fs, taskRepoRoot, requestId, sourcePath
|
|
|
1568
1568
|
return snapshots;
|
|
1569
1569
|
}
|
|
1570
1570
|
// The reviewer judges authorization and scope against the flow's PM route
|
|
1571
|
-
// messages, captured like any other immutable Gate input.
|
|
1571
|
+
// messages, captured like any other immutable Gate input. Tester commits are
|
|
1572
|
+
// part of every reviewed range, so routes to Tester are always included.
|
|
1572
1573
|
async function captureRouteMessageSnapshot(deps, context, requestId, codeDiffSources) {
|
|
1573
1574
|
const sourcePath = path.posix.join(context.stateRoot, "messages", `${context.taskSlug}.jsonl`);
|
|
1574
|
-
const roles = new Set(
|
|
1575
|
+
const roles = new Set([
|
|
1576
|
+
...codeDiffSources.map((source) => source === "coder" ? "coder" : "architect"),
|
|
1577
|
+
"tester"
|
|
1578
|
+
]);
|
|
1575
1579
|
const flow = await collectFlowRouteMessages(deps.fs, deps.workflowControlService, context, roles);
|
|
1576
1580
|
if (flow.messages.length === 0) {
|
|
1577
1581
|
return { sourcePath, status: "missing" };
|
|
@@ -38,8 +38,6 @@ import { renderResolveDurableDocAssignmentTool } from "../templates/harness/reso
|
|
|
38
38
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
39
39
|
import { VcmError } from "../errors.js";
|
|
40
40
|
import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
|
|
41
|
-
import { createHarnessCodeIntelligenceDetector } from "./code-intelligence-service.js";
|
|
42
|
-
import { lspUnavailableWarning } from "./lsp-plugin.js";
|
|
43
41
|
const execFileAsync = promisify(execFile);
|
|
44
42
|
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
45
43
|
const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
@@ -55,6 +53,17 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
55
53
|
".ai/tools/request-codex-review"
|
|
56
54
|
];
|
|
57
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'`;
|
|
56
|
+
// The hook self-test runs the installed hook pipeline unchanged except for its
|
|
57
|
+
// endpoint, so a passing probe proves sh, node, curl, PATH, and proxy settings
|
|
58
|
+
// let role hooks reach VCM.
|
|
59
|
+
export const VCM_HOOK_PROBE_PATH = "/api/hooks/probe";
|
|
60
|
+
export function renderVcmHookProbeCommand() {
|
|
61
|
+
const endpoint = '"${VCM_API_URL}/api/hooks/claude-code"';
|
|
62
|
+
if (!VCM_HOOK_COMMAND.includes(endpoint)) {
|
|
63
|
+
throw new Error("VCM hook command no longer posts to /api/hooks/claude-code.");
|
|
64
|
+
}
|
|
65
|
+
return VCM_HOOK_COMMAND.replace(endpoint, () => `"\${VCM_API_URL}${VCM_HOOK_PROBE_PATH}"`);
|
|
66
|
+
}
|
|
58
67
|
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'`;
|
|
59
68
|
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'`;
|
|
60
69
|
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; node "$guard" || exit 0'`;
|
|
@@ -369,20 +378,14 @@ export function createHarnessService(deps) {
|
|
|
369
378
|
? normalizeArchitectWorkerSettings(await deps.getArchitectWorkerSettings())
|
|
370
379
|
: DEFAULT_ARCHITECT_WORKER_SETTINGS;
|
|
371
380
|
}
|
|
372
|
-
const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(deps.fs, {
|
|
373
|
-
runner: deps.commandRunner
|
|
374
|
-
});
|
|
375
381
|
return {
|
|
376
382
|
async getHarnessStatus(repoRoot) {
|
|
377
|
-
const
|
|
378
|
-
analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
|
|
379
|
-
codeIntelligenceDetector.detect(repoRoot)
|
|
380
|
-
]);
|
|
383
|
+
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
|
|
381
384
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
382
385
|
const manifestChange = deps.runFixedInstaller
|
|
383
386
|
? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
|
|
384
387
|
: undefined;
|
|
385
|
-
return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange
|
|
388
|
+
return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange);
|
|
386
389
|
},
|
|
387
390
|
async getHarnessFileContent(repoRoot, filePath) {
|
|
388
391
|
return readHarnessFileContent(deps.fs, repoRoot, filePath);
|
|
@@ -411,17 +414,14 @@ export function createHarnessService(deps) {
|
|
|
411
414
|
harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "[VCM Harness] Update harness file")).harnessCommit;
|
|
412
415
|
}
|
|
413
416
|
const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
|
|
414
|
-
const
|
|
415
|
-
analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
|
|
416
|
-
codeIntelligenceDetector.detect(repoRoot)
|
|
417
|
-
]);
|
|
417
|
+
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
|
|
418
418
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
419
419
|
const manifestChange = deps.runFixedInstaller
|
|
420
420
|
? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
|
|
421
421
|
: undefined;
|
|
422
422
|
return {
|
|
423
423
|
file,
|
|
424
|
-
status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange
|
|
424
|
+
status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange),
|
|
425
425
|
harnessCommit
|
|
426
426
|
};
|
|
427
427
|
},
|
|
@@ -1393,7 +1393,7 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
|
|
|
1393
1393
|
}
|
|
1394
1394
|
return changes;
|
|
1395
1395
|
}
|
|
1396
|
-
function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange
|
|
1396
|
+
function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange) {
|
|
1397
1397
|
const files = analyses.map((analysis) => analysis.status);
|
|
1398
1398
|
const plannedChanges = analyses
|
|
1399
1399
|
.map((analysis) => analysis.plannedChange)
|
|
@@ -1419,13 +1419,7 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
|
|
|
1419
1419
|
files,
|
|
1420
1420
|
needsApply: plannedChanges.length > 0,
|
|
1421
1421
|
plannedChanges,
|
|
1422
|
-
|
|
1423
|
-
warnings: [
|
|
1424
|
-
...(plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []),
|
|
1425
|
-
...((codeIntelligence?.languages ?? [])
|
|
1426
|
-
.filter((language) => language.state !== "server_runnable")
|
|
1427
|
-
.map(lspUnavailableWarning))
|
|
1428
|
-
]
|
|
1422
|
+
warnings: plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []
|
|
1429
1423
|
};
|
|
1430
1424
|
}
|
|
1431
1425
|
async function readHarnessRevisionValue(fs, repoRoot) {
|
|
@@ -314,7 +314,19 @@ export function createMessageService(deps) {
|
|
|
314
314
|
if (!current || current.acceptedAt) {
|
|
315
315
|
return;
|
|
316
316
|
}
|
|
317
|
-
await markDispatchConfirmationFailure(input, message.id,
|
|
317
|
+
await markDispatchConfirmationFailure(input, message.id, await renderDispatchNotConfirmedReason());
|
|
318
|
+
}
|
|
319
|
+
async function renderDispatchNotConfirmedReason() {
|
|
320
|
+
let hookWarning;
|
|
321
|
+
try {
|
|
322
|
+
hookWarning = await deps.probeHookDelivery?.();
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
hookWarning = undefined;
|
|
326
|
+
}
|
|
327
|
+
return hookWarning
|
|
328
|
+
? `${DISPATCH_NOT_CONFIRMED_REASON} The VCM hook self-test also failed, so the role may have run without reporting: ${hookWarning}`
|
|
329
|
+
: DISPATCH_NOT_CONFIRMED_REASON;
|
|
318
330
|
}
|
|
319
331
|
async function readMessageById(input, messageId) {
|
|
320
332
|
return withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mergeNoProxy } from "./codex-bridge-integration-service.js";
|
|
2
|
+
// Role hooks, harness tools, and usage telemetry post to VCM_API_URL. curl and
|
|
3
|
+
// other HTTP clients send even loopback requests through http_proxy, so a proxy
|
|
4
|
+
// without a NO_PROXY entry for the VCM host would drop every hook silently.
|
|
5
|
+
export function buildVcmNoProxyEnvironment(apiUrl, modelEnvironment = {}, baseEnv = process.env) {
|
|
6
|
+
const host = readUrlHostname(apiUrl);
|
|
7
|
+
if (!host) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
const current = modelEnvironment.NO_PROXY
|
|
11
|
+
?? modelEnvironment.no_proxy
|
|
12
|
+
?? baseEnv.NO_PROXY
|
|
13
|
+
?? baseEnv.no_proxy;
|
|
14
|
+
const noProxy = mergeNoProxy(current, host);
|
|
15
|
+
return { NO_PROXY: noProxy, no_proxy: noProxy };
|
|
16
|
+
}
|
|
17
|
+
function readUrlHostname(value) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const hostname = new URL(value).hostname;
|
|
23
|
+
// URL keeps IPv6 brackets; NO_PROXY lists the bare address.
|
|
24
|
+
return hostname.replace(/^\[(.*)\]$/, "$1") || undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
|
+
import { renderGitIdentityWarning } from "../adapters/git-adapter.js";
|
|
4
5
|
const DEFAULT_HANDOFF_ROOT = ".ai/vcm/handoffs";
|
|
5
6
|
const DEFAULT_STATE_ROOT = ".ai/vcm";
|
|
7
|
+
const DEFAULT_ENVIRONMENT_CHECK_WAIT_MS = 5 * 1000;
|
|
6
8
|
export function createProjectService(deps) {
|
|
9
|
+
const environmentCheckWaitMs = deps.environmentCheckWaitMs ?? DEFAULT_ENVIRONMENT_CHECK_WAIT_MS;
|
|
7
10
|
let currentProject = null;
|
|
8
11
|
return {
|
|
9
12
|
async connectProject(input) {
|
|
@@ -33,7 +36,7 @@ export function createProjectService(deps) {
|
|
|
33
36
|
await deps.fs.ensureDir(path.join(this.getProjectDataRoot(repoRoot), "tasks"));
|
|
34
37
|
await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
|
|
35
38
|
await this.saveConfig(config, true);
|
|
36
|
-
currentProject = await readConnectedProjectSummary(repoRoot, config, {
|
|
39
|
+
currentProject = await readConnectedProjectSummary(repoRoot, config, { refreshChecks: true });
|
|
37
40
|
await deps.appSettings.recordRecentRepositoryPath(repoRoot);
|
|
38
41
|
return currentProject;
|
|
39
42
|
},
|
|
@@ -97,14 +100,58 @@ export function createProjectService(deps) {
|
|
|
97
100
|
return path.dirname(deps.appSettings.getProjectConfigPath(repoRoot));
|
|
98
101
|
}
|
|
99
102
|
};
|
|
103
|
+
async function readEnvironmentWarnings(repoRoot, refresh) {
|
|
104
|
+
const [host, repository] = await Promise.all([
|
|
105
|
+
// Startup already ran the host checks; their cache retries failures
|
|
106
|
+
// after 30 s, so a connect only refreshes the repository's own checks.
|
|
107
|
+
(deps.environmentCheck?.getWarnings() ?? Promise.resolve([])).catch((caught) => [
|
|
108
|
+
`Unable to check the host environment. ${getErrorHint(caught)}`
|
|
109
|
+
]),
|
|
110
|
+
(deps.environmentCheck?.getRepositoryWarnings(repoRoot, { refresh }) ?? Promise.resolve([])).catch((caught) => [
|
|
111
|
+
`Unable to check access to the Git remote. ${getErrorHint(caught)}`
|
|
112
|
+
])
|
|
113
|
+
]);
|
|
114
|
+
return { host, repository };
|
|
115
|
+
}
|
|
116
|
+
// A slow network must not stall the Repository panel: checks that outlast the
|
|
117
|
+
// wait keep running and fill the cache for the next refresh.
|
|
118
|
+
async function waitForChecks(checks) {
|
|
119
|
+
let timer;
|
|
120
|
+
const pending = new Promise((resolve) => {
|
|
121
|
+
timer = setTimeout(() => resolve({
|
|
122
|
+
host: ["VCM is still checking the host environment and Git remote access. Refresh the Repository panel to see the result."],
|
|
123
|
+
repository: []
|
|
124
|
+
}), environmentCheckWaitMs);
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
return await Promise.race([checks, pending]);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
100
133
|
async function readConnectedProjectSummary(repoRoot, config, options = {}) {
|
|
101
134
|
const warnings = [];
|
|
135
|
+
// Environment and remote checks reach the network (gh auth status, git
|
|
136
|
+
// ls-remote), so they run alongside the local Git reads below.
|
|
137
|
+
const environmentChecks = readEnvironmentWarnings(repoRoot, options.refreshChecks);
|
|
102
138
|
const claudeVersionWarning = await deps.claudeVersionCheck?.getWarning(config.claudeCommand, {
|
|
103
|
-
refresh: options.
|
|
139
|
+
refresh: options.refreshChecks
|
|
104
140
|
});
|
|
105
141
|
if (claudeVersionWarning) {
|
|
106
142
|
warnings.push(claudeVersionWarning);
|
|
107
143
|
}
|
|
144
|
+
const environmentWarnings = await waitForChecks(environmentChecks);
|
|
145
|
+
warnings.push(...environmentWarnings.host);
|
|
146
|
+
try {
|
|
147
|
+
const identity = await deps.git.resolveCommitIdentity(repoRoot);
|
|
148
|
+
if (identity.defaulted.length > 0) {
|
|
149
|
+
warnings.push(renderGitIdentityWarning("this repository", identity));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (caught) {
|
|
153
|
+
warnings.push(`Unable to read the Git commit identity. ${getErrorHint(caught)}`);
|
|
154
|
+
}
|
|
108
155
|
let branch = "unknown";
|
|
109
156
|
let isDirty = false;
|
|
110
157
|
let headCommit = "unknown";
|
|
@@ -145,6 +192,7 @@ export function createProjectService(deps) {
|
|
|
145
192
|
warnings.push(`Unable to read Git ahead/behind status. ${getErrorHint(caught)}`);
|
|
146
193
|
}
|
|
147
194
|
}
|
|
195
|
+
warnings.push(...environmentWarnings.repository);
|
|
148
196
|
const pullDisabledReason = getPullDisabledReason({ isDirty, upstreamBranch });
|
|
149
197
|
return {
|
|
150
198
|
repoRoot,
|
|
@@ -7,7 +7,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
8
8
|
import { readHarnessRevisionState } from "./harness-revision.js";
|
|
9
9
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
10
|
-
import {
|
|
10
|
+
import { buildVcmNoProxyEnvironment } from "./no-proxy-environment.js";
|
|
11
11
|
const TRANSLATOR_ROLE = "translator";
|
|
12
12
|
const HARNESS_ENGINEER_ROLE = "harness-engineer";
|
|
13
13
|
const TRANSLATION_DIR = ".ai/vcm/translations";
|
|
@@ -79,16 +79,8 @@ export function createSessionService(deps) {
|
|
|
79
79
|
// Transcript paths come from Claude Code hook payloads; VCM does not derive
|
|
80
80
|
// them from Claude Code's project-directory encoding.
|
|
81
81
|
const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
|
|
82
|
-
const lspStatus = roleUsesLsp(role) && deps.codeIntelligenceDetector
|
|
83
|
-
? await deps.codeIntelligenceDetector.detect(taskRepoRoot, { refresh: true })
|
|
84
|
-
: undefined;
|
|
85
|
-
const lspPluginDirs = roleUsesLsp(role)
|
|
86
|
-
? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
|
|
87
|
-
: [];
|
|
88
|
-
const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus, role)]
|
|
89
|
-
.filter(Boolean).join("\n\n") || undefined;
|
|
90
82
|
const startCommand = {
|
|
91
|
-
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt
|
|
83
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
|
|
92
84
|
cwd: taskRepoRoot
|
|
93
85
|
};
|
|
94
86
|
const runtimeSessionToken = randomUUID();
|
|
@@ -102,20 +94,13 @@ export function createSessionService(deps) {
|
|
|
102
94
|
env: withClaudeCodeRuntimeEnv({
|
|
103
95
|
VCM_API_URL: deps.apiUrl,
|
|
104
96
|
VCM_BASE_REPO_ROOT: repoRoot,
|
|
97
|
+
...await gitIdentityEnvironment(taskRepoRoot),
|
|
105
98
|
VCM_TASK_REPO_ROOT: taskRepoRoot,
|
|
106
99
|
VCM_TASK_SLUG: taskSlug,
|
|
107
100
|
VCM_ROLE: role,
|
|
108
101
|
VCM_SESSION_ID: claudeSessionId || undefined,
|
|
109
102
|
VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
|
|
110
|
-
}, modelEnvironment,
|
|
111
|
-
...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
|
|
112
|
-
...(lspPluginDirs.length > 0
|
|
113
|
-
? {
|
|
114
|
-
ENABLE_LSP_TOOL: "true",
|
|
115
|
-
ENABLE_TOOL_SEARCH: "false"
|
|
116
|
-
}
|
|
117
|
-
: {})
|
|
118
|
-
}),
|
|
103
|
+
}, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
|
|
119
104
|
cols: input.cols,
|
|
120
105
|
rows: input.rows
|
|
121
106
|
});
|
|
@@ -235,6 +220,7 @@ export function createSessionService(deps) {
|
|
|
235
220
|
env: withClaudeCodeRuntimeEnv({
|
|
236
221
|
VCM_API_URL: deps.apiUrl,
|
|
237
222
|
VCM_BASE_REPO_ROOT: repoRoot,
|
|
223
|
+
...await gitIdentityEnvironment(taskContext.taskRepoRoot),
|
|
238
224
|
VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
|
|
239
225
|
// Project-scoped sessions report their project sentinel as VCM_TASK_SLUG so
|
|
240
226
|
// hook payloads match this session's record and cannot be attributed to the
|
|
@@ -349,6 +335,7 @@ export function createSessionService(deps) {
|
|
|
349
335
|
env: withClaudeCodeRuntimeEnv({
|
|
350
336
|
VCM_API_URL: deps.apiUrl,
|
|
351
337
|
VCM_BASE_REPO_ROOT: repoRoot,
|
|
338
|
+
...await gitIdentityEnvironment(taskContext.taskRepoRoot),
|
|
352
339
|
VCM_TASK_REPO_ROOT: taskContext.taskRepoRoot,
|
|
353
340
|
// Project-scoped sessions report their project sentinel as VCM_TASK_SLUG so
|
|
354
341
|
// hook payloads match this session's record and cannot be attributed to the
|
|
@@ -431,6 +418,9 @@ export function createSessionService(deps) {
|
|
|
431
418
|
// silent) session still proceeds best-effort. Returns "exited" if the runtime
|
|
432
419
|
// session is gone or no longer running, which a resume launch treats as a
|
|
433
420
|
// resume-by-id failure.
|
|
421
|
+
async function gitIdentityEnvironment(repoRoot) {
|
|
422
|
+
return deps.gitIdentity ? (await deps.gitIdentity.resolveCommitIdentity(repoRoot)).env : {};
|
|
423
|
+
}
|
|
434
424
|
async function waitForSessionInputReady(sessionId) {
|
|
435
425
|
let sawOutput = false;
|
|
436
426
|
let lastOutputAt;
|
|
@@ -547,6 +537,7 @@ export function createSessionService(deps) {
|
|
|
547
537
|
env: withClaudeCodeRuntimeEnv({
|
|
548
538
|
VCM_API_URL: deps.apiUrl,
|
|
549
539
|
VCM_BASE_REPO_ROOT: repoRoot,
|
|
540
|
+
...await gitIdentityEnvironment(targetCwd),
|
|
550
541
|
VCM_TASK_REPO_ROOT: targetCwd,
|
|
551
542
|
VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
|
|
552
543
|
VCM_ROLE: session.role,
|
|
@@ -1669,6 +1660,7 @@ function withClaudeCodeRuntimeEnv(env, modelEnvironment = {}, telemetryEnvironme
|
|
|
1669
1660
|
...env,
|
|
1670
1661
|
...modelEnvironment,
|
|
1671
1662
|
...telemetryEnvironment,
|
|
1663
|
+
...buildVcmNoProxyEnvironment(env.VCM_API_URL, modelEnvironment),
|
|
1672
1664
|
CLAUDE_CODE_DISABLE_AUTO_MEMORY,
|
|
1673
1665
|
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
|
|
1674
1666
|
};
|
|
@@ -890,10 +890,16 @@ async function allowedAfterTester(fs, input, state, currentFlow, codeSource, opt
|
|
|
890
890
|
if (test.value === "incomplete" || test.infrastructure === "repair-required")
|
|
891
891
|
return [`${currentFlow}/tester`];
|
|
892
892
|
const validation = await gateState(fs, input, "validation-adequacy");
|
|
893
|
+
const unapprovedFailure = test.value === "fail" && !test.hasUserApprovedCoverageGap;
|
|
893
894
|
if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
895
|
+
// Findings on a genuine failure may be Tester-owned (wrong tests or
|
|
896
|
+
// evidence) or need the production investigation that Tester Failure
|
|
897
|
+
// starts; PM routes by who can dispose of them.
|
|
898
|
+
return unapprovedFailure && options.testerFailureFlow
|
|
899
|
+
? [`${currentFlow}/tester`, `${options.testerFailureFlow}/architect`]
|
|
900
|
+
: [`${currentFlow}/tester`];
|
|
901
|
+
}
|
|
902
|
+
if (unapprovedFailure) {
|
|
897
903
|
return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
|
|
898
904
|
}
|
|
899
905
|
if (!gatePassedForDispatch(state, currentFlow, "tester", "validation-adequacy", validation))
|