vibe-coding-master 0.4.9 → 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.
@@ -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 stopProjectToolSessions(deps, project.repoRoot);
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 stopProjectToolSessions(deps, repoRoot) {
49
+ async function moveProjectToolSessionsToSafeCwd(deps, repoRoot) {
50
50
  await Promise.all([
51
- ignoreMissingSession(deps.sessionService.stopProjectTranslatorSession(repoRoot)),
52
- ignoreMissingSession(deps.sessionService.stopProjectHarnessEngineerSession(repoRoot))
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 stopProjectToolSessions(project.repoRoot);
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 stopProjectToolSessions(repoRoot) {
564
+ async function moveProjectToolSessionsToSafeCwd(repoRoot) {
565
565
  await Promise.all([
566
- ignoreMissingSession(deps.sessionService.stopProjectTranslatorSession(repoRoot)),
567
- ignoreMissingSession(deps.sessionService.stopProjectHarnessEngineerSession(repoRoot))
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" || value === "Stop" || value === "StopFailure" || value === "PostCompact") {
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 PostCompact hooks only."
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 transcriptPath = claudeTranscriptPath(taskContext.taskRepoRoot, claudeSessionId);
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: taskContext.taskRepoRoot
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 transcriptPath = claudeTranscriptPath(taskContext.taskRepoRoot, claudeSessionId);
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: taskContext.taskRepoRoot
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;
@@ -18,6 +18,7 @@ const DEFAULT_PROFILE = "default";
18
18
  const DEFAULT_CHUNK_SOURCE_TOKEN_TARGET = 80000;
19
19
  const BOOTSTRAP_DEFAULT_LIMIT = 12;
20
20
  const MEMORY_TOTAL_LIMIT_BYTES = 80 * 1024;
21
+ const MEMORY_INITIALIZED_MIN_FILES = 2;
21
22
  const MEMORY_FILE_NAMES = ["glossary.md", "style-guide.md", "project-context.md", "decisions.md"];
22
23
  const TRANSLATOR_ROLE = "translator";
23
24
  const FILE_BROWSER_DEFAULT_LIMIT = 200;
@@ -1552,14 +1553,15 @@ function isFileTranslationChunk(value) {
1552
1553
  typeof candidate.sourceLineEnd === "number";
1553
1554
  }
1554
1555
  async function isMemoryInitialized(repoRoot, fs) {
1556
+ let initializedFiles = 0;
1555
1557
  for (const file of MEMORY_FILE_NAMES) {
1556
1558
  const content = await fs.readText(resolveRepoPath(repoRoot, `${MEMORY_DIR}/${file}`));
1557
1559
  const meaningfulLines = content.split("\n").filter((line) => line.trim() && !line.trim().startsWith("#"));
1558
1560
  if (meaningfulLines.length > 0) {
1559
- return true;
1561
+ initializedFiles += 1;
1560
1562
  }
1561
1563
  }
1562
- return false;
1564
+ return initializedFiles >= MEMORY_INITIALIZED_MIN_FILES;
1563
1565
  }
1564
1566
  async function getMemoryUsage(repoRoot, fs) {
1565
1567
  let totalBytes = 0;