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
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
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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,
|
|
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
|
|
24
|
-
const args =
|
|
25
|
-
|
|
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.
|
|
16
|
-
|
|
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
|
|
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 = [...
|
|
35
|
+
export const CODE_ROLE_DISALLOWED_TOOLS = [...FRONTMATTER_DISALLOWED_TOOLS];
|
|
36
36
|
export const REVIEWER_DISALLOWED_TOOLS = [
|
|
37
37
|
"Agent",
|
|
38
38
|
"Edit",
|
|
39
|
-
...
|
|
39
|
+
...FRONTMATTER_DISALLOWED_TOOLS
|
|
40
40
|
];
|
|
41
|
-
// The evidence worker reads and
|
|
42
|
-
// or delegates.
|
|
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
|
-
...
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 = {
|
package/dist/backend/server.js
CHANGED
|
@@ -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 {
|
|
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() {
|