vibe-coding-master 0.3.28 → 0.3.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/dist/backend/api/project-routes.js +1 -1
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/api/{codex-translation-routes.js → translation-worker-routes.js} +21 -21
- package/dist/backend/cli/install-vcm-harness.js +30 -25
- package/dist/backend/gateway/gateway-service.js +24 -5
- package/dist/backend/server.js +12 -22
- package/dist/backend/services/claude-hook-service.js +93 -26
- package/dist/backend/services/harness-service.js +49 -29
- package/dist/backend/services/message-service.js +9 -6
- package/dist/backend/services/project-service.js +2 -2
- package/dist/backend/services/session-service.js +99 -172
- package/dist/backend/services/translation-service.js +17 -17
- package/dist/backend/services/{codex-translation-service.js → translation-worker-service.js} +11 -11
- package/dist/backend/templates/harness/gate-review.js +2 -58
- package/dist/shared/constants.js +19 -12
- package/dist/shared/types/app-settings.js +1 -1
- package/dist/shared/types/session.js +2 -14
- package/dist-frontend/assets/{index-Cfum1Prr.css → index-BvUHLq4X.css} +1 -1
- package/dist-frontend/assets/{index-x9I-bGUt.js → index-D0ezB4na.js} +36 -36
- package/dist-frontend/index.html +2 -2
- package/docs/{codex-translation-plan.md → claude-code-translation-plan.md} +151 -153
- package/docs/full-harness-baseline.md +3 -7
- package/docs/gate-review-gates.md +13 -5
- package/docs/gateway-design.md +1 -1
- package/docs/product-design.md +12 -11
- package/docs/v0.4-custom-workflow-plan.md +0 -1
- package/docs/vcm-cc-best-practices.md +2 -5
- package/package.json +1 -1
- package/dist/backend/api/codex-hook-routes.js +0 -9
- package/dist/backend/codex-sandbox-mode.js +0 -28
- package/dist/backend/services/codex-hook-service.js +0 -71
- package/dist/shared/types/codex-hook.js +0 -1
|
@@ -1,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
|
|
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:
|
|
89
|
+
taskSlug: context.taskSlug,
|
|
43
90
|
role: input.role
|
|
44
91
|
});
|
|
45
92
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
46
|
-
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
183
|
+
clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
|
|
137
184
|
return recordTurnEnd(input, context, eventName, {
|
|
138
|
-
dispatchRouteFiles:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
|
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: "
|
|
111
|
-
path: ".
|
|
112
|
-
title: "
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
286
|
-
for (const toRole of
|
|
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 (!
|
|
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 (!
|
|
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 || !
|
|
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 {
|
|
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: [...
|
|
182
|
+
defaultRoles: [...CORE_VCM_ROLE_NAMES],
|
|
183
183
|
handoffRoot: DEFAULT_HANDOFF_ROOT,
|
|
184
184
|
stateRoot: DEFAULT_STATE_ROOT,
|
|
185
185
|
terminalBackend: "node-pty",
|