vibe-coding-master 0.2.13 → 0.3.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 +9 -6
- 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 +58 -0
- package/dist/backend/api/translation-routes.js +2 -2
- package/dist/backend/cli/install-vcm-harness.js +68 -0
- package/dist/backend/gateway/gateway-service.js +4 -3
- package/dist/backend/server.js +25 -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 +850 -0
- package/dist/backend/services/harness-service.js +90 -3
- 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/services/task-service.js +2 -0
- package/dist/backend/templates/harness/claude-root.js +2 -0
- package/dist/backend/templates/harness/codex-review.js +605 -0
- package/dist/backend/templates/harness/project-manager-agent.js +13 -3
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +6 -1
- 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/codex-review.js +5 -0
- package/dist/shared/types/session.js +44 -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/codex-review-gates.md +553 -0
- package/docs/gateway-design.md +5 -4
- package/docs/known-issues.md +1 -0
- package/docs/product-design.md +55 -10
- package/package.json +1 -1
- package/scripts/verify-package.mjs +4 -0
- package/dist-frontend/assets/index-CT20u9Fk.js +0 -92
- package/dist-frontend/assets/index-DC9-SB7F.css +0 -32
|
@@ -4,6 +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, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
|
|
7
8
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
8
9
|
import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
|
|
9
10
|
import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
|
|
@@ -88,6 +89,76 @@ const HARNESS_FILES = [
|
|
|
88
89
|
ownership: "whole-file",
|
|
89
90
|
renderRules: renderVcmLongRunningValidationSkillRules
|
|
90
91
|
},
|
|
92
|
+
{
|
|
93
|
+
kind: "skill-vcm-codex-review-gate",
|
|
94
|
+
path: ".claude/skills/vcm-codex-review-gate/SKILL.md",
|
|
95
|
+
title: "VCM Codex Review Gate Skill",
|
|
96
|
+
frontmatter: renderSkillFrontmatter("vcm-codex-review-gate", "Use when project-manager reaches a Codex Review Gate or receives a VCM Codex Review callback."),
|
|
97
|
+
ownership: "whole-file",
|
|
98
|
+
renderRules: renderVcmCodexReviewGateSkillRules
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
kind: "codex-agents",
|
|
102
|
+
path: ".ai/codex/AGENTS.md",
|
|
103
|
+
title: "VCM Codex Reviewer",
|
|
104
|
+
renderRules: renderCodexAgentsHarnessRules
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: "codex-config",
|
|
108
|
+
path: ".ai/codex/config.toml",
|
|
109
|
+
title: "VCM Codex Config",
|
|
110
|
+
ownership: "raw-file",
|
|
111
|
+
renderRules: renderCodexConfigHarnessRules
|
|
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
|
+
},
|
|
127
|
+
{
|
|
128
|
+
kind: "codex-prompt-architecture-plan",
|
|
129
|
+
path: ".ai/codex/prompts/architecture-plan-gate.md",
|
|
130
|
+
title: "Codex Architecture Plan Gate Prompt",
|
|
131
|
+
ownership: "raw-file",
|
|
132
|
+
renderRules: renderCodexArchitecturePlanPrompt
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
kind: "codex-prompt-validation-adequacy",
|
|
136
|
+
path: ".ai/codex/prompts/validation-adequacy-gate.md",
|
|
137
|
+
title: "Codex Validation Adequacy Gate Prompt",
|
|
138
|
+
ownership: "raw-file",
|
|
139
|
+
renderRules: renderCodexValidationAdequacyPrompt
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: "codex-prompt-final-diff",
|
|
143
|
+
path: ".ai/codex/prompts/final-diff-gate.md",
|
|
144
|
+
title: "Codex Final Diff Gate Prompt",
|
|
145
|
+
ownership: "raw-file",
|
|
146
|
+
renderRules: renderCodexFinalDiffPrompt
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
kind: "codex-review-schema",
|
|
150
|
+
path: ".ai/codex/schemas/codex-review-result.schema.json",
|
|
151
|
+
title: "Codex Review Result Schema",
|
|
152
|
+
ownership: "raw-file",
|
|
153
|
+
renderRules: renderCodexReviewResultSchema
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
kind: "tool-request-codex-review",
|
|
157
|
+
path: ".ai/tools/request-codex-review",
|
|
158
|
+
title: "Request Codex Review Tool",
|
|
159
|
+
ownership: "raw-file",
|
|
160
|
+
renderRules: renderRequestCodexReviewTool
|
|
161
|
+
},
|
|
91
162
|
{
|
|
92
163
|
kind: "agent-project-manager",
|
|
93
164
|
path: ".claude/agents/project-manager.md",
|
|
@@ -232,13 +303,13 @@ async function analyzeHarnessFiles(fs, repoRoot) {
|
|
|
232
303
|
}
|
|
233
304
|
async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
234
305
|
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
235
|
-
const expectedContent = definition.ownership === "whole-file"
|
|
306
|
+
const expectedContent = definition.ownership === "whole-file" || definition.ownership === "raw-file"
|
|
236
307
|
? renderWholeHarnessFile(definition)
|
|
237
308
|
: undefined;
|
|
238
|
-
const expectedBlock = definition.ownership === "whole-file"
|
|
309
|
+
const expectedBlock = definition.ownership === "whole-file" || definition.ownership === "raw-file"
|
|
239
310
|
? undefined
|
|
240
311
|
: renderManagedBlock(definition, definition.renderRules());
|
|
241
|
-
const managedBlockPattern = definition.ownership === "whole-file"
|
|
312
|
+
const managedBlockPattern = definition.ownership === "whole-file" || definition.ownership === "raw-file"
|
|
242
313
|
? undefined
|
|
243
314
|
: getManagedBlockPattern(definition);
|
|
244
315
|
const exists = await fs.pathExists(absolutePath);
|
|
@@ -336,8 +407,21 @@ function renderHarnessStatus(analyses) {
|
|
|
336
407
|
const plannedChanges = analyses
|
|
337
408
|
.map((analysis) => analysis.plannedChange)
|
|
338
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));
|
|
339
422
|
return {
|
|
340
423
|
version: VCM_HARNESS_VERSION,
|
|
424
|
+
initialized,
|
|
341
425
|
files,
|
|
342
426
|
needsApply: plannedChanges.length > 0,
|
|
343
427
|
plannedChanges,
|
|
@@ -365,6 +449,9 @@ function renderNewHarnessFile(definition, block) {
|
|
|
365
449
|
return `${frontmatter}# ${definition.title}\n\n${block}\n`;
|
|
366
450
|
}
|
|
367
451
|
function renderWholeHarnessFile(definition) {
|
|
452
|
+
if (definition.ownership === "raw-file") {
|
|
453
|
+
return ensureTrailingNewline(definition.renderRules().trimEnd());
|
|
454
|
+
}
|
|
368
455
|
return ensureTrailingNewline(renderNewHarnessFile(definition, definition.renderRules().trimEnd()));
|
|
369
456
|
}
|
|
370
457
|
async function analyzeClaudeSettingsFile(fs, repoRoot) {
|
|
@@ -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) {
|
|
@@ -5,6 +5,10 @@ import { VcmError } from "../errors.js";
|
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
7
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
8
|
+
const CODEX_REVIEWER_ROLE = "codex-reviewer";
|
|
9
|
+
const CODEX_DIR = ".ai/codex";
|
|
10
|
+
const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
|
|
11
|
+
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
8
12
|
export function createSessionService(deps) {
|
|
9
13
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
10
14
|
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
@@ -17,29 +21,38 @@ export function createSessionService(deps) {
|
|
|
17
21
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
18
22
|
const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
|
|
19
23
|
const persisted = await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
|
|
24
|
+
const isCodexReviewer = role === CODEX_REVIEWER_ROLE;
|
|
20
25
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
21
|
-
const model =
|
|
26
|
+
const model = isCodexReviewer
|
|
27
|
+
? normalizeCodexModel(input.model ?? persisted?.model)
|
|
28
|
+
: normalizeClaudeModel(input.model ?? persisted?.model);
|
|
29
|
+
const effort = normalizeSessionEffort(input.effort ?? persisted?.effort);
|
|
22
30
|
const claudeSessionId = launchMode === "resume"
|
|
23
31
|
? persisted?.claudeSessionId
|
|
24
32
|
: randomUUID();
|
|
25
33
|
if (!claudeSessionId) {
|
|
26
34
|
throw new VcmError({
|
|
27
35
|
code: "CLAUDE_SESSION_MISSING",
|
|
28
|
-
message: `${role} does not have a
|
|
36
|
+
message: `${role} does not have a session id to resume.`,
|
|
29
37
|
statusCode: 409,
|
|
30
38
|
hint: "Start the role once before using Resume."
|
|
31
39
|
});
|
|
32
40
|
}
|
|
33
41
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
34
42
|
? persisted.transcriptPath
|
|
35
|
-
: claudeTranscriptPath(taskRepoRoot, claudeSessionId);
|
|
36
|
-
const startCommand =
|
|
43
|
+
: isCodexReviewer ? undefined : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
|
|
44
|
+
const startCommand = isCodexReviewer
|
|
45
|
+
? await buildCodexReviewerStartCommand(deps.fs, taskRepoRoot, launchMode, model, effort)
|
|
46
|
+
: {
|
|
47
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
48
|
+
cwd: taskRepoRoot
|
|
49
|
+
};
|
|
37
50
|
const runtimeSession = await deps.runtime.createSession({
|
|
38
51
|
taskSlug,
|
|
39
52
|
role,
|
|
40
53
|
command: startCommand.command,
|
|
41
54
|
args: startCommand.args,
|
|
42
|
-
cwd:
|
|
55
|
+
cwd: startCommand.cwd,
|
|
43
56
|
env: {
|
|
44
57
|
VCM_API_URL: deps.apiUrl,
|
|
45
58
|
VCM_TASK_REPO_ROOT: taskRepoRoot,
|
|
@@ -63,7 +76,8 @@ export function createSessionService(deps) {
|
|
|
63
76
|
command: startCommand.display,
|
|
64
77
|
permissionMode,
|
|
65
78
|
model,
|
|
66
|
-
|
|
79
|
+
effort,
|
|
80
|
+
cwd: startCommand.cwd,
|
|
67
81
|
terminalBackend: "node-pty",
|
|
68
82
|
pid: runtimeSession.pid,
|
|
69
83
|
logPath: paths.roleLogPaths[role],
|
|
@@ -159,15 +173,18 @@ export function createSessionService(deps) {
|
|
|
159
173
|
}
|
|
160
174
|
return sessions;
|
|
161
175
|
},
|
|
162
|
-
async
|
|
176
|
+
async recordRoleHookEvent(repoRoot, input) {
|
|
163
177
|
const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
|
|
164
|
-
if (!current || !
|
|
178
|
+
if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
|
|
165
179
|
return undefined;
|
|
166
180
|
}
|
|
167
181
|
const timestamp = now();
|
|
168
182
|
const isStop = input.eventName === "Stop";
|
|
169
183
|
const updated = {
|
|
170
184
|
...current,
|
|
185
|
+
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
186
|
+
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
187
|
+
cwd: input.cwd ?? current.cwd,
|
|
171
188
|
activityStatus: isStop ? "idle" : "running",
|
|
172
189
|
lastHookEventAt: timestamp,
|
|
173
190
|
lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
|
|
@@ -180,6 +197,16 @@ export function createSessionService(deps) {
|
|
|
180
197
|
await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
181
198
|
return updated;
|
|
182
199
|
},
|
|
200
|
+
recordClaudeHookEvent(repoRoot, input) {
|
|
201
|
+
return this.recordRoleHookEvent(repoRoot, {
|
|
202
|
+
taskSlug: input.taskSlug,
|
|
203
|
+
role: input.role,
|
|
204
|
+
eventName: input.eventName,
|
|
205
|
+
sessionId: input.claudeSessionId,
|
|
206
|
+
transcriptPath: input.transcriptPath,
|
|
207
|
+
cwd: input.cwd
|
|
208
|
+
});
|
|
209
|
+
},
|
|
183
210
|
async markRoleActivityRunning(repoRoot, taskSlug, role) {
|
|
184
211
|
const current = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
185
212
|
if (!current) {
|
|
@@ -201,14 +228,67 @@ export function createSessionService(deps) {
|
|
|
201
228
|
}
|
|
202
229
|
};
|
|
203
230
|
}
|
|
204
|
-
function
|
|
205
|
-
|
|
231
|
+
async function buildCodexReviewerStartCommand(fs, taskRepoRoot, launchMode, selectedModel, selectedEffort) {
|
|
232
|
+
const codexDir = resolveRepoPath(taskRepoRoot, CODEX_DIR);
|
|
233
|
+
const reviewDir = resolveRepoPath(taskRepoRoot, CODEX_REVIEW_DIR);
|
|
234
|
+
if (!(await fs.pathExists(codexDir))) {
|
|
235
|
+
throw new VcmError({
|
|
236
|
+
code: "CODEX_REVIEW_CONFIG_MISSING",
|
|
237
|
+
message: `${CODEX_DIR} does not exist.`,
|
|
238
|
+
statusCode: 409,
|
|
239
|
+
hint: "Apply the VCM harness before starting Codex Reviewer."
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
await fs.ensureDir(reviewDir);
|
|
243
|
+
const config = await loadCodexSessionConfig(fs, taskRepoRoot);
|
|
244
|
+
const model = selectedModel === "default"
|
|
245
|
+
? config.model
|
|
246
|
+
: selectedModel;
|
|
247
|
+
const modelReasoningEffort = selectedEffort === "default"
|
|
248
|
+
? config.modelReasoningEffort
|
|
249
|
+
: selectedEffort;
|
|
250
|
+
const args = launchMode === "resume"
|
|
251
|
+
? ["resume", "--last"]
|
|
252
|
+
: [];
|
|
253
|
+
args.push("--cd", codexDir, "--add-dir", reviewDir, "--sandbox", "workspace-write", "--ask-for-approval", "never", "--dangerously-bypass-hook-trust");
|
|
254
|
+
if (model && model !== "default") {
|
|
255
|
+
args.push("--model", model);
|
|
256
|
+
}
|
|
257
|
+
if (modelReasoningEffort && modelReasoningEffort !== "default") {
|
|
258
|
+
args.push("--config", `model_reasoning_effort="${modelReasoningEffort}"`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
command: config.command,
|
|
262
|
+
args,
|
|
263
|
+
cwd: taskRepoRoot,
|
|
264
|
+
display: [config.command, ...args].map(formatDisplayArg).join(" ")
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function loadCodexSessionConfig(fs, taskRepoRoot) {
|
|
268
|
+
const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
|
|
269
|
+
if (!(await fs.pathExists(configPath))) {
|
|
270
|
+
return {
|
|
271
|
+
command: "codex",
|
|
272
|
+
model: "gpt-5.5",
|
|
273
|
+
modelReasoningEffort: "xhigh"
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const content = await fs.readText(configPath);
|
|
277
|
+
const reviewSection = extractTomlSection(content, "vcm.codex_review");
|
|
278
|
+
return {
|
|
279
|
+
command: parseTomlString(reviewSection, "command") ?? "codex",
|
|
280
|
+
model: parseTomlString(content, "model") ?? "gpt-5.5",
|
|
281
|
+
modelReasoningEffort: parseTomlString(content, "model_reasoning_effort") ?? "xhigh"
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function matchesRoleHookSession(record, input) {
|
|
285
|
+
if (input.sessionId && record.claudeSessionId === input.sessionId) {
|
|
206
286
|
return true;
|
|
207
287
|
}
|
|
208
288
|
if (input.transcriptPath && record.transcriptPath === input.transcriptPath) {
|
|
209
289
|
return true;
|
|
210
290
|
}
|
|
211
|
-
if (!input.
|
|
291
|
+
if (!input.sessionId && !input.transcriptPath) {
|
|
212
292
|
return true;
|
|
213
293
|
}
|
|
214
294
|
return false;
|
|
@@ -249,7 +329,10 @@ async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role)
|
|
|
249
329
|
? legacy.lastStopAt
|
|
250
330
|
: undefined),
|
|
251
331
|
permissionMode: normalizeClaudePermissionMode(record.permissionMode),
|
|
252
|
-
model:
|
|
332
|
+
model: record.role === CODEX_REVIEWER_ROLE
|
|
333
|
+
? normalizeCodexModel(record.model)
|
|
334
|
+
: normalizeClaudeModel(record.model),
|
|
335
|
+
effort: normalizeSessionEffort(record.effort)
|
|
253
336
|
}
|
|
254
337
|
: undefined;
|
|
255
338
|
}
|
|
@@ -311,3 +394,49 @@ function normalizeClaudeModel(value) {
|
|
|
311
394
|
}
|
|
312
395
|
return "default";
|
|
313
396
|
}
|
|
397
|
+
function normalizeCodexModel(value) {
|
|
398
|
+
if (value === "default" || value === "gpt-5.5") {
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
return "gpt-5.5";
|
|
402
|
+
}
|
|
403
|
+
function normalizeSessionEffort(value) {
|
|
404
|
+
if (value === "low"
|
|
405
|
+
|| value === "medium"
|
|
406
|
+
|| value === "high"
|
|
407
|
+
|| value === "xhigh"
|
|
408
|
+
|| value === "max") {
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
return "default";
|
|
412
|
+
}
|
|
413
|
+
function extractTomlSection(content, sectionName) {
|
|
414
|
+
const lines = content.split(/\r?\n/);
|
|
415
|
+
const header = `[${sectionName}]`;
|
|
416
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
417
|
+
if (start < 0) {
|
|
418
|
+
return "";
|
|
419
|
+
}
|
|
420
|
+
const section = [];
|
|
421
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
422
|
+
const line = lines[index];
|
|
423
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(line)) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
section.push(line);
|
|
427
|
+
}
|
|
428
|
+
return section.join("\n");
|
|
429
|
+
}
|
|
430
|
+
function parseTomlString(content, key) {
|
|
431
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "m");
|
|
432
|
+
return pattern.exec(content)?.[1];
|
|
433
|
+
}
|
|
434
|
+
function escapeRegExp(value) {
|
|
435
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
436
|
+
}
|
|
437
|
+
function formatDisplayArg(value) {
|
|
438
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
442
|
+
}
|
|
@@ -204,6 +204,7 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
|
|
|
204
204
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "messages"));
|
|
205
205
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
|
|
206
206
|
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
|
|
207
|
+
await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "codex-reviews"));
|
|
207
208
|
}
|
|
208
209
|
async function findActiveInlineTask(fs, taskStoreRoot) {
|
|
209
210
|
const tasksDir = path.join(taskStoreRoot, "tasks");
|
|
@@ -255,6 +256,7 @@ function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot,
|
|
|
255
256
|
path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
|
|
256
257
|
path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
|
|
257
258
|
path.join(taskRepoRoot, stateRoot, "translation", taskSlug),
|
|
259
|
+
path.join(taskRepoRoot, stateRoot, "codex-reviews"),
|
|
258
260
|
path.join(taskRepoRoot, handoffRoot)
|
|
259
261
|
];
|
|
260
262
|
}
|
|
@@ -5,6 +5,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
5
5
|
- Read module-local \`CLAUDE.md\` before editing a subdirectory if one exists.
|
|
6
6
|
- Use \`vcm-route-message\` whenever a VCM role hands off work, asks another role a question, reports a result, reports a blocker, or raises a finding. Follow its write-then-stop rule.
|
|
7
7
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
8
|
+
- Project-manager uses \`vcm-codex-review-gate\` at enabled Codex Review Gate trigger points and on VCM Codex review callbacks.
|
|
8
9
|
|
|
9
10
|
## VCM Background Jobs
|
|
10
11
|
|
|
@@ -31,6 +32,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
31
32
|
- Test-only or validation-only work may use: \`project-manager -> reviewer -> project-manager final acceptance\`.
|
|
32
33
|
- If a docs/test/validation-only task reveals required code, architecture, public contract, dependency, durable-doc, or test-strategy changes, route back through the full code-change flow.
|
|
33
34
|
- Keep role outputs under \`.ai/vcm/handoffs/\`.
|
|
35
|
+
- Codex Review Gate reports live under \`.ai/vcm/codex-reviews/\` and are VCM-managed task evidence.
|
|
34
36
|
- Runtime task records and handoffs under \`.ai/vcm/\` are temporary. Durable facts must move into code, tests, PR text, commit history, or long-term docs.
|
|
35
37
|
- Record current-task unresolved findings in \`.ai/vcm/handoffs/known-issues.md\`.
|
|
36
38
|
|