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.
Files changed (41) hide show
  1. package/README.md +9 -6
  2. package/dist/backend/adapters/claude-adapter.js +4 -1
  3. package/dist/backend/api/codex-hook-routes.js +9 -0
  4. package/dist/backend/api/codex-review-routes.js +58 -0
  5. package/dist/backend/api/translation-routes.js +2 -2
  6. package/dist/backend/cli/install-vcm-harness.js +68 -0
  7. package/dist/backend/gateway/gateway-service.js +4 -3
  8. package/dist/backend/server.js +25 -0
  9. package/dist/backend/services/app-settings-service.js +12 -4
  10. package/dist/backend/services/artifact-service.js +2 -1
  11. package/dist/backend/services/claude-hook-service.js +3 -3
  12. package/dist/backend/services/codex-hook-service.js +87 -0
  13. package/dist/backend/services/codex-review-service.js +850 -0
  14. package/dist/backend/services/harness-service.js +90 -3
  15. package/dist/backend/services/message-service.js +8 -7
  16. package/dist/backend/services/project-service.js +2 -2
  17. package/dist/backend/services/round-service.js +29 -25
  18. package/dist/backend/services/session-service.js +141 -12
  19. package/dist/backend/services/task-service.js +2 -0
  20. package/dist/backend/templates/harness/claude-root.js +2 -0
  21. package/dist/backend/templates/harness/codex-review.js +605 -0
  22. package/dist/backend/templates/harness/project-manager-agent.js +13 -3
  23. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +6 -1
  24. package/dist/shared/constants.js +15 -1
  25. package/dist/shared/types/app-settings.js +4 -3
  26. package/dist/shared/types/codex-hook.js +1 -0
  27. package/dist/shared/types/codex-review.js +5 -0
  28. package/dist/shared/types/session.js +44 -0
  29. package/dist-frontend/assets/index-BavJjWQY.js +92 -0
  30. package/dist-frontend/assets/index-CR1EOe-w.css +32 -0
  31. package/dist-frontend/index.html +2 -2
  32. package/docs/ARCHITECTURE.md +1 -0
  33. package/docs/TESTING.md +82 -0
  34. package/docs/codex-review-gates.md +553 -0
  35. package/docs/gateway-design.md +5 -4
  36. package/docs/known-issues.md +1 -0
  37. package/docs/product-design.md +55 -10
  38. package/package.json +1 -1
  39. package/scripts/verify-package.mjs +4 -0
  40. package/dist-frontend/assets/index-CT20u9Fk.js +0 -92
  41. 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 { ROLE_NAMES, isRoleName } from "../../shared/constants.js";
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 ROLE_NAMES) {
286
- for (const toRole of ROLE_NAMES) {
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 (!ROLE_NAMES.includes(toRole)) {
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 (!isRoleName(String(fromRole))) {
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
- if (!message.routePath || !isRoleName(String(message.fromRole))) {
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, message.fromRole, message.toRole);
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 { ROLE_NAMES } from "../../shared/constants.js";
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: [...ROLE_NAMES],
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
- async recordClaudeHookEvent(input) {
159
- return withTaskLock(input, async () => {
160
- const timestamp = now();
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 = normalizeClaudeModel(input.model ?? persisted?.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 Claude session id to resume.`,
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 = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model);
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: taskRepoRoot,
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
- cwd: taskRepoRoot,
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 recordClaudeHookEvent(repoRoot, input) {
176
+ async recordRoleHookEvent(repoRoot, input) {
163
177
  const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
164
- if (!current || !matchesClaudeHookSession(current, input)) {
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 matchesClaudeHookSession(record, input) {
205
- if (input.claudeSessionId && record.claudeSessionId === input.claudeSessionId) {
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.claudeSessionId && !input.transcriptPath) {
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: normalizeClaudeModel(record.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