vibe-coding-master 0.8.13 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/adapters/claude-adapter.js +2 -1
  3. package/dist/backend/cli/install-vcm-harness.js +66 -16
  4. package/dist/backend/gateway/gateway-service.js +5 -3
  5. package/dist/backend/runtime/terminal-submit.js +1 -0
  6. package/dist/backend/server.js +14 -4
  7. package/dist/backend/services/app-settings-service.js +3 -2
  8. package/dist/backend/services/auto-memory-service.js +4 -3
  9. package/dist/backend/services/claude-hook-service.js +6 -0
  10. package/dist/backend/services/claude-transcript-service.js +2 -13
  11. package/dist/backend/services/claude-version-check.js +87 -0
  12. package/dist/backend/services/gate-review-service.js +46 -8
  13. package/dist/backend/services/harness-service.js +41 -20
  14. package/dist/backend/services/message-service.js +30 -6
  15. package/dist/backend/services/project-service.js +8 -2
  16. package/dist/backend/services/runtime-recovery-service.js +36 -9
  17. package/dist/backend/services/session-service.js +10 -19
  18. package/dist/backend/services/task-close-service.js +1 -0
  19. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  20. package/dist/backend/services/workflow-control-service.js +55 -11
  21. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  22. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  23. package/dist/backend/templates/harness/gate-review.js +676 -643
  24. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  25. package/dist/backend/templates/harness/project-owned-history.js +21 -0
  26. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  27. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  28. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  29. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  30. package/dist/shared/types/app-settings.js +31 -0
  31. package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
  32. package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
  33. package/dist-frontend/index.html +2 -2
  34. package/package.json +1 -1
  35. package/scripts/harness-tools/check-durable-docs +332 -298
  36. package/scripts/harness-tools/generate-module-index +1047 -512
  37. package/scripts/harness-tools/generate-public-surface +1231 -1069
  38. package/scripts/harness-tools/package.json +3 -0
  39. package/scripts/harness-tools/run-long-check +608 -531
  40. package/scripts/harness-tools/vcm-artifact +181 -113
  41. package/scripts/harness-tools/vcm-bash-guard +839 -605
  42. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  43. package/scripts/harness-tools/watch-job +313 -238
  44. package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
package/README.md CHANGED
@@ -20,7 +20,8 @@ orchestration, translation, harness setup, and mobile gateway control.
20
20
  - Node.js 20 LTS or 22+
21
21
  - npm
22
22
  - Git
23
- - Claude Code installed and available as `claude`
23
+ - Claude Code installed and available as `claude` (verified with 2.1.310 or
24
+ newer; VCM shows a Repository warning when an older release is detected)
24
25
  - Claude Code authenticated in the same environment where VCM runs
25
26
 
26
27
  For Linux containers, make sure common native build/runtime tools are available:
@@ -306,7 +307,10 @@ Codex Bridge child processes use a hard context limit and auto-compaction window
306
307
  (about `232560` tokens), leaving headroom before the Codex API limit. Native
307
308
  Claude sessions receive no VCM-owned context override. Bridge-backed subagents
308
309
  inherit the selected GPT model. Native Architect evidence, scaffold, and
309
- validation workers use `opus` with `xhigh` effort.
310
+ validation worker model and effort are set under `Architect workers` in the
311
+ Harness panel (defaults: evidence `opus`/`high`, scaffold `sonnet`/`medium`,
312
+ validation `sonnet`/`low`); apply the harness to write a change into the worker
313
+ definitions.
310
314
 
311
315
  Resume keeps the provider recorded by the existing Session. Use Restart when
312
316
  switching between native Claude and Codex Bridge. If the Bridge is disabled,
@@ -464,7 +468,7 @@ Common commands:
464
468
  /role
465
469
  /role <pm|architect|coder|tester|reviewer>
466
470
  /projects
467
- /use-project <index-or-path>
471
+ /use-project <index-or-listed-path>
468
472
  /pull-current
469
473
  /tasks
470
474
  /use-task <index-or-task-slug>
@@ -8,7 +8,8 @@ export function createClaudeAdapter(runner) {
8
8
  return result.exitCode === 0;
9
9
  },
10
10
  async getVersion(command = "claude") {
11
- const result = await runner.run(command, ["--version"]);
11
+ // Bounded so a stalled CLI cannot leave a probe process behind.
12
+ const result = await runner.run(command, ["--version"], { timeoutMs: 10_000 });
12
13
  if (result.exitCode !== 0) {
13
14
  throw new VcmError({
14
15
  code: "CLAUDE_UNAVAILABLE",
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-nocheck
3
+ import { createHash } from "node:crypto";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import process from "node:process";
@@ -36,7 +37,10 @@ import { renderVcmWorkflowReviewSkillRules } from "../templates/harness/vcm-work
36
37
  import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
37
38
  import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
38
39
  import { renderResolveDurableDocAssignmentTool } from "../templates/harness/resolve-durable-doc-assignment.js";
40
+ import { PROJECT_OWNED_SHIPPED_SHA256 } from "../templates/harness/project-owned-history.js";
39
41
  import { readVcmPackageVersion } from "../app-version.js";
42
+ import { DEFAULT_ARCHITECT_WORKER_SETTINGS, normalizeArchitectWorkerSettings } from "../../shared/types/app-settings.js";
43
+ import { ARCHITECT_WORKER_AGENT_TYPES, applyArchitectWorkerSetting } from "../templates/harness/architect-worker-frontmatter.js";
40
44
  const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
41
45
  const APP_ROOT = path.resolve(CLI_DIR, "../../..");
42
46
  const VCM_PACKAGE_VERSION = readVcmPackageVersion(APP_ROOT);
@@ -54,8 +58,8 @@ const LEGACY_CODEX_HARNESS_PATHS = [
54
58
  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
59
  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
60
  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
- 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
- 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"'`;
61
+ const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; node "$guard" || exit 0'`;
62
+ 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; }; node "$guard"'`;
59
63
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
60
64
  const VCM_AUTO_MEMORY_ENABLED = false;
61
65
  const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
@@ -104,20 +108,14 @@ const AGENT_FRONTMATTER = {
104
108
  },
105
109
  "vcm-architect-evidence-worker": {
106
110
  description: "Foreground Architect worker for bounded project evidence collection.",
107
- tools: "Read, Grep, Glob, Write",
108
- model: "opus",
109
- effort: "xhigh"
111
+ tools: "Read, Grep, Glob, Write"
110
112
  },
111
113
  "vcm-architect-scaffold-worker": {
112
- description: "Foreground Architect worker for exact scaffold execution and scaffold validation.",
113
- model: "opus",
114
- effort: "xhigh"
114
+ description: "Foreground Architect worker for exact scaffold execution and scaffold validation."
115
115
  },
116
116
  "vcm-architect-validation-worker": {
117
117
  description: "Foreground Architect worker for exact assigned command execution and evidence capture.",
118
- tools: "Read, Grep, Glob, Bash, Write",
119
- model: "opus",
120
- effort: "xhigh"
118
+ tools: "Read, Grep, Glob, Bash, Write"
121
119
  }
122
120
  };
123
121
  const REQUIRED_AGENT_DISALLOWED_TOOLS = {
@@ -289,6 +287,8 @@ const DURABLE_DOC_TEMPLATES = [
289
287
  content: "# Testing\n"
290
288
  },
291
289
  ];
290
+ // An installed copy that still matches a VCM-shipped version is upgraded
291
+ // instead of kept as project-modified.
292
292
  const PROJECT_OWNED_FILES = [
293
293
  {
294
294
  path: ".ai/tools/generate-module-index",
@@ -304,6 +304,15 @@ const PROJECT_OWNED_FILES = [
304
304
  }
305
305
  ];
306
306
  const WHOLE_FILES = [
307
+ {
308
+ // Makes the extensionless tools load as CommonJS inside ESM projects. A
309
+ // project's own package.json here is kept rather than replaced.
310
+ path: ".ai/tools/package.json",
311
+ category: "runtime-tool",
312
+ mode: 0o644,
313
+ templatePath: "scripts/harness-tools/package.json",
314
+ preserveExisting: true
315
+ },
307
316
  {
308
317
  path: ".ai/tools/check-durable-docs",
309
318
  category: "durable-docs-tool",
@@ -489,7 +498,7 @@ async function main() {
489
498
  await migrateReviewerAgent({ projectRoot, dryRun, operations });
490
499
  const manifest = await buildManifest(projectRoot);
491
500
  for (const definition of MANAGED_FILES) {
492
- await installManagedFile({ projectRoot, definition, dryRun, operations });
501
+ await installManagedFile({ projectRoot, definition, dryRun, operations, workerSettings: args.architectWorkers });
493
502
  }
494
503
  for (const template of DURABLE_DOC_TEMPLATES) {
495
504
  await installDurableDocTemplate({ projectRoot, template, dryRun, operations });
@@ -545,7 +554,8 @@ function parseArgs(argv) {
545
554
  const args = {
546
555
  dryRun: false,
547
556
  help: false,
548
- projectRoot: undefined
557
+ projectRoot: undefined,
558
+ architectWorkers: DEFAULT_ARCHITECT_WORKER_SETTINGS
549
559
  };
550
560
  for (let index = 0; index < argv.length; index += 1) {
551
561
  const arg = argv[index];
@@ -557,6 +567,20 @@ function parseArgs(argv) {
557
567
  args.dryRun = true;
558
568
  continue;
559
569
  }
570
+ if (arg === "--architect-workers") {
571
+ const value = argv[index + 1];
572
+ if (!value) {
573
+ fail("--architect-workers requires a JSON object.");
574
+ }
575
+ try {
576
+ args.architectWorkers = normalizeArchitectWorkerSettings(JSON.parse(value));
577
+ }
578
+ catch {
579
+ fail("--architect-workers must be valid JSON.");
580
+ }
581
+ index += 1;
582
+ continue;
583
+ }
560
584
  if (arg.startsWith("--")) {
561
585
  fail(`Unknown option: ${arg}`);
562
586
  }
@@ -571,6 +595,7 @@ function printUsage() {
571
595
  console.log(`Usage:
572
596
  node scripts/install-vcm-harness.mjs <project-root>
573
597
  node scripts/install-vcm-harness.mjs <project-root> --dry-run
598
+ node scripts/install-vcm-harness.mjs <project-root> --architect-workers '<json>'
574
599
 
575
600
  Installs only fixed VCM harness content.
576
601
 
@@ -740,7 +765,7 @@ async function installManifest({ projectRoot, manifest, dryRun, operations, forc
740
765
  action: "write fixed harness manifest"
741
766
  });
742
767
  }
743
- async function installManagedFile({ projectRoot, definition, dryRun, operations }) {
768
+ async function installManagedFile({ projectRoot, definition, dryRun, operations, workerSettings = DEFAULT_ARCHITECT_WORKER_SETTINGS }) {
744
769
  const targetPath = resolveInside(projectRoot, definition.path);
745
770
  const block = renderManagedBlock(definition);
746
771
  const currentContent = await readOptionalText(targetPath);
@@ -774,6 +799,10 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
774
799
  for (const removedSkill of REMOVED_AGENT_SKILLS[definition.agentName] ?? []) {
775
800
  nextContent = removeAgentSkill(nextContent, removedSkill);
776
801
  }
802
+ const workerType = ARCHITECT_WORKER_AGENT_TYPES[definition.agentName];
803
+ if (workerType) {
804
+ nextContent = applyArchitectWorkerSetting(nextContent, workerSettings[workerType]);
805
+ }
777
806
  await writeIfChanged({
778
807
  targetPath,
779
808
  relativePath: definition.path,
@@ -1017,6 +1046,15 @@ async function ensureDirectory({ projectRoot, relativePath, dryRun, operations }
1017
1046
  }
1018
1047
  async function installWholeFile({ projectRoot, file, dryRun, operations }) {
1019
1048
  const content = await wholeFileContent(file);
1049
+ if (file.preserveExisting) {
1050
+ const currentContent = await readOptionalText(resolveInside(projectRoot, file.path));
1051
+ if (currentContent !== undefined && currentContent !== ensureTrailingNewline(content)) {
1052
+ operations.push(skip(file.path, isCommonJsPackage(currentContent)
1053
+ ? "exists; project-owned"
1054
+ : "exists; project-owned, but harness tools need \"type\": \"commonjs\" here"));
1055
+ return;
1056
+ }
1057
+ }
1020
1058
  await writeIfChanged({
1021
1059
  targetPath: resolveInside(projectRoot, file.path),
1022
1060
  relativePath: file.path,
@@ -1029,7 +1067,10 @@ async function installWholeFile({ projectRoot, file, dryRun, operations }) {
1029
1067
  }
1030
1068
  async function installProjectOwnedFile({ projectRoot, file, dryRun, operations }) {
1031
1069
  const targetPath = resolveInside(projectRoot, file.path);
1032
- if (await pathExists(targetPath)) {
1070
+ const currentContent = await readOptionalText(targetPath);
1071
+ const upgradeable = currentContent !== undefined
1072
+ && (PROJECT_OWNED_SHIPPED_SHA256[file.path] ?? []).includes(createHash("sha256").update(currentContent).digest("hex"));
1073
+ if (currentContent !== undefined && !upgradeable) {
1033
1074
  operations.push(skip(file.path, "exists; project-owned"));
1034
1075
  return;
1035
1076
  }
@@ -1041,7 +1082,7 @@ async function installProjectOwnedFile({ projectRoot, file, dryRun, operations }
1041
1082
  mode: file.mode,
1042
1083
  dryRun,
1043
1084
  operations,
1044
- action: "seed project-owned VCM file"
1085
+ action: upgradeable ? "upgrade unmodified project-owned VCM file" : "seed project-owned VCM file"
1045
1086
  });
1046
1087
  }
1047
1088
  async function removeLegacyFlatSkillFiles({ projectRoot, dryRun, operations }) {
@@ -1085,6 +1126,15 @@ async function removeLegacyCodexHarnessPaths({ projectRoot, dryRun, operations }
1085
1126
  operations.push(done(relativePath, "deleted legacy Codex harness path"));
1086
1127
  }
1087
1128
  }
1129
+ function isCommonJsPackage(content) {
1130
+ try {
1131
+ const parsed = JSON.parse(content);
1132
+ return parsed?.type === undefined || parsed.type === "commonjs";
1133
+ }
1134
+ catch {
1135
+ return false;
1136
+ }
1137
+ }
1088
1138
  function stripSkillFrontmatter(content) {
1089
1139
  return content.replace(/^---\n[\s\S]*?\n---\n\n/, "");
1090
1140
  }
@@ -451,14 +451,16 @@ export function createGatewayService(deps) {
451
451
  async function useProject(selector) {
452
452
  const options = await listProjectOptions();
453
453
  const index = Number.parseInt(selector, 10);
454
+ // Gateway senders may select only projects already connected on the desktop.
454
455
  const repoPath = Number.isFinite(index) && String(index) === selector.trim()
455
456
  ? options[index - 1]
456
- : selector.trim();
457
+ : options.find((option) => option === selector.trim());
457
458
  if (!repoPath) {
458
459
  throw new VcmError({
459
460
  code: "PROJECT_SELECTION_INVALID",
460
461
  message: `Project not found: ${selector}`,
461
- statusCode: 404
462
+ statusCode: 404,
463
+ hint: "Use /projects and select a listed project. Connect new repositories from the desktop app."
462
464
  });
463
465
  }
464
466
  const project = await deps.projectService.connectProject({ repoPath });
@@ -1563,7 +1565,7 @@ function helpText(enabled) {
1563
1565
  "/role",
1564
1566
  "/role <pm|architect|coder|tester|reviewer>",
1565
1567
  "/projects",
1566
- "/use-project <index-or-path>",
1568
+ "/use-project <index-or-listed-path>",
1567
1569
  "/pull-current",
1568
1570
  "/tasks",
1569
1571
  "/use-task <index-or-task-slug>",
@@ -4,6 +4,7 @@ const DEFAULT_ENTER_DELAY_MS = 75;
4
4
  export async function submitTerminalInput(runtime, sessionId, text, options = {}) {
5
5
  runtime.write(sessionId, formatTerminalPaste(text));
6
6
  await delay(options.enterDelayMs ?? DEFAULT_ENTER_DELAY_MS);
7
+ await options.beforeSubmit?.();
7
8
  runtime.write(sessionId, "\r");
8
9
  }
9
10
  export function formatTerminalPaste(text) {
@@ -8,6 +8,7 @@ import { createCodexBridgeAdapter } from "./adapters/codex-bridge-adapter.js";
8
8
  import { createCommandRunner } from "./adapters/command-runner.js";
9
9
  import { createCommandDispatcher } from "./services/command-dispatcher.js";
10
10
  import { createClaudeHookService } from "./services/claude-hook-service.js";
11
+ import { createClaudeVersionCheck } from "./services/claude-version-check.js";
11
12
  import { createGitAdapter } from "./adapters/git-adapter.js";
12
13
  import { createAppSettingsService } from "./services/app-settings-service.js";
13
14
  import { createCodexBridgeIntegrationService } from "./services/codex-bridge-integration-service.js";
@@ -248,7 +249,12 @@ export function createDefaultServerDeps(options = {}) {
248
249
  workflowControlService,
249
250
  getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role)
250
251
  });
251
- const projectService = createProjectService({ fs, git, appSettings });
252
+ const projectService = createProjectService({
253
+ fs,
254
+ git,
255
+ appSettings,
256
+ claudeVersionCheck: createClaudeVersionCheck(claude)
257
+ });
252
258
  const taskService = createTaskService({ fs, git, artifactService, projectService });
253
259
  const taskWorkflowService = createTaskWorkflowService({ fs });
254
260
  const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(fs, { runner });
@@ -272,6 +278,7 @@ export function createDefaultServerDeps(options = {}) {
272
278
  runtime,
273
279
  harnessEngineerSessions: sessionService,
274
280
  runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
281
+ getArchitectWorkerSettings: async () => (await appSettings.getPreferences()).architectWorkerSettings,
275
282
  vcmVersion
276
283
  });
277
284
  const autoMemoryService = createAutoMemoryService({
@@ -388,7 +395,8 @@ export function createDefaultServerDeps(options = {}) {
388
395
  projectService,
389
396
  taskWorkflowService,
390
397
  architectRestartService,
391
- roleContextRestartService
398
+ roleContextRestartService,
399
+ gateReviewService
392
400
  });
393
401
  const gatewayService = createGatewayService({
394
402
  fs,
@@ -432,7 +440,8 @@ export function createDefaultServerDeps(options = {}) {
432
440
  translationWorkerService,
433
441
  architectRestartService,
434
442
  roleContextRestartService,
435
- workflowControlService
443
+ workflowControlService,
444
+ gateReviewService
436
445
  });
437
446
  const runtimeCoordinator = createRuntimeCoordinatorService({
438
447
  appSettings,
@@ -461,7 +470,8 @@ export function createDefaultServerDeps(options = {}) {
461
470
  projectService,
462
471
  taskService,
463
472
  sessionService,
464
- roundService
473
+ roundService,
474
+ gateReviewService
465
475
  });
466
476
  const diagnosticsService = createDiagnosticsService({
467
477
  appRoot,
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
3
  import { VCM_ROLE_NAMES } from "../../shared/constants.js";
4
4
  import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
5
- import { createDefaultLaunchTemplate, createDefaultToolSessionDefaults, DEFAULT_TRANSLATION_OUTPUT_MODE, DEFAULT_TRANSLATION_TARGET_LANGUAGE, TRANSLATION_OUTPUT_MODE_OPTIONS, TRANSLATION_TARGET_LANGUAGE_OPTIONS } from "../../shared/types/app-settings.js";
5
+ import { createDefaultLaunchTemplate, createDefaultToolSessionDefaults, DEFAULT_TRANSLATION_OUTPUT_MODE, normalizeArchitectWorkerSettings, DEFAULT_TRANSLATION_TARGET_LANGUAGE, TRANSLATION_OUTPUT_MODE_OPTIONS, TRANSLATION_TARGET_LANGUAGE_OPTIONS } from "../../shared/types/app-settings.js";
6
6
  import { CLAUDE_MODEL_OPTIONS, isCodexBridgeSessionModel, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
7
7
  import { resolveVcmDataDir } from "../vcm-data-dir.js";
8
8
  const MAX_RECENT_REPOSITORIES = 5;
@@ -290,7 +290,8 @@ function normalizePreferences(input) {
290
290
  translationTargetLanguage: normalizeTranslationTargetLanguage(candidate.translationTargetLanguage),
291
291
  translationOutputMode: normalizeTranslationOutputMode(candidate.translationOutputMode),
292
292
  launchTemplate: normalizeLaunchTemplate(candidate.launchTemplate),
293
- toolSessionDefaults: normalizeToolSessionDefaults(candidate.toolSessionDefaults)
293
+ toolSessionDefaults: normalizeToolSessionDefaults(candidate.toolSessionDefaults),
294
+ architectWorkerSettings: normalizeArchitectWorkerSettings(candidate.architectWorkerSettings)
294
295
  };
295
296
  }
296
297
  function normalizeThemeMode(input) {
@@ -855,11 +855,12 @@ export function createAutoMemoryService(deps) {
855
855
  if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
856
856
  return existing;
857
857
  }
858
- const preferences = await deps.appSettings.getPreferences();
859
- const options = preferences.launchTemplate.roles[role];
860
858
  if (existing?.claudeSessionId) {
861
- return deps.sessionService.resumeRoleSession(baseRepoRoot, taskSlug, role, options);
859
+ // Resume keeps the launch options recorded by the saved Session.
860
+ return deps.sessionService.resumeRoleSession(baseRepoRoot, taskSlug, role);
862
861
  }
862
+ const preferences = await deps.appSettings.getPreferences();
863
+ const options = preferences.launchTemplate.roles[role];
863
864
  return deps.sessionService.startRoleSession(baseRepoRoot, taskSlug, role, options);
864
865
  }
865
866
  async function snapshotArchitectPlanningCandidate(taskRepoRoot, runId) {
@@ -225,6 +225,9 @@ export function createClaudeHookService(deps) {
225
225
  await deps.workflowControlService.resolveUserInput(createWorkflowControlContext(context));
226
226
  }
227
227
  }
228
+ if (isReviewerRoleName(input.role)) {
229
+ deps.gateReviewService?.recordReviewerPromptSubmitted(context.taskSlug, prompt);
230
+ }
228
231
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
229
232
  taskSlug: context.taskSlug,
230
233
  role: input.role,
@@ -508,6 +511,9 @@ export function createClaudeHookService(deps) {
508
511
  if (!session) {
509
512
  return completedHookResult(input, eventName);
510
513
  }
514
+ if (isReviewerRoleName(input.role)) {
515
+ deps.gateReviewService?.recordReviewerTurnEnded(context.taskSlug, "turn-end");
516
+ }
511
517
  const boundToTask = await isHookSessionBoundToTask(context, input.role);
512
518
  if (boundToTask) {
513
519
  await deps.roundService.recordClaudeHookEvent({
@@ -210,10 +210,8 @@ export function resolveExistingClaudeTranscriptPath(session) {
210
210
  if (sessionPath) {
211
211
  return sessionPath;
212
212
  }
213
- const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId, session.claudeConfigDir));
214
- if (cwdPath) {
215
- return cwdPath;
216
- }
213
+ // Without a hook-reported path, locate the transcript by session id instead of
214
+ // reproducing Claude Code's undocumented project-directory encoding.
217
215
  return findClaudeTranscriptPathBySessionId(session.claudeSessionId, session.claudeConfigDir);
218
216
  }
219
217
  function transcriptPathMatchesSession(transcriptPath, claudeSessionId) {
@@ -263,15 +261,6 @@ function existingFile(candidate) {
263
261
  export function claudeProjectsRoot(configDir) {
264
262
  return join(configDir ?? join(homedir(), ".claude"), "projects");
265
263
  }
266
- export function projectHash(projectDir) {
267
- return projectDir.replace(/[\/\s]+/g, "-");
268
- }
269
- export function projectsTranscriptDir(projectDir, configDir) {
270
- return join(claudeProjectsRoot(configDir), projectHash(projectDir));
271
- }
272
- export function claudeTranscriptPath(projectDir, claudeSessionId, configDir) {
273
- return join(projectsTranscriptDir(projectDir, configDir), `${claudeSessionId}.jsonl`);
274
- }
275
264
  export function parseAssistantContent(line) {
276
265
  let obj;
277
266
  try {
@@ -0,0 +1,87 @@
1
+ // The oldest Claude Code release VCM has been verified against. Raise it after
2
+ // verifying a newer release; older releases may lack hooks or flags VCM uses.
3
+ export const MIN_CLAUDE_CODE_VERSION = "2.1.310";
4
+ const VERSION_CACHE_TTL_MS = 10 * 60 * 1000;
5
+ // A failed probe is retried sooner so a fixed installation clears the warning.
6
+ const FAILED_PROBE_TTL_MS = 30 * 1000;
7
+ export const VERSION_PROBE_TIMEOUT_MS = 10 * 1000;
8
+ export function createClaudeVersionCheck(claude, now = Date.now, timeoutMs = VERSION_PROBE_TIMEOUT_MS) {
9
+ const cache = new Map();
10
+ const inFlight = new Map();
11
+ return {
12
+ async getWarning(command, options = {}) {
13
+ const cached = cache.get(command);
14
+ if (!cached || options.refresh) {
15
+ return probeOnce(command);
16
+ }
17
+ const ttl = cached.failed ? FAILED_PROBE_TTL_MS : VERSION_CACHE_TTL_MS;
18
+ if (now() - cached.checkedAt >= ttl) {
19
+ void probeOnce(command);
20
+ }
21
+ return cached.warning;
22
+ }
23
+ };
24
+ // Concurrent callers share one probe per command.
25
+ function probeOnce(command) {
26
+ const running = inFlight.get(command);
27
+ if (running) {
28
+ return running;
29
+ }
30
+ const probe = (async () => {
31
+ try {
32
+ const result = await probeVersion(command);
33
+ cache.set(command, { ...result, checkedAt: now() });
34
+ return result.warning;
35
+ }
36
+ finally {
37
+ inFlight.delete(command);
38
+ }
39
+ })();
40
+ inFlight.set(command, probe);
41
+ return probe;
42
+ }
43
+ async function probeVersion(command) {
44
+ let output;
45
+ try {
46
+ output = await withTimeout(claude.getVersion(command), timeoutMs);
47
+ }
48
+ catch (error) {
49
+ return {
50
+ warning: `Claude Code version could not be checked: ${error instanceof Error ? error.message : String(error)}`,
51
+ failed: true
52
+ };
53
+ }
54
+ const version = parseClaudeCodeVersion(output);
55
+ if (!version) {
56
+ return {
57
+ warning: `Claude Code version could not be parsed from "${output}". VCM is verified with ${MIN_CLAUDE_CODE_VERSION} or newer.`,
58
+ failed: true
59
+ };
60
+ }
61
+ return {
62
+ warning: compareVersions(version, parseClaudeCodeVersion(MIN_CLAUDE_CODE_VERSION)) < 0
63
+ ? `Claude Code ${version.join(".")} is older than the minimum verified version ${MIN_CLAUDE_CODE_VERSION}. Upgrade Claude Code; older releases may lack hooks or flags VCM relies on.`
64
+ : undefined,
65
+ failed: false
66
+ };
67
+ }
68
+ }
69
+ function withTimeout(promise, timeoutMs) {
70
+ let timer;
71
+ const timeout = new Promise((_resolve, reject) => {
72
+ timer = setTimeout(() => reject(new Error(`claude --version did not respond within ${timeoutMs}ms`)), timeoutMs);
73
+ });
74
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
75
+ }
76
+ export function parseClaudeCodeVersion(output) {
77
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
78
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
79
+ }
80
+ function compareVersions(left, right) {
81
+ for (let index = 0; index < 3; index += 1) {
82
+ if (left[index] !== right[index]) {
83
+ return left[index] - right[index];
84
+ }
85
+ }
86
+ return 0;
87
+ }
@@ -496,7 +496,8 @@ export function createGateReviewService(deps) {
496
496
  return;
497
497
  }
498
498
  const controller = new AbortController();
499
- activeRuns.set(runKey, controller);
499
+ const run = { controller, taskSlug: context.taskSlug, requestId, gateTurnActive: false };
500
+ activeRuns.set(runKey, run);
500
501
  try {
501
502
  const timestamp = now();
502
503
  if (!(await markGateRunStarted(context, gate, requestId, timestamp))) {
@@ -530,7 +531,7 @@ export function createGateReviewService(deps) {
530
531
  role: REVIEWER_ROLE,
531
532
  eventName: "UserPromptSubmit"
532
533
  });
533
- const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs, controller.signal);
534
+ const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs, controller.signal, () => run.reviewerEnded);
534
535
  const completedRecord = await completeGateRun(context, gate, requestId, parsed);
535
536
  if (!completedRecord) {
536
537
  return;
@@ -551,7 +552,7 @@ export function createGateReviewService(deps) {
551
552
  await callbackProjectManager(context, gate, "failed", undefined, reportPathForRequest(requestId), message, requestId, failedRecord.inputHash);
552
553
  }
553
554
  finally {
554
- if (activeRuns.get(runKey) === controller) {
555
+ if (activeRuns.get(runKey) === run) {
555
556
  activeRuns.delete(runKey);
556
557
  }
557
558
  }
@@ -644,20 +645,21 @@ export function createGateReviewService(deps) {
644
645
  }
645
646
  if (existing?.claudeSessionId) {
646
647
  try {
648
+ // Resume keeps the launch options recorded by the saved Reviewer Session.
647
649
  return await deps.sessionService.resumeRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE, {
648
650
  cols: 100,
649
- rows: 28,
650
- model: "default"
651
+ rows: 28
651
652
  });
652
653
  }
653
654
  catch {
654
655
  // Fall through to a fresh Reviewer terminal if the saved session cannot be resumed.
655
656
  }
656
657
  }
658
+ const template = (await deps.appSettings.getPreferences?.())?.launchTemplate.roles[REVIEWER_ROLE];
657
659
  return deps.sessionService.startRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE, {
658
660
  cols: 100,
659
661
  rows: 28,
660
- model: "default"
662
+ ...template
661
663
  });
662
664
  }
663
665
  async function callbackProjectManager(context, gate, status, decision, reportPath, error, requestId, inputHash) {
@@ -818,7 +820,7 @@ export function createGateReviewService(deps) {
818
820
  hint: "Refresh Gate Review state before cancelling the current request."
819
821
  });
820
822
  }
821
- activeRuns.get(gateRunKey(context, gate, input.requestId))?.abort();
823
+ activeRuns.get(gateRunKey(context, gate, input.requestId))?.controller.abort();
822
824
  await deps.sessionService.restartRoleSession(context.repoRoot, context.taskSlug, REVIEWER_ROLE);
823
825
  await updateRequestStatus(deps.fs, context, input.requestId, "cancelled", {
824
826
  completedAt: timestamp,
@@ -925,9 +927,35 @@ export function createGateReviewService(deps) {
925
927
  async readReport(repoRoot, taskSlug, gate) {
926
928
  const context = await getContext(repoRoot, taskSlug);
927
929
  return parseGateReport(deps.fs, context.taskRepoRoot, gate, undefined, now(), reportPathForGate(gate));
930
+ },
931
+ recordReviewerPromptSubmitted(taskSlug, prompt) {
932
+ for (const run of activeRunsForTask(taskSlug)) {
933
+ run.gateTurnActive = Boolean(prompt?.includes(run.requestId));
934
+ }
935
+ },
936
+ recordReviewerTurnEnded(taskSlug, reason) {
937
+ for (const run of activeRunsForTask(taskSlug)) {
938
+ // Only the end of the turn that ran this request's prompt counts: a late
939
+ // Stop from an earlier turn, or a turn the user started in the Reviewer
940
+ // terminal, must not fail the review.
941
+ if (reason === "process-exit") {
942
+ run.reviewerEnded = "Reviewer process exited before completing the review.";
943
+ }
944
+ else if (run.gateTurnActive) {
945
+ run.reviewerEnded = "Reviewer ended its turn without submitting the report.";
946
+ }
947
+ }
948
+ },
949
+ stopTask(taskSlug) {
950
+ for (const run of activeRunsForTask(taskSlug)) {
951
+ run.controller.abort();
952
+ }
928
953
  }
929
954
  };
930
955
  }
956
+ function activeRunsForTask(taskSlug) {
957
+ return [...activeRuns.values()].filter((run) => run.taskSlug === taskSlug);
958
+ }
931
959
  export function isGateReviewGate(value) {
932
960
  return GATE_REVIEW_GATES.includes(value);
933
961
  }
@@ -1529,10 +1557,13 @@ function isTaskEvidencePath(sourcePath) {
1529
1557
  return sourcePath.startsWith(".ai/vcm/handoffs/")
1530
1558
  || sourcePath.startsWith(".ai/vcm/gate-reviews/");
1531
1559
  }
1532
- async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs, signal) {
1560
+ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs, signal, reviewerEnded) {
1533
1561
  const reportPath = reportPathForRequest(requestId);
1534
1562
  while (true) {
1535
1563
  throwIfGateRunCancelled(signal);
1564
+ // Read the end signal before the report so a report written just before the
1565
+ // Reviewer stopped is still accepted.
1566
+ const ended = reviewerEnded();
1536
1567
  try {
1537
1568
  return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp, reportPath);
1538
1569
  }
@@ -1540,6 +1571,13 @@ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, i
1540
1571
  if (!isPendingReportError(error)) {
1541
1572
  throw error;
1542
1573
  }
1574
+ if (ended) {
1575
+ throw new VcmError({
1576
+ code: "GATE_REVIEW_REPORT_NOT_WRITTEN",
1577
+ message: `${ended} Gate review report was not written: ${reportPath}`,
1578
+ statusCode: 500
1579
+ });
1580
+ }
1543
1581
  }
1544
1582
  await delay(intervalMs);
1545
1583
  }