vibe-coding-master 0.7.17 → 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 +7 -2
- package/dist/backend/api/harness-routes.js +21 -4
- package/dist/backend/api/translation-worker-routes.js +15 -2
- package/dist/backend/cli/install-vcm-harness.js +3 -3
- package/dist/backend/server.js +2 -0
- package/dist/backend/services/app-settings-service.js +26 -2
- package/dist/backend/services/claude-hook-service.js +128 -54
- package/dist/backend/services/gate-review-service.js +52 -27
- package/dist/backend/services/harness-service.js +3 -3
- package/dist/backend/services/message-service.js +1 -1
- package/dist/backend/services/runtime-coordinator-service.js +10 -10
- package/dist/backend/services/session-service.js +44 -19
- package/dist/backend/services/terminal-interrupt-service.js +4 -1
- package/dist/backend/services/turn-reconciler-service.js +1 -0
- package/dist/backend/templates/handoff.js +4 -0
- package/dist/backend/templates/harness/gate-review.js +18 -1
- package/dist/backend/templates/harness/project-manager-agent.js +9 -1
- package/dist/backend/templates/harness/tester-agent.js +15 -7
- package/dist/shared/types/app-settings.js +14 -0
- package/dist/shared/validation/artifact-check.js +29 -3
- package/dist-frontend/assets/index-CDkDHrWQ.js +97 -0
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/dist-frontend/assets/index-CStWyouh.js +0 -97
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ not fall back to another model.
|
|
|
282
282
|
|
|
283
283
|
## Launch Template
|
|
284
284
|
|
|
285
|
-
The launch template stores per-role defaults:
|
|
285
|
+
The global launch template stores per-role defaults for the five workflow roles:
|
|
286
286
|
|
|
287
287
|
- permission mode
|
|
288
288
|
- model
|
|
@@ -293,7 +293,12 @@ One-click start launches the four core roles. If any Gate Review Gate is enabled
|
|
|
293
293
|
it also launches Gate Reviewer.
|
|
294
294
|
|
|
295
295
|
Translator and Harness Engineer are tool roles. They are controlled from their
|
|
296
|
-
own panels, not from the main role tab bar.
|
|
296
|
+
own panels, not from the main role tab bar or the one-click launch template.
|
|
297
|
+
Their permission, model, and effort are stored as independent tool Session
|
|
298
|
+
defaults only after an explicit Start or Restart succeeds. Selector changes,
|
|
299
|
+
Resume, and backend automatic startup do not write these defaults. Without a
|
|
300
|
+
saved value, both tools use `bypassPermissions`, the default model, and medium
|
|
301
|
+
effort. An existing task Session keeps its own recorded values.
|
|
297
302
|
|
|
298
303
|
## Orchestration
|
|
299
304
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isOpenFileLimitError, VcmError } from "../errors.js";
|
|
2
|
+
import { createDefaultToolSessionDefaults } from "../../shared/types/app-settings.js";
|
|
2
3
|
export function registerHarnessRoutes(app, deps) {
|
|
3
4
|
app.get("/api/projects/harness", async (request) => {
|
|
4
5
|
const { task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
|
|
@@ -68,13 +69,17 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
68
69
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
69
70
|
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
70
71
|
await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
|
|
71
|
-
|
|
72
|
+
const result = await deps.harnessService.startHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
|
|
73
|
+
await persistHarnessEngineerDefaults(deps.appSettings, result.session);
|
|
74
|
+
return result;
|
|
72
75
|
});
|
|
73
76
|
app.post("/api/projects/harness/bootstrap/restart", async (request) => {
|
|
74
77
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
75
78
|
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
76
79
|
await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
|
|
77
|
-
|
|
80
|
+
const result = await deps.harnessService.restartHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
|
|
81
|
+
await persistHarnessEngineerDefaults(deps.appSettings, result.session);
|
|
82
|
+
return result;
|
|
78
83
|
});
|
|
79
84
|
app.post("/api/projects/harness/bootstrap/stop", async (request) => {
|
|
80
85
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
@@ -111,7 +116,9 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
111
116
|
const taskSlug = requireTaskSlug(request.body?.taskSlug, "Harness Engineer");
|
|
112
117
|
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
113
118
|
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
114
|
-
|
|
119
|
+
const session = await deps.sessionService.startRoleSession(project.repoRoot, taskSlug, "harness-engineer", request.body);
|
|
120
|
+
await persistHarnessEngineerDefaults(deps.appSettings, session);
|
|
121
|
+
return session;
|
|
115
122
|
});
|
|
116
123
|
app.post("/api/projects/harness/engineer/session/resume", async (request) => {
|
|
117
124
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -125,7 +132,9 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
125
132
|
const taskSlug = requireTaskSlug(request.body?.taskSlug, "Harness Engineer");
|
|
126
133
|
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
127
134
|
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
128
|
-
|
|
135
|
+
const session = await deps.sessionService.restartRoleSession(project.repoRoot, taskSlug, "harness-engineer", request.body);
|
|
136
|
+
await persistHarnessEngineerDefaults(deps.appSettings, session);
|
|
137
|
+
return session;
|
|
129
138
|
});
|
|
130
139
|
app.post("/api/projects/harness/engineer/session/stop", async (request) => {
|
|
131
140
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -201,6 +210,14 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
201
210
|
return deps.autoMemoryService.retryFailedReview(project.repoRoot, task.worktreePath);
|
|
202
211
|
});
|
|
203
212
|
}
|
|
213
|
+
async function persistHarnessEngineerDefaults(appSettings, session) {
|
|
214
|
+
const defaults = createDefaultToolSessionDefaults()["harness-engineer"];
|
|
215
|
+
await appSettings.updateToolSessionDefaults("harness-engineer", {
|
|
216
|
+
permissionMode: session.permissionMode ?? defaults.permissionMode,
|
|
217
|
+
model: session.model ?? defaults.model,
|
|
218
|
+
effort: session.effort ?? defaults.effort
|
|
219
|
+
});
|
|
220
|
+
}
|
|
204
221
|
function requireTaskSlug(value, roleLabel) {
|
|
205
222
|
const taskSlug = value?.trim();
|
|
206
223
|
if (!taskSlug) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VcmError } from "../errors.js";
|
|
2
|
+
import { createDefaultToolSessionDefaults } from "../../shared/types/app-settings.js";
|
|
2
3
|
export function registerTranslationWorkerRoutes(app, deps) {
|
|
3
4
|
app.get("/api/translation/state", async () => {
|
|
4
5
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -29,7 +30,9 @@ export function registerTranslationWorkerRoutes(app, deps) {
|
|
|
29
30
|
app.post("/api/translation/session/start", async (request) => {
|
|
30
31
|
const project = await requireCurrentProject(deps.projectService);
|
|
31
32
|
const taskSlug = requireTaskSlug(request.body?.taskSlug, "Translator");
|
|
32
|
-
|
|
33
|
+
const session = await deps.sessionService.startRoleSession(project.repoRoot, taskSlug, "translator", request.body);
|
|
34
|
+
await persistToolSessionDefaults(deps.appSettings, "translator", session);
|
|
35
|
+
return session;
|
|
33
36
|
});
|
|
34
37
|
app.post("/api/translation/session/resume", async (request) => {
|
|
35
38
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -44,7 +47,9 @@ export function registerTranslationWorkerRoutes(app, deps) {
|
|
|
44
47
|
if (existing) {
|
|
45
48
|
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
46
49
|
}
|
|
47
|
-
|
|
50
|
+
const session = await deps.sessionService.restartRoleSession(project.repoRoot, taskSlug, "translator", request.body);
|
|
51
|
+
await persistToolSessionDefaults(deps.appSettings, "translator", session);
|
|
52
|
+
return session;
|
|
48
53
|
});
|
|
49
54
|
app.post("/api/translation/session/stop", async (request) => {
|
|
50
55
|
const project = await requireCurrentProject(deps.projectService);
|
|
@@ -90,6 +95,14 @@ export function registerTranslationWorkerRoutes(app, deps) {
|
|
|
90
95
|
return deps.translationWorkerService.promoteFileJob(project.repoRoot, request.params.jobId, targetPath);
|
|
91
96
|
});
|
|
92
97
|
}
|
|
98
|
+
async function persistToolSessionDefaults(appSettings, role, session) {
|
|
99
|
+
const defaults = createDefaultToolSessionDefaults().translator;
|
|
100
|
+
await appSettings.updateToolSessionDefaults(role, {
|
|
101
|
+
permissionMode: session.permissionMode,
|
|
102
|
+
model: session.model ?? defaults.model,
|
|
103
|
+
effort: session.effort ?? defaults.effort
|
|
104
|
+
});
|
|
105
|
+
}
|
|
93
106
|
function requireTaskSlug(value, roleLabel) {
|
|
94
107
|
const taskSlug = value?.trim();
|
|
95
108
|
if (!taskSlug) {
|
|
@@ -42,9 +42,9 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
42
42
|
".claude/skills/vcm-codex-review-gate",
|
|
43
43
|
".ai/tools/request-codex-review"
|
|
44
44
|
];
|
|
45
|
-
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'`;
|
|
46
|
-
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 --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
47
|
-
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'`;
|
|
45
|
+
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,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,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'`;
|
|
46
|
+
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,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
47
|
+
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,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,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'`;
|
|
48
48
|
const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
|
|
49
49
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
50
50
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
package/dist/backend/server.js
CHANGED
|
@@ -93,6 +93,7 @@ export async function createServer(deps, options = {}) {
|
|
|
93
93
|
gateReviewService: deps.gateReviewService
|
|
94
94
|
});
|
|
95
95
|
registerTranslationWorkerRoutes(app, {
|
|
96
|
+
appSettings: deps.appSettings,
|
|
96
97
|
projectService: deps.projectService,
|
|
97
98
|
translationWorkerService: deps.translationWorkerService,
|
|
98
99
|
sessionService: deps.sessionService,
|
|
@@ -103,6 +104,7 @@ export async function createServer(deps, options = {}) {
|
|
|
103
104
|
runtimeRecoveryService: deps.runtimeRecoveryService
|
|
104
105
|
});
|
|
105
106
|
registerHarnessRoutes(app, {
|
|
107
|
+
appSettings: deps.appSettings,
|
|
106
108
|
projectService: deps.projectService,
|
|
107
109
|
harnessService: deps.harnessService,
|
|
108
110
|
harnessFeedbackService: deps.harnessFeedbackService,
|
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
4
|
import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
5
|
-
import { createDefaultLaunchTemplate, DEFAULT_TRANSLATION_OUTPUT_MODE, DEFAULT_TRANSLATION_TARGET_LANGUAGE, TRANSLATION_OUTPUT_MODE_OPTIONS, TRANSLATION_TARGET_LANGUAGE_OPTIONS } from "../../shared/types/app-settings.js";
|
|
5
|
+
import { createDefaultLaunchTemplate, createDefaultToolSessionDefaults, DEFAULT_TRANSLATION_OUTPUT_MODE, DEFAULT_TRANSLATION_TARGET_LANGUAGE, TRANSLATION_OUTPUT_MODE_OPTIONS, TRANSLATION_TARGET_LANGUAGE_OPTIONS } from "../../shared/types/app-settings.js";
|
|
6
6
|
import { CLAUDE_MODEL_OPTIONS, CCR_GPT_SESSION_MODEL, SESSION_EFFORT_OPTIONS } from "../../shared/types/session.js";
|
|
7
7
|
import { resolveVcmDataDir } from "../vcm-data-dir.js";
|
|
8
8
|
const MAX_RECENT_REPOSITORIES = 5;
|
|
@@ -73,6 +73,21 @@ export function createAppSettingsService(deps) {
|
|
|
73
73
|
});
|
|
74
74
|
return preferences;
|
|
75
75
|
},
|
|
76
|
+
async updateToolSessionDefaults(role, input) {
|
|
77
|
+
const current = await loadSettings();
|
|
78
|
+
const preferences = normalizePreferences({
|
|
79
|
+
...current.preferences,
|
|
80
|
+
toolSessionDefaults: {
|
|
81
|
+
...current.preferences.toolSessionDefaults,
|
|
82
|
+
[role]: input
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await saveSettings({
|
|
86
|
+
...current,
|
|
87
|
+
preferences
|
|
88
|
+
});
|
|
89
|
+
return preferences.toolSessionDefaults[role];
|
|
90
|
+
},
|
|
76
91
|
async getRecentRepositoryPaths() {
|
|
77
92
|
return (await loadSettings()).recentRepositoryPaths;
|
|
78
93
|
},
|
|
@@ -274,7 +289,8 @@ function normalizePreferences(input) {
|
|
|
274
289
|
translationAutoSendEnabled: candidate.translationAutoSendEnabled === true,
|
|
275
290
|
translationTargetLanguage: normalizeTranslationTargetLanguage(candidate.translationTargetLanguage),
|
|
276
291
|
translationOutputMode: normalizeTranslationOutputMode(candidate.translationOutputMode),
|
|
277
|
-
launchTemplate: normalizeLaunchTemplate(candidate.launchTemplate)
|
|
292
|
+
launchTemplate: normalizeLaunchTemplate(candidate.launchTemplate),
|
|
293
|
+
toolSessionDefaults: normalizeToolSessionDefaults(candidate.toolSessionDefaults)
|
|
278
294
|
};
|
|
279
295
|
}
|
|
280
296
|
function normalizeThemeMode(input) {
|
|
@@ -313,6 +329,14 @@ function normalizeLaunchTemplate(input) {
|
|
|
313
329
|
autoOrchestration: input.autoOrchestration !== false
|
|
314
330
|
};
|
|
315
331
|
}
|
|
332
|
+
function normalizeToolSessionDefaults(input) {
|
|
333
|
+
const defaults = createDefaultToolSessionDefaults();
|
|
334
|
+
const candidate = isObject(input) ? input : {};
|
|
335
|
+
return {
|
|
336
|
+
translator: normalizeRoleLaunchTemplateEntry(candidate.translator, defaults.translator),
|
|
337
|
+
"harness-engineer": normalizeRoleLaunchTemplateEntry(candidate["harness-engineer"], defaults["harness-engineer"])
|
|
338
|
+
};
|
|
339
|
+
}
|
|
316
340
|
function normalizeRoleLaunchTemplateEntry(input, fallback) {
|
|
317
341
|
const candidate = isObject(input) ? input : {};
|
|
318
342
|
return {
|
|
@@ -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,
|
|
@@ -246,6 +265,9 @@ export function createClaudeHookService(deps) {
|
|
|
246
265
|
throwUnsupportedEvent(eventName);
|
|
247
266
|
}
|
|
248
267
|
const context = await getHookContext(input);
|
|
268
|
+
if (!(await isCurrentRoleHook(context, input, eventName))) {
|
|
269
|
+
return completedHookResult(input, eventName);
|
|
270
|
+
}
|
|
249
271
|
const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
|
|
250
272
|
if (memoryResult) {
|
|
251
273
|
return memoryResult;
|
|
@@ -312,6 +334,9 @@ export function createClaudeHookService(deps) {
|
|
|
312
334
|
throwUnsupportedEvent(eventName);
|
|
313
335
|
}
|
|
314
336
|
const context = await getHookContext(input);
|
|
337
|
+
if (!(await isCurrentRoleHook(context, input, eventName))) {
|
|
338
|
+
return completedHookResult(input, eventName);
|
|
339
|
+
}
|
|
315
340
|
const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
|
|
316
341
|
if (memoryResult) {
|
|
317
342
|
return memoryResult;
|
|
@@ -372,6 +397,9 @@ export function createClaudeHookService(deps) {
|
|
|
372
397
|
throwUnsupportedEvent(eventName);
|
|
373
398
|
}
|
|
374
399
|
const context = await getHookContext(input);
|
|
400
|
+
if (!(await isCurrentRoleHook(context, input, eventName))) {
|
|
401
|
+
return completedHookResult(input, eventName);
|
|
402
|
+
}
|
|
375
403
|
const memoryResult = await processAutoMemoryRoleHook(input, context, eventName);
|
|
376
404
|
if (memoryResult) {
|
|
377
405
|
return memoryResult;
|
|
@@ -382,7 +410,9 @@ export function createClaudeHookService(deps) {
|
|
|
382
410
|
eventName,
|
|
383
411
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
384
412
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
385
|
-
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
|
|
386
416
|
});
|
|
387
417
|
return {
|
|
388
418
|
ok: true,
|
|
@@ -396,15 +426,20 @@ export function createClaudeHookService(deps) {
|
|
|
396
426
|
async function recordTurnEnd(input, context, eventName, options) {
|
|
397
427
|
const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
|
|
398
428
|
const settleRouteDispatchInput = createRouteDispatchInput(input, context);
|
|
399
|
-
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
400
429
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
401
430
|
taskSlug: context.taskSlug,
|
|
402
431
|
role: input.role,
|
|
403
432
|
eventName,
|
|
404
433
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
405
434
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
406
|
-
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
|
|
407
438
|
});
|
|
439
|
+
if (!session) {
|
|
440
|
+
return completedHookResult(input, eventName);
|
|
441
|
+
}
|
|
442
|
+
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
408
443
|
if (boundToTask) {
|
|
409
444
|
await deps.roundService.recordClaudeHookEvent({
|
|
410
445
|
repoRoot: context.project.repoRoot,
|
|
@@ -483,8 +518,13 @@ export function createClaudeHookService(deps) {
|
|
|
483
518
|
eventName,
|
|
484
519
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
485
520
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
486
|
-
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
|
|
487
524
|
});
|
|
525
|
+
if (!session) {
|
|
526
|
+
return completedHookResult(input, eventName);
|
|
527
|
+
}
|
|
488
528
|
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
489
529
|
if (boundToTask && eventName !== "PostCompact") {
|
|
490
530
|
await deps.roundService.recordClaudeHookEvent({
|
|
@@ -523,6 +563,31 @@ export function createClaudeHookService(deps) {
|
|
|
523
563
|
...(stoppedRole ? { stoppedRole } : {})
|
|
524
564
|
};
|
|
525
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
|
+
}
|
|
526
591
|
async function scheduleStopFailureRetry(input, context, failure) {
|
|
527
592
|
const preferences = await deps.appSettings.getPreferences();
|
|
528
593
|
if (!preferences.roleRetryEnabled || !deps.runtime) {
|
|
@@ -562,7 +627,10 @@ export function createClaudeHookService(deps) {
|
|
|
562
627
|
nextRetryAt
|
|
563
628
|
}
|
|
564
629
|
});
|
|
565
|
-
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
|
+
}
|
|
566
634
|
scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
|
|
567
635
|
return "scheduled";
|
|
568
636
|
}
|
|
@@ -630,7 +698,7 @@ export function createClaudeHookService(deps) {
|
|
|
630
698
|
}
|
|
631
699
|
});
|
|
632
700
|
await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt());
|
|
633
|
-
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);
|
|
634
702
|
}
|
|
635
703
|
async function markStopFailureRecoveryFailed(input, context, failure, attempt, timestamp = now()) {
|
|
636
704
|
clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
|
|
@@ -736,44 +804,50 @@ export function createClaudeHookService(deps) {
|
|
|
736
804
|
}
|
|
737
805
|
return {
|
|
738
806
|
async handleHook(input) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
});
|
|
758
828
|
},
|
|
759
829
|
handleStopHook(input) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
+
});
|
|
767
839
|
},
|
|
768
840
|
handleReconciledTurnEnd(input) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
});
|
|
777
851
|
},
|
|
778
852
|
handlePermissionRequestHook
|
|
779
853
|
};
|