vibe-coding-master 0.7.48 → 0.7.50
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 +11 -2
- package/dist/backend/api/session-routes.js +41 -1
- package/dist/backend/server.js +15 -3
- package/dist/backend/services/claude-hook-service.js +3 -0
- package/dist/backend/services/role-context-restart-service.js +240 -0
- package/dist/backend/services/runtime-recovery-service.js +24 -7
- package/dist/backend/services/session-service.js +39 -2
- package/dist/backend/services/task-close-service.js +3 -0
- package/dist/backend/services/task-service.js +13 -0
- package/dist/backend/templates/harness/architect-agent.js +0 -1
- package/dist/backend/templates/harness/coder-agent.js +2 -1
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +1 -4
- package/dist-frontend/assets/{index-nAV6toi8.js → index-DLsIPTvK.js} +34 -34
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,8 +124,9 @@ If you want VCM app state to survive container rebuilds, set:
|
|
|
124
124
|
1. Start VCM with `vcm`.
|
|
125
125
|
2. Open the GUI.
|
|
126
126
|
3. In `Repository`, enter a local Git repository path and click `Connect`.
|
|
127
|
-
4. Create or select a task in the `Task` section.
|
|
128
|
-
|
|
127
|
+
4. Create or select a task in the `Task` section. Before creating a new task,
|
|
128
|
+
VCM fast-forward pulls the connected branch when it has an upstream, then
|
|
129
|
+
creates the task branch and worktree from the updated `HEAD`.
|
|
129
130
|
5. In `VCM Harness`, initialize or update fixed harness files if VCM reports
|
|
130
131
|
pending changes. Harness changes are written to the active task worktree.
|
|
131
132
|
6. If bootstrap is incomplete, open Harness Studio and run bootstrap through
|
|
@@ -221,8 +222,15 @@ Controls:
|
|
|
221
222
|
- `Start`: start a new Claude Code role session
|
|
222
223
|
- `Resume`: resume a saved Claude Code session
|
|
223
224
|
- `Restart`: stop current process and start fresh
|
|
225
|
+
- `Restart With Context`: restart a VCM workflow role and continue from its current task artifacts
|
|
224
226
|
- `Stop`: stop the embedded terminal process
|
|
225
227
|
|
|
228
|
+
`Restart With Context` is available only for Project Manager, Architect, Coder,
|
|
229
|
+
Tester, and Reviewer. It reuses their existing handoffs, workflow state, Gate
|
|
230
|
+
requests, worktree, and Git state; it does not create a separate context file.
|
|
231
|
+
Translator and Harness Engineer are tool Agents and do not use this workflow-role
|
|
232
|
+
recovery path.
|
|
233
|
+
|
|
226
234
|
Permission modes:
|
|
227
235
|
|
|
228
236
|
```text
|
|
@@ -625,6 +633,7 @@ Make sure:
|
|
|
625
633
|
|
|
626
634
|
- the repository is a Git repository
|
|
627
635
|
- the connected base repo is clean
|
|
636
|
+
- the connected branch can be fast-forward pulled when it has an upstream
|
|
628
637
|
- no other task is currently active for this project
|
|
629
638
|
- the derived `feature/<task>` branch does not already exist
|
|
630
639
|
- the derived `.claude/worktrees/<task>` directory does not already exist
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
|
|
1
|
+
import { isDispatchableRole, isRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
export function registerSessionRoutes(app, deps) {
|
|
4
4
|
app.get("/api/tasks/:taskSlug/sessions", async (request) => {
|
|
@@ -8,6 +8,9 @@ export function registerSessionRoutes(app, deps) {
|
|
|
8
8
|
app.post("/api/tasks/:taskSlug/sessions/:role/start", async (request) => {
|
|
9
9
|
const project = await requireCurrentProject(deps.projectService);
|
|
10
10
|
const role = parseRole(request.params.role);
|
|
11
|
+
if (isVcmRoleName(role)) {
|
|
12
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
13
|
+
}
|
|
11
14
|
return deps.sessionService.startRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
12
15
|
});
|
|
13
16
|
app.post("/api/tasks/:taskSlug/sessions/architect/restart-after-planning", async (request, reply) => {
|
|
@@ -18,14 +21,38 @@ export function registerSessionRoutes(app, deps) {
|
|
|
18
21
|
app.post("/api/tasks/:taskSlug/sessions/:role/stop", async (request) => {
|
|
19
22
|
const project = await requireCurrentProject(deps.projectService);
|
|
20
23
|
const role = parseRole(request.params.role);
|
|
24
|
+
if (isVcmRoleName(role)) {
|
|
25
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
26
|
+
}
|
|
21
27
|
const session = await deps.sessionService.stopRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
22
28
|
await deps.translationService.stopSession(session.id);
|
|
23
29
|
deps.roundService.stopSession(session.id);
|
|
24
30
|
return session;
|
|
25
31
|
});
|
|
32
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/restart-with-context", async (request) => {
|
|
33
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
34
|
+
const role = parseVcmRole(request.params.role);
|
|
35
|
+
if (role === "architect" && deps.architectRestartService.getState(project.repoRoot, request.params.taskSlug)) {
|
|
36
|
+
throw new VcmError({
|
|
37
|
+
code: "ARCHITECT_RESTART_CONFLICT",
|
|
38
|
+
message: "Architect already has a pending post-planning restart.",
|
|
39
|
+
statusCode: 409
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
43
|
+
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
44
|
+
if (existing) {
|
|
45
|
+
await deps.translationService.stopSession(existing.id, { clearCache: true });
|
|
46
|
+
deps.roundService.stopSession(existing.id);
|
|
47
|
+
}
|
|
48
|
+
return deps.roleContextRestartService.restart(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
49
|
+
});
|
|
26
50
|
app.post("/api/tasks/:taskSlug/sessions/:role/restart", async (request) => {
|
|
27
51
|
const project = await requireCurrentProject(deps.projectService);
|
|
28
52
|
const role = parseRole(request.params.role);
|
|
53
|
+
if (isVcmRoleName(role)) {
|
|
54
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
55
|
+
}
|
|
29
56
|
const existing = await deps.sessionService.getRoleSession(project.repoRoot, request.params.taskSlug, role);
|
|
30
57
|
await deps.sessionService.assertModelLaunchReady(request.body?.model ?? existing?.model);
|
|
31
58
|
if (existing) {
|
|
@@ -37,6 +64,9 @@ export function registerSessionRoutes(app, deps) {
|
|
|
37
64
|
app.post("/api/tasks/:taskSlug/sessions/:role/resume", async (request) => {
|
|
38
65
|
const project = await requireCurrentProject(deps.projectService);
|
|
39
66
|
const role = parseRole(request.params.role);
|
|
67
|
+
if (isVcmRoleName(role)) {
|
|
68
|
+
await deps.roleContextRestartService.clearRole(project.repoRoot, request.params.taskSlug, role);
|
|
69
|
+
}
|
|
40
70
|
return deps.sessionService.resumeRoleSession(project.repoRoot, request.params.taskSlug, role, request.body);
|
|
41
71
|
});
|
|
42
72
|
app.post("/api/tasks/:taskSlug/sessions/:role/notify-harness", async (request) => {
|
|
@@ -70,6 +100,16 @@ function parseRole(role) {
|
|
|
70
100
|
}
|
|
71
101
|
return role;
|
|
72
102
|
}
|
|
103
|
+
function parseVcmRole(role) {
|
|
104
|
+
if (!isVcmRoleName(role)) {
|
|
105
|
+
throw new VcmError({
|
|
106
|
+
code: "ROLE_CONTEXT_RESTART_UNSUPPORTED",
|
|
107
|
+
message: `Restart With Context is available only for VCM workflow roles: ${role}`,
|
|
108
|
+
statusCode: 400
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return role;
|
|
112
|
+
}
|
|
73
113
|
async function requireCurrentProject(projectService) {
|
|
74
114
|
const project = await projectService.getCurrentProject();
|
|
75
115
|
if (!project) {
|
package/dist/backend/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createAppSettingsService } from "./services/app-settings-service.js";
|
|
|
13
13
|
import { createCodexBridgeIntegrationService } from "./services/codex-bridge-integration-service.js";
|
|
14
14
|
import { createAutoMemoryService } from "./services/auto-memory-service.js";
|
|
15
15
|
import { createArchitectRestartService } from "./services/architect-restart-service.js";
|
|
16
|
+
import { createRoleContextRestartService } from "./services/role-context-restart-service.js";
|
|
16
17
|
import { createRoleStallDetectorService } from "./services/role-stall-detector-service.js";
|
|
17
18
|
import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
|
|
18
19
|
import { createGateReviewService } from "./services/gate-review-service.js";
|
|
@@ -144,7 +145,8 @@ export async function createServer(deps, options = {}) {
|
|
|
144
145
|
commandDispatcher: deps.commandDispatcher,
|
|
145
146
|
translationService: deps.translationService,
|
|
146
147
|
roundService: deps.roundService,
|
|
147
|
-
architectRestartService: deps.architectRestartService
|
|
148
|
+
architectRestartService: deps.architectRestartService,
|
|
149
|
+
roleContextRestartService: deps.roleContextRestartService
|
|
148
150
|
});
|
|
149
151
|
registerArtifactRoutes(app, {
|
|
150
152
|
projectService: deps.projectService,
|
|
@@ -301,6 +303,12 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
301
303
|
sessionService,
|
|
302
304
|
appSettings
|
|
303
305
|
});
|
|
306
|
+
const roleContextRestartService = createRoleContextRestartService({
|
|
307
|
+
fs,
|
|
308
|
+
projectService,
|
|
309
|
+
taskService,
|
|
310
|
+
sessionService
|
|
311
|
+
});
|
|
304
312
|
const messageService = createMessageService({
|
|
305
313
|
fs,
|
|
306
314
|
runtime,
|
|
@@ -378,7 +386,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
378
386
|
roundService,
|
|
379
387
|
projectService,
|
|
380
388
|
taskWorkflowService,
|
|
381
|
-
architectRestartService
|
|
389
|
+
architectRestartService,
|
|
390
|
+
roleContextRestartService
|
|
382
391
|
});
|
|
383
392
|
const gatewayService = createGatewayService({
|
|
384
393
|
fs,
|
|
@@ -401,7 +410,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
401
410
|
projectService,
|
|
402
411
|
taskService,
|
|
403
412
|
translationWorkerService,
|
|
404
|
-
architectRestartService
|
|
413
|
+
architectRestartService,
|
|
414
|
+
roleContextRestartService
|
|
405
415
|
});
|
|
406
416
|
const claudeHookService = createClaudeHookService({
|
|
407
417
|
projectService,
|
|
@@ -419,6 +429,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
419
429
|
jobGuard: createJobGuardService(),
|
|
420
430
|
translationWorkerService,
|
|
421
431
|
architectRestartService,
|
|
432
|
+
roleContextRestartService,
|
|
422
433
|
roleStallDetector,
|
|
423
434
|
workflowControlService
|
|
424
435
|
});
|
|
@@ -466,6 +477,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
466
477
|
taskCloseService,
|
|
467
478
|
taskWorkflowService,
|
|
468
479
|
architectRestartService,
|
|
480
|
+
roleContextRestartService,
|
|
469
481
|
sessionService,
|
|
470
482
|
artifactService,
|
|
471
483
|
harnessService,
|
|
@@ -238,6 +238,9 @@ export function createClaudeHookService(deps) {
|
|
|
238
238
|
if (input.role === "architect") {
|
|
239
239
|
await deps.architectRestartService?.recordReplacementPromptSubmitted(context.project.repoRoot, context.taskSlug, session.id);
|
|
240
240
|
}
|
|
241
|
+
if (isVcmRoleName(input.role)) {
|
|
242
|
+
await deps.roleContextRestartService?.recordPromptSubmitted(context.project.repoRoot, context.taskSlug, input.role, session.id);
|
|
243
|
+
}
|
|
241
244
|
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
242
245
|
if (boundToTask) {
|
|
243
246
|
deps.jobGuard?.notePromptSubmitted({
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
|
+
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
4
|
+
import { toVcmError, VcmError } from "../errors.js";
|
|
5
|
+
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
6
|
+
const STATE_DIR = "restart-with-context";
|
|
7
|
+
export function createRoleContextRestartService(deps) {
|
|
8
|
+
const pending = new Map();
|
|
9
|
+
const operations = new Map();
|
|
10
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
11
|
+
return {
|
|
12
|
+
async restart(repoRoot, taskSlug, role, input = {}) {
|
|
13
|
+
return withLock(restartKey(repoRoot, taskSlug, role), async () => {
|
|
14
|
+
const current = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
15
|
+
if (!current) {
|
|
16
|
+
throw new VcmError({
|
|
17
|
+
code: "SESSION_MISSING",
|
|
18
|
+
message: `${role} session has not been started.`,
|
|
19
|
+
statusCode: 404
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const state = {
|
|
23
|
+
version: 1,
|
|
24
|
+
taskSlug,
|
|
25
|
+
role,
|
|
26
|
+
sourceSessionId: current.id,
|
|
27
|
+
status: "launching",
|
|
28
|
+
permissionMode: input.permissionMode ?? current.permissionMode,
|
|
29
|
+
model: input.model ?? current.model,
|
|
30
|
+
effort: input.effort ?? current.effort,
|
|
31
|
+
cols: input.cols,
|
|
32
|
+
rows: input.rows,
|
|
33
|
+
updatedAt: now()
|
|
34
|
+
};
|
|
35
|
+
await persist(repoRoot, state);
|
|
36
|
+
return launch(repoRoot, state);
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
async recoverTask(repoRoot, taskSlug) {
|
|
40
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
41
|
+
await withLock(restartKey(repoRoot, taskSlug, role), async () => {
|
|
42
|
+
const state = await load(repoRoot, taskSlug, role);
|
|
43
|
+
if (!state) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, role);
|
|
47
|
+
if (state.replacementSessionId
|
|
48
|
+
&& session?.id === state.replacementSessionId
|
|
49
|
+
&& session.claudeSessionId) {
|
|
50
|
+
await remove(repoRoot, state);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
state.status = "launching";
|
|
54
|
+
state.error = undefined;
|
|
55
|
+
state.updatedAt = now();
|
|
56
|
+
await persist(repoRoot, state);
|
|
57
|
+
await launch(repoRoot, state);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async recordPromptSubmitted(repoRoot, taskSlug, role, sessionId) {
|
|
62
|
+
await withLock(restartKey(repoRoot, taskSlug, role), async () => {
|
|
63
|
+
const state = pending.get(restartKey(repoRoot, taskSlug, role))
|
|
64
|
+
?? await load(repoRoot, taskSlug, role);
|
|
65
|
+
if (!state
|
|
66
|
+
|| state.status !== "awaiting_prompt_confirmation"
|
|
67
|
+
|| state.replacementSessionId !== sessionId) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await remove(repoRoot, state);
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async clearRole(repoRoot, taskSlug, role) {
|
|
74
|
+
await withLock(restartKey(repoRoot, taskSlug, role), async () => {
|
|
75
|
+
pending.delete(restartKey(repoRoot, taskSlug, role));
|
|
76
|
+
const target = await statePath(repoRoot, taskSlug, role);
|
|
77
|
+
if (await deps.fs.pathExists(target)) {
|
|
78
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
async clear(repoRoot, taskSlug) {
|
|
83
|
+
for (const role of VCM_ROLE_NAMES) {
|
|
84
|
+
pending.delete(restartKey(repoRoot, taskSlug, role));
|
|
85
|
+
}
|
|
86
|
+
const target = await stateDirectory(repoRoot, taskSlug);
|
|
87
|
+
if (await deps.fs.pathExists(target)) {
|
|
88
|
+
await deps.fs.removePath?.(target, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
async function launch(repoRoot, state) {
|
|
93
|
+
const prompts = await buildPrompts(repoRoot, state.taskSlug, state.role);
|
|
94
|
+
try {
|
|
95
|
+
const replacement = await deps.sessionService.restartRoleSessionForContext(repoRoot, state.taskSlug, state.role, {
|
|
96
|
+
permissionMode: state.permissionMode,
|
|
97
|
+
model: state.model,
|
|
98
|
+
effort: state.effort,
|
|
99
|
+
cols: state.cols,
|
|
100
|
+
rows: state.rows,
|
|
101
|
+
appendSystemPrompt: prompts.system
|
|
102
|
+
});
|
|
103
|
+
state.replacementSessionId = replacement.id;
|
|
104
|
+
state.status = "awaiting_prompt_confirmation";
|
|
105
|
+
state.updatedAt = now();
|
|
106
|
+
await persist(repoRoot, state);
|
|
107
|
+
await deps.sessionService.submitRolePrompt(repoRoot, state.taskSlug, state.role, replacement.id, prompts.user);
|
|
108
|
+
return replacement;
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const normalized = toVcmError(error);
|
|
112
|
+
state.status = "blocked";
|
|
113
|
+
state.error = {
|
|
114
|
+
code: normalized.code,
|
|
115
|
+
message: normalized.message
|
|
116
|
+
};
|
|
117
|
+
state.updatedAt = now();
|
|
118
|
+
await persist(repoRoot, state);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function buildPrompts(repoRoot, taskSlug, role) {
|
|
123
|
+
const [config, task] = await Promise.all([
|
|
124
|
+
deps.projectService.loadConfig(repoRoot),
|
|
125
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
126
|
+
]);
|
|
127
|
+
const handoffDir = task.handoffDir;
|
|
128
|
+
const stateRoot = config.stateRoot;
|
|
129
|
+
const files = roleContextFiles(role, handoffDir, stateRoot);
|
|
130
|
+
return {
|
|
131
|
+
system: [
|
|
132
|
+
`This fresh ${role} session continues the current VCM task after Restart With Context.`,
|
|
133
|
+
"",
|
|
134
|
+
"Before continuing, read the existing files or directories that apply:",
|
|
135
|
+
...files.map((file) => `- ${file}`),
|
|
136
|
+
"- the current worktree and Git state",
|
|
137
|
+
"",
|
|
138
|
+
"Treat the current artifacts and worktree as the source of truth. Continue the accepted assignment from the recorded current state. Do not repeat completed work or rely on the previous Session transcript."
|
|
139
|
+
].join("\n"),
|
|
140
|
+
user: `[VCM RESTART WITH CONTEXT]\nRestore the current ${role} work from the listed task artifacts and continue the accepted assignment.`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function persist(repoRoot, state) {
|
|
144
|
+
await deps.fs.writeJsonAtomic(await statePath(repoRoot, state.taskSlug, state.role), state);
|
|
145
|
+
pending.set(restartKey(repoRoot, state.taskSlug, state.role), state);
|
|
146
|
+
}
|
|
147
|
+
async function load(repoRoot, taskSlug, role) {
|
|
148
|
+
const target = await statePath(repoRoot, taskSlug, role);
|
|
149
|
+
if (!(await deps.fs.pathExists(target))) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const state = await deps.fs.readJson(target);
|
|
153
|
+
if (state.version !== 1
|
|
154
|
+
|| state.taskSlug !== taskSlug
|
|
155
|
+
|| state.role !== role
|
|
156
|
+
|| !["launching", "awaiting_prompt_confirmation", "blocked"].includes(state.status)) {
|
|
157
|
+
throw new VcmError({
|
|
158
|
+
code: "ROLE_CONTEXT_RESTART_STATE_INVALID",
|
|
159
|
+
message: `Restart With Context state is invalid for ${role} in task ${taskSlug}.`,
|
|
160
|
+
statusCode: 500
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
pending.set(restartKey(repoRoot, taskSlug, role), state);
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
async function remove(repoRoot, state) {
|
|
167
|
+
pending.delete(restartKey(repoRoot, state.taskSlug, state.role));
|
|
168
|
+
const target = await statePath(repoRoot, state.taskSlug, state.role);
|
|
169
|
+
if (await deps.fs.pathExists(target)) {
|
|
170
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function stateDirectory(repoRoot, taskSlug) {
|
|
174
|
+
const [config, task] = await Promise.all([
|
|
175
|
+
deps.projectService.loadConfig(repoRoot),
|
|
176
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
177
|
+
]);
|
|
178
|
+
return resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(config.stateRoot, STATE_DIR));
|
|
179
|
+
}
|
|
180
|
+
async function statePath(repoRoot, taskSlug, role) {
|
|
181
|
+
return path.join(await stateDirectory(repoRoot, taskSlug), `${role}.json`);
|
|
182
|
+
}
|
|
183
|
+
async function withLock(key, operation) {
|
|
184
|
+
const previous = operations.get(key) ?? Promise.resolve();
|
|
185
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
186
|
+
operations.set(key, current);
|
|
187
|
+
try {
|
|
188
|
+
return await current;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (operations.get(key) === current) {
|
|
192
|
+
operations.delete(key);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function roleContextFiles(role, handoffDir, stateRoot) {
|
|
198
|
+
switch (role) {
|
|
199
|
+
case "project-manager":
|
|
200
|
+
return [
|
|
201
|
+
`${handoffDir}/workflow-progress.md`,
|
|
202
|
+
`${stateRoot}/workflow/state.json`,
|
|
203
|
+
`${stateRoot}/workflow-control.json`,
|
|
204
|
+
`${handoffDir}/messages/`
|
|
205
|
+
];
|
|
206
|
+
case "architect":
|
|
207
|
+
return [
|
|
208
|
+
`${handoffDir}/role-commands/architect.md`,
|
|
209
|
+
`${handoffDir}/architecture-brief.md`,
|
|
210
|
+
`${handoffDir}/architecture-evidence.md`,
|
|
211
|
+
`${handoffDir}/planning-progress.md`,
|
|
212
|
+
`${handoffDir}/architecture-plan.md`,
|
|
213
|
+
`${handoffDir}/architect-debug.md`,
|
|
214
|
+
`${handoffDir}/architecture-diagnosis.md`,
|
|
215
|
+
".ai/vcm/gate-reviews/index.json"
|
|
216
|
+
];
|
|
217
|
+
case "coder":
|
|
218
|
+
return [
|
|
219
|
+
`${handoffDir}/role-commands/coder.md`,
|
|
220
|
+
`${handoffDir}/architecture-plan.md`,
|
|
221
|
+
`${handoffDir}/coder-completion.md`,
|
|
222
|
+
".ai/vcm/coder-workers/"
|
|
223
|
+
];
|
|
224
|
+
case "tester":
|
|
225
|
+
return [
|
|
226
|
+
`${handoffDir}/role-commands/tester.md`,
|
|
227
|
+
`${handoffDir}/architecture-plan.md`,
|
|
228
|
+
`${handoffDir}/coder-completion.md`,
|
|
229
|
+
`${handoffDir}/test-report.md`
|
|
230
|
+
];
|
|
231
|
+
case "reviewer":
|
|
232
|
+
return [
|
|
233
|
+
".ai/vcm/gate-reviews/index.json",
|
|
234
|
+
".ai/vcm/gate-reviews/requests/"
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function restartKey(repoRoot, taskSlug, role) {
|
|
239
|
+
return `${repoRoot}\0${taskSlug}\0${role}`;
|
|
240
|
+
}
|
|
@@ -34,8 +34,9 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
34
34
|
const roundRecovered = await recoverRound(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
35
35
|
await recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
36
36
|
await recoverGateReview(taskRepoRoot, recoveredAt, context);
|
|
37
|
-
await
|
|
37
|
+
await recoverCoderWorkers(taskRepoRoot, task.taskSlug, context);
|
|
38
38
|
await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
39
|
+
await deps.roleContextRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
39
40
|
if ((roundRecovered || task.status === "running") && !hasLiveTaskSession(task.taskSlug)) {
|
|
40
41
|
await deps.taskService.updateTaskStatus(repoRoot, task.taskSlug, "stopped");
|
|
41
42
|
}
|
|
@@ -237,16 +238,32 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
237
238
|
});
|
|
238
239
|
context.changedPaths.add(relativePath);
|
|
239
240
|
}
|
|
240
|
-
async function
|
|
241
|
-
|
|
242
|
-
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
241
|
+
async function recoverCoderWorkers(taskRepoRoot, taskSlug, context) {
|
|
242
|
+
if (hasLiveRoundRole(taskSlug, "coder") || !deps.fs.removePath) {
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
|
-
|
|
245
|
+
const tasksPath = path.join(taskRepoRoot, CODER_WORKERS_RUNTIME_DIR, "tasks");
|
|
246
|
+
if (!(await deps.fs.pathExists(tasksPath))) {
|
|
246
247
|
return;
|
|
247
248
|
}
|
|
248
|
-
await deps.fs.
|
|
249
|
-
|
|
249
|
+
for (const entry of await deps.fs.readDir(tasksPath)) {
|
|
250
|
+
if (!entry.endsWith(".json")) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const absolutePath = path.join(tasksPath, entry);
|
|
254
|
+
let state;
|
|
255
|
+
try {
|
|
256
|
+
state = await deps.fs.readJson(absolutePath);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (state.status !== "running" || state.handled === true) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
await deps.fs.removePath(absolutePath, { force: true });
|
|
265
|
+
context.changedPaths.add(path.posix.join(CODER_WORKERS_RUNTIME_DIR, "tasks", entry));
|
|
266
|
+
}
|
|
250
267
|
}
|
|
251
268
|
async function recoverHarnessBootstrap(repoRoot, _timestamp, context) {
|
|
252
269
|
const absolutePath = path.join(repoRoot, BOOTSTRAP_SESSION_PATH);
|
|
@@ -45,7 +45,7 @@ export function createSessionService(deps) {
|
|
|
45
45
|
harnessOutdated: sessionRevision < currentRevision
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
|
|
48
|
+
async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode, options = {}) {
|
|
49
49
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
50
50
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
51
51
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
@@ -145,7 +145,7 @@ export function createSessionService(deps) {
|
|
|
145
145
|
};
|
|
146
146
|
deps.registry.upsert(record);
|
|
147
147
|
await persistRoleSessionRecord(deps.fs, repoRoot, taskRepoRoot, config.stateRoot, record);
|
|
148
|
-
if (role === "project-manager") {
|
|
148
|
+
if (role === "project-manager" && options.restoreProjectManagerContext !== false) {
|
|
149
149
|
await restoreProjectManagerWorkflowContext(record, taskRepoRoot, config.stateRoot);
|
|
150
150
|
}
|
|
151
151
|
return withHarnessRevisionView(taskRepoRoot, record);
|
|
@@ -1110,6 +1110,43 @@ export function createSessionService(deps) {
|
|
|
1110
1110
|
await clearPersistedRoleSessionRecord(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, taskSlug, role, now());
|
|
1111
1111
|
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
|
|
1112
1112
|
},
|
|
1113
|
+
async restartRoleSessionForContext(repoRoot, taskSlug, role, input = {}) {
|
|
1114
|
+
const existing = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
1115
|
+
if (!existing) {
|
|
1116
|
+
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh", {
|
|
1117
|
+
restoreProjectManagerContext: false
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
await getModelLaunchEnvironment(normalizeClaudeModel(input.model ?? existing.model));
|
|
1121
|
+
if (deps.runtime.getSession(existing.id)) {
|
|
1122
|
+
await deps.runtime.stop(existing.id);
|
|
1123
|
+
}
|
|
1124
|
+
deps.registry.remove(existing.id);
|
|
1125
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
1126
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
1127
|
+
await clearPersistedRoleSessionRecord(deps.fs, getTaskRuntimeRepoRoot(task), config.stateRoot, taskSlug, role, now());
|
|
1128
|
+
return launchRoleSession(repoRoot, taskSlug, role, input, "fresh", {
|
|
1129
|
+
restoreProjectManagerContext: false
|
|
1130
|
+
});
|
|
1131
|
+
},
|
|
1132
|
+
async submitRolePrompt(repoRoot, taskSlug, role, expectedSessionId, prompt) {
|
|
1133
|
+
const session = await this.getRoleSession(repoRoot, taskSlug, role);
|
|
1134
|
+
if (!session || session.id !== expectedSessionId || session.status !== "running") {
|
|
1135
|
+
throw new VcmError({
|
|
1136
|
+
code: "ROLE_SESSION_NOT_RUNNING",
|
|
1137
|
+
message: `${role} replacement session is not running.`,
|
|
1138
|
+
statusCode: 409
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if ((await waitForSessionInputReady(session.id)) === "exited") {
|
|
1142
|
+
throw new VcmError({
|
|
1143
|
+
code: "ROLE_SESSION_START_FAILED",
|
|
1144
|
+
message: `${role} replacement session exited before it could accept the recovery prompt.`,
|
|
1145
|
+
statusCode: 409
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
await submitTerminalInput(deps.runtime, session.id, prompt);
|
|
1149
|
+
},
|
|
1113
1150
|
async getRoleSession(repoRoot, taskSlug, role) {
|
|
1114
1151
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
1115
1152
|
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
@@ -8,6 +8,9 @@ export function createTaskCloseService(deps) {
|
|
|
8
8
|
if (deps.architectRestartService) {
|
|
9
9
|
await bestEffort("Unable to clear Architect restart state", () => deps.architectRestartService.clear(repoRoot, taskSlug), warnings);
|
|
10
10
|
}
|
|
11
|
+
if (deps.roleContextRestartService) {
|
|
12
|
+
await bestEffort("Unable to clear Restart With Context state", () => deps.roleContextRestartService.clear(repoRoot, taskSlug), warnings);
|
|
13
|
+
}
|
|
11
14
|
await stopTaskRoleSessions(repoRoot, taskSlug, warnings);
|
|
12
15
|
await bestEffort("Unable to stop task translation runtime", () => deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true }), warnings);
|
|
13
16
|
await bestEffort("Unable to clear task round runtime", () => deps.roundService.stopTask(taskSlug), warnings);
|
|
@@ -53,6 +53,19 @@ export function createTaskService(deps) {
|
|
|
53
53
|
hint: `Commit, stash, or discard these changes before creating a task worktree: ${baseVisibleChanges.slice(0, 12).join(", ")}`
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
+
const upstreamBranch = await deps.git.getUpstreamBranch(repoRoot);
|
|
57
|
+
if (upstreamBranch) {
|
|
58
|
+
await deps.git.pullFastForward(repoRoot);
|
|
59
|
+
const postPullVisibleChanges = await getBaseRepoVisibleChanges(deps.git, repoRoot);
|
|
60
|
+
if (postPullVisibleChanges.length > 0) {
|
|
61
|
+
throw new VcmError({
|
|
62
|
+
code: "BASE_REPO_DIRTY",
|
|
63
|
+
message: "The connected repository has Git-visible changes after pulling its upstream branch.",
|
|
64
|
+
statusCode: 409,
|
|
65
|
+
hint: `Commit, stash, or discard these changes before creating a task worktree: ${postPullVisibleChanges.slice(0, 12).join(", ")}`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
const timestamp = now();
|
|
57
70
|
await deps.fs.ensureDir(path.dirname(worktreePath));
|
|
58
71
|
await deps.git.createWorktree({
|
|
@@ -51,7 +51,6 @@ ${renderRoleMemoryRules("architect")}
|
|
|
51
51
|
- Use \`vcm-architect-validation-worker\` for exact non-interactive commands already selected by Architect. The worker does not select validation scope, modify tests or code, diagnose failures, or decide whether validation is sufficient.
|
|
52
52
|
- Do not rerun a green command already reported by another Architect worker merely to execute it in the main Architect context.
|
|
53
53
|
- Validation workers write reports under \`.ai/vcm/architect-workers/validation/\`. Architect must interpret the raw results and copy required evidence into the owning Architect artifact.
|
|
54
|
-
- Remove \`.ai/vcm/architect-workers/\` after its accepted facts and command results have been consolidated into Architect-owned artifacts.
|
|
55
54
|
|
|
56
55
|
### Architecture Interview
|
|
57
56
|
|
|
@@ -74,7 +74,7 @@ ${renderRoleMemoryRules("coder")}
|
|
|
74
74
|
- Stay in the same Coder turn until every worker state is \`completed\` and Coder has reviewed and integrated all reports and commits. Do not end the turn to wait for worker callbacks.
|
|
75
75
|
- A completed worker reports \`Implementation Result: success|has_failed_items\`; \`completed\` means the full assigned sweep and handoff finished, not that every item passed.
|
|
76
76
|
- After workers finish, inspect each item disposition and commit, integrate successful work and committed failure scenes, resolve integration conflicts or invalid edits, and verify that every remaining marker corresponds to a failed disposition. Only then mark \`handled: true\` in each worker state.
|
|
77
|
-
- Run coder-level baseline validation
|
|
77
|
+
- Run coder-level baseline validation and summarize worker reports and commits in \`.ai/vcm/handoffs/coder-completion.md\`.
|
|
78
78
|
|
|
79
79
|
### Handoff
|
|
80
80
|
|
|
@@ -85,6 +85,7 @@ ${renderRoleMemoryRules("coder")}
|
|
|
85
85
|
|
|
86
86
|
- In Docs-Only Flow, commit the documentation changes and submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` with the decision, changed and reviewed documents, evidence, checks, commit, and remaining documentation issues. Do not submit \`coder-completion.md\` for Docs-Only work.
|
|
87
87
|
- Submit \`.ai/vcm/handoffs/coder-completion.md\` before routing back to project-manager: write a candidate outside \`.ai/vcm\`, then run \`.ai/tools/vcm-artifact coder-completion --file <candidate> --mode draft|final\`. This file is the complete, self-contained current implementation completion evidence, not a log. Each revision must restate every Scaffold Manifest disposition, changed file, helper, deviation, generated-context result, baseline-test change, L0/L1 command and result, worker result, commit, and objective failure still needed to review the current implementation without a prior revision. Replace stale content instead of appending history.
|
|
88
|
+
- Maintain an incomplete \`coder-completion.md\` draft while implementation is in progress. Replace it after each completed module or worker result and after compile, L0, or L1 results so it always states completed work, remaining scaffold items, current validation, and current commits. The final submission replaces this draft.
|
|
88
89
|
- After committing the actual implementation state and before submitting a final \`coder-completion.md\`, run \`.ai/tools/check-scaffold-ledger --mode completion --completion <candidate>\`; submit the candidate only after it passes. An incomplete draft does not use completion mode.
|
|
89
90
|
- \`coder-completion.md\` must include \`Decision: ready_for_review | incomplete | failed\`.
|
|
90
91
|
- \`coder-completion.md\` must report every Scaffold Manifest item disposition in the fixed Scaffold Completion table, plus changed files, private helpers added, manifest deviations as report-only facts, generated context status, baseline tests added or updated, L0/L1 commands and results, worker commits and integration status when workers were used, and compile/typecheck or L0/L1 failures.
|
|
@@ -151,6 +151,7 @@ Coverage Gap.
|
|
|
151
151
|
### Outputs
|
|
152
152
|
|
|
153
153
|
- Write \`.ai/vcm/handoffs/test-report.md\` with \`Test Result: pass|fail|incomplete\`, evidence reviewed, tests added or updated, coverage mapping, validation progress, commands run or checked, validation results, test-infrastructure status and evidence, failed expectations, reproduction steps, skipped checks with reasons, coverage gaps, blocking validation issues, and user approval evidence.
|
|
154
|
+
- Maintain an incomplete \`test-report.md\` draft while validation is in progress. Replace it after each completed validation level or long-running check and after changing tests or fixtures so it always states completed validation, remaining validation, current results, and current commits. The final submission replaces this draft.
|
|
154
155
|
- \`test-report.md\` must include this test-infrastructure section:
|
|
155
156
|
|
|
156
157
|
\`\`\`md
|
|
@@ -18,7 +18,7 @@ The hard ceiling is 60 minutes per job, enforced by the job worker itself. No ap
|
|
|
18
18
|
3. If watch-job exits 125, the job is still running and a bounded handoff is active for the next watcher: run \`.ai/tools/watch-job <job-id>\` again immediately. Do not end the turn between windows.
|
|
19
19
|
4. Repeat until watch-job reports a terminal result.
|
|
20
20
|
5. Read the final status and the relevant log tail.
|
|
21
|
-
6. Record command, result, duration, and required follow-up wherever the caller normally records command evidence.
|
|
21
|
+
6. Record job ID, command, result, duration, and required follow-up wherever the caller normally records command evidence.
|
|
22
22
|
|
|
23
23
|
Example:
|
|
24
24
|
|
|
@@ -71,8 +71,5 @@ On timeout the worker stops the command process group and records \`timeout\` in
|
|
|
71
71
|
- do not mark the command as passed
|
|
72
72
|
- do not retry in the background
|
|
73
73
|
|
|
74
|
-
## Cleanup
|
|
75
|
-
|
|
76
|
-
\`.ai/vcm/jobs/**\` is runtime state. Delete it after the command result and useful log evidence have been recorded where needed.
|
|
77
74
|
`;
|
|
78
75
|
}
|