vibe-coding-master 0.3.28 → 0.3.30
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 +14 -18
- package/dist/backend/api/project-routes.js +1 -1
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/api/{codex-translation-routes.js → translation-worker-routes.js} +21 -21
- package/dist/backend/cli/install-vcm-harness.js +30 -25
- package/dist/backend/gateway/gateway-service.js +24 -5
- package/dist/backend/server.js +12 -22
- package/dist/backend/services/claude-hook-service.js +93 -26
- package/dist/backend/services/harness-service.js +49 -29
- package/dist/backend/services/message-service.js +9 -6
- package/dist/backend/services/project-service.js +2 -2
- package/dist/backend/services/session-service.js +99 -172
- package/dist/backend/services/translation-service.js +17 -17
- package/dist/backend/services/{codex-translation-service.js → translation-worker-service.js} +11 -11
- package/dist/backend/templates/harness/gate-review.js +2 -58
- package/dist/shared/constants.js +19 -12
- package/dist/shared/types/app-settings.js +1 -1
- package/dist/shared/types/session.js +2 -14
- package/dist-frontend/assets/{index-Cfum1Prr.css → index-BvUHLq4X.css} +1 -1
- package/dist-frontend/assets/{index-x9I-bGUt.js → index-D0ezB4na.js} +36 -36
- package/dist-frontend/index.html +2 -2
- package/docs/{codex-translation-plan.md → claude-code-translation-plan.md} +151 -153
- package/docs/full-harness-baseline.md +3 -7
- package/docs/gate-review-gates.md +13 -5
- package/docs/gateway-design.md +1 -1
- package/docs/product-design.md +12 -11
- package/docs/v0.4-custom-workflow-plan.md +0 -1
- package/docs/vcm-cc-best-practices.md +2 -5
- package/package.json +1 -1
- package/dist/backend/api/codex-hook-routes.js +0 -9
- package/dist/backend/codex-sandbox-mode.js +0 -28
- package/dist/backend/services/codex-hook-service.js +0 -71
- package/dist/shared/types/codex-hook.js +0 -1
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { CORE_VCM_ROLE_NAMES, 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 GATE_REVIEWER_ROLE = "gate-reviewer";
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
13
|
-
const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
|
|
9
|
+
const TRANSLATOR_ROLE = "translator";
|
|
10
|
+
const TRANSLATION_DIR = ".ai/vcm/translations";
|
|
11
|
+
const TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
14
12
|
const GATE_REVIEWER_SESSION_PATH = ".ai/vcm/gate-reviewer/session.json";
|
|
15
13
|
const PROJECT_TRANSLATOR_SCOPE = "__project__";
|
|
16
14
|
const PROJECT_GATE_REVIEWER_SCOPE = "__project_gate_reviewer__";
|
|
@@ -26,16 +24,9 @@ export function createSessionService(deps) {
|
|
|
26
24
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
27
25
|
const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
|
|
28
26
|
const persisted = await loadPersistedRoleRecordForRole(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, taskSlug, role);
|
|
29
|
-
const isCodexRole = isCodexRoleName(role);
|
|
30
|
-
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
31
|
-
const sessionRepoRoot = isTranslator ? repoRoot : taskRepoRoot;
|
|
32
27
|
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
33
|
-
const model =
|
|
34
|
-
|
|
35
|
-
: normalizeClaudeModel(input.model ?? persisted?.model);
|
|
36
|
-
const effort = isCodexRole
|
|
37
|
-
? normalizeCodexEffort(input.effort ?? persisted?.effort)
|
|
38
|
-
: normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
28
|
+
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
29
|
+
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
39
30
|
const claudeSessionId = launchMode === "resume"
|
|
40
31
|
? persisted?.claudeSessionId
|
|
41
32
|
: randomUUID();
|
|
@@ -49,13 +40,11 @@ export function createSessionService(deps) {
|
|
|
49
40
|
}
|
|
50
41
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
51
42
|
? persisted.transcriptPath
|
|
52
|
-
:
|
|
53
|
-
const startCommand =
|
|
54
|
-
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
cwd: taskRepoRoot
|
|
58
|
-
};
|
|
43
|
+
: claudeTranscriptPath(taskRepoRoot, claudeSessionId);
|
|
44
|
+
const startCommand = {
|
|
45
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
46
|
+
cwd: taskRepoRoot
|
|
47
|
+
};
|
|
59
48
|
const runtimeSession = await deps.runtime.createSession({
|
|
60
49
|
taskSlug,
|
|
61
50
|
role,
|
|
@@ -64,7 +53,7 @@ export function createSessionService(deps) {
|
|
|
64
53
|
cwd: startCommand.cwd,
|
|
65
54
|
env: {
|
|
66
55
|
VCM_API_URL: deps.apiUrl,
|
|
67
|
-
VCM_TASK_REPO_ROOT:
|
|
56
|
+
VCM_TASK_REPO_ROOT: taskRepoRoot,
|
|
68
57
|
VCM_TASK_SLUG: taskSlug,
|
|
69
58
|
VCM_ROLE: role,
|
|
70
59
|
VCM_SESSION_ID: claudeSessionId
|
|
@@ -101,10 +90,10 @@ export function createSessionService(deps) {
|
|
|
101
90
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
102
91
|
return record;
|
|
103
92
|
}
|
|
104
|
-
async function launchProjectGateReviewerSession(repoRoot, input, launchMode) {
|
|
93
|
+
async function launchProjectGateReviewerSession(repoRoot, input, launchMode, activeTaskSlug) {
|
|
105
94
|
const live = toRoleSessionRecordView(getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
|
|
106
95
|
if (live && live.status === "running") {
|
|
107
|
-
return live;
|
|
96
|
+
return bindProjectGateReviewerSession(repoRoot, live, activeTaskSlug);
|
|
108
97
|
}
|
|
109
98
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
110
99
|
const persisted = await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
|
|
@@ -125,6 +114,10 @@ export function createSessionService(deps) {
|
|
|
125
114
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
126
115
|
? persisted.transcriptPath
|
|
127
116
|
: claudeTranscriptPath(repoRoot, claudeSessionId);
|
|
117
|
+
const activeTask = activeTaskSlug
|
|
118
|
+
? await deps.taskService.loadTask(repoRoot, activeTaskSlug)
|
|
119
|
+
: undefined;
|
|
120
|
+
const activeTaskRepoRoot = activeTask ? getTaskRuntimeRepoRoot(activeTask) : undefined;
|
|
128
121
|
const startCommand = {
|
|
129
122
|
...deps.claude.buildRoleStartCommand(GATE_REVIEWER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
130
123
|
cwd: repoRoot
|
|
@@ -137,8 +130,8 @@ export function createSessionService(deps) {
|
|
|
137
130
|
cwd: startCommand.cwd,
|
|
138
131
|
env: {
|
|
139
132
|
VCM_API_URL: deps.apiUrl,
|
|
140
|
-
VCM_TASK_REPO_ROOT: repoRoot,
|
|
141
|
-
VCM_TASK_SLUG: PROJECT_GATE_REVIEWER_SCOPE,
|
|
133
|
+
VCM_TASK_REPO_ROOT: activeTaskRepoRoot ?? repoRoot,
|
|
134
|
+
VCM_TASK_SLUG: activeTaskSlug ?? PROJECT_GATE_REVIEWER_SCOPE,
|
|
142
135
|
VCM_ROLE: GATE_REVIEWER_ROLE,
|
|
143
136
|
VCM_SESSION_ID: claudeSessionId
|
|
144
137
|
},
|
|
@@ -164,35 +157,63 @@ export function createSessionService(deps) {
|
|
|
164
157
|
startedAt: runtimeSession.startedAt,
|
|
165
158
|
updatedAt: timestamp,
|
|
166
159
|
lastOutputAt: runtimeSession.lastOutputAt,
|
|
160
|
+
activeTaskSlug,
|
|
161
|
+
activeTaskRepoRoot,
|
|
167
162
|
exitCode: runtimeSession.exitCode
|
|
168
163
|
};
|
|
169
164
|
deps.registry.upsert(record);
|
|
170
165
|
await persistProjectGateReviewerSession(deps.fs, repoRoot, record);
|
|
171
166
|
return record;
|
|
172
167
|
}
|
|
168
|
+
async function bindProjectGateReviewerSession(repoRoot, record, activeTaskSlug) {
|
|
169
|
+
if (!activeTaskSlug) {
|
|
170
|
+
return record;
|
|
171
|
+
}
|
|
172
|
+
const task = await deps.taskService.loadTask(repoRoot, activeTaskSlug);
|
|
173
|
+
const activeTaskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
174
|
+
const updated = {
|
|
175
|
+
...record,
|
|
176
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
177
|
+
activeTaskSlug,
|
|
178
|
+
activeTaskRepoRoot,
|
|
179
|
+
updatedAt: now()
|
|
180
|
+
};
|
|
181
|
+
deps.registry.upsert(updated);
|
|
182
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
|
|
183
|
+
return updated;
|
|
184
|
+
}
|
|
173
185
|
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
174
186
|
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
175
187
|
if (live && live.status === "running") {
|
|
176
188
|
return live;
|
|
177
189
|
}
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
190
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
191
|
+
const persisted = await loadPersistedTranslatorSession(deps.fs, repoRoot);
|
|
192
|
+
const permissionMode = normalizeClaudePermissionMode(input.permissionMode ?? persisted?.permissionMode);
|
|
193
|
+
const model = normalizeClaudeModel(input.model ?? persisted?.model);
|
|
194
|
+
const effort = normalizeClaudeEffort(input.effort ?? persisted?.effort);
|
|
181
195
|
const claudeSessionId = launchMode === "resume"
|
|
182
196
|
? persisted?.claudeSessionId
|
|
183
197
|
: randomUUID();
|
|
184
198
|
if (!claudeSessionId) {
|
|
185
199
|
throw new VcmError({
|
|
186
|
-
code: "
|
|
187
|
-
message: "
|
|
200
|
+
code: "TRANSLATOR_SESSION_MISSING",
|
|
201
|
+
message: "Translator does not have a session id to resume.",
|
|
188
202
|
statusCode: 409,
|
|
189
203
|
hint: "Start the translator once before using Resume."
|
|
190
204
|
});
|
|
191
205
|
}
|
|
192
|
-
|
|
206
|
+
await deps.fs.ensureDir(resolveRepoPath(repoRoot, TRANSLATION_DIR));
|
|
207
|
+
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
208
|
+
? persisted.transcriptPath
|
|
209
|
+
: claudeTranscriptPath(repoRoot, claudeSessionId);
|
|
210
|
+
const startCommand = {
|
|
211
|
+
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
212
|
+
cwd: repoRoot
|
|
213
|
+
};
|
|
193
214
|
const runtimeSession = await deps.runtime.createSession({
|
|
194
215
|
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
195
|
-
role:
|
|
216
|
+
role: TRANSLATOR_ROLE,
|
|
196
217
|
command: startCommand.command,
|
|
197
218
|
args: startCommand.args,
|
|
198
219
|
cwd: startCommand.cwd,
|
|
@@ -200,7 +221,7 @@ export function createSessionService(deps) {
|
|
|
200
221
|
VCM_API_URL: deps.apiUrl,
|
|
201
222
|
VCM_TASK_REPO_ROOT: repoRoot,
|
|
202
223
|
VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
|
|
203
|
-
VCM_ROLE:
|
|
224
|
+
VCM_ROLE: TRANSLATOR_ROLE,
|
|
204
225
|
VCM_SESSION_ID: claudeSessionId
|
|
205
226
|
},
|
|
206
227
|
cols: input.cols,
|
|
@@ -210,12 +231,13 @@ export function createSessionService(deps) {
|
|
|
210
231
|
const record = {
|
|
211
232
|
id: runtimeSession.id,
|
|
212
233
|
claudeSessionId,
|
|
234
|
+
transcriptPath,
|
|
213
235
|
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
214
|
-
role:
|
|
236
|
+
role: TRANSLATOR_ROLE,
|
|
215
237
|
status: runtimeSession.status,
|
|
216
238
|
activityStatus: "idle",
|
|
217
239
|
command: startCommand.display,
|
|
218
|
-
permissionMode
|
|
240
|
+
permissionMode,
|
|
219
241
|
model,
|
|
220
242
|
effort,
|
|
221
243
|
cwd: startCommand.cwd,
|
|
@@ -227,7 +249,7 @@ export function createSessionService(deps) {
|
|
|
227
249
|
exitCode: runtimeSession.exitCode
|
|
228
250
|
};
|
|
229
251
|
deps.registry.upsert(record);
|
|
230
|
-
await
|
|
252
|
+
await persistTranslatorSession(deps.fs, repoRoot, record);
|
|
231
253
|
return record;
|
|
232
254
|
}
|
|
233
255
|
return {
|
|
@@ -303,7 +325,7 @@ export function createSessionService(deps) {
|
|
|
303
325
|
if (!existing) {
|
|
304
326
|
throw new VcmError({
|
|
305
327
|
code: "SESSION_MISSING",
|
|
306
|
-
message: "
|
|
328
|
+
message: "Translator session has not been started.",
|
|
307
329
|
statusCode: 404
|
|
308
330
|
});
|
|
309
331
|
}
|
|
@@ -317,7 +339,7 @@ export function createSessionService(deps) {
|
|
|
317
339
|
updatedAt: now()
|
|
318
340
|
};
|
|
319
341
|
deps.registry.upsert(updated);
|
|
320
|
-
await
|
|
342
|
+
await persistTranslatorSession(deps.fs, repoRoot, updated);
|
|
321
343
|
return updated;
|
|
322
344
|
},
|
|
323
345
|
async restartProjectTranslatorSession(repoRoot, input = {}) {
|
|
@@ -333,7 +355,7 @@ export function createSessionService(deps) {
|
|
|
333
355
|
},
|
|
334
356
|
async getProjectTranslatorSession(repoRoot) {
|
|
335
357
|
const record = getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
|
|
336
|
-
?? await
|
|
358
|
+
?? await loadPersistedTranslatorSession(deps.fs, repoRoot);
|
|
337
359
|
return toRoleSessionRecordView(record, deps.runtime);
|
|
338
360
|
},
|
|
339
361
|
async ensureProjectTranslatorSession(repoRoot, input = {}) {
|
|
@@ -372,15 +394,15 @@ export function createSessionService(deps) {
|
|
|
372
394
|
updatedAt: timestamp
|
|
373
395
|
};
|
|
374
396
|
deps.registry.upsert(updated);
|
|
375
|
-
await
|
|
397
|
+
await persistTranslatorSession(deps.fs, repoRoot, updated);
|
|
376
398
|
return updated;
|
|
377
399
|
},
|
|
378
400
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
379
401
|
if (role === GATE_REVIEWER_ROLE) {
|
|
380
|
-
return
|
|
402
|
+
return launchProjectGateReviewerSession(repoRoot, input, "fresh", taskSlug)
|
|
381
403
|
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
382
404
|
}
|
|
383
|
-
if (role ===
|
|
405
|
+
if (role === TRANSLATOR_ROLE) {
|
|
384
406
|
void taskSlug;
|
|
385
407
|
return this.startProjectTranslatorSession(repoRoot, input);
|
|
386
408
|
}
|
|
@@ -388,10 +410,10 @@ export function createSessionService(deps) {
|
|
|
388
410
|
},
|
|
389
411
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
390
412
|
if (role === GATE_REVIEWER_ROLE) {
|
|
391
|
-
return
|
|
413
|
+
return launchProjectGateReviewerSession(repoRoot, input, "resume", taskSlug)
|
|
392
414
|
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
393
415
|
}
|
|
394
|
-
if (role ===
|
|
416
|
+
if (role === TRANSLATOR_ROLE) {
|
|
395
417
|
void taskSlug;
|
|
396
418
|
return this.resumeProjectTranslatorSession(repoRoot, input);
|
|
397
419
|
}
|
|
@@ -401,7 +423,7 @@ export function createSessionService(deps) {
|
|
|
401
423
|
if (role === GATE_REVIEWER_ROLE) {
|
|
402
424
|
return scopeProjectRoleSession(await this.stopProjectGateReviewerSession(repoRoot), taskSlug);
|
|
403
425
|
}
|
|
404
|
-
if (role ===
|
|
426
|
+
if (role === TRANSLATOR_ROLE) {
|
|
405
427
|
void taskSlug;
|
|
406
428
|
return this.stopProjectTranslatorSession(repoRoot);
|
|
407
429
|
}
|
|
@@ -430,9 +452,16 @@ export function createSessionService(deps) {
|
|
|
430
452
|
},
|
|
431
453
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
432
454
|
if (role === GATE_REVIEWER_ROLE) {
|
|
433
|
-
|
|
455
|
+
const existing = await this.getProjectGateReviewerSession(repoRoot);
|
|
456
|
+
if (existing && deps.runtime.getSession(existing.id)) {
|
|
457
|
+
await deps.runtime.stop(existing.id);
|
|
458
|
+
}
|
|
459
|
+
if (existing) {
|
|
460
|
+
deps.registry.remove(existing.id);
|
|
461
|
+
}
|
|
462
|
+
return scopeProjectRoleSession(await launchProjectGateReviewerSession(repoRoot, input, "fresh", taskSlug), taskSlug);
|
|
434
463
|
}
|
|
435
|
-
if (role ===
|
|
464
|
+
if (role === TRANSLATOR_ROLE) {
|
|
436
465
|
void taskSlug;
|
|
437
466
|
return this.restartProjectTranslatorSession(repoRoot, input);
|
|
438
467
|
}
|
|
@@ -448,9 +477,13 @@ export function createSessionService(deps) {
|
|
|
448
477
|
},
|
|
449
478
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
450
479
|
if (role === GATE_REVIEWER_ROLE) {
|
|
451
|
-
|
|
480
|
+
const session = await this.getProjectGateReviewerSession(repoRoot);
|
|
481
|
+
if (!session) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
return scopeProjectRoleSession(await bindProjectGateReviewerSession(repoRoot, session, taskSlug), taskSlug);
|
|
452
485
|
}
|
|
453
|
-
if (role ===
|
|
486
|
+
if (role === TRANSLATOR_ROLE) {
|
|
454
487
|
void taskSlug;
|
|
455
488
|
return this.getProjectTranslatorSession(repoRoot);
|
|
456
489
|
}
|
|
@@ -470,7 +503,7 @@ export function createSessionService(deps) {
|
|
|
470
503
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
471
504
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
472
505
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
473
|
-
for (const role of
|
|
506
|
+
for (const role of CORE_VCM_ROLE_NAMES) {
|
|
474
507
|
const record = deps.registry.getByRole(taskSlug, role)
|
|
475
508
|
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
476
509
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
@@ -510,7 +543,7 @@ export function createSessionService(deps) {
|
|
|
510
543
|
await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
|
|
511
544
|
return scopeProjectRoleSession(updated, input.taskSlug);
|
|
512
545
|
}
|
|
513
|
-
if (input.role ===
|
|
546
|
+
if (input.role === TRANSLATOR_ROLE) {
|
|
514
547
|
void input.taskSlug;
|
|
515
548
|
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
516
549
|
eventName: input.eventName,
|
|
@@ -633,80 +666,6 @@ function toRoleSessionRecordView(record, runtime) {
|
|
|
633
666
|
exitCode: runtimeSession.exitCode
|
|
634
667
|
};
|
|
635
668
|
}
|
|
636
|
-
async function buildCodexStartCommand(fs, baseRepoRoot, _taskRepoRoot, role, launchMode, selectedModel, selectedEffort, sandboxMode, resumeSessionId) {
|
|
637
|
-
const isTranslator = role === CODEX_TRANSLATOR_ROLE;
|
|
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);
|
|
647
|
-
if (!(await fs.pathExists(codexDir))) {
|
|
648
|
-
throw new VcmError({
|
|
649
|
-
code: "CODEX_TRANSLATOR_CONFIG_MISSING",
|
|
650
|
-
message: `${CODEX_TRANSLATOR_DIR} does not exist.`,
|
|
651
|
-
statusCode: 409,
|
|
652
|
-
hint: "Apply the VCM harness before starting Codex Translator."
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
await fs.ensureDir(outputDir);
|
|
656
|
-
const config = await loadCodexSessionConfig(fs, baseRepoRoot, CODEX_TRANSLATOR_CONFIG_PATH);
|
|
657
|
-
const args = launchMode === "resume"
|
|
658
|
-
? resumeSessionId ? ["resume", resumeSessionId] : ["resume", "--last"]
|
|
659
|
-
: [];
|
|
660
|
-
args.push("--cd", codexDir);
|
|
661
|
-
if (isDevContainerSandbox(sandboxMode)) {
|
|
662
|
-
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
args.push("--add-dir", baseRepoRoot);
|
|
666
|
-
args.push("--sandbox", "workspace-write", "--ask-for-approval", "never");
|
|
667
|
-
}
|
|
668
|
-
args.push("--dangerously-bypass-hook-trust", "--search");
|
|
669
|
-
if (selectedModel !== "default") {
|
|
670
|
-
args.push("--model", selectedModel);
|
|
671
|
-
}
|
|
672
|
-
if (selectedEffort !== "default") {
|
|
673
|
-
args.push("--config", `model_reasoning_effort="${selectedEffort}"`);
|
|
674
|
-
}
|
|
675
|
-
return {
|
|
676
|
-
command: config.command,
|
|
677
|
-
args,
|
|
678
|
-
cwd: baseRepoRoot,
|
|
679
|
-
display: [config.command, ...args].map(formatDisplayArg).join(" ")
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
function hasCapturedCodexSession(record) {
|
|
683
|
-
return Boolean(record?.lastHookEventAt || record?.transcriptPath);
|
|
684
|
-
}
|
|
685
|
-
function isDevContainerSandbox(value) {
|
|
686
|
-
const normalized = value?.toLowerCase().replace(/[\s_-]+/g, "");
|
|
687
|
-
return normalized === "devcontainer" ||
|
|
688
|
-
normalized === "container" ||
|
|
689
|
-
normalized === "docker" ||
|
|
690
|
-
normalized === "podman" ||
|
|
691
|
-
normalized === "codespaces" ||
|
|
692
|
-
normalized === "bypass" ||
|
|
693
|
-
normalized === "nosandbox" ||
|
|
694
|
-
normalized === "disabled" ||
|
|
695
|
-
normalized === "off" ||
|
|
696
|
-
normalized === "none";
|
|
697
|
-
}
|
|
698
|
-
async function loadCodexSessionConfig(fs, repoRoot, configRelativePath) {
|
|
699
|
-
const configPath = resolveRepoPath(repoRoot, configRelativePath);
|
|
700
|
-
if (!(await fs.pathExists(configPath))) {
|
|
701
|
-
return {
|
|
702
|
-
command: "codex"
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
const content = await fs.readText(configPath);
|
|
706
|
-
return {
|
|
707
|
-
command: parseTomlString(content, "command") ?? "codex"
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
669
|
function matchesRoleHookSession(record, input) {
|
|
711
670
|
if (input.sessionId && record.claudeSessionId === input.sessionId) {
|
|
712
671
|
return true;
|
|
@@ -744,7 +703,7 @@ function getHandoffArtifactPath(paths, role) {
|
|
|
744
703
|
return undefined;
|
|
745
704
|
}
|
|
746
705
|
function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
747
|
-
if (role !==
|
|
706
|
+
if (role !== TRANSLATOR_ROLE && role !== GATE_REVIEWER_ROLE) {
|
|
748
707
|
return registry.getByRole(taskSlug, role);
|
|
749
708
|
}
|
|
750
709
|
const candidates = registry.list().filter((session) => session.role === role);
|
|
@@ -760,7 +719,7 @@ function getRegisteredProjectGateReviewerSession(registry, runtime) {
|
|
|
760
719
|
return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
761
720
|
}
|
|
762
721
|
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
763
|
-
const candidates = registry.list().filter((session) => session.role ===
|
|
722
|
+
const candidates = registry.list().filter((session) => session.role === TRANSLATOR_ROLE);
|
|
764
723
|
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
765
724
|
return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
766
725
|
}
|
|
@@ -768,7 +727,7 @@ function compareSessionUpdatedAtDesc(left, right) {
|
|
|
768
727
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
769
728
|
}
|
|
770
729
|
function scopeProjectRoleSession(record, taskSlug) {
|
|
771
|
-
if (!record || (record.role !==
|
|
730
|
+
if (!record || (record.role !== TRANSLATOR_ROLE && record.role !== GATE_REVIEWER_ROLE)) {
|
|
772
731
|
return record;
|
|
773
732
|
}
|
|
774
733
|
return {
|
|
@@ -781,9 +740,9 @@ async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, st
|
|
|
781
740
|
void taskSlug;
|
|
782
741
|
return loadPersistedProjectGateReviewerSession(fs, baseRepoRoot);
|
|
783
742
|
}
|
|
784
|
-
if (role ===
|
|
743
|
+
if (role === TRANSLATOR_ROLE) {
|
|
785
744
|
void taskSlug;
|
|
786
|
-
return
|
|
745
|
+
return loadPersistedTranslatorSession(fs, baseRepoRoot);
|
|
787
746
|
}
|
|
788
747
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
789
748
|
}
|
|
@@ -802,8 +761,8 @@ async function loadPersistedProjectGateReviewerSession(fs, repoRoot) {
|
|
|
802
761
|
taskSlug: PROJECT_GATE_REVIEWER_SCOPE
|
|
803
762
|
};
|
|
804
763
|
}
|
|
805
|
-
async function
|
|
806
|
-
const sessionPath = resolveRepoPath(repoRoot,
|
|
764
|
+
async function loadPersistedTranslatorSession(fs, repoRoot) {
|
|
765
|
+
const sessionPath = resolveRepoPath(repoRoot, TRANSLATOR_SESSION_PATH);
|
|
807
766
|
if (!(await fs.pathExists(sessionPath))) {
|
|
808
767
|
return undefined;
|
|
809
768
|
}
|
|
@@ -840,12 +799,8 @@ function normalizePersistedRoleRecord(record) {
|
|
|
840
799
|
? legacy.lastStopAt
|
|
841
800
|
: undefined),
|
|
842
801
|
permissionMode: normalizeClaudePermissionMode(record.permissionMode),
|
|
843
|
-
model:
|
|
844
|
-
|
|
845
|
-
: normalizeClaudeModel(record.model),
|
|
846
|
-
effort: isCodexRoleName(record.role)
|
|
847
|
-
? normalizeCodexEffort(record.effort)
|
|
848
|
-
: normalizeClaudeEffort(record.effort)
|
|
802
|
+
model: normalizeClaudeModel(record.model),
|
|
803
|
+
effort: normalizeClaudeEffort(record.effort)
|
|
849
804
|
}
|
|
850
805
|
: undefined;
|
|
851
806
|
}
|
|
@@ -879,8 +834,8 @@ async function persistRoleSessionRecord(fs, baseRepoRoot, taskRepoRoot, stateRoo
|
|
|
879
834
|
await persistProjectGateReviewerSession(fs, baseRepoRoot, session);
|
|
880
835
|
return;
|
|
881
836
|
}
|
|
882
|
-
if (session.role ===
|
|
883
|
-
await
|
|
837
|
+
if (session.role === TRANSLATOR_ROLE) {
|
|
838
|
+
await persistTranslatorSession(fs, baseRepoRoot, session);
|
|
884
839
|
return;
|
|
885
840
|
}
|
|
886
841
|
await persistTaskSession(fs, taskRepoRoot, stateRoot, session);
|
|
@@ -896,8 +851,8 @@ async function persistProjectGateReviewerSession(fs, repoRoot, session) {
|
|
|
896
851
|
}
|
|
897
852
|
});
|
|
898
853
|
}
|
|
899
|
-
async function
|
|
900
|
-
await fs.writeJsonAtomic(resolveRepoPath(repoRoot,
|
|
854
|
+
async function persistTranslatorSession(fs, repoRoot, session) {
|
|
855
|
+
await fs.writeJsonAtomic(resolveRepoPath(repoRoot, TRANSLATOR_SESSION_PATH), {
|
|
901
856
|
version: 1,
|
|
902
857
|
role: session.role,
|
|
903
858
|
updatedAt: session.updatedAt,
|
|
@@ -943,12 +898,6 @@ function normalizeClaudeModel(value) {
|
|
|
943
898
|
}
|
|
944
899
|
return "default";
|
|
945
900
|
}
|
|
946
|
-
function normalizeCodexModel(value) {
|
|
947
|
-
if (value === "default" || value === "gpt-5.5") {
|
|
948
|
-
return value;
|
|
949
|
-
}
|
|
950
|
-
return "gpt-5.5";
|
|
951
|
-
}
|
|
952
901
|
function normalizeClaudeEffort(value) {
|
|
953
902
|
if (value === "low"
|
|
954
903
|
|| value === "medium"
|
|
@@ -960,25 +909,3 @@ function normalizeClaudeEffort(value) {
|
|
|
960
909
|
}
|
|
961
910
|
return "default";
|
|
962
911
|
}
|
|
963
|
-
function normalizeCodexEffort(value) {
|
|
964
|
-
if (value === "low"
|
|
965
|
-
|| value === "medium"
|
|
966
|
-
|| value === "high"
|
|
967
|
-
|| value === "xhigh") {
|
|
968
|
-
return value;
|
|
969
|
-
}
|
|
970
|
-
return "default";
|
|
971
|
-
}
|
|
972
|
-
function parseTomlString(content, key) {
|
|
973
|
-
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "m");
|
|
974
|
-
return pattern.exec(content)?.[1];
|
|
975
|
-
}
|
|
976
|
-
function escapeRegExp(value) {
|
|
977
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
978
|
-
}
|
|
979
|
-
function formatDisplayArg(value) {
|
|
980
|
-
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
|
|
981
|
-
return value;
|
|
982
|
-
}
|
|
983
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
984
|
-
}
|
|
@@ -7,8 +7,8 @@ const TRANSLATION_SOURCE_LANGUAGE = "auto";
|
|
|
7
7
|
const TRANSLATION_INPUT_MODE = "review-before-send";
|
|
8
8
|
const TRANSLATION_CONTEXT_ENABLED = false;
|
|
9
9
|
const TRANSLATION_TIMEOUT_MS = 120000;
|
|
10
|
-
const TRANSLATION_PROVIDER = "
|
|
11
|
-
const TRANSLATION_MODEL = "
|
|
10
|
+
const TRANSLATION_PROVIDER = "claude-code";
|
|
11
|
+
const TRANSLATION_MODEL = "translator";
|
|
12
12
|
const OUTPUT_TRANSLATION_BATCH_DELAY_MS = 10000;
|
|
13
13
|
const TRANSCRIPT_REPLAY_GRACE_MS = 5000;
|
|
14
14
|
export function createTranslationService(deps) {
|
|
@@ -313,7 +313,7 @@ export function createTranslationService(deps) {
|
|
|
313
313
|
const jobs = [];
|
|
314
314
|
for (let index = 0; index < items.length; index += 1) {
|
|
315
315
|
const item = items[index];
|
|
316
|
-
const job = await
|
|
316
|
+
const job = await createConversationJob({
|
|
317
317
|
repoRoot: item.repoRoot,
|
|
318
318
|
taskSlug: item.entry.taskSlug,
|
|
319
319
|
role: item.entry.role,
|
|
@@ -329,7 +329,7 @@ export function createTranslationService(deps) {
|
|
|
329
329
|
}
|
|
330
330
|
for (const { item, job } of jobs) {
|
|
331
331
|
try {
|
|
332
|
-
const result = await
|
|
332
|
+
const result = await waitForConversationResult(item.repoRoot, job, item.config.requestTimeoutMs);
|
|
333
333
|
const completed = {
|
|
334
334
|
...item.entry,
|
|
335
335
|
status: "translated",
|
|
@@ -882,28 +882,28 @@ export function createTranslationService(deps) {
|
|
|
882
882
|
await submitTerminalInput(deps.runtime, record.id, text);
|
|
883
883
|
}
|
|
884
884
|
async function translateText(input) {
|
|
885
|
-
const job = await
|
|
886
|
-
const result = await
|
|
885
|
+
const job = await createConversationJob(input);
|
|
886
|
+
const result = await waitForConversationResult(input.repoRoot, job, input.config.requestTimeoutMs);
|
|
887
887
|
return {
|
|
888
888
|
text: result.translatedText
|
|
889
889
|
};
|
|
890
890
|
}
|
|
891
|
-
async function
|
|
892
|
-
if (!deps.
|
|
891
|
+
async function createConversationJob(input) {
|
|
892
|
+
if (!deps.translationWorkerService) {
|
|
893
893
|
throw new VcmError({
|
|
894
|
-
code: "
|
|
895
|
-
message: "
|
|
894
|
+
code: "TRANSLATION_WORKER_UNAVAILABLE",
|
|
895
|
+
message: "translation service is unavailable.",
|
|
896
896
|
statusCode: 500
|
|
897
897
|
});
|
|
898
898
|
}
|
|
899
899
|
if (!input.repoRoot) {
|
|
900
900
|
throw new VcmError({
|
|
901
901
|
code: "TRANSLATION_REPO_ROOT_MISSING",
|
|
902
|
-
message: "
|
|
902
|
+
message: "translation requires a base repository root.",
|
|
903
903
|
statusCode: 500
|
|
904
904
|
});
|
|
905
905
|
}
|
|
906
|
-
return deps.
|
|
906
|
+
return deps.translationWorkerService.createConversationJob(input.repoRoot, {
|
|
907
907
|
direction: input.direction,
|
|
908
908
|
sourceText: input.text,
|
|
909
909
|
sourceLanguage: input.sourceLanguage,
|
|
@@ -912,18 +912,18 @@ export function createTranslationService(deps) {
|
|
|
912
912
|
deferDispatch: input.deferDispatch
|
|
913
913
|
});
|
|
914
914
|
}
|
|
915
|
-
async function
|
|
915
|
+
async function waitForConversationResult(repoRoot, job, timeoutMs) {
|
|
916
916
|
const deadline = Date.now() + timeoutMs;
|
|
917
917
|
let lastError;
|
|
918
918
|
while (Date.now() <= deadline) {
|
|
919
|
-
const state = await deps.
|
|
919
|
+
const state = await deps.translationWorkerService.getState(repoRoot);
|
|
920
920
|
const item = job.queueItemId
|
|
921
921
|
? state.queue.items.find((candidate) => candidate.id === job.queueItemId)
|
|
922
922
|
: undefined;
|
|
923
923
|
if (item && ["failed", "cancelled", "interrupted", "skipped"].includes(item.status)) {
|
|
924
924
|
throw new VcmError({
|
|
925
925
|
code: "TRANSLATION_FAILED",
|
|
926
|
-
message: item.error ?? "
|
|
926
|
+
message: item.error ?? "translation failed.",
|
|
927
927
|
statusCode: 502
|
|
928
928
|
});
|
|
929
929
|
}
|
|
@@ -932,7 +932,7 @@ export function createTranslationService(deps) {
|
|
|
932
932
|
continue;
|
|
933
933
|
}
|
|
934
934
|
try {
|
|
935
|
-
return await deps.
|
|
935
|
+
return await deps.translationWorkerService.validateConversationResult(repoRoot, {
|
|
936
936
|
resultPath: job.resultPath,
|
|
937
937
|
sourceHash: job.sourceHash,
|
|
938
938
|
targetLanguage: job.targetLanguage
|
|
@@ -948,7 +948,7 @@ export function createTranslationService(deps) {
|
|
|
948
948
|
}
|
|
949
949
|
throw new VcmError({
|
|
950
950
|
code: "TRANSLATION_TIMEOUT",
|
|
951
|
-
message: lastError instanceof Error ? `
|
|
951
|
+
message: lastError instanceof Error ? `translation timed out: ${lastError.message}` : "translation timed out.",
|
|
952
952
|
statusCode: 504
|
|
953
953
|
});
|
|
954
954
|
}
|