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
package/README.md CHANGED
@@ -17,12 +17,30 @@ orchestration, translation, harness setup, and mobile gateway control.
17
17
 
18
18
  ## Requirements
19
19
 
20
- - Node.js 20 LTS or 22+
20
+ - Node.js 20 LTS or 22+, with `node` on the `PATH` VCM starts with (role hooks
21
+ and `.ai/tools` scripts run with it)
21
22
  - npm
22
- - Git
23
+ - Git 2.22 or newer
24
+ - `sh` and `curl` (role hooks report Claude Code events to VCM through them)
25
+ - Non-interactive credentials for the repository's upstream remote (an SSH key
26
+ in ssh-agent or a Git credential helper). Creating a task pulls the upstream
27
+ branch, and VCM runs Git without credential prompts.
28
+ - Optional: GitHub CLI (`gh`). VCM never runs it, but when it is installed and
29
+ logged out VCM warns, because roles that use it will fail.
23
30
  - Claude Code installed and available as `claude` (verified with 2.1.310 or
24
31
  newer; VCM shows a Repository warning when an older release is detected)
25
32
  - Claude Code authenticated in the same environment where VCM runs
33
+ - A Git commit identity (`user.name` and `user.email`) in the same environment.
34
+ When either is missing, VCM and its roles commit as `VCM <vcm@localhost>`
35
+ and the Repository panel shows a warning; VCM never changes your Git config.
36
+
37
+ VCM checks these programs at startup, runs a hook self-test that sends one
38
+ request through the installed hook pipeline, and checks access to the upstream
39
+ remote when a repository connects. Any problem appears in the startup log and
40
+ the Repository warnings, and the Repository header shows how many there are
41
+ while the section is collapsed; none of them stops VCM from running. Role sessions
42
+ add the VCM host to `NO_PROXY`, so an `http_proxy` setting does not capture
43
+ their hook requests.
26
44
 
27
45
  For Linux containers, make sure common native build/runtime tools are available:
28
46
 
@@ -511,22 +529,16 @@ Use it to:
511
529
  - merge task harness commits back to the connected repository branch when
512
530
  appropriate
513
531
 
514
- VCM bundles the Claude Code LSP bridge and loads it for Architect and Reviewer
515
- sessions, including Codex Bridge launches; Architect's evidence worker can use it
516
- too. While a language server is still indexing, VCM holds LSP queries until it
517
- is ready and fails them explicitly if indexing does not finish, instead of
518
- returning partial results. The project environment must still
519
- provide the language server for each detected language: `rust-analyzer`,
520
- `typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
521
- `jdtls`. Architect preloads `vcm-code-navigation` and uses LSP for semantic
522
- relationships. Accessors, backing fields, trait items, implementations, wrappers,
523
- and aliases are queried as separate symbols; a bounded source-text fallback may
524
- only locate candidates after the correct LSP query is demonstrably partial.
525
- At the architecture-plan gate, Reviewer checks that every decision-bearing
526
- relationship cites its evidence source and re-queries it with LSP. Other roles
527
- use generated context and ordinary source reads. Harness Studio
528
- reports whether the server executable and plugin can run; the role Session
529
- performs the real workspace indexing and semantic query retries.
532
+ VCM roles navigate code without language servers, so a task does not hold a
533
+ semantic index in memory per session. Architect preloads `vcm-code-navigation`
534
+ and establishes relationships with exact source-text search over the whole
535
+ project source tree, searching every spelling that can reach a symbol and
536
+ reading every match. Accessors, backing fields, trait items, implementations,
537
+ wrappers, and aliases are searched as separate symbols, and each relationship
538
+ records the search that produced it. At the architecture-plan gate, Reviewer
539
+ checks that every decision-bearing relationship cites its evidence source and
540
+ reruns the recorded search. Other roles use generated context and ordinary
541
+ source reads.
530
542
 
531
543
  Harness Engineer is task-scoped and runs from the active task worktree. The
532
544
  backend automatically starts a fresh Harness Engineer for each active task or
@@ -1,4 +1,4 @@
1
- import { roleRuntimeDisallowedTools, roleUsesLsp } from "../role-tool-policy.js";
1
+ import { roleRuntimeDisallowedTools, roleUsesRuntimeDenyList } from "../role-tool-policy.js";
2
2
  import { isCodexBridgeSessionModel } from "../../shared/types/session.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  export function createClaudeAdapter(runner) {
@@ -20,10 +20,9 @@ export function createClaudeAdapter(runner) {
20
20
  }
21
21
  return result.stdout.trim();
22
22
  },
23
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt, pluginDirs = []) {
24
- const args = pluginDirs.flatMap((pluginDir) => ["--plugin-dir", pluginDir]);
25
- const lspEnabled = roleUsesLsp(role) && pluginDirs.length > 0;
26
- if (lspEnabled) {
23
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt) {
24
+ const args = [];
25
+ if (roleUsesRuntimeDenyList(role)) {
27
26
  args.push("--disallowedTools", roleRuntimeDisallowedTools(role).join(","));
28
27
  }
29
28
  else {
@@ -10,10 +10,15 @@ export function createCommandRunner() {
10
10
  input: options.input,
11
11
  reject: false
12
12
  });
13
+ // A command that could not start, timed out, or died from a signal has
14
+ // no exit code; it failed, so it must not read as a clean exit.
15
+ const exitCode = result.exitCode ?? (result.failed ? 1 : 0);
13
16
  return {
14
17
  stdout: result.stdout,
15
- stderr: result.stderr,
16
- exitCode: result.exitCode ?? 0
18
+ stderr: result.exitCode === undefined && result.failed
19
+ ? [result.stderr, result.shortMessage].filter(Boolean).join("\n")
20
+ : result.stderr,
21
+ exitCode
17
22
  };
18
23
  }
19
24
  catch (error) {
@@ -1,7 +1,24 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { VcmError } from "../errors.js";
4
+ // Network Git commands run without a terminal prompt: VCM is driven from the
5
+ // browser, so a credential prompt would hang the request on the terminal that
6
+ // started VCM instead of failing with an error the UI can show.
7
+ const REMOTE_COMMAND_TIMEOUT_MS = 20 * 1000;
8
+ const PULL_TIMEOUT_MS = 5 * 60 * 1000;
9
+ // Commit identity used when neither Git config nor the environment provides one.
10
+ // Without it, Git either refuses to commit or invents an identity from the OS
11
+ // account and hostname, which silently attributes commits to a machine name.
12
+ export const DEFAULT_GIT_IDENTITY = { name: "VCM", email: "vcm@localhost" };
13
+ const GIT_IDENTITY_CACHE_TTL_MS = 30 * 1000;
14
+ export function renderGitIdentityWarning(location, identity) {
15
+ const missing = identity.defaulted.map((part) => `user.${part}`).join(" and ");
16
+ return `Git ${missing} is not configured for ${location}, so VCM and role commits use ${identity.name} <${identity.email}>. `
17
+ + "Set your own identity in the environment that runs VCM: "
18
+ + "git config --global user.name \"<name>\" and git config --global user.email \"<email>\".";
19
+ }
4
20
  export function createGitAdapter(runner) {
21
+ const identityCache = new Map();
5
22
  return {
6
23
  async checkRepo(repoRoot) {
7
24
  return checkGitMarker(repoRoot);
@@ -303,7 +320,7 @@ export function createGitAdapter(runner) {
303
320
  }
304
321
  },
305
322
  async commit(repoRoot, message) {
306
- const result = await runGit(runner, repoRoot, ["commit", "-m", message]);
323
+ const result = await runGit(runner, repoRoot, ["commit", "-m", message], (await this.resolveCommitIdentity(repoRoot)).env);
307
324
  if (result.exitCode !== 0) {
308
325
  throw new VcmError({
309
326
  code: "GIT_COMMIT_FAILED",
@@ -318,7 +335,7 @@ export function createGitAdapter(runner) {
318
335
  if (paths.length === 0) {
319
336
  return this.getHeadCommit(repoRoot);
320
337
  }
321
- const result = await runGit(runner, repoRoot, ["commit", "--only", "-m", message, "--", ...paths]);
338
+ const result = await runGit(runner, repoRoot, ["commit", "--only", "-m", message, "--", ...paths], (await this.resolveCommitIdentity(repoRoot)).env);
322
339
  if (result.exitCode !== 0) {
323
340
  throw new VcmError({
324
341
  code: "GIT_COMMIT_FAILED",
@@ -329,6 +346,29 @@ export function createGitAdapter(runner) {
329
346
  }
330
347
  return this.getHeadCommit(repoRoot);
331
348
  },
349
+ async resolveCommitIdentity(repoRoot) {
350
+ const cached = identityCache.get(repoRoot);
351
+ if (cached && Date.now() - cached.checkedAt < GIT_IDENTITY_CACHE_TTL_MS) {
352
+ return cached.identity;
353
+ }
354
+ const identity = { name: "", email: "", defaulted: [], env: {} };
355
+ for (const part of ["name", "email"]) {
356
+ const result = await runGit(runner, repoRoot, ["config", "--get", `user.${part}`]);
357
+ const configured = result.exitCode === 0 ? result.stdout.trim() : "";
358
+ const variables = [`GIT_AUTHOR_${part.toUpperCase()}`, `GIT_COMMITTER_${part.toUpperCase()}`];
359
+ for (const variable of variables) {
360
+ if (!configured && !process.env[variable]) {
361
+ identity.env[variable] = DEFAULT_GIT_IDENTITY[part];
362
+ }
363
+ }
364
+ if (variables.some((variable) => identity.env[variable])) {
365
+ identity.defaulted.push(part);
366
+ }
367
+ identity[part] = process.env[variables[0]] || configured || DEFAULT_GIT_IDENTITY[part];
368
+ }
369
+ identityCache.set(repoRoot, { checkedAt: Date.now(), identity });
370
+ return identity;
371
+ },
332
372
  async createWorktree(input) {
333
373
  const result = await runGit(runner, input.repoRoot, [
334
374
  "worktree",
@@ -375,7 +415,7 @@ export function createGitAdapter(runner) {
375
415
  }
376
416
  },
377
417
  async pullFastForward(repoRoot) {
378
- const result = await runGit(runner, repoRoot, ["pull", "--ff-only"]);
418
+ const result = await runGit(runner, repoRoot, ["pull", "--ff-only"], await buildNonInteractiveGitEnv(runner, repoRoot), PULL_TIMEOUT_MS);
379
419
  if (result.exitCode !== 0) {
380
420
  throw new VcmError({
381
421
  code: "GIT_PULL_FAILED",
@@ -388,9 +428,47 @@ export function createGitAdapter(runner) {
388
428
  stdout: result.stdout,
389
429
  stderr: result.stderr
390
430
  };
431
+ },
432
+ async checkRemoteAccess(repoRoot) {
433
+ const branch = (await runGit(runner, repoRoot, ["branch", "--show-current"])).stdout.trim();
434
+ if (!branch) {
435
+ return { status: "skipped" };
436
+ }
437
+ const remoteResult = await runGit(runner, repoRoot, ["config", "--get", `branch.${branch}.remote`]);
438
+ const remote = remoteResult.stdout.trim();
439
+ if (remoteResult.exitCode !== 0 || !remote || remote === ".") {
440
+ return { status: "skipped" };
441
+ }
442
+ const result = await runGit(runner, repoRoot, ["ls-remote", remote, "HEAD"], await buildNonInteractiveGitEnv(runner, repoRoot), REMOTE_COMMAND_TIMEOUT_MS);
443
+ if (result.exitCode === 0) {
444
+ return { status: "ok", remote };
445
+ }
446
+ return {
447
+ status: "failed",
448
+ remote,
449
+ detail: firstMeaningfulLine(result.stderr) || firstMeaningfulLine(result.stdout) || `git ls-remote exited with ${result.exitCode}`
450
+ };
391
451
  }
392
452
  };
393
453
  }
454
+ async function buildNonInteractiveGitEnv(runner, repoRoot) {
455
+ const env = {
456
+ GIT_TERMINAL_PROMPT: "0",
457
+ GCM_INTERACTIVE: "never"
458
+ };
459
+ // Keep a configured SSH command; otherwise make ssh fail instead of asking
460
+ // for a passphrase or host-key confirmation.
461
+ if (!process.env.GIT_SSH_COMMAND && !process.env.GIT_SSH) {
462
+ const configured = await runGit(runner, repoRoot, ["config", "--get", "core.sshCommand"]);
463
+ if (configured.exitCode !== 0 || !configured.stdout.trim()) {
464
+ env.GIT_SSH_COMMAND = "ssh -o BatchMode=yes";
465
+ }
466
+ }
467
+ return { ...process.env, ...env };
468
+ }
469
+ function firstMeaningfulLine(value) {
470
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
471
+ }
394
472
  async function checkGitMarker(repoRoot) {
395
473
  const markerPath = path.join(repoRoot, ".git");
396
474
  try {
@@ -449,8 +527,8 @@ async function pathExists(targetPath) {
449
527
  return false;
450
528
  }
451
529
  }
452
- async function runGit(runner, repoRoot, args) {
453
- return runner.run("git", [...await buildSafeDirectoryArgs(repoRoot), ...args], { cwd: repoRoot });
530
+ async function runGit(runner, repoRoot, args, env, timeoutMs) {
531
+ return runner.run("git", [...await buildSafeDirectoryArgs(repoRoot), ...args], { cwd: repoRoot, env, timeoutMs });
454
532
  }
455
533
  function parseWorktreePaths(output) {
456
534
  return output
@@ -1,4 +1,9 @@
1
+ import { VCM_HOOK_PROBE_PATH } from "../services/harness-service.js";
1
2
  export function registerClaudeHookRoutes(app, deps) {
3
+ // The hook self-test posts here through the installed hook pipeline.
4
+ app.post(VCM_HOOK_PROBE_PATH, async (request) => {
5
+ return { received: deps.environmentCheck?.receiveHookProbe(request.body?.runtimeSessionToken) ?? false };
6
+ });
2
7
  app.post("/api/hooks/claude-code", async (request) => {
3
8
  return deps.claudeHookService.handleHook(request.body);
4
9
  });
@@ -1,4 +1,4 @@
1
- const LSP_FRONTMATTER_DISALLOWED_TOOLS = [
1
+ const FRONTMATTER_DISALLOWED_TOOLS = [
2
2
  "NotebookEdit",
3
3
  "WebFetch",
4
4
  "WebSearch"
@@ -32,19 +32,19 @@ const NON_VCM_ROLE_RUNTIME_TOOLS = [
32
32
  "WebSearch",
33
33
  "Workflow"
34
34
  ];
35
- export const CODE_ROLE_DISALLOWED_TOOLS = [...LSP_FRONTMATTER_DISALLOWED_TOOLS];
35
+ export const CODE_ROLE_DISALLOWED_TOOLS = [...FRONTMATTER_DISALLOWED_TOOLS];
36
36
  export const REVIEWER_DISALLOWED_TOOLS = [
37
37
  "Agent",
38
38
  "Edit",
39
- ...LSP_FRONTMATTER_DISALLOWED_TOOLS
39
+ ...FRONTMATTER_DISALLOWED_TOOLS
40
40
  ];
41
- // The evidence worker reads and queries LSP but never runs commands, edits code,
42
- // or delegates. A deny list keeps the parent session's LSP tool available.
41
+ // The evidence worker reads and searches but never runs commands, edits code,
42
+ // or delegates.
43
43
  export const EVIDENCE_WORKER_DISALLOWED_TOOLS = [
44
44
  "Agent",
45
45
  "Bash",
46
46
  "Edit",
47
- ...LSP_FRONTMATTER_DISALLOWED_TOOLS
47
+ ...FRONTMATTER_DISALLOWED_TOOLS
48
48
  ];
49
49
  export const CODE_ROLE_RUNTIME_DISALLOWED_TOOLS = [...NON_VCM_ROLE_RUNTIME_TOOLS];
50
50
  export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
@@ -52,10 +52,11 @@ export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
52
52
  "Edit",
53
53
  ...NON_VCM_ROLE_RUNTIME_TOOLS
54
54
  ];
55
- // Architect makes semantic code claims; Reviewer verifies them at Gate Review.
56
- const LSP_ROLE_NAMES = new Set(["architect", "reviewer"]);
57
- export function roleUsesLsp(role) {
58
- return LSP_ROLE_NAMES.has(role);
55
+ // Architect and Reviewer Agent frontmatter uses a deny list, so their launch
56
+ // also removes the runtime tools no VCM workflow role may use.
57
+ const RUNTIME_DENY_LIST_ROLE_NAMES = new Set(["architect", "reviewer"]);
58
+ export function roleUsesRuntimeDenyList(role) {
59
+ return RUNTIME_DENY_LIST_ROLE_NAMES.has(role);
59
60
  }
60
61
  export function roleRuntimeDisallowedTools(role) {
61
62
  if (role === "reviewer") {
@@ -46,7 +46,7 @@ export function createNodePtyTerminalRuntime(deps) {
46
46
  process: child,
47
47
  listeners: new Set(),
48
48
  logWriter,
49
- replayBuffer: "",
49
+ replayBuffer: new TerminalReplayBuffer(),
50
50
  disposed: false
51
51
  };
52
52
  entries.set(session.id, entry);
@@ -55,7 +55,7 @@ export function createNodePtyTerminalRuntime(deps) {
55
55
  return;
56
56
  }
57
57
  entry.session.lastOutputAt = now();
58
- entry.replayBuffer = appendTerminalReplay(entry.replayBuffer, data);
58
+ entry.replayBuffer.append(data);
59
59
  entry.logWriter.append(data);
60
60
  emit(entry, {
61
61
  sessionId: session.id,
@@ -150,7 +150,8 @@ export function createNodePtyTerminalRuntime(deps) {
150
150
  subscribe(sessionId, listener, options = {}) {
151
151
  const entry = getEntry(entries, sessionId);
152
152
  entry.listeners.add(listener);
153
- if (options.replay !== false && entry.replayBuffer) {
153
+ const replay = options.replay !== false ? entry.replayBuffer.text() : "";
154
+ if (replay) {
154
155
  listener({
155
156
  id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
156
157
  sessionId,
@@ -158,7 +159,7 @@ export function createNodePtyTerminalRuntime(deps) {
158
159
  role: entry.session.role,
159
160
  type: "output",
160
161
  timestamp: now(),
161
- data: entry.replayBuffer
162
+ data: replay
162
163
  });
163
164
  }
164
165
  else if (options.replay !== false && entry.input.logPath) {
@@ -257,11 +258,41 @@ export function tailTerminalReplay(data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT
257
258
  const firstLineBreak = tail.indexOf("\n");
258
259
  return firstLineBreak >= 0 ? tail.slice(firstLineBreak + 1) : tail;
259
260
  }
260
- export function appendTerminalReplay(current, data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
261
- if (!data) {
262
- return current;
261
+ // Keeps the recent terminal output for subscriber replay. PTY output arrives in
262
+ // many small chunks, so appending measures only the new chunk and drops whole
263
+ // old chunks; the exact tail is cut only when a subscriber asks for it.
264
+ export class TerminalReplayBuffer {
265
+ limitBytes;
266
+ chunks = [];
267
+ head = 0;
268
+ bytes = 0;
269
+ constructor(limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
270
+ this.limitBytes = limitBytes;
271
+ }
272
+ append(data) {
273
+ if (!data) {
274
+ return;
275
+ }
276
+ const bytes = Buffer.byteLength(data, "utf8");
277
+ this.chunks.push({ data, bytes });
278
+ this.bytes += bytes;
279
+ // Drop a chunk only while the rest still covers the limit.
280
+ while (this.head < this.chunks.length - 1 && this.bytes - this.chunks[this.head].bytes >= this.limitBytes) {
281
+ this.bytes -= this.chunks[this.head].bytes;
282
+ this.head += 1;
283
+ }
284
+ if (this.head > 1024 && this.head * 2 > this.chunks.length) {
285
+ this.chunks = this.chunks.slice(this.head);
286
+ this.head = 0;
287
+ }
288
+ }
289
+ text() {
290
+ if (this.head >= this.chunks.length) {
291
+ return "";
292
+ }
293
+ const joined = this.chunks.slice(this.head).map((chunk) => chunk.data).join("");
294
+ return tailTerminalReplay(joined, this.limitBytes);
263
295
  }
264
- return tailTerminalReplay(`${current}${data}`, limitBytes);
265
296
  }
266
297
  export function buildPtyEnvironment(baseEnv, inputEnv = {}) {
267
298
  const env = {
@@ -1,3 +1,4 @@
1
+ import os from "node:os";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import Fastify from "fastify";
@@ -10,7 +11,8 @@ import { createCommandRunner } from "./adapters/command-runner.js";
10
11
  import { createCommandDispatcher } from "./services/command-dispatcher.js";
11
12
  import { createClaudeHookService } from "./services/claude-hook-service.js";
12
13
  import { createClaudeVersionCheck } from "./services/claude-version-check.js";
13
- import { createGitAdapter } from "./adapters/git-adapter.js";
14
+ import { createEnvironmentCheck } from "./services/environment-check.js";
15
+ import { createGitAdapter, renderGitIdentityWarning } from "./adapters/git-adapter.js";
14
16
  import { createAppSettingsService } from "./services/app-settings-service.js";
15
17
  import { createCodexBridgeIntegrationService } from "./services/codex-bridge-integration-service.js";
16
18
  import { createAutoMemoryService } from "./services/auto-memory-service.js";
@@ -35,7 +37,6 @@ import { createJobGuardService } from "./services/job-guard-service.js";
35
37
  import { createProjectService } from "./services/project-service.js";
36
38
  import { createSessionRegistry } from "./runtime/session-registry.js";
37
39
  import { createSessionService } from "./services/session-service.js";
38
- import { createHarnessCodeIntelligenceDetector } from "./services/code-intelligence-service.js";
39
40
  import { createMessageService } from "./services/message-service.js";
40
41
  import { createRoundService } from "./services/round-service.js";
41
42
  import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
@@ -93,7 +94,7 @@ export async function createServer(deps, options = {}) {
93
94
  appSettings: deps.appSettings,
94
95
  codexBridgeIntegration: deps.codexBridgeIntegration
95
96
  });
96
- registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
97
+ registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService, environmentCheck: deps.environmentCheck });
97
98
  registerGateReviewRoutes(app, {
98
99
  projectService: deps.projectService,
99
100
  gateReviewService: deps.gateReviewService
@@ -216,6 +217,29 @@ async function cleanupRecentTranslationRuntime(deps) {
216
217
  const repoRoots = await deps.projectService.getRecentRepositoryPaths();
217
218
  await Promise.all(repoRoots.map((repoRoot) => deps.translationWorkerService.cleanupStartupRuntime(repoRoot)));
218
219
  }
220
+ async function warnOnEnvironmentProblems(environmentCheck) {
221
+ try {
222
+ for (const warning of await environmentCheck?.getWarnings({ refresh: true }) ?? []) {
223
+ console.warn(`[VCM] ${warning}`);
224
+ }
225
+ }
226
+ catch {
227
+ // Project summaries report environment problems when a repository connects.
228
+ }
229
+ }
230
+ // Projects can still configure their own identity; this only reports the
231
+ // identity VCM falls back to outside any repository-specific Git config.
232
+ async function warnOnDefaultGitIdentity() {
233
+ try {
234
+ const identity = await createGitAdapter(createCommandRunner()).resolveCommitIdentity(os.homedir());
235
+ if (identity.defaulted.length > 0) {
236
+ console.warn(`[VCM] ${renderGitIdentityWarning("the VCM user", identity)}`);
237
+ }
238
+ }
239
+ catch {
240
+ // Project summaries report identity problems per repository.
241
+ }
242
+ }
219
243
  export async function startServer(options = {}) {
220
244
  const host = options.host ?? "127.0.0.1";
221
245
  const port = options.port ?? 4173;
@@ -224,6 +248,8 @@ export async function startServer(options = {}) {
224
248
  });
225
249
  const app = await createServer(deps, options);
226
250
  await app.listen({ host, port });
251
+ await warnOnEnvironmentProblems(deps.environmentCheck);
252
+ await warnOnDefaultGitIdentity();
227
253
  return {
228
254
  url: `http://${host}:${port}`,
229
255
  close() {
@@ -238,6 +264,7 @@ export function createDefaultServerDeps(options = {}) {
238
264
  const runner = createCommandRunner();
239
265
  const git = createGitAdapter(runner);
240
266
  const claude = createClaudeAdapter(runner);
267
+ const environmentCheck = createEnvironmentCheck({ runner, git, apiUrl: options.apiUrl });
241
268
  const appSettings = createAppSettingsService({ fs });
242
269
  const codexBridgeIntegration = createCodexBridgeIntegrationService({
243
270
  settings: appSettings,
@@ -253,13 +280,14 @@ export function createDefaultServerDeps(options = {}) {
253
280
  const projectService = createProjectService({
254
281
  fs,
255
282
  git,
283
+ environmentCheck,
256
284
  appSettings,
257
285
  claudeVersionCheck: createClaudeVersionCheck(claude)
258
286
  });
259
287
  const taskService = createTaskService({ fs, git, artifactService, projectService });
260
288
  const taskWorkflowService = createTaskWorkflowService({ fs });
261
- const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(fs, { runner });
262
289
  const sessionService = createSessionService({
290
+ gitIdentity: git,
263
291
  fs,
264
292
  runtime,
265
293
  registry,
@@ -268,13 +296,11 @@ export function createDefaultServerDeps(options = {}) {
268
296
  projectService,
269
297
  taskService,
270
298
  taskWorkflowService,
271
- codeIntelligenceDetector,
272
299
  codexBridgeIntegration,
273
300
  apiUrl: options.apiUrl
274
301
  });
275
302
  const harnessService = createHarnessService({
276
303
  fs,
277
- commandRunner: runner,
278
304
  git,
279
305
  runtime,
280
306
  harnessEngineerSessions: sessionService,
@@ -330,6 +356,7 @@ export function createDefaultServerDeps(options = {}) {
330
356
  taskService,
331
357
  taskWorkflowService,
332
358
  workflowControlService,
359
+ probeHookDelivery: () => environmentCheck.probeHookDelivery(),
333
360
  onRouteDelivered: ({ repoRoot, taskSlug, message }) => architectRestartService.recordRouteDelivered(repoRoot, taskSlug, message)
334
361
  });
335
362
  const taskLaunchService = createTaskLaunchService({
@@ -513,7 +540,8 @@ export function createDefaultServerDeps(options = {}) {
513
540
  runtime,
514
541
  diagnosticsService,
515
542
  usageAnalyticsService,
516
- workflowControlService
543
+ workflowControlService,
544
+ environmentCheck
517
545
  };
518
546
  }
519
547
  export function getDefaultStaticDir() {