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.
Files changed (31) hide show
  1. package/README.md +30 -18
  2. package/dist/backend/adapters/claude-adapter.js +4 -5
  3. package/dist/backend/adapters/command-runner.js +7 -2
  4. package/dist/backend/adapters/git-adapter.js +83 -5
  5. package/dist/backend/api/claude-hook-routes.js +5 -0
  6. package/dist/backend/role-tool-policy.js +11 -10
  7. package/dist/backend/runtime/node-pty-runtime.js +39 -8
  8. package/dist/backend/server.js +35 -7
  9. package/dist/backend/services/environment-check.js +245 -0
  10. package/dist/backend/services/gate-review-service.js +6 -2
  11. package/dist/backend/services/harness-service.js +17 -23
  12. package/dist/backend/services/message-service.js +13 -1
  13. package/dist/backend/services/no-proxy-environment.js +29 -0
  14. package/dist/backend/services/project-service.js +50 -2
  15. package/dist/backend/services/session-service.js +11 -19
  16. package/dist/backend/services/workflow-control-service.js +9 -3
  17. package/dist/backend/templates/harness/architect-agent.js +8 -11
  18. package/dist/backend/templates/harness/architect-evidence-worker-agent.js +3 -3
  19. package/dist/backend/templates/harness/gate-review.js +29 -27
  20. package/dist/backend/templates/harness/project-manager-agent.js +2 -2
  21. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +13 -9
  22. package/dist-frontend/assets/index-C6tXxGlk.css +32 -0
  23. package/dist-frontend/assets/{index-CjM9OqZh.js → index-Ct8NwH3l.js} +35 -35
  24. package/dist-frontend/index.html +2 -2
  25. package/package.json +1 -1
  26. package/scripts/verify-package.mjs +0 -2
  27. package/dist/backend/services/code-intelligence-service.js +0 -255
  28. package/dist/backend/services/lsp-plugin.js +0 -64
  29. package/dist-frontend/assets/index-Bs4g_1yF.css +0 -32
  30. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +0 -88
  31. 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(codeDiffSources.map((source) => source === "coder" ? "coder" : "architect"));
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 [analyses, codeIntelligence] = await Promise.all([
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, codeIntelligence);
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 [analyses, codeIntelligence] = await Promise.all([
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, codeIntelligence),
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, codeIntelligence) {
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
- codeIntelligence,
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, DISPATCH_NOT_CONFIRMED_REASON);
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, { refreshClaudeVersion: true });
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.refreshClaudeVersion
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 { roleUsesLsp, prepareTaskLspPlugin, renderLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
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, lspPluginDirs),
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
- return [`${currentFlow}/tester`];
895
- }
896
- if (test.value === "fail" && !test.hasUserApprovedCoverageGap) {
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))