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.
- package/README.md +19 -2
- package/dist/backend/adapters/claude-adapter.js +4 -1
- package/dist/backend/api/harness-routes.js +21 -4
- package/dist/backend/api/session-routes.js +5 -0
- package/dist/backend/api/translation-worker-routes.js +15 -2
- package/dist/backend/api/usage-analytics-routes.js +31 -0
- package/dist/backend/cli/install-vcm-harness.js +54 -5
- package/dist/backend/server.js +27 -5
- package/dist/backend/services/app-settings-service.js +26 -2
- package/dist/backend/services/architect-restart-service.js +136 -0
- package/dist/backend/services/claude-hook-service.js +134 -54
- package/dist/backend/services/gate-review-service.js +92 -27
- package/dist/backend/services/harness-service.js +54 -7
- package/dist/backend/services/message-service.js +6 -1
- package/dist/backend/services/runtime-coordinator-service.js +10 -10
- package/dist/backend/services/session-service.js +78 -25
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-interrupt-service.js +4 -1
- package/dist/backend/services/turn-reconciler-service.js +1 -0
- package/dist/backend/services/usage-analytics-service.js +346 -0
- package/dist/backend/templates/handoff.js +4 -0
- package/dist/backend/templates/harness/architect-agent.js +18 -10
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +23 -0
- package/dist/backend/templates/harness/claude-root.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +25 -3
- package/dist/backend/templates/harness/project-manager-agent.js +10 -2
- package/dist/backend/templates/harness/restart-architect-skill.js +75 -0
- package/dist/backend/templates/harness/tester-agent.js +15 -7
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +31 -5
- package/dist/backend/templates/harness/vcm-route-message-skill.js +3 -3
- package/dist/shared/types/app-settings.js +14 -0
- package/dist/shared/types/usage-analytics.js +1 -0
- package/dist/shared/validation/artifact-check.js +29 -3
- package/dist-frontend/assets/{index-BAE_pjXJ.js → index-CDkDHrWQ.js} +43 -43
- package/dist-frontend/assets/{index-CiEUp9Si.css → index-Ci7z8tW3.css} +1 -1
- package/dist-frontend/index.html +2 -2
- 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
|
-
|
|
95
|
+
runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
|
|
96
|
+
runtimeSessionToken: input.runtimeSessionToken
|
|
92
97
|
});
|
|
93
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
};
|