vibe-coding-master 0.3.25 → 0.3.26
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/dist/backend/cli/install-vcm-harness.js +2 -0
- package/dist/backend/server.js +1 -0
- package/dist/backend/services/claude-hook-service.js +138 -28
- package/dist/backend/services/harness-service.js +2 -0
- package/dist/backend/services/session-service.js +18 -8
- package/docs/product-design.md +12 -7
- package/package.json +1 -1
|
@@ -31,6 +31,8 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
31
31
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
32
32
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
33
33
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
34
|
+
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
35
|
+
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
34
36
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
35
37
|
];
|
|
36
38
|
const AGENT_FRONTMATTER = {
|
package/dist/backend/server.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
|
+
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
3
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
5
|
+
const MAX_STOP_FAILURE_RECOVERY_ATTEMPTS = 2;
|
|
4
6
|
export function createClaudeHookService(deps) {
|
|
7
|
+
const stopFailureRecoveryAttempts = new Map();
|
|
5
8
|
async function getHookContext(input) {
|
|
6
9
|
if (!isVcmRoleName(input.role)) {
|
|
7
10
|
throw new VcmError({
|
|
@@ -92,6 +95,7 @@ export function createClaudeHookService(deps) {
|
|
|
92
95
|
throwUnsupportedEvent(eventName);
|
|
93
96
|
}
|
|
94
97
|
const context = await getHookContext(input);
|
|
98
|
+
clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
|
|
95
99
|
if (options.allowBlock && deps.jobGuard) {
|
|
96
100
|
const verdict = await deps.jobGuard.evaluateStop({
|
|
97
101
|
repoRoot: context.project.repoRoot,
|
|
@@ -113,23 +117,72 @@ export function createClaudeHookService(deps) {
|
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
return recordTurnEnd(input, context, eventName, {
|
|
121
|
+
dispatchRouteFiles: true,
|
|
122
|
+
notifyGateway: true,
|
|
123
|
+
settleGuard: true
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function processStopFailureHook(input) {
|
|
127
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
128
|
+
if (eventName !== "StopFailure") {
|
|
129
|
+
throwUnsupportedEvent(eventName);
|
|
130
|
+
}
|
|
131
|
+
const context = await getHookContext(input);
|
|
132
|
+
const routeDispatchInput = createRouteDispatchInput(input, context);
|
|
133
|
+
const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
|
|
134
|
+
const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
|
|
135
|
+
if (hasCompletionEvidence) {
|
|
136
|
+
clearStopFailureRecovery(context.project.repoRoot, input.taskSlug, input.role);
|
|
137
|
+
return recordTurnEnd(input, context, eventName, {
|
|
138
|
+
dispatchRouteFiles: true,
|
|
139
|
+
notifyGateway: false,
|
|
140
|
+
settleGuard: true
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const recovered = await dispatchStopFailureRecovery(input, context);
|
|
144
|
+
if (recovered) {
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
eventName,
|
|
148
|
+
taskSlug: input.taskSlug,
|
|
149
|
+
role: input.role,
|
|
150
|
+
sessionUpdated: true,
|
|
151
|
+
dispatchedCount: 0
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return recordTurnEnd(input, context, eventName, {
|
|
155
|
+
dispatchRouteFiles: false,
|
|
156
|
+
notifyGateway: false,
|
|
157
|
+
settleGuard: false
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function processPostCompactHook(input) {
|
|
161
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
162
|
+
if (eventName !== "PostCompact") {
|
|
163
|
+
throwUnsupportedEvent(eventName);
|
|
164
|
+
}
|
|
165
|
+
const context = await getHookContext(input);
|
|
166
|
+
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
122
167
|
taskSlug: input.taskSlug,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
role: input.role,
|
|
169
|
+
eventName,
|
|
170
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
171
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
172
|
+
cwd: stringOrUndefined(input.event.cwd)
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
eventName,
|
|
177
|
+
taskSlug: input.taskSlug,
|
|
178
|
+
role: input.role,
|
|
179
|
+
sessionUpdated: Boolean(session),
|
|
180
|
+
dispatchedCount: 0
|
|
132
181
|
};
|
|
182
|
+
}
|
|
183
|
+
async function recordTurnEnd(input, context, eventName, options) {
|
|
184
|
+
const scopedRouteDispatchInput = createRouteDispatchInput(input, context, input.role);
|
|
185
|
+
const settleRouteDispatchInput = createRouteDispatchInput(input, context);
|
|
133
186
|
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
134
187
|
taskSlug: input.taskSlug,
|
|
135
188
|
role: input.role,
|
|
@@ -145,16 +198,20 @@ export function createClaudeHookService(deps) {
|
|
|
145
198
|
taskSlug: input.taskSlug,
|
|
146
199
|
role: input.role,
|
|
147
200
|
eventName,
|
|
148
|
-
settleGuard
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
201
|
+
...(options.settleGuard
|
|
202
|
+
? {
|
|
203
|
+
settleGuard: async () => {
|
|
204
|
+
const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
|
|
205
|
+
if (pending.length === 0) {
|
|
206
|
+
return { action: "stop" };
|
|
207
|
+
}
|
|
208
|
+
const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
|
|
209
|
+
return retried.some((result) => result.delivered)
|
|
210
|
+
? { action: "continue", reason: "pending route message dispatched" }
|
|
211
|
+
: { action: "stop" };
|
|
212
|
+
}
|
|
152
213
|
}
|
|
153
|
-
|
|
154
|
-
return retried.some((result) => result.delivered)
|
|
155
|
-
? { action: "continue", reason: "pending route message dispatched" }
|
|
156
|
-
: { action: "stop" };
|
|
157
|
-
}
|
|
214
|
+
: {})
|
|
158
215
|
});
|
|
159
216
|
if (session) {
|
|
160
217
|
await deps.translationService.recordConversationBoundary({
|
|
@@ -167,14 +224,16 @@ export function createClaudeHookService(deps) {
|
|
|
167
224
|
occurredAt: session.lastTurnEndedAt ?? session.updatedAt
|
|
168
225
|
});
|
|
169
226
|
}
|
|
170
|
-
if (session && input.role === "project-manager") {
|
|
227
|
+
if (options.notifyGateway && session && input.role === "project-manager") {
|
|
171
228
|
void deps.gatewayService?.handlePmStop({
|
|
172
229
|
repoRoot: context.project.repoRoot,
|
|
173
230
|
taskSlug: input.taskSlug,
|
|
174
231
|
session
|
|
175
232
|
}).catch(() => undefined);
|
|
176
233
|
}
|
|
177
|
-
const dispatched =
|
|
234
|
+
const dispatched = options.dispatchRouteFiles
|
|
235
|
+
? await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput)
|
|
236
|
+
: [];
|
|
178
237
|
return {
|
|
179
238
|
ok: true,
|
|
180
239
|
eventName,
|
|
@@ -184,6 +243,51 @@ export function createClaudeHookService(deps) {
|
|
|
184
243
|
dispatchedCount: dispatched.filter((result) => result.delivered).length
|
|
185
244
|
};
|
|
186
245
|
}
|
|
246
|
+
function createRouteDispatchInput(input, context, stoppedRole) {
|
|
247
|
+
return {
|
|
248
|
+
repoRoot: context.project.repoRoot,
|
|
249
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
250
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
251
|
+
stateRoot: context.config.stateRoot,
|
|
252
|
+
handoffDir: context.task.handoffDir,
|
|
253
|
+
taskSlug: input.taskSlug,
|
|
254
|
+
...(stoppedRole ? { stoppedRole } : {})
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function dispatchStopFailureRecovery(input, context) {
|
|
258
|
+
if (!deps.runtime) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const key = stopFailureRecoveryKey(context.project.repoRoot, input.taskSlug, input.role);
|
|
262
|
+
const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
|
|
263
|
+
if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const session = await deps.sessionService.getRoleSession(context.project.repoRoot, input.taskSlug, input.role);
|
|
267
|
+
if (!session || session.status !== "running") {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
stopFailureRecoveryAttempts.set(key, attempt);
|
|
271
|
+
await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
|
|
272
|
+
await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, input.taskSlug, input.role);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
function clearStopFailureRecovery(repoRoot, taskSlug, role) {
|
|
276
|
+
stopFailureRecoveryAttempts.delete(stopFailureRecoveryKey(repoRoot, taskSlug, role));
|
|
277
|
+
}
|
|
278
|
+
function stopFailureRecoveryKey(repoRoot, taskSlug, role) {
|
|
279
|
+
return `${repoRoot}:${taskSlug}:${role}`;
|
|
280
|
+
}
|
|
281
|
+
function renderStopFailureRecoveryPrompt(attempt) {
|
|
282
|
+
return [
|
|
283
|
+
"[VCM Recovery]",
|
|
284
|
+
"Your previous turn ended unexpectedly after context compaction or an API error.",
|
|
285
|
+
"Continue the same assigned work from the current repository and VCM handoff state.",
|
|
286
|
+
"Do not repeat completed edits, duplicate validation, or duplicate route messages.",
|
|
287
|
+
"If the assigned work is already complete, write/send the expected VCM handoff now.",
|
|
288
|
+
`Recovery attempt: ${attempt}/${MAX_STOP_FAILURE_RECOVERY_ATTEMPTS}.`
|
|
289
|
+
].join("\n");
|
|
290
|
+
}
|
|
187
291
|
async function handlePermissionRequestHook(input) {
|
|
188
292
|
if (!isVcmRoleName(input.role)) {
|
|
189
293
|
throw new VcmError({
|
|
@@ -220,6 +324,12 @@ export function createClaudeHookService(deps) {
|
|
|
220
324
|
if (eventName === "UserPromptSubmit") {
|
|
221
325
|
return handleUserPromptSubmitHook(input);
|
|
222
326
|
}
|
|
327
|
+
if (eventName === "StopFailure") {
|
|
328
|
+
return processStopFailureHook(input);
|
|
329
|
+
}
|
|
330
|
+
if (eventName === "PostCompact") {
|
|
331
|
+
return processPostCompactHook(input);
|
|
332
|
+
}
|
|
223
333
|
// Legacy combined endpoint: the installed hook discards the response,
|
|
224
334
|
// so a block decision could not be enforced. Never block here.
|
|
225
335
|
return processStopHook(input, { allowBlock: false });
|
|
@@ -231,14 +341,14 @@ export function createClaudeHookService(deps) {
|
|
|
231
341
|
};
|
|
232
342
|
}
|
|
233
343
|
function parseHookEvent(value) {
|
|
234
|
-
if (value === "UserPromptSubmit" || value === "Stop") {
|
|
344
|
+
if (value === "UserPromptSubmit" || value === "Stop" || value === "StopFailure" || value === "PostCompact") {
|
|
235
345
|
return value;
|
|
236
346
|
}
|
|
237
347
|
throw new VcmError({
|
|
238
348
|
code: "HOOK_EVENT_UNSUPPORTED",
|
|
239
349
|
message: `Unsupported Claude Code hook event: ${String(value)}`,
|
|
240
350
|
statusCode: 400,
|
|
241
|
-
hint: "VCM accepts UserPromptSubmit and
|
|
351
|
+
hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, and PostCompact hooks only."
|
|
242
352
|
});
|
|
243
353
|
}
|
|
244
354
|
function throwUnsupportedEvent(eventName) {
|
|
@@ -34,6 +34,8 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
34
34
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
35
35
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
36
36
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
37
|
+
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
38
|
+
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
37
39
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
38
40
|
];
|
|
39
41
|
const HARNESS_FILES = [
|
|
@@ -228,16 +228,18 @@ export function createSessionService(deps) {
|
|
|
228
228
|
return undefined;
|
|
229
229
|
}
|
|
230
230
|
const timestamp = now();
|
|
231
|
-
const
|
|
231
|
+
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
232
|
+
const isCompact = isCompactHook(input.eventName);
|
|
232
233
|
const updated = {
|
|
233
234
|
...current,
|
|
234
235
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
235
236
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
236
237
|
cwd: input.cwd ?? current.cwd,
|
|
237
|
-
activityStatus:
|
|
238
|
+
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
238
239
|
lastHookEventAt: timestamp,
|
|
239
|
-
lastTurnEndedAt:
|
|
240
|
-
lastTurnStartedAt:
|
|
240
|
+
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
241
|
+
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
242
|
+
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
241
243
|
updatedAt: timestamp
|
|
242
244
|
};
|
|
243
245
|
deps.registry.upsert(updated);
|
|
@@ -347,16 +349,18 @@ export function createSessionService(deps) {
|
|
|
347
349
|
return undefined;
|
|
348
350
|
}
|
|
349
351
|
const timestamp = now();
|
|
350
|
-
const
|
|
352
|
+
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
353
|
+
const isCompact = isCompactHook(input.eventName);
|
|
351
354
|
const updated = {
|
|
352
355
|
...current,
|
|
353
356
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
354
357
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
355
358
|
cwd: input.cwd ?? current.cwd,
|
|
356
|
-
activityStatus:
|
|
359
|
+
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
357
360
|
lastHookEventAt: timestamp,
|
|
358
|
-
lastTurnEndedAt:
|
|
359
|
-
lastTurnStartedAt:
|
|
361
|
+
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
362
|
+
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
363
|
+
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
360
364
|
updatedAt: timestamp
|
|
361
365
|
};
|
|
362
366
|
deps.registry.upsert(updated);
|
|
@@ -521,6 +525,12 @@ function matchesRoleHookSession(record, input) {
|
|
|
521
525
|
}
|
|
522
526
|
return false;
|
|
523
527
|
}
|
|
528
|
+
function isTurnEndHook(eventName) {
|
|
529
|
+
return eventName === "Stop" || eventName === "StopFailure";
|
|
530
|
+
}
|
|
531
|
+
function isCompactHook(eventName) {
|
|
532
|
+
return eventName === "PostCompact";
|
|
533
|
+
}
|
|
524
534
|
function getRecoverableStatus(record) {
|
|
525
535
|
if (!record.claudeSessionId) {
|
|
526
536
|
return record.status === "running" ? "missing" : record.status;
|
package/docs/product-design.md
CHANGED
|
@@ -29,13 +29,13 @@ Session = n x Round = m x Turn
|
|
|
29
29
|
|
|
30
30
|
- `Session`: one VCM task. It is the statistics container for the whole task, from task creation until Close Task.
|
|
31
31
|
- `Round`: one VCM conversation cycle. It starts with an accepted user prompt or VCM-delivered prompt, continues through sequential role orchestration, and ends when the final role turn stops and no next turn starts inside the 10 second stop window. In the normal orchestrated workflow, the final visible result comes back from `project-manager`.
|
|
32
|
-
- `Turn`: one role-level Claude Code conversation. It starts when a prompt is submitted to one role session and ends when that role's Claude Code process emits `Stop`.
|
|
32
|
+
- `Turn`: one role-level Claude Code conversation. It starts when a prompt is submitted to one role session and ends when that role's Claude Code process emits `Stop` or `StopFailure`.
|
|
33
33
|
|
|
34
34
|
Turns inside one Round are strictly sequential. VCM should finish one role Turn before starting the next role Turn in that Round.
|
|
35
35
|
|
|
36
36
|
Session state is intentionally small: `created` means no Round has started yet, `running` means there is a current running Round, and `stopped` means there is no current running Round after at least one Round has stopped. Starting Claude Code role sessions does not make the VCM Session `running`.
|
|
37
37
|
|
|
38
|
-
Round state is only `running` or `stopped`. After a
|
|
38
|
+
Round state is only `running` or `stopped`. After a turn-end hook, the 10 second stop window still counts as `running`; the Round becomes `stopped` only when the timer expires without another `UserPromptSubmit`.
|
|
39
39
|
|
|
40
40
|
This `Session` term is only for VCM statistics. It must not be confused with a Claude Code role session, terminal runtime session, or Claude transcript session id. VCM still runs one Claude Code role session per role inside one task-level VCM Session.
|
|
41
41
|
|
|
@@ -395,6 +395,8 @@ Backend role state:
|
|
|
395
395
|
|
|
396
396
|
- VCM terminal submit: role becomes `running`.
|
|
397
397
|
- `Stop`: role becomes `idle` and records `lastTurnEndedAt`.
|
|
398
|
+
- `PostCompact`: refreshes role session metadata and records `lastCompactAt` without changing `running`/`idle`.
|
|
399
|
+
- `StopFailure`: first checks completion evidence. If the role already wrote an outgoing route file, VCM marks the role idle and dispatches normally. If not, VCM sends a recovery prompt to the same role without marking it idle.
|
|
398
400
|
- The role tab and flow pause state both react to Claude Code and Codex Reviewer hook events.
|
|
399
401
|
|
|
400
402
|
Task-level Round state:
|
|
@@ -402,14 +404,15 @@ Task-level Round state:
|
|
|
402
404
|
- The first `UserPromptSubmit` starts a Round for the current VCM Session.
|
|
403
405
|
- Each accepted `UserPromptSubmit` is the start of one Turn.
|
|
404
406
|
- `Stop` ends the current Turn and starts a 10 second stop timer; during that timer, the Round is still `running`.
|
|
407
|
+
- `StopFailure` ends the Turn only after VCM decides the role completed or recovery is unavailable. When recovery is sent, the existing Turn stays running.
|
|
405
408
|
- A new `UserPromptSubmit` inside the window continues the same Round and starts the next Turn.
|
|
406
409
|
- If no new prompt is accepted before the deadline, the Round becomes `stopped`.
|
|
407
|
-
- The stop transition is timer-driven from the
|
|
410
|
+
- The stop transition is timer-driven from the turn-end event. Round-state reads do not end a Round.
|
|
408
411
|
- Before stopping, VCM checks `.ai/vcm/handoffs/messages`; if a pending route message exists and can be delivered, VCM retries delivery and extends the stop window instead of alerting.
|
|
409
|
-
- The same Round state stores total Round count, Turn count, completed Turn count, and role active runtime. Active runtime is measured only between `UserPromptSubmit` and
|
|
412
|
+
- The same Round state stores total Round count, Turn count, completed Turn count, and role active runtime. Active runtime is measured only between `UserPromptSubmit` and the turn-end event, not during the stop window.
|
|
410
413
|
- The Round dock shows both wall-clock Round duration and role active runtime. For a running Round, `Total` is `now - Round.startedAt`; for a stopped Round, it is `stoppedAt - Round.startedAt`. `Role runtime` is the accumulated active runtime across Turns in that Round; `Turn count` is the number of accepted prompts in the Round.
|
|
411
414
|
|
|
412
|
-
The frontend polls this task-level Round state and deduplicates each stopped Round so the same stopped state does not alert on every poll. Flow duration is measured from the first `UserPromptSubmit` to `stoppedAt`, falling back to the last
|
|
415
|
+
The frontend polls this task-level Round state and deduplicates each stopped Round so the same stopped state does not alert on every poll. Flow duration is measured from the first `UserPromptSubmit` to `stoppedAt`, falling back to the last turn-end timestamp when needed. Runs under 2 minutes trigger the weak 3-chime reminder at 1.4 second intervals. Runs at or above 2 minutes trigger the strong alert dialog and repeating sound until confirmation.
|
|
413
416
|
|
|
414
417
|
## 9. Session Lifecycle
|
|
415
418
|
|
|
@@ -542,9 +545,9 @@ Default route policy:
|
|
|
542
545
|
- Peer routes such as `coder-reviewer.md` may exist for explicit task designs, but VCM still serializes delivery per target role.
|
|
543
546
|
- `user` talks to `project-manager` through the normal active terminal or translation composer, not through a route file.
|
|
544
547
|
|
|
545
|
-
Claude
|
|
548
|
+
Claude turn-end scan:
|
|
546
549
|
|
|
547
|
-
- VCM injects Claude Code `UserPromptSubmit`, `Stop`, and `PermissionRequest` hooks into `.claude/settings.json`.
|
|
550
|
+
- VCM injects Claude Code `UserPromptSubmit`, `Stop`, `StopFailure`, `PostCompact`, and `PermissionRequest` hooks into `.claude/settings.json`.
|
|
548
551
|
- The hooks do not call `vcmctl`; they POST directly to the local VCM backend.
|
|
549
552
|
- When a Claude Code role stops, VCM marks that role idle, then scans pending route files.
|
|
550
553
|
- VCM scans the stopped role's outgoing files and also any pending files targeting newly idle roles, so messages that were blocked by a busy target can be delivered after that target stops.
|
|
@@ -578,6 +581,8 @@ VCM Harness injects Claude Code hooks into `.claude/settings.json`:
|
|
|
578
581
|
|
|
579
582
|
- `UserPromptSubmit`: posts directly to the VCM backend, marks the role running, and confirms any matching VCM message by recording `acceptedAt`
|
|
580
583
|
- `Stop`: posts directly to the VCM backend, marks the role idle, and triggers pending route-file dispatch
|
|
584
|
+
- `StopFailure`: posts directly to the VCM backend; VCM checks for outgoing route-file completion evidence before marking idle. If no evidence exists, VCM sends a same-role recovery prompt and keeps the Turn running.
|
|
585
|
+
- `PostCompact`: posts directly to the VCM backend, refreshes `session_id`/`transcript_path`/`cwd`, and records `lastCompactAt`
|
|
581
586
|
- `PermissionRequest`: posts directly to the VCM backend and prints the backend response to stdout so Claude Code can consume an allow decision when the local VCM setting is enabled
|
|
582
587
|
|
|
583
588
|
VCM uses `UserPromptSubmit` as the Claude Code acceptance signal. A successful PTY write only proves VCM delivered text to the embedded terminal; `UserPromptSubmit` proves Claude Code accepted the prompt.
|