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.
Files changed (33) hide show
  1. package/README.md +14 -18
  2. package/dist/backend/api/project-routes.js +1 -1
  3. package/dist/backend/api/task-routes.js +2 -2
  4. package/dist/backend/api/{codex-translation-routes.js → translation-worker-routes.js} +21 -21
  5. package/dist/backend/cli/install-vcm-harness.js +30 -25
  6. package/dist/backend/gateway/gateway-service.js +24 -5
  7. package/dist/backend/server.js +12 -22
  8. package/dist/backend/services/claude-hook-service.js +93 -26
  9. package/dist/backend/services/harness-service.js +49 -29
  10. package/dist/backend/services/message-service.js +9 -6
  11. package/dist/backend/services/project-service.js +2 -2
  12. package/dist/backend/services/session-service.js +99 -172
  13. package/dist/backend/services/translation-service.js +17 -17
  14. package/dist/backend/services/{codex-translation-service.js → translation-worker-service.js} +11 -11
  15. package/dist/backend/templates/harness/gate-review.js +2 -58
  16. package/dist/shared/constants.js +19 -12
  17. package/dist/shared/types/app-settings.js +1 -1
  18. package/dist/shared/types/session.js +2 -14
  19. package/dist-frontend/assets/{index-Cfum1Prr.css → index-BvUHLq4X.css} +1 -1
  20. package/dist-frontend/assets/{index-x9I-bGUt.js → index-D0ezB4na.js} +36 -36
  21. package/dist-frontend/index.html +2 -2
  22. package/docs/{codex-translation-plan.md → claude-code-translation-plan.md} +151 -153
  23. package/docs/full-harness-baseline.md +3 -7
  24. package/docs/gate-review-gates.md +13 -5
  25. package/docs/gateway-design.md +1 -1
  26. package/docs/product-design.md +12 -11
  27. package/docs/v0.4-custom-workflow-plan.md +0 -1
  28. package/docs/vcm-cc-best-practices.md +2 -5
  29. package/package.json +1 -1
  30. package/dist/backend/api/codex-hook-routes.js +0 -9
  31. package/dist/backend/codex-sandbox-mode.js +0 -28
  32. package/dist/backend/services/codex-hook-service.js +0 -71
  33. package/dist/shared/types/codex-hook.js +0 -1
@@ -1,4 +1,4 @@
1
- import { isVcmRoleName } from "../../shared/constants.js";
1
+ import { isGateReviewerRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
3
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
4
4
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
@@ -22,15 +22,62 @@ export function createClaudeHookService(deps) {
22
22
  });
23
23
  }
24
24
  const config = await deps.projectService.loadConfig(project.repoRoot);
25
- const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
25
+ const taskSlug = await resolveHookTaskSlug(project.repoRoot, input);
26
+ const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
26
27
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
27
28
  return {
28
29
  project,
29
30
  config,
30
31
  task,
32
+ taskSlug,
31
33
  taskRepoRoot
32
34
  };
33
35
  }
36
+ async function getTranslatorHookContext() {
37
+ const project = await deps.projectService.getCurrentProject();
38
+ if (!project) {
39
+ throw new VcmError({
40
+ code: "PROJECT_NOT_CONNECTED",
41
+ message: "Connect a repository before accepting Translator hooks.",
42
+ statusCode: 409
43
+ });
44
+ }
45
+ return { project };
46
+ }
47
+ async function processTranslatorHook(input) {
48
+ const eventName = parseHookEvent(input.event.hook_event_name);
49
+ const context = await getTranslatorHookContext();
50
+ const session = await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
51
+ eventName,
52
+ sessionId: stringOrUndefined(input.event.session_id),
53
+ transcriptPath: stringOrUndefined(input.event.transcript_path),
54
+ cwd: stringOrUndefined(input.event.cwd)
55
+ });
56
+ await deps.translationWorkerService?.handleTranslatorHook(context.project.repoRoot, eventName, input.taskSlug);
57
+ return {
58
+ ok: true,
59
+ eventName,
60
+ taskSlug: input.taskSlug,
61
+ role: input.role,
62
+ sessionUpdated: Boolean(session),
63
+ dispatchedCount: 0
64
+ };
65
+ }
66
+ async function resolveHookTaskSlug(repoRoot, input) {
67
+ if (!isGateReviewerRoleName(input.role)) {
68
+ return input.taskSlug;
69
+ }
70
+ const session = await deps.sessionService.getProjectGateReviewerSession(repoRoot);
71
+ if (session?.activeTaskSlug) {
72
+ return session.activeTaskSlug;
73
+ }
74
+ throw new VcmError({
75
+ code: "GATE_REVIEWER_TASK_UNBOUND",
76
+ message: "Gate Reviewer hook arrived without an active task binding.",
77
+ statusCode: 409,
78
+ hint: "Start or resume Gate Reviewer from the current task before submitting work."
79
+ });
80
+ }
34
81
  async function handleUserPromptSubmitHook(input) {
35
82
  const eventName = parseHookEvent(input.event.hook_event_name);
36
83
  if (eventName !== "UserPromptSubmit") {
@@ -39,11 +86,11 @@ export function createClaudeHookService(deps) {
39
86
  const context = await getHookContext(input);
40
87
  deps.jobGuard?.notePromptSubmitted({
41
88
  repoRoot: context.project.repoRoot,
42
- taskSlug: input.taskSlug,
89
+ taskSlug: context.taskSlug,
43
90
  role: input.role
44
91
  });
45
92
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
46
- taskSlug: input.taskSlug,
93
+ taskSlug: context.taskSlug,
47
94
  role: input.role,
48
95
  eventName,
49
96
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -54,7 +101,7 @@ export function createClaudeHookService(deps) {
54
101
  repoRoot: context.project.repoRoot,
55
102
  stateRepoRoot: context.taskRepoRoot,
56
103
  stateRoot: context.config.stateRoot,
57
- taskSlug: input.taskSlug,
104
+ taskSlug: context.taskSlug,
58
105
  role: input.role,
59
106
  eventName
60
107
  });
@@ -62,7 +109,7 @@ export function createClaudeHookService(deps) {
62
109
  await deps.translationService.recordConversationBoundary({
63
110
  repoRoot: context.project.repoRoot,
64
111
  taskRepoRoot: context.taskRepoRoot,
65
- taskSlug: input.taskSlug,
112
+ taskSlug: context.taskSlug,
66
113
  role: input.role,
67
114
  sessionId: session.id,
68
115
  boundaryKind: "start",
@@ -75,14 +122,14 @@ export function createClaudeHookService(deps) {
75
122
  stateRepoRoot: context.taskRepoRoot,
76
123
  stateRoot: context.config.stateRoot,
77
124
  handoffDir: context.task.handoffDir,
78
- taskSlug: input.taskSlug,
125
+ taskSlug: context.taskSlug,
79
126
  role: input.role,
80
127
  prompt: stringOrUndefined(input.event.prompt)
81
128
  });
82
129
  return {
83
130
  ok: true,
84
131
  eventName,
85
- taskSlug: input.taskSlug,
132
+ taskSlug: context.taskSlug,
86
133
  role: input.role,
87
134
  sessionUpdated: Boolean(session),
88
135
  dispatchedCount: 0,
@@ -95,11 +142,11 @@ export function createClaudeHookService(deps) {
95
142
  throwUnsupportedEvent(eventName);
96
143
  }
97
144
  const context = await getHookContext(input);
98
- clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
145
+ clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
99
146
  if (options.allowBlock && deps.jobGuard) {
100
147
  const verdict = await deps.jobGuard.evaluateStop({
101
148
  repoRoot: context.project.repoRoot,
102
- taskSlug: input.taskSlug,
149
+ taskSlug: context.taskSlug,
103
150
  role: input.role,
104
151
  taskRepoRoot: context.taskRepoRoot
105
152
  });
@@ -109,7 +156,7 @@ export function createClaudeHookService(deps) {
109
156
  return {
110
157
  ok: true,
111
158
  eventName,
112
- taskSlug: input.taskSlug,
159
+ taskSlug: context.taskSlug,
113
160
  role: input.role,
114
161
  sessionUpdated: false,
115
162
  dispatchedCount: 0,
@@ -118,7 +165,7 @@ export function createClaudeHookService(deps) {
118
165
  }
119
166
  }
120
167
  return recordTurnEnd(input, context, eventName, {
121
- dispatchRouteFiles: true,
168
+ dispatchRouteFiles: !isGateReviewerRoleName(input.role),
122
169
  notifyGateway: true,
123
170
  settleGuard: true
124
171
  });
@@ -133,9 +180,9 @@ export function createClaudeHookService(deps) {
133
180
  const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
134
181
  const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
135
182
  if (hasCompletionEvidence) {
136
- clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
183
+ clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
137
184
  return recordTurnEnd(input, context, eventName, {
138
- dispatchRouteFiles: true,
185
+ dispatchRouteFiles: !isGateReviewerRoleName(input.role),
139
186
  notifyGateway: false,
140
187
  settleGuard: true
141
188
  });
@@ -145,7 +192,7 @@ export function createClaudeHookService(deps) {
145
192
  return {
146
193
  ok: true,
147
194
  eventName,
148
- taskSlug: input.taskSlug,
195
+ taskSlug: context.taskSlug,
149
196
  role: input.role,
150
197
  sessionUpdated: true,
151
198
  dispatchedCount: 0
@@ -164,7 +211,7 @@ export function createClaudeHookService(deps) {
164
211
  }
165
212
  const context = await getHookContext(input);
166
213
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
167
- taskSlug: input.taskSlug,
214
+ taskSlug: context.taskSlug,
168
215
  role: input.role,
169
216
  eventName,
170
217
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -174,7 +221,7 @@ export function createClaudeHookService(deps) {
174
221
  return {
175
222
  ok: true,
176
223
  eventName,
177
- taskSlug: input.taskSlug,
224
+ taskSlug: context.taskSlug,
178
225
  role: input.role,
179
226
  sessionUpdated: Boolean(session),
180
227
  dispatchedCount: 0
@@ -184,7 +231,7 @@ export function createClaudeHookService(deps) {
184
231
  const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
185
232
  const settleRouteDispatchInput = createRouteDispatchInput(input, context);
186
233
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
187
- taskSlug: input.taskSlug,
234
+ taskSlug: context.taskSlug,
188
235
  role: input.role,
189
236
  eventName,
190
237
  claudeSessionId: stringOrUndefined(input.event.session_id),
@@ -195,7 +242,7 @@ export function createClaudeHookService(deps) {
195
242
  repoRoot: context.project.repoRoot,
196
243
  stateRepoRoot: context.taskRepoRoot,
197
244
  stateRoot: context.config.stateRoot,
198
- taskSlug: input.taskSlug,
245
+ taskSlug: context.taskSlug,
199
246
  role: input.role,
200
247
  eventName,
201
248
  ...(options.settleGuard
@@ -217,7 +264,7 @@ export function createClaudeHookService(deps) {
217
264
  await deps.translationService.recordConversationBoundary({
218
265
  repoRoot: context.project.repoRoot,
219
266
  taskRepoRoot: context.taskRepoRoot,
220
- taskSlug: input.taskSlug,
267
+ taskSlug: context.taskSlug,
221
268
  role: input.role,
222
269
  sessionId: session.id,
223
270
  boundaryKind: "end",
@@ -227,7 +274,7 @@ export function createClaudeHookService(deps) {
227
274
  if (options.notifyGateway && session && input.role === "project-manager") {
228
275
  void deps.gatewayService?.handlePmStop({
229
276
  repoRoot: context.project.repoRoot,
230
- taskSlug: input.taskSlug,
277
+ taskSlug: context.taskSlug,
231
278
  session
232
279
  }).catch(() => undefined);
233
280
  }
@@ -237,7 +284,7 @@ export function createClaudeHookService(deps) {
237
284
  return {
238
285
  ok: true,
239
286
  eventName,
240
- taskSlug: input.taskSlug,
287
+ taskSlug: context.taskSlug,
241
288
  role: input.role,
242
289
  sessionUpdated: Boolean(session),
243
290
  dispatchedCount: dispatched.filter((result) => result.delivered).length
@@ -250,7 +297,7 @@ export function createClaudeHookService(deps) {
250
297
  stateRepoRoot: context.taskRepoRoot,
251
298
  stateRoot: context.config.stateRoot,
252
299
  handoffDir: context.task.handoffDir,
253
- taskSlug: input.taskSlug,
300
+ taskSlug: context.taskSlug,
254
301
  ...(stoppedRole ? { stoppedRole } : {})
255
302
  };
256
303
  }
@@ -258,18 +305,18 @@ export function createClaudeHookService(deps) {
258
305
  if (!deps.runtime) {
259
306
  return false;
260
307
  }
261
- const key = stopFailureRecoveryKey(context.project.repoRoot, input.taskSlug, input.role);
308
+ const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
262
309
  const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
263
310
  if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
264
311
  return false;
265
312
  }
266
- const session = await deps.sessionService.getRoleSession(context.project.repoRoot, input.taskSlug, input.role);
313
+ const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
267
314
  if (!session || session.status !== "running") {
268
315
  return false;
269
316
  }
270
317
  stopFailureRecoveryAttempts.set(key, attempt);
271
318
  await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
272
- await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, input.taskSlug, input.role);
319
+ await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
273
320
  return true;
274
321
  }
275
322
  function clearStopFailureRecovery(repoRoot, taskSlug, role) {
@@ -290,6 +337,20 @@ export function createClaudeHookService(deps) {
290
337
  }
291
338
  async function handlePermissionRequestHook(input) {
292
339
  if (!isVcmRoleName(input.role)) {
340
+ if (isTranslatorToolRoleName(input.role)) {
341
+ const preferences = await deps.appSettings.getPreferences();
342
+ if (preferences.permissionRequestMode !== "allowAll") {
343
+ return undefined;
344
+ }
345
+ return {
346
+ hookSpecificOutput: {
347
+ hookEventName: "PermissionRequest",
348
+ decision: {
349
+ behavior: "allow"
350
+ }
351
+ }
352
+ };
353
+ }
293
354
  throw new VcmError({
294
355
  code: "HOOK_ROLE_INVALID",
295
356
  message: `Unknown hook role: ${input.role}`,
@@ -320,6 +381,9 @@ export function createClaudeHookService(deps) {
320
381
  }
321
382
  return {
322
383
  async handleHook(input) {
384
+ if (isTranslatorToolRoleName(input.role)) {
385
+ return processTranslatorHook(input);
386
+ }
323
387
  const eventName = parseHookEvent(input.event.hook_event_name);
324
388
  if (eventName === "UserPromptSubmit") {
325
389
  return handleUserPromptSubmitHook(input);
@@ -335,6 +399,9 @@ export function createClaudeHookService(deps) {
335
399
  return processStopHook(input, { allowBlock: false });
336
400
  },
337
401
  handleStopHook(input) {
402
+ if (isTranslatorToolRoleName(input.role)) {
403
+ return processTranslatorHook(input);
404
+ }
338
405
  return processStopHook(input, { allowBlock: true });
339
406
  },
340
407
  handlePermissionRequestHook
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
6
6
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
7
- import { renderCodexCliConfigHarnessRules, renderCodexHooksHarnessRules, renderCodexTranslatorAgentsHarnessRules, renderCodexTranslatorConfigHarnessRules, renderGateReviewerAgentRules, renderRequestGateReviewTool, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
7
+ import { renderGateReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
8
8
  import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
9
9
  import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
10
10
  import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
@@ -25,6 +25,12 @@ export const VCM_HARNESS_VERSION = 1;
25
25
  const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
26
26
  const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
27
27
  const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
28
+ const LEGACY_CODEX_HARNESS_PATHS = [
29
+ ".ai/codex",
30
+ ".ai/codex-translator",
31
+ ".claude/skills/vcm-codex-review-gate",
32
+ ".ai/tools/request-codex-review"
33
+ ];
28
34
  const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
29
35
  const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
30
36
  const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
@@ -107,31 +113,11 @@ const HARNESS_FILES = [
107
113
  renderRules: renderGateReviewerAgentRules
108
114
  },
109
115
  {
110
- kind: "codex-translator-agents",
111
- path: ".ai/codex-translator/AGENTS.md",
112
- title: "VCM Codex Translator",
113
- renderRules: renderCodexTranslatorAgentsHarnessRules
114
- },
115
- {
116
- kind: "codex-translator-config",
117
- path: ".ai/codex-translator/config.toml",
118
- title: "VCM Codex Translator Config",
119
- ownership: "raw-file",
120
- renderRules: renderCodexTranslatorConfigHarnessRules
121
- },
122
- {
123
- kind: "codex-translator-cli-config",
124
- path: ".ai/codex-translator/.codex/config.toml",
125
- title: "VCM Codex Translator CLI Config",
126
- ownership: "raw-file",
127
- renderRules: renderCodexCliConfigHarnessRules
128
- },
129
- {
130
- kind: "codex-translator-hooks",
131
- path: ".ai/codex-translator/.codex/hooks.json",
132
- title: "VCM Codex Translator Hooks",
133
- ownership: "raw-file",
134
- renderRules: () => renderCodexHooksHarnessRules("codex-translator")
116
+ kind: "agent-translator",
117
+ path: ".claude/agents/translator.md",
118
+ title: "Translator Agent",
119
+ frontmatter: renderAgentFrontmatter("translator", "VCM project translation tool role for conversation translation, file translation, bootstrap, and memory updates."),
120
+ renderRules: renderTranslatorAgentRules
135
121
  },
136
122
  {
137
123
  kind: "tool-request-gate-review",
@@ -175,7 +161,8 @@ export function createHarnessService(deps) {
175
161
  return {
176
162
  async getHarnessStatus(repoRoot) {
177
163
  const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
178
- return renderHarnessStatus(analyses);
164
+ const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
165
+ return renderHarnessStatus(analyses, legacyChanges);
179
166
  },
180
167
  async applyHarness(repoRoot) {
181
168
  if (deps.runFixedInstaller) {
@@ -192,6 +179,8 @@ export function createHarnessService(deps) {
192
179
  changedFiles.push(analysis.plannedChange);
193
180
  }
194
181
  }
182
+ const legacyChanges = await removeLegacyCodexHarnessPaths(deps.fs, repoRoot);
183
+ changedFiles.push(...legacyChanges);
195
184
  return {
196
185
  version: VCM_HARNESS_VERSION,
197
186
  changedFiles,
@@ -489,11 +478,42 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
489
478
  : currentContent.replace(managedBlockPattern, expectedBlock)
490
479
  };
491
480
  }
492
- function renderHarnessStatus(analyses) {
481
+ async function analyzeLegacyCodexHarnessPaths(fs, repoRoot) {
482
+ const changes = [];
483
+ for (const relativePath of LEGACY_CODEX_HARNESS_PATHS) {
484
+ if (!await fs.pathExists(resolveHarnessPath(repoRoot, relativePath))) {
485
+ continue;
486
+ }
487
+ changes.push({
488
+ path: relativePath,
489
+ action: "delete",
490
+ reason: "Legacy Codex harness path is obsolete; VCM now uses Claude Code Gate Reviewer and Translator roles."
491
+ });
492
+ }
493
+ return changes;
494
+ }
495
+ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
496
+ const changes = await analyzeLegacyCodexHarnessPaths(fs, repoRoot);
497
+ if (changes.length === 0) {
498
+ return [];
499
+ }
500
+ if (!fs.removePath) {
501
+ return [];
502
+ }
503
+ for (const change of changes) {
504
+ await fs.removePath(resolveHarnessPath(repoRoot, change.path), {
505
+ recursive: true,
506
+ force: true
507
+ });
508
+ }
509
+ return changes;
510
+ }
511
+ function renderHarnessStatus(analyses, legacyChanges = []) {
493
512
  const files = analyses.map((analysis) => analysis.status);
494
513
  const plannedChanges = analyses
495
514
  .map((analysis) => analysis.plannedChange)
496
- .filter((change) => Boolean(change));
515
+ .filter((change) => Boolean(change))
516
+ .concat(legacyChanges);
497
517
  // Derive `initialized`: the VCM harness is considered installed when at least one
498
518
  // VCM-exclusive marker is present (per analysis):
499
519
  // - status.hasManagedBlock === true (a managed block already lives in the file), OR
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
- import { VCM_ROLE_NAMES, isVcmRoleName } from "../../shared/constants.js";
3
+ import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
@@ -282,8 +282,8 @@ async function listRouteFiles(fs, input) {
282
282
  return routeFiles;
283
283
  }
284
284
  function parseRouteFileName(fileName) {
285
- for (const fromRole of VCM_ROLE_NAMES) {
286
- for (const toRole of VCM_ROLE_NAMES) {
285
+ for (const fromRole of CORE_VCM_ROLE_NAMES) {
286
+ for (const toRole of CORE_VCM_ROLE_NAMES) {
287
287
  if (fromRole === toRole) {
288
288
  continue;
289
289
  }
@@ -371,14 +371,14 @@ function selectDispatchCandidates(routeFiles, stoppedRole) {
371
371
  });
372
372
  }
373
373
  function validateMessagePolicy(fromRole, toRole, type) {
374
- if (!VCM_ROLE_NAMES.includes(toRole)) {
374
+ if (!CORE_VCM_ROLE_NAMES.some((role) => role === toRole)) {
375
375
  throw new VcmError({
376
376
  code: "MESSAGE_TARGET_UNKNOWN",
377
377
  message: `Unknown target role: ${toRole}`,
378
378
  statusCode: 400
379
379
  });
380
380
  }
381
- if (!isVcmRoleName(String(fromRole))) {
381
+ if (!CORE_VCM_ROLE_NAMES.some((role) => role === fromRole)) {
382
382
  throw new VcmError({
383
383
  code: "MESSAGE_SENDER_UNKNOWN",
384
384
  message: `Unknown sender role: ${fromRole}`,
@@ -425,7 +425,7 @@ function getMessageDeliveryTime(message) {
425
425
  }
426
426
  async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
427
427
  const fromRole = message.fromRole;
428
- if (!message.routePath || !isVcmRoleName(fromRole)) {
428
+ if (!message.routePath || !isCoreRouteRole(fromRole) || !isCoreRouteRole(message.toRole)) {
429
429
  return;
430
430
  }
431
431
  const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
@@ -440,6 +440,9 @@ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
440
440
  await fs.writeText(absolutePath, "");
441
441
  }
442
442
  }
443
+ function isCoreRouteRole(role) {
444
+ return CORE_VCM_ROLE_NAMES.some((candidate) => candidate === role);
445
+ }
443
446
  function arraysEqual(left, right) {
444
447
  if (left.length !== right.length) {
445
448
  return false;
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { VCM_ROLE_NAMES } from "../../shared/constants.js";
2
+ import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  const DEFAULT_HANDOFF_ROOT = ".ai/vcm/handoffs";
5
5
  const DEFAULT_STATE_ROOT = ".ai/vcm";
@@ -179,7 +179,7 @@ export function buildDefaultProjectConfig(repoRoot) {
179
179
  return {
180
180
  version: 1,
181
181
  repoRoot,
182
- defaultRoles: [...VCM_ROLE_NAMES],
182
+ defaultRoles: [...CORE_VCM_ROLE_NAMES],
183
183
  handoffRoot: DEFAULT_HANDOFF_ROOT,
184
184
  stateRoot: DEFAULT_STATE_ROOT,
185
185
  terminalBackend: "node-pty",