vibe-coding-master 0.7.42 → 0.7.44
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 +49 -17
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +16 -0
- package/dist/backend/api/task-routes.js +32 -2
- package/dist/backend/api/workflow-control-routes.js +7 -26
- package/dist/backend/cli/install-vcm-harness.js +61 -6
- package/dist/backend/role-tool-policy.js +1 -1
- package/dist/backend/server.js +14 -5
- package/dist/backend/services/artifact-service.js +10 -32
- package/dist/backend/services/auto-memory-service.js +642 -14
- package/dist/backend/services/claude-hook-service.js +183 -6
- package/dist/backend/services/gate-review-service.js +173 -74
- package/dist/backend/services/harness-feedback-service.js +180 -80
- package/dist/backend/services/harness-service.js +65 -9
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +12 -1
- package/dist/backend/services/session-service.js +70 -3
- package/dist/backend/services/status-service.js +1 -0
- package/dist/backend/services/translation-worker-service.js +19 -4
- package/dist/backend/services/workflow-control-service.js +524 -204
- package/dist/backend/templates/handoff.js +46 -5
- package/dist/backend/templates/harness/architect-agent.js +12 -6
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
- package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
- package/dist/backend/templates/harness/claude-root.js +3 -2
- package/dist/backend/templates/harness/coder-agent.js +12 -5
- package/dist/backend/templates/harness/gate-review.js +150 -57
- package/dist/backend/templates/harness/harness-engineer-agent.js +38 -13
- package/dist/backend/templates/harness/project-manager-agent.js +18 -12
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
- package/dist/backend/templates/harness/tester-agent.js +13 -0
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -6
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +15 -0
- package/dist/shared/validation/artifact-check.js +3 -3
- package/dist/shared/validation/artifact-contract.js +1 -1
- package/dist/shared/validation/artifact-registry.js +17 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/index-BvCmrFlN.js +97 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
- package/scripts/harness-tools/vcm-artifact +1 -2
- package/scripts/harness-tools/vcm-bash-guard +204 -14
- package/dist-frontend/assets/index-Bocc2DWF.js +0 -97
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export const ROLE_MODEL_STALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
export const ROLE_TOOL_STALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
5
|
+
export const ROLE_SUBAGENT_STALL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
6
|
+
const TOOL_TIMEOUT_GRACE_MS = 30_000;
|
|
7
|
+
export function createRoleStallDetectorService(deps) {
|
|
8
|
+
const modelTimeoutMs = deps.modelTimeoutMs ?? ROLE_MODEL_STALL_TIMEOUT_MS;
|
|
9
|
+
const toolTimeoutMs = deps.toolTimeoutMs ?? ROLE_TOOL_STALL_TIMEOUT_MS;
|
|
10
|
+
const subagentTimeoutMs = deps.subagentTimeoutMs ?? ROLE_SUBAGENT_STALL_TIMEOUT_MS;
|
|
11
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
12
|
+
const setTimer = deps.setTimeout ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
|
|
13
|
+
const clearTimer = deps.clearTimeout ?? ((timer) => globalThis.clearTimeout(timer));
|
|
14
|
+
const monitors = new Map();
|
|
15
|
+
return {
|
|
16
|
+
async recordHook(input) {
|
|
17
|
+
if (!isVcmRoleName(input.role)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
21
|
+
if (input.eventName === "Stop" || input.eventName === "StopFailure") {
|
|
22
|
+
clearMonitor(key);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const [session, round] = await Promise.all([
|
|
26
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role),
|
|
27
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
28
|
+
]);
|
|
29
|
+
if (!session
|
|
30
|
+
|| session.status !== "running"
|
|
31
|
+
|| session.activityStatus !== "running"
|
|
32
|
+
|| round.status !== "running"
|
|
33
|
+
|| round.activeRole !== input.role
|
|
34
|
+
|| !round.roundId) {
|
|
35
|
+
clearMonitor(key);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let monitor = monitors.get(key);
|
|
39
|
+
if (!monitor
|
|
40
|
+
|| monitor.sessionId !== session.id
|
|
41
|
+
|| monitor.runtimeSessionToken !== session.runtimeSessionToken
|
|
42
|
+
|| monitor.roundId !== round.roundId
|
|
43
|
+
|| monitor.role !== input.role) {
|
|
44
|
+
clearMonitor(key);
|
|
45
|
+
monitor = {
|
|
46
|
+
context: taskInput(input),
|
|
47
|
+
role: input.role,
|
|
48
|
+
sessionId: session.id,
|
|
49
|
+
runtimeSessionToken: session.runtimeSessionToken,
|
|
50
|
+
roundId: round.roundId,
|
|
51
|
+
phase: "awaiting-model",
|
|
52
|
+
phaseStartedAt: now(),
|
|
53
|
+
generation: 0,
|
|
54
|
+
activeTools: new Map()
|
|
55
|
+
};
|
|
56
|
+
monitors.set(key, monitor);
|
|
57
|
+
}
|
|
58
|
+
advanceMonitor(key, monitor, input.eventName, input.event);
|
|
59
|
+
},
|
|
60
|
+
async reconcileTask(input) {
|
|
61
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
62
|
+
for (const [candidateKey, monitor] of monitors) {
|
|
63
|
+
if (monitor.context.repoRoot === input.repoRoot && candidateKey !== key) {
|
|
64
|
+
clearMonitor(candidateKey);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const monitor = monitors.get(key);
|
|
68
|
+
if (!monitor) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const [session, round] = await Promise.all([
|
|
72
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, monitor.role),
|
|
73
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
74
|
+
]);
|
|
75
|
+
if (!session
|
|
76
|
+
|| session.id !== monitor.sessionId
|
|
77
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
78
|
+
|| session.status !== "running"
|
|
79
|
+
|| session.activityStatus !== "running"
|
|
80
|
+
|| round.status !== "running"
|
|
81
|
+
|| round.activeRole !== monitor.role
|
|
82
|
+
|| round.roundId !== monitor.roundId) {
|
|
83
|
+
clearMonitor(key);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
getWarning(repoRoot, taskSlug) {
|
|
87
|
+
const warning = monitors.get(monitorKey(repoRoot, taskSlug))?.warning;
|
|
88
|
+
return warning ? { ...warning } : null;
|
|
89
|
+
},
|
|
90
|
+
ignoreWarning(repoRoot, taskSlug, warningId) {
|
|
91
|
+
const monitor = requireWarning(repoRoot, taskSlug, warningId);
|
|
92
|
+
monitor.ignoredGeneration = monitor.generation;
|
|
93
|
+
monitor.warning = undefined;
|
|
94
|
+
cancelTimer(monitor);
|
|
95
|
+
return { ok: true, warning: null };
|
|
96
|
+
},
|
|
97
|
+
async recoverWarning(input) {
|
|
98
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
99
|
+
const monitor = requireWarning(input.repoRoot, input.taskSlug, input.warningId);
|
|
100
|
+
const [session, round] = await Promise.all([
|
|
101
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, monitor.role),
|
|
102
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
103
|
+
]);
|
|
104
|
+
if (!session
|
|
105
|
+
|| session.id !== monitor.sessionId
|
|
106
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
107
|
+
|| session.status !== "running"
|
|
108
|
+
|| session.activityStatus !== "running"
|
|
109
|
+
|| round.status !== "running"
|
|
110
|
+
|| round.activeRole !== monitor.role
|
|
111
|
+
|| round.roundId !== monitor.roundId) {
|
|
112
|
+
clearMonitor(key);
|
|
113
|
+
return { ok: true, warning: null };
|
|
114
|
+
}
|
|
115
|
+
await deps.sessionService.recoverRoleSession(input.repoRoot, input.taskSlug, {
|
|
116
|
+
role: monitor.role,
|
|
117
|
+
expectedSessionId: monitor.sessionId,
|
|
118
|
+
expectedRuntimeSessionToken: monitor.runtimeSessionToken,
|
|
119
|
+
recoveryPrompt: renderRecoveryPrompt(monitor)
|
|
120
|
+
});
|
|
121
|
+
clearMonitor(key);
|
|
122
|
+
return { ok: true, warning: null };
|
|
123
|
+
},
|
|
124
|
+
clearProject(repoRoot) {
|
|
125
|
+
for (const [key, monitor] of monitors) {
|
|
126
|
+
if (monitor.context.repoRoot === repoRoot) {
|
|
127
|
+
clearMonitor(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
stop() {
|
|
132
|
+
for (const key of [...monitors.keys()]) {
|
|
133
|
+
clearMonitor(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function advanceMonitor(key, monitor, eventName, event) {
|
|
138
|
+
monitor.generation += 1;
|
|
139
|
+
monitor.warning = undefined;
|
|
140
|
+
monitor.ignoredGeneration = undefined;
|
|
141
|
+
cancelTimer(monitor);
|
|
142
|
+
switch (eventName) {
|
|
143
|
+
case "UserPromptSubmit":
|
|
144
|
+
case "PostCompact":
|
|
145
|
+
case "PostToolBatch":
|
|
146
|
+
monitor.activeTools.clear();
|
|
147
|
+
monitor.currentTool = undefined;
|
|
148
|
+
setPhase(key, monitor, "awaiting-model", modelTimeoutMs);
|
|
149
|
+
return;
|
|
150
|
+
case "PreToolUse": {
|
|
151
|
+
const tool = readTool(event, toolTimeoutMs, subagentTimeoutMs);
|
|
152
|
+
const toolKey = tool.toolUseId ?? `anonymous-${monitor.generation}`;
|
|
153
|
+
monitor.activeTools.set(toolKey, tool);
|
|
154
|
+
monitor.currentTool = tool;
|
|
155
|
+
setPhase(key, monitor, tool.toolName === "Agent" ? "subagent-running" : "tool-running", tool.timeoutMs);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case "SubagentStart":
|
|
159
|
+
setPhase(key, monitor, "subagent-running", subagentTimeoutMs);
|
|
160
|
+
return;
|
|
161
|
+
case "PostToolUse":
|
|
162
|
+
case "PostToolUseFailure": {
|
|
163
|
+
const toolUseId = stringValue(event.tool_use_id);
|
|
164
|
+
if (toolUseId) {
|
|
165
|
+
monitor.activeTools.delete(toolUseId);
|
|
166
|
+
}
|
|
167
|
+
monitor.currentTool = firstTool(monitor.activeTools);
|
|
168
|
+
if (monitor.currentTool) {
|
|
169
|
+
setPhase(key, monitor, monitor.currentTool.toolName === "Agent" ? "subagent-running" : "tool-running", monitor.currentTool.timeoutMs);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "SubagentStop":
|
|
174
|
+
monitor.currentTool = firstTool(monitor.activeTools);
|
|
175
|
+
if (monitor.currentTool) {
|
|
176
|
+
setPhase(key, monitor, monitor.currentTool.toolName === "Agent" ? "subagent-running" : "tool-running", monitor.currentTool.timeoutMs);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
case "PreCompact":
|
|
180
|
+
monitor.activeTools.clear();
|
|
181
|
+
monitor.currentTool = undefined;
|
|
182
|
+
setPhase(key, monitor, "compacting", modelTimeoutMs);
|
|
183
|
+
return;
|
|
184
|
+
case "PermissionRequest":
|
|
185
|
+
setPhase(key, monitor, "awaiting-permission");
|
|
186
|
+
return;
|
|
187
|
+
default:
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function setPhase(key, monitor, phase, timeoutMs) {
|
|
192
|
+
monitor.phase = phase;
|
|
193
|
+
monitor.phaseStartedAt = now();
|
|
194
|
+
if (timeoutMs === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const generation = monitor.generation;
|
|
198
|
+
monitor.timer = setTimer(() => {
|
|
199
|
+
void detectStall(key, generation).catch(() => undefined);
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
}
|
|
202
|
+
async function detectStall(key, generation) {
|
|
203
|
+
const monitor = monitors.get(key);
|
|
204
|
+
if (!monitor || monitor.generation !== generation || monitor.ignoredGeneration === generation) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const [session, round] = await Promise.all([
|
|
208
|
+
deps.sessionService.getRoleSession(monitor.context.repoRoot, monitor.context.taskSlug, monitor.role),
|
|
209
|
+
deps.roundService.getSessionRoundState(roundInput(monitor.context))
|
|
210
|
+
]);
|
|
211
|
+
if (!session
|
|
212
|
+
|| session.id !== monitor.sessionId
|
|
213
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
214
|
+
|| session.status !== "running"
|
|
215
|
+
|| session.activityStatus !== "running"
|
|
216
|
+
|| round.status !== "running"
|
|
217
|
+
|| round.activeRole !== monitor.role
|
|
218
|
+
|| round.roundId !== monitor.roundId) {
|
|
219
|
+
clearMonitor(key);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const detectedAt = now();
|
|
223
|
+
monitor.timer = undefined;
|
|
224
|
+
monitor.warning = {
|
|
225
|
+
id: `${monitor.sessionId}:${monitor.roundId}:${generation}:${detectedAt}`,
|
|
226
|
+
taskSlug: monitor.context.taskSlug,
|
|
227
|
+
role: monitor.role,
|
|
228
|
+
sessionId: monitor.sessionId,
|
|
229
|
+
runtimeSessionToken: monitor.runtimeSessionToken,
|
|
230
|
+
roundId: monitor.roundId,
|
|
231
|
+
phase: monitor.phase,
|
|
232
|
+
phaseStartedAt: monitor.phaseStartedAt,
|
|
233
|
+
detectedAt,
|
|
234
|
+
toolUseId: monitor.currentTool?.toolUseId,
|
|
235
|
+
toolName: monitor.currentTool?.toolName
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function requireWarning(repoRoot, taskSlug, warningId) {
|
|
239
|
+
const monitor = monitors.get(monitorKey(repoRoot, taskSlug));
|
|
240
|
+
if (!monitor?.warning || monitor.warning.id !== warningId) {
|
|
241
|
+
throw new VcmError({
|
|
242
|
+
code: "ROLE_STALL_WARNING_STALE",
|
|
243
|
+
message: "The role stall warning is no longer active.",
|
|
244
|
+
statusCode: 409,
|
|
245
|
+
hint: "Refresh the task workspace before retrying the action."
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return monitor;
|
|
249
|
+
}
|
|
250
|
+
function cancelTimer(monitor) {
|
|
251
|
+
if (monitor.timer === undefined) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
clearTimer(monitor.timer);
|
|
255
|
+
monitor.timer = undefined;
|
|
256
|
+
}
|
|
257
|
+
function clearMonitor(key) {
|
|
258
|
+
const monitor = monitors.get(key);
|
|
259
|
+
if (!monitor) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
cancelTimer(monitor);
|
|
263
|
+
monitors.delete(key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function monitorKey(repoRoot, taskSlug) {
|
|
267
|
+
return `${repoRoot}:${taskSlug}`;
|
|
268
|
+
}
|
|
269
|
+
function taskInput(input) {
|
|
270
|
+
return {
|
|
271
|
+
repoRoot: input.repoRoot,
|
|
272
|
+
taskRepoRoot: input.taskRepoRoot,
|
|
273
|
+
stateRoot: input.stateRoot,
|
|
274
|
+
taskSlug: input.taskSlug
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function roundInput(input) {
|
|
278
|
+
return {
|
|
279
|
+
repoRoot: input.repoRoot,
|
|
280
|
+
stateRepoRoot: input.taskRepoRoot,
|
|
281
|
+
stateRoot: input.stateRoot,
|
|
282
|
+
taskSlug: input.taskSlug
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function readTool(event, defaultTimeoutMs, subagentTimeoutMs) {
|
|
286
|
+
const toolName = stringValue(event.tool_name);
|
|
287
|
+
const toolUseId = stringValue(event.tool_use_id);
|
|
288
|
+
const input = objectValue(event.tool_input);
|
|
289
|
+
const requestedTimeout = numberValue(input?.timeout);
|
|
290
|
+
return {
|
|
291
|
+
toolName,
|
|
292
|
+
toolUseId,
|
|
293
|
+
timeoutMs: toolName === "Agent"
|
|
294
|
+
? subagentTimeoutMs
|
|
295
|
+
: Math.max(defaultTimeoutMs, requestedTimeout ? requestedTimeout + TOOL_TIMEOUT_GRACE_MS : 0)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function firstTool(tools) {
|
|
299
|
+
return tools.values().next().value;
|
|
300
|
+
}
|
|
301
|
+
function renderRecoveryPrompt(monitor) {
|
|
302
|
+
const phase = monitor.currentTool?.toolName
|
|
303
|
+
? `${monitor.phase} (${monitor.currentTool.toolName})`
|
|
304
|
+
: monitor.phase;
|
|
305
|
+
return [
|
|
306
|
+
"[VCM Session Recovery]",
|
|
307
|
+
`The previous ${monitor.role} turn may have stalled during ${phase}.`,
|
|
308
|
+
"Continue the current assigned work from the existing Claude session context.",
|
|
309
|
+
"Do not repeat completed work unless it is needed to verify the current state."
|
|
310
|
+
].join("\n");
|
|
311
|
+
}
|
|
312
|
+
function objectValue(value) {
|
|
313
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
314
|
+
? value
|
|
315
|
+
: undefined;
|
|
316
|
+
}
|
|
317
|
+
function stringValue(value) {
|
|
318
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
319
|
+
}
|
|
320
|
+
function numberValue(value) {
|
|
321
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
322
|
+
}
|
|
@@ -280,6 +280,31 @@ export function createRoundService(deps) {
|
|
|
280
280
|
return toSessionRoundState(next, timestamp);
|
|
281
281
|
});
|
|
282
282
|
},
|
|
283
|
+
async recordRoleRecoveryFailure(input) {
|
|
284
|
+
return withTaskLock(input, async () => {
|
|
285
|
+
const timestamp = now();
|
|
286
|
+
const state = await load(input);
|
|
287
|
+
const stopped = applyTerminalExit({
|
|
288
|
+
state,
|
|
289
|
+
taskSlug: input.taskSlug,
|
|
290
|
+
role: input.recovery.role,
|
|
291
|
+
timestamp
|
|
292
|
+
});
|
|
293
|
+
const next = {
|
|
294
|
+
...stopped,
|
|
295
|
+
roleRecovery: normalizeRoleRecovery({
|
|
296
|
+
...input.recovery,
|
|
297
|
+
status: "failed",
|
|
298
|
+
failedAt: input.recovery.failedAt ?? timestamp
|
|
299
|
+
}),
|
|
300
|
+
updatedAt: timestamp
|
|
301
|
+
};
|
|
302
|
+
await save(input, next);
|
|
303
|
+
clearSettleTimer(input);
|
|
304
|
+
await updateSessionStatus(input, next.currentRound?.status === "running" ? "running" : "stopped");
|
|
305
|
+
return toSessionRoundState(next, timestamp);
|
|
306
|
+
});
|
|
307
|
+
},
|
|
283
308
|
async setRoleRecovery(input) {
|
|
284
309
|
return withTaskLock(input, async () => {
|
|
285
310
|
const timestamp = now();
|
|
@@ -42,9 +42,17 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
42
42
|
]);
|
|
43
43
|
const preferences = await deps.appSettings.getPreferences();
|
|
44
44
|
if (!activeTask) {
|
|
45
|
+
deps.roleStallDetector.clearProject(repoRoot);
|
|
45
46
|
return { activeTask: null, gatewayStatus };
|
|
46
47
|
}
|
|
47
48
|
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
49
|
+
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
50
|
+
await deps.roleStallDetector.reconcileTask({
|
|
51
|
+
repoRoot,
|
|
52
|
+
taskRepoRoot,
|
|
53
|
+
stateRoot,
|
|
54
|
+
taskSlug: activeTask.taskSlug
|
|
55
|
+
});
|
|
48
56
|
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
49
57
|
.then((status) => status.initialized)
|
|
50
58
|
.catch(() => false);
|
|
@@ -58,7 +66,8 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
58
66
|
else {
|
|
59
67
|
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
60
68
|
}
|
|
61
|
-
await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
69
|
+
const memoryState = await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
70
|
+
await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState.status);
|
|
62
71
|
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
63
72
|
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
64
73
|
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
@@ -77,6 +86,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
77
86
|
void reconcileCurrentProject().catch(() => undefined);
|
|
78
87
|
},
|
|
79
88
|
stop() {
|
|
89
|
+
deps.roleStallDetector.stop();
|
|
80
90
|
if (reconcileTimer === undefined) {
|
|
81
91
|
return;
|
|
82
92
|
}
|
|
@@ -88,6 +98,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
88
98
|
async function reconcileCurrentProject() {
|
|
89
99
|
const project = await deps.projectService.getCurrentProject();
|
|
90
100
|
if (!project) {
|
|
101
|
+
deps.roleStallDetector.stop();
|
|
91
102
|
return;
|
|
92
103
|
}
|
|
93
104
|
await reconcileProject(project.repoRoot);
|
|
@@ -721,7 +721,8 @@ export function createSessionService(deps) {
|
|
|
721
721
|
return undefined;
|
|
722
722
|
}
|
|
723
723
|
const timestamp = now();
|
|
724
|
-
const turnWasRunning = current.activityStatus === "running"
|
|
724
|
+
const turnWasRunning = current.activityStatus === "running"
|
|
725
|
+
&& current.expectedRuntimeExitReason !== "role-stall-recovery";
|
|
725
726
|
const updated = {
|
|
726
727
|
...current,
|
|
727
728
|
status: input.status,
|
|
@@ -737,6 +738,71 @@ export function createSessionService(deps) {
|
|
|
737
738
|
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
|
|
738
739
|
return { record: updated, turnWasRunning };
|
|
739
740
|
}
|
|
741
|
+
async function persistTaskRoleRecord(repoRoot, taskSlug, record) {
|
|
742
|
+
deps.registry.upsert(record);
|
|
743
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
744
|
+
const task = await deps.taskService.loadTask(repoRoot, taskSlug);
|
|
745
|
+
await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, record);
|
|
746
|
+
return record;
|
|
747
|
+
}
|
|
748
|
+
async function recoverRoleSession(repoRoot, taskSlug, input) {
|
|
749
|
+
const current = await getTaskRoleSessionView(repoRoot, taskSlug, input.role);
|
|
750
|
+
if (!current
|
|
751
|
+
|| current.id !== input.expectedSessionId
|
|
752
|
+
|| current.runtimeSessionToken !== input.expectedRuntimeSessionToken
|
|
753
|
+
|| current.status !== "running"
|
|
754
|
+
|| current.activityStatus !== "running") {
|
|
755
|
+
throw new VcmError({
|
|
756
|
+
code: "ROLE_STALL_SESSION_CHANGED",
|
|
757
|
+
message: `${input.role} session changed before recovery could begin.`,
|
|
758
|
+
statusCode: 409
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (!current.claudeSessionId) {
|
|
762
|
+
throw new VcmError({
|
|
763
|
+
code: "CLAUDE_SESSION_MISSING",
|
|
764
|
+
message: `${input.role} does not have a confirmed Claude session id to resume.`,
|
|
765
|
+
statusCode: 409
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
const prepared = await persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
769
|
+
...current,
|
|
770
|
+
expectedRuntimeExitReason: "role-stall-recovery",
|
|
771
|
+
updatedAt: now()
|
|
772
|
+
});
|
|
773
|
+
if (deps.runtime.getSession(prepared.id)) {
|
|
774
|
+
await deps.runtime.stop(prepared.id);
|
|
775
|
+
}
|
|
776
|
+
await persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
777
|
+
...prepared,
|
|
778
|
+
status: "exited",
|
|
779
|
+
activityStatus: "idle",
|
|
780
|
+
pid: undefined,
|
|
781
|
+
expectedRuntimeExitReason: undefined,
|
|
782
|
+
updatedAt: now()
|
|
783
|
+
});
|
|
784
|
+
const resumed = await launchRoleSession(repoRoot, taskSlug, input.role, {
|
|
785
|
+
permissionMode: prepared.permissionMode,
|
|
786
|
+
model: prepared.model,
|
|
787
|
+
effort: prepared.effort
|
|
788
|
+
}, "resume");
|
|
789
|
+
if ((await waitForSessionInputReady(resumed.id)) === "exited") {
|
|
790
|
+
throw new VcmError({
|
|
791
|
+
code: "ROLE_STALL_RESUME_FAILED",
|
|
792
|
+
message: `${input.role} session exited before recovery input could be submitted.`,
|
|
793
|
+
statusCode: 409
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
await submitTerminalInput(deps.runtime, resumed.id, input.recoveryPrompt);
|
|
797
|
+
return persistTaskRoleRecord(repoRoot, taskSlug, {
|
|
798
|
+
...resumed,
|
|
799
|
+
activityStatus: "running",
|
|
800
|
+
lastTurnStartedAt: now(),
|
|
801
|
+
lastHookEventAt: now(),
|
|
802
|
+
expectedRuntimeExitReason: undefined,
|
|
803
|
+
updatedAt: now()
|
|
804
|
+
});
|
|
805
|
+
}
|
|
740
806
|
return {
|
|
741
807
|
async assertModelLaunchReady(model = "default") {
|
|
742
808
|
await getModelLaunchEnvironment(normalizeClaudeModel(model));
|
|
@@ -1204,7 +1270,8 @@ export function createSessionService(deps) {
|
|
|
1204
1270
|
: undefined;
|
|
1205
1271
|
}
|
|
1206
1272
|
return markTaskRoleActivityIdle(repoRoot, taskSlug, role);
|
|
1207
|
-
}
|
|
1273
|
+
},
|
|
1274
|
+
recoverRoleSession
|
|
1208
1275
|
};
|
|
1209
1276
|
}
|
|
1210
1277
|
function toRoleSessionRecordView(record, runtime) {
|
|
@@ -1294,7 +1361,7 @@ function isSessionMissingError(error) {
|
|
|
1294
1361
|
error.code === "SESSION_MISSING";
|
|
1295
1362
|
}
|
|
1296
1363
|
function withoutRuntimeOnlySessionFields(session) {
|
|
1297
|
-
const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, ...persisted } = session;
|
|
1364
|
+
const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, expectedRuntimeExitReason: _expectedRuntimeExitReason, ...persisted } = session;
|
|
1298
1365
|
return persisted;
|
|
1299
1366
|
}
|
|
1300
1367
|
function defaultIsProcessAlive(pid) {
|
|
@@ -59,6 +59,7 @@ function degradedArtifactSummary(handoffDir) {
|
|
|
59
59
|
architectDebugPath: `${handoffDir}/architect-debug.md`,
|
|
60
60
|
architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
|
|
61
61
|
testReportPath: `${handoffDir}/test-report.md`,
|
|
62
|
+
docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
|
|
62
63
|
docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
|
|
63
64
|
workflowProgressPath: `${handoffDir}/workflow-progress.md`,
|
|
64
65
|
finalAcceptancePath: `${handoffDir}/final-acceptance.md`
|
|
@@ -214,14 +214,23 @@ export function createTranslationWorkerService(deps) {
|
|
|
214
214
|
}
|
|
215
215
|
const session = await ensureTranslatorSession(repoRoot, next.targetLanguage, next.taskSlug);
|
|
216
216
|
await submitTerminalInput(deps.runtime, session.id, batch?.prompt ?? await buildQueuePrompt(repoRoot, next));
|
|
217
|
+
const latestQueue = await loadQueue(repoRoot);
|
|
218
|
+
if (latestQueue.activeItemId !== next.id) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const dispatchedItems = latestQueue.items.filter((item) => item.status === "dispatching" &&
|
|
222
|
+
(batch ? item.batchId === batch.items[0]?.batchId : item.id === next.id));
|
|
223
|
+
if (dispatchedItems.length === 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
217
226
|
const dispatchedAt = now();
|
|
218
|
-
for (const item of
|
|
227
|
+
for (const item of dispatchedItems) {
|
|
219
228
|
item.status = "running";
|
|
220
229
|
item.updatedAt = dispatchedAt;
|
|
221
230
|
}
|
|
222
|
-
|
|
223
|
-
await saveQueue(repoRoot,
|
|
224
|
-
await Promise.all(
|
|
231
|
+
latestQueue.updatedAt = dispatchedAt;
|
|
232
|
+
await saveQueue(repoRoot, latestQueue);
|
|
233
|
+
await Promise.all(dispatchedItems.map((item) => syncJobStatus(repoRoot, item)));
|
|
225
234
|
}
|
|
226
235
|
catch (error) {
|
|
227
236
|
const failedItems = queue.activeItemId === next.id
|
|
@@ -438,6 +447,12 @@ export function createTranslationWorkerService(deps) {
|
|
|
438
447
|
await validateActiveQueueItem(repoRoot);
|
|
439
448
|
return true;
|
|
440
449
|
}
|
|
450
|
+
// `dispatching` covers the interval between writing the prompt and the
|
|
451
|
+
// Translator's UserPromptSubmit hook. The role session is legitimately idle
|
|
452
|
+
// during that interval, so only an actual result can reconcile the item.
|
|
453
|
+
if (active.status === "dispatching") {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
441
456
|
if (await translatorSessionSettled(repoRoot, active.taskSlug)) {
|
|
442
457
|
await validateActiveQueueItem(repoRoot);
|
|
443
458
|
return true;
|