vibe-coding-master 0.5.5 → 0.6.0

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.
@@ -23,4 +23,9 @@ export function registerGatewayRoutes(app, deps) {
23
23
  app.post("/api/gateway/binding/reset", async () => {
24
24
  return deps.gatewayService.resetBinding();
25
25
  });
26
+ // Arm/disarm the runtime channel-connection switch (process-local, not
27
+ // persisted). Returns the updated GatewayStatus.
28
+ app.put("/api/gateway/connection", async (request) => {
29
+ return deps.gatewayService.setConnectionEnabled(request.body.enabled);
30
+ });
26
31
  }
@@ -30,10 +30,20 @@ export function createGatewayService(deps) {
30
30
  let qrLogin = null;
31
31
  let larkRegistrationState = null;
32
32
  let lastFailedTranslation = null;
33
+ // Runtime channel-connection arming switch. Not persisted: every process starts
34
+ // disarmed. `ensurePolling` is the single chokepoint that reads it, so no
35
+ // self-heal path can connect the channel while this is false.
36
+ let connectionEnabled = false;
33
37
  function isRunning() {
34
38
  return Boolean(pollAbort && !pollAbort.signal.aborted);
35
39
  }
36
40
  async function ensurePolling() {
41
+ // Single chokepoint for the runtime connection switch: while disarmed, no
42
+ // auto-connect path (boot, getStatus/reconcile self-heal, QR success,
43
+ // updateSettings) may start the poll loop.
44
+ if (!connectionEnabled) {
45
+ return;
46
+ }
37
47
  if (isRunning()) {
38
48
  return;
39
49
  }
@@ -720,7 +730,7 @@ export function createGatewayService(deps) {
720
730
  await enableGatewayTranslationRuntime();
721
731
  }
722
732
  await ensurePolling();
723
- return deps.settings.expose(settings, isRunning());
733
+ return deps.settings.expose(settings, isRunning(), connectionEnabled);
724
734
  },
725
735
  async updateSettings(input) {
726
736
  const gatewayStartInput = input.enabled === true
@@ -745,15 +755,31 @@ export function createGatewayService(deps) {
745
755
  else {
746
756
  await stopPolling();
747
757
  }
748
- return deps.settings.expose(settings, isRunning());
758
+ return deps.settings.expose(settings, isRunning(), connectionEnabled);
749
759
  },
750
760
  async resetBinding() {
751
761
  await stopPolling();
762
+ // Disarm the connection switch on reset so no connection survives a
763
+ // binding reset.
764
+ connectionEnabled = false;
752
765
  lastFailedTranslation = null;
753
766
  qrLogin = null;
754
767
  larkRegistrationState = null;
755
768
  const settings = await deps.settings.resetBinding();
756
- return deps.settings.expose(settings, isRunning());
769
+ return deps.settings.expose(settings, isRunning(), connectionEnabled);
770
+ },
771
+ async setConnectionEnabled(enabled) {
772
+ connectionEnabled = enabled;
773
+ // Armed → connect now if an account is configured; disarmed → abort the
774
+ // poll loop (the Lark WS closes via the abort path in waitForUpdates).
775
+ if (enabled) {
776
+ await ensurePolling();
777
+ }
778
+ else {
779
+ await stopPolling();
780
+ }
781
+ const settings = await deps.settings.loadSettings();
782
+ return deps.settings.expose(settings, isRunning(), connectionEnabled);
757
783
  },
758
784
  async startQrLogin() {
759
785
  const settings = await deps.settings.loadSettings();
@@ -921,7 +947,7 @@ export function createGatewayService(deps) {
921
947
  });
922
948
  larkRegistrationState = null;
923
949
  await ensurePolling();
924
- const status = deps.settings.expose(settings, isRunning());
950
+ const status = deps.settings.expose(settings, isRunning(), connectionEnabled);
925
951
  return {
926
952
  status: "confirmed",
927
953
  appIdConfigured: Boolean(result.appId),
@@ -985,7 +1011,7 @@ export function createGatewayService(deps) {
985
1011
  });
986
1012
  larkRegistrationState = null;
987
1013
  await ensurePolling();
988
- const status = deps.settings.expose(settings, isRunning());
1014
+ const status = deps.settings.expose(settings, isRunning(), connectionEnabled);
989
1015
  return {
990
1016
  status: "confirmed",
991
1017
  appIdConfigured: true,
@@ -1013,7 +1039,9 @@ export function createGatewayService(deps) {
1013
1039
  const settings = await deps.settings.loadSettings();
1014
1040
  const account = toAccount(settings);
1015
1041
  const boundUserId = settings.binding.boundUserId;
1016
- if (!settings.enabled || !account || !boundUserId) {
1042
+ // A disarmed gateway never touches the channel: skip the outbound push.
1043
+ // The latest reply was already cached above and replays on the next /start.
1044
+ if (!connectionEnabled || !settings.enabled || !account || !boundUserId) {
1017
1045
  return;
1018
1046
  }
1019
1047
  const cursorKey = `${input.taskSlug}:project-manager:${input.session.claudeSessionId}`;
@@ -79,11 +79,12 @@ export function createGatewaySettingsService(deps) {
79
79
  updatedAt: now()
80
80
  });
81
81
  },
82
- expose(settings, running = false) {
82
+ expose(settings, running = false, connectionEnabled = false) {
83
83
  return {
84
84
  version: 1,
85
85
  enabled: settings.enabled,
86
86
  running,
87
+ connectionEnabled,
87
88
  channel: settings.channel,
88
89
  translationEnabled: settings.translationEnabled,
89
90
  currentProjectId: settings.currentProjectId,
@@ -15,6 +15,15 @@ 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
17
  const PROJECT_TOOL_CD_ENTER_DELAY_MS = 500;
18
+ // Project tool sessions launch a Claude Code TUI inside a PTY. The PTY reports
19
+ // "running" the instant it is spawned, which is earlier than the moment the TUI
20
+ // can actually accept pasted input. These bounds drive a quiescence-based
21
+ // readiness wait (first output seen, then no further output for a short window)
22
+ // used before any programmatic input, and as the liveness probe that detects a
23
+ // resume-by-id launch that died before becoming usable.
24
+ const SESSION_READY_POLL_INTERVAL_MS = 100;
25
+ const SESSION_READY_QUIESCENT_POLLS = 3;
26
+ const SESSION_READY_MAX_POLLS = 60;
18
27
  export function createSessionService(deps) {
19
28
  const now = deps.now ?? (() => new Date().toISOString());
20
29
  async function readCurrentHarnessRevision(repoRoot) {
@@ -201,6 +210,17 @@ export function createSessionService(deps) {
201
210
  };
202
211
  deps.registry.upsert(record);
203
212
  await persistTranslatorSession(deps.fs, repoRoot, record);
213
+ if (launchMode === "resume") {
214
+ if ((await waitForSessionInputReady(record.id)) === "exited") {
215
+ // Resume by claudeSessionId failed (Claude could not reopen the session
216
+ // and the process exited). Drop the stale id and rebuild a fresh session
217
+ // so a broken id cannot wedge auto-reconcile on the same resume forever.
218
+ deps.registry.remove(record.id);
219
+ await clearPersistedTranslatorSession(deps.fs, repoRoot);
220
+ return launchProjectTranslatorSession(repoRoot, input, "fresh");
221
+ }
222
+ return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
223
+ }
204
224
  return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
205
225
  }
206
226
  async function launchProjectHarnessEngineerSession(repoRoot, input, launchMode) {
@@ -288,6 +308,16 @@ export function createSessionService(deps) {
288
308
  };
289
309
  deps.registry.upsert(record);
290
310
  await persistHarnessEngineerSession(deps.fs, repoRoot, record);
311
+ if (launchMode === "resume") {
312
+ if ((await waitForSessionInputReady(record.id)) === "exited") {
313
+ // Resume by claudeSessionId failed; drop the stale id and rebuild a fresh
314
+ // session so a broken id cannot wedge auto-reconcile on the same resume.
315
+ deps.registry.remove(record.id);
316
+ await clearPersistedHarnessEngineerSession(deps.fs, repoRoot);
317
+ return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
318
+ }
319
+ return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
320
+ }
291
321
  return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
292
322
  }
293
323
  async function resolveProjectToolTaskContext(repoRoot, input, roleLabel) {
@@ -306,7 +336,42 @@ export function createSessionService(deps) {
306
336
  taskRepoRoot: getTaskRuntimeRepoRoot(task)
307
337
  };
308
338
  }
309
- async function migrateRunningProjectToolSessionCwd(repoRoot, session, targetCwd) {
339
+ // Wait until a freshly spawned/resumed session's TUI is ready to receive
340
+ // programmatic input, or until it has clearly failed to start. Readiness is
341
+ // inferred from terminal output quiescence reported by the runtime: once the
342
+ // session has emitted output and then stayed quiet for SESSION_READY_QUIESCENT_POLLS
343
+ // consecutive polls, the TUI prompt is treated as input-ready. The wait is
344
+ // capped at SESSION_READY_MAX_POLLS so a perpetually chatty (or perpetually
345
+ // silent) session still proceeds best-effort. Returns "exited" if the runtime
346
+ // session is gone or no longer running, which a resume launch treats as a
347
+ // resume-by-id failure.
348
+ async function waitForSessionInputReady(sessionId) {
349
+ let sawOutput = false;
350
+ let lastOutputAt;
351
+ let quietPolls = 0;
352
+ for (let poll = 0; poll < SESSION_READY_MAX_POLLS; poll += 1) {
353
+ const live = deps.runtime.getSession(sessionId);
354
+ if (!live || isExitedStatus(live.status)) {
355
+ return "exited";
356
+ }
357
+ if (live.lastOutputAt) {
358
+ if (!sawOutput || live.lastOutputAt !== lastOutputAt) {
359
+ sawOutput = true;
360
+ lastOutputAt = live.lastOutputAt;
361
+ quietPolls = 0;
362
+ }
363
+ else {
364
+ quietPolls += 1;
365
+ if (quietPolls >= SESSION_READY_QUIESCENT_POLLS) {
366
+ return "ready";
367
+ }
368
+ }
369
+ }
370
+ await delay(SESSION_READY_POLL_INTERVAL_MS);
371
+ }
372
+ return "ready";
373
+ }
374
+ async function migrateRunningProjectToolSessionCwd(repoRoot, session, targetCwd, options = {}) {
310
375
  if (session.role !== TRANSLATOR_ROLE
311
376
  && session.role !== HARNESS_ENGINEER_ROLE) {
312
377
  return session;
@@ -318,6 +383,9 @@ export function createSessionService(deps) {
318
383
  if (!runtimeSession || runtimeSession.status !== "running") {
319
384
  return session;
320
385
  }
386
+ if (!options.alreadyReady && (await waitForSessionInputReady(session.id)) === "exited") {
387
+ return session;
388
+ }
321
389
  assertSafeCwdTarget(targetCwd);
322
390
  const timestamp = now();
323
391
  await submitTerminalInput(deps.runtime, session.id, formatClaudeCdCommand(targetCwd), {
@@ -1293,3 +1361,12 @@ function formatClaudeCdCommand(targetCwd) {
1293
1361
  // newline is the only unsafe character and is rejected by assertSafeCwdTarget.
1294
1362
  return `/cd ${targetCwd}`;
1295
1363
  }
1364
+ function isExitedStatus(status) {
1365
+ return status === "exited" || status === "crashed" || status === "missing";
1366
+ }
1367
+ function delay(ms) {
1368
+ if (ms <= 0) {
1369
+ return Promise.resolve();
1370
+ }
1371
+ return new Promise((resolve) => setTimeout(resolve, ms));
1372
+ }