vibe-coding-master 0.8.13 → 0.9.0

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 (42) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/cli/install-vcm-harness.js +57 -18
  3. package/dist/backend/gateway/gateway-service.js +5 -3
  4. package/dist/backend/runtime/terminal-submit.js +1 -0
  5. package/dist/backend/server.js +14 -4
  6. package/dist/backend/services/app-settings-service.js +3 -2
  7. package/dist/backend/services/auto-memory-service.js +4 -3
  8. package/dist/backend/services/claude-hook-service.js +6 -0
  9. package/dist/backend/services/claude-transcript-service.js +2 -13
  10. package/dist/backend/services/claude-version-check.js +46 -0
  11. package/dist/backend/services/gate-review-service.js +47 -8
  12. package/dist/backend/services/harness-service.js +41 -20
  13. package/dist/backend/services/message-service.js +30 -6
  14. package/dist/backend/services/project-service.js +8 -2
  15. package/dist/backend/services/runtime-recovery-service.js +36 -9
  16. package/dist/backend/services/session-service.js +10 -19
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  19. package/dist/backend/services/workflow-control-service.js +55 -11
  20. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  22. package/dist/backend/templates/harness/gate-review.js +676 -643
  23. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  24. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  25. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  26. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  27. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  28. package/dist/shared/types/app-settings.js +31 -0
  29. package/dist-frontend/assets/index-CShYYQQa.css +32 -0
  30. package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/scripts/harness-tools/check-durable-docs +332 -298
  34. package/scripts/harness-tools/generate-module-index +1047 -512
  35. package/scripts/harness-tools/generate-public-surface +1231 -1069
  36. package/scripts/harness-tools/package.json +3 -0
  37. package/scripts/harness-tools/run-long-check +608 -531
  38. package/scripts/harness-tools/vcm-artifact +181 -113
  39. package/scripts/harness-tools/vcm-bash-guard +839 -605
  40. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  41. package/scripts/harness-tools/watch-job +313 -238
  42. 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>
@@ -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";
@@ -37,6 +38,8 @@ import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffo
37
38
  import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
38
39
  import { renderResolveDurableDocAssignmentTool } from "../templates/harness/resolve-durable-doc-assignment.js";
39
40
  import { readVcmPackageVersion } from "../app-version.js";
41
+ import { DEFAULT_ARCHITECT_WORKER_SETTINGS, normalizeArchitectWorkerSettings } from "../../shared/types/app-settings.js";
42
+ import { ARCHITECT_WORKER_AGENT_TYPES, applyArchitectWorkerSetting } from "../templates/harness/architect-worker-frontmatter.js";
40
43
  const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
41
44
  const APP_ROOT = path.resolve(CLI_DIR, "../../..");
42
45
  const VCM_PACKAGE_VERSION = readVcmPackageVersion(APP_ROOT);
@@ -54,8 +57,8 @@ const LEGACY_CODEX_HARNESS_PATHS = [
54
57
  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
58
  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
59
  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"'`;
60
+ 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'`;
61
+ 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
62
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
60
63
  const VCM_AUTO_MEMORY_ENABLED = false;
61
64
  const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
@@ -104,20 +107,14 @@ const AGENT_FRONTMATTER = {
104
107
  },
105
108
  "vcm-architect-evidence-worker": {
106
109
  description: "Foreground Architect worker for bounded project evidence collection.",
107
- tools: "Read, Grep, Glob, Write",
108
- model: "opus",
109
- effort: "xhigh"
110
+ tools: "Read, Grep, Glob, Write"
110
111
  },
111
112
  "vcm-architect-scaffold-worker": {
112
- description: "Foreground Architect worker for exact scaffold execution and scaffold validation.",
113
- model: "opus",
114
- effort: "xhigh"
113
+ description: "Foreground Architect worker for exact scaffold execution and scaffold validation."
115
114
  },
116
115
  "vcm-architect-validation-worker": {
117
116
  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"
117
+ tools: "Read, Grep, Glob, Bash, Write"
121
118
  }
122
119
  };
123
120
  const REQUIRED_AGENT_DISALLOWED_TOOLS = {
@@ -289,21 +286,40 @@ const DURABLE_DOC_TEMPLATES = [
289
286
  content: "# Testing\n"
290
287
  },
291
288
  ];
289
+ // upgradeableSha256 lists earlier VCM-shipped versions of a project-owned file;
290
+ // an unmodified copy of one of them is upgraded instead of kept.
292
291
  const PROJECT_OWNED_FILES = [
293
292
  {
294
293
  path: ".ai/tools/generate-module-index",
295
294
  category: "generated-context-tool",
296
295
  mode: 0o755,
297
- templatePath: "scripts/harness-tools/generate-module-index"
296
+ templatePath: "scripts/harness-tools/generate-module-index",
297
+ upgradeableSha256: [
298
+ "432fc89448bd26c9504b4bfee93ae2306d0cf01e8f00a946189c3c8148f615f7",
299
+ "96f5763d107e6d95cb605bb5b949e4c40b0715ba3e5bacd9554dfb7f98c332f3",
300
+ "06b9fcc6da0635f6a53a72f68e0ec1934b1bc4037df386e93d8190004c21b1bb",
301
+ "711753b3e4c9afa3198e0ae150d8b686dcb0b1110e4208c60686dcac249c5b6e"
302
+ ]
298
303
  },
299
304
  {
300
305
  path: ".ai/tools/generate-public-surface",
301
306
  category: "generated-context-tool",
302
307
  mode: 0o755,
303
- templatePath: "scripts/harness-tools/generate-public-surface"
308
+ templatePath: "scripts/harness-tools/generate-public-surface",
309
+ upgradeableSha256: [
310
+ "08eb2f5a8df6e5db0bc7385f23f19a572ff586124623a985588842a77ce6ffa3",
311
+ "a4b177f780335962cfccab8de3254456351f9a97ff308d238b5725c7dcb5105e",
312
+ "f8d3a49b0b055302bc9f780b869fe9e8e04b67decdce79b92d0b62b1d911a15e"
313
+ ]
304
314
  }
305
315
  ];
306
316
  const WHOLE_FILES = [
317
+ {
318
+ path: ".ai/tools/package.json",
319
+ category: "runtime-tool",
320
+ mode: 0o644,
321
+ templatePath: "scripts/harness-tools/package.json"
322
+ },
307
323
  {
308
324
  path: ".ai/tools/check-durable-docs",
309
325
  category: "durable-docs-tool",
@@ -489,7 +505,7 @@ async function main() {
489
505
  await migrateReviewerAgent({ projectRoot, dryRun, operations });
490
506
  const manifest = await buildManifest(projectRoot);
491
507
  for (const definition of MANAGED_FILES) {
492
- await installManagedFile({ projectRoot, definition, dryRun, operations });
508
+ await installManagedFile({ projectRoot, definition, dryRun, operations, workerSettings: args.architectWorkers });
493
509
  }
494
510
  for (const template of DURABLE_DOC_TEMPLATES) {
495
511
  await installDurableDocTemplate({ projectRoot, template, dryRun, operations });
@@ -545,7 +561,8 @@ function parseArgs(argv) {
545
561
  const args = {
546
562
  dryRun: false,
547
563
  help: false,
548
- projectRoot: undefined
564
+ projectRoot: undefined,
565
+ architectWorkers: DEFAULT_ARCHITECT_WORKER_SETTINGS
549
566
  };
550
567
  for (let index = 0; index < argv.length; index += 1) {
551
568
  const arg = argv[index];
@@ -557,6 +574,20 @@ function parseArgs(argv) {
557
574
  args.dryRun = true;
558
575
  continue;
559
576
  }
577
+ if (arg === "--architect-workers") {
578
+ const value = argv[index + 1];
579
+ if (!value) {
580
+ fail("--architect-workers requires a JSON object.");
581
+ }
582
+ try {
583
+ args.architectWorkers = normalizeArchitectWorkerSettings(JSON.parse(value));
584
+ }
585
+ catch {
586
+ fail("--architect-workers must be valid JSON.");
587
+ }
588
+ index += 1;
589
+ continue;
590
+ }
560
591
  if (arg.startsWith("--")) {
561
592
  fail(`Unknown option: ${arg}`);
562
593
  }
@@ -571,6 +602,7 @@ function printUsage() {
571
602
  console.log(`Usage:
572
603
  node scripts/install-vcm-harness.mjs <project-root>
573
604
  node scripts/install-vcm-harness.mjs <project-root> --dry-run
605
+ node scripts/install-vcm-harness.mjs <project-root> --architect-workers '<json>'
574
606
 
575
607
  Installs only fixed VCM harness content.
576
608
 
@@ -740,7 +772,7 @@ async function installManifest({ projectRoot, manifest, dryRun, operations, forc
740
772
  action: "write fixed harness manifest"
741
773
  });
742
774
  }
743
- async function installManagedFile({ projectRoot, definition, dryRun, operations }) {
775
+ async function installManagedFile({ projectRoot, definition, dryRun, operations, workerSettings = DEFAULT_ARCHITECT_WORKER_SETTINGS }) {
744
776
  const targetPath = resolveInside(projectRoot, definition.path);
745
777
  const block = renderManagedBlock(definition);
746
778
  const currentContent = await readOptionalText(targetPath);
@@ -774,6 +806,10 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
774
806
  for (const removedSkill of REMOVED_AGENT_SKILLS[definition.agentName] ?? []) {
775
807
  nextContent = removeAgentSkill(nextContent, removedSkill);
776
808
  }
809
+ const workerType = ARCHITECT_WORKER_AGENT_TYPES[definition.agentName];
810
+ if (workerType) {
811
+ nextContent = applyArchitectWorkerSetting(nextContent, workerSettings[workerType]);
812
+ }
777
813
  await writeIfChanged({
778
814
  targetPath,
779
815
  relativePath: definition.path,
@@ -1029,7 +1065,10 @@ async function installWholeFile({ projectRoot, file, dryRun, operations }) {
1029
1065
  }
1030
1066
  async function installProjectOwnedFile({ projectRoot, file, dryRun, operations }) {
1031
1067
  const targetPath = resolveInside(projectRoot, file.path);
1032
- if (await pathExists(targetPath)) {
1068
+ const currentContent = await readOptionalText(targetPath);
1069
+ const upgradeable = currentContent !== undefined
1070
+ && (file.upgradeableSha256 ?? []).includes(createHash("sha256").update(currentContent).digest("hex"));
1071
+ if (currentContent !== undefined && !upgradeable) {
1033
1072
  operations.push(skip(file.path, "exists; project-owned"));
1034
1073
  return;
1035
1074
  }
@@ -1041,7 +1080,7 @@ async function installProjectOwnedFile({ projectRoot, file, dryRun, operations }
1041
1080
  mode: file.mode,
1042
1081
  dryRun,
1043
1082
  operations,
1044
- action: "seed project-owned VCM file"
1083
+ action: upgradeable ? "upgrade unmodified project-owned VCM file" : "seed project-owned VCM file"
1045
1084
  });
1046
1085
  }
1047
1086
  async function removeLegacyFlatSkillFiles({ projectRoot, dryRun, operations }) {
@@ -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,46 @@
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
+ export function createClaudeVersionCheck(claude, now = Date.now) {
6
+ const cache = new Map();
7
+ return {
8
+ async getWarning(command, options = {}) {
9
+ const cached = cache.get(command);
10
+ if (cached && !options.refresh && now() - cached.checkedAt < VERSION_CACHE_TTL_MS) {
11
+ return cached.warning;
12
+ }
13
+ const warning = await probe(command);
14
+ cache.set(command, { warning, checkedAt: now() });
15
+ return warning;
16
+ }
17
+ };
18
+ async function probe(command) {
19
+ let output;
20
+ try {
21
+ output = await claude.getVersion(command);
22
+ }
23
+ catch (error) {
24
+ return `Claude Code version could not be checked: ${error instanceof Error ? error.message : String(error)}`;
25
+ }
26
+ const version = parseClaudeCodeVersion(output);
27
+ if (!version) {
28
+ return `Claude Code version could not be parsed from "${output}". VCM is verified with ${MIN_CLAUDE_CODE_VERSION} or newer.`;
29
+ }
30
+ return compareVersions(version, parseClaudeCodeVersion(MIN_CLAUDE_CODE_VERSION)) < 0
31
+ ? `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.`
32
+ : undefined;
33
+ }
34
+ }
35
+ export function parseClaudeCodeVersion(output) {
36
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
37
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
38
+ }
39
+ function compareVersions(left, right) {
40
+ for (let index = 0; index < 3; index += 1) {
41
+ if (left[index] !== right[index]) {
42
+ return left[index] - right[index];
43
+ }
44
+ }
45
+ return 0;
46
+ }
@@ -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, promptAccepted: 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,36 @@ 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
+ if (prompt?.includes(run.requestId)) {
934
+ run.promptAccepted = true;
935
+ }
936
+ }
937
+ },
938
+ recordReviewerTurnEnded(taskSlug, reason) {
939
+ for (const run of activeRunsForTask(taskSlug)) {
940
+ // A turn end only counts once the Reviewer accepted this request's prompt,
941
+ // so a late Stop from an earlier turn cannot fail a newer request.
942
+ if (reason === "process-exit") {
943
+ run.reviewerEnded = "Reviewer process exited before completing the review.";
944
+ }
945
+ else if (run.promptAccepted) {
946
+ run.reviewerEnded = "Reviewer ended its turn without submitting the report.";
947
+ }
948
+ }
949
+ },
950
+ stopTask(taskSlug) {
951
+ for (const run of activeRunsForTask(taskSlug)) {
952
+ run.controller.abort();
953
+ }
928
954
  }
929
955
  };
930
956
  }
957
+ function activeRunsForTask(taskSlug) {
958
+ return [...activeRuns.values()].filter((run) => run.taskSlug === taskSlug);
959
+ }
931
960
  export function isGateReviewGate(value) {
932
961
  return GATE_REVIEW_GATES.includes(value);
933
962
  }
@@ -1529,10 +1558,13 @@ function isTaskEvidencePath(sourcePath) {
1529
1558
  return sourcePath.startsWith(".ai/vcm/handoffs/")
1530
1559
  || sourcePath.startsWith(".ai/vcm/gate-reviews/");
1531
1560
  }
1532
- async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs, signal) {
1561
+ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs, signal, reviewerEnded) {
1533
1562
  const reportPath = reportPathForRequest(requestId);
1534
1563
  while (true) {
1535
1564
  throwIfGateRunCancelled(signal);
1565
+ // Read the end signal before the report so a report written just before the
1566
+ // Reviewer stopped is still accepted.
1567
+ const ended = reviewerEnded();
1536
1568
  try {
1537
1569
  return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp, reportPath);
1538
1570
  }
@@ -1540,6 +1572,13 @@ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, i
1540
1572
  if (!isPendingReportError(error)) {
1541
1573
  throw error;
1542
1574
  }
1575
+ if (ended) {
1576
+ throw new VcmError({
1577
+ code: "GATE_REVIEW_REPORT_NOT_WRITTEN",
1578
+ message: `${ended} Gate review report was not written: ${reportPath}`,
1579
+ statusCode: 500
1580
+ });
1581
+ }
1543
1582
  }
1544
1583
  await delay(intervalMs);
1545
1584
  }