vibe-coding-master 0.8.9 → 0.8.11

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 CHANGED
@@ -253,10 +253,9 @@ such as a Dev Container or VM.
253
253
 
254
254
  Model and effort can be selected before start/resume/restart. Changes affect the
255
255
  next launched process, not a currently running Claude Code process.
256
- The native model list includes explicit `Fable 5.1` (`claude-fable-5-1`) and a
257
- separate `Fable Alias` whose resolved version is selected by Claude Code and the
258
- active account provider. `Opus` and `Sonnet` continue to track their Claude Code
259
- aliases, while `Opus 4.8` remains available as a pinned model.
256
+ The native model list is `Default`, `Fable`, `Opus`, and `Sonnet`. The latter
257
+ three use Claude Code aliases, so the resolved model depends on the installed
258
+ Claude Code version and provider.
260
259
 
261
260
  ## Usage Analytics
262
261
 
@@ -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
  });
@@ -355,6 +355,12 @@ function normalizeClaudeModel(input, fallback) {
355
355
  if (typeof input !== "string") {
356
356
  return fallback;
357
357
  }
358
+ if (input === "claude-fable-5-1") {
359
+ return "fable";
360
+ }
361
+ if (input === "claude-opus-4-8") {
362
+ return "opus";
363
+ }
358
364
  if (isCodexBridgeSessionModel(input)) {
359
365
  return input;
360
366
  }
@@ -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
+ ], { allowFailure: true })).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
+ ], { allowFailure: true })).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
+ ], { allowFailure: true })).trim();
1179
1195
  if (mergeBase
1180
1196
  && mergeBase !== headCommit
1181
1197
  && await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
@@ -1395,9 +1411,27 @@ async function readCodeDiffSourceArtifactError(fs, taskRepoRoot, source) {
1395
1411
  }
1396
1412
  return undefined;
1397
1413
  }
1398
- async function commandStdout(runner, cwd, args) {
1399
- const result = await runner.run("git", args, { cwd });
1400
- return result.exitCode === 0 ? result.stdout : "";
1414
+ async function commandStdout(runner, cwd, args, options = {}) {
1415
+ const retryHead = args.length === 2 && args[0] === "rev-parse" && args[1] === "HEAD";
1416
+ const attempts = retryHead ? 3 : 1;
1417
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1418
+ const result = await runner.run("git", args, { cwd });
1419
+ if (result.exitCode === 0)
1420
+ return result.stdout;
1421
+ if (attempt + 1 < attempts) {
1422
+ await delay(100 * (attempt + 1));
1423
+ continue;
1424
+ }
1425
+ if (options.allowFailure)
1426
+ return "";
1427
+ const detail = result.stderr.trim() || result.stdout.trim() || "Git did not provide an error message.";
1428
+ throw new VcmError({
1429
+ code: "GATE_REVIEW_GIT_COMMAND_FAILED",
1430
+ message: `Git ${args.join(" ")} failed in ${cwd} (exit ${result.exitCode}): ${detail}`,
1431
+ statusCode: 409
1432
+ });
1433
+ }
1434
+ return "";
1401
1435
  }
1402
1436
  function splitLines(value) {
1403
1437
  return value
@@ -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
  }
@@ -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 HARNESS_FILES = [
@@ -1405,9 +1406,12 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
1405
1406
  needsApply: plannedChanges.length > 0,
1406
1407
  plannedChanges,
1407
1408
  codeIntelligence,
1408
- warnings: plannedChanges.length > 0
1409
- ? ["Review and commit VCM Harness changes before starting long-running work."]
1410
- : []
1409
+ warnings: [
1410
+ ...(plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []),
1411
+ ...((codeIntelligence?.languages ?? [])
1412
+ .filter((language) => language.state !== "server_runnable")
1413
+ .map((language) => `Architect LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`}`))
1414
+ ]
1411
1415
  };
1412
1416
  }
1413
1417
  async function readHarnessRevisionValue(fs, repoRoot) {
@@ -4,3 +4,16 @@ 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
+ }
@@ -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,
@@ -353,7 +355,7 @@ export function createMessageService(deps) {
353
355
  return;
354
356
  }
355
357
  if (current.fromRole === PM_ROLE) {
356
- await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
358
+ await deps.workflowControlService?.markDispatchUnconfirmed(toWorkflowContext(input), messageId, failureReason);
357
359
  }
358
360
  await appendMessageSnapshot(deps.fs, input, {
359
361
  ...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, 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,13 @@ export function createSessionService(deps) {
82
82
  : resumeClaudeSessionId
83
83
  ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
84
84
  : undefined;
85
+ const lspPluginDirs = roleUsesLsp(role)
86
+ ? deps.codeIntelligenceDetector
87
+ ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, await deps.codeIntelligenceDetector.detect(taskRepoRoot))
88
+ : [VCM_LSP_PLUGIN_DIR]
89
+ : [];
85
90
  const startCommand = {
86
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt, roleUsesLsp(role) ? [VCM_LSP_PLUGIN_DIR] : []),
91
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt, lspPluginDirs),
87
92
  cwd: taskRepoRoot
88
93
  };
89
94
  const runtimeSessionToken = randomUUID();
@@ -104,7 +109,7 @@ export function createSessionService(deps) {
104
109
  VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
105
110
  }, modelEnvironment, {
106
111
  ...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
107
- ...(roleUsesLsp(role)
112
+ ...(lspPluginDirs.length > 0
108
113
  ? {
109
114
  ENABLE_LSP_TOOL: "true",
110
115
  ENABLE_TOOL_SEARCH: "false"
@@ -1608,9 +1613,13 @@ function normalizeClaudePermissionMode(value) {
1608
1613
  return "default";
1609
1614
  }
1610
1615
  function normalizeClaudeModel(value) {
1611
- if (value === "claude-fable-5-1"
1612
- || value === "opus"
1613
- || value === "claude-opus-4-8"
1616
+ if (value === "claude-fable-5-1") {
1617
+ return "fable";
1618
+ }
1619
+ if (value === "claude-opus-4-8") {
1620
+ return "opus";
1621
+ }
1622
+ if (value === "opus"
1614
1623
  || value === "sonnet"
1615
1624
  || value === "fable"
1616
1625
  || isCodexBridgeSessionModel(value)) {
@@ -215,18 +215,36 @@ export function createWorkflowControlService(deps) {
215
215
  status: "pending",
216
216
  routeContentHash: undefined,
217
217
  messageId: undefined,
218
+ confirmationError: undefined,
218
219
  updatedAt: now()
219
220
  },
220
221
  updatedAt: now()
221
222
  });
222
223
  });
223
224
  }
225
+ async function markDispatchUnconfirmed(input, messageId, reason) {
226
+ await withLock(statePath(input), async () => {
227
+ const state = await getState(input);
228
+ const pending = state.pendingDispatch;
229
+ if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId)
230
+ return;
231
+ await saveState(input, {
232
+ ...state,
233
+ pendingDispatch: { ...pending, confirmationError: reason, updatedAt: now() },
234
+ updatedAt: now()
235
+ });
236
+ });
237
+ }
224
238
  async function cancelPendingDispatch(input) {
225
239
  await withLock(statePath(input), async () => {
226
240
  const state = await getState(input);
227
241
  const pending = state.pendingDispatch;
228
242
  if (!pending)
229
243
  return;
244
+ if (pending.status === "dispatching" && !pending.confirmationError) {
245
+ throw workflowError("WORKFLOW_DISPATCH_PENDING", `Message ${pending.messageId} is being delivered and cannot be cleared before confirmation.`);
246
+ }
247
+ const timestamp = now();
230
248
  const progress = await readProgress(deps.fs, input);
231
249
  await commitStateAndProgress(input, renderWorkflowProgress({
232
250
  ...progress,
@@ -234,16 +252,18 @@ export function createWorkflowControlService(deps) {
234
252
  }), {
235
253
  ...state,
236
254
  pendingDispatch: null,
237
- userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending.overrideAuthorizationId),
238
- userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending.followUpApprovalId),
239
- updatedAt: now()
255
+ ...abandonPendingApprovals(state, pending, "Pending route was cleared before confirmation.", timestamp),
256
+ updatedAt: timestamp
240
257
  });
241
258
  });
242
259
  }
243
260
  async function confirmDispatch(input, messageId) {
244
261
  await withLock(statePath(input), async () => {
245
262
  const state = await getState(input);
246
- failOnStateWarnings(state);
263
+ failOnStateWarnings({
264
+ ...state,
265
+ warnings: state.warnings.filter((warning) => warning !== unconfirmedDispatchWarning(state.pendingDispatch))
266
+ });
247
267
  failWhileAwaitingUser(state);
248
268
  const pending = state.pendingDispatch;
249
269
  if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId) {
@@ -293,8 +313,24 @@ export function createWorkflowControlService(deps) {
293
313
  return withLock(statePath(input), async () => {
294
314
  await recoverCommitJournal(input);
295
315
  const state = await getState(input);
296
- if (state.warnings.length > 0 || state.pendingDispatch?.status !== "dispatching")
316
+ if (state.pendingDispatch?.status !== "dispatching"
317
+ || state.warnings.some((warning) => warning !== unconfirmedDispatchWarning(state.pendingDispatch)))
297
318
  return false;
319
+ if (await wasDispatchDelivered(deps.fs, input, state.pendingDispatch.messageId)) {
320
+ if (state.pendingDispatch.confirmationError)
321
+ return false;
322
+ const timestamp = now();
323
+ await saveState(input, {
324
+ ...state,
325
+ pendingDispatch: {
326
+ ...state.pendingDispatch,
327
+ confirmationError: "VCM restarted before the delivered message received UserPromptSubmit confirmation.",
328
+ updatedAt: timestamp
329
+ },
330
+ updatedAt: timestamp
331
+ });
332
+ return true;
333
+ }
298
334
  await saveState(input, {
299
335
  ...state,
300
336
  pendingDispatch: {
@@ -302,6 +338,7 @@ export function createWorkflowControlService(deps) {
302
338
  status: "pending",
303
339
  routeContentHash: undefined,
304
340
  messageId: undefined,
341
+ confirmationError: undefined,
305
342
  updatedAt: now()
306
343
  },
307
344
  updatedAt: now()
@@ -323,6 +360,7 @@ export function createWorkflowControlService(deps) {
323
360
  assertRouteAuthorized,
324
361
  claimDispatch,
325
362
  releaseDispatch,
363
+ markDispatchUnconfirmed,
326
364
  cancelPendingDispatch,
327
365
  confirmDispatch,
328
366
  recoverTask
@@ -361,10 +399,12 @@ export function createWorkflowControlService(deps) {
361
399
  async function requestUserInput(input, question) {
362
400
  return withLock(statePath(input), async () => {
363
401
  const state = await getState(input);
364
- failOnStateWarnings(state);
365
402
  if (state.pendingDispatch?.status === "dispatching") {
366
- throw workflowError("WORKFLOW_DISPATCH_PENDING", "A role dispatch is already being submitted and cannot be canceled by a user question.", "Wait for the target UserPromptSubmit confirmation or recover the dispatch before asking the user.");
403
+ throw workflowError("WORKFLOW_DISPATCH_PENDING", state.pendingDispatch.confirmationError
404
+ ? `A delivered role dispatch is unconfirmed: ${state.pendingDispatch.confirmationError}`
405
+ : "A role dispatch is already being submitted and cannot be canceled by a user question.", "Inspect the target session and confirm or clear the pending dispatch before asking the user.");
367
406
  }
407
+ failOnStateWarnings(state);
368
408
  const normalizedQuestion = question.trim();
369
409
  if (!normalizedQuestion) {
370
410
  throw workflowError("WORKFLOW_USER_QUESTION_REQUIRED", "A non-empty user question is required.");
@@ -378,8 +418,7 @@ export function createWorkflowControlService(deps) {
378
418
  requestedAt: timestamp
379
419
  },
380
420
  pendingDispatch: null,
381
- userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending?.overrideAuthorizationId),
382
- userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending?.followUpApprovalId),
421
+ ...(pending ? abandonPendingApprovals(state, pending, "PM asked the user before dispatch confirmation.", timestamp) : {}),
383
422
  updatedAt: timestamp
384
423
  };
385
424
  if (pending) {
@@ -1226,6 +1265,9 @@ function validateCandidateAgainstCurrent(current, candidate) {
1226
1265
  function stateProgressConsistencyWarnings(state, progress) {
1227
1266
  const warnings = [];
1228
1267
  const pending = state.pendingDispatch;
1268
+ const unconfirmedWarning = unconfirmedDispatchWarning(pending);
1269
+ if (unconfirmedWarning)
1270
+ warnings.push(unconfirmedWarning);
1229
1271
  if (!pending && progress.proposal) {
1230
1272
  warnings.push("Workflow Progress contains a dispatch proposal without a runtime approval.");
1231
1273
  }
@@ -1248,6 +1290,40 @@ function stateProgressConsistencyWarnings(state, progress) {
1248
1290
  }
1249
1291
  return warnings;
1250
1292
  }
1293
+ function unconfirmedDispatchWarning(pending) {
1294
+ return pending?.confirmationError
1295
+ ? `PM dispatch ${pending.messageId ?? "unknown"} to ${pending.targetRole} was delivered but not confirmed: ${pending.confirmationError} Inspect the target session before clearing or retrying this route.`
1296
+ : undefined;
1297
+ }
1298
+ function abandonPendingApprovals(state, pending, reason, timestamp) {
1299
+ return {
1300
+ userAuthorizations: state.userAuthorizations.map((entry) => entry.id === pending.overrideAuthorizationId
1301
+ ? { ...entry, status: "abandoned", abandonedAt: timestamp, abandonReason: reason }
1302
+ : entry),
1303
+ userApprovedFollowUps: state.userApprovedFollowUps.map((entry) => entry.id === pending.followUpApprovalId
1304
+ ? { ...entry, status: "abandoned", abandonedAt: timestamp, abandonReason: reason }
1305
+ : entry)
1306
+ };
1307
+ }
1308
+ async function wasDispatchDelivered(fs, input, messageId) {
1309
+ if (!messageId)
1310
+ return false;
1311
+ const messagesPath = path.join(input.taskRepoRoot, input.stateRoot, "messages", `${input.taskSlug}.jsonl`);
1312
+ if (!(await fs.pathExists(messagesPath)))
1313
+ return false;
1314
+ try {
1315
+ let latest;
1316
+ for (const line of (await fs.readText(messagesPath)).split(/\r?\n/).filter(Boolean)) {
1317
+ const message = JSON.parse(line);
1318
+ if (message.id === messageId)
1319
+ latest = message;
1320
+ }
1321
+ return Boolean(latest?.deliveredAt);
1322
+ }
1323
+ catch {
1324
+ return true;
1325
+ }
1326
+ }
1251
1327
  function createUserAuthorization(authorizationId, current, candidate, effectiveFlow, baseHistoryHash, violation, authorizationText, timestamp) {
1252
1328
  return {
1253
1329
  id: authorizationId,
@@ -1492,6 +1568,7 @@ function isPendingDispatch(value) {
1492
1568
  && (value.status === "pending" || value.status === "dispatching")
1493
1569
  && (value.routeContentHash === undefined || typeof value.routeContentHash === "string")
1494
1570
  && (value.messageId === undefined || typeof value.messageId === "string")
1571
+ && (value.confirmationError === undefined || typeof value.confirmationError === "string")
1495
1572
  && typeof value.createdAt === "string"
1496
1573
  && typeof value.updatedAt === "string";
1497
1574
  }
@@ -1499,7 +1576,7 @@ function isUserAuthorization(value) {
1499
1576
  if (!isRecord(value))
1500
1577
  return false;
1501
1578
  return typeof value.id === "string"
1502
- && (value.status === "accepted" || value.status === "consumed")
1579
+ && (value.status === "accepted" || value.status === "consumed" || value.status === "abandoned")
1503
1580
  && value.role === "project-manager"
1504
1581
  && value.operation === "workflow-dispatch"
1505
1582
  && Number.isInteger(value.baseRevision)
@@ -1511,13 +1588,15 @@ function isUserAuthorization(value) {
1511
1588
  && typeof value.violatedRule === "string"
1512
1589
  && typeof value.authorizationText === "string"
1513
1590
  && typeof value.createdAt === "string"
1514
- && (value.consumedAt === undefined || typeof value.consumedAt === "string");
1591
+ && (value.consumedAt === undefined || typeof value.consumedAt === "string")
1592
+ && (value.abandonedAt === undefined || typeof value.abandonedAt === "string")
1593
+ && (value.abandonReason === undefined || typeof value.abandonReason === "string");
1515
1594
  }
1516
1595
  function isUserApprovedFollowUp(value) {
1517
1596
  if (!isRecord(value))
1518
1597
  return false;
1519
1598
  return typeof value.id === "string"
1520
- && (value.status === "accepted" || value.status === "consumed")
1599
+ && (value.status === "accepted" || value.status === "consumed" || value.status === "abandoned")
1521
1600
  && value.role === "project-manager"
1522
1601
  && value.operation === "post-validation-follow-up"
1523
1602
  && Number.isInteger(value.baseRevision)
@@ -1529,7 +1608,9 @@ function isUserApprovedFollowUp(value) {
1529
1608
  && typeof value.evidence === "string"
1530
1609
  && typeof value.approvalText === "string"
1531
1610
  && typeof value.createdAt === "string"
1532
- && (value.consumedAt === undefined || typeof value.consumedAt === "string");
1611
+ && (value.consumedAt === undefined || typeof value.consumedAt === "string")
1612
+ && (value.abandonedAt === undefined || typeof value.abandonedAt === "string")
1613
+ && (value.abandonReason === undefined || typeof value.abandonReason === "string");
1533
1614
  }
1534
1615
  function normalizeDispatchEvidenceBaseline(value) {
1535
1616
  if (!isRecord(value) || !isRecord(value.artifactHashes) || !isRecord(value.gateFingerprints))