vibe-coding-master 0.3.22 → 0.3.23
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 -2
- package/dist/backend/api/codex-translation-routes.js +24 -0
- package/dist/backend/server.js +2 -1
- package/dist/backend/services/codex-hook-service.js +30 -9
- package/dist/backend/services/codex-translation-service.js +17 -40
- package/dist/backend/services/session-service.js +188 -11
- package/dist/backend/services/translation-service.js +15 -4
- package/dist/backend/templates/handoff.js +3 -3
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/coder-agent.js +6 -1
- package/dist/shared/types/app-settings.js +3 -2
- package/dist-frontend/assets/index-C2xTNLmb.js +95 -0
- package/dist-frontend/assets/index-Cfum1Prr.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/docs/codex-translation-plan.md +41 -37
- package/docs/product-design.md +5 -5
- package/docs/v0.4-custom-workflow-plan.md +785 -0
- package/docs/vcm-cc-best-practices.md +15 -12
- package/package.json +1 -1
- package/dist-frontend/assets/index-CEB6Bssn.js +0 -95
- package/dist-frontend/assets/index-Dmefx9m7.css +0 -32
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ the base repo has uncommitted changes, when the current branch has no upstream,
|
|
|
237
237
|
or when the active task is an inline task using the base repo directly. It does
|
|
238
238
|
not stash, merge, or mutate task worktrees.
|
|
239
239
|
|
|
240
|
-
When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the active VCM Session title, task status, start time, total elapsed time, total Round count, and role active runtime.
|
|
240
|
+
When VCM is connected to an active task, the bottom of the sidebar shows a task status dock. It stays outside the collapsible groups and shows the active VCM Session title, task status, start time, total elapsed time, total Round count, and role active runtime. The dock also shows the Current Round while it is running, or the Last Round after a flow pause, including start time, total elapsed time, role active runtime, Turn count, and Round status.
|
|
241
241
|
|
|
242
242
|
## Mobile Gateway
|
|
243
243
|
|
|
@@ -586,7 +586,7 @@ The normal path is timer-driven: `Stop` starts a backend timer, and `UserPromptS
|
|
|
586
586
|
|
|
587
587
|
When `Flow pause alert` is enabled, the frontend polls the task Round state and deduplicates each stopped Round so the same stopped state does not alert on every poll. Flow duration is measured from the first `UserPromptSubmit` to `stoppedAt`, falling back to the last `Stop` when needed. If the flow lasted less than 2 minutes, it plays the local chime 3 times at 1.4 second intervals. If the flow lasted 2 minutes or longer, it shows a modal alert and repeats the local chime until the user clicks `Confirm`. A stopped Round can mean normal completion, user decision needed, dispatch failure, or another workflow interruption; the point is to get the user to look with the right amount of urgency.
|
|
588
588
|
|
|
589
|
-
The
|
|
589
|
+
The Round dock separates wall-clock Round duration from role active runtime. For a running Round, `Total` is `now - Round.startedAt`; for a stopped Round, it is `stoppedAt - Round.startedAt`. `Role runtime` is only the accumulated time between each Turn's `UserPromptSubmit` and `Stop`; `Turn count` is the number of accepted prompts inside the Round.
|
|
590
590
|
|
|
591
591
|
## Resume Behavior
|
|
592
592
|
|
|
@@ -4,6 +4,30 @@ export function registerCodexTranslationRoutes(app, deps) {
|
|
|
4
4
|
const project = await requireCurrentProject(deps.projectService);
|
|
5
5
|
return deps.codexTranslationService.getState(project.repoRoot);
|
|
6
6
|
});
|
|
7
|
+
app.get("/api/translation/codex/session", async () => {
|
|
8
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
9
|
+
return (await deps.sessionService.getProjectTranslatorSession(project.repoRoot)) ?? null;
|
|
10
|
+
});
|
|
11
|
+
app.post("/api/translation/codex/session/ensure", async (request) => {
|
|
12
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
13
|
+
return deps.sessionService.ensureProjectTranslatorSession(project.repoRoot, request.body);
|
|
14
|
+
});
|
|
15
|
+
app.post("/api/translation/codex/session/start", async (request) => {
|
|
16
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
17
|
+
return deps.sessionService.startProjectTranslatorSession(project.repoRoot, request.body);
|
|
18
|
+
});
|
|
19
|
+
app.post("/api/translation/codex/session/resume", async (request) => {
|
|
20
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
21
|
+
return deps.sessionService.resumeProjectTranslatorSession(project.repoRoot, request.body);
|
|
22
|
+
});
|
|
23
|
+
app.post("/api/translation/codex/session/restart", async (request) => {
|
|
24
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
25
|
+
return deps.sessionService.restartProjectTranslatorSession(project.repoRoot, request.body);
|
|
26
|
+
});
|
|
27
|
+
app.post("/api/translation/codex/session/stop", async () => {
|
|
28
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
29
|
+
return deps.sessionService.stopProjectTranslatorSession(project.repoRoot);
|
|
30
|
+
});
|
|
7
31
|
app.get("/api/translation/codex/source-files", async (request) => {
|
|
8
32
|
const project = await requireCurrentProject(deps.projectService);
|
|
9
33
|
return deps.codexTranslationService.browseSourceFiles(project.repoRoot, {
|
package/dist/backend/server.js
CHANGED
|
@@ -73,7 +73,8 @@ export async function createServer(deps, options = {}) {
|
|
|
73
73
|
});
|
|
74
74
|
registerCodexTranslationRoutes(app, {
|
|
75
75
|
projectService: deps.projectService,
|
|
76
|
-
codexTranslationService: deps.codexTranslationService
|
|
76
|
+
codexTranslationService: deps.codexTranslationService,
|
|
77
|
+
sessionService: deps.sessionService
|
|
77
78
|
});
|
|
78
79
|
registerProjectRoutes(app, {
|
|
79
80
|
projectService: deps.projectService,
|
|
@@ -18,6 +18,13 @@ export function createCodexHookService(deps) {
|
|
|
18
18
|
statusCode: 409
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
+
if (input.role === "codex-translator") {
|
|
22
|
+
return {
|
|
23
|
+
project,
|
|
24
|
+
config: undefined,
|
|
25
|
+
taskRepoRoot: undefined
|
|
26
|
+
};
|
|
27
|
+
}
|
|
21
28
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
22
29
|
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
23
30
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -37,16 +44,30 @@ export function createCodexHookService(deps) {
|
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
const context = await getHookContext(input);
|
|
40
|
-
const session =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const session = input.role === "codex-translator"
|
|
48
|
+
? await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
|
|
49
|
+
eventName,
|
|
50
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
51
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
52
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
53
|
+
})
|
|
54
|
+
: await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
55
|
+
taskSlug: input.taskSlug,
|
|
56
|
+
role: input.role,
|
|
57
|
+
eventName,
|
|
58
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
59
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
60
|
+
cwd: stringOrUndefined(input.event.cwd),
|
|
61
|
+
allowSessionMismatch: true
|
|
62
|
+
});
|
|
49
63
|
if (input.role === "codex-reviewer") {
|
|
64
|
+
if (!context.config || !context.taskRepoRoot) {
|
|
65
|
+
throw new VcmError({
|
|
66
|
+
code: "CODEX_HOOK_TASK_CONTEXT_MISSING",
|
|
67
|
+
message: "Codex Reviewer hook is missing task context.",
|
|
68
|
+
statusCode: 500
|
|
69
|
+
});
|
|
70
|
+
}
|
|
50
71
|
await deps.roundService.recordRoleTurnEvent({
|
|
51
72
|
repoRoot: context.project.repoRoot,
|
|
52
73
|
stateRepoRoot: context.taskRepoRoot,
|
|
@@ -178,7 +178,7 @@ export function createCodexTranslationService(deps) {
|
|
|
178
178
|
await saveQueue(repoRoot, queue);
|
|
179
179
|
return queued;
|
|
180
180
|
}
|
|
181
|
-
async function dispatchNext(repoRoot
|
|
181
|
+
async function dispatchNext(repoRoot) {
|
|
182
182
|
if (!deps.runtime || !deps.sessionService) {
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
@@ -207,7 +207,7 @@ export function createCodexTranslationService(deps) {
|
|
|
207
207
|
queue.updatedAt = next.updatedAt;
|
|
208
208
|
await saveQueue(repoRoot, queue);
|
|
209
209
|
}
|
|
210
|
-
const session = await ensureTranslatorSession(repoRoot,
|
|
210
|
+
const session = await ensureTranslatorSession(repoRoot, next.targetLanguage);
|
|
211
211
|
await submitTerminalInput(deps.runtime, session.id, batch?.prompt ?? await buildQueuePrompt(repoRoot, next));
|
|
212
212
|
const dispatchedAt = now();
|
|
213
213
|
for (const item of batch?.items ?? [next]) {
|
|
@@ -255,7 +255,7 @@ export function createCodexTranslationService(deps) {
|
|
|
255
255
|
await saveQueue(repoRoot, queue);
|
|
256
256
|
return { items: candidates, prompt };
|
|
257
257
|
}
|
|
258
|
-
async function ensureTranslatorSession(repoRoot,
|
|
258
|
+
async function ensureTranslatorSession(repoRoot, targetLanguage) {
|
|
259
259
|
void targetLanguage;
|
|
260
260
|
if (!deps.sessionService) {
|
|
261
261
|
throw new VcmError({
|
|
@@ -264,17 +264,7 @@ export function createCodexTranslationService(deps) {
|
|
|
264
264
|
statusCode: 500
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
if (existing?.status === "running") {
|
|
269
|
-
return existing;
|
|
270
|
-
}
|
|
271
|
-
if (existing?.claudeSessionId) {
|
|
272
|
-
return deps.sessionService.resumeRoleSession(repoRoot, taskSlug, CODEX_TRANSLATOR_ROLE, {
|
|
273
|
-
model: existing.model,
|
|
274
|
-
effort: existing.effort
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
return deps.sessionService.startRoleSession(repoRoot, taskSlug, CODEX_TRANSLATOR_ROLE, {
|
|
267
|
+
return deps.sessionService.ensureProjectTranslatorSession(repoRoot, {
|
|
278
268
|
model: "gpt-5.5",
|
|
279
269
|
effort: "medium"
|
|
280
270
|
});
|
|
@@ -902,9 +892,7 @@ export function createCodexTranslationService(deps) {
|
|
|
902
892
|
const activeExisting = (await loadRuntimeFileJobs(repoRoot)).find((job) => fileTranslationReplacementKey(job) === replacementKey &&
|
|
903
893
|
isActiveFileTranslationJobStatus(job.status));
|
|
904
894
|
if (activeExisting) {
|
|
905
|
-
|
|
906
|
-
void dispatchNext(repoRoot, input.taskSlug);
|
|
907
|
-
}
|
|
895
|
+
void dispatchNext(repoRoot);
|
|
908
896
|
return activeExisting;
|
|
909
897
|
}
|
|
910
898
|
const timestamp = now();
|
|
@@ -1003,9 +991,7 @@ export function createCodexTranslationService(deps) {
|
|
|
1003
991
|
lastUpdatedAt: timestamp
|
|
1004
992
|
});
|
|
1005
993
|
await deps.fs.writeText(resolveRepoPath(repoRoot, job.reportPath), `# Translation Report\n\nStatus: queued\nJob: ${jobId}\n`);
|
|
1006
|
-
|
|
1007
|
-
void dispatchNext(repoRoot, input.taskSlug);
|
|
1008
|
-
}
|
|
994
|
+
void dispatchNext(repoRoot);
|
|
1009
995
|
return job;
|
|
1010
996
|
},
|
|
1011
997
|
async createBootstrapRun(repoRoot, input) {
|
|
@@ -1062,9 +1048,7 @@ export function createCodexTranslationService(deps) {
|
|
|
1062
1048
|
index.runs = [run, ...index.runs];
|
|
1063
1049
|
index.updatedAt = timestamp;
|
|
1064
1050
|
await saveBootstrapIndex(repoRoot, index);
|
|
1065
|
-
|
|
1066
|
-
void dispatchNext(repoRoot, input.taskSlug);
|
|
1067
|
-
}
|
|
1051
|
+
void dispatchNext(repoRoot);
|
|
1068
1052
|
return run;
|
|
1069
1053
|
},
|
|
1070
1054
|
async createMemoryUpdate(repoRoot, input) {
|
|
@@ -1120,9 +1104,7 @@ export function createCodexTranslationService(deps) {
|
|
|
1120
1104
|
"Delete or merge stale, duplicate, temporary, and task-local content."
|
|
1121
1105
|
]
|
|
1122
1106
|
});
|
|
1123
|
-
|
|
1124
|
-
void dispatchNext(repoRoot, input.taskSlug);
|
|
1125
|
-
}
|
|
1107
|
+
void dispatchNext(repoRoot);
|
|
1126
1108
|
return queueItem;
|
|
1127
1109
|
},
|
|
1128
1110
|
async readFileJobOutput(repoRoot, jobId) {
|
|
@@ -1156,14 +1138,12 @@ export function createCodexTranslationService(deps) {
|
|
|
1156
1138
|
});
|
|
1157
1139
|
}
|
|
1158
1140
|
const timestamp = now();
|
|
1159
|
-
const jobId = `conversation-${
|
|
1160
|
-
const jobRoot = `${CONVERSATION_RUNTIME_DIR}
|
|
1141
|
+
const jobId = `conversation-${Date.now()}-${createId().slice(0, 8)}`;
|
|
1142
|
+
const jobRoot = `${CONVERSATION_RUNTIME_DIR}/jobs/${jobId}`;
|
|
1161
1143
|
const sourceHash = `sha256:${sha256(sourceText)}`;
|
|
1162
1144
|
const targetLanguage = input.targetLanguage.trim() || "zh-CN";
|
|
1163
1145
|
const job = {
|
|
1164
1146
|
id: jobId,
|
|
1165
|
-
taskSlug: input.taskSlug,
|
|
1166
|
-
role: input.role,
|
|
1167
1147
|
direction: input.direction,
|
|
1168
1148
|
sourceHash,
|
|
1169
1149
|
sourceLanguage: input.sourceLanguage.trim() || "auto",
|
|
@@ -1187,15 +1167,10 @@ export function createCodexTranslationService(deps) {
|
|
|
1187
1167
|
version: 1,
|
|
1188
1168
|
baseRepoRoot: repoRoot,
|
|
1189
1169
|
pathBase: "baseRepoRoot",
|
|
1190
|
-
job,
|
|
1191
|
-
taskSlug: input.taskSlug,
|
|
1192
|
-
role: input.role,
|
|
1193
1170
|
direction: input.direction,
|
|
1194
|
-
sourceHash,
|
|
1195
1171
|
sourceLanguage: job.sourceLanguage,
|
|
1196
1172
|
targetLanguage,
|
|
1197
1173
|
translationProfile: input.translationProfile?.trim() || DEFAULT_PROFILE,
|
|
1198
|
-
contextText: input.contextText,
|
|
1199
1174
|
sourceContentBoundary: "VCM_TEXT",
|
|
1200
1175
|
sourceText,
|
|
1201
1176
|
absolutePaths: {
|
|
@@ -1208,8 +1183,8 @@ export function createCodexTranslationService(deps) {
|
|
|
1208
1183
|
format: "plain-text"
|
|
1209
1184
|
}
|
|
1210
1185
|
});
|
|
1211
|
-
if (
|
|
1212
|
-
void dispatchNext(repoRoot
|
|
1186
|
+
if (!input.deferDispatch) {
|
|
1187
|
+
void dispatchNext(repoRoot);
|
|
1213
1188
|
}
|
|
1214
1189
|
return job;
|
|
1215
1190
|
},
|
|
@@ -1290,6 +1265,7 @@ export function createCodexTranslationService(deps) {
|
|
|
1290
1265
|
return job;
|
|
1291
1266
|
},
|
|
1292
1267
|
async handleCodexHook(repoRoot, eventName, taskSlug) {
|
|
1268
|
+
void taskSlug;
|
|
1293
1269
|
if (eventName === "UserPromptSubmit") {
|
|
1294
1270
|
const queue = await loadQueue(repoRoot);
|
|
1295
1271
|
const active = queue.activeItemId
|
|
@@ -1306,10 +1282,11 @@ export function createCodexTranslationService(deps) {
|
|
|
1306
1282
|
}
|
|
1307
1283
|
if (eventName === "Stop") {
|
|
1308
1284
|
await validateActiveQueueItem(repoRoot);
|
|
1309
|
-
|
|
1310
|
-
await dispatchNext(repoRoot, taskSlug);
|
|
1311
|
-
}
|
|
1285
|
+
await dispatchNext(repoRoot);
|
|
1312
1286
|
}
|
|
1287
|
+
},
|
|
1288
|
+
ensureTranslatorSession(repoRoot) {
|
|
1289
|
+
return ensureTranslatorSession(repoRoot, "zh-CN");
|
|
1313
1290
|
}
|
|
1314
1291
|
};
|
|
1315
1292
|
}
|
|
@@ -12,8 +12,9 @@ const CODEX_REVIEW_DIR = ".ai/vcm/codex-reviews";
|
|
|
12
12
|
const CODEX_CONFIG_PATH = ".ai/codex/config.toml";
|
|
13
13
|
const CODEX_TRANSLATOR_DIR = ".ai/codex-translator";
|
|
14
14
|
const CODEX_TRANSLATION_DIR = ".ai/vcm/translations";
|
|
15
|
-
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/
|
|
15
|
+
const CODEX_TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
16
16
|
const CODEX_TRANSLATOR_CONFIG_PATH = ".ai/codex-translator/config.toml";
|
|
17
|
+
const PROJECT_TRANSLATOR_SCOPE = "__project__";
|
|
17
18
|
export function createSessionService(deps) {
|
|
18
19
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
19
20
|
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
@@ -104,14 +105,167 @@ export function createSessionService(deps) {
|
|
|
104
105
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
105
106
|
return record;
|
|
106
107
|
}
|
|
108
|
+
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
109
|
+
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
110
|
+
if (live && live.status === "running") {
|
|
111
|
+
return live;
|
|
112
|
+
}
|
|
113
|
+
const persisted = await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
|
|
114
|
+
const model = normalizeCodexModel(input.model ?? persisted?.model);
|
|
115
|
+
const effort = normalizeCodexEffort(input.effort ?? persisted?.effort);
|
|
116
|
+
const claudeSessionId = launchMode === "resume"
|
|
117
|
+
? persisted?.claudeSessionId
|
|
118
|
+
: randomUUID();
|
|
119
|
+
if (!claudeSessionId) {
|
|
120
|
+
throw new VcmError({
|
|
121
|
+
code: "CODEX_TRANSLATOR_SESSION_MISSING",
|
|
122
|
+
message: "Codex Translator does not have a session id to resume.",
|
|
123
|
+
statusCode: 409,
|
|
124
|
+
hint: "Start the translator once before using Resume."
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const startCommand = await buildCodexStartCommand(deps.fs, repoRoot, repoRoot, CODEX_TRANSLATOR_ROLE, launchMode, model, effort, deps.sandboxMode, launchMode === "resume" && hasCapturedCodexSession(persisted) ? claudeSessionId : undefined);
|
|
128
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
129
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
130
|
+
role: CODEX_TRANSLATOR_ROLE,
|
|
131
|
+
command: startCommand.command,
|
|
132
|
+
args: startCommand.args,
|
|
133
|
+
cwd: startCommand.cwd,
|
|
134
|
+
env: {
|
|
135
|
+
VCM_API_URL: deps.apiUrl,
|
|
136
|
+
VCM_TASK_REPO_ROOT: repoRoot,
|
|
137
|
+
VCM_TASK_SLUG: PROJECT_TRANSLATOR_SCOPE,
|
|
138
|
+
VCM_ROLE: CODEX_TRANSLATOR_ROLE,
|
|
139
|
+
VCM_SESSION_ID: claudeSessionId
|
|
140
|
+
},
|
|
141
|
+
cols: input.cols,
|
|
142
|
+
rows: input.rows
|
|
143
|
+
});
|
|
144
|
+
const timestamp = now();
|
|
145
|
+
const record = {
|
|
146
|
+
id: runtimeSession.id,
|
|
147
|
+
claudeSessionId,
|
|
148
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
149
|
+
role: CODEX_TRANSLATOR_ROLE,
|
|
150
|
+
status: runtimeSession.status,
|
|
151
|
+
activityStatus: "idle",
|
|
152
|
+
command: startCommand.display,
|
|
153
|
+
permissionMode: "default",
|
|
154
|
+
model,
|
|
155
|
+
effort,
|
|
156
|
+
cwd: startCommand.cwd,
|
|
157
|
+
terminalBackend: "node-pty",
|
|
158
|
+
pid: runtimeSession.pid,
|
|
159
|
+
startedAt: runtimeSession.startedAt,
|
|
160
|
+
updatedAt: timestamp,
|
|
161
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
162
|
+
exitCode: runtimeSession.exitCode
|
|
163
|
+
};
|
|
164
|
+
deps.registry.upsert(record);
|
|
165
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, record);
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
107
168
|
return {
|
|
169
|
+
startProjectTranslatorSession(repoRoot, input = {}) {
|
|
170
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
171
|
+
},
|
|
172
|
+
resumeProjectTranslatorSession(repoRoot, input = {}) {
|
|
173
|
+
return launchProjectTranslatorSession(repoRoot, input, "resume");
|
|
174
|
+
},
|
|
175
|
+
async stopProjectTranslatorSession(repoRoot) {
|
|
176
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
177
|
+
if (!existing) {
|
|
178
|
+
throw new VcmError({
|
|
179
|
+
code: "SESSION_MISSING",
|
|
180
|
+
message: "Codex Translator session has not been started.",
|
|
181
|
+
statusCode: 404
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
185
|
+
await deps.runtime.stop(existing.id);
|
|
186
|
+
}
|
|
187
|
+
const updated = {
|
|
188
|
+
...existing,
|
|
189
|
+
status: "exited",
|
|
190
|
+
activityStatus: "idle",
|
|
191
|
+
updatedAt: now()
|
|
192
|
+
};
|
|
193
|
+
deps.registry.upsert(updated);
|
|
194
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
|
|
195
|
+
return updated;
|
|
196
|
+
},
|
|
197
|
+
async restartProjectTranslatorSession(repoRoot, input = {}) {
|
|
198
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
199
|
+
if (!existing) {
|
|
200
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
201
|
+
}
|
|
202
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
203
|
+
await deps.runtime.stop(existing.id);
|
|
204
|
+
}
|
|
205
|
+
deps.registry.remove(existing.id);
|
|
206
|
+
return launchProjectTranslatorSession(repoRoot, input, "fresh");
|
|
207
|
+
},
|
|
208
|
+
async getProjectTranslatorSession(repoRoot) {
|
|
209
|
+
const record = getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
|
|
210
|
+
?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot);
|
|
211
|
+
return toRoleSessionRecordView(record, deps.runtime);
|
|
212
|
+
},
|
|
213
|
+
async ensureProjectTranslatorSession(repoRoot, input = {}) {
|
|
214
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
215
|
+
if (existing?.status === "running") {
|
|
216
|
+
return existing;
|
|
217
|
+
}
|
|
218
|
+
if (existing?.claudeSessionId) {
|
|
219
|
+
return this.resumeProjectTranslatorSession(repoRoot, {
|
|
220
|
+
model: input.model ?? existing.model,
|
|
221
|
+
effort: input.effort ?? existing.effort,
|
|
222
|
+
cols: input.cols,
|
|
223
|
+
rows: input.rows
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return this.startProjectTranslatorSession(repoRoot, input);
|
|
227
|
+
},
|
|
228
|
+
async recordProjectTranslatorHookEvent(repoRoot, input) {
|
|
229
|
+
const current = await this.getProjectTranslatorSession(repoRoot);
|
|
230
|
+
if (!current) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const timestamp = now();
|
|
234
|
+
const isStop = input.eventName === "Stop";
|
|
235
|
+
const updated = {
|
|
236
|
+
...current,
|
|
237
|
+
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
238
|
+
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
239
|
+
cwd: input.cwd ?? current.cwd,
|
|
240
|
+
activityStatus: isStop ? "idle" : "running",
|
|
241
|
+
lastHookEventAt: timestamp,
|
|
242
|
+
lastTurnEndedAt: isStop ? timestamp : current.lastTurnEndedAt,
|
|
243
|
+
lastTurnStartedAt: isStop ? current.lastTurnStartedAt : timestamp,
|
|
244
|
+
updatedAt: timestamp
|
|
245
|
+
};
|
|
246
|
+
deps.registry.upsert(updated);
|
|
247
|
+
await persistCodexTranslatorSession(deps.fs, repoRoot, updated);
|
|
248
|
+
return updated;
|
|
249
|
+
},
|
|
108
250
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
251
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
252
|
+
void taskSlug;
|
|
253
|
+
return this.startProjectTranslatorSession(repoRoot, input);
|
|
254
|
+
}
|
|
109
255
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
110
256
|
},
|
|
111
257
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
258
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
259
|
+
void taskSlug;
|
|
260
|
+
return this.resumeProjectTranslatorSession(repoRoot, input);
|
|
261
|
+
}
|
|
112
262
|
return launchRoleSession(repoRoot, taskSlug, role, input, "resume");
|
|
113
263
|
},
|
|
114
264
|
async stopRoleSession(repoRoot, taskSlug, role) {
|
|
265
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
266
|
+
void taskSlug;
|
|
267
|
+
return this.stopProjectTranslatorSession(repoRoot);
|
|
268
|
+
}
|
|
115
269
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
116
270
|
if (!existing) {
|
|
117
271
|
throw new VcmError({
|
|
@@ -136,6 +290,10 @@ export function createSessionService(deps) {
|
|
|
136
290
|
return updated;
|
|
137
291
|
},
|
|
138
292
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
293
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
294
|
+
void taskSlug;
|
|
295
|
+
return this.restartProjectTranslatorSession(repoRoot, input);
|
|
296
|
+
}
|
|
139
297
|
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
140
298
|
if (!existing) {
|
|
141
299
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
@@ -147,6 +305,10 @@ export function createSessionService(deps) {
|
|
|
147
305
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
148
306
|
},
|
|
149
307
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
308
|
+
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
309
|
+
void taskSlug;
|
|
310
|
+
return this.getProjectTranslatorSession(repoRoot);
|
|
311
|
+
}
|
|
150
312
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
151
313
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
152
314
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -163,12 +325,9 @@ export function createSessionService(deps) {
|
|
|
163
325
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
164
326
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
165
327
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
166
|
-
for (const role of ROLE_NAMES) {
|
|
167
|
-
const record = role
|
|
168
|
-
|
|
169
|
-
?? await loadPersistedCodexTranslatorSession(deps.fs, repoRoot, taskSlug)
|
|
170
|
-
: deps.registry.getByRole(taskSlug, role)
|
|
171
|
-
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
328
|
+
for (const role of ROLE_NAMES.filter((candidate) => candidate !== CODEX_TRANSLATOR_ROLE)) {
|
|
329
|
+
const record = deps.registry.getByRole(taskSlug, role)
|
|
330
|
+
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
172
331
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
|
173
332
|
if (session) {
|
|
174
333
|
sessions.push(session);
|
|
@@ -177,6 +336,15 @@ export function createSessionService(deps) {
|
|
|
177
336
|
return sessions;
|
|
178
337
|
},
|
|
179
338
|
async recordRoleHookEvent(repoRoot, input) {
|
|
339
|
+
if (input.role === CODEX_TRANSLATOR_ROLE) {
|
|
340
|
+
void input.taskSlug;
|
|
341
|
+
return this.recordProjectTranslatorHookEvent(repoRoot, {
|
|
342
|
+
eventName: input.eventName,
|
|
343
|
+
sessionId: input.sessionId,
|
|
344
|
+
transcriptPath: input.transcriptPath,
|
|
345
|
+
cwd: input.cwd
|
|
346
|
+
});
|
|
347
|
+
}
|
|
180
348
|
const current = await this.getRoleSession(repoRoot, input.taskSlug, input.role);
|
|
181
349
|
if (!current || (!input.allowSessionMismatch && !matchesRoleHookSession(current, input))) {
|
|
182
350
|
return undefined;
|
|
@@ -385,6 +553,11 @@ function getRegisteredRoleSession(registry, runtime, taskSlug, role) {
|
|
|
385
553
|
?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
386
554
|
return scopeProjectRoleSession(scoped, taskSlug);
|
|
387
555
|
}
|
|
556
|
+
function getRegisteredProjectTranslatorSession(registry, runtime) {
|
|
557
|
+
const candidates = registry.list().filter((session) => session.role === CODEX_TRANSLATOR_ROLE);
|
|
558
|
+
const live = candidates.find((session) => runtime.getSession(session.id)?.status === "running");
|
|
559
|
+
return live ?? candidates.sort(compareSessionUpdatedAtDesc)[0];
|
|
560
|
+
}
|
|
388
561
|
function compareSessionUpdatedAtDesc(left, right) {
|
|
389
562
|
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
|
390
563
|
}
|
|
@@ -399,11 +572,12 @@ function scopeProjectRoleSession(record, taskSlug) {
|
|
|
399
572
|
}
|
|
400
573
|
async function loadPersistedRoleRecordForRole(fs, baseRepoRoot, taskRepoRoot, stateRoot, taskSlug, role) {
|
|
401
574
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
402
|
-
|
|
575
|
+
void taskSlug;
|
|
576
|
+
return loadPersistedCodexTranslatorSession(fs, baseRepoRoot);
|
|
403
577
|
}
|
|
404
578
|
return loadPersistedRoleRecord(fs, taskRepoRoot, stateRoot, taskSlug, role);
|
|
405
579
|
}
|
|
406
|
-
async function loadPersistedCodexTranslatorSession(fs, repoRoot
|
|
580
|
+
async function loadPersistedCodexTranslatorSession(fs, repoRoot) {
|
|
407
581
|
const sessionPath = resolveRepoPath(repoRoot, CODEX_TRANSLATOR_SESSION_PATH);
|
|
408
582
|
if (!(await fs.pathExists(sessionPath))) {
|
|
409
583
|
return undefined;
|
|
@@ -413,7 +587,10 @@ async function loadPersistedCodexTranslatorSession(fs, repoRoot, taskSlug) {
|
|
|
413
587
|
if (!record) {
|
|
414
588
|
return undefined;
|
|
415
589
|
}
|
|
416
|
-
return
|
|
590
|
+
return {
|
|
591
|
+
...record,
|
|
592
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
593
|
+
};
|
|
417
594
|
}
|
|
418
595
|
async function loadPersistedRoleRecord(fs, repoRoot, stateRoot, taskSlug, role) {
|
|
419
596
|
const current = await loadPersistedTaskSessionRecord(fs, repoRoot, stateRoot, taskSlug);
|
|
@@ -486,7 +663,7 @@ async function persistCodexTranslatorSession(fs, repoRoot, session) {
|
|
|
486
663
|
updatedAt: session.updatedAt,
|
|
487
664
|
record: {
|
|
488
665
|
...session,
|
|
489
|
-
taskSlug:
|
|
666
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
490
667
|
}
|
|
491
668
|
});
|
|
492
669
|
}
|
|
@@ -157,6 +157,8 @@ export function createTranslationService(deps) {
|
|
|
157
157
|
}
|
|
158
158
|
function startTranscriptTail(roleSession) {
|
|
159
159
|
const state = getState(roleSession.id);
|
|
160
|
+
state.taskSlug = roleSession.taskSlug;
|
|
161
|
+
state.role = roleSession.role;
|
|
160
162
|
if (state.unsubscribeTranscript) {
|
|
161
163
|
return;
|
|
162
164
|
}
|
|
@@ -183,7 +185,7 @@ export function createTranslationService(deps) {
|
|
|
183
185
|
const config = await loadConfig();
|
|
184
186
|
let displayed = false;
|
|
185
187
|
if (event.kind === "text") {
|
|
186
|
-
const shouldTranslate =
|
|
188
|
+
const shouldTranslate = shouldTranslateTextTranscriptEvent(state, event, config);
|
|
187
189
|
displayed = shouldTranslate
|
|
188
190
|
? processClaudeOutputText(sessionId, event.text, config, event.id, {
|
|
189
191
|
flushImmediately: event.stopReason === "end_turn"
|
|
@@ -206,6 +208,18 @@ export function createTranslationService(deps) {
|
|
|
206
208
|
state.seenTranscriptIds.add(event.id);
|
|
207
209
|
}
|
|
208
210
|
}
|
|
211
|
+
function shouldTranslateTextTranscriptEvent(state, event, config) {
|
|
212
|
+
if (config.outputMode === "all") {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (event.stopReason !== "end_turn") {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (config.outputMode === "pm-final-only") {
|
|
219
|
+
return state.role === "project-manager";
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
209
223
|
function processClaudeOutputText(sessionId, rawText, config, entryId, options = {}) {
|
|
210
224
|
return startClaudeOutputTranslation(sessionId, rawText, config, {
|
|
211
225
|
entryId,
|
|
@@ -890,8 +904,6 @@ export function createTranslationService(deps) {
|
|
|
890
904
|
});
|
|
891
905
|
}
|
|
892
906
|
return deps.codexTranslationService.createConversationJob(input.repoRoot, {
|
|
893
|
-
taskSlug: input.taskSlug,
|
|
894
|
-
role: input.role,
|
|
895
907
|
direction: input.direction,
|
|
896
908
|
sourceText: input.text,
|
|
897
909
|
sourceLanguage: input.sourceLanguage,
|
|
@@ -921,7 +933,6 @@ export function createTranslationService(deps) {
|
|
|
921
933
|
}
|
|
922
934
|
try {
|
|
923
935
|
return await deps.codexTranslationService.validateConversationResult(repoRoot, {
|
|
924
|
-
taskSlug: job.taskSlug,
|
|
925
936
|
resultPath: job.resultPath,
|
|
926
937
|
sourceHash: job.sourceHash,
|
|
927
938
|
targetLanguage: job.targetLanguage
|
|
@@ -15,9 +15,9 @@ Task-specific context and coder guidance go here, not in source-code comments.
|
|
|
15
15
|
Source-code comments should only describe durable behavior, contracts, invariants,
|
|
16
16
|
error boundaries, or non-obvious logic that should remain useful after this task.
|
|
17
17
|
|
|
18
|
-
| File | Action | Task Context | Durable Code Comment Needed | Coder Work | Allowed Freedom | VCM:CODE | Proof Point | Replan Trigger |
|
|
19
|
-
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
20
|
-
| TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
|
|
18
|
+
| ID | File | Action | Task Context | Durable Code Comment Needed | Coder Work | Allowed Freedom | VCM:CODE | Proof Point | Replan Trigger |
|
|
19
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
20
|
+
| SCF-001 | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD | TBD |
|
|
21
21
|
|
|
22
22
|
## Implementation Plan
|
|
23
23
|
|
|
@@ -27,7 +27,8 @@ export function renderArchitectHarnessRules() {
|
|
|
27
27
|
|
|
28
28
|
- Define the expected implementation scope: affected modules, changed or created files, each file's responsibility, why it is in scope, and user-visible behavior changes.
|
|
29
29
|
- Define every non-private callable surface intended for use outside its file: visibility, signature shape, responsibility, expected callers, behavior contract, side effects, and error boundaries.
|
|
30
|
-
- Include a \`Scaffold Manifest\` for task-specific file context: file action, why the file is in scope, coder work, allowed implementation freedom, expected \`VCM:CODE\` placeholders, durable code comment needs, proof points, and Replan triggers.
|
|
30
|
+
- Include a \`Scaffold Manifest\` for task-specific file context: stable row ID, file action, why the file is in scope, coder work, allowed implementation freedom, expected \`VCM:CODE\` placeholders, durable code comment needs, proof points, and Replan triggers.
|
|
31
|
+
- Give each Scaffold Manifest row a stable ID such as \`SCF-001\`; use that ID in any related \`VCM:CODE\` marker so coder can report completion by ID.
|
|
31
32
|
- Put task context, phase notes, handoff instructions, temporary rationale, and coder guidance in the \`Scaffold Manifest\`, not in source-code comments.
|
|
32
33
|
- Cover architecture docs impact, known risks, and Replan triggers.
|
|
33
34
|
- For docs impact, state whether changes belong in \`docs/ARCHITECTURE.md\`, affected \`<module>/ARCHITECTURE.md\`, \`.ai/generated/public-surface.json\`, or no durable architecture doc.
|
|
@@ -41,7 +42,7 @@ export function renderArchitectHarnessRules() {
|
|
|
41
42
|
- Define every new or changed non-private callable surface directly in code with its signature shape and contract comment.
|
|
42
43
|
- When changing an existing non-private callable surface, update its signature and contract comment in code before coder work starts; leave \`VCM:CODE\` only where implementation must change.
|
|
43
44
|
- Non-private callable surface includes any function, method, type, trait, enum, constant, re-export, or similar symbol that another file can call or depend on.
|
|
44
|
-
- Mark incomplete implementation bodies with \`VCM:CODE
|
|
45
|
+
- Mark incomplete implementation bodies with \`VCM:CODE <Scaffold Manifest ID>\`; coder must implement them and remove the markers before handoff.
|
|
45
46
|
- Architect scaffolding may include modules, files, signatures, type shapes, durable comments, and placeholder bodies, but not real business implementation beyond minimal scaffold code.
|
|
46
47
|
- Coder may add private implementation helpers, but must not add or change cross-file callable surface without architect replan.
|
|
47
48
|
|
|
@@ -10,7 +10,7 @@ export function renderCoderHarnessRules() {
|
|
|
10
10
|
### Coder Implementation Discipline
|
|
11
11
|
|
|
12
12
|
- Implement the architect-defined scaffold exactly; do not change file responsibilities, callable-surface signatures, or architect-defined contract intent unless the architecture plan explicitly allows it.
|
|
13
|
-
- Implement every \`VCM:CODE\` placeholder and remove all \`VCM:CODE\` markers before handoff.
|
|
13
|
+
- Implement every \`VCM:CODE\` placeholder, track completion by Scaffold Manifest ID when present, and remove all \`VCM:CODE\` markers before handoff.
|
|
14
14
|
- Do not fake completion: no hardcoded success, disabled logic, swallowed errors, test-only shortcuts, or silent fallback that hides failure.
|
|
15
15
|
- Keep the diff inside approved scope: no unrelated rewrites, drive-by refactors, renamed symbols, moved files, or formatting churn.
|
|
16
16
|
- Preserve existing behavior unless the architecture plan explicitly changes it; keep existing call sites and shared code paths working.
|
|
@@ -40,6 +40,11 @@ export function renderCoderHarnessRules() {
|
|
|
40
40
|
- Do not weaken, delete, or skip tests to make validation pass.
|
|
41
41
|
- Record confirmed out-of-scope issues found during implementation in \`.ai/vcm/handoffs/known-issues.md\`.
|
|
42
42
|
|
|
43
|
+
### Handoff
|
|
44
|
+
|
|
45
|
+
- In the route message back to project-manager, include a \`Scaffold Completion\` section when the architecture plan contains a Scaffold Manifest.
|
|
46
|
+
- The \`Scaffold Completion\` section must report completed Scaffold Manifest IDs or \`VCM:CODE\` IDs, remaining markers if any, private helpers added, manifest deviations, and whether Replan is needed.
|
|
47
|
+
|
|
43
48
|
### Generated Context
|
|
44
49
|
|
|
45
50
|
- Regenerate \`.ai/generated/module-index.json\` with \`.ai/tools/generate-module-index\` after module, manifest, source-file, or test-file changes.
|