vibe-coding-master 0.4.10 → 0.4.11
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/api/task-routes.js +4 -4
- package/dist/backend/cli/install-vcm-harness.js +1 -0
- package/dist/backend/gateway/gateway-service.js +4 -4
- package/dist/backend/services/claude-hook-service.js +37 -7
- package/dist/backend/services/harness-service.js +2 -1
- package/dist/backend/services/session-service.js +204 -16
- package/dist-frontend/assets/{index-C3HtJs-w.js → index-DEz0--de.js} +25 -25
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ export function registerTaskRoutes(app, deps) {
|
|
|
32
32
|
const project = await requireCurrentProject(deps.projectService);
|
|
33
33
|
const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
34
34
|
await stopRunningRoleSessions(deps, project.repoRoot, request.params.taskSlug);
|
|
35
|
-
await
|
|
35
|
+
await moveProjectToolSessionsToSafeCwd(deps, project.repoRoot);
|
|
36
36
|
await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), request.params.taskSlug, { clearCache: true });
|
|
37
37
|
deps.roundService.stopTask(request.params.taskSlug);
|
|
38
38
|
return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
|
|
@@ -46,10 +46,10 @@ async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
async function
|
|
49
|
+
async function moveProjectToolSessionsToSafeCwd(deps, repoRoot) {
|
|
50
50
|
await Promise.all([
|
|
51
|
-
ignoreMissingSession(deps.sessionService.
|
|
52
|
-
ignoreMissingSession(deps.sessionService.
|
|
51
|
+
ignoreMissingSession(deps.sessionService.moveProjectTranslatorSessionToSafeCwd(repoRoot)),
|
|
52
|
+
ignoreMissingSession(deps.sessionService.moveProjectHarnessEngineerSessionToSafeCwd(repoRoot))
|
|
53
53
|
]);
|
|
54
54
|
}
|
|
55
55
|
async function ignoreMissingSession(operation) {
|
|
@@ -40,6 +40,7 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
40
40
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
41
41
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
42
42
|
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
43
|
+
{ eventName: "CwdChanged", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
43
44
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
44
45
|
];
|
|
45
46
|
const AGENT_FRONTMATTER = {
|
|
@@ -526,7 +526,7 @@ export function createGatewayService(deps) {
|
|
|
526
526
|
}
|
|
527
527
|
const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
|
|
528
528
|
await stopRunningRoleSessions(project.repoRoot, taskSlug);
|
|
529
|
-
await
|
|
529
|
+
await moveProjectToolSessionsToSafeCwd(project.repoRoot);
|
|
530
530
|
await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true });
|
|
531
531
|
deps.roundService.stopTask(taskSlug);
|
|
532
532
|
const result = await deps.taskService.cleanupTask(project.repoRoot, taskSlug, {
|
|
@@ -561,10 +561,10 @@ export function createGatewayService(deps) {
|
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
|
-
async function
|
|
564
|
+
async function moveProjectToolSessionsToSafeCwd(repoRoot) {
|
|
565
565
|
await Promise.all([
|
|
566
|
-
ignoreMissingSession(deps.sessionService.
|
|
567
|
-
ignoreMissingSession(deps.sessionService.
|
|
566
|
+
ignoreMissingSession(deps.sessionService.moveProjectTranslatorSessionToSafeCwd(repoRoot)),
|
|
567
|
+
ignoreMissingSession(deps.sessionService.moveProjectHarnessEngineerSessionToSafeCwd(repoRoot))
|
|
568
568
|
]);
|
|
569
569
|
}
|
|
570
570
|
async function ignoreMissingSession(operation) {
|
|
@@ -62,7 +62,7 @@ export function createClaudeHookService(deps) {
|
|
|
62
62
|
eventName,
|
|
63
63
|
sessionId: stringOrUndefined(input.event.session_id),
|
|
64
64
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
65
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
65
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
66
66
|
});
|
|
67
67
|
await deps.translationWorkerService?.handleTranslatorHook(context.project.repoRoot, eventName, input.taskSlug);
|
|
68
68
|
return {
|
|
@@ -81,7 +81,7 @@ export function createClaudeHookService(deps) {
|
|
|
81
81
|
eventName,
|
|
82
82
|
sessionId: stringOrUndefined(input.event.session_id),
|
|
83
83
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
84
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
84
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
85
85
|
});
|
|
86
86
|
await deps.harnessService?.recordHarnessBootstrapHook(context.project.repoRoot, {
|
|
87
87
|
eventName,
|
|
@@ -129,7 +129,7 @@ export function createClaudeHookService(deps) {
|
|
|
129
129
|
eventName,
|
|
130
130
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
131
131
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
132
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
132
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
133
133
|
});
|
|
134
134
|
await deps.roundService.recordClaudeHookEvent({
|
|
135
135
|
repoRoot: context.project.repoRoot,
|
|
@@ -250,7 +250,30 @@ export function createClaudeHookService(deps) {
|
|
|
250
250
|
eventName,
|
|
251
251
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
252
252
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
253
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
253
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
eventName,
|
|
258
|
+
taskSlug: context.taskSlug,
|
|
259
|
+
role: input.role,
|
|
260
|
+
sessionUpdated: Boolean(session),
|
|
261
|
+
dispatchedCount: 0
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function processCwdChangedHook(input) {
|
|
265
|
+
const eventName = parseHookEvent(input.event.hook_event_name);
|
|
266
|
+
if (eventName !== "CwdChanged") {
|
|
267
|
+
throwUnsupportedEvent(eventName);
|
|
268
|
+
}
|
|
269
|
+
const context = await getHookContext(input);
|
|
270
|
+
const session = await deps.sessionService.recordClaudeHookEvent(context.project.repoRoot, {
|
|
271
|
+
taskSlug: context.taskSlug,
|
|
272
|
+
role: input.role,
|
|
273
|
+
eventName,
|
|
274
|
+
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
275
|
+
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
276
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
254
277
|
});
|
|
255
278
|
return {
|
|
256
279
|
ok: true,
|
|
@@ -270,7 +293,7 @@ export function createClaudeHookService(deps) {
|
|
|
270
293
|
eventName,
|
|
271
294
|
claudeSessionId: stringOrUndefined(input.event.session_id),
|
|
272
295
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
273
|
-
cwd: stringOrUndefined(input.event.cwd)
|
|
296
|
+
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd)
|
|
274
297
|
});
|
|
275
298
|
await deps.roundService.recordClaudeHookEvent({
|
|
276
299
|
repoRoot: context.project.repoRoot,
|
|
@@ -431,6 +454,9 @@ export function createClaudeHookService(deps) {
|
|
|
431
454
|
if (eventName === "PostCompact") {
|
|
432
455
|
return processPostCompactHook(input);
|
|
433
456
|
}
|
|
457
|
+
if (eventName === "CwdChanged") {
|
|
458
|
+
return processCwdChangedHook(input);
|
|
459
|
+
}
|
|
434
460
|
// Legacy combined endpoint: the installed hook discards the response,
|
|
435
461
|
// so a block decision could not be enforced. Never block here.
|
|
436
462
|
return processStopHook(input, { allowBlock: false });
|
|
@@ -448,14 +474,18 @@ export function createClaudeHookService(deps) {
|
|
|
448
474
|
};
|
|
449
475
|
}
|
|
450
476
|
function parseHookEvent(value) {
|
|
451
|
-
if (value === "UserPromptSubmit"
|
|
477
|
+
if (value === "UserPromptSubmit"
|
|
478
|
+
|| value === "Stop"
|
|
479
|
+
|| value === "StopFailure"
|
|
480
|
+
|| value === "PostCompact"
|
|
481
|
+
|| value === "CwdChanged") {
|
|
452
482
|
return value;
|
|
453
483
|
}
|
|
454
484
|
throw new VcmError({
|
|
455
485
|
code: "HOOK_EVENT_UNSUPPORTED",
|
|
456
486
|
message: `Unsupported Claude Code hook event: ${String(value)}`,
|
|
457
487
|
statusCode: 400,
|
|
458
|
-
hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, and
|
|
488
|
+
hint: "VCM accepts UserPromptSubmit, Stop, StopFailure, PostCompact, and CwdChanged hooks only."
|
|
459
489
|
});
|
|
460
490
|
}
|
|
461
491
|
function throwUnsupportedEvent(eventName) {
|
|
@@ -41,6 +41,7 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
41
41
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
42
42
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
43
43
|
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
44
|
+
{ eventName: "CwdChanged", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
44
45
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
45
46
|
];
|
|
46
47
|
const HARNESS_FILES = [
|
|
@@ -1420,7 +1421,7 @@ async function clearHarnessBootstrapRunState(fs, repoRoot) {
|
|
|
1420
1421
|
await fs.removePath?.(resolveHarnessPath(repoRoot, BOOTSTRAP_SESSION_PATH), { force: true });
|
|
1421
1422
|
}
|
|
1422
1423
|
function isHarnessBootstrapHookEvent(value) {
|
|
1423
|
-
return value === "Stop" || value === "StopFailure" || value === "UserPromptSubmit" || value === "PostCompact";
|
|
1424
|
+
return value === "Stop" || value === "StopFailure" || value === "UserPromptSubmit" || value === "PostCompact" || value === "CwdChanged";
|
|
1424
1425
|
}
|
|
1425
1426
|
function matchesBootstrapRunState(state, input) {
|
|
1426
1427
|
if (state.sessionId && input.sessionId && state.sessionId !== input.sessionId) {
|
|
@@ -209,7 +209,7 @@ export function createSessionService(deps) {
|
|
|
209
209
|
const taskContext = await resolveProjectToolTaskContext(repoRoot, input, "Translator");
|
|
210
210
|
const live = toRoleSessionRecordView(getRegisteredProjectTranslatorSession(deps.registry, deps.runtime), deps.runtime);
|
|
211
211
|
if (live && live.status === "running") {
|
|
212
|
-
return withHarnessRevisionView(repoRoot, live);
|
|
212
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, live, taskContext.taskRepoRoot));
|
|
213
213
|
}
|
|
214
214
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
215
215
|
const persisted = await loadPersistedTranslatorSession(deps.fs, repoRoot);
|
|
@@ -228,10 +228,15 @@ export function createSessionService(deps) {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
await deps.fs.ensureDir(resolveRepoPath(repoRoot, TRANSLATION_DIR));
|
|
231
|
-
const
|
|
231
|
+
const launchCwd = launchMode === "resume"
|
|
232
|
+
? persisted?.cwd ?? taskContext.taskRepoRoot
|
|
233
|
+
: taskContext.taskRepoRoot;
|
|
234
|
+
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
235
|
+
? persisted.transcriptPath
|
|
236
|
+
: claudeTranscriptPath(launchCwd, claudeSessionId);
|
|
232
237
|
const startCommand = {
|
|
233
238
|
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
234
|
-
cwd:
|
|
239
|
+
cwd: launchCwd
|
|
235
240
|
};
|
|
236
241
|
const runtimeSession = await deps.runtime.createSession({
|
|
237
242
|
taskSlug: PROJECT_TRANSLATOR_SCOPE,
|
|
@@ -274,13 +279,13 @@ export function createSessionService(deps) {
|
|
|
274
279
|
};
|
|
275
280
|
deps.registry.upsert(record);
|
|
276
281
|
await persistTranslatorSession(deps.fs, repoRoot, record);
|
|
277
|
-
return withHarnessRevisionView(repoRoot, record);
|
|
282
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
|
|
278
283
|
}
|
|
279
284
|
async function launchProjectHarnessEngineerSession(repoRoot, input, launchMode) {
|
|
280
285
|
const taskContext = await resolveProjectToolTaskContext(repoRoot, input, "Harness Engineer");
|
|
281
286
|
const live = toRoleSessionRecordView(getRegisteredProjectHarnessEngineerSession(deps.registry, deps.runtime), deps.runtime);
|
|
282
287
|
if (live && live.status === "running") {
|
|
283
|
-
return withHarnessRevisionView(repoRoot, live);
|
|
288
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, live, taskContext.taskRepoRoot));
|
|
284
289
|
}
|
|
285
290
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
286
291
|
const persisted = await loadPersistedHarnessEngineerSession(deps.fs, repoRoot);
|
|
@@ -299,10 +304,15 @@ export function createSessionService(deps) {
|
|
|
299
304
|
});
|
|
300
305
|
}
|
|
301
306
|
await deps.fs.ensureDir(resolveRepoPath(repoRoot, HARNESS_ENGINEER_DIR));
|
|
302
|
-
const
|
|
307
|
+
const launchCwd = launchMode === "resume"
|
|
308
|
+
? persisted?.cwd ?? taskContext.taskRepoRoot
|
|
309
|
+
: taskContext.taskRepoRoot;
|
|
310
|
+
const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
|
|
311
|
+
? persisted.transcriptPath
|
|
312
|
+
: claudeTranscriptPath(launchCwd, claudeSessionId);
|
|
303
313
|
const startCommand = {
|
|
304
314
|
...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume", model, effort),
|
|
305
|
-
cwd:
|
|
315
|
+
cwd: launchCwd
|
|
306
316
|
};
|
|
307
317
|
const runtimeSession = await deps.runtime.createSession({
|
|
308
318
|
taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE,
|
|
@@ -345,7 +355,7 @@ export function createSessionService(deps) {
|
|
|
345
355
|
};
|
|
346
356
|
deps.registry.upsert(record);
|
|
347
357
|
await persistHarnessEngineerSession(deps.fs, repoRoot, record);
|
|
348
|
-
return withHarnessRevisionView(repoRoot, record);
|
|
358
|
+
return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
|
|
349
359
|
}
|
|
350
360
|
async function resolveProjectToolTaskContext(repoRoot, input, roleLabel) {
|
|
351
361
|
const taskSlug = input.taskSlug?.trim();
|
|
@@ -363,6 +373,103 @@ export function createSessionService(deps) {
|
|
|
363
373
|
taskRepoRoot: getTaskRuntimeRepoRoot(task)
|
|
364
374
|
};
|
|
365
375
|
}
|
|
376
|
+
async function migrateRunningProjectToolSessionCwd(repoRoot, session, targetCwd) {
|
|
377
|
+
if (session.role !== TRANSLATOR_ROLE
|
|
378
|
+
&& session.role !== HARNESS_ENGINEER_ROLE
|
|
379
|
+
&& session.role !== GATE_REVIEWER_ROLE) {
|
|
380
|
+
return session;
|
|
381
|
+
}
|
|
382
|
+
if (samePath(session.cwd, targetCwd)) {
|
|
383
|
+
return session;
|
|
384
|
+
}
|
|
385
|
+
const runtimeSession = deps.runtime.getSession(session.id);
|
|
386
|
+
if (!runtimeSession || runtimeSession.status !== "running") {
|
|
387
|
+
return session;
|
|
388
|
+
}
|
|
389
|
+
assertSafeCwdTarget(targetCwd);
|
|
390
|
+
const timestamp = now();
|
|
391
|
+
await submitTerminalInput(deps.runtime, session.id, `/cd ${targetCwd}`);
|
|
392
|
+
const updated = {
|
|
393
|
+
...session,
|
|
394
|
+
cwd: targetCwd,
|
|
395
|
+
previousCwd: session.cwd,
|
|
396
|
+
transcriptPath: claudeTranscriptPath(targetCwd, session.claudeSessionId),
|
|
397
|
+
cwdMigrationTarget: targetCwd,
|
|
398
|
+
cwdMigrationPending: true,
|
|
399
|
+
lastCwdMigrationAt: timestamp,
|
|
400
|
+
updatedAt: timestamp
|
|
401
|
+
};
|
|
402
|
+
deps.registry.upsert(normalizeProjectScopedRecordForPersistence(updated));
|
|
403
|
+
await persistProjectScopedToolSession(repoRoot, updated);
|
|
404
|
+
return updated;
|
|
405
|
+
}
|
|
406
|
+
async function resumeProjectToolSessionAtCwd(repoRoot, session, targetCwd) {
|
|
407
|
+
const live = toRoleSessionRecordView(session.role === TRANSLATOR_ROLE
|
|
408
|
+
? getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
|
|
409
|
+
: session.role === HARNESS_ENGINEER_ROLE
|
|
410
|
+
? getRegisteredProjectHarnessEngineerSession(deps.registry, deps.runtime)
|
|
411
|
+
: getRegisteredProjectGateReviewerSession(deps.registry, deps.runtime), deps.runtime);
|
|
412
|
+
if (live?.status === "running") {
|
|
413
|
+
return migrateRunningProjectToolSessionCwd(repoRoot, live, targetCwd);
|
|
414
|
+
}
|
|
415
|
+
const config = await deps.projectService.loadConfig(repoRoot);
|
|
416
|
+
const permissionMode = normalizeClaudePermissionMode(session.permissionMode);
|
|
417
|
+
const model = normalizeClaudeModel(session.model);
|
|
418
|
+
const effort = normalizeClaudeEffort(session.effort);
|
|
419
|
+
const launchCwd = session.cwd || targetCwd;
|
|
420
|
+
const startCommand = {
|
|
421
|
+
...deps.claude.buildRoleStartCommand(session.role, config.claudeCommand, permissionMode, session.claudeSessionId, true, model, effort),
|
|
422
|
+
cwd: launchCwd
|
|
423
|
+
};
|
|
424
|
+
const runtimeSession = await deps.runtime.createSession({
|
|
425
|
+
taskSlug: normalizeProjectScopedRecordForPersistence(session).taskSlug,
|
|
426
|
+
role: session.role,
|
|
427
|
+
command: startCommand.command,
|
|
428
|
+
args: startCommand.args,
|
|
429
|
+
cwd: startCommand.cwd,
|
|
430
|
+
env: {
|
|
431
|
+
VCM_API_URL: deps.apiUrl,
|
|
432
|
+
VCM_TASK_REPO_ROOT: targetCwd,
|
|
433
|
+
VCM_TASK_SLUG: normalizeProjectScopedRecordForPersistence(session).taskSlug,
|
|
434
|
+
VCM_ROLE: session.role,
|
|
435
|
+
VCM_SESSION_ID: session.claudeSessionId
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
const timestamp = now();
|
|
439
|
+
const resumed = {
|
|
440
|
+
...session,
|
|
441
|
+
id: runtimeSession.id,
|
|
442
|
+
status: runtimeSession.status,
|
|
443
|
+
activityStatus: "idle",
|
|
444
|
+
command: startCommand.display,
|
|
445
|
+
permissionMode,
|
|
446
|
+
model,
|
|
447
|
+
effort,
|
|
448
|
+
cwd: launchCwd,
|
|
449
|
+
pid: runtimeSession.pid,
|
|
450
|
+
startedAt: runtimeSession.startedAt,
|
|
451
|
+
updatedAt: timestamp,
|
|
452
|
+
lastOutputAt: runtimeSession.lastOutputAt,
|
|
453
|
+
exitCode: runtimeSession.exitCode,
|
|
454
|
+
transcriptPath: session.transcriptPath ?? claudeTranscriptPath(launchCwd, session.claudeSessionId)
|
|
455
|
+
};
|
|
456
|
+
deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
|
|
457
|
+
await persistProjectScopedToolSession(repoRoot, resumed);
|
|
458
|
+
return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
|
|
459
|
+
}
|
|
460
|
+
async function persistProjectScopedToolSession(repoRoot, session) {
|
|
461
|
+
if (session.role === GATE_REVIEWER_ROLE) {
|
|
462
|
+
await persistProjectGateReviewerSession(deps.fs, repoRoot, session);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (session.role === TRANSLATOR_ROLE) {
|
|
466
|
+
await persistTranslatorSession(deps.fs, repoRoot, session);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (session.role === HARNESS_ENGINEER_ROLE) {
|
|
470
|
+
await persistHarnessEngineerSession(deps.fs, repoRoot, session);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
366
473
|
async function notifyHarnessUpdatedForSession(repoRoot, session) {
|
|
367
474
|
const runtimeSession = deps.runtime.getSession(session.id);
|
|
368
475
|
if (!runtimeSession || runtimeSession.status !== "running") {
|
|
@@ -505,6 +612,23 @@ export function createSessionService(deps) {
|
|
|
505
612
|
await persistTranslatorSession(deps.fs, repoRoot, updated);
|
|
506
613
|
return updated;
|
|
507
614
|
},
|
|
615
|
+
async moveProjectTranslatorSessionToSafeCwd(repoRoot) {
|
|
616
|
+
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
617
|
+
if (!existing) {
|
|
618
|
+
throw new VcmError({
|
|
619
|
+
code: "SESSION_MISSING",
|
|
620
|
+
message: "Translator session has not been started.",
|
|
621
|
+
statusCode: 404
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (samePath(existing.cwd, repoRoot)) {
|
|
625
|
+
return existing;
|
|
626
|
+
}
|
|
627
|
+
return withHarnessRevisionView(repoRoot, await resumeProjectToolSessionAtCwd(repoRoot, {
|
|
628
|
+
...existing,
|
|
629
|
+
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
630
|
+
}, repoRoot));
|
|
631
|
+
},
|
|
508
632
|
async restartProjectTranslatorSession(repoRoot, input = {}) {
|
|
509
633
|
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
510
634
|
if (!existing) {
|
|
@@ -525,6 +649,16 @@ export function createSessionService(deps) {
|
|
|
525
649
|
async ensureProjectTranslatorSession(repoRoot, input = {}) {
|
|
526
650
|
const existing = await this.getProjectTranslatorSession(repoRoot);
|
|
527
651
|
if (existing?.status === "running") {
|
|
652
|
+
if (input.taskSlug) {
|
|
653
|
+
return this.resumeProjectTranslatorSession(repoRoot, {
|
|
654
|
+
taskSlug: input.taskSlug,
|
|
655
|
+
permissionMode: input.permissionMode ?? existing.permissionMode,
|
|
656
|
+
model: input.model ?? existing.model,
|
|
657
|
+
effort: input.effort ?? existing.effort,
|
|
658
|
+
cols: input.cols,
|
|
659
|
+
rows: input.rows
|
|
660
|
+
});
|
|
661
|
+
}
|
|
528
662
|
return existing;
|
|
529
663
|
}
|
|
530
664
|
if (existing?.claudeSessionId) {
|
|
@@ -547,16 +681,19 @@ export function createSessionService(deps) {
|
|
|
547
681
|
const timestamp = now();
|
|
548
682
|
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
549
683
|
const isCompact = isCompactHook(input.eventName);
|
|
684
|
+
const isCwdChanged = isCwdChangedHook(input.eventName);
|
|
550
685
|
const updated = {
|
|
551
686
|
...current,
|
|
552
687
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
553
688
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
554
689
|
cwd: input.cwd ?? current.cwd,
|
|
555
|
-
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
690
|
+
activityStatus: isTurnEnd ? "idle" : (isCompact || isCwdChanged) ? current.activityStatus : "running",
|
|
556
691
|
lastHookEventAt: timestamp,
|
|
557
692
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
558
|
-
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
693
|
+
lastTurnStartedAt: isTurnEnd || isCompact || isCwdChanged ? current.lastTurnStartedAt : timestamp,
|
|
559
694
|
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
695
|
+
cwdMigrationPending: isCwdChanged ? false : current.cwdMigrationPending,
|
|
696
|
+
cwdMigrationTarget: isCwdChanged ? undefined : current.cwdMigrationTarget,
|
|
560
697
|
updatedAt: timestamp
|
|
561
698
|
};
|
|
562
699
|
deps.registry.upsert(updated);
|
|
@@ -602,6 +739,23 @@ export function createSessionService(deps) {
|
|
|
602
739
|
await persistHarnessEngineerSession(deps.fs, repoRoot, updated);
|
|
603
740
|
return updated;
|
|
604
741
|
},
|
|
742
|
+
async moveProjectHarnessEngineerSessionToSafeCwd(repoRoot) {
|
|
743
|
+
const existing = await this.getProjectHarnessEngineerSession(repoRoot);
|
|
744
|
+
if (!existing) {
|
|
745
|
+
throw new VcmError({
|
|
746
|
+
code: "SESSION_MISSING",
|
|
747
|
+
message: "Harness Engineer session has not been started.",
|
|
748
|
+
statusCode: 404
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
if (samePath(existing.cwd, repoRoot)) {
|
|
752
|
+
return existing;
|
|
753
|
+
}
|
|
754
|
+
return withHarnessRevisionView(repoRoot, await resumeProjectToolSessionAtCwd(repoRoot, {
|
|
755
|
+
...existing,
|
|
756
|
+
taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
|
|
757
|
+
}, repoRoot));
|
|
758
|
+
},
|
|
605
759
|
async restartProjectHarnessEngineerSession(repoRoot, input = {}) {
|
|
606
760
|
const existing = await this.getProjectHarnessEngineerSession(repoRoot);
|
|
607
761
|
if (!existing) {
|
|
@@ -622,6 +776,16 @@ export function createSessionService(deps) {
|
|
|
622
776
|
async ensureProjectHarnessEngineerSession(repoRoot, input = {}) {
|
|
623
777
|
const existing = await this.getProjectHarnessEngineerSession(repoRoot);
|
|
624
778
|
if (existing?.status === "running") {
|
|
779
|
+
if (input.taskSlug) {
|
|
780
|
+
return this.resumeProjectHarnessEngineerSession(repoRoot, {
|
|
781
|
+
taskSlug: input.taskSlug,
|
|
782
|
+
permissionMode: input.permissionMode ?? existing.permissionMode,
|
|
783
|
+
model: input.model ?? existing.model,
|
|
784
|
+
effort: input.effort ?? existing.effort,
|
|
785
|
+
cols: input.cols,
|
|
786
|
+
rows: input.rows
|
|
787
|
+
});
|
|
788
|
+
}
|
|
625
789
|
return existing;
|
|
626
790
|
}
|
|
627
791
|
if (existing?.claudeSessionId) {
|
|
@@ -644,16 +808,19 @@ export function createSessionService(deps) {
|
|
|
644
808
|
const timestamp = now();
|
|
645
809
|
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
646
810
|
const isCompact = isCompactHook(input.eventName);
|
|
811
|
+
const isCwdChanged = isCwdChangedHook(input.eventName);
|
|
647
812
|
const updated = {
|
|
648
813
|
...current,
|
|
649
814
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
650
815
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
651
816
|
cwd: input.cwd ?? current.cwd,
|
|
652
|
-
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
817
|
+
activityStatus: isTurnEnd ? "idle" : (isCompact || isCwdChanged) ? current.activityStatus : "running",
|
|
653
818
|
lastHookEventAt: timestamp,
|
|
654
819
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
655
|
-
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
820
|
+
lastTurnStartedAt: isTurnEnd || isCompact || isCwdChanged ? current.lastTurnStartedAt : timestamp,
|
|
656
821
|
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
822
|
+
cwdMigrationPending: isCwdChanged ? false : current.cwdMigrationPending,
|
|
823
|
+
cwdMigrationTarget: isCwdChanged ? undefined : current.cwdMigrationTarget,
|
|
657
824
|
updatedAt: timestamp
|
|
658
825
|
};
|
|
659
826
|
deps.registry.upsert(updated);
|
|
@@ -827,17 +994,20 @@ export function createSessionService(deps) {
|
|
|
827
994
|
const timestamp = now();
|
|
828
995
|
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
829
996
|
const isCompact = isCompactHook(input.eventName);
|
|
997
|
+
const isCwdChanged = isCwdChangedHook(input.eventName);
|
|
830
998
|
const updated = {
|
|
831
999
|
...current,
|
|
832
1000
|
taskSlug: PROJECT_GATE_REVIEWER_SCOPE,
|
|
833
1001
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
834
1002
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
835
1003
|
cwd: input.cwd ?? current.cwd,
|
|
836
|
-
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
1004
|
+
activityStatus: isTurnEnd ? "idle" : (isCompact || isCwdChanged) ? current.activityStatus : "running",
|
|
837
1005
|
lastHookEventAt: timestamp,
|
|
838
1006
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
839
|
-
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
1007
|
+
lastTurnStartedAt: isTurnEnd || isCompact || isCwdChanged ? current.lastTurnStartedAt : timestamp,
|
|
840
1008
|
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
1009
|
+
cwdMigrationPending: isCwdChanged ? false : current.cwdMigrationPending,
|
|
1010
|
+
cwdMigrationTarget: isCwdChanged ? undefined : current.cwdMigrationTarget,
|
|
841
1011
|
updatedAt: timestamp
|
|
842
1012
|
};
|
|
843
1013
|
deps.registry.upsert(updated);
|
|
@@ -869,16 +1039,19 @@ export function createSessionService(deps) {
|
|
|
869
1039
|
const timestamp = now();
|
|
870
1040
|
const isTurnEnd = isTurnEndHook(input.eventName);
|
|
871
1041
|
const isCompact = isCompactHook(input.eventName);
|
|
1042
|
+
const isCwdChanged = isCwdChangedHook(input.eventName);
|
|
872
1043
|
const updated = {
|
|
873
1044
|
...current,
|
|
874
1045
|
claudeSessionId: input.sessionId ?? current.claudeSessionId,
|
|
875
1046
|
transcriptPath: input.transcriptPath ?? current.transcriptPath,
|
|
876
1047
|
cwd: input.cwd ?? current.cwd,
|
|
877
|
-
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
1048
|
+
activityStatus: isTurnEnd ? "idle" : (isCompact || isCwdChanged) ? current.activityStatus : "running",
|
|
878
1049
|
lastHookEventAt: timestamp,
|
|
879
1050
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
880
|
-
lastTurnStartedAt: isTurnEnd || isCompact ? current.lastTurnStartedAt : timestamp,
|
|
1051
|
+
lastTurnStartedAt: isTurnEnd || isCompact || isCwdChanged ? current.lastTurnStartedAt : timestamp,
|
|
881
1052
|
lastCompactAt: isCompact ? timestamp : current.lastCompactAt,
|
|
1053
|
+
cwdMigrationPending: isCwdChanged ? false : current.cwdMigrationPending,
|
|
1054
|
+
cwdMigrationTarget: isCwdChanged ? undefined : current.cwdMigrationTarget,
|
|
882
1055
|
updatedAt: timestamp
|
|
883
1056
|
};
|
|
884
1057
|
deps.registry.upsert(updated);
|
|
@@ -994,6 +1167,9 @@ function isTurnEndHook(eventName) {
|
|
|
994
1167
|
function isCompactHook(eventName) {
|
|
995
1168
|
return eventName === "PostCompact";
|
|
996
1169
|
}
|
|
1170
|
+
function isCwdChangedHook(eventName) {
|
|
1171
|
+
return eventName === "CwdChanged";
|
|
1172
|
+
}
|
|
997
1173
|
function getRecoverableStatus(record) {
|
|
998
1174
|
if (!record.claudeSessionId) {
|
|
999
1175
|
return record.status === "running" ? "missing" : record.status;
|
|
@@ -1003,6 +1179,18 @@ function getRecoverableStatus(record) {
|
|
|
1003
1179
|
}
|
|
1004
1180
|
return "resumable";
|
|
1005
1181
|
}
|
|
1182
|
+
function samePath(left, right) {
|
|
1183
|
+
return path.resolve(left) === path.resolve(right);
|
|
1184
|
+
}
|
|
1185
|
+
function assertSafeCwdTarget(targetCwd) {
|
|
1186
|
+
if (!targetCwd.trim() || /[\r\n]/.test(targetCwd)) {
|
|
1187
|
+
throw new VcmError({
|
|
1188
|
+
code: "SESSION_CWD_INVALID",
|
|
1189
|
+
message: "Session cwd target is invalid.",
|
|
1190
|
+
statusCode: 400
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1006
1194
|
function getHandoffArtifactPath(paths, role) {
|
|
1007
1195
|
if (role === "architect") {
|
|
1008
1196
|
return paths.architecturePlanPath;
|