vibe-coding-master 0.3.0 → 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 (35) 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 +4 -0
  5. package/dist/backend/api/translation-routes.js +2 -2
  6. package/dist/backend/cli/install-vcm-harness.js +14 -1
  7. package/dist/backend/gateway/gateway-service.js +4 -3
  8. package/dist/backend/server.js +10 -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 +144 -59
  14. package/dist/backend/services/harness-service.js +28 -1
  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/templates/harness/codex-review.js +37 -6
  20. package/dist/shared/constants.js +15 -1
  21. package/dist/shared/types/app-settings.js +4 -3
  22. package/dist/shared/types/codex-hook.js +1 -0
  23. package/dist/shared/types/session.js +44 -0
  24. package/dist-frontend/assets/index-BavJjWQY.js +92 -0
  25. package/dist-frontend/assets/index-CR1EOe-w.css +32 -0
  26. package/dist-frontend/index.html +2 -2
  27. package/docs/ARCHITECTURE.md +1 -0
  28. package/docs/TESTING.md +82 -0
  29. package/docs/codex-review-gates.md +83 -45
  30. package/docs/gateway-design.md +5 -4
  31. package/docs/known-issues.md +1 -0
  32. package/docs/product-design.md +26 -10
  33. package/package.json +1 -1
  34. package/dist-frontend/assets/index-C9l94uxA.js +0 -92
  35. package/dist-frontend/assets/index-D6jWo6Jd.css +0 -32
package/README.md CHANGED
@@ -11,6 +11,8 @@ VCM is designed for long-running coding work where one Claude Code conversation
11
11
 
12
12
  Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, restart, switch, observe, and manually intervene in those sessions without juggling separate terminal windows.
13
13
 
14
+ When Codex Review Gates are enabled for a task, or when a Codex Reviewer session already exists, the workspace can also show a fifth `Codex Reviewer` terminal role. It runs Codex CLI from `.ai/codex` with Codex model and effort selectors, receives gate prompts in the same long-lived terminal session, reports hook state back to VCM, and stays outside the normal Claude Code PM routing flow.
15
+
14
16
  ## Current Capabilities
15
17
 
16
18
  - GUI-first task workspace.
@@ -19,6 +21,7 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
19
21
  - Connected repository status for the base repo, including branch, upstream status, commit hash, dirty state, and fast-forward-only pull.
20
22
  - Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
21
23
  - One Claude Code session per role, with role tabs in the task header.
24
+ - Optional Codex Reviewer terminal role when any Codex Review Gate is enabled.
22
25
  - Role session recovery through persisted Claude session ids and `claude --resume`.
23
26
  - Permission mode selection before start, resume, or restart:
24
27
  - `default`
@@ -26,7 +29,7 @@ Each role runs as a real Claude Code process inside an embedded terminal. The GU
26
29
  - PM-mediated role messaging through VCM-dispatched route files.
27
30
  - Manual and automatic orchestration modes.
28
31
  - Two-stage VCM harness setup: deterministic fixed install plus AI-assisted bootstrap.
29
- - VCM-managed root rules, four role agents, repo-local VCM skills, Claude Code hooks, generated-context tools, and PR template.
32
+ - VCM-managed root rules, four role agents, repo-local VCM skills, Claude Code hooks, Codex Reviewer hooks, generated-context tools, and PR template.
30
33
  - Rust generated context for module indexing and crate-external public surface indexing.
31
34
  - Translation panel powered by an OpenAI-compatible low-cost model.
32
35
  - Mobile Gateway through Tencent iLink Bot API / Weixin DM, for talking to PM and managing tasks from Weixin.
@@ -233,7 +236,7 @@ the base repo has uncommitted changes, when the current branch has no upstream,
233
236
  or when the active task is an inline task using the base repo directly. It does
234
237
  not stash, merge, or mutate task worktrees.
235
238
 
236
- When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the active VCM Session title, task status, start time, total elapsed time, total Round count, and Claude Code active runtime. If a Round is currently running, the dock also shows the Current Round start time, total elapsed time, Claude Code active runtime, and Turn count.
239
+ When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the active VCM Session title, task status, start time, total elapsed time, total Round count, and role active runtime. If a Round is currently running, the dock also shows the Current Round start time, total elapsed time, role active runtime, and Turn count.
237
240
 
238
241
  ## Mobile Gateway
239
242
 
@@ -356,7 +359,7 @@ Typical mobile flow:
356
359
 
357
360
  `/pull-current` only pulls the connected base repository. It does not pull task worktrees, stash local changes, merge divergent branches, or run arbitrary shell commands.
358
361
 
359
- `/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, auto orchestration, and translation defaults for the four role sessions.
362
+ `/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, effort, auto orchestration, and translation defaults for the four role sessions.
360
363
 
361
364
  `/close-task` is destructive. It stops VCM-managed role sessions and removes task-owned worktree/branch state according to the same cleanup behavior as the desktop `Close Task` action.
362
365
 
@@ -478,7 +481,7 @@ For `.gitignore`, VCM uses a gitignore-native managed block:
478
481
 
479
482
  `.ai/vcm/` is the active VCM local control area, and `.claude/worktrees/` is the Claude-compatible task worktree area. VCM keeps the task index in app-local project state under `~/.vcm/projects/`; each task runtime repo keeps its own session, message, orchestration, and translation state.
480
483
 
481
- VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running.
484
+ VCM also JSON-merges `.claude/settings.json` to install Claude Code `PreToolUse`, `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks plus a managed `env.BASH_DEFAULT_TIMEOUT_MS` so foreground watch windows fit inside the Bash tool timeout. The hooks post directly to the local VCM backend, so roles do not need a VCM CLI command to confirm delivery or report turn completion. The `Stop` hook forwards the backend response to Claude Code, which lets VCM block turn-end while a validation job is still running. When Codex Review Gates are installed, VCM also writes `.ai/codex/.codex/config.toml` and `.ai/codex/.codex/hooks.json` so the embedded Codex Reviewer terminal can POST `UserPromptSubmit` and `Stop` events back to VCM.
482
485
 
483
486
  Bootstrap is AI-assisted. VCM starts a visible temporary Claude Code session in the connected repository and asks it to use the `vcm-harness-bootstrap` skill. Bootstrap fills project-specific content and generated context:
484
487
 
@@ -565,7 +568,7 @@ When it is on, VCM is in auto mode:
565
568
  - When the target role later reaches `Stop`, VCM scans again and may deliver the next pending route file.
566
569
  - If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears pending route files. It does not mutate message history.
567
570
 
568
- VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. The terminal process status is still tracked separately. The injected role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn.
571
+ VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`, and Codex Reviewer hooks into `.ai/codex/.codex/hooks.json`. Role tabs become `running` when Claude Code or Codex accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. The terminal process status is still tracked separately. Claude role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn; Codex Reviewer does not dispatch route files.
569
572
 
570
573
  The implementation keeps only the active manual/auto orchestration mode. It does not expose pause/resume, stage/approve/reject, or a separate agent-facing message CLI.
571
574
 
@@ -579,7 +582,7 @@ The normal path is timer-driven: `Stop` starts a backend timer, and `UserPromptS
579
582
 
580
583
  When `Flow pause alert` is enabled, the frontend polls the task Round state and deduplicates each stopped Round so the same stopped state does not alert on every poll. Flow duration is measured from the first `UserPromptSubmit` to `stoppedAt`, falling back to the last `Stop` when needed. If the flow lasted less than 2 minutes, it plays the local chime 3 times at 1.4 second intervals. If the flow lasted 2 minutes or longer, it shows a modal alert and repeats the local chime until the user clicks `Confirm`. A stopped Round can mean normal completion, user decision needed, dispatch failure, or another workflow interruption; the point is to get the user to look with the right amount of urgency.
581
584
 
582
- The Current Round dock separates wall-clock Round duration from Claude Code active runtime. `Total` is `now - Round.startedAt`; `CC runtime` is only the accumulated time between each Turn's `UserPromptSubmit` and `Stop`; `Turn count` is the number of accepted prompts inside the current Round.
585
+ The Current Round dock separates wall-clock Round duration from role active runtime. `Total` is `now - Round.startedAt`; `Role runtime` is only the accumulated time between each Turn's `UserPromptSubmit` and `Stop`; `Turn count` is the number of accepted prompts inside the current Round.
583
586
 
584
587
  ## Resume Behavior
585
588
 
@@ -17,12 +17,15 @@ export function createClaudeAdapter(runner) {
17
17
  }
18
18
  return result.stdout.trim();
19
19
  },
20
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default") {
20
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default") {
21
21
  const args = ["--agent", role];
22
22
  if (claudeSessionId) {
23
23
  args.push(resume ? "--resume" : "--session-id", claudeSessionId);
24
24
  }
25
25
  args.push("--model", model);
26
+ if (effort !== "default") {
27
+ args.push("--effort", effort);
28
+ }
26
29
  if (permissionMode === "bypassPermissions") {
27
30
  args.push("--permission-mode", "bypassPermissions");
28
31
  }
@@ -0,0 +1,9 @@
1
+ export function registerCodexHookRoutes(app, deps) {
2
+ app.post("/api/hooks/codex-reviewer", async (request) => {
3
+ return deps.codexHookService.handleHook(request.body);
4
+ });
5
+ app.post("/api/hooks/codex-reviewer/stop", async (request) => {
6
+ await deps.codexHookService.handleStopHook(request.body);
7
+ return {};
8
+ });
9
+ }
@@ -5,6 +5,10 @@ export function registerCodexReviewRoutes(app, deps) {
5
5
  const project = await requireCurrentProject(deps.projectService);
6
6
  return deps.codexReviewService.getState(project.repoRoot, request.params.taskSlug);
7
7
  });
8
+ app.put("/api/tasks/:taskSlug/codex-review/settings", async (request) => {
9
+ const project = await requireCurrentProject(deps.projectService);
10
+ return deps.codexReviewService.updateSettings(project.repoRoot, request.params.taskSlug, request.body);
11
+ });
8
12
  app.post("/api/tasks/:taskSlug/codex-review/:gate/request", async (request) => {
9
13
  const project = await requireCurrentProject(deps.projectService);
10
14
  const gate = parseGate(request.params.gate);
@@ -1,4 +1,4 @@
1
- import { isRoleName } from "../../shared/constants.js";
1
+ import { isVcmRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
4
  export function registerTranslationRoutes(app, deps) {
@@ -74,7 +74,7 @@ export function registerTranslationRoutes(app, deps) {
74
74
  });
75
75
  }
76
76
  function parseRole(role) {
77
- if (!isRoleName(role)) {
77
+ if (!isVcmRoleName(role)) {
78
78
  throw new VcmError({
79
79
  code: "UNKNOWN_ROLE",
80
80
  message: `Unknown role: ${role}`,
@@ -6,7 +6,7 @@ import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
8
8
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
9
- import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
9
+ import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
10
10
  import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
11
11
  import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
12
12
  import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
@@ -174,6 +174,18 @@ const WHOLE_FILES = [
174
174
  mode: 0o644,
175
175
  content: renderCodexConfigHarnessRules()
176
176
  },
177
+ {
178
+ path: ".ai/codex/.codex/config.toml",
179
+ category: "codex-review-hooks",
180
+ mode: 0o644,
181
+ content: renderCodexCliConfigHarnessRules()
182
+ },
183
+ {
184
+ path: ".ai/codex/.codex/hooks.json",
185
+ category: "codex-review-hooks",
186
+ mode: 0o644,
187
+ content: renderCodexHooksHarnessRules()
188
+ },
177
189
  {
178
190
  path: ".ai/codex/prompts/architecture-plan-gate.md",
179
191
  category: "codex-review-prompt",
@@ -417,6 +429,7 @@ function fixedDirectories() {
417
429
  ".claude/skills/vcm-route-message/",
418
430
  ".claude/skills/vcm-codex-review-gate/",
419
431
  ".ai/codex/",
432
+ ".ai/codex/.codex/",
420
433
  ".ai/codex/prompts/",
421
434
  ".ai/codex/schemas/",
422
435
  ".ai/vcm/codex-reviews/",
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { ROLE_DEFINITIONS } from "../../shared/constants.js";
2
+ import { VCM_ROLE_DEFINITIONS } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
5
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -396,14 +396,15 @@ export function createGatewayService(deps) {
396
396
  mode: template.autoOrchestration ? "auto" : "manual"
397
397
  });
398
398
  const startedRoles = [];
399
- for (const definition of ROLE_DEFINITIONS) {
399
+ for (const definition of VCM_ROLE_DEFINITIONS) {
400
400
  const roleTemplate = template.roles[definition.name];
401
401
  try {
402
402
  await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, {
403
403
  cols: 100,
404
404
  rows: 28,
405
405
  permissionMode: roleTemplate.permissionMode,
406
- model: roleTemplate.model
406
+ model: roleTemplate.model,
407
+ effort: roleTemplate.effort
407
408
  });
408
409
  startedRoles.push(definition.name);
409
410
  }
@@ -7,6 +7,7 @@ import { createClaudeAdapter } from "./adapters/claude-adapter.js";
7
7
  import { createCommandRunner } from "./adapters/command-runner.js";
8
8
  import { createCommandDispatcher } from "./services/command-dispatcher.js";
9
9
  import { createClaudeHookService } from "./services/claude-hook-service.js";
10
+ import { createCodexHookService } from "./services/codex-hook-service.js";
10
11
  import { createGitAdapter } from "./adapters/git-adapter.js";
11
12
  import { createAppSettingsService } from "./services/app-settings-service.js";
12
13
  import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
@@ -32,6 +33,7 @@ import { createTranslationService } from "./services/translation-service.js";
32
33
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
33
34
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
34
35
  import { registerClaudeHookRoutes } from "./api/claude-hook-routes.js";
36
+ import { registerCodexHookRoutes } from "./api/codex-hook-routes.js";
35
37
  import { registerCodexReviewRoutes } from "./api/codex-review-routes.js";
36
38
  import { registerHarnessRoutes } from "./api/harness-routes.js";
37
39
  import { registerMessageRoutes } from "./api/message-routes.js";
@@ -58,6 +60,7 @@ export async function createServer(deps, options = {}) {
58
60
  });
59
61
  registerAppSettingsRoutes(app, { appSettings: deps.appSettings });
60
62
  registerClaudeHookRoutes(app, { claudeHookService: deps.claudeHookService });
63
+ registerCodexHookRoutes(app, { codexHookService: deps.codexHookService });
61
64
  registerCodexReviewRoutes(app, {
62
65
  projectService: deps.projectService,
63
66
  codexReviewService: deps.codexReviewService
@@ -236,6 +239,12 @@ export function createDefaultServerDeps(options = {}) {
236
239
  gatewayService,
237
240
  jobGuard: createJobGuardService()
238
241
  });
242
+ const codexHookService = createCodexHookService({
243
+ projectService,
244
+ taskService,
245
+ sessionService,
246
+ roundService
247
+ });
239
248
  return {
240
249
  appSettings,
241
250
  projectService,
@@ -245,6 +254,7 @@ export function createDefaultServerDeps(options = {}) {
245
254
  harnessService,
246
255
  commandDispatcher,
247
256
  claudeHookService,
257
+ codexHookService,
248
258
  messageService,
249
259
  codexReviewService,
250
260
  roundService,
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { createHash } from "node:crypto";
4
- import { ROLE_NAMES } from "../../shared/constants.js";
4
+ import { VCM_ROLE_NAMES } from "../../shared/constants.js";
5
5
  import { createDefaultLaunchTemplate } from "../../shared/types/app-settings.js";
6
- import { CLAUDE_MODEL_OPTIONS } from "../../shared/types/session.js";
6
+ import { CLAUDE_MODEL_OPTIONS, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
7
7
  const MAX_RECENT_REPOSITORIES = 5;
8
8
  export function createAppSettingsService(deps) {
9
9
  const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "settings.json");
@@ -211,7 +211,7 @@ function normalizeLaunchTemplate(input) {
211
211
  }
212
212
  const rawRoles = isObject(input.roles) ? input.roles : {};
213
213
  const roles = {};
214
- for (const role of ROLE_NAMES) {
214
+ for (const role of VCM_ROLE_NAMES) {
215
215
  roles[role] = normalizeRoleLaunchTemplateEntry(rawRoles[role], defaults.roles[role]);
216
216
  }
217
217
  return {
@@ -225,7 +225,8 @@ function normalizeRoleLaunchTemplateEntry(input, fallback) {
225
225
  const candidate = isObject(input) ? input : {};
226
226
  return {
227
227
  permissionMode: normalizeClaudePermissionMode(candidate.permissionMode, fallback.permissionMode),
228
- model: normalizeClaudeModel(candidate.model, fallback.model)
228
+ model: normalizeClaudeModel(candidate.model, fallback.model),
229
+ effort: normalizeSessionEffort(candidate.effort, fallback.effort)
229
230
  };
230
231
  }
231
232
  function normalizeClaudePermissionMode(input, fallback) {
@@ -241,6 +242,13 @@ function normalizeClaudeModel(input, fallback) {
241
242
  const model = CLAUDE_MODEL_OPTIONS.find((option) => option.value === input);
242
243
  return model?.value ?? fallback;
243
244
  }
245
+ function normalizeSessionEffort(input, fallback) {
246
+ if (typeof input !== "string") {
247
+ return fallback;
248
+ }
249
+ const effort = SESSION_EFFORT_OPTIONS.find((option) => option.value === input);
250
+ return effort?.value ?? fallback;
251
+ }
244
252
  function normalizeTranslationConfig(input) {
245
253
  if (!input || typeof input !== "object") {
246
254
  return undefined;
@@ -41,7 +41,8 @@ export function createArtifactService(fs) {
41
41
  "project-manager": path.posix.join(logsDir, "project-manager.log"),
42
42
  architect: path.posix.join(logsDir, "architect.log"),
43
43
  coder: path.posix.join(logsDir, "coder.log"),
44
- reviewer: path.posix.join(logsDir, "reviewer.log")
44
+ reviewer: path.posix.join(logsDir, "reviewer.log"),
45
+ "codex-reviewer": path.posix.join(logsDir, "codex-reviewer.log")
45
46
  },
46
47
  messageRoutePaths: getDefaultMessageRoutePaths(messagesDir),
47
48
  architecturePlanPath: path.posix.join(handoffDir, "architecture-plan.md"),
@@ -1,9 +1,9 @@
1
- import { isRoleName } from "../../shared/constants.js";
1
+ import { isVcmRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
4
4
  export function createClaudeHookService(deps) {
5
5
  async function getHookContext(input) {
6
- if (!isRoleName(input.role)) {
6
+ if (!isVcmRoleName(input.role)) {
7
7
  throw new VcmError({
8
8
  code: "HOOK_ROLE_INVALID",
9
9
  message: `Unknown hook role: ${input.role}`,
@@ -185,7 +185,7 @@ export function createClaudeHookService(deps) {
185
185
  };
186
186
  }
187
187
  async function handlePermissionRequestHook(input) {
188
- if (!isRoleName(input.role)) {
188
+ if (!isVcmRoleName(input.role)) {
189
189
  throw new VcmError({
190
190
  code: "HOOK_ROLE_INVALID",
191
191
  message: `Unknown hook role: ${input.role}`,
@@ -0,0 +1,87 @@
1
+ import { VcmError } from "../errors.js";
2
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
3
+ const CODEX_REVIEWER_ROLE = "codex-reviewer";
4
+ export function createCodexHookService(deps) {
5
+ async function getHookContext(input) {
6
+ if (input.role !== CODEX_REVIEWER_ROLE) {
7
+ throw new VcmError({
8
+ code: "CODEX_HOOK_ROLE_INVALID",
9
+ message: `Unknown Codex hook role: ${input.role}`,
10
+ statusCode: 400
11
+ });
12
+ }
13
+ const project = await deps.projectService.getCurrentProject();
14
+ if (!project) {
15
+ throw new VcmError({
16
+ code: "PROJECT_NOT_CONNECTED",
17
+ message: "Connect a repository before accepting Codex Reviewer hooks.",
18
+ statusCode: 409
19
+ });
20
+ }
21
+ const config = await deps.projectService.loadConfig(project.repoRoot);
22
+ const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
23
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
24
+ return {
25
+ project,
26
+ config,
27
+ taskRepoRoot
28
+ };
29
+ }
30
+ async function processHook(input, expectedEventName) {
31
+ const eventName = parseHookEvent(input.event.hook_event_name);
32
+ if (eventName !== expectedEventName) {
33
+ throw new VcmError({
34
+ code: "CODEX_HOOK_EVENT_UNSUPPORTED",
35
+ message: `Unsupported Codex Reviewer hook event for this endpoint: ${eventName}`,
36
+ statusCode: 400
37
+ });
38
+ }
39
+ const context = await getHookContext(input);
40
+ const session = await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
41
+ taskSlug: input.taskSlug,
42
+ role: CODEX_REVIEWER_ROLE,
43
+ eventName,
44
+ sessionId: stringOrUndefined(input.event.session_id),
45
+ transcriptPath: stringOrUndefined(input.event.transcript_path),
46
+ cwd: stringOrUndefined(input.event.cwd),
47
+ allowSessionMismatch: true
48
+ });
49
+ await deps.roundService.recordRoleTurnEvent({
50
+ repoRoot: context.project.repoRoot,
51
+ stateRepoRoot: context.taskRepoRoot,
52
+ stateRoot: context.config.stateRoot,
53
+ taskSlug: input.taskSlug,
54
+ role: CODEX_REVIEWER_ROLE,
55
+ eventName
56
+ });
57
+ return {
58
+ ok: true,
59
+ eventName,
60
+ taskSlug: input.taskSlug,
61
+ role: CODEX_REVIEWER_ROLE,
62
+ sessionUpdated: Boolean(session)
63
+ };
64
+ }
65
+ return {
66
+ handleHook(input) {
67
+ return processHook(input, "UserPromptSubmit");
68
+ },
69
+ handleStopHook(input) {
70
+ return processHook(input, "Stop");
71
+ }
72
+ };
73
+ }
74
+ function parseHookEvent(value) {
75
+ if (value === "UserPromptSubmit" || value === "Stop") {
76
+ return value;
77
+ }
78
+ throw new VcmError({
79
+ code: "CODEX_HOOK_EVENT_UNSUPPORTED",
80
+ message: `Unsupported Codex Reviewer hook event: ${String(value)}`,
81
+ statusCode: 400,
82
+ hint: "VCM accepts Codex Reviewer UserPromptSubmit and Stop hooks only."
83
+ });
84
+ }
85
+ function stringOrUndefined(value) {
86
+ return typeof value === "string" ? value : undefined;
87
+ }