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.
Files changed (50) hide show
  1. package/README.md +49 -17
  2. package/dist/backend/api/artifact-routes.js +3 -0
  3. package/dist/backend/api/harness-routes.js +16 -0
  4. package/dist/backend/api/task-routes.js +32 -2
  5. package/dist/backend/api/workflow-control-routes.js +7 -26
  6. package/dist/backend/cli/install-vcm-harness.js +61 -6
  7. package/dist/backend/role-tool-policy.js +1 -1
  8. package/dist/backend/server.js +14 -5
  9. package/dist/backend/services/artifact-service.js +10 -32
  10. package/dist/backend/services/auto-memory-service.js +642 -14
  11. package/dist/backend/services/claude-hook-service.js +183 -6
  12. package/dist/backend/services/gate-review-service.js +173 -74
  13. package/dist/backend/services/harness-feedback-service.js +180 -80
  14. package/dist/backend/services/harness-service.js +65 -9
  15. package/dist/backend/services/memory-review-paths.js +13 -0
  16. package/dist/backend/services/role-stall-detector-service.js +322 -0
  17. package/dist/backend/services/round-service.js +25 -0
  18. package/dist/backend/services/runtime-coordinator-service.js +12 -1
  19. package/dist/backend/services/session-service.js +70 -3
  20. package/dist/backend/services/status-service.js +1 -0
  21. package/dist/backend/services/translation-worker-service.js +19 -4
  22. package/dist/backend/services/workflow-control-service.js +524 -204
  23. package/dist/backend/templates/handoff.js +46 -5
  24. package/dist/backend/templates/harness/architect-agent.js +12 -6
  25. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
  26. package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
  27. package/dist/backend/templates/harness/claude-root.js +3 -2
  28. package/dist/backend/templates/harness/coder-agent.js +12 -5
  29. package/dist/backend/templates/harness/gate-review.js +150 -57
  30. package/dist/backend/templates/harness/harness-engineer-agent.js +38 -13
  31. package/dist/backend/templates/harness/project-manager-agent.js +18 -12
  32. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
  33. package/dist/backend/templates/harness/tester-agent.js +13 -0
  34. package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
  35. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -6
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
  38. package/dist/shared/types/role-stall.js +1 -0
  39. package/dist/shared/types/workflow.js +15 -0
  40. package/dist/shared/validation/artifact-check.js +3 -3
  41. package/dist/shared/validation/artifact-contract.js +1 -1
  42. package/dist/shared/validation/artifact-registry.js +17 -1
  43. package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
  44. package/dist-frontend/assets/index-BvCmrFlN.js +97 -0
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
  48. package/scripts/harness-tools/vcm-artifact +1 -2
  49. package/scripts/harness-tools/vcm-bash-guard +204 -14
  50. 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 batch?.items ?? [next]) {
227
+ for (const item of dispatchedItems) {
219
228
  item.status = "running";
220
229
  item.updatedAt = dispatchedAt;
221
230
  }
222
- queue.updatedAt = dispatchedAt;
223
- await saveQueue(repoRoot, queue);
224
- await Promise.all((batch?.items ?? [next]).map((item) => syncJobStatus(repoRoot, item)));
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;