vibe-coding-master 0.3.0 → 0.3.2
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 +10 -7
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/codex-hook-routes.js +9 -0
- package/dist/backend/api/codex-review-routes.js +4 -0
- package/dist/backend/api/translation-routes.js +2 -2
- package/dist/backend/cli/install-vcm-harness.js +14 -1
- package/dist/backend/gateway/gateway-service.js +4 -3
- package/dist/backend/server.js +10 -0
- package/dist/backend/services/app-settings-service.js +12 -4
- package/dist/backend/services/artifact-service.js +2 -1
- package/dist/backend/services/claude-hook-service.js +3 -3
- package/dist/backend/services/codex-hook-service.js +87 -0
- package/dist/backend/services/codex-review-service.js +145 -60
- package/dist/backend/services/harness-service.js +28 -1
- package/dist/backend/services/message-service.js +8 -7
- package/dist/backend/services/project-service.js +2 -2
- package/dist/backend/services/round-service.js +29 -25
- package/dist/backend/services/session-service.js +141 -12
- package/dist/backend/templates/handoff.js +10 -0
- package/dist/backend/templates/harness/architect-agent.js +8 -4
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -1
- package/dist/backend/templates/harness/codex-review.js +41 -6
- package/dist/backend/templates/harness/reviewer-agent.js +1 -0
- package/dist/shared/constants.js +15 -1
- package/dist/shared/types/app-settings.js +4 -3
- package/dist/shared/types/codex-hook.js +1 -0
- package/dist/shared/types/session.js +44 -0
- package/dist/shared/validation/artifact-check.js +1 -0
- package/dist-frontend/assets/index-BavJjWQY.js +92 -0
- package/dist-frontend/assets/index-CR1EOe-w.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/ARCHITECTURE.md +1 -0
- package/docs/TESTING.md +82 -0
- package/docs/cc-best-practices.md +1 -0
- package/docs/codex-review-gates.md +88 -45
- package/docs/full-harness-baseline.md +1 -1
- package/docs/gateway-design.md +5 -4
- package/docs/known-issues.md +1 -0
- package/docs/product-design.md +28 -11
- package/docs/vcm-cc-best-practices.md +15 -5
- package/package.json +1 -1
- package/dist-frontend/assets/index-C9l94uxA.js +0 -92
- package/dist-frontend/assets/index-D6jWo6Jd.css +0 -32
|
@@ -4,12 +4,16 @@ import { CODEX_REVIEW_GATES } from "../../shared/types/codex-review.js";
|
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
|
+
import { renderCodexConfigHarnessRules } from "../templates/harness/codex-review.js";
|
|
7
8
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
8
9
|
const CODEX_DIR = ".ai/codex";
|
|
9
10
|
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
10
11
|
const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
|
|
11
12
|
const REQUESTS_DIR = ".ai/vcm/codex-reviews/requests";
|
|
12
13
|
const CODEX_REVIEW_VERSION = 1;
|
|
14
|
+
const CODEX_REVIEWER_ROLE = "codex-reviewer";
|
|
15
|
+
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
16
|
+
const DEFAULT_REPORT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
13
17
|
const activeRuns = new Set();
|
|
14
18
|
const SOURCE_ARTIFACTS = {
|
|
15
19
|
"architecture-plan": [
|
|
@@ -29,6 +33,8 @@ const SOURCE_ARTIFACTS = {
|
|
|
29
33
|
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
30
34
|
export function createCodexReviewService(deps) {
|
|
31
35
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
36
|
+
const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
|
|
37
|
+
const reportTimeoutMs = deps.reportTimeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS;
|
|
32
38
|
async function getContext(repoRoot, taskSlug) {
|
|
33
39
|
const projectConfig = await deps.projectService.loadConfig(repoRoot);
|
|
34
40
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -153,9 +159,7 @@ export function createCodexReviewService(deps) {
|
|
|
153
159
|
const codexDir = resolveRepoPath(context.taskRepoRoot, CODEX_DIR);
|
|
154
160
|
const reviewDir = resolveRepoPath(context.taskRepoRoot, CODEX_REVIEW_DIR);
|
|
155
161
|
const prompt = await buildCodexPrompt(deps.fs, context.taskRepoRoot, gate, requestId);
|
|
156
|
-
const outputMessagePath = path.join(reviewDir, "logs", `${requestId}.last-message.txt`);
|
|
157
162
|
await deps.fs.ensureDir(reviewDir);
|
|
158
|
-
await deps.fs.ensureDir(path.dirname(outputMessagePath));
|
|
159
163
|
if (!(await deps.fs.pathExists(codexDir))) {
|
|
160
164
|
throw new VcmError({
|
|
161
165
|
code: "CODEX_REVIEW_CONFIG_MISSING",
|
|
@@ -164,32 +168,13 @@ export function createCodexReviewService(deps) {
|
|
|
164
168
|
hint: "Apply the VCM harness before requesting Codex review gates."
|
|
165
169
|
});
|
|
166
170
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
const result = await deps.runner.run(context.config.command, args, {
|
|
175
|
-
cwd: context.taskRepoRoot,
|
|
176
|
-
env: {
|
|
177
|
-
...process.env,
|
|
178
|
-
VCM_TASK_REPO_ROOT: context.taskRepoRoot,
|
|
179
|
-
VCM_TASK_SLUG: context.taskSlug,
|
|
180
|
-
VCM_CODEX_REVIEW_GATE: gate,
|
|
181
|
-
VCM_CODEX_REVIEW_REQUEST_ID: requestId
|
|
182
|
-
}
|
|
171
|
+
const session = await ensureCodexReviewerSession(context);
|
|
172
|
+
await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
173
|
+
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
174
|
+
const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), {
|
|
175
|
+
intervalMs: reportPollIntervalMs,
|
|
176
|
+
timeoutMs: reportTimeoutMs
|
|
183
177
|
});
|
|
184
|
-
if (result.exitCode !== 0) {
|
|
185
|
-
throw new VcmError({
|
|
186
|
-
code: "CODEX_REVIEW_FAILED",
|
|
187
|
-
message: `Codex CLI exited with ${result.exitCode}.`,
|
|
188
|
-
statusCode: 500,
|
|
189
|
-
hint: trimCommandOutput(`${result.stderr}\n${result.stdout}`)
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const parsed = await parseGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now());
|
|
193
178
|
const completedAt = now();
|
|
194
179
|
await updateGateRecord(context, gate, {
|
|
195
180
|
status: "completed",
|
|
@@ -230,6 +215,29 @@ export function createCodexReviewService(deps) {
|
|
|
230
215
|
activeRuns.delete(runKey);
|
|
231
216
|
}
|
|
232
217
|
}
|
|
218
|
+
async function ensureCodexReviewerSession(context) {
|
|
219
|
+
const existing = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE);
|
|
220
|
+
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
221
|
+
return existing;
|
|
222
|
+
}
|
|
223
|
+
if (existing?.claudeSessionId) {
|
|
224
|
+
try {
|
|
225
|
+
return await deps.sessionService.resumeRoleSession(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE, {
|
|
226
|
+
cols: 100,
|
|
227
|
+
rows: 28,
|
|
228
|
+
model: "default"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Fall through to a fresh Codex Reviewer terminal if the saved session cannot be resumed.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return deps.sessionService.startRoleSession(context.repoRoot, context.taskSlug, CODEX_REVIEWER_ROLE, {
|
|
236
|
+
cols: 100,
|
|
237
|
+
rows: 28,
|
|
238
|
+
model: "default"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
233
241
|
async function updateGateRecord(context, gate, patch, options = {}) {
|
|
234
242
|
const index = await loadIndex(deps.fs, context, now());
|
|
235
243
|
const next = applyGateState(index, gate, patch, now(), options.clearActiveGate);
|
|
@@ -276,6 +284,29 @@ export function createCodexReviewService(deps) {
|
|
|
276
284
|
const context = await getContext(repoRoot, taskSlug);
|
|
277
285
|
return loadIndex(deps.fs, context, now());
|
|
278
286
|
},
|
|
287
|
+
async updateSettings(repoRoot, taskSlug, input) {
|
|
288
|
+
const context = await getContext(repoRoot, taskSlug);
|
|
289
|
+
const requiredGates = new Set(context.config.enabled ? context.config.requiredGates : []);
|
|
290
|
+
for (const [gate, enabled] of Object.entries(input?.gates ?? {})) {
|
|
291
|
+
if (!isCodexReviewGate(gate)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (enabled) {
|
|
295
|
+
requiredGates.add(gate);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
requiredGates.delete(gate);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await writeRuntimeConfigSettings(deps.fs, context.taskRepoRoot, [...requiredGates]);
|
|
302
|
+
const nextContext = await getContext(repoRoot, taskSlug);
|
|
303
|
+
const index = await loadIndex(deps.fs, nextContext, now());
|
|
304
|
+
await saveIndex(deps.fs, nextContext.taskRepoRoot, {
|
|
305
|
+
...index,
|
|
306
|
+
updatedAt: now()
|
|
307
|
+
});
|
|
308
|
+
return loadIndex(deps.fs, nextContext, now());
|
|
309
|
+
},
|
|
279
310
|
requestReviewGate(repoRoot, taskSlug, gate) {
|
|
280
311
|
return requestReviewGateInternal(repoRoot, taskSlug, gate);
|
|
281
312
|
},
|
|
@@ -356,12 +387,60 @@ async function loadRuntimeConfig(fs, taskRepoRoot) {
|
|
|
356
387
|
const parsedGates = parseTomlStringArray(section, "required_gates").filter(isCodexReviewGate);
|
|
357
388
|
return {
|
|
358
389
|
enabled,
|
|
359
|
-
requiredGates: parsedGates.length > 0 ? parsedGates : [...CODEX_REVIEW_GATES],
|
|
390
|
+
requiredGates: parsedGates.length > 0 ? parsedGates : enabled ? [...CODEX_REVIEW_GATES] : [],
|
|
360
391
|
model: parseTomlString(content, "model"),
|
|
361
392
|
modelReasoningEffort: parseTomlString(content, "model_reasoning_effort"),
|
|
362
393
|
command: parseTomlString(section, "command") ?? "codex"
|
|
363
394
|
};
|
|
364
395
|
}
|
|
396
|
+
async function writeRuntimeConfigSettings(fs, taskRepoRoot, requiredGates) {
|
|
397
|
+
const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
|
|
398
|
+
const current = await fs.pathExists(configPath)
|
|
399
|
+
? await fs.readText(configPath)
|
|
400
|
+
: renderCodexConfigHarnessRules();
|
|
401
|
+
const section = renderCodexReviewSettingsSection(requiredGates);
|
|
402
|
+
const next = replaceTomlSection(current, "vcm.codex_review", section);
|
|
403
|
+
await fs.writeText(configPath, next);
|
|
404
|
+
}
|
|
405
|
+
function renderCodexReviewSettingsSection(requiredGates) {
|
|
406
|
+
if (requiredGates.length === 0) {
|
|
407
|
+
return [
|
|
408
|
+
"[vcm.codex_review]",
|
|
409
|
+
"enabled = false",
|
|
410
|
+
"required_gates = []"
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
const lines = [
|
|
414
|
+
"[vcm.codex_review]",
|
|
415
|
+
"enabled = true",
|
|
416
|
+
"required_gates = [",
|
|
417
|
+
...CODEX_REVIEW_GATES
|
|
418
|
+
.filter((gate) => requiredGates.includes(gate))
|
|
419
|
+
.map((gate) => ` "${gate}",`),
|
|
420
|
+
"]"
|
|
421
|
+
];
|
|
422
|
+
return lines.join("\n");
|
|
423
|
+
}
|
|
424
|
+
function replaceTomlSection(content, sectionName, nextSection) {
|
|
425
|
+
const lines = content.replace(/\s+$/g, "").split(/\r?\n/);
|
|
426
|
+
const header = `[${sectionName}]`;
|
|
427
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
428
|
+
if (start < 0) {
|
|
429
|
+
return `${lines.join("\n").trimEnd()}\n\n${nextSection}\n`;
|
|
430
|
+
}
|
|
431
|
+
let end = lines.length;
|
|
432
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
433
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(lines[index])) {
|
|
434
|
+
end = index;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return [
|
|
439
|
+
...lines.slice(0, start),
|
|
440
|
+
nextSection,
|
|
441
|
+
...lines.slice(end)
|
|
442
|
+
].join("\n").trimEnd() + "\n";
|
|
443
|
+
}
|
|
365
444
|
async function loadIndex(fs, context, timestamp) {
|
|
366
445
|
const indexPath = getIndexPath(context.taskRepoRoot);
|
|
367
446
|
const raw = await readJsonOrNull(fs, indexPath);
|
|
@@ -444,6 +523,8 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
|
|
|
444
523
|
"CLAUDE.md",
|
|
445
524
|
".ai/codex/AGENTS.md",
|
|
446
525
|
".ai/codex/config.toml",
|
|
526
|
+
".ai/codex/.codex/config.toml",
|
|
527
|
+
".ai/codex/.codex/hooks.json",
|
|
447
528
|
promptPathForGate(gate)
|
|
448
529
|
];
|
|
449
530
|
for (const relativePath of [...common, ...SOURCE_ARTIFACTS[gate]]) {
|
|
@@ -456,7 +537,7 @@ async function computeInputHash(deps, taskRepoRoot, gate) {
|
|
|
456
537
|
digest.update("<missing>");
|
|
457
538
|
}
|
|
458
539
|
}
|
|
459
|
-
if (gate === "final-diff") {
|
|
540
|
+
if (gate === "architecture-plan" || gate === "final-diff") {
|
|
460
541
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["status", "--porcelain=v1"]));
|
|
461
542
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--binary"]));
|
|
462
543
|
digest.update(await commandStdout(deps.runner, taskRepoRoot, ["diff", "--cached", "--binary"]));
|
|
@@ -497,31 +578,28 @@ Decision: approve|request_changes
|
|
|
497
578
|
|
|
498
579
|
Write only that report file. Do not edit any other file.`;
|
|
499
580
|
}
|
|
500
|
-
function
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"--output-last-message",
|
|
515
|
-
input.outputMessagePath
|
|
516
|
-
];
|
|
517
|
-
if (input.config.model) {
|
|
518
|
-
args.push("--model", input.config.model);
|
|
519
|
-
}
|
|
520
|
-
if (input.config.modelReasoningEffort) {
|
|
521
|
-
args.push("--config", `model_reasoning_effort="${input.config.modelReasoningEffort}"`);
|
|
581
|
+
async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, options) {
|
|
582
|
+
const startedAt = Date.now();
|
|
583
|
+
let lastError;
|
|
584
|
+
while (Date.now() - startedAt <= options.timeoutMs) {
|
|
585
|
+
try {
|
|
586
|
+
return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp);
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
lastError = error;
|
|
590
|
+
if (!isPendingReportError(error)) {
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
await delay(options.intervalMs);
|
|
522
595
|
}
|
|
523
|
-
|
|
524
|
-
|
|
596
|
+
const detail = errorMessage(lastError);
|
|
597
|
+
throw new VcmError({
|
|
598
|
+
code: "CODEX_REVIEW_REPORT_TIMEOUT",
|
|
599
|
+
message: `Codex Reviewer did not produce a valid ${gate} report within ${Math.round(options.timeoutMs / 1000)}s.`,
|
|
600
|
+
statusCode: 504,
|
|
601
|
+
hint: detail
|
|
602
|
+
});
|
|
525
603
|
}
|
|
526
604
|
async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
|
|
527
605
|
const reportPath = reportPathForGate(gate);
|
|
@@ -744,13 +822,6 @@ function renderProjectManagerCallback(input) {
|
|
|
744
822
|
];
|
|
745
823
|
return lines.join("\n");
|
|
746
824
|
}
|
|
747
|
-
function trimCommandOutput(output) {
|
|
748
|
-
const trimmed = output.trim();
|
|
749
|
-
if (!trimmed) {
|
|
750
|
-
return undefined;
|
|
751
|
-
}
|
|
752
|
-
return trimmed.length > 4000 ? `${trimmed.slice(0, 4000)}...` : trimmed;
|
|
753
|
-
}
|
|
754
825
|
function errorMessage(error) {
|
|
755
826
|
if (error instanceof VcmError) {
|
|
756
827
|
return error.hint ? `${error.message} ${error.hint}` : error.message;
|
|
@@ -760,6 +831,20 @@ function errorMessage(error) {
|
|
|
760
831
|
}
|
|
761
832
|
return "Unknown Codex review error.";
|
|
762
833
|
}
|
|
834
|
+
function isPendingReportError(error) {
|
|
835
|
+
return error instanceof VcmError && [
|
|
836
|
+
"CODEX_REVIEW_DECISION_MISSING",
|
|
837
|
+
"CODEX_REVIEW_REPORT_GATE_MISMATCH",
|
|
838
|
+
"CODEX_REVIEW_REPORT_MISSING",
|
|
839
|
+
"CODEX_REVIEW_REPORT_STALE"
|
|
840
|
+
].includes(error.code);
|
|
841
|
+
}
|
|
842
|
+
function delay(ms) {
|
|
843
|
+
if (ms <= 0) {
|
|
844
|
+
return Promise.resolve();
|
|
845
|
+
}
|
|
846
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
847
|
+
}
|
|
763
848
|
function escapeRegex(value) {
|
|
764
849
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
765
850
|
}
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
6
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
7
|
-
import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
|
|
7
|
+
import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
|
|
8
8
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
9
9
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
10
10
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -110,6 +110,20 @@ const HARNESS_FILES = [
|
|
|
110
110
|
ownership: "raw-file",
|
|
111
111
|
renderRules: renderCodexConfigHarnessRules
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
kind: "codex-cli-config",
|
|
115
|
+
path: ".ai/codex/.codex/config.toml",
|
|
116
|
+
title: "VCM Codex CLI Config",
|
|
117
|
+
ownership: "raw-file",
|
|
118
|
+
renderRules: renderCodexCliConfigHarnessRules
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
kind: "codex-hooks",
|
|
122
|
+
path: ".ai/codex/.codex/hooks.json",
|
|
123
|
+
title: "VCM Codex Hooks",
|
|
124
|
+
ownership: "raw-file",
|
|
125
|
+
renderRules: renderCodexHooksHarnessRules
|
|
126
|
+
},
|
|
113
127
|
{
|
|
114
128
|
kind: "codex-prompt-architecture-plan",
|
|
115
129
|
path: ".ai/codex/prompts/architecture-plan-gate.md",
|
|
@@ -393,8 +407,21 @@ function renderHarnessStatus(analyses) {
|
|
|
393
407
|
const plannedChanges = analyses
|
|
394
408
|
.map((analysis) => analysis.plannedChange)
|
|
395
409
|
.filter((change) => Boolean(change));
|
|
410
|
+
// Derive `initialized`: the VCM harness is considered installed when at least one
|
|
411
|
+
// VCM-exclusive marker is present (per analysis):
|
|
412
|
+
// - status.hasManagedBlock === true (a managed block already lives in the file), OR
|
|
413
|
+
// - definition.ownership is "whole-file" | "raw-file" AND status.exists === true
|
|
414
|
+
// (a VCM-owned file lives at a VCM-exclusive path).
|
|
415
|
+
// A pre-existing claude-settings (default "managed-block" ownership, no managed block)
|
|
416
|
+
// or a non-VCM CLAUDE.md/.gitignore (action "insert", hasManagedBlock === false) is
|
|
417
|
+
// intentionally NOT counted as initialized.
|
|
418
|
+
const initialized = analyses.some((analysis) => analysis.status.hasManagedBlock ||
|
|
419
|
+
((analysis.definition.ownership === "whole-file" ||
|
|
420
|
+
analysis.definition.ownership === "raw-file") &&
|
|
421
|
+
analysis.status.exists));
|
|
396
422
|
return {
|
|
397
423
|
version: VCM_HARNESS_VERSION,
|
|
424
|
+
initialized,
|
|
398
425
|
files,
|
|
399
426
|
needsApply: plannedChanges.length > 0,
|
|
400
427
|
plannedChanges,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { VCM_ROLE_NAMES, isVcmRoleName } from "../../shared/constants.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -282,8 +282,8 @@ async function listRouteFiles(fs, input) {
|
|
|
282
282
|
return routeFiles;
|
|
283
283
|
}
|
|
284
284
|
function parseRouteFileName(fileName) {
|
|
285
|
-
for (const fromRole of
|
|
286
|
-
for (const toRole of
|
|
285
|
+
for (const fromRole of VCM_ROLE_NAMES) {
|
|
286
|
+
for (const toRole of VCM_ROLE_NAMES) {
|
|
287
287
|
if (fromRole === toRole) {
|
|
288
288
|
continue;
|
|
289
289
|
}
|
|
@@ -371,14 +371,14 @@ function selectDispatchCandidates(routeFiles, stoppedRole) {
|
|
|
371
371
|
});
|
|
372
372
|
}
|
|
373
373
|
function validateMessagePolicy(fromRole, toRole, type) {
|
|
374
|
-
if (!
|
|
374
|
+
if (!VCM_ROLE_NAMES.includes(toRole)) {
|
|
375
375
|
throw new VcmError({
|
|
376
376
|
code: "MESSAGE_TARGET_UNKNOWN",
|
|
377
377
|
message: `Unknown target role: ${toRole}`,
|
|
378
378
|
statusCode: 400
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
|
-
if (!
|
|
381
|
+
if (!isVcmRoleName(String(fromRole))) {
|
|
382
382
|
throw new VcmError({
|
|
383
383
|
code: "MESSAGE_SENDER_UNKNOWN",
|
|
384
384
|
message: `Unknown sender role: ${fromRole}`,
|
|
@@ -424,7 +424,8 @@ function getMessageDeliveryTime(message) {
|
|
|
424
424
|
return message.deliveredAt ?? message.createdAt;
|
|
425
425
|
}
|
|
426
426
|
async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
427
|
-
|
|
427
|
+
const fromRole = message.fromRole;
|
|
428
|
+
if (!message.routePath || !isVcmRoleName(fromRole)) {
|
|
428
429
|
return;
|
|
429
430
|
}
|
|
430
431
|
const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
|
|
@@ -432,7 +433,7 @@ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
|
432
433
|
return;
|
|
433
434
|
}
|
|
434
435
|
const routeContent = await fs.readText(absolutePath);
|
|
435
|
-
const parsed = parseRouteFileContent(routeContent,
|
|
436
|
+
const parsed = parseRouteFileContent(routeContent, fromRole, message.toRole);
|
|
436
437
|
if (parsed.body.trim() === message.body.trim() &&
|
|
437
438
|
parsed.type === message.type &&
|
|
438
439
|
arraysEqual(parsed.artifactRefs, message.artifactRefs)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
const DEFAULT_HANDOFF_ROOT = ".ai/vcm/handoffs";
|
|
5
5
|
const DEFAULT_STATE_ROOT = ".ai/vcm";
|
|
@@ -182,7 +182,7 @@ export function buildDefaultProjectConfig(repoRoot) {
|
|
|
182
182
|
return {
|
|
183
183
|
version: 1,
|
|
184
184
|
repoRoot,
|
|
185
|
-
defaultRoles: [...
|
|
185
|
+
defaultRoles: [...VCM_ROLE_NAMES],
|
|
186
186
|
handoffRoot: DEFAULT_HANDOFF_ROOT,
|
|
187
187
|
stateRoot: DEFAULT_STATE_ROOT,
|
|
188
188
|
terminalBackend: "node-pty",
|
|
@@ -134,6 +134,32 @@ export function createRoundService(deps) {
|
|
|
134
134
|
}, delayMs);
|
|
135
135
|
settleTimers.set(getRoundStatePath(input), timer);
|
|
136
136
|
}
|
|
137
|
+
async function recordRoleTurnEvent(input) {
|
|
138
|
+
return withTaskLock(input, async () => {
|
|
139
|
+
const timestamp = now();
|
|
140
|
+
const settled = await settleIfNeeded(input, await load(input), timestamp);
|
|
141
|
+
const current = settled.currentRound;
|
|
142
|
+
const shouldStartNewRound = input.eventName === "UserPromptSubmit" && (!current || current.status === "stopped");
|
|
143
|
+
const next = applyRoundHookEvent({
|
|
144
|
+
state: settled,
|
|
145
|
+
taskSlug: input.taskSlug,
|
|
146
|
+
role: input.role,
|
|
147
|
+
eventName: input.eventName,
|
|
148
|
+
timestamp,
|
|
149
|
+
roundId: shouldStartNewRound ? id() : current?.id ?? "",
|
|
150
|
+
settleMs
|
|
151
|
+
});
|
|
152
|
+
await save(input, next);
|
|
153
|
+
if (input.eventName === "UserPromptSubmit") {
|
|
154
|
+
clearSettleTimer(input);
|
|
155
|
+
await updateSessionStatus(input, "running");
|
|
156
|
+
}
|
|
157
|
+
else if (next.currentRound) {
|
|
158
|
+
scheduleSettleTimer(input, next.currentRound, timestamp, input.settleGuard);
|
|
159
|
+
}
|
|
160
|
+
return toSessionRoundState(next, timestamp);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
137
163
|
async function withTaskLock(input, run) {
|
|
138
164
|
const key = getRoundStatePath(input);
|
|
139
165
|
const previous = taskLocks.get(key) ?? Promise.resolve();
|
|
@@ -155,31 +181,9 @@ export function createRoundService(deps) {
|
|
|
155
181
|
return toSessionRoundState(await load(input), timestamp);
|
|
156
182
|
});
|
|
157
183
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const settled = await settleIfNeeded(input, await load(input), timestamp);
|
|
162
|
-
const current = settled.currentRound;
|
|
163
|
-
const shouldStartNewRound = input.eventName === "UserPromptSubmit" && (!current || current.status === "stopped");
|
|
164
|
-
const next = applyRoundHookEvent({
|
|
165
|
-
state: settled,
|
|
166
|
-
taskSlug: input.taskSlug,
|
|
167
|
-
role: input.role,
|
|
168
|
-
eventName: input.eventName,
|
|
169
|
-
timestamp,
|
|
170
|
-
roundId: shouldStartNewRound ? id() : current?.id ?? "",
|
|
171
|
-
settleMs
|
|
172
|
-
});
|
|
173
|
-
await save(input, next);
|
|
174
|
-
if (input.eventName === "UserPromptSubmit") {
|
|
175
|
-
clearSettleTimer(input);
|
|
176
|
-
await updateSessionStatus(input, "running");
|
|
177
|
-
}
|
|
178
|
-
else if (next.currentRound) {
|
|
179
|
-
scheduleSettleTimer(input, next.currentRound, timestamp, input.settleGuard);
|
|
180
|
-
}
|
|
181
|
-
return toSessionRoundState(next, timestamp);
|
|
182
|
-
});
|
|
184
|
+
recordRoleTurnEvent,
|
|
185
|
+
recordClaudeHookEvent(input) {
|
|
186
|
+
return recordRoleTurnEvent(input);
|
|
183
187
|
},
|
|
184
188
|
stopSession() { },
|
|
185
189
|
stopTask(taskSlug) {
|