vibe-coding-master 0.4.38 → 0.4.39

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.
@@ -3,6 +3,7 @@ export function registerRuntimeStateRoutes(app, deps) {
3
3
  app.get("/api/projects/runtime-state", async (request) => {
4
4
  const project = await requireCurrentProject(deps.projectService);
5
5
  const taskSlug = request.query.taskSlug?.trim();
6
+ const coordinated = await deps.runtimeCoordinator.reconcileProject(project.repoRoot, { taskSlug });
6
7
  const [translatorSession, translationState, harnessEngineerSession, harnessFeedbackState] = await Promise.all([
7
8
  deps.sessionService.getProjectTranslatorSession(project.repoRoot).then((session) => session ?? null),
8
9
  deps.translationWorkerService.getState(project.repoRoot, { visibility: "public" }),
@@ -16,7 +17,8 @@ export function registerRuntimeStateRoutes(app, deps) {
16
17
  harnessEngineerSession,
17
18
  harnessStatus: null,
18
19
  harnessBootstrapStatus: null,
19
- harnessFeedbackState
20
+ harnessFeedbackState,
21
+ gatewayStatus: coordinated.gatewayStatus
20
22
  };
21
23
  }
22
24
  try {
@@ -31,7 +33,8 @@ export function registerRuntimeStateRoutes(app, deps) {
31
33
  harnessEngineerSession,
32
34
  harnessStatus,
33
35
  harnessBootstrapStatus,
34
- harnessFeedbackState
36
+ harnessFeedbackState,
37
+ gatewayStatus: coordinated.gatewayStatus
35
38
  };
36
39
  }
37
40
  catch (error) {
@@ -42,7 +45,8 @@ export function registerRuntimeStateRoutes(app, deps) {
42
45
  harnessEngineerSession,
43
46
  harnessStatus: degradedHarnessStatus(error),
44
47
  harnessBootstrapStatus: degradedBootstrapStatus(error),
45
- harnessFeedbackState
48
+ harnessFeedbackState,
49
+ gatewayStatus: coordinated.gatewayStatus
46
50
  };
47
51
  }
48
52
  throw error;
@@ -613,6 +613,16 @@ export function createGatewayService(deps) {
613
613
  const settings = await deps.settings.updateSettings({ translationEnabled: enabled });
614
614
  return `Gateway translation ${settings.translationEnabled ? "on" : "off"}.`;
615
615
  }
616
+ async function enableGatewayTranslationRuntime() {
617
+ const preferences = await deps.appSettings.getPreferences();
618
+ if (preferences.translationEnabled && preferences.translationAutoSendEnabled) {
619
+ return;
620
+ }
621
+ await deps.appSettings.updatePreferences({
622
+ translationEnabled: true,
623
+ translationAutoSendEnabled: true
624
+ });
625
+ }
616
626
  async function startGateway() {
617
627
  const settings = await deps.settings.loadSettings();
618
628
  if (!toAccount(settings)) {
@@ -621,9 +631,14 @@ export function createGatewayService(deps) {
621
631
  : "Gateway is not bound. Start QR login from desktop VCM first.";
622
632
  }
623
633
  if (settings.enabled) {
634
+ await enableGatewayTranslationRuntime();
624
635
  return "Gateway is already on.";
625
636
  }
626
- const enabled = await syncDesktopContext(await deps.settings.updateSettings({ enabled: true }));
637
+ await enableGatewayTranslationRuntime();
638
+ const enabled = await syncDesktopContext(await deps.settings.updateSettings({
639
+ enabled: true,
640
+ translationEnabled: true
641
+ }));
627
642
  await ensurePolling();
628
643
  const lines = [
629
644
  "Gateway started.",
@@ -732,16 +747,25 @@ export function createGatewayService(deps) {
732
747
  },
733
748
  async getStatus() {
734
749
  const settings = await syncDesktopContext(await deps.settings.loadSettings());
750
+ if (settings.enabled) {
751
+ await enableGatewayTranslationRuntime();
752
+ }
735
753
  await ensurePolling();
736
754
  return deps.settings.expose(settings, isRunning());
737
755
  },
738
756
  async updateSettings(input) {
739
- const updateInput = input.channel && input.baseUrl === undefined
757
+ const gatewayStartInput = input.enabled === true
758
+ ? { ...input, translationEnabled: true }
759
+ : input;
760
+ const updateInput = gatewayStartInput.channel && gatewayStartInput.baseUrl === undefined
740
761
  ? {
741
- ...input,
742
- baseUrl: deps.channels.get(input.channel).defaultBaseUrl
762
+ ...gatewayStartInput,
763
+ baseUrl: deps.channels.get(gatewayStartInput.channel).defaultBaseUrl
743
764
  }
744
- : input;
765
+ : gatewayStartInput;
766
+ if (input.enabled === true) {
767
+ await enableGatewayTranslationRuntime();
768
+ }
745
769
  let settings = await deps.settings.updateSettings(updateInput);
746
770
  if (settings.enabled) {
747
771
  settings = await syncDesktopContext(settings);
@@ -1025,7 +1049,8 @@ export function createGatewayService(deps) {
1025
1049
  }
1026
1050
  const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
1027
1051
  const cursor = settings.pushCursors[cursorKey];
1028
- const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId);
1052
+ const nextEvents = selectEventsAfterCursor(events, cursor?.lastTranscriptEventId)
1053
+ .filter(isFinalTurnTextEvent);
1029
1054
  if (nextEvents.length === 0) {
1030
1055
  return;
1031
1056
  }
@@ -1203,13 +1228,17 @@ async function readTranscriptTextEvents(transcriptPath) {
1203
1228
  events.push({
1204
1229
  id: event.id,
1205
1230
  timestamp: event.timestamp,
1206
- text: event.text
1231
+ text: event.text,
1232
+ stopReason: event.stopReason
1207
1233
  });
1208
1234
  }
1209
1235
  }
1210
1236
  }
1211
1237
  return events;
1212
1238
  }
1239
+ function isFinalTurnTextEvent(event) {
1240
+ return event.stopReason === "end_turn";
1241
+ }
1213
1242
  function selectEventsAfterCursor(events, cursorId) {
1214
1243
  if (events.length === 0) {
1215
1244
  return [];
@@ -1229,9 +1258,10 @@ function selectLatestTurnReply(events, session) {
1229
1258
  }
1230
1259
  const startMs = timestampMs(session.lastTurnStartedAt);
1231
1260
  const endMs = timestampMs(session.lastTurnEndedAt);
1261
+ const finalEvents = events.filter(isFinalTurnTextEvent);
1232
1262
  const selected = startMs === undefined
1233
- ? events.slice(-1)
1234
- : events.filter((event) => {
1263
+ ? finalEvents.slice(-1)
1264
+ : finalEvents.filter((event) => {
1235
1265
  const eventMs = timestampMs(event.timestamp);
1236
1266
  return eventMs !== undefined
1237
1267
  && eventMs >= startMs - 1_000
@@ -30,6 +30,7 @@ import { createSessionRegistry } from "./runtime/session-registry.js";
30
30
  import { createSessionService } from "./services/session-service.js";
31
31
  import { createMessageService } from "./services/message-service.js";
32
32
  import { createRoundService } from "./services/round-service.js";
33
+ import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
33
34
  import { createStatusService } from "./services/status-service.js";
34
35
  import { createTaskService } from "./services/task-service.js";
35
36
  import { createTranslationService } from "./services/translation-service.js";
@@ -99,7 +100,8 @@ export async function createServer(deps, options = {}) {
99
100
  sessionService: deps.sessionService,
100
101
  translationWorkerService: deps.translationWorkerService,
101
102
  harnessService: deps.harnessService,
102
- harnessFeedbackService: deps.harnessFeedbackService
103
+ harnessFeedbackService: deps.harnessFeedbackService,
104
+ runtimeCoordinator: deps.runtimeCoordinator
103
105
  });
104
106
  registerTaskRoutes(app, {
105
107
  projectService: deps.projectService,
@@ -290,6 +292,19 @@ export function createDefaultServerDeps(options = {}) {
290
292
  runtime,
291
293
  appSettings
292
294
  });
295
+ const runtimeCoordinator = createRuntimeCoordinatorService({
296
+ appSettings,
297
+ taskService,
298
+ sessionService,
299
+ translationService,
300
+ harnessService,
301
+ harnessFeedbackService,
302
+ roundService,
303
+ gatewayService,
304
+ async getStateRoot(repoRoot) {
305
+ return (await projectService.loadConfig(repoRoot)).stateRoot;
306
+ }
307
+ });
293
308
  const claudeHookService = createClaudeHookService({
294
309
  projectService,
295
310
  taskService,
@@ -328,6 +343,7 @@ export function createDefaultServerDeps(options = {}) {
328
343
  statusService,
329
344
  translationService,
330
345
  gatewayService,
346
+ runtimeCoordinator,
331
347
  runtime,
332
348
  diagnosticsService
333
349
  };
@@ -0,0 +1,162 @@
1
+ import { isVcmRoleName } from "../../shared/constants.js";
2
+ import { VcmError } from "../errors.js";
3
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
4
+ const EXPECTED_AUTO_RETROSPECTIVE_SKIP_CODES = new Set([
5
+ "HARNESS_FEEDBACK_ACTIVE",
6
+ "TASK_HARNESS_RETROSPECTIVE_EXISTS",
7
+ "TASK_FINAL_ACCEPTANCE_NOT_READY",
8
+ "HARNESS_ENGINEER_BUSY",
9
+ "HARNESS_ENGINEER_SESSION_MISSING",
10
+ "PROJECT_TOOL_TASK_REQUIRED"
11
+ ]);
12
+ export function createRuntimeCoordinatorService(deps) {
13
+ const locks = new Map();
14
+ async function withRepoLock(repoRoot, run) {
15
+ const previous = locks.get(repoRoot) ?? Promise.resolve({
16
+ activeTask: null,
17
+ gatewayStatus: null
18
+ });
19
+ const next = previous.catch(() => ({
20
+ activeTask: null,
21
+ gatewayStatus: null
22
+ })).then(run);
23
+ locks.set(repoRoot, next);
24
+ try {
25
+ return await next;
26
+ }
27
+ finally {
28
+ if (locks.get(repoRoot) === next) {
29
+ locks.delete(repoRoot);
30
+ }
31
+ }
32
+ }
33
+ return {
34
+ reconcileProject(repoRoot, input = {}) {
35
+ return withRepoLock(repoRoot, async () => {
36
+ const [activeTask, gatewayStatus] = await Promise.all([
37
+ resolveActiveTask(repoRoot, input.taskSlug),
38
+ deps.gatewayService.getStatus().catch(() => null)
39
+ ]);
40
+ const preferences = await deps.appSettings.getPreferences();
41
+ if (!activeTask) {
42
+ return { activeTask: null, gatewayStatus };
43
+ }
44
+ const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
45
+ const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
46
+ .then((status) => status.initialized)
47
+ .catch(() => false);
48
+ await Promise.all([
49
+ reconcileHarnessEngineer(repoRoot, activeTask),
50
+ reconcileTranslator(repoRoot, activeTask, preferences.translationEnabled && harnessInitialized)
51
+ ]);
52
+ if (preferences.translationEnabled && harnessInitialized) {
53
+ await startConversationTranslationListeners(repoRoot, activeTask);
54
+ }
55
+ else {
56
+ await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
57
+ }
58
+ if (preferences.autoTaskHarnessReviewEnabled) {
59
+ await maybeStartTaskHarnessRetrospective(repoRoot, activeTask);
60
+ }
61
+ return { activeTask, gatewayStatus };
62
+ });
63
+ }
64
+ };
65
+ async function resolveActiveTask(repoRoot, requestedTaskSlug) {
66
+ const tasks = await deps.taskService.listTasks(repoRoot);
67
+ const activeTasks = tasks.filter((task) => task.cleanupStatus !== "cleaned");
68
+ if (requestedTaskSlug) {
69
+ const requested = activeTasks.find((task) => task.taskSlug === requestedTaskSlug);
70
+ if (requested) {
71
+ return requested;
72
+ }
73
+ }
74
+ return activeTasks[0] ?? null;
75
+ }
76
+ async function reconcileHarnessEngineer(repoRoot, task) {
77
+ const existing = await deps.sessionService.getProjectHarnessEngineerSession(repoRoot);
78
+ if (!shouldAutoEnsureProjectToolSession(existing)) {
79
+ return;
80
+ }
81
+ await deps.sessionService.ensureProjectHarnessEngineerSession(repoRoot, {
82
+ taskSlug: task.taskSlug,
83
+ permissionMode: existing?.permissionMode,
84
+ model: existing?.model,
85
+ effort: existing?.effort
86
+ });
87
+ }
88
+ async function reconcileTranslator(repoRoot, task, enabled) {
89
+ if (!enabled) {
90
+ return;
91
+ }
92
+ const existing = await deps.sessionService.getProjectTranslatorSession(repoRoot);
93
+ if (!shouldAutoEnsureProjectToolSession(existing)) {
94
+ return;
95
+ }
96
+ await deps.sessionService.ensureProjectTranslatorSession(repoRoot, {
97
+ taskSlug: task.taskSlug,
98
+ permissionMode: existing?.permissionMode,
99
+ model: existing?.model,
100
+ effort: existing?.effort
101
+ });
102
+ }
103
+ function shouldAutoEnsureProjectToolSession(session) {
104
+ return Boolean(session && (session.status === "running" || session.claudeSessionId));
105
+ }
106
+ async function startConversationTranslationListeners(repoRoot, task) {
107
+ const translator = await deps.sessionService.getProjectTranslatorSession(repoRoot);
108
+ if (translator?.status !== "running") {
109
+ return;
110
+ }
111
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
112
+ const sessions = await deps.sessionService.listRoleSessions(repoRoot, task.taskSlug);
113
+ await Promise.all(sessions
114
+ .filter((session) => session.status === "running" && isVcmRoleName(session.role))
115
+ .map((session) => startConversationTranslationListener(repoRoot, taskRepoRoot, task.taskSlug, session.role)));
116
+ }
117
+ async function startConversationTranslationListener(repoRoot, taskRepoRoot, taskSlug, role) {
118
+ try {
119
+ await deps.translationService.startSession({
120
+ repoRoot,
121
+ taskRepoRoot,
122
+ taskSlug,
123
+ role
124
+ });
125
+ }
126
+ catch (error) {
127
+ if (error instanceof VcmError && error.code === "SESSION_NOT_RUNNING") {
128
+ return;
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+ async function maybeStartTaskHarnessRetrospective(repoRoot, task) {
134
+ const stateRoot = await deps.getStateRoot(repoRoot);
135
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
136
+ const roundState = await deps.roundService.getSessionRoundState({
137
+ repoRoot,
138
+ stateRepoRoot: taskRepoRoot,
139
+ stateRoot,
140
+ taskSlug: task.taskSlug
141
+ });
142
+ if (roundState.status !== "stopped"
143
+ || !roundState.roundId
144
+ || roundState.roleRecovery?.status === "failed") {
145
+ return;
146
+ }
147
+ try {
148
+ await deps.harnessFeedbackService.startTaskRetrospective(repoRoot, {
149
+ taskSlug: task.taskSlug,
150
+ taskRepoRoot,
151
+ handoffDir: task.handoffDir,
152
+ trigger: "auto"
153
+ });
154
+ }
155
+ catch (error) {
156
+ if (error instanceof VcmError && (EXPECTED_AUTO_RETROSPECTIVE_SKIP_CODES.has(error.code) || error.statusCode === 409)) {
157
+ return;
158
+ }
159
+ throw error;
160
+ }
161
+ }
162
+ }
@@ -14,6 +14,7 @@ const HARNESS_ENGINEER_DIR = ".ai/vcm/harness-engineer";
14
14
  const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
15
15
  const PROJECT_TRANSLATOR_SCOPE = "__project__";
16
16
  const PROJECT_HARNESS_ENGINEER_SCOPE = "__project_harness_engineer__";
17
+ const PROJECT_TOOL_CD_ENTER_DELAY_MS = 500;
17
18
  export function createSessionService(deps) {
18
19
  const now = deps.now ?? (() => new Date().toISOString());
19
20
  async function readCurrentHarnessRevision(repoRoot) {
@@ -301,7 +302,9 @@ export function createSessionService(deps) {
301
302
  }
302
303
  assertSafeCwdTarget(targetCwd);
303
304
  const timestamp = now();
304
- await submitTerminalInput(deps.runtime, session.id, `/cd ${targetCwd}`);
305
+ await submitTerminalInput(deps.runtime, session.id, formatClaudeCdCommand(targetCwd), {
306
+ enterDelayMs: PROJECT_TOOL_CD_ENTER_DELAY_MS
307
+ });
305
308
  const updated = {
306
309
  ...session,
307
310
  cwd: targetCwd,
@@ -1260,3 +1263,6 @@ function normalizeClaudeEffort(value) {
1260
1263
  }
1261
1264
  return "default";
1262
1265
  }
1266
+ function formatClaudeCdCommand(targetCwd) {
1267
+ return `/cd ${JSON.stringify(targetCwd)}`;
1268
+ }