vibe-coding-master 0.8.10 → 0.8.12

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