vibe-coding-master 0.3.25 → 0.3.27

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 (28) hide show
  1. package/README.md +7 -7
  2. package/dist/backend/api/codex-hook-routes.js +0 -7
  3. package/dist/backend/api/gate-review-routes.js +58 -0
  4. package/dist/backend/api/task-routes.js +2 -2
  5. package/dist/backend/cli/install-vcm-harness.js +17 -57
  6. package/dist/backend/server.js +7 -8
  7. package/dist/backend/services/app-settings-service.js +15 -15
  8. package/dist/backend/services/claude-hook-service.js +138 -28
  9. package/dist/backend/services/codex-hook-service.js +8 -50
  10. package/dist/backend/services/{codex-review-service.js → gate-review-service.js} +108 -108
  11. package/dist/backend/services/harness-service.js +17 -63
  12. package/dist/backend/services/session-service.js +275 -49
  13. package/dist/backend/services/task-service.js +2 -2
  14. package/dist/backend/templates/harness/claude-root.js +2 -2
  15. package/dist/backend/templates/harness/{codex-review.js → gate-review.js} +35 -278
  16. package/dist/backend/templates/harness/project-manager-agent.js +7 -7
  17. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +5 -5
  18. package/dist/shared/constants.js +8 -5
  19. package/dist/shared/types/{codex-review.js → gate-review.js} +1 -1
  20. package/dist-frontend/assets/{index-CKWy15WL.js → index-DVy34Iwn.js} +31 -31
  21. package/dist-frontend/index.html +1 -1
  22. package/docs/codex-translation-plan.md +9 -9
  23. package/docs/gate-review-gates.md +133 -0
  24. package/docs/product-design.md +37 -40
  25. package/package.json +1 -1
  26. package/scripts/verify-package.mjs +4 -4
  27. package/dist/backend/api/codex-review-routes.js +0 -58
  28. package/docs/codex-review-gates.md +0 -593
@@ -1,20 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { ROLE_NAMES, isCodexRoleName, isDispatchableRole } from "../../shared/constants.js";
3
+ import { VCM_ROLE_NAMES, isCodexRoleName, isDispatchableRole } from "../../shared/constants.js";
4
4
  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";
8
+ const GATE_REVIEWER_ROLE = "gate-reviewer";
9
9
  const CODEX_TRANSLATOR_ROLE = "codex-translator";
10
- const CODEX_DIR = ".ai/codex";
11
- const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
12
- const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
13
10
  const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
14
11
  const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
15
12
  const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
16
13
  const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
14
+ const GATE_REVIEWER_SESSION_PATH = ".ai/vcm/gate-reviewer/session.json";
17
15
  const PROJECT_TRANSLATOR_SCOPE = "__project__";
16
+ const PROJECT_GATE_REVIEWER_SCOPE = "__project_gate_reviewer__";
18
17
  export function createSessionService(deps) {
19
18
  const now = deps.now ?? (() => new Date().toISOString());
20
19
  async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
@@ -102,6 +101,75 @@ export function createSessionService(deps) {
102
101
  await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
103
102
  return record;
104
103
  }
104
+ async function launchProjectGateReviewerSession(repoRoot, input, launchMode) {
105
+ const live = toRoleSessionRecordView(getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
106
+ if (live && live.status === "running") {
107
+ return live;
108
+ }
109
+ const config = await deps.projectService.loadConfig(repoRoot);
110
+ const persisted = await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
111
+ const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
112
+ const model = normalizeClaudeModel(input.model ?? persisted?.model);
113
+ const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
114
+ const claudeSessionId = launchMode === "resume"
115
+ ? persisted?.claudeSessionId
116
+ : randomUUID();
117
+ if (!claudeSessionId) {
118
+ throw new VcmError({
119
+ code: "GATE_REVIEWER_SESSION_MISSING",
120
+ message: "Gate Reviewer does not have a session id to resume.",
121
+ statusCode: 409,
122
+ hint: "Start Gate Reviewer once before using Resume."
123
+ });
124
+ }
125
+ const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
126
+ ? persisted.transcriptPath
127
+ : claudeTranscriptPath(repoRoot, claudeSessionId);
128
+ const startCommand = {
129
+ ...deps.claude.buildRoleStartCommand(GATE_REVIEWER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
130
+ cwd: repoRoot
131
+ };
132
+ const runtimeSession = await deps.runtime.createSession({
133
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
134
+ role: GATE_REVIEWER_ROLE,
135
+ command: startCommand.command,
136
+ args: startCommand.args,
137
+ cwd: startCommand.cwd,
138
+ env: {
139
+ VCM_API_URL: deps.apiUrl,
140
+ VCM_TASK_REPO_ROOT: repoRoot,
141
+ VCM_TASK_SLUG: PROJECT_GATE_REVIEWER_SCOPE,
142
+ VCM_ROLE: GATE_REVIEWER_ROLE,
143
+ VCM_SESSION_ID: claudeSessionId
144
+ },
145
+ cols: input.cols,
146
+ rows: input.rows
147
+ });
148
+ const timestamp = now();
149
+ const record = {
150
+ id: runtimeSession.id,
151
+ claudeSessionId,
152
+ transcriptPath,
153
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
154
+ role: GATE_REVIEWER_ROLE,
155
+ status: runtimeSession.status,
156
+ activityStatus: "idle",
157
+ command: startCommand.display,
158
+ permissionMode,
159
+ model,
160
+ effort,
161
+ cwd: startCommand.cwd,
162
+ terminalBackend: "node-pty",
163
+ pid: runtimeSession.pid,
164
+ startedAt: runtimeSession.startedAt,
165
+ updatedAt: timestamp,
166
+ lastOutputAt: runtimeSession.lastOutputAt,
167
+ exitCode: runtimeSession.exitCode
168
+ };
169
+ deps.registry.upsert(record);
170
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, record);
171
+ return record;
172
+ }
105
173
  async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
106
174
  const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
107
175
  if (live && live.status === "running") {
@@ -163,6 +231,67 @@ export function createSessionService(deps) {
163
231
  return record;
164
232
  }
165
233
  return {
234
+ startProjectGateReviewerSession(repoRoot, input = {}) {
235
+ return launchProjectGateReviewerSession(repoRoot, input, "fresh");
236
+ },
237
+ resumeProjectGateReviewerSession(repoRoot, input = {}) {
238
+ return launchProjectGateReviewerSession(repoRoot, input, "resume");
239
+ },
240
+ async stopProjectGateReviewerSession(repoRoot) {
241
+ const existing = await this.getProjectGateReviewerSession(repoRoot);
242
+ if (!existing) {
243
+ throw new VcmError({
244
+ code: "SESSION_MISSING",
245
+ message: "Gate Reviewer session has not been started.",
246
+ statusCode: 404
247
+ });
248
+ }
249
+ if (deps.runtime.getSession(existing.id)) {
250
+ await deps.runtime.stop(existing.id);
251
+ }
252
+ const updated = {
253
+ ...existing,
254
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
255
+ status: "exited",
256
+ activityStatus: "idle",
257
+ updatedAt: now()
258
+ };
259
+ deps.registry.upsert(updated);
260
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
261
+ return updated;
262
+ },
263
+ async restartProjectGateReviewerSession(repoRoot, input = {}) {
264
+ const existing = await this.getProjectGateReviewerSession(repoRoot);
265
+ if (!existing) {
266
+ return launchProjectGateReviewerSession(repoRoot, input, "fresh");
267
+ }
268
+ if (deps.runtime.getSession(existing.id)) {
269
+ await deps.runtime.stop(existing.id);
270
+ }
271
+ deps.registry.remove(existing.id);
272
+ return launchProjectGateReviewerSession(repoRoot, input, "fresh");
273
+ },
274
+ async getProjectGateReviewerSession(repoRoot) {
275
+ const record = getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime)
276
+ ?? await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
277
+ return toRoleSessionRecordView(record, deps.runtime);
278
+ },
279
+ async ensureProjectGateReviewerSession(repoRoot, input = {}) {
280
+ const existing = await this.getProjectGateReviewerSession(repoRoot);
281
+ if (existing?.status === "running") {
282
+ return existing;
283
+ }
284
+ if (existing?.claudeSessionId) {
285
+ return this.resumeProjectGateReviewerSession(repoRoot, {
286
+ permissionMode: input.permissionMode ?? existing.permissionMode,
287
+ model: input.model ?? existing.model,
288
+ effort: input.effort ?? existing.effort,
289
+ cols: input.cols,
290
+ rows: input.rows
291
+ });
292
+ }
293
+ return this.startProjectGateReviewerSession(repoRoot, input);
294
+ },
166
295
  startProjectTranslatorSession(repoRoot, input = {}) {
167
296
  return launchProjectTranslatorSession(repoRoot, input, "fresh");
168
297
  },
@@ -228,16 +357,18 @@ export function createSessionService(deps) {
228
357
  return undefined;
229
358
  }
230
359
  const timestamp = now();
231
- const isStop = input.eventName === "Stop";
360
+ const isTurnEnd = isTurnEndHook(input.eventName);
361
+ const isCompact = isCompactHook(input.eventName);
232
362
  const updated = {
233
363
  ...current,
234
364
  claudeSessionId: input.sessionId ?? current.claudeSessionId,
235
365
  transcriptPath: input.transcriptPath ?? current.transcriptPath,
236
366
  cwd: input.cwd ?? current.cwd,
237
- activityStatus: isStop ? "idle" : "running",
367
+ activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
238
368
  lastHookEventAt: timestamp,
239
- lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
240
- lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
369
+ lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
370
+ lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
371
+ lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
241
372
  updatedAt: timestamp
242
373
  };
243
374
  deps.registry.upsert(updated);
@@ -245,6 +376,10 @@ export function createSessionService(deps) {
245
376
  return updated;
246
377
  },
247
378
  startRoleSession(repoRoot, taskSlug, role, input = {}) {
379
+ if (role === GATE_REVIEWER_ROLE) {
380
+ return this.startProjectGateReviewerSession(repoRoot, input)
381
+ .then((session) => scopeProjectRoleSession(session, taskSlug));
382
+ }
248
383
  if (role === CODEX_TRANSLATOR_ROLE) {
249
384
  void taskSlug;
250
385
  return this.startProjectTranslatorSession(repoRoot, input);
@@ -252,6 +387,10 @@ export function createSessionService(deps) {
252
387
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
253
388
  },
254
389
  resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
390
+ if (role === GATE_REVIEWER_ROLE) {
391
+ return this.resumeProjectGateReviewerSession(repoRoot, input)
392
+ .then((session) => scopeProjectRoleSession(session, taskSlug));
393
+ }
255
394
  if (role === CODEX_TRANSLATOR_ROLE) {
256
395
  void taskSlug;
257
396
  return this.resumeProjectTranslatorSession(repoRoot, input);
@@ -259,6 +398,9 @@ export function createSessionService(deps) {
259
398
  return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
260
399
  },
261
400
  async stopRoleSession(repoRoot, taskSlug, role) {
401
+ if (role === GATE_REVIEWER_ROLE) {
402
+ return scopeProjectRoleSession(await this.stopProjectGateReviewerSession(repoRoot), taskSlug);
403
+ }
262
404
  if (role === CODEX_TRANSLATOR_ROLE) {
263
405
  void taskSlug;
264
406
  return this.stopProjectTranslatorSession(repoRoot);
@@ -287,6 +429,9 @@ export function createSessionService(deps) {
287
429
  return updated;
288
430
  },
289
431
  async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
432
+ if (role === GATE_REVIEWER_ROLE) {
433
+ return scopeProjectRoleSession(await this.restartProjectGateReviewerSession(repoRoot, input), taskSlug);
434
+ }
290
435
  if (role === CODEX_TRANSLATOR_ROLE) {
291
436
  void taskSlug;
292
437
  return this.restartProjectTranslatorSession(repoRoot, input);
@@ -302,6 +447,9 @@ export function createSessionService(deps) {
302
447
  return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
303
448
  },
304
449
  async getRoleSession(repoRoot, taskSlug, role) {
450
+ if (role === GATE_REVIEWER_ROLE) {
451
+ return scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
452
+ }
305
453
  if (role === CODEX_TRANSLATOR_ROLE) {
306
454
  void taskSlug;
307
455
  return this.getProjectTranslatorSession(repoRoot);
@@ -322,7 +470,7 @@ export function createSessionService(deps) {
322
470
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
323
471
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
324
472
  const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
325
- for (const role of ROLE_NAMES.filter((candidate) => candidate !== CODEX_TRANSLATOR_ROLE)) {
473
+ for (const role of VCM_ROLE_NAMES) {
326
474
  const record = deps.registry.getByRole(taskSlug, role)
327
475
  ?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
328
476
  const session = toRoleSessionRecordView(record, deps.runtime);
@@ -330,9 +478,38 @@ export function createSessionService(deps) {
330
478
  sessions.push(session);
331
479
  }
332
480
  }
481
+ const gateReviewerSession = scopeProjectRoleSession(await this.getProjectGateReviewerSession(repoRoot), taskSlug);
482
+ if (gateReviewerSession) {
483
+ sessions.push(gateReviewerSession);
484
+ }
333
485
  return sessions;
334
486
  },
335
487
  async recordRoleHookEvent(repoRoot, input) {
488
+ if (input.role === GATE_REVIEWER_ROLE) {
489
+ const current = await this.getProjectGateReviewerSession(repoRoot);
490
+ if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
491
+ return undefined;
492
+ }
493
+ const timestamp = now();
494
+ const isTurnEnd = isTurnEndHook(input.eventName);
495
+ const isCompact = isCompactHook(input.eventName);
496
+ const updated = {
497
+ ...current,
498
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
499
+ claudeSessionId: input.sessionId ?? current.claudeSessionId,
500
+ transcriptPath: input.transcriptPath ?? current.transcriptPath,
501
+ cwd: input.cwd ?? current.cwd,
502
+ activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
503
+ lastHookEventAt: timestamp,
504
+ lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
505
+ lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
506
+ lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
507
+ updatedAt: timestamp
508
+ };
509
+ deps.registry.upsert(updated);
510
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
511
+ return scopeProjectRoleSession(updated, input.taskSlug);
512
+ }
336
513
  if (input.role === CODEX_TRANSLATOR_ROLE) {
337
514
  void input.taskSlug;
338
515
  return this.recordProjectTranslatorHookEvent(repoRoot, {
@@ -347,16 +524,18 @@ export function createSessionService(deps) {
347
524
  return undefined;
348
525
  }
349
526
  const timestamp = now();
350
- const isStop = input.eventName === "Stop";
527
+ const isTurnEnd = isTurnEndHook(input.eventName);
528
+ const isCompact = isCompactHook(input.eventName);
351
529
  const updated = {
352
530
  ...current,
353
531
  claudeSessionId: input.sessionId ?? current.claudeSessionId,
354
532
  transcriptPath: input.transcriptPath ?? current.transcriptPath,
355
533
  cwd: input.cwd ?? current.cwd,
356
- activityStatus: isStop ? "idle" : "running",
534
+ activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
357
535
  lastHookEventAt: timestamp,
358
- lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
359
- lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
536
+ lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
537
+ lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
538
+ lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
360
539
  updatedAt: timestamp
361
540
  };
362
541
  deps.registry.upsert(updated);
@@ -388,6 +567,15 @@ export function createSessionService(deps) {
388
567
  lastHookEventAt: timestamp,
389
568
  updatedAt: timestamp
390
569
  };
570
+ if (role === GATE_REVIEWER_ROLE) {
571
+ const persisted = {
572
+ ...updated,
573
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
574
+ };
575
+ deps.registry.upsert(persisted);
576
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, persisted);
577
+ return updated;
578
+ }
391
579
  deps.registry.upsert(updated);
392
580
  const config = await deps.projectService.loadConfig(repoRoot);
393
581
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
@@ -406,6 +594,15 @@ export function createSessionService(deps) {
406
594
  lastTurnEndedAt: timestamp,
407
595
  updatedAt: timestamp
408
596
  };
597
+ if (role === GATE_REVIEWER_ROLE) {
598
+ const persisted = {
599
+ ...updated,
600
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
601
+ };
602
+ deps.registry.upsert(persisted);
603
+ await persistProjectGateReviewerSession(deps.fs, repoRoot, persisted);
604
+ return updated;
605
+ }
409
606
  deps.registry.upsert(updated);
410
607
  const config = await deps.projectService.loadConfig(repoRoot);
411
608
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
@@ -436,20 +633,27 @@ function toRoleSessionRecordView(record, runtime) {
436
633
  exitCode: runtimeSession.exitCode
437
634
  };
438
635
  }
439
- async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
636
+ async function buildCodexStartCommand(fs, baseRepoRoot, _taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
440
637
  const isTranslator = role === CODEX_TRANSLATOR_ROLE;
441
- const codexDir = resolveRepoPath(isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATOR_DIR : CODEX_DIR);
442
- const outputDir = resolveRepoPath(isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATION_DIR : CODEX_REVIEW_DIR);
638
+ if (!isTranslator) {
639
+ throw new VcmError({
640
+ code: "CODEX_ROLE_UNSUPPORTED",
641
+ message: `${role} is not a Codex role.`,
642
+ statusCode: 400
643
+ });
644
+ }
645
+ const codexDir = resolveRepoPath(baseRepoRoot, CODEX_TRANSLATOR_DIR);
646
+ const outputDir = resolveRepoPath(baseRepoRoot, CODEX_TRANSLATION_DIR);
443
647
  if (!(await fs.pathExists(codexDir))) {
444
648
  throw new VcmError({
445
- code: isTranslator ? "CODEX_TRANSLATOR_CONFIG_MISSING" : "CODEX_REVIEW_CONFIG_MISSING",
446
- message: `${isTranslator ? CODEX_TRANSLATOR_DIR : CODEX_DIR} does not exist.`,
649
+ code: "CODEX_TRANSLATOR_CONFIG_MISSING",
650
+ message: `${CODEX_TRANSLATOR_DIR} does not exist.`,
447
651
  statusCode: 409,
448
- hint: `Apply the VCM harness before starting ${isTranslator ? "Codex Translator" : "Codex Reviewer"}.`
652
+ hint: "Apply the VCM harness before starting Codex Translator."
449
653
  });
450
654
  }
451
655
  await fs.ensureDir(outputDir);
452
- const config = await loadCodexSessionConfig(fs, isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATOR_CONFIG_PATH : CODEX_CONFIG_PATH);
656
+ const config = await loadCodexSessionConfig(fs, baseRepoRoot, CODEX_TRANSLATOR_CONFIG_PATH);
453
657
  const args = launchMode === "resume"
454
658
  ? resumeSessionId ? ["resume", resumeSessionId] : ["resume", "--last"]
455
659
  : [];
@@ -458,12 +662,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
458
662
  args.push("--dangerously-bypass-approvals-and-sandbox");
459
663
  }
460
664
  else {
461
- if (isTranslator) {
462
- args.push("--add-dir", baseRepoRoot);
463
- }
464
- else {
465
- args.push("--add-dir", outputDir);
466
- }
665
+ args.push("--add-dir", baseRepoRoot);
467
666
  args.push("--sandbox", "workspace-write", "--ask-for-approval", "never");
468
667
  }
469
668
  args.push("--dangerously-bypass-hook-trust", "--search");
@@ -476,7 +675,7 @@ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, laun
476
675
  return {
477
676
  command: config.command,
478
677
  args,
479
- cwd: isTranslator ? baseRepoRoot : taskRepoRoot,
678
+ cwd: baseRepoRoot,
480
679
  display: [config.command, ...args].map(formatDisplayArg).join(" ")
481
680
  };
482
681
  }
@@ -504,9 +703,8 @@ async function loadCodexSessionConfig(fs, repoRoot, configRelativePath) {
504
703
  };
505
704
  }
506
705
  const content = await fs.readText(configPath);
507
- const reviewSection = extractTomlSection(content, "vcm.codex_review");
508
706
  return {
509
- command: parseTomlString(content, "command") ?? parseTomlString(reviewSection, "command") ?? "codex"
707
+ command: parseTomlString(content, "command") ?? "codex"
510
708
  };
511
709
  }
512
710
  function matchesRoleHookSession(record, input) {
@@ -521,6 +719,12 @@ function matchesRoleHookSession(record, input) {
521
719
  }
522
720
  return false;
523
721
  }
722
+ function isTurnEndHook(eventName) {
723
+ return eventName === "Stop" || eventName === "StopFailure";
724
+ }
725
+ function isCompactHook(eventName) {
726
+ return eventName === "PostCompact";
727
+ }
524
728
  function getRecoverableStatus(record) {
525
729
  if (!record.claudeSessionId) {
526
730
  return record.status === "running" ? "missing" : record.status;
@@ -540,7 +744,7 @@ function getHandoffArtifactPath(paths, role) {
540
744
  return undefined;
541
745
  }
542
746
  function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
543
- if (role !== CODEX_TRANSLATOR_ROLE) {
747
+ if (role !== CODEX_TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE) {
544
748
  return registry.getByRole(taskSlug, role);
545
749
  }
546
750
  const candidates = registry.list().filter((session) => session.role === role);
@@ -550,6 +754,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
550
754
  ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
551
755
  return scopeProjectRoleSession(scoped, taskSlug);
552
756
  }
757
+ function getRegisteredProjectGateReviewerSession(registry, runtime) {
758
+ const candidates = registry.list().filter((session) => session.role === GATE_REVIEWER_ROLE);
759
+ const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
760
+ return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
761
+ }
553
762
  function getRegisteredProjectTranslatorSession(registry, runtime) {
554
763
  const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
555
764
  const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
@@ -559,7 +768,7 @@ function compareSessionUpdatedAtDesc(left, right) {
559
768
  return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
560
769
  }
561
770
  function scopeProjectRoleSession(record, taskSlug) {
562
- if (!record || record.role !== CODEX_TRANSLATOR_ROLE) {
771
+ if (!record || (record.role !== CODEX_TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE)) {
563
772
  return record;
564
773
  }
565
774
  return {
@@ -568,12 +777,31 @@ function scopeProjectRoleSession(record, taskSlug) {
568
777
  };
569
778
  }
570
779
  async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
780
+ if (role === GATE_REVIEWER_ROLE) {
781
+ void taskSlug;
782
+ return loadPersistedProjectGateReviewerSession(fs, baseRepoRoot);
783
+ }
571
784
  if (role === CODEX_TRANSLATOR_ROLE) {
572
785
  void taskSlug;
573
786
  return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
574
787
  }
575
788
  return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
576
789
  }
790
+ async function loadPersistedProjectGateReviewerSession(fs, repoRoot) {
791
+ const sessionPath = resolveRepoPath(repoRoot, GATE_REVIEWER_SESSION_PATH);
792
+ if (!(await fs.pathExists(sessionPath))) {
793
+ return undefined;
794
+ }
795
+ const payload = await fs.readJson(sessionPath);
796
+ const record = normalizePersistedRoleRecord(isProjectRoleSessionFile(payload) ? payload.record : payload);
797
+ if (!record) {
798
+ return undefined;
799
+ }
800
+ return {
801
+ ...record,
802
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
803
+ };
804
+ }
577
805
  async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
578
806
  const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
579
807
  if (!(await fs.pathExists(sessionPath))) {
@@ -647,12 +875,27 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
647
875
  });
648
876
  }
649
877
  async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoot, session) {
878
+ if (session.role === GATE_REVIEWER_ROLE) {
879
+ await persistProjectGateReviewerSession(fs, baseRepoRoot, session);
880
+ return;
881
+ }
650
882
  if (session.role === CODEX_TRANSLATOR_ROLE) {
651
883
  await persistCodexTranslatorSession(fs, baseRepoRoot, session);
652
884
  return;
653
885
  }
654
886
  await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
655
887
  }
888
+ async function persistProjectGateReviewerSession(fs, repoRoot, session) {
889
+ await fs.writeJsonAtomic(resolveRepoPath(repoRoot, GATE_REVIEWER_SESSION_PATH), {
890
+ version: 1,
891
+ role: session.role,
892
+ updatedAt: session.updatedAt,
893
+ record: {
894
+ ...session,
895
+ taskSlug: PROJECT_GATE_REVIEWER_SCOPE
896
+ }
897
+ });
898
+ }
656
899
  async function persistCodexTranslatorSession(fs, repoRoot, session) {
657
900
  await fs.writeJsonAtomic(resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH), {
658
901
  version: 1,
@@ -726,23 +969,6 @@ function normalizeCodexEffort(value) {
726
969
  }
727
970
  return "default";
728
971
  }
729
- function extractTomlSection(content, sectionName) {
730
- const lines = content.split(/\r?\n/);
731
- const header = `[${sectionName}]`;
732
- const start = lines.findIndex((line) => line.trim() === header);
733
- if (start < 0) {
734
- return "";
735
- }
736
- const section = [];
737
- for (let index = start + 1; index < lines.length; index += 1) {
738
- const line = lines[index];
739
- if (/^\s*\[[^\]]+\]\s*$/.test(line)) {
740
- break;
741
- }
742
- section.push(line);
743
- }
744
- return section.join("\n");
745
- }
746
972
  function parseTomlString(content, key) {
747
973
  const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "m");
748
974
  return pattern.exec(content)?.[1];
@@ -179,7 +179,7 @@ async function ensureTaskRuntimeStateDirs(fs, taskRepoRoot, stateRoot) {
179
179
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "messages"));
180
180
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "orchestration"));
181
181
  await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "translation"));
182
- await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "codex-reviews"));
182
+ await fs.ensureDir(path.join(taskRepoRoot, stateRoot, "gate-reviews"));
183
183
  }
184
184
  function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot, taskSlug) {
185
185
  return [
@@ -188,7 +188,7 @@ function getTaskStatePaths(taskStoreRoot, taskRepoRoot, stateRoot, handoffRoot,
188
188
  path.join(taskRepoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
189
189
  path.join(taskRepoRoot, stateRoot, "orchestration", `${taskSlug}.json`),
190
190
  path.join(taskRepoRoot, stateRoot, "translation", taskSlug),
191
- path.join(taskRepoRoot, stateRoot, "codex-reviews"),
191
+ path.join(taskRepoRoot, stateRoot, "gate-reviews"),
192
192
  path.join(taskRepoRoot, handoffRoot)
193
193
  ];
194
194
  }
@@ -5,7 +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
+ - Project-manager uses \`vcm-gate-review\` at enabled Gate Review trigger points and on VCM Gate Review callbacks.
9
9
 
10
10
  ## VCM Background Jobs
11
11
 
@@ -32,7 +32,7 @@ export function renderRootClaudeHarnessRules() {
32
32
  - Test-only or validation-only work may use: \`project-manager -> reviewer -> project-manager final acceptance\`.
33
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.
34
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.
35
+ - Gate Review Gate reports live under \`.ai/vcm/gate-reviews/\` and are VCM-managed task evidence.
36
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.
37
37
  - Record current-task unresolved findings in \`.ai/vcm/handoffs/known-issues.md\`.
38
38