vibe-coding-master 0.3.25 → 0.3.27
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 +7 -7
- package/dist/backend/api/codex-hook-routes.js +0 -7
- package/dist/backend/api/gate-review-routes.js +58 -0
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/cli/install-vcm-harness.js +17 -57
- package/dist/backend/server.js +7 -8
- package/dist/backend/services/app-settings-service.js +15 -15
- package/dist/backend/services/claude-hook-service.js +138 -28
- package/dist/backend/services/codex-hook-service.js +8 -50
- package/dist/backend/services/{codex-review-service.js → gate-review-service.js} +108 -108
- package/dist/backend/services/harness-service.js +17 -63
- package/dist/backend/services/session-service.js +275 -49
- package/dist/backend/services/task-service.js +2 -2
- package/dist/backend/templates/harness/claude-root.js +2 -2
- package/dist/backend/templates/harness/{codex-review.js → gate-review.js} +35 -278
- package/dist/backend/templates/harness/project-manager-agent.js +7 -7
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +5 -5
- package/dist/shared/constants.js +8 -5
- package/dist/shared/types/{codex-review.js → gate-review.js} +1 -1
- package/dist-frontend/assets/{index-CKWy15WL.js → index-DVy34Iwn.js} +31 -31
- package/dist-frontend/index.html +1 -1
- package/docs/codex-translation-plan.md +9 -9
- package/docs/gate-review-gates.md +133 -0
- package/docs/product-design.md +37 -40
- package/package.json +1 -1
- package/scripts/verify-package.mjs +4 -4
- package/dist/backend/api/codex-review-routes.js +0 -58
- package/docs/codex-review-gates.md +0 -593
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
3
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
5
|
+
const MAX_STOP_FAILURE_RECOVERY_ATTEMPTS = 2;
|
|
4
6
|
export function createClaudeHookService(deps) {
|
|
7
|
+
const stopFailureRecoveryAttempts = new Map();
|
|
5
8
|
async function getHookContext(input) {
|
|
6
9
|
if (!isVcmRoleName(input.role)) {
|
|
7
10
|
throw new VcmError({
|
|
@@ -92,6 +95,7 @@ export function createClaudeHookService(deps) {
|
|
|
92
95
|
throwUnsupportedEvent(eventName);
|
|
93
96
|
}
|
|
94
97
|
const context = await getHookContext(input);
|
|
98
|
+
clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
|
|
95
99
|
if (options.allowBlock && deps.jobGuard) {
|
|
96
100
|
const verdict = await deps.jobGuard.evaluateStop({
|
|
97
101
|
repoRoot: context.project.repoRoot,
|
|
@@ -113,23 +117,72 @@ export function createClaudeHookService(deps) {
|
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
return recordTurnEnd(input, context, eventName, {
|
|
121
|
+
dispatchRouteFiles: true,
|
|
122
|
+
notifyGateway: true,
|
|
123
|
+
settleGuard: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function processStopFailureHook(input) {
|
|
127
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
128
|
+
if (eventName !== "StopFailure") {
|
|
129
|
+
throwUnsupportedEvent(eventName);
|
|
130
|
+
}
|
|
131
|
+
const context = await getHookContext(input);
|
|
132
|
+
const routeDispatchInput = createRouteDispatchInput(input, context);
|
|
133
|
+
const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
|
|
134
|
+
const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
|
|
135
|
+
if (hasCompletionEvidence) {
|
|
136
|
+
clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
|
|
137
|
+
return recordTurnEnd(input, context, eventName, {
|
|
138
|
+
dispatchRouteFiles: true,
|
|
139
|
+
notifyGateway: false,
|
|
140
|
+
settleGuard: true
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const recovered = await dispatchStopFailureRecovery(input, context);
|
|
144
|
+
if (recovered) {
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
eventName,
|
|
148
|
+
taskSlug: input.taskSlug,
|
|
149
|
+
role: input.role,
|
|
150
|
+
sessionUpdated: true,
|
|
151
|
+
dispatchedCount: 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return recordTurnEnd(input, context, eventName, {
|
|
155
|
+
dispatchRouteFiles: false,
|
|
156
|
+
notifyGateway: false,
|
|
157
|
+
settleGuard: false
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function processPostCompactHook(input) {
|
|
161
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
162
|
+
if (eventName !== "PostCompact") {
|
|
163
|
+
throwUnsupportedEvent(eventName);
|
|
164
|
+
}
|
|
165
|
+
const context = await getHookContext(input);
|
|
166
|
+
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
122
167
|
taskSlug: input.taskSlug,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
role: input.role,
|
|
169
|
+
eventName,
|
|
170
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
171
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
172
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
eventName,
|
|
177
|
+
taskSlug: input.taskSlug,
|
|
178
|
+
role: input.role,
|
|
179
|
+
sessionUpdated: Boolean(session),
|
|
180
|
+
dispatchedCount: 0
|
|
132
181
|
};
|
|
182
|
+
}
|
|
183
|
+
async function recordTurnEnd(input, context, eventName, options) {
|
|
184
|
+
const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
|
|
185
|
+
const settleRouteDispatchInput = createRouteDispatchInput(input, context);
|
|
133
186
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
134
187
|
taskSlug: input.taskSlug,
|
|
135
188
|
role: input.role,
|
|
@@ -145,16 +198,20 @@ export function createClaudeHookService(deps) {
|
|
|
145
198
|
taskSlug: input.taskSlug,
|
|
146
199
|
role: input.role,
|
|
147
200
|
eventName,
|
|
148
|
-
settleGuard
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
...(options.settleGuard
|
|
202
|
+
? {
|
|
203
|
+
settleGuard: async () => {
|
|
204
|
+
const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
|
|
205
|
+
if (pending.length === 0) {
|
|
206
|
+
return { action: "stop" };
|
|
207
|
+
}
|
|
208
|
+
const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
|
|
209
|
+
return retried.some((result) => result.delivered)
|
|
210
|
+
? { action: "continue", reason: "pending route message dispatched" }
|
|
211
|
+
: { action: "stop" };
|
|
212
|
+
}
|
|
152
213
|
}
|
|
153
|
-
|
|
154
|
-
return retried.some((result) => result.delivered)
|
|
155
|
-
? { action: "continue", reason: "pending route message dispatched" }
|
|
156
|
-
: { action: "stop" };
|
|
157
|
-
}
|
|
214
|
+
: {})
|
|
158
215
|
});
|
|
159
216
|
if (session) {
|
|
160
217
|
await deps.translationService.recordConversationBoundary({
|
|
@@ -167,14 +224,16 @@ export function createClaudeHookService(deps) {
|
|
|
167
224
|
occurredAt: session.lastTurnEndedAt ?? session.updatedAt
|
|
168
225
|
});
|
|
169
226
|
}
|
|
170
|
-
if (session && input.role === "project-manager") {
|
|
227
|
+
if (options.notifyGateway && session && input.role === "project-manager") {
|
|
171
228
|
void deps.gatewayService?.handlePmStop({
|
|
172
229
|
repoRoot: context.project.repoRoot,
|
|
173
230
|
taskSlug: input.taskSlug,
|
|
174
231
|
session
|
|
175
232
|
}).catch(() => undefined);
|
|
176
233
|
}
|
|
177
|
-
const dispatched =
|
|
234
|
+
const dispatched = options.dispatchRouteFiles
|
|
235
|
+
? await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput)
|
|
236
|
+
: [];
|
|
178
237
|
return {
|
|
179
238
|
ok: true,
|
|
180
239
|
eventName,
|
|
@@ -184,6 +243,51 @@ export function createClaudeHookService(deps) {
|
|
|
184
243
|
dispatchedCount: dispatched.filter((result) => result.delivered).length
|
|
185
244
|
};
|
|
186
245
|
}
|
|
246
|
+
function createRouteDispatchInput(input, context, stoppedRole) {
|
|
247
|
+
return {
|
|
248
|
+
repoRoot: context.project.repoRoot,
|
|
249
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
250
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
251
|
+
stateRoot: context.config.stateRoot,
|
|
252
|
+
handoffDir: context.task.handoffDir,
|
|
253
|
+
taskSlug: input.taskSlug,
|
|
254
|
+
...(stoppedRole ? { stoppedRole } : {})
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function dispatchStopFailureRecovery(input, context) {
|
|
258
|
+
if (!deps.runtime) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const key = stopFailureRecoveryKey(context.project.repoRoot, input.taskSlug, input.role);
|
|
262
|
+
const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
|
|
263
|
+
if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const session = await deps.sessionService.getRoleSession(context.project.repoRoot, input.taskSlug, input.role);
|
|
267
|
+
if (!session || session.status !== "running") {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
stopFailureRecoveryAttempts.set(key, attempt);
|
|
271
|
+
await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
|
|
272
|
+
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, input.taskSlug, input.role);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
function clearStopFailureRecovery(repoRoot, taskSlug, role) {
|
|
276
|
+
stopFailureRecoveryAttempts.delete(stopFailureRecoveryKey(repoRoot, taskSlug, role));
|
|
277
|
+
}
|
|
278
|
+
function stopFailureRecoveryKey(repoRoot, taskSlug, role) {
|
|
279
|
+
return `${repoRoot}:${taskSlug}:${role}`;
|
|
280
|
+
}
|
|
281
|
+
function renderStopFailureRecoveryPrompt(attempt) {
|
|
282
|
+
return [
|
|
283
|
+
"[VCM Recovery]",
|
|
284
|
+
"Your previous turn ended unexpectedly after context compaction or an API error.",
|
|
285
|
+
"Continue the same assigned work from the current repository and VCM handoff state.",
|
|
286
|
+
"Do not repeat completed edits, duplicate validation, or duplicate route messages.",
|
|
287
|
+
"If the assigned work is already complete, write/send the expected VCM handoff now.",
|
|
288
|
+
`Recovery attempt: ${attempt}/${MAX_STOP_FAILURE_RECOVERY_ATTEMPTS}.`
|
|
289
|
+
].join("\n");
|
|
290
|
+
}
|
|
187
291
|
async function handlePermissionRequestHook(input) {
|
|
188
292
|
if (!isVcmRoleName(input.role)) {
|
|
189
293
|
throw new VcmError({
|
|
@@ -220,6 +324,12 @@ export function createClaudeHookService(deps) {
|
|
|
220
324
|
if (eventName === "UserPromptSubmit") {
|
|
221
325
|
return handleUserPromptSubmitHook(input);
|
|
222
326
|
}
|
|
327
|
+
if (eventName === "StopFailure") {
|
|
328
|
+
return processStopFailureHook(input);
|
|
329
|
+
}
|
|
330
|
+
if (eventName === "PostCompact") {
|
|
331
|
+
return processPostCompactHook(input);
|
|
332
|
+
}
|
|
223
333
|
// Legacy combined endpoint: the installed hook discards the response,
|
|
224
334
|
// so a block decision could not be enforced. Never block here.
|
|
225
335
|
return processStopHook(input, { allowBlock: false });
|
|
@@ -231,14 +341,14 @@ export function createClaudeHookService(deps) {
|
|
|
231
341
|
};
|
|
232
342
|
}
|
|
233
343
|
function parseHookEvent(value) {
|
|
234
|
-
if (value === "UserPromptSubmit" || value === "Stop") {
|
|
344
|
+
if (value === "UserPromptSubmit" || value === "Stop" || value === "StopFailure" || value === "PostCompact") {
|
|
235
345
|
return value;
|
|
236
346
|
}
|
|
237
347
|
throw new VcmError({
|
|
238
348
|
code: "HOOK_EVENT_UNSUPPORTED",
|
|
239
349
|
message: `Unsupported Claude Code hook event: ${String(value)}`,
|
|
240
350
|
statusCode: 400,
|
|
241
|
-
hint: "VCM accepts UserPromptSubmit and
|
|
351
|
+
hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, and PostCompact hooks only."
|
|
242
352
|
});
|
|
243
353
|
}
|
|
244
354
|
function throwUnsupportedEvent(eventName) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isCodexRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
-
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
3
|
export function createCodexHookService(deps) {
|
|
5
4
|
async function getHookContext(input) {
|
|
6
5
|
if (!isCodexRoleName(input.role)) {
|
|
@@ -18,20 +17,8 @@ export function createCodexHookService(deps) {
|
|
|
18
17
|
statusCode: 409
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
|
-
if (input.role === "codex-translator") {
|
|
22
|
-
return {
|
|
23
|
-
project,
|
|
24
|
-
config: undefined,
|
|
25
|
-
taskRepoRoot: undefined
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
29
|
-
const task = await deps.taskService.loadTask(project.repoRoot, input.taskSlug);
|
|
30
|
-
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
31
20
|
return {
|
|
32
|
-
project
|
|
33
|
-
config,
|
|
34
|
-
taskRepoRoot
|
|
21
|
+
project
|
|
35
22
|
};
|
|
36
23
|
}
|
|
37
24
|
async function processHook(input, expectedEventName) {
|
|
@@ -44,42 +31,13 @@ export function createCodexHookService(deps) {
|
|
|
44
31
|
});
|
|
45
32
|
}
|
|
46
33
|
const context = await getHookContext(input);
|
|
47
|
-
const session =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
});
|
|
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
|
-
}
|
|
71
|
-
await deps.roundService.recordRoleTurnEvent({
|
|
72
|
-
repoRoot: context.project.repoRoot,
|
|
73
|
-
stateRepoRoot: context.taskRepoRoot,
|
|
74
|
-
stateRoot: context.config.stateRoot,
|
|
75
|
-
taskSlug: input.taskSlug,
|
|
76
|
-
role: input.role,
|
|
77
|
-
eventName
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (input.role === "codex-translator") {
|
|
81
|
-
await deps.codexTranslationService?.handleCodexHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
82
|
-
}
|
|
34
|
+
const session = await deps.sessionService.recordProjectTranslatorHookEvent(context.project.repoRoot, {
|
|
35
|
+
eventName,
|
|
36
|
+
sessionId: stringOrUndefined(input.event.session_id),
|
|
37
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
38
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
39
|
+
});
|
|
40
|
+
await deps.codexTranslationService?.handleCodexHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
83
41
|
return {
|
|
84
42
|
ok: true,
|
|
85
43
|
eventName,
|