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.
- package/README.md +2 -1
- package/dist/backend/api/codex-hook-routes.js +7 -0
- package/dist/backend/api/codex-translation-routes.js +50 -0
- package/dist/backend/cli/install-vcm-harness.js +29 -1
- package/dist/backend/server.js +17 -2
- package/dist/backend/services/artifact-service.js +2 -1
- package/dist/backend/services/codex-hook-service.js +12 -9
- package/dist/backend/services/codex-translation-service.js +964 -0
- package/dist/backend/services/harness-service.js +28 -1
- package/dist/backend/services/session-service.js +128 -36
- package/dist/backend/services/translation-service.js +120 -35
- package/dist/backend/templates/harness/codex-review.js +68 -3
- package/dist/shared/constants.js +15 -1
- package/dist-frontend/assets/index-B13y-ZM8.css +32 -0
- package/dist-frontend/assets/index-C7Nb1xPJ.js +92 -0
- package/dist-frontend/index.html +2 -2
- package/docs/codex-review-gates.md +24 -5
- package/docs/codex-translation-plan.md +1184 -0
- package/package.json +1 -1
- package/dist-frontend/assets/index-CR1EOe-w.css +0 -32
- package/dist-frontend/assets/index-D-6FVz_K.js +0 -92
- package/docs/codex-file-translation-plan.md +0 -618
|
@@ -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.
|
|
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
|
|
24
|
-
const
|
|
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 =
|
|
35
|
+
const model = isCodexRole
|
|
27
36
|
? normalizeCodexModel(input.model ?? persisted?.model)
|
|
28
37
|
: normalizeClaudeModel(input.model ?? persisted?.model);
|
|
29
|
-
const effort =
|
|
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
|
-
:
|
|
46
|
-
const startCommand =
|
|
47
|
-
? await
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
146
|
-
?? await
|
|
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
|
|
160
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
263
|
-
const
|
|
264
|
-
const
|
|
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:
|
|
284
|
+
hint: `Apply the VCM harness before starting ${isTranslator ? "Codex Translator" : "Codex Reviewer"}.`
|
|
271
285
|
});
|
|
272
286
|
}
|
|
273
|
-
await fs.ensureDir(
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
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
|
|
429
|
+
model: isCodexRoleName(record.role)
|
|
359
430
|
? normalizeCodexModel(record.model)
|
|
360
431
|
: normalizeClaudeModel(record.model),
|
|
361
|
-
effort: record.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
|
|
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
|
-
|
|
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:
|
|
285
|
+
translatedText: translation.text,
|
|
285
286
|
completedAt: now(),
|
|
286
|
-
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
|
|
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:
|
|
634
|
-
warning:
|
|
637
|
+
translatedText: translation.text,
|
|
638
|
+
warning: translation.warning,
|
|
635
639
|
completedAt: now(),
|
|
636
|
-
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" && !
|
|
646
|
+
const shouldSend = input.send === true && mode === "auto-send" && !translation.warning;
|
|
643
647
|
if (shouldSend) {
|
|
644
|
-
await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role,
|
|
648
|
+
await writeToCurrentRole(input.repoRoot, input.taskSlug, input.role, translation.text);
|
|
645
649
|
}
|
|
646
650
|
return {
|
|
647
651
|
translation: completed,
|
|
648
|
-
englishPreview:
|
|
652
|
+
englishPreview: translation.text,
|
|
649
653
|
contextUsed: Boolean(contextText),
|
|
650
|
-
requiresReview: mode === "review-before-send" || Boolean(
|
|
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
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
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;
|