vibe-coding-master 0.3.10 → 0.3.12

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.
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
6
6
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
7
- import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
7
+ import { renderCodexAgentsHarnessRules, renderCodexArchitecturePlanPrompt, renderCodexCliConfigHarnessRules, renderCodexConfigHarnessRules, renderCodexFinalDiffPrompt, renderCodexHooksHarnessRules, renderCodexReviewResultSchema, renderCodexTranslatorAgentsHarnessRules, renderCodexTranslatorConfigHarnessRules, renderCodexValidationAdequacyPrompt, renderRequestCodexReviewTool, renderVcmCodexReviewGateSkillRules } from "../templates/harness/codex-review.js";
8
8
  import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
9
9
  import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
10
10
  import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
@@ -124,6 +124,33 @@ const HARNESS_FILES = [
124
124
  ownership: "raw-file",
125
125
  renderRules: renderCodexHooksHarnessRules
126
126
  },
127
+ {
128
+ kind: "codex-translator-agents",
129
+ path: ".ai/codex-translator/AGENTS.md",
130
+ title: "VCM Codex Translator",
131
+ renderRules: renderCodexTranslatorAgentsHarnessRules
132
+ },
133
+ {
134
+ kind: "codex-translator-config",
135
+ path: ".ai/codex-translator/config.toml",
136
+ title: "VCM Codex Translator Config",
137
+ ownership: "raw-file",
138
+ renderRules: renderCodexTranslatorConfigHarnessRules
139
+ },
140
+ {
141
+ kind: "codex-translator-cli-config",
142
+ path: ".ai/codex-translator/.codex/config.toml",
143
+ title: "VCM Codex Translator CLI Config",
144
+ ownership: "raw-file",
145
+ renderRules: renderCodexCliConfigHarnessRules
146
+ },
147
+ {
148
+ kind: "codex-translator-hooks",
149
+ path: ".ai/codex-translator/.codex/hooks.json",
150
+ title: "VCM Codex Translator Hooks",
151
+ ownership: "raw-file",
152
+ renderRules: () => renderCodexHooksHarnessRules("codex-translator")
153
+ },
127
154
  {
128
155
  kind: "codex-prompt-architecture-plan",
129
156
  path: ".ai/codex/prompts/architecture-plan-gate.md",
@@ -1,18 +1,24 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
3
+ import { 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
8
  const CODEX_REVIEWER_ROLE = "codex-reviewer";
9
+ const CODEX_TRANSLATOR_ROLE = "codex-translator";
9
10
  const CODEX_DIR = ".ai/codex";
10
11
  const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
11
12
  const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
13
+ const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
14
+ const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
15
+ const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
16
+ const CODEX_TRANSLATOR_LOG_PATH = ".ai/vcm/translations/codex-translator.log";
17
+ const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
12
18
  export function createSessionService(deps) {
13
19
  const now = deps.now ?? (() => new Date().toISOString());
14
20
  async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
15
- const live = deps.registry.getByRole(taskSlug, role);
21
+ const live = toRoleSessionRecordView(getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role), deps.runtime);
16
22
  if (live && live.status === "running") {
17
23
  return live;
18
24
  }
@@ -20,13 +26,16 @@ export function createSessionService(deps) {
20
26
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
21
27
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
22
28
  const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
23
- const persisted = await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
24
- const isCodexReviewer = role === CODEX_REVIEWER_ROLE;
29
+ const persisted = await loadPersistedRoleRecordForRole(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, taskSlug, role);
30
+ const isCodexRole = isCodexRoleName(role);
31
+ const isTranslator = role === CODEX_TRANSLATOR_ROLE;
32
+ const sessionRepoRoot = isTranslator ? repoRoot : taskRepoRoot;
33
+ const sessionLogPath = isTranslator ? CODEX_TRANSLATOR_LOG_PATH : paths.roleLogPaths[role];
25
34
  const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
26
- const model = isCodexReviewer
35
+ const model = isCodexRole
27
36
  ? normalizeCodexModel(input.model ?? persisted?.model)
28
37
  : normalizeClaudeModel(input.model ?? persisted?.model);
29
- const effort = isCodexReviewer
38
+ const effort = isCodexRole
30
39
  ? normalizeCodexEffort(input.effort ?? persisted?.effort)
31
40
  : normalizeClaudeEffort(input.effort ?? persisted?.effort);
32
41
  const claudeSessionId = launchMode === "resume"
@@ -42,9 +51,9 @@ export function createSessionService(deps) {
42
51
  }
43
52
  const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
44
53
  ? persisted.transcriptPath
45
- : isCodexReviewer ? undefined : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
46
- const startCommand = isCodexReviewer
47
- ? await buildCodexReviewerStartCommand(deps.fs, taskRepoRoot, launchMode, model, effort)
54
+ : isCodexRole ? undefined : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
55
+ const startCommand = isCodexRole
56
+ ? await buildCodexStartCommand(deps.fs, repoRoot, taskRepoRoot, role, launchMode, model, effort, deps.sandboxMode, launchMode === "resume" && hasCapturedCodexSession(persisted) ? claudeSessionId : undefined)
48
57
  : {
49
58
  ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
50
59
  cwd: taskRepoRoot
@@ -57,14 +66,14 @@ export function createSessionService(deps) {
57
66
  cwd: startCommand.cwd,
58
67
  env: {
59
68
  VCM_API_URL: deps.apiUrl,
60
- VCM_TASK_REPO_ROOT: taskRepoRoot,
69
+ VCM_TASK_REPO_ROOT: sessionRepoRoot,
61
70
  VCM_TASK_SLUG: taskSlug,
62
71
  VCM_ROLE: role,
63
72
  VCM_SESSION_ID: claudeSessionId
64
73
  },
65
74
  cols: input.cols,
66
75
  rows: input.rows,
67
- logPath: resolveRepoPath(taskRepoRoot, paths.roleLogPaths[role])
76
+ logPath: resolveRepoPath(sessionRepoRoot, sessionLogPath)
68
77
  });
69
78
  const timestamp = now();
70
79
  const record = {
@@ -82,7 +91,7 @@ export function createSessionService(deps) {
82
91
  cwd: startCommand.cwd,
83
92
  terminalBackend: "node-pty",
84
93
  pid: runtimeSession.pid,
85
- logPath: paths.roleLogPaths[role],
94
+ logPath: sessionLogPath,
86
95
  roleCommandPath: isDispatchableRole(role)
87
96
  ? paths.roleCommandPaths[role]
88
97
  : undefined,
@@ -93,7 +102,7 @@ export function createSessionService(deps) {
93
102
  exitCode: runtimeSession.exitCode
94
103
  };
95
104
  deps.registry.upsert(record);
96
- await persistTaskSession(deps.fs, taskRepoRoot, config.stateRoot, record);
105
+ await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
97
106
  return record;
98
107
  }
99
108
  return {
@@ -124,7 +133,7 @@ export function createSessionService(deps) {
124
133
  deps.registry.upsert(updated);
125
134
  const config = await deps.projectService.loadConfig(repoRoot);
126
135
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
127
- await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
136
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
128
137
  return updated;
129
138
  },
130
139
  async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
@@ -142,8 +151,8 @@ export function createSessionService(deps) {
142
151
  const config = await deps.projectService.loadConfig(repoRoot);
143
152
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
144
153
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
145
- const record = deps.registry.getByRole(taskSlug, role)
146
- ?? await loadPersistedRoleRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug, role);
154
+ const record = getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role)
155
+ ?? await loadPersistedRoleRecordForRole(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, taskSlug, role);
147
156
  if (!record) {
148
157
  return undefined;
149
158
  }
@@ -156,8 +165,12 @@ export function createSessionService(deps) {
156
165
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
157
166
  const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
158
167
  for (const role of ROLE_NAMES) {
159
- const session = toRoleSessionRecordView(deps.registry.getByRole(taskSlug, role)
160
- ?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record), deps.runtime);
168
+ const record = role === CODEX_TRANSLATOR_ROLE
169
+ ? getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role)
170
+ ?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot, taskSlug)
171
+ : deps.registry.getByRole(taskSlug, role)
172
+ ?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
173
+ const session = toRoleSessionRecordView(record, deps.runtime);
161
174
  if (session) {
162
175
  sessions.push(session);
163
176
  }
@@ -185,7 +198,7 @@ export function createSessionService(deps) {
185
198
  deps.registry.upsert(updated);
186
199
  const config = await deps.projectService.loadConfig(repoRoot);
187
200
  const task = await deps.taskService.loadTask(repoRoot, input.taskSlug);
188
- await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
201
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
189
202
  return updated;
190
203
  },
191
204
  recordClaudeHookEvent(repoRoot, input) {
@@ -214,7 +227,7 @@ export function createSessionService(deps) {
214
227
  deps.registry.upsert(updated);
215
228
  const config = await deps.projectService.loadConfig(repoRoot);
216
229
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
217
- await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
230
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
218
231
  return updated;
219
232
  },
220
233
  async markRoleActivityIdle(repoRoot, taskSlug, role) {
@@ -232,7 +245,7 @@ export function createSessionService(deps) {
232
245
  deps.registry.upsert(updated);
233
246
  const config = await deps.projectService.loadConfig(repoRoot);
234
247
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
235
- await persistTaskSession(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
248
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
236
249
  return updated;
237
250
  }
238
251
  };
@@ -259,23 +272,37 @@ function toRoleSessionRecordView(record, runtime) {
259
272
  exitCode: runtimeSession.exitCode
260
273
  };
261
274
  }
262
- async function buildCodexReviewerStartCommand(fs, taskRepoRoot, launchMode, selectedModel, selectedEffort) {
263
- const codexDir = resolveRepoPath(taskRepoRoot, CODEX_DIR);
264
- const reviewDir = resolveRepoPath(taskRepoRoot, CODEX_REVIEW_DIR);
275
+ async function buildCodexStartCommand(fs, baseRepoRoot, taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
276
+ const isTranslator = role === CODEX_TRANSLATOR_ROLE;
277
+ const codexDir = resolveRepoPath(isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATOR_DIR : CODEX_DIR);
278
+ const outputDir = resolveRepoPath(isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATION_DIR : CODEX_REVIEW_DIR);
265
279
  if (!(await fs.pathExists(codexDir))) {
266
280
  throw new VcmError({
267
- code: "CODEX_REVIEW_CONFIG_MISSING",
268
- message: `${CODEX_DIR} does not exist.`,
281
+ code: isTranslator ? "CODEX_TRANSLATOR_CONFIG_MISSING" : "CODEX_REVIEW_CONFIG_MISSING",
282
+ message: `${isTranslator ? CODEX_TRANSLATOR_DIR : CODEX_DIR} does not exist.`,
269
283
  statusCode: 409,
270
- hint: "Apply the VCM harness before starting Codex Reviewer."
284
+ hint: `Apply the VCM harness before starting ${isTranslator ? "Codex Translator" : "Codex Reviewer"}.`
271
285
  });
272
286
  }
273
- await fs.ensureDir(reviewDir);
274
- const config = await loadCodexSessionConfig(fs, taskRepoRoot);
287
+ await fs.ensureDir(outputDir);
288
+ const config = await loadCodexSessionConfig(fs, isTranslator ? baseRepoRoot : taskRepoRoot, isTranslator ? CODEX_TRANSLATOR_CONFIG_PATH : CODEX_CONFIG_PATH);
275
289
  const args = launchMode === "resume"
276
- ? ["resume", "--last"]
290
+ ? resumeSessionId ? ["resume", resumeSessionId] : ["resume", "--last"]
277
291
  : [];
278
- args.push("--cd", codexDir, "--add-dir", reviewDir, "--sandbox", "workspace-write", "--ask-for-approval", "never", "--dangerously-bypass-hook-trust", "--search");
292
+ args.push("--cd", codexDir);
293
+ if (isDevContainerSandbox(sandboxMode)) {
294
+ args.push("--dangerously-bypass-approvals-and-sandbox");
295
+ }
296
+ else {
297
+ if (isTranslator) {
298
+ args.push("--add-dir", baseRepoRoot);
299
+ }
300
+ else {
301
+ args.push("--add-dir", outputDir);
302
+ }
303
+ args.push("--sandbox", "workspace-write", "--ask-for-approval", "never");
304
+ }
305
+ args.push("--dangerously-bypass-hook-trust", "--search");
279
306
  if (selectedModel !== "default") {
280
307
  args.push("--model", selectedModel);
281
308
  }
@@ -285,12 +312,18 @@ async function buildCodexReviewerStartCommand(fs, taskRepoRoot, launchMode, sele
285
312
  return {
286
313
  command: config.command,
287
314
  args,
288
- cwd: taskRepoRoot,
315
+ cwd: isTranslator ? baseRepoRoot : taskRepoRoot,
289
316
  display: [config.command, ...args].map(formatDisplayArg).join(" ")
290
317
  };
291
318
  }
292
- async function loadCodexSessionConfig(fs, taskRepoRoot) {
293
- const configPath = resolveRepoPath(taskRepoRoot, CODEX_CONFIG_PATH);
319
+ function hasCapturedCodexSession(record) {
320
+ return Boolean(record?.lastHookEventAt || record?.transcriptPath);
321
+ }
322
+ function isDevContainerSandbox(value) {
323
+ return value?.toLowerCase() === "devcontainer";
324
+ }
325
+ async function loadCodexSessionConfig(fs, repoRoot, configRelativePath) {
326
+ const configPath = resolveRepoPath(repoRoot, configRelativePath);
294
327
  if (!(await fs.pathExists(configPath))) {
295
328
  return {
296
329
  command: "codex"
@@ -332,6 +365,44 @@ function getHandoffArtifactPath(paths, role) {
332
365
  }
333
366
  return undefined;
334
367
  }
368
+ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
369
+ if (role !== CODEX_TRANSLATOR_ROLE) {
370
+ return registry.getByRole(taskSlug, role);
371
+ }
372
+ const candidates = registry.list().filter((session) => session.role === role);
373
+ const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
374
+ const scoped = live
375
+ ?? candidates.find((session) => session.taskSlug === taskSlug)
376
+ ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
377
+ return scopeProjectRoleSession(scoped, taskSlug);
378
+ }
379
+ function compareSessionUpdatedAtDesc(left, right) {
380
+ return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
381
+ }
382
+ function scopeProjectRoleSession(record, taskSlug) {
383
+ if (!record || record.role !== CODEX_TRANSLATOR_ROLE) {
384
+ return record;
385
+ }
386
+ return {
387
+ ...record,
388
+ taskSlug
389
+ };
390
+ }
391
+ async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
392
+ if (role === CODEX_TRANSLATOR_ROLE) {
393
+ return loadPersistedCodexTranslatorSession(fs, baseRepoRoot, taskSlug);
394
+ }
395
+ return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
396
+ }
397
+ async function loadPersistedCodexTranslatorSession(fs, repoRoot, taskSlug) {
398
+ const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
399
+ if (!(await fs.pathExists(sessionPath))) {
400
+ return undefined;
401
+ }
402
+ const payload = await fs.readJson(sessionPath);
403
+ const record = normalizePersistedRoleRecord(isProjectRoleSessionFile(payload) ? payload.record : payload);
404
+ return scopeProjectRoleSession(record, taskSlug);
405
+ }
335
406
  async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
336
407
  const current = await loadPersistedTaskSessionRecord(fs, repoRoot, stateRoot, taskSlug);
337
408
  return normalizePersistedRoleRecord(current?.roles[role]?.record);
@@ -355,10 +426,10 @@ function normalizePersistedRoleRecord(record) {
355
426
  ? legacy.lastStopAt
356
427
  : undefined),
357
428
  permissionMode: normalizeClaudePermissionMode(record.permissionMode),
358
- model: record.role === CODEX_REVIEWER_ROLE
429
+ model: isCodexRoleName(record.role)
359
430
  ? normalizeCodexModel(record.model)
360
431
  : normalizeClaudeModel(record.model),
361
- effort: record.role === CODEX_REVIEWER_ROLE
432
+ effort: isCodexRoleName(record.role)
362
433
  ? normalizeCodexEffort(record.effort)
363
434
  : normalizeClaudeEffort(record.effort)
364
435
  }
@@ -389,6 +460,27 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
389
460
  }
390
461
  });
391
462
  }
463
+ async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoot, session) {
464
+ if (session.role === CODEX_TRANSLATOR_ROLE) {
465
+ await persistCodexTranslatorSession(fs, baseRepoRoot, session);
466
+ return;
467
+ }
468
+ await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
469
+ }
470
+ async function persistCodexTranslatorSession(fs, repoRoot, session) {
471
+ await fs.writeJsonAtomic(resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH), {
472
+ version: 1,
473
+ role: session.role,
474
+ updatedAt: session.updatedAt,
475
+ record: {
476
+ ...session,
477
+ taskSlug: session.taskSlug
478
+ }
479
+ });
480
+ }
481
+ function isProjectRoleSessionFile(value) {
482
+ return "record" in value && typeof value.record === "object" && value.record !== null;
483
+ }
392
484
  function createEmptyTaskSessionRecord(taskSlug, updatedAt) {
393
485
  return {
394
486
  version: 1,
@@ -112,6 +112,7 @@ export function createTranslationService(deps) {
112
112
  async function prepareCache(input) {
113
113
  const state = getState(input.sessionId);
114
114
  state.repoRoot = input.repoRoot;
115
+ state.baseRepoRoot = input.baseRepoRoot ?? input.repoRoot;
115
116
  state.taskSlug = input.taskSlug;
116
117
  state.role = input.role;
117
118
  if (!deps.fs || !deps.projectService) {
@@ -266,24 +267,24 @@ export function createTranslationService(deps) {
266
267
  void queue.enqueue(async () => {
267
268
  publishStatus(sessionId, "translating");
268
269
  try {
269
- const prompt = buildTranslationPrompt({
270
+ const translation = await translateText({
271
+ repoRoot: getState(sessionId).baseRepoRoot,
272
+ taskSlug: baseEntry.taskSlug,
273
+ role: baseEntry.role,
270
274
  direction: "cc-output-to-user",
271
275
  text,
272
276
  sourceKind: "prose",
273
- settings
274
- });
275
- const result = await deps.provider.translate({
277
+ sourceLanguage: "en",
278
+ targetLanguage: settings.targetLanguage,
276
279
  settings,
277
- secrets,
278
- systemPrompt: prompt.systemPrompt,
279
- userPrompt: prompt.userPrompt
280
+ secrets
280
281
  });
281
282
  const completed = {
282
283
  ...baseEntry,
283
284
  status: "translated",
284
- translatedText: result.text,
285
+ translatedText: translation.text,
285
286
  completedAt: now(),
286
- tokenUsage: result.tokenUsage
287
+ tokenUsage: translation.tokenUsage
287
288
  };
288
289
  replaceEntry(sessionId, completed);
289
290
  clearFailure(sessionId, completed.id);
@@ -510,6 +511,7 @@ export function createTranslationService(deps) {
510
511
  }
511
512
  const state = await prepareCache({
512
513
  repoRoot: input.taskRepoRoot ?? input.repoRoot,
514
+ baseRepoRoot: input.repoRoot,
513
515
  taskSlug: input.taskSlug,
514
516
  role: input.role,
515
517
  sessionId: roleSession.id
@@ -541,6 +543,7 @@ export function createTranslationService(deps) {
541
543
  const { settings } = await loadConfig();
542
544
  const state = await prepareCache({
543
545
  repoRoot: input.taskRepoRoot ?? input.repoRoot,
546
+ baseRepoRoot: input.repoRoot,
544
547
  taskSlug: input.taskSlug,
545
548
  role: input.role,
546
549
  sessionId: input.sessionId
@@ -588,6 +591,7 @@ export function createTranslationService(deps) {
588
591
  if (roleSession) {
589
592
  await prepareCache({
590
593
  repoRoot: input.taskRepoRoot ?? input.repoRoot,
594
+ baseRepoRoot: input.repoRoot,
591
595
  taskSlug: input.taskSlug,
592
596
  role: input.role,
593
597
  sessionId: roleSession.id
@@ -597,12 +601,6 @@ export function createTranslationService(deps) {
597
601
  const contextText = settings.contextEnabled && input.useContext !== false
598
602
  ? sessionState?.lastAssistantText
599
603
  : undefined;
600
- const prompt = buildTranslationPrompt({
601
- direction: "user-input-to-english",
602
- text: input.text,
603
- contextText,
604
- settings
605
- });
606
604
  const entry = {
607
605
  ...createEntry({
608
606
  taskSlug: input.taskSlug,
@@ -620,34 +618,40 @@ export function createTranslationService(deps) {
620
618
  pushEntry(roleSession.id, entry);
621
619
  }
622
620
  try {
623
- const result = await deps.provider.translate({
621
+ const translation = await translateText({
622
+ repoRoot: input.repoRoot,
623
+ taskSlug: input.taskSlug,
624
+ role: input.role,
625
+ direction: "user-input-to-english",
626
+ text: input.text,
627
+ sourceKind: "prose",
628
+ sourceLanguage: settings.sourceLanguage,
629
+ targetLanguage: "en",
630
+ contextText,
624
631
  settings,
625
- secrets,
626
- systemPrompt: prompt.systemPrompt,
627
- userPrompt: prompt.userPrompt
632
+ secrets
628
633
  });
629
- const parsed = prompt.parseWarning ? parseTranslationWarning(result.text) : { text: result.text };
630
634
  const completed = {
631
635
  ...entry,
632
636
  status: "translated",
633
- translatedText: parsed.text,
634
- warning: parsed.warning,
637
+ translatedText: translation.text,
638
+ warning: translation.warning,
635
639
  completedAt: now(),
636
- tokenUsage: result.tokenUsage
640
+ tokenUsage: translation.tokenUsage
637
641
  };
638
642
  if (roleSession) {
639
643
  replaceEntry(roleSession.id, completed);
640
644
  }
641
645
  const mode = input.mode ?? settings.inputMode;
642
- const shouldSend = input.send === true && mode === "auto-send" && !parsed.warning;
646
+ const shouldSend = input.send === true && mode === "auto-send" && !translation.warning;
643
647
  if (shouldSend) {
644
- await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role, parsed.text);
648
+ await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role, translation.text);
645
649
  }
646
650
  return {
647
651
  translation: completed,
648
- englishPreview: parsed.text,
652
+ englishPreview: translation.text,
649
653
  contextUsed: Boolean(contextText),
650
- requiresReview: mode === "review-before-send" || Boolean(parsed.warning),
654
+ requiresReview: mode === "review-before-send" || Boolean(translation.warning),
651
655
  sent: shouldSend
652
656
  };
653
657
  }
@@ -787,18 +791,19 @@ export function createTranslationService(deps) {
787
791
  },
788
792
  async translateGatewayOutput(input) {
789
793
  const { settings, secrets } = await loadConfig();
790
- const prompt = buildTranslationPrompt({
794
+ const translation = await translateText({
795
+ repoRoot: input.repoRoot,
796
+ taskSlug: input.taskSlug,
797
+ role: input.role,
791
798
  direction: "cc-output-to-user",
792
799
  text: input.text,
793
- settings
794
- });
795
- const result = await deps.provider.translate({
800
+ sourceKind: "prose",
801
+ sourceLanguage: "en",
802
+ targetLanguage: settings.targetLanguage,
796
803
  settings,
797
- secrets,
798
- systemPrompt: prompt.systemPrompt,
799
- userPrompt: prompt.userPrompt
804
+ secrets
800
805
  });
801
- return result.text.trim();
806
+ return translation.text.trim();
802
807
  },
803
808
  getDiagnostics() {
804
809
  let transcriptWatchers = 0;
@@ -827,6 +832,86 @@ export function createTranslationService(deps) {
827
832
  }
828
833
  await submitTerminalInput(deps.runtime, record.id, text);
829
834
  }
835
+ async function translateText(input) {
836
+ if (deps.codexTranslationService && input.repoRoot) {
837
+ const job = await deps.codexTranslationService.createConversationJob(input.repoRoot, {
838
+ taskSlug: input.taskSlug,
839
+ role: input.role,
840
+ direction: input.direction,
841
+ sourceText: input.text,
842
+ sourceLanguage: input.sourceLanguage,
843
+ targetLanguage: input.targetLanguage,
844
+ contextText: input.contextText
845
+ });
846
+ const result = await waitForCodexConversationResult(input.repoRoot, job, input.settings.requestTimeoutMs);
847
+ return {
848
+ text: result.translatedText
849
+ };
850
+ }
851
+ const prompt = buildTranslationPrompt({
852
+ direction: input.direction,
853
+ text: input.text,
854
+ sourceKind: input.sourceKind,
855
+ contextText: input.contextText,
856
+ settings: input.settings
857
+ });
858
+ const result = await deps.provider.translate({
859
+ settings: input.settings,
860
+ secrets: input.secrets,
861
+ systemPrompt: prompt.systemPrompt,
862
+ userPrompt: prompt.userPrompt
863
+ });
864
+ const parsed = prompt.parseWarning ? parseTranslationWarning(result.text) : { text: result.text };
865
+ return {
866
+ text: parsed.text,
867
+ warning: parsed.warning,
868
+ tokenUsage: result.tokenUsage
869
+ };
870
+ }
871
+ async function waitForCodexConversationResult(repoRoot, job, timeoutMs) {
872
+ const deadline = Date.now() + timeoutMs;
873
+ let lastError;
874
+ while (Date.now() <= deadline) {
875
+ const state = await deps.codexTranslationService.getState(repoRoot);
876
+ const item = job.queueItemId
877
+ ? state.queue.items.find((candidate) => candidate.id === job.queueItemId)
878
+ : undefined;
879
+ if (item && ["failed", "cancelled", "interrupted", "skipped"].includes(item.status)) {
880
+ throw new VcmError({
881
+ code: "TRANSLATION_FAILED",
882
+ message: item.error ?? "Codex translation failed.",
883
+ statusCode: 502
884
+ });
885
+ }
886
+ if (item && item.status !== "completed") {
887
+ await delay(Math.min(500, Math.max(25, timeoutMs)));
888
+ continue;
889
+ }
890
+ try {
891
+ return await deps.codexTranslationService.validateConversationResult(repoRoot, {
892
+ taskSlug: job.taskSlug,
893
+ resultPath: job.resultPath,
894
+ sourceHash: job.sourceHash,
895
+ targetLanguage: job.targetLanguage
896
+ });
897
+ }
898
+ catch (error) {
899
+ lastError = error;
900
+ if (item?.status === "completed") {
901
+ throw error;
902
+ }
903
+ }
904
+ await delay(Math.min(500, Math.max(25, timeoutMs)));
905
+ }
906
+ throw new VcmError({
907
+ code: "TRANSLATION_TIMEOUT",
908
+ message: lastError instanceof Error ? `Codex translation timed out: ${lastError.message}` : "Codex translation timed out.",
909
+ statusCode: 504
910
+ });
911
+ }
912
+ }
913
+ function delay(ms) {
914
+ return new Promise((resolve) => setTimeout(resolve, ms));
830
915
  }
831
916
  function getTranscriptReplaySince(roleSession) {
832
917
  const rawTimestamp = roleSession.startedAt ?? roleSession.updatedAt;