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.
- package/README.md +7 -3
- package/dist/backend/cli/install-vcm-harness.js +57 -18
- 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 +46 -0
- package/dist/backend/services/gate-review-service.js +47 -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/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-CShYYQQa.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.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>
|
|
@@ -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;
|
|
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; };
|
|
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
|
-
|
|
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) {
|
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,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
|
-
|
|
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) ===
|
|
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,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
|
}
|