vibe-coding-master 0.7.24 → 0.7.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.
@@ -3,6 +3,7 @@ import { VcmError } from "../errors.js";
3
3
  export const TERMINAL_REPLAY_TAIL_LIMIT_BYTES = 2 * 1024 * 1024;
4
4
  export function createNodePtyTerminalRuntime(deps) {
5
5
  const entries = new Map();
6
+ const processExitListeners = new Set();
6
7
  const now = deps.now ?? (() => new Date().toISOString());
7
8
  const id = deps.id ?? (() => `session_${Date.now()}_${Math.random().toString(16).slice(2)}`);
8
9
  const emit = (entry, event) => {
@@ -68,6 +69,12 @@ export function createNodePtyTerminalRuntime(deps) {
68
69
  if (!disposeEntry(entries, entry, exitCode === 0 ? "exited" : "crashed", exitCode)) {
69
70
  return;
70
71
  }
72
+ for (const listener of processExitListeners) {
73
+ listener({
74
+ session: { ...entry.session },
75
+ exitCode
76
+ });
77
+ }
71
78
  emit(entry, {
72
79
  sessionId: session.id,
73
80
  taskSlug: input.taskSlug,
@@ -117,14 +124,16 @@ export function createNodePtyTerminalRuntime(deps) {
117
124
  },
118
125
  async stop(sessionId) {
119
126
  const entry = getEntry(entries, sessionId);
127
+ const exitCode = entry.session.exitCode ?? null;
128
+ const disposed = disposeEntry(entries, entry, "exited", exitCode);
120
129
  entry.process.kill();
121
- if (disposeEntry(entries, entry, "exited", entry.session.exitCode ?? null)) {
130
+ if (disposed) {
122
131
  emit(entry, {
123
132
  sessionId,
124
133
  taskSlug: entry.session.taskSlug,
125
134
  role: entry.session.role,
126
135
  type: "exit",
127
- exitCode: entry.session.exitCode ?? null
136
+ exitCode
128
137
  });
129
138
  entry.listeners.clear();
130
139
  }
@@ -132,8 +141,8 @@ export function createNodePtyTerminalRuntime(deps) {
132
141
  },
133
142
  async restart(sessionId) {
134
143
  const entry = getEntry(entries, sessionId);
135
- entry.process.kill();
136
144
  disposeEntry(entries, entry, "exited", entry.session.exitCode ?? null);
145
+ entry.process.kill();
137
146
  entry.listeners.clear();
138
147
  await entry.logWriter.close();
139
148
  return create(entry.input, sessionId);
@@ -175,6 +184,12 @@ export function createNodePtyTerminalRuntime(deps) {
175
184
  return () => {
176
185
  entry.listeners.delete(listener);
177
186
  };
187
+ },
188
+ subscribeProcessExits(listener) {
189
+ processExitListeners.add(listener);
190
+ return () => {
191
+ processExitListeners.delete(listener);
192
+ };
178
193
  }
179
194
  };
180
195
  }
@@ -42,9 +42,9 @@ import { createTaskCloseService } from "./services/task-close-service.js";
42
42
  import { createTaskWorkflowService } from "./services/task-workflow-service.js";
43
43
  import { createTaskLaunchService } from "./services/task-launch-service.js";
44
44
  import { createTerminalInterruptService } from "./services/terminal-interrupt-service.js";
45
+ import { createTerminalProcessExitService } from "./services/terminal-process-exit-service.js";
45
46
  import { createTranslationService } from "./services/translation-service.js";
46
47
  import { createUsageAnalyticsService } from "./services/usage-analytics-service.js";
47
- import { createTurnReconcilerService } from "./services/turn-reconciler-service.js";
48
48
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
49
49
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
50
50
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
@@ -174,10 +174,12 @@ export async function createServer(deps, options = {}) {
174
174
  app.addHook("onReady", async () => {
175
175
  await deps.ccrIntegration.initialize();
176
176
  await cleanupRecentTranslationRuntime(deps);
177
+ deps.terminalProcessExitService.start();
177
178
  deps.runtimeCoordinator.start();
178
179
  await deps.gatewayService.start();
179
180
  });
180
181
  app.addHook("onClose", async () => {
182
+ deps.terminalProcessExitService.stop();
181
183
  deps.runtimeCoordinator.stop();
182
184
  await deps.gatewayService.stop();
183
185
  });
@@ -390,11 +392,6 @@ export function createDefaultServerDeps(options = {}) {
390
392
  translationWorkerService,
391
393
  architectRestartService
392
394
  });
393
- const turnReconciler = createTurnReconcilerService({
394
- sessionService,
395
- roundService,
396
- claudeHookService
397
- });
398
395
  const runtimeCoordinator = createRuntimeCoordinatorService({
399
396
  appSettings,
400
397
  projectService,
@@ -406,7 +403,6 @@ export function createDefaultServerDeps(options = {}) {
406
403
  autoMemoryService,
407
404
  roundService,
408
405
  gatewayService,
409
- turnReconciler,
410
406
  async getStateRoot(repoRoot) {
411
407
  return (await projectService.loadConfig(repoRoot)).stateRoot;
412
408
  }
@@ -418,6 +414,13 @@ export function createDefaultServerDeps(options = {}) {
418
414
  sessionService,
419
415
  roundService
420
416
  });
417
+ const terminalProcessExitService = createTerminalProcessExitService({
418
+ runtime,
419
+ projectService,
420
+ taskService,
421
+ sessionService,
422
+ roundService
423
+ });
421
424
  const diagnosticsService = createDiagnosticsService({
422
425
  appRoot,
423
426
  runtime,
@@ -450,6 +453,7 @@ export function createDefaultServerDeps(options = {}) {
450
453
  runtimeCoordinator,
451
454
  runtimeRecoveryService,
452
455
  terminalInterruptService,
456
+ terminalProcessExitService,
453
457
  runtime,
454
458
  diagnosticsService,
455
459
  usageAnalyticsService
@@ -837,18 +837,6 @@ export function createClaudeHookService(deps) {
837
837
  return processStopHook(input, { allowBlock: true });
838
838
  });
839
839
  },
840
- handleReconciledTurnEnd(input) {
841
- return withRoleHookLock(input, async () => {
842
- const eventName = parseHookEvent(input.event.hook_event_name);
843
- if (eventName === "Stop") {
844
- return processStopHook(input, { allowBlock: false });
845
- }
846
- if (eventName === "StopFailure") {
847
- return processStopFailureHook(input);
848
- }
849
- throwUnsupportedEvent(eventName);
850
- });
851
- },
852
840
  handlePermissionRequestHook
853
841
  };
854
842
  }
@@ -12,7 +12,6 @@ const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
12
12
  const GATE_REVIEW_VERSION = 1;
13
13
  const REVIEWER_ROLE = "reviewer";
14
14
  const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
15
- const DEFAULT_REPORT_TIMEOUT_MS = 30 * 60 * 1000;
16
15
  const activeRuns = new Set();
17
16
  const ARCHITECTURE_ANALYSIS_FIELDS = [
18
17
  "Evidence Read",
@@ -92,7 +91,6 @@ const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
92
91
  export function createGateReviewService(deps) {
93
92
  const now = deps.now ?? (() => new Date().toISOString());
94
93
  const reportPollIntervalMs = deps.reportPollIntervalMs ?? DEFAULT_REPORT_POLL_INTERVAL_MS;
95
- const reportTimeoutMs = deps.reportTimeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS;
96
94
  async function getContext(repoRoot, taskSlug) {
97
95
  const projectConfig = await deps.projectService.loadConfig(repoRoot);
98
96
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
@@ -457,10 +455,7 @@ export function createGateReviewService(deps) {
457
455
  role: REVIEWER_ROLE,
458
456
  eventName: "UserPromptSubmit"
459
457
  });
460
- const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), {
461
- intervalMs: reportPollIntervalMs,
462
- timeoutMs: reportTimeoutMs
463
- });
458
+ const parsed = await waitForGateReport(deps.fs, context.taskRepoRoot, gate, requestId, now(), reportPollIntervalMs);
464
459
  const completedAt = now();
465
460
  await updateRequestStatus(deps.fs, context, requestId, "completed", {
466
461
  completedAt,
@@ -1093,28 +1088,18 @@ Decision: approve|request_changes
1093
1088
  Summary: <one or two sentences>
1094
1089
  [/VCM GATE REVIEW]`;
1095
1090
  }
1096
- async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, options) {
1097
- const startedAt = Date.now();
1098
- let lastError;
1099
- while (Date.now() - startedAt <= options.timeoutMs) {
1091
+ async function waitForGateReport(fs, taskRepoRoot, gate, requestId, timestamp, intervalMs) {
1092
+ while (true) {
1100
1093
  try {
1101
1094
  return await parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp);
1102
1095
  }
1103
1096
  catch (error) {
1104
- lastError = error;
1105
1097
  if (!isPendingReportError(error)) {
1106
1098
  throw error;
1107
1099
  }
1108
1100
  }
1109
- await delay(options.intervalMs);
1101
+ await delay(intervalMs);
1110
1102
  }
1111
- const detail = errorMessage(lastError);
1112
- throw new VcmError({
1113
- code: "GATE_REVIEW_REPORT_TIMEOUT",
1114
- message: `Reviewer did not produce a valid ${gate} report within ${Math.round(options.timeoutMs / 1000)}s.`,
1115
- statusCode: 504,
1116
- hint: detail
1117
- });
1118
1103
  }
1119
1104
  async function parseGateReport(fs, taskRepoRoot, gate, requestId, timestamp) {
1120
1105
  const reportPath = reportPathForGate(gate);
@@ -261,6 +261,25 @@ export function createRoundService(deps) {
261
261
  return toSessionRoundState(next, timestamp);
262
262
  });
263
263
  },
264
+ async recordTerminalExit(input) {
265
+ return withTaskLock(input, async () => {
266
+ const timestamp = now();
267
+ const state = await load(input);
268
+ const next = applyTerminalExit({
269
+ state,
270
+ taskSlug: input.taskSlug,
271
+ role: input.role,
272
+ timestamp
273
+ });
274
+ if (!terminalExitWasApplied(state, next, input.role)) {
275
+ return toSessionRoundState(state, timestamp);
276
+ }
277
+ await save(input, next);
278
+ clearSettleTimer(input);
279
+ await updateSessionStatus(input, "stopped");
280
+ return toSessionRoundState(next, timestamp);
281
+ });
282
+ },
264
283
  async setRoleRecovery(input) {
265
284
  return withTaskLock(input, async () => {
266
285
  const timestamp = now();
@@ -392,6 +411,16 @@ function applyStop(input) {
392
411
  };
393
412
  }
394
413
  function applyManualInterrupt(input) {
414
+ return applyForcedTurnStop(input, "manual-interrupt");
415
+ }
416
+ function applyTerminalExit(input) {
417
+ const stopped = applyForcedTurnStop(input, "terminal-exit");
418
+ return stopped.currentRound?.stopReason === "terminal-exit"
419
+ && stopped.roleRecovery?.role === input.role
420
+ ? { ...stopped, roleRecovery: undefined }
421
+ : stopped;
422
+ }
423
+ function applyForcedTurnStop(input, stopReason) {
395
424
  const current = input.state.currentRound;
396
425
  if (!current || current.status === "stopped" || !current.activeTurnStartedAt || current.activeRole !== input.role) {
397
426
  return {
@@ -407,7 +436,7 @@ function applyManualInterrupt(input) {
407
436
  activeRole: input.role,
408
437
  lastTurnEndedAt: input.timestamp,
409
438
  stoppedAt: input.timestamp,
410
- stopReason: "manual-interrupt",
439
+ stopReason,
411
440
  settleDeadlineAt: undefined,
412
441
  activeTurnStartedAt: undefined,
413
442
  ccActiveMs: current.ccActiveMs + activeDurationMs,
@@ -431,6 +460,13 @@ function manualInterruptWasApplied(previous, next, role) {
431
460
  && next.currentRound?.stopReason === "manual-interrupt"
432
461
  && next.currentRound.status === "stopped");
433
462
  }
463
+ function terminalExitWasApplied(previous, next, role) {
464
+ return Boolean(previous.currentRound?.status === "running"
465
+ && previous.currentRound.activeTurnStartedAt
466
+ && previous.currentRound.activeRole === role
467
+ && next.currentRound?.stopReason === "terminal-exit"
468
+ && next.currentRound.status === "stopped");
469
+ }
434
470
  function toSessionRoundState(state, updatedAt) {
435
471
  const current = state.currentRound;
436
472
  if (!current) {
@@ -587,7 +623,9 @@ function normalizeRound(input) {
587
623
  : typeof legacy.pausedAt === "string"
588
624
  ? legacy.pausedAt
589
625
  : undefined,
590
- stopReason: input.stopReason === "manual-interrupt" || input.stopReason === "runtime-recovery"
626
+ stopReason: input.stopReason === "manual-interrupt"
627
+ || input.stopReason === "runtime-recovery"
628
+ || input.stopReason === "terminal-exit"
591
629
  ? input.stopReason
592
630
  : undefined,
593
631
  activeTurnStartedAt: typeof input.activeTurnStartedAt === "string"
@@ -45,8 +45,6 @@ export function createRuntimeCoordinatorService(deps) {
45
45
  return { activeTask: null, gatewayStatus };
46
46
  }
47
47
  const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
48
- const stateRoot = await deps.getStateRoot(repoRoot);
49
- await deps.turnReconciler.reconcileTask(repoRoot, activeTask, stateRoot);
50
48
  const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
51
49
  .then((status) => status.initialized)
52
50
  .catch(() => false);
@@ -712,6 +712,44 @@ export function createSessionService(deps) {
712
712
  await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
713
713
  return updated;
714
714
  }
715
+ async function recordProjectToolProcessExit(repoRoot, current, input, persist) {
716
+ const timestamp = now();
717
+ const turnWasRunning = current.activityStatus === "running";
718
+ const updated = {
719
+ ...current,
720
+ status: input.status,
721
+ activityStatus: "idle",
722
+ pid: undefined,
723
+ exitCode: input.exitCode,
724
+ lastTurnEndedAt: turnWasRunning ? timestamp : current.lastTurnEndedAt,
725
+ updatedAt: timestamp
726
+ };
727
+ deps.registry.upsert(updated);
728
+ await persist(deps.fs, repoRoot, updated);
729
+ return { record: updated, turnWasRunning };
730
+ }
731
+ async function recordTaskRoleProcessExit(repoRoot, taskSlug, role, input) {
732
+ const current = await getTaskRoleSessionView(repoRoot, taskSlug, role);
733
+ if (!current || current.id !== input.sessionId) {
734
+ return undefined;
735
+ }
736
+ const timestamp = now();
737
+ const turnWasRunning = current.activityStatus === "running";
738
+ const updated = {
739
+ ...current,
740
+ status: input.status,
741
+ activityStatus: "idle",
742
+ pid: undefined,
743
+ exitCode: input.exitCode,
744
+ lastTurnEndedAt: turnWasRunning ? timestamp : current.lastTurnEndedAt,
745
+ updatedAt: timestamp
746
+ };
747
+ deps.registry.upsert(updated);
748
+ const config = await deps.projectService.loadConfig(repoRoot);
749
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
750
+ await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
751
+ return { record: updated, turnWasRunning };
752
+ }
715
753
  return {
716
754
  async assertModelLaunchReady(model = "default") {
717
755
  await getModelLaunchEnvironment(normalizeClaudeModel(model));
@@ -1113,6 +1151,27 @@ export function createSessionService(deps) {
1113
1151
  }
1114
1152
  return markTaskRoleActivityIdle(repoRoot, taskSlug, role, sessionId);
1115
1153
  },
1154
+ async recordTerminalProcessExit(repoRoot, input) {
1155
+ const registered = deps.registry.get(input.sessionId);
1156
+ const role = registered?.role;
1157
+ const taskSlug = registered?.taskSlug;
1158
+ if (!role || !taskSlug) {
1159
+ return undefined;
1160
+ }
1161
+ if (role === TRANSLATOR_ROLE && taskSlug === PROJECT_TRANSLATOR_SCOPE) {
1162
+ const current = await getProjectToolSessionView(repoRoot, TRANSLATOR_ROLE);
1163
+ return current?.id === input.sessionId
1164
+ ? recordProjectToolProcessExit(repoRoot, current, input, persistTranslatorSession)
1165
+ : undefined;
1166
+ }
1167
+ if (role === HARNESS_ENGINEER_ROLE && taskSlug === PROJECT_HARNESS_ENGINEER_SCOPE) {
1168
+ const current = await getProjectToolSessionView(repoRoot, HARNESS_ENGINEER_ROLE);
1169
+ return current?.id === input.sessionId
1170
+ ? recordProjectToolProcessExit(repoRoot, current, input, persistHarnessEngineerSession)
1171
+ : undefined;
1172
+ }
1173
+ return recordTaskRoleProcessExit(repoRoot, taskSlug, role, input);
1174
+ },
1116
1175
  async markRoleActivityRunning(repoRoot, taskSlug, role, expectedSessionId) {
1117
1176
  const current = await this.getRoleSession(repoRoot, taskSlug, role);
1118
1177
  if (!current || (expectedSessionId && current.id !== expectedSessionId)) {
@@ -0,0 +1,43 @@
1
+ import { isVcmRoleName } from "../../shared/constants.js";
2
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
3
+ export function createTerminalProcessExitService(deps) {
4
+ let unsubscribe;
5
+ async function handleProcessExit(event) {
6
+ const repoRoot = event.session.repoRoot;
7
+ if (!repoRoot) {
8
+ return;
9
+ }
10
+ const recorded = await deps.sessionService.recordTerminalProcessExit(repoRoot, {
11
+ sessionId: event.session.id,
12
+ status: event.session.status === "crashed" ? "crashed" : "exited",
13
+ exitCode: event.exitCode
14
+ });
15
+ if (!recorded?.turnWasRunning || !isVcmRoleName(recorded.record.role)) {
16
+ return;
17
+ }
18
+ const config = await deps.projectService.loadConfig(repoRoot);
19
+ const task = await deps.taskService.loadTask(repoRoot, recorded.record.taskSlug);
20
+ await deps.roundService.recordTerminalExit({
21
+ repoRoot,
22
+ stateRepoRoot: getTaskRuntimeRepoRoot(task),
23
+ stateRoot: config.stateRoot,
24
+ taskSlug: recorded.record.taskSlug,
25
+ role: recorded.record.role
26
+ });
27
+ }
28
+ return {
29
+ start() {
30
+ if (unsubscribe) {
31
+ return;
32
+ }
33
+ unsubscribe = deps.runtime.subscribeProcessExits((event) => {
34
+ void handleProcessExit(event).catch(() => undefined);
35
+ });
36
+ },
37
+ stop() {
38
+ unsubscribe?.();
39
+ unsubscribe = undefined;
40
+ },
41
+ handleProcessExit
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.7.24",
3
+ "version": "0.7.26",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,59 +0,0 @@
1
- import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
2
- import { getTaskRuntimeRepoRoot } from "./task-service.js";
3
- export function createTurnReconcilerService(deps) {
4
- const readEvidence = deps.readTranscriptEvidence ?? readTranscriptTurnEvidence;
5
- return {
6
- async reconcileTask(repoRoot, task, stateRoot) {
7
- const taskRepoRoot = getTaskRuntimeRepoRoot(task);
8
- const round = await deps.roundService.getSessionRoundState({
9
- repoRoot,
10
- stateRepoRoot: taskRepoRoot,
11
- stateRoot,
12
- taskSlug: task.taskSlug
13
- });
14
- if (round.status !== "running"
15
- || !round.activeRole
16
- || !round.activeTurnStartedAt
17
- || round.roleRecovery) {
18
- return { status: "inactive" };
19
- }
20
- const role = round.activeRole;
21
- const session = await deps.sessionService.getRoleSession(repoRoot, task.taskSlug, role);
22
- const evidence = session
23
- ? await readEvidence({ ...session, lastTurnStartedAt: round.activeTurnStartedAt })
24
- : {};
25
- if (evidence.completion) {
26
- await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "Stop", {
27
- vcm_reconcile_reason: "transcript-end-turn",
28
- vcm_completion_id: evidence.completion.id,
29
- vcm_completion_at: evidence.completion.timestamp
30
- }));
31
- return { status: "completed", role, reason: "transcript-end-turn" };
32
- }
33
- if (!session || session.status !== "running") {
34
- const reason = session ? "terminal-session-exited" : "terminal-session-missing";
35
- await deps.claudeHookService.handleReconciledTurnEnd(buildReconciledHook(task.taskSlug, role, session, "StopFailure", {
36
- error: reason.replaceAll("-", "_"),
37
- error_details: `VCM reconciled an active turn because its ${reason.replaceAll("-", " ")}.`
38
- }));
39
- return { status: "failed", role, reason };
40
- }
41
- return { status: "active" };
42
- }
43
- };
44
- }
45
- function buildReconciledHook(taskSlug, role, session, eventName, evidence) {
46
- return {
47
- taskSlug,
48
- role,
49
- event: {
50
- hook_event_name: eventName,
51
- ...(session?.claudeSessionId ? { session_id: session.claudeSessionId } : {}),
52
- ...(session?.transcriptPath ? { transcript_path: session.transcriptPath } : {}),
53
- ...(session?.cwd ? { cwd: session.cwd } : {}),
54
- ...(session?.id ? { vcm_runtime_session_id: session.id } : {}),
55
- vcm_reconciled: true,
56
- ...evidence
57
- }
58
- };
59
- }