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.
- package/README.md +7 -3
- package/dist/backend/adapters/claude-adapter.js +2 -1
- package/dist/backend/cli/install-vcm-harness.js +66 -16
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +4 -3
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +87 -0
- package/dist/backend/services/gate-review-service.js +46 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/project-owned-history.js +21 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- 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
|
|
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
|
-
|
|
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;
|
|
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; };
|
|
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
|
-
|
|
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) {
|
package/dist/backend/server.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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) ===
|
|
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
|
-
|
|
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
|
}
|