vibe-coding-master 0.3.28 → 0.3.29
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 +5 -5
- package/dist/backend/api/task-routes.js +2 -2
- package/dist/backend/gateway/gateway-service.js +24 -5
- package/dist/backend/services/claude-hook-service.js +43 -26
- package/dist/backend/services/message-service.js +9 -6
- package/dist/backend/services/project-service.js +2 -2
- package/dist/backend/services/session-service.js +44 -10
- package/dist/shared/constants.js +6 -2
- package/dist-frontend/assets/{index-x9I-bGUt.js → index-K34QFpWK.js} +29 -29
- package/dist-frontend/index.html +1 -1
- package/docs/gate-review-gates.md +13 -5
- package/docs/product-design.md +10 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ VCM is designed for long-running coding work where one Claude Code conversation
|
|
|
11
11
|
|
|
12
12
|
Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, restart, switch, observe, and manually intervene in those sessions without juggling separate terminal windows.
|
|
13
13
|
|
|
14
|
-
When Gate Review Gates are enabled for a task, or when a Gate Reviewer session already exists, the workspace can also show a fifth `Gate Reviewer` terminal role. It
|
|
14
|
+
When Gate Review Gates are enabled for a task, or when a Gate Reviewer session already exists, the workspace can also show a fifth `Gate Reviewer` terminal role. It is an optional VCM flow role powered by Claude Code. Its session is project-scoped and reusable across tasks, but each gate turn is bound to the current task/worktree, writes reports under the task worktree, and stays outside normal PM route-file dispatch.
|
|
15
15
|
|
|
16
16
|
## Current Capabilities
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ When Gate Review Gates are enabled for a task, or when a Gate Reviewer session a
|
|
|
21
21
|
- Connected repository status for the base repo, including branch, upstream status, commit hash, dirty state, and fast-forward-only pull.
|
|
22
22
|
- Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
|
|
23
23
|
- One Claude Code session per role, with role tabs in the task header.
|
|
24
|
-
- Optional Gate Reviewer
|
|
24
|
+
- Optional Gate Reviewer VCM flow role when any Gate Review Gate is enabled.
|
|
25
25
|
- Role session recovery through persisted Claude session ids and `claude --resume`.
|
|
26
26
|
- Permission mode selection before start, resume, or restart:
|
|
27
27
|
- `default`
|
|
@@ -350,14 +350,14 @@ Typical mobile flow:
|
|
|
350
350
|
- `/pull-current`: runs the same fast-forward-only connected repository pull as the desktop `Pull` button.
|
|
351
351
|
- `/tasks`: lists tasks for the selected project.
|
|
352
352
|
- `/use-task <index-or-task-slug>`: selects the Gateway's current task context.
|
|
353
|
-
- `/create-task <task-slug> [title]`: creates a worktree-backed task and starts the four role sessions using the saved launch template.
|
|
353
|
+
- `/create-task <task-slug> [title]`: creates a worktree-backed task and starts the four core role sessions using the saved launch template. If any Gate Review Gate is enabled, Gateway also resumes or starts Gate Reviewer from the saved template.
|
|
354
354
|
- `/close-task`: starts a destructive close confirmation for the current task.
|
|
355
355
|
- `/close-task confirm <task-slug>`: closes the task through VCM cleanup after exact slug confirmation.
|
|
356
356
|
- `/translate on` and `/translate off`: changes Gateway translation for mobile messages.
|
|
357
357
|
|
|
358
358
|
`/pull-current` only pulls the connected base repository. It does not pull task worktrees, stash local changes, merge divergent branches, or run arbitrary shell commands.
|
|
359
359
|
|
|
360
|
-
`/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, effort, and auto orchestration for the four core role sessions. Gateway translation uses the global Gateway translation setting, not the launch template.
|
|
360
|
+
`/create-task` uses the saved launch template from the desktop settings. The template controls permission mode, model, effort, and auto orchestration for the four core role sessions plus the optional Gate Reviewer. Gateway translation uses the global Gateway translation setting, not the launch template.
|
|
361
361
|
|
|
362
362
|
`/close-task` is destructive. It stops VCM-managed role sessions and removes task-owned worktree/branch state according to the same cleanup behavior as the desktop `Close Task` action.
|
|
363
363
|
|
|
@@ -574,7 +574,7 @@ When it is on, VCM is in auto mode:
|
|
|
574
574
|
- When the target role later reaches `Stop`, VCM scans again and may deliver the next pending route file.
|
|
575
575
|
- If auto orchestration gets stuck after a manual copy/paste recovery, `Mark All Done` clears pending route files. It does not mutate message history.
|
|
576
576
|
|
|
577
|
-
VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. Gate Reviewer
|
|
577
|
+
VCM Harness injects Claude Code `UserPromptSubmit` and `Stop` hooks into `.claude/settings.json`. Role tabs become `running` when Claude Code accepts a prompt and `idle` after `Stop`; VCM also marks a role `running` immediately after it writes a message to that embedded terminal. Gate Reviewer uses the same Claude hook/Round/translation path as other VCM flow roles, while VCM still owns gate report polling and the PM callback. The terminal process status is tracked separately. Claude role rules require a role to end its turn after writing or updating a message route file, rather than polling, looping, or waiting for another role inside the same Claude Code turn.
|
|
578
578
|
|
|
579
579
|
The implementation keeps only the active manual/auto orchestration mode. It does not expose pause/resume, stage/approve/reject, or a separate agent-facing message CLI.
|
|
580
580
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CORE_VCM_ROLE_NAMES, DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
2
2
|
import { isOpenFileLimitError, VcmError } from "../errors.js";
|
|
3
3
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
4
4
|
export function registerTaskRoutes(app, deps) {
|
|
@@ -40,7 +40,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
40
40
|
async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
|
|
41
41
|
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
42
42
|
for (const session of sessions) {
|
|
43
|
-
if (session.status === "running" &&
|
|
43
|
+
if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
|
|
44
44
|
await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { CORE_VCM_ROLE_DEFINITIONS, CORE_VCM_ROLE_NAMES, GATE_REVIEWER_ROLE_DEFINITION } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
5
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
@@ -407,17 +407,36 @@ export function createGatewayService(deps) {
|
|
|
407
407
|
taskSlug: task.taskSlug,
|
|
408
408
|
mode: template.autoOrchestration ? "auto" : "manual"
|
|
409
409
|
});
|
|
410
|
+
const gateReviewSettings = await deps.appSettings.getGateReviewSettings(project.repoRoot, task.taskSlug);
|
|
411
|
+
const roleDefinitions = [
|
|
412
|
+
...CORE_VCM_ROLE_DEFINITIONS,
|
|
413
|
+
...(gateReviewSettings.enabled ? [GATE_REVIEWER_ROLE_DEFINITION] : [])
|
|
414
|
+
];
|
|
410
415
|
const startedRoles = [];
|
|
411
|
-
for (const definition of
|
|
416
|
+
for (const definition of roleDefinitions) {
|
|
412
417
|
const roleTemplate = template.roles[definition.name];
|
|
413
418
|
try {
|
|
414
|
-
|
|
419
|
+
const sessionInput = {
|
|
415
420
|
cols: 100,
|
|
416
421
|
rows: 28,
|
|
417
422
|
permissionMode: roleTemplate.permissionMode,
|
|
418
423
|
model: roleTemplate.model,
|
|
419
424
|
effort: roleTemplate.effort
|
|
420
|
-
}
|
|
425
|
+
};
|
|
426
|
+
const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
|
|
427
|
+
if (existing?.status === "running") {
|
|
428
|
+
if (definition.name === "gate-reviewer") {
|
|
429
|
+
await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
430
|
+
}
|
|
431
|
+
startedRoles.push(definition.name);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (existing?.claudeSessionId) {
|
|
435
|
+
await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
await deps.sessionService.startRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
|
|
439
|
+
}
|
|
421
440
|
startedRoles.push(definition.name);
|
|
422
441
|
}
|
|
423
442
|
catch (error) {
|
|
@@ -536,7 +555,7 @@ export function createGatewayService(deps) {
|
|
|
536
555
|
async function stopRunningRoleSessions(repoRoot, taskSlug) {
|
|
537
556
|
const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
|
|
538
557
|
for (const session of sessions) {
|
|
539
|
-
if (session.status === "running") {
|
|
558
|
+
if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
|
|
540
559
|
await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
|
|
541
560
|
}
|
|
542
561
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isVcmRoleName } from "../../shared/constants.js";
|
|
1
|
+
import { isGateReviewerRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
@@ -22,15 +22,32 @@ export function createClaudeHookService(deps) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
const config = await deps.projectService.loadConfig(project.repoRoot);
|
|
25
|
-
const
|
|
25
|
+
const taskSlug = await resolveHookTaskSlug(project.repoRoot, input);
|
|
26
|
+
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
26
27
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
27
28
|
return {
|
|
28
29
|
project,
|
|
29
30
|
config,
|
|
30
31
|
task,
|
|
32
|
+
taskSlug,
|
|
31
33
|
taskRepoRoot
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
async function resolveHookTaskSlug(repoRoot, input) {
|
|
37
|
+
if (!isGateReviewerRoleName(input.role)) {
|
|
38
|
+
return input.taskSlug;
|
|
39
|
+
}
|
|
40
|
+
const session = await deps.sessionService.getProjectGateReviewerSession(repoRoot);
|
|
41
|
+
if (session?.activeTaskSlug) {
|
|
42
|
+
return session.activeTaskSlug;
|
|
43
|
+
}
|
|
44
|
+
throw new VcmError({
|
|
45
|
+
code: "GATE_REVIEWER_TASK_UNBOUND",
|
|
46
|
+
message: "Gate Reviewer hook arrived without an active task binding.",
|
|
47
|
+
statusCode: 409,
|
|
48
|
+
hint: "Start or resume Gate Reviewer from the current task before submitting work."
|
|
49
|
+
});
|
|
50
|
+
}
|
|
34
51
|
async function handleUserPromptSubmitHook(input) {
|
|
35
52
|
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
36
53
|
if (eventName !== "UserPromptSubmit") {
|
|
@@ -39,11 +56,11 @@ export function createClaudeHookService(deps) {
|
|
|
39
56
|
const context = await getHookContext(input);
|
|
40
57
|
deps.jobGuard?.notePromptSubmitted({
|
|
41
58
|
repoRoot: context.project.repoRoot,
|
|
42
|
-
taskSlug:
|
|
59
|
+
taskSlug: context.taskSlug,
|
|
43
60
|
role: input.role
|
|
44
61
|
});
|
|
45
62
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
46
|
-
taskSlug:
|
|
63
|
+
taskSlug: context.taskSlug,
|
|
47
64
|
role: input.role,
|
|
48
65
|
eventName,
|
|
49
66
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
@@ -54,7 +71,7 @@ export function createClaudeHookService(deps) {
|
|
|
54
71
|
repoRoot: context.project.repoRoot,
|
|
55
72
|
stateRepoRoot: context.taskRepoRoot,
|
|
56
73
|
stateRoot: context.config.stateRoot,
|
|
57
|
-
taskSlug:
|
|
74
|
+
taskSlug: context.taskSlug,
|
|
58
75
|
role: input.role,
|
|
59
76
|
eventName
|
|
60
77
|
});
|
|
@@ -62,7 +79,7 @@ export function createClaudeHookService(deps) {
|
|
|
62
79
|
await deps.translationService.recordConversationBoundary({
|
|
63
80
|
repoRoot: context.project.repoRoot,
|
|
64
81
|
taskRepoRoot: context.taskRepoRoot,
|
|
65
|
-
taskSlug:
|
|
82
|
+
taskSlug: context.taskSlug,
|
|
66
83
|
role: input.role,
|
|
67
84
|
sessionId: session.id,
|
|
68
85
|
boundaryKind: "start",
|
|
@@ -75,14 +92,14 @@ export function createClaudeHookService(deps) {
|
|
|
75
92
|
stateRepoRoot: context.taskRepoRoot,
|
|
76
93
|
stateRoot: context.config.stateRoot,
|
|
77
94
|
handoffDir: context.task.handoffDir,
|
|
78
|
-
taskSlug:
|
|
95
|
+
taskSlug: context.taskSlug,
|
|
79
96
|
role: input.role,
|
|
80
97
|
prompt: stringOrUndefined(input.event.prompt)
|
|
81
98
|
});
|
|
82
99
|
return {
|
|
83
100
|
ok: true,
|
|
84
101
|
eventName,
|
|
85
|
-
taskSlug:
|
|
102
|
+
taskSlug: context.taskSlug,
|
|
86
103
|
role: input.role,
|
|
87
104
|
sessionUpdated: Boolean(session),
|
|
88
105
|
dispatchedCount: 0,
|
|
@@ -95,11 +112,11 @@ export function createClaudeHookService(deps) {
|
|
|
95
112
|
throwUnsupportedEvent(eventName);
|
|
96
113
|
}
|
|
97
114
|
const context = await getHookContext(input);
|
|
98
|
-
clearStopFailureRecovery(context.project.repoRoot,
|
|
115
|
+
clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
|
|
99
116
|
if (options.allowBlock && deps.jobGuard) {
|
|
100
117
|
const verdict = await deps.jobGuard.evaluateStop({
|
|
101
118
|
repoRoot: context.project.repoRoot,
|
|
102
|
-
taskSlug:
|
|
119
|
+
taskSlug: context.taskSlug,
|
|
103
120
|
role: input.role,
|
|
104
121
|
taskRepoRoot: context.taskRepoRoot
|
|
105
122
|
});
|
|
@@ -109,7 +126,7 @@ export function createClaudeHookService(deps) {
|
|
|
109
126
|
return {
|
|
110
127
|
ok: true,
|
|
111
128
|
eventName,
|
|
112
|
-
taskSlug:
|
|
129
|
+
taskSlug: context.taskSlug,
|
|
113
130
|
role: input.role,
|
|
114
131
|
sessionUpdated: false,
|
|
115
132
|
dispatchedCount: 0,
|
|
@@ -118,7 +135,7 @@ export function createClaudeHookService(deps) {
|
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
return recordTurnEnd(input, context, eventName, {
|
|
121
|
-
dispatchRouteFiles:
|
|
138
|
+
dispatchRouteFiles: !isGateReviewerRoleName(input.role),
|
|
122
139
|
notifyGateway: true,
|
|
123
140
|
settleGuard: true
|
|
124
141
|
});
|
|
@@ -133,9 +150,9 @@ export function createClaudeHookService(deps) {
|
|
|
133
150
|
const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
|
|
134
151
|
const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
|
|
135
152
|
if (hasCompletionEvidence) {
|
|
136
|
-
clearStopFailureRecovery(context.project.repoRoot,
|
|
153
|
+
clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
|
|
137
154
|
return recordTurnEnd(input, context, eventName, {
|
|
138
|
-
dispatchRouteFiles:
|
|
155
|
+
dispatchRouteFiles: !isGateReviewerRoleName(input.role),
|
|
139
156
|
notifyGateway: false,
|
|
140
157
|
settleGuard: true
|
|
141
158
|
});
|
|
@@ -145,7 +162,7 @@ export function createClaudeHookService(deps) {
|
|
|
145
162
|
return {
|
|
146
163
|
ok: true,
|
|
147
164
|
eventName,
|
|
148
|
-
taskSlug:
|
|
165
|
+
taskSlug: context.taskSlug,
|
|
149
166
|
role: input.role,
|
|
150
167
|
sessionUpdated: true,
|
|
151
168
|
dispatchedCount: 0
|
|
@@ -164,7 +181,7 @@ export function createClaudeHookService(deps) {
|
|
|
164
181
|
}
|
|
165
182
|
const context = await getHookContext(input);
|
|
166
183
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
167
|
-
taskSlug:
|
|
184
|
+
taskSlug: context.taskSlug,
|
|
168
185
|
role: input.role,
|
|
169
186
|
eventName,
|
|
170
187
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
@@ -174,7 +191,7 @@ export function createClaudeHookService(deps) {
|
|
|
174
191
|
return {
|
|
175
192
|
ok: true,
|
|
176
193
|
eventName,
|
|
177
|
-
taskSlug:
|
|
194
|
+
taskSlug: context.taskSlug,
|
|
178
195
|
role: input.role,
|
|
179
196
|
sessionUpdated: Boolean(session),
|
|
180
197
|
dispatchedCount: 0
|
|
@@ -184,7 +201,7 @@ export function createClaudeHookService(deps) {
|
|
|
184
201
|
const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
|
|
185
202
|
const settleRouteDispatchInput = createRouteDispatchInput(input, context);
|
|
186
203
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
187
|
-
taskSlug:
|
|
204
|
+
taskSlug: context.taskSlug,
|
|
188
205
|
role: input.role,
|
|
189
206
|
eventName,
|
|
190
207
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
@@ -195,7 +212,7 @@ export function createClaudeHookService(deps) {
|
|
|
195
212
|
repoRoot: context.project.repoRoot,
|
|
196
213
|
stateRepoRoot: context.taskRepoRoot,
|
|
197
214
|
stateRoot: context.config.stateRoot,
|
|
198
|
-
taskSlug:
|
|
215
|
+
taskSlug: context.taskSlug,
|
|
199
216
|
role: input.role,
|
|
200
217
|
eventName,
|
|
201
218
|
...(options.settleGuard
|
|
@@ -217,7 +234,7 @@ export function createClaudeHookService(deps) {
|
|
|
217
234
|
await deps.translationService.recordConversationBoundary({
|
|
218
235
|
repoRoot: context.project.repoRoot,
|
|
219
236
|
taskRepoRoot: context.taskRepoRoot,
|
|
220
|
-
taskSlug:
|
|
237
|
+
taskSlug: context.taskSlug,
|
|
221
238
|
role: input.role,
|
|
222
239
|
sessionId: session.id,
|
|
223
240
|
boundaryKind: "end",
|
|
@@ -227,7 +244,7 @@ export function createClaudeHookService(deps) {
|
|
|
227
244
|
if (options.notifyGateway && session && input.role === "project-manager") {
|
|
228
245
|
void deps.gatewayService?.handlePmStop({
|
|
229
246
|
repoRoot: context.project.repoRoot,
|
|
230
|
-
taskSlug:
|
|
247
|
+
taskSlug: context.taskSlug,
|
|
231
248
|
session
|
|
232
249
|
}).catch(() => undefined);
|
|
233
250
|
}
|
|
@@ -237,7 +254,7 @@ export function createClaudeHookService(deps) {
|
|
|
237
254
|
return {
|
|
238
255
|
ok: true,
|
|
239
256
|
eventName,
|
|
240
|
-
taskSlug:
|
|
257
|
+
taskSlug: context.taskSlug,
|
|
241
258
|
role: input.role,
|
|
242
259
|
sessionUpdated: Boolean(session),
|
|
243
260
|
dispatchedCount: dispatched.filter((result) => result.delivered).length
|
|
@@ -250,7 +267,7 @@ export function createClaudeHookService(deps) {
|
|
|
250
267
|
stateRepoRoot: context.taskRepoRoot,
|
|
251
268
|
stateRoot: context.config.stateRoot,
|
|
252
269
|
handoffDir: context.task.handoffDir,
|
|
253
|
-
taskSlug:
|
|
270
|
+
taskSlug: context.taskSlug,
|
|
254
271
|
...(stoppedRole ? { stoppedRole } : {})
|
|
255
272
|
};
|
|
256
273
|
}
|
|
@@ -258,18 +275,18 @@ export function createClaudeHookService(deps) {
|
|
|
258
275
|
if (!deps.runtime) {
|
|
259
276
|
return false;
|
|
260
277
|
}
|
|
261
|
-
const key = stopFailureRecoveryKey(context.project.repoRoot,
|
|
278
|
+
const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
|
|
262
279
|
const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
|
|
263
280
|
if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
|
|
264
281
|
return false;
|
|
265
282
|
}
|
|
266
|
-
const session = await deps.sessionService.getRoleSession(context.project.repoRoot,
|
|
283
|
+
const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
|
|
267
284
|
if (!session || session.status !== "running") {
|
|
268
285
|
return false;
|
|
269
286
|
}
|
|
270
287
|
stopFailureRecoveryAttempts.set(key, attempt);
|
|
271
288
|
await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
|
|
272
|
-
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot,
|
|
289
|
+
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
|
|
273
290
|
return true;
|
|
274
291
|
}
|
|
275
292
|
function clearStopFailureRecovery(repoRoot, taskSlug, role) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -282,8 +282,8 @@ async function listRouteFiles(fs, input) {
|
|
|
282
282
|
return routeFiles;
|
|
283
283
|
}
|
|
284
284
|
function parseRouteFileName(fileName) {
|
|
285
|
-
for (const fromRole of
|
|
286
|
-
for (const toRole of
|
|
285
|
+
for (const fromRole of CORE_VCM_ROLE_NAMES) {
|
|
286
|
+
for (const toRole of CORE_VCM_ROLE_NAMES) {
|
|
287
287
|
if (fromRole === toRole) {
|
|
288
288
|
continue;
|
|
289
289
|
}
|
|
@@ -371,14 +371,14 @@ function selectDispatchCandidates(routeFiles, stoppedRole) {
|
|
|
371
371
|
});
|
|
372
372
|
}
|
|
373
373
|
function validateMessagePolicy(fromRole, toRole, type) {
|
|
374
|
-
if (!
|
|
374
|
+
if (!CORE_VCM_ROLE_NAMES.some((role) => role === toRole)) {
|
|
375
375
|
throw new VcmError({
|
|
376
376
|
code: "MESSAGE_TARGET_UNKNOWN",
|
|
377
377
|
message: `Unknown target role: ${toRole}`,
|
|
378
378
|
statusCode: 400
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
|
-
if (!
|
|
381
|
+
if (!CORE_VCM_ROLE_NAMES.some((role) => role === fromRole)) {
|
|
382
382
|
throw new VcmError({
|
|
383
383
|
code: "MESSAGE_SENDER_UNKNOWN",
|
|
384
384
|
message: `Unknown sender role: ${fromRole}`,
|
|
@@ -425,7 +425,7 @@ function getMessageDeliveryTime(message) {
|
|
|
425
425
|
}
|
|
426
426
|
async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
427
427
|
const fromRole = message.fromRole;
|
|
428
|
-
if (!message.routePath || !
|
|
428
|
+
if (!message.routePath || !isCoreRouteRole(fromRole) || !isCoreRouteRole(message.toRole)) {
|
|
429
429
|
return;
|
|
430
430
|
}
|
|
431
431
|
const absolutePath = resolveRepoPath(input.taskRepoRoot ?? input.repoRoot, message.routePath);
|
|
@@ -440,6 +440,9 @@ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
|
440
440
|
await fs.writeText(absolutePath, "");
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
|
+
function isCoreRouteRole(role) {
|
|
444
|
+
return CORE_VCM_ROLE_NAMES.some((candidate) => candidate === role);
|
|
445
|
+
}
|
|
443
446
|
function arraysEqual(left, right) {
|
|
444
447
|
if (left.length !== right.length) {
|
|
445
448
|
return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { VcmError } from "../errors.js";
|
|
4
4
|
const DEFAULT_HANDOFF_ROOT = ".ai/vcm/handoffs";
|
|
5
5
|
const DEFAULT_STATE_ROOT = ".ai/vcm";
|
|
@@ -179,7 +179,7 @@ export function buildDefaultProjectConfig(repoRoot) {
|
|
|
179
179
|
return {
|
|
180
180
|
version: 1,
|
|
181
181
|
repoRoot,
|
|
182
|
-
defaultRoles: [...
|
|
182
|
+
defaultRoles: [...CORE_VCM_ROLE_NAMES],
|
|
183
183
|
handoffRoot: DEFAULT_HANDOFF_ROOT,
|
|
184
184
|
stateRoot: DEFAULT_STATE_ROOT,
|
|
185
185
|
terminalBackend: "node-pty",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { CORE_VCM_ROLE_NAMES, isCodexRoleName, isDispatchableRole } from "../../shared/constants.js";
|
|
4
4
|
import { VcmError } from "../errors.js";
|
|
5
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
6
6
|
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
@@ -101,10 +101,10 @@ export function createSessionService(deps) {
|
|
|
101
101
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
102
102
|
return record;
|
|
103
103
|
}
|
|
104
|
-
async function launchProjectGateReviewerSession(repoRoot, input, launchMode) {
|
|
104
|
+
async function launchProjectGateReviewerSession(repoRoot, input, launchMode, activeTaskSlug) {
|
|
105
105
|
const live = toRoleSessionRecordView(getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
|
|
106
106
|
if (live && live.status === "running") {
|
|
107
|
-
return live;
|
|
107
|
+
return bindProjectGateReviewerSession(repoRoot, live, activeTaskSlug);
|
|
108
108
|
}
|
|
109
109
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
110
110
|
const persisted = await loadPersistedProjectGateReviewerSession(deps.fs, repoRoot);
|
|
@@ -125,6 +125,10 @@ export function createSessionService(deps) {
|
|
|
125
125
|
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
126
126
|
? persisted.transcriptPath
|
|
127
127
|
: claudeTranscriptPath(repoRoot, claudeSessionId);
|
|
128
|
+
const activeTask = activeTaskSlug
|
|
129
|
+
? await deps.taskService.loadTask(repoRoot, activeTaskSlug)
|
|
130
|
+
: undefined;
|
|
131
|
+
const activeTaskRepoRoot = activeTask ? getTaskRuntimeRepoRoot(activeTask) : undefined;
|
|
128
132
|
const startCommand = {
|
|
129
133
|
...deps.claude.buildRoleStartCommand(GATE_REVIEWER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
130
134
|
cwd: repoRoot
|
|
@@ -137,8 +141,8 @@ export function createSessionService(deps) {
|
|
|
137
141
|
cwd: startCommand.cwd,
|
|
138
142
|
env: {
|
|
139
143
|
VCM_API_URL: deps.apiUrl,
|
|
140
|
-
VCM_TASK_REPO_ROOT: repoRoot,
|
|
141
|
-
VCM_TASK_SLUG: PROJECT_GATE_REVIEWER_SCOPE,
|
|
144
|
+
VCM_TASK_REPO_ROOT: activeTaskRepoRoot ?? repoRoot,
|
|
145
|
+
VCM_TASK_SLUG: activeTaskSlug ?? PROJECT_GATE_REVIEWER_SCOPE,
|
|
142
146
|
VCM_ROLE: GATE_REVIEWER_ROLE,
|
|
143
147
|
VCM_SESSION_ID: claudeSessionId
|
|
144
148
|
},
|
|
@@ -164,12 +168,31 @@ export function createSessionService(deps) {
|
|
|
164
168
|
startedAt: runtimeSession.startedAt,
|
|
165
169
|
updatedAt: timestamp,
|
|
166
170
|
lastOutputAt: runtimeSession.lastOutputAt,
|
|
171
|
+
activeTaskSlug,
|
|
172
|
+
activeTaskRepoRoot,
|
|
167
173
|
exitCode: runtimeSession.exitCode
|
|
168
174
|
};
|
|
169
175
|
deps.registry.upsert(record);
|
|
170
176
|
await persistProjectGateReviewerSession(deps.fs, repoRoot, record);
|
|
171
177
|
return record;
|
|
172
178
|
}
|
|
179
|
+
async function bindProjectGateReviewerSession(repoRoot, record, activeTaskSlug) {
|
|
180
|
+
if (!activeTaskSlug) {
|
|
181
|
+
return record;
|
|
182
|
+
}
|
|
183
|
+
const task = await deps.taskService.loadTask(repoRoot, activeTaskSlug);
|
|
184
|
+
const activeTaskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
185
|
+
const updated = {
|
|
186
|
+
...record,
|
|
187
|
+
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
188
|
+
activeTaskSlug,
|
|
189
|
+
activeTaskRepoRoot,
|
|
190
|
+
updatedAt: now()
|
|
191
|
+
};
|
|
192
|
+
deps.registry.upsert(updated);
|
|
193
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, updated);
|
|
194
|
+
return updated;
|
|
195
|
+
}
|
|
173
196
|
async function launchProjectTranslatorSession(repoRoot, input, launchMode) {
|
|
174
197
|
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
175
198
|
if (live && live.status === "running") {
|
|
@@ -377,7 +400,7 @@ export function createSessionService(deps) {
|
|
|
377
400
|
},
|
|
378
401
|
startRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
379
402
|
if (role === GATE_REVIEWER_ROLE) {
|
|
380
|
-
return
|
|
403
|
+
return launchProjectGateReviewerSession(repoRoot, input, "fresh", taskSlug)
|
|
381
404
|
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
382
405
|
}
|
|
383
406
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
@@ -388,7 +411,7 @@ export function createSessionService(deps) {
|
|
|
388
411
|
},
|
|
389
412
|
resumeRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
390
413
|
if (role === GATE_REVIEWER_ROLE) {
|
|
391
|
-
return
|
|
414
|
+
return launchProjectGateReviewerSession(repoRoot, input, "resume", taskSlug)
|
|
392
415
|
.then((session) => scopeProjectRoleSession(session, taskSlug));
|
|
393
416
|
}
|
|
394
417
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
@@ -430,7 +453,14 @@ export function createSessionService(deps) {
|
|
|
430
453
|
},
|
|
431
454
|
async restartRoleSession(repoRoot, taskSlug, role, input = {}) {
|
|
432
455
|
if (role === GATE_REVIEWER_ROLE) {
|
|
433
|
-
|
|
456
|
+
const existing = await this.getProjectGateReviewerSession(repoRoot);
|
|
457
|
+
if (existing && deps.runtime.getSession(existing.id)) {
|
|
458
|
+
await deps.runtime.stop(existing.id);
|
|
459
|
+
}
|
|
460
|
+
if (existing) {
|
|
461
|
+
deps.registry.remove(existing.id);
|
|
462
|
+
}
|
|
463
|
+
return scopeProjectRoleSession(await launchProjectGateReviewerSession(repoRoot, input, "fresh", taskSlug), taskSlug);
|
|
434
464
|
}
|
|
435
465
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
436
466
|
void taskSlug;
|
|
@@ -448,7 +478,11 @@ export function createSessionService(deps) {
|
|
|
448
478
|
},
|
|
449
479
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
450
480
|
if (role === GATE_REVIEWER_ROLE) {
|
|
451
|
-
|
|
481
|
+
const session = await this.getProjectGateReviewerSession(repoRoot);
|
|
482
|
+
if (!session) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
return scopeProjectRoleSession(await bindProjectGateReviewerSession(repoRoot, session, taskSlug), taskSlug);
|
|
452
486
|
}
|
|
453
487
|
if (role === CODEX_TRANSLATOR_ROLE) {
|
|
454
488
|
void taskSlug;
|
|
@@ -470,7 +504,7 @@ export function createSessionService(deps) {
|
|
|
470
504
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
471
505
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
472
506
|
const persistedTaskSession = await loadPersistedTaskSessionRecord(deps.fs, taskRepoRoot, config.stateRoot, taskSlug);
|
|
473
|
-
for (const role of
|
|
507
|
+
for (const role of CORE_VCM_ROLE_NAMES) {
|
|
474
508
|
const record = deps.registry.getByRole(taskSlug, role)
|
|
475
509
|
?? normalizePersistedRoleRecord(persistedTaskSession?.roles[role]?.record);
|
|
476
510
|
const session = toRoleSessionRecordView(record, deps.runtime);
|
package/dist/shared/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_BACKEND_PORT = 4173;
|
|
2
2
|
export const DEFAULT_FRONTEND_PORT = 5173;
|
|
3
|
-
export const
|
|
3
|
+
export const CORE_VCM_ROLE_DEFINITIONS = [
|
|
4
4
|
{
|
|
5
5
|
name: "project-manager",
|
|
6
6
|
label: "Project Manager",
|
|
@@ -32,6 +32,10 @@ export const GATE_REVIEWER_ROLE_DEFINITION = {
|
|
|
32
32
|
commandAgent: "gate-reviewer",
|
|
33
33
|
dispatchable: false
|
|
34
34
|
};
|
|
35
|
+
export const VCM_ROLE_DEFINITIONS = [
|
|
36
|
+
...CORE_VCM_ROLE_DEFINITIONS,
|
|
37
|
+
GATE_REVIEWER_ROLE_DEFINITION
|
|
38
|
+
];
|
|
35
39
|
export const CODEX_TRANSLATOR_ROLE_DEFINITION = {
|
|
36
40
|
name: "codex-translator",
|
|
37
41
|
label: "Codex Translator",
|
|
@@ -43,9 +47,9 @@ export const CODEX_ROLE_DEFINITIONS = [
|
|
|
43
47
|
];
|
|
44
48
|
export const ROLE_DEFINITIONS = [
|
|
45
49
|
...VCM_ROLE_DEFINITIONS,
|
|
46
|
-
GATE_REVIEWER_ROLE_DEFINITION,
|
|
47
50
|
...CODEX_ROLE_DEFINITIONS
|
|
48
51
|
];
|
|
52
|
+
export const CORE_VCM_ROLE_NAMES = CORE_VCM_ROLE_DEFINITIONS.map((role) => role.name);
|
|
49
53
|
export const VCM_ROLE_NAMES = VCM_ROLE_DEFINITIONS.map((role) => role.name);
|
|
50
54
|
export const CODEX_ROLE_NAMES = CODEX_ROLE_DEFINITIONS.map((role) => role.name);
|
|
51
55
|
export const ROLE_NAMES = ROLE_DEFINITIONS.map((role) => role.name);
|