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 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. If a Round is currently running, the dock also shows the Current Round start time, total elapsed time, role active runtime, and Turn count.
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 Current Round dock separates wall-clock Round duration from role active runtime. `Total` is `now - 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 current Round.
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, {
@@ -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 = await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
41
- taskSlug: input.taskSlug,
42
- role: input.role,
43
- eventName,
44
- sessionId: stringOrUndefined(input.event.session_id),
45
- transcriptPath: stringOrUndefined(input.event.transcript_path),
46
- cwd: stringOrUndefined(input.event.cwd),
47
- allowSessionMismatch: true
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, taskSlug) {
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, taskSlug, next.targetLanguage);
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, taskSlug, targetLanguage) {
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
- const existing = await deps.sessionService.getRoleSession(repoRoot, taskSlug, CODEX_TRANSLATOR_ROLE);
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
- if (input.taskSlug) {
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
- if (input.taskSlug) {
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
- if (input.taskSlug) {
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
- if (input.taskSlug) {
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-${safeId(input.role)}-${Date.now()}-${createId().slice(0, 8)}`;
1160
- const jobRoot = `${CONVERSATION_RUNTIME_DIR}/${safeId(input.taskSlug)}/${safeId(input.role)}/jobs/${jobId}`;
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 (input.taskSlug && !input.deferDispatch) {
1212
- void dispatchNext(repoRoot, input.taskSlug);
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
- if (taskSlug) {
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/runtime/session.json";
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 === CODEX_TRANSLATOR_ROLE
168
- ? getRegisteredRoleSession(deps.registry, deps.runtime, taskSlug, role)
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
- return loadPersistedCodexTranslatorSession(fs, baseRepoRoot, taskSlug);
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, taskSlug) {
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 scopeProjectRoleSession(record, taskSlug);
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: session.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 = config.outputMode === "all" || event.stopReason === "end_turn";
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\`; coder must implement them and remove the markers before handoff.
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.