vibe-coding-master 0.7.16 → 0.7.18

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 (37) hide show
  1. package/README.md +19 -2
  2. package/dist/backend/adapters/claude-adapter.js +4 -1
  3. package/dist/backend/api/harness-routes.js +21 -4
  4. package/dist/backend/api/session-routes.js +5 -0
  5. package/dist/backend/api/translation-worker-routes.js +15 -2
  6. package/dist/backend/api/usage-analytics-routes.js +31 -0
  7. package/dist/backend/cli/install-vcm-harness.js +54 -5
  8. package/dist/backend/server.js +27 -5
  9. package/dist/backend/services/app-settings-service.js +26 -2
  10. package/dist/backend/services/architect-restart-service.js +136 -0
  11. package/dist/backend/services/claude-hook-service.js +134 -54
  12. package/dist/backend/services/gate-review-service.js +92 -27
  13. package/dist/backend/services/harness-service.js +54 -7
  14. package/dist/backend/services/message-service.js +6 -1
  15. package/dist/backend/services/runtime-coordinator-service.js +10 -10
  16. package/dist/backend/services/session-service.js +78 -25
  17. package/dist/backend/services/task-close-service.js +1 -0
  18. package/dist/backend/services/terminal-interrupt-service.js +4 -1
  19. package/dist/backend/services/turn-reconciler-service.js +1 -0
  20. package/dist/backend/services/usage-analytics-service.js +346 -0
  21. package/dist/backend/templates/handoff.js +4 -0
  22. package/dist/backend/templates/harness/architect-agent.js +18 -10
  23. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +23 -0
  24. package/dist/backend/templates/harness/claude-root.js +1 -1
  25. package/dist/backend/templates/harness/gate-review.js +25 -3
  26. package/dist/backend/templates/harness/project-manager-agent.js +10 -2
  27. package/dist/backend/templates/harness/restart-architect-skill.js +75 -0
  28. package/dist/backend/templates/harness/tester-agent.js +15 -7
  29. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +31 -5
  30. package/dist/backend/templates/harness/vcm-route-message-skill.js +3 -3
  31. package/dist/shared/types/app-settings.js +14 -0
  32. package/dist/shared/types/usage-analytics.js +1 -0
  33. package/dist/shared/validation/artifact-check.js +29 -3
  34. package/dist-frontend/assets/{index-BAE_pjXJ.js → index-CDkDHrWQ.js} +43 -43
  35. package/dist-frontend/assets/{index-CiEUp9Si.css → index-Ci7z8tW3.css} +1 -1
  36. package/dist-frontend/index.html +2 -2
  37. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ import path from "node:path";
2
+ import { resolveRepoPath } from "../adapters/filesystem.js";
3
+ import { VcmError } from "../errors.js";
4
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
5
+ const ARCHITECT_ROLE = "architect";
6
+ const PM_ROLE = "project-manager";
7
+ const COMPLETE_PLAN_PATTERN = /^Planning Result:\s*complete\s*$/im;
8
+ export const ARCHITECT_RESTORE_PROMPT = `This Architect session continues the current task after completed architecture planning.
9
+
10
+ Before performing any assigned work, read:
11
+ - .ai/vcm/handoffs/architecture-brief.md
12
+ - .ai/vcm/handoffs/architecture-evidence.md
13
+ - .ai/vcm/handoffs/architecture-plan.md
14
+ - the current scaffold commit and worktree state
15
+ - the latest Gate Review report when present
16
+
17
+ Treat the current artifacts and worktree as the source of truth. Do not repeat the completed interview or planning work unless current evidence contradicts them.`;
18
+ export function createArchitectRestartService(deps) {
19
+ const pendingByTask = new Map();
20
+ return {
21
+ async schedule(repoRoot, taskSlug) {
22
+ const session = await requireRunningArchitect(repoRoot, taskSlug);
23
+ await requireCompletePlan(repoRoot, taskSlug);
24
+ const key = taskKey(repoRoot, taskSlug);
25
+ const existing = pendingByTask.get(key);
26
+ if (existing?.sessionId === session.id) {
27
+ return { taskSlug, sessionId: session.id, status: "scheduled" };
28
+ }
29
+ pendingByTask.set(key, {
30
+ repoRoot,
31
+ taskSlug,
32
+ sessionId: session.id,
33
+ stopped: false,
34
+ executing: false
35
+ });
36
+ return { taskSlug, sessionId: session.id, status: "scheduled" };
37
+ },
38
+ async recordArchitectStop(repoRoot, taskSlug, sessionId) {
39
+ const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
40
+ if (!pending || pending.sessionId !== sessionId) {
41
+ return;
42
+ }
43
+ pending.stopped = true;
44
+ await tryRestart(pending);
45
+ },
46
+ async recordRouteDelivered(repoRoot, taskSlug, message) {
47
+ if (!isArchitectToPm(message)) {
48
+ return;
49
+ }
50
+ const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
51
+ if (!pending) {
52
+ return;
53
+ }
54
+ pending.deliveredMessageId = message.id;
55
+ await tryRestart(pending);
56
+ },
57
+ async recordRouteAccepted(repoRoot, taskSlug, message) {
58
+ if (!isArchitectToPm(message)) {
59
+ return;
60
+ }
61
+ const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
62
+ if (!pending) {
63
+ return;
64
+ }
65
+ pending.acceptedMessageId = message.id;
66
+ await tryRestart(pending);
67
+ },
68
+ clear(repoRoot, taskSlug) {
69
+ pendingByTask.delete(taskKey(repoRoot, taskSlug));
70
+ }
71
+ };
72
+ async function requireRunningArchitect(repoRoot, taskSlug) {
73
+ const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
74
+ if (!session || session.status !== "running") {
75
+ throw new VcmError({
76
+ code: "ARCHITECT_SESSION_NOT_RUNNING",
77
+ message: "Architect session is not running.",
78
+ statusCode: 409
79
+ });
80
+ }
81
+ return session;
82
+ }
83
+ async function requireCompletePlan(repoRoot, taskSlug) {
84
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
85
+ const planPath = resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(task.handoffDir, "architecture-plan.md"));
86
+ if (!(await deps.fs.pathExists(planPath))) {
87
+ throw incompletePlanError("architecture-plan.md does not exist.");
88
+ }
89
+ const content = await deps.fs.readText(planPath);
90
+ if (!COMPLETE_PLAN_PATTERN.test(content)) {
91
+ throw incompletePlanError("architecture-plan.md is not marked complete.");
92
+ }
93
+ }
94
+ async function tryRestart(pending) {
95
+ if (pending.executing
96
+ || !pending.stopped
97
+ || !pending.deliveredMessageId
98
+ || pending.deliveredMessageId !== pending.acceptedMessageId) {
99
+ return;
100
+ }
101
+ const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
102
+ if (!session
103
+ || session.id !== pending.sessionId
104
+ || session.status !== "running"
105
+ || session.activityStatus !== "idle") {
106
+ return;
107
+ }
108
+ pending.executing = true;
109
+ try {
110
+ await requireCompletePlan(pending.repoRoot, pending.taskSlug);
111
+ await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
112
+ permissionMode: session.permissionMode,
113
+ model: session.model,
114
+ effort: session.effort,
115
+ appendSystemPrompt: ARCHITECT_RESTORE_PROMPT
116
+ });
117
+ pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
118
+ }
119
+ catch {
120
+ pending.executing = false;
121
+ }
122
+ }
123
+ }
124
+ function isArchitectToPm(message) {
125
+ return message.fromRole === ARCHITECT_ROLE && message.toRole === PM_ROLE;
126
+ }
127
+ function taskKey(repoRoot, taskSlug) {
128
+ return `${repoRoot}\0${taskSlug}`;
129
+ }
130
+ function incompletePlanError(reason) {
131
+ return new VcmError({
132
+ code: "ARCHITECT_PLAN_INCOMPLETE",
133
+ message: `Architect restart cannot be scheduled. ${reason}`,
134
+ statusCode: 409
135
+ });
136
+ }
@@ -2,6 +2,7 @@ import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorTool
2
2
  import { VcmError } from "../errors.js";
3
3
  import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
4
4
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
+ import { matchesRoleHookSession } from "./session-service.js";
5
6
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
6
7
  const MAX_ROLE_RETRY_ATTEMPTS = 20;
7
8
  const ROLE_RETRY_BASE_DELAY_MS = 60_000;
@@ -18,6 +19,7 @@ const NON_RETRYABLE_STOP_FAILURE_ERRORS = new Set([
18
19
  const DIAGNOSTIC_SNIPPET_MAX_LENGTH = 2000;
19
20
  export function createClaudeHookService(deps) {
20
21
  const stopFailureRetryTimers = new Map();
22
+ const roleHookLocks = new Map();
21
23
  const now = deps.now ?? (() => new Date().toISOString());
22
24
  const retrySetTimeout = deps.retrySetTimeout ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
23
25
  const retryClearTimeout = deps.retryClearTimeout ?? ((timer) => globalThis.clearTimeout(timer));
@@ -79,7 +81,9 @@ export function createClaudeHookService(deps) {
79
81
  eventName,
80
82
  sessionId: stringOrUndefined(input.event.session_id),
81
83
  transcriptPath: stringOrUndefined(input.event.transcript_path),
82
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
84
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
85
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
86
+ runtimeSessionToken: input.runtimeSessionToken
83
87
  })
84
88
  : await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
85
89
  taskSlug: input.taskSlug,
@@ -88,9 +92,12 @@ export function createClaudeHookService(deps) {
88
92
  sessionId: stringOrUndefined(input.event.session_id),
89
93
  transcriptPath: stringOrUndefined(input.event.transcript_path),
90
94
  cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
91
- allowSessionMismatch: true
95
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
96
+ runtimeSessionToken: input.runtimeSessionToken
92
97
  });
93
- await deps.translationWorkerService?.handleTranslatorHook(context.project.repoRoot, eventName, input.taskSlug);
98
+ if (session) {
99
+ await deps.translationWorkerService?.handleTranslatorHook(context.project.repoRoot, eventName, input.taskSlug);
100
+ }
94
101
  return {
95
102
  ok: true,
96
103
  eventName,
@@ -109,7 +116,9 @@ export function createClaudeHookService(deps) {
109
116
  eventName,
110
117
  sessionId: stringOrUndefined(input.event.session_id),
111
118
  transcriptPath: stringOrUndefined(input.event.transcript_path),
112
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
119
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
120
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
121
+ runtimeSessionToken: input.runtimeSessionToken
113
122
  })
114
123
  : await deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
115
124
  taskSlug: input.taskSlug,
@@ -118,8 +127,12 @@ export function createClaudeHookService(deps) {
118
127
  sessionId: stringOrUndefined(input.event.session_id),
119
128
  transcriptPath: stringOrUndefined(input.event.transcript_path),
120
129
  cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
121
- allowSessionMismatch: true
130
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
131
+ runtimeSessionToken: input.runtimeSessionToken
122
132
  });
133
+ if (!session) {
134
+ return completedHookResult(input, eventName);
135
+ }
123
136
  const activeTask = deps.autoMemoryService
124
137
  ? (await deps.taskService.listTasks(context.project.repoRoot))
125
138
  .find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
@@ -176,27 +189,33 @@ export function createClaudeHookService(deps) {
176
189
  throwUnsupportedEvent(eventName);
177
190
  }
178
191
  const context = await getHookContext(input);
192
+ if (!(await isCurrentRoleHook(context, input, eventName))) {
193
+ return completedHookResult(input, eventName);
194
+ }
179
195
  const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
180
196
  if (memoryResult) {
181
197
  return memoryResult;
182
198
  }
183
- const boundToTask = await isHookSessionBoundToTask(context, input.role);
184
- if (boundToTask) {
185
- deps.jobGuard?.notePromptSubmitted({
186
- repoRoot: context.project.repoRoot,
187
- taskSlug: context.taskSlug,
188
- role: input.role
189
- });
190
- }
191
199
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
192
200
  taskSlug: context.taskSlug,
193
201
  role: input.role,
194
202
  eventName,
195
203
  claudeSessionId: stringOrUndefined(input.event.session_id),
196
204
  transcriptPath: stringOrUndefined(input.event.transcript_path),
197
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
205
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
206
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
207
+ runtimeSessionToken: input.runtimeSessionToken
198
208
  });
209
+ if (!session) {
210
+ return completedHookResult(input, eventName);
211
+ }
212
+ const boundToTask = await isHookSessionBoundToTask(context, input.role);
199
213
  if (boundToTask) {
214
+ deps.jobGuard?.notePromptSubmitted({
215
+ repoRoot: context.project.repoRoot,
216
+ taskSlug: context.taskSlug,
217
+ role: input.role
218
+ });
200
219
  await deps.roundService.recordClaudeHookEvent({
201
220
  repoRoot: context.project.repoRoot,
202
221
  stateRepoRoot: context.taskRepoRoot,
@@ -227,6 +246,9 @@ export function createClaudeHookService(deps) {
227
246
  role: input.role,
228
247
  prompt: stringOrUndefined(input.event.prompt)
229
248
  });
249
+ if (submitted) {
250
+ await deps.architectRestartService?.recordRouteAccepted(context.project.repoRoot, context.taskSlug, submitted);
251
+ }
230
252
  return {
231
253
  ok: true,
232
254
  eventName,
@@ -243,6 +265,9 @@ export function createClaudeHookService(deps) {
243
265
  throwUnsupportedEvent(eventName);
244
266
  }
245
267
  const context = await getHookContext(input);
268
+ if (!(await isCurrentRoleHook(context, input, eventName))) {
269
+ return completedHookResult(input, eventName);
270
+ }
246
271
  const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
247
272
  if (memoryResult) {
248
273
  return memoryResult;
@@ -309,6 +334,9 @@ export function createClaudeHookService(deps) {
309
334
  throwUnsupportedEvent(eventName);
310
335
  }
311
336
  const context = await getHookContext(input);
337
+ if (!(await isCurrentRoleHook(context, input, eventName))) {
338
+ return completedHookResult(input, eventName);
339
+ }
312
340
  const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
313
341
  if (memoryResult) {
314
342
  return memoryResult;
@@ -369,6 +397,9 @@ export function createClaudeHookService(deps) {
369
397
  throwUnsupportedEvent(eventName);
370
398
  }
371
399
  const context = await getHookContext(input);
400
+ if (!(await isCurrentRoleHook(context, input, eventName))) {
401
+ return completedHookResult(input, eventName);
402
+ }
372
403
  const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
373
404
  if (memoryResult) {
374
405
  return memoryResult;
@@ -379,7 +410,9 @@ export function createClaudeHookService(deps) {
379
410
  eventName,
380
411
  claudeSessionId: stringOrUndefined(input.event.session_id),
381
412
  transcriptPath: stringOrUndefined(input.event.transcript_path),
382
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
413
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
414
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
415
+ runtimeSessionToken: input.runtimeSessionToken
383
416
  });
384
417
  return {
385
418
  ok: true,
@@ -393,15 +426,20 @@ export function createClaudeHookService(deps) {
393
426
  async function recordTurnEnd(input, context, eventName, options) {
394
427
  const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
395
428
  const settleRouteDispatchInput = createRouteDispatchInput(input, context);
396
- const boundToTask = await isHookSessionBoundToTask(context, input.role);
397
429
  const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
398
430
  taskSlug: context.taskSlug,
399
431
  role: input.role,
400
432
  eventName,
401
433
  claudeSessionId: stringOrUndefined(input.event.session_id),
402
434
  transcriptPath: stringOrUndefined(input.event.transcript_path),
403
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
435
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
436
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
437
+ runtimeSessionToken: input.runtimeSessionToken
404
438
  });
439
+ if (!session) {
440
+ return completedHookResult(input, eventName);
441
+ }
442
+ const boundToTask = await isHookSessionBoundToTask(context, input.role);
405
443
  if (boundToTask) {
406
444
  await deps.roundService.recordClaudeHookEvent({
407
445
  repoRoot: context.project.repoRoot,
@@ -437,6 +475,9 @@ export function createClaudeHookService(deps) {
437
475
  occurredAt: session.lastTurnEndedAt ?? session.updatedAt
438
476
  });
439
477
  }
478
+ if (eventName === "Stop" && input.role === "architect" && session) {
479
+ await deps.architectRestartService?.recordArchitectStop(context.project.repoRoot, context.taskSlug, session.id);
480
+ }
440
481
  if (options.notifyGateway && session && input.role === "project-manager") {
441
482
  void deps.gatewayService?.handlePmStop({
442
483
  repoRoot: context.project.repoRoot,
@@ -477,8 +518,13 @@ export function createClaudeHookService(deps) {
477
518
  eventName,
478
519
  claudeSessionId: stringOrUndefined(input.event.session_id),
479
520
  transcriptPath: stringOrUndefined(input.event.transcript_path),
480
- cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
521
+ cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
522
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
523
+ runtimeSessionToken: input.runtimeSessionToken
481
524
  });
525
+ if (!session) {
526
+ return completedHookResult(input, eventName);
527
+ }
482
528
  const boundToTask = await isHookSessionBoundToTask(context, input.role);
483
529
  if (boundToTask && eventName !== "PostCompact") {
484
530
  await deps.roundService.recordClaudeHookEvent({
@@ -517,6 +563,31 @@ export function createClaudeHookService(deps) {
517
563
  ...(stoppedRole ? { stoppedRole } : {})
518
564
  };
519
565
  }
566
+ async function isCurrentRoleHook(context, input, eventName) {
567
+ const current = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
568
+ return Boolean(current && matchesRoleHookSession(current, {
569
+ eventName,
570
+ sessionId: stringOrUndefined(input.event.session_id),
571
+ transcriptPath: stringOrUndefined(input.event.transcript_path),
572
+ runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
573
+ runtimeSessionToken: input.runtimeSessionToken
574
+ }));
575
+ }
576
+ async function withRoleHookLock(input, run) {
577
+ const key = `${input.taskSlug}:${input.role}`;
578
+ const previous = roleHookLocks.get(key) ?? Promise.resolve();
579
+ const result = previous.catch(() => undefined).then(run);
580
+ const tail = result.then(() => undefined, () => undefined);
581
+ roleHookLocks.set(key, tail);
582
+ try {
583
+ return await result;
584
+ }
585
+ finally {
586
+ if (roleHookLocks.get(key) === tail) {
587
+ roleHookLocks.delete(key);
588
+ }
589
+ }
590
+ }
520
591
  async function scheduleStopFailureRetry(input, context, failure) {
521
592
  const preferences = await deps.appSettings.getPreferences();
522
593
  if (!preferences.roleRetryEnabled || !deps.runtime) {
@@ -556,7 +627,10 @@ export function createClaudeHookService(deps) {
556
627
  nextRetryAt
557
628
  }
558
629
  });
559
- await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
630
+ const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
631
+ if (session) {
632
+ await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role, session.id);
633
+ }
560
634
  scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
561
635
  return "scheduled";
562
636
  }
@@ -624,7 +698,7 @@ export function createClaudeHookService(deps) {
624
698
  }
625
699
  });
626
700
  await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt());
627
- await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
701
+ await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role, session.id);
628
702
  }
629
703
  async function markStopFailureRecoveryFailed(input, context, failure, attempt, timestamp = now()) {
630
704
  clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
@@ -730,44 +804,50 @@ export function createClaudeHookService(deps) {
730
804
  }
731
805
  return {
732
806
  async handleHook(input) {
733
- if (isTranslatorToolRoleName(input.role)) {
734
- return processTranslatorHook(input);
735
- }
736
- if (isHarnessEngineerToolRoleName(input.role)) {
737
- return processHarnessEngineerHook(input);
738
- }
739
- const eventName = parseHookEvent(input.event.hook_event_name);
740
- if (eventName === "UserPromptSubmit") {
741
- return handleUserPromptSubmitHook(input);
742
- }
743
- if (eventName === "StopFailure") {
744
- return processStopFailureHook(input);
745
- }
746
- if (eventName === "PostCompact") {
747
- return processPostCompactHook(input);
748
- }
749
- // Legacy combined endpoint: the installed hook discards the response,
750
- // so a block decision could not be enforced. Never block here.
751
- return processStopHook(input, { allowBlock: false });
807
+ return withRoleHookLock(input, async () => {
808
+ if (isTranslatorToolRoleName(input.role)) {
809
+ return processTranslatorHook(input);
810
+ }
811
+ if (isHarnessEngineerToolRoleName(input.role)) {
812
+ return processHarnessEngineerHook(input);
813
+ }
814
+ const eventName = parseHookEvent(input.event.hook_event_name);
815
+ if (eventName === "UserPromptSubmit") {
816
+ return handleUserPromptSubmitHook(input);
817
+ }
818
+ if (eventName === "StopFailure") {
819
+ return processStopFailureHook(input);
820
+ }
821
+ if (eventName === "PostCompact") {
822
+ return processPostCompactHook(input);
823
+ }
824
+ // Legacy combined endpoint: the installed hook discards the response,
825
+ // so a block decision could not be enforced. Never block here.
826
+ return processStopHook(input, { allowBlock: false });
827
+ });
752
828
  },
753
829
  handleStopHook(input) {
754
- if (isTranslatorToolRoleName(input.role)) {
755
- return processTranslatorHook(input);
756
- }
757
- if (isHarnessEngineerToolRoleName(input.role)) {
758
- return processHarnessEngineerHook(input);
759
- }
760
- return processStopHook(input, { allowBlock: true });
830
+ return withRoleHookLock(input, async () => {
831
+ if (isTranslatorToolRoleName(input.role)) {
832
+ return processTranslatorHook(input);
833
+ }
834
+ if (isHarnessEngineerToolRoleName(input.role)) {
835
+ return processHarnessEngineerHook(input);
836
+ }
837
+ return processStopHook(input, { allowBlock: true });
838
+ });
761
839
  },
762
840
  handleReconciledTurnEnd(input) {
763
- const eventName = parseHookEvent(input.event.hook_event_name);
764
- if (eventName === "Stop") {
765
- return processStopHook(input, { allowBlock: false });
766
- }
767
- if (eventName === "StopFailure") {
768
- return processStopFailureHook(input);
769
- }
770
- throwUnsupportedEvent(eventName);
841
+ return withRoleHookLock(input, async () => {
842
+ const eventName = parseHookEvent(input.event.hook_event_name);
843
+ if (eventName === "Stop") {
844
+ return processStopHook(input, { allowBlock: false });
845
+ }
846
+ if (eventName === "StopFailure") {
847
+ return processStopFailureHook(input);
848
+ }
849
+ throwUnsupportedEvent(eventName);
850
+ });
771
851
  },
772
852
  handlePermissionRequestHook
773
853
  };