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.
Files changed (28) hide show
  1. package/README.md +7 -7
  2. package/dist/backend/api/codex-hook-routes.js +0 -7
  3. package/dist/backend/api/gate-review-routes.js +58 -0
  4. package/dist/backend/api/task-routes.js +2 -2
  5. package/dist/backend/cli/install-vcm-harness.js +17 -57
  6. package/dist/backend/server.js +7 -8
  7. package/dist/backend/services/app-settings-service.js +15 -15
  8. package/dist/backend/services/claude-hook-service.js +138 -28
  9. package/dist/backend/services/codex-hook-service.js +8 -50
  10. package/dist/backend/services/{codex-review-service.js → gate-review-service.js} +108 -108
  11. package/dist/backend/services/harness-service.js +17 -63
  12. package/dist/backend/services/session-service.js +275 -49
  13. package/dist/backend/services/task-service.js +2 -2
  14. package/dist/backend/templates/harness/claude-root.js +2 -2
  15. package/dist/backend/templates/harness/{codex-review.js → gate-review.js} +35 -278
  16. package/dist/backend/templates/harness/project-manager-agent.js +7 -7
  17. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +5 -5
  18. package/dist/shared/constants.js +8 -5
  19. package/dist/shared/types/{codex-review.js → gate-review.js} +1 -1
  20. package/dist-frontend/assets/{index-CKWy15WL.js → index-DVy34Iwn.js} +31 -31
  21. package/dist-frontend/index.html +1 -1
  22. package/docs/codex-translation-plan.md +9 -9
  23. package/docs/gate-review-gates.md +133 -0
  24. package/docs/product-design.md +37 -40
  25. package/package.json +1 -1
  26. package/scripts/verify-package.mjs +4 -4
  27. package/dist/backend/api/codex-review-routes.js +0 -58
  28. 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
- const scopedRouteDispatchInput = {
117
- repoRoot: context.project.repoRoot,
118
- taskRepoRoot: context.taskRepoRoot,
119
- stateRepoRoot: context.taskRepoRoot,
120
- stateRoot: context.config.stateRoot,
121
- handoffDir: context.task.handoffDir,
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
- stoppedRole: input.role
124
- };
125
- const settleRouteDispatchInput = {
126
- repoRoot: context.project.repoRoot,
127
- taskRepoRoot: context.taskRepoRoot,
128
- stateRepoRoot: context.taskRepoRoot,
129
- stateRoot: context.config.stateRoot,
130
- handoffDir: context.task.handoffDir,
131
- taskSlug: input.taskSlug
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: async () => {
149
- const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
150
- if (pending.length === 0) {
151
- return { action: "stop" };
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
- const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
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 = await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput);
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 Stop hooks only."
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 = 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
- });
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,