vibe-coding-master 0.6.12 → 0.6.13

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.
@@ -223,6 +223,35 @@ export function createGitAdapter(runner) {
223
223
  hint: result.stderr
224
224
  });
225
225
  },
226
+ async isWorktreeRegistered(repoRoot, worktreePath) {
227
+ const result = await runGit(runner, repoRoot, ["worktree", "list", "--porcelain"]);
228
+ if (result.exitCode !== 0) {
229
+ throw new VcmError({
230
+ code: "GIT_ERROR",
231
+ message: "Unable to list Git worktrees.",
232
+ statusCode: 400,
233
+ hint: result.stderr
234
+ });
235
+ }
236
+ const expectedPath = await normalizeWorktreePath(worktreePath);
237
+ for (const candidate of parseWorktreePaths(result.stdout)) {
238
+ if (await normalizeWorktreePath(candidate) === expectedPath) {
239
+ return true;
240
+ }
241
+ }
242
+ return false;
243
+ },
244
+ async pruneWorktrees(repoRoot) {
245
+ const result = await runGit(runner, repoRoot, ["worktree", "prune"]);
246
+ if (result.exitCode !== 0) {
247
+ throw new VcmError({
248
+ code: "GIT_WORKTREE_PRUNE_FAILED",
249
+ message: "Unable to prune Git worktree metadata.",
250
+ statusCode: 400,
251
+ hint: result.stderr
252
+ });
253
+ }
254
+ },
226
255
  async mergeBranchFastForward(repoRoot, branch) {
227
256
  const result = await runGit(runner, repoRoot, ["merge", "--ff-only", branch]);
228
257
  if (result.exitCode !== 0) {
@@ -387,6 +416,21 @@ async function pathExists(targetPath) {
387
416
  async function runGit(runner, repoRoot, args) {
388
417
  return runner.run("git", [...await buildSafeDirectoryArgs(repoRoot), ...args], { cwd: repoRoot });
389
418
  }
419
+ function parseWorktreePaths(output) {
420
+ return output
421
+ .split(/\r?\n/)
422
+ .filter((line) => line.startsWith("worktree "))
423
+ .map((line) => line.slice("worktree ".length).trim())
424
+ .filter(Boolean);
425
+ }
426
+ async function normalizeWorktreePath(worktreePath) {
427
+ try {
428
+ return path.resolve(await fs.realpath(worktreePath));
429
+ }
430
+ catch {
431
+ return path.resolve(worktreePath);
432
+ }
433
+ }
390
434
  async function buildSafeDirectoryArgs(repoRoot) {
391
435
  const safeDirs = new Set([repoRoot]);
392
436
  try {
@@ -627,12 +627,16 @@ export function createGatewayService(deps) {
627
627
  },
628
628
  updatedAt: now()
629
629
  });
630
- return [
630
+ const lines = [
631
631
  `Closed task: ${result.taskSlug}`,
632
632
  result.removedWorktreePath ? `removed worktree: ${result.removedWorktreePath}` : "removed worktree: none",
633
633
  result.deletedBranch ? `deleted branch: ${result.deletedBranch}` : "deleted branch: none",
634
634
  `removed state paths: ${result.removedStatePaths.length}`
635
- ].join("\n");
635
+ ];
636
+ if (result.warnings?.length) {
637
+ lines.push("warnings:", ...result.warnings.map((warning) => `- ${warning}`));
638
+ }
639
+ return lines.join("\n");
636
640
  }
637
641
  async function stopRunningRoleSessions(repoRoot, taskSlug) {
638
642
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
@@ -26,6 +26,7 @@ const SESSION_READY_QUIESCENT_POLLS = 3;
26
26
  const SESSION_READY_MAX_POLLS = 60;
27
27
  export function createSessionService(deps) {
28
28
  const now = deps.now ?? (() => new Date().toISOString());
29
+ const isProcessAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
29
30
  async function readCurrentHarnessRevision(repoRoot) {
30
31
  return (await readHarnessRevisionState(deps.fs, repoRoot)).revision;
31
32
  }
@@ -221,7 +222,13 @@ export function createSessionService(deps) {
221
222
  await clearPersistedTranslatorSession(deps.fs, repoRoot);
222
223
  return launchProjectTranslatorSession(repoRoot, input, "fresh");
223
224
  }
224
- return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
225
+ const migrated = await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true });
226
+ if (migrated.status !== "running") {
227
+ deps.registry.remove(record.id);
228
+ await clearPersistedTranslatorSession(deps.fs, repoRoot);
229
+ return launchProjectTranslatorSession(repoRoot, input, "fresh");
230
+ }
231
+ return withHarnessRevisionView(repoRoot, migrated);
225
232
  }
226
233
  return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
227
234
  }
@@ -319,7 +326,13 @@ export function createSessionService(deps) {
319
326
  await clearPersistedHarnessEngineerSession(deps.fs, repoRoot);
320
327
  return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
321
328
  }
322
- return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true }));
329
+ const migrated = await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot, { alreadyReady: true });
330
+ if (migrated.status !== "running") {
331
+ deps.registry.remove(record.id);
332
+ await clearPersistedHarnessEngineerSession(deps.fs, repoRoot);
333
+ return launchProjectHarnessEngineerSession(repoRoot, input, "fresh");
334
+ }
335
+ return withHarnessRevisionView(repoRoot, migrated);
323
336
  }
324
337
  return withHarnessRevisionView(repoRoot, await migrateRunningProjectToolSessionCwd(repoRoot, record, taskContext.taskRepoRoot));
325
338
  }
@@ -354,7 +367,7 @@ export function createSessionService(deps) {
354
367
  let quietPolls = 0;
355
368
  for (let poll = 0; poll < SESSION_READY_MAX_POLLS; poll += 1) {
356
369
  const live = deps.runtime.getSession(sessionId);
357
- if (!live || isExitedStatus(live.status)) {
370
+ if (!isRuntimeSessionAlive(live)) {
358
371
  return "exited";
359
372
  }
360
373
  if (live.lastOutputAt) {
@@ -379,21 +392,29 @@ export function createSessionService(deps) {
379
392
  && session.role !== HARNESS_ENGINEER_ROLE) {
380
393
  return session;
381
394
  }
382
- if (samePath(session.cwd, targetCwd)) {
383
- return session;
384
- }
385
395
  const runtimeSession = deps.runtime.getSession(session.id);
386
- if (!runtimeSession || runtimeSession.status !== "running") {
396
+ if (!isRuntimeSessionAlive(runtimeSession)) {
397
+ return markProjectToolRuntimeUnavailable(repoRoot, session);
398
+ }
399
+ if (samePath(session.cwd, targetCwd)) {
387
400
  return session;
388
401
  }
389
402
  if (!options.alreadyReady && (await waitForSessionInputReady(session.id)) === "exited") {
390
- return session;
403
+ return markProjectToolRuntimeUnavailable(repoRoot, session);
391
404
  }
392
405
  assertSafeCwdTarget(targetCwd);
393
406
  const timestamp = now();
394
- await submitTerminalInput(deps.runtime, session.id, formatClaudeCdCommand(targetCwd), {
395
- enterDelayMs: PROJECT_TOOL_CD_ENTER_DELAY_MS
396
- });
407
+ try {
408
+ await submitTerminalInput(deps.runtime, session.id, formatClaudeCdCommand(targetCwd), {
409
+ enterDelayMs: PROJECT_TOOL_CD_ENTER_DELAY_MS
410
+ });
411
+ }
412
+ catch (error) {
413
+ if (isSessionMissingError(error) || !isRuntimeSessionAlive(deps.runtime.getSession(session.id))) {
414
+ return markProjectToolRuntimeUnavailable(repoRoot, session);
415
+ }
416
+ throw error;
417
+ }
397
418
  // `cwd` tracks the logical `/cd` target only. The transcript stays anchored at
398
419
  // the first-launch cwd (repoRoot for project tools), so transcriptPath must
399
420
  // not be recomputed from targetCwd here.
@@ -407,6 +428,19 @@ export function createSessionService(deps) {
407
428
  await persistProjectScopedToolSession(repoRoot, updated);
408
429
  return updated;
409
430
  }
431
+ async function markProjectToolRuntimeUnavailable(repoRoot, session) {
432
+ const updated = {
433
+ ...session,
434
+ status: getRecoverableStatus(session),
435
+ activityStatus: "idle",
436
+ pid: undefined,
437
+ exitCode: session.exitCode ?? null,
438
+ updatedAt: now()
439
+ };
440
+ deps.registry.remove(session.id);
441
+ await persistProjectScopedToolSession(repoRoot, updated);
442
+ return updated;
443
+ }
410
444
  async function resumeProjectToolSessionAtCwd(repoRoot, session, targetCwd) {
411
445
  const live = toRoleSessionRecordView(session.role === TRANSLATOR_ROLE
412
446
  ? getRegisteredProjectTranslatorSession(deps.registry, deps.runtime)
@@ -443,6 +477,24 @@ export function createSessionService(deps) {
443
477
  VCM_SESSION_ID: session.claudeSessionId
444
478
  }
445
479
  });
480
+ if ((await waitForSessionInputReady(runtimeSession.id)) === "exited") {
481
+ deps.registry.remove(runtimeSession.id);
482
+ return markProjectToolRuntimeUnavailable(repoRoot, {
483
+ ...session,
484
+ id: runtimeSession.id,
485
+ status: "crashed",
486
+ activityStatus: "idle",
487
+ command: startCommand.display,
488
+ permissionMode,
489
+ model,
490
+ effort,
491
+ pid: runtimeSession.pid,
492
+ startedAt: runtimeSession.startedAt,
493
+ updatedAt: now(),
494
+ lastOutputAt: runtimeSession.lastOutputAt,
495
+ exitCode: runtimeSession.exitCode ?? 1
496
+ });
497
+ }
446
498
  const timestamp = now();
447
499
  const resumed = {
448
500
  ...session,
@@ -466,6 +518,12 @@ export function createSessionService(deps) {
466
518
  await persistProjectScopedToolSession(repoRoot, resumed);
467
519
  return migrateRunningProjectToolSessionCwd(repoRoot, resumed, targetCwd);
468
520
  }
521
+ function isRuntimeSessionAlive(session) {
522
+ if (!session || isExitedStatus(session.status) || session.pid === undefined) {
523
+ return false;
524
+ }
525
+ return isProcessAlive(session.pid);
526
+ }
469
527
  async function persistProjectScopedToolSession(repoRoot, session) {
470
528
  if (session.role === TRANSLATOR_ROLE) {
471
529
  await persistTranslatorSession(deps.fs, repoRoot, session);
@@ -1116,6 +1174,33 @@ function getRecoverableStatus(record) {
1116
1174
  }
1117
1175
  return "resumable";
1118
1176
  }
1177
+ function isSessionMissingError(error) {
1178
+ if (error instanceof VcmError && error.code === "SESSION_MISSING") {
1179
+ return true;
1180
+ }
1181
+ return typeof error === "object" &&
1182
+ error !== null &&
1183
+ "code" in error &&
1184
+ error.code === "SESSION_MISSING";
1185
+ }
1186
+ function withoutRuntimeOnlySessionFields(session) {
1187
+ const { pid: _pid, ...persisted } = session;
1188
+ return persisted;
1189
+ }
1190
+ function defaultIsProcessAlive(pid) {
1191
+ try {
1192
+ process.kill(pid, 0);
1193
+ return true;
1194
+ }
1195
+ catch (error) {
1196
+ return getErrorCode(error) === "EPERM";
1197
+ }
1198
+ }
1199
+ function getErrorCode(error) {
1200
+ return typeof error === "object" && error !== null && "code" in error
1201
+ ? String(error.code)
1202
+ : undefined;
1203
+ }
1119
1204
  function samePath(left, right) {
1120
1205
  return path.resolve(left) === path.resolve(right);
1121
1206
  }
@@ -1290,7 +1375,7 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
1290
1375
  claudeSessionId: session.claudeSessionId,
1291
1376
  transcriptPath: session.transcriptPath,
1292
1377
  status: session.status,
1293
- record
1378
+ record: withoutRuntimeOnlySessionFields(record)
1294
1379
  }
1295
1380
  }
1296
1381
  });
@@ -1332,7 +1417,7 @@ async function persistTranslatorSession(fs, repoRoot, session) {
1332
1417
  role: session.role,
1333
1418
  updatedAt: session.updatedAt,
1334
1419
  record: {
1335
- ...session,
1420
+ ...withoutRuntimeOnlySessionFields(session),
1336
1421
  taskSlug: PROJECT_TRANSLATOR_SCOPE
1337
1422
  }
1338
1423
  });
@@ -1349,7 +1434,7 @@ async function persistHarnessEngineerSession(fs, repoRoot, session) {
1349
1434
  role: session.role,
1350
1435
  updatedAt: session.updatedAt,
1351
1436
  record: {
1352
- ...session,
1437
+ ...withoutRuntimeOnlySessionFields(session),
1353
1438
  taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
1354
1439
  }
1355
1440
  });
@@ -130,26 +130,91 @@ export function createTaskService(deps) {
130
130
  const config = await deps.projectService.loadConfig(repoRoot);
131
131
  const task = await this.loadTask(repoRoot, taskSlug);
132
132
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
133
- const statePaths = getTaskStatePaths(deps.projectService.getProjectDataRoot(repoRoot), taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
133
+ const taskStoreRoot = deps.projectService.getProjectDataRoot(repoRoot);
134
+ const taskPath = getTaskPath(taskStoreRoot, taskSlug);
135
+ const statePaths = getTaskStatePaths(taskStoreRoot, taskRepoRoot, config.stateRoot, config.handoffRoot, taskSlug);
134
136
  const removedStatePaths = [];
137
+ const warnings = [];
135
138
  const cleanedAt = now();
136
139
  assertTaskWorktreePath(repoRoot, task.worktreePath);
137
- await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force ?? true });
138
- await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch ?? true });
139
- for (const statePath of statePaths) {
140
- await deps.fs.removePath(statePath, { recursive: true, force: true });
141
- removedStatePaths.push(statePath);
140
+ await removeTaskWorktreeIdempotent(deps.fs, deps.git, repoRoot, task.worktreePath, options.force ?? true, warnings);
141
+ await deleteTaskBranchIdempotent(deps.git, repoRoot, task.branch, options.forceDeleteBranch ?? true);
142
+ for (const statePath of statePaths.filter((candidate) => candidate !== taskPath)) {
143
+ await removeWorktreeStatePathBestEffort(deps.fs, statePath, removedStatePaths, warnings);
142
144
  }
145
+ await deps.fs.removePath(taskPath, { recursive: true, force: true });
146
+ removedStatePaths.push(taskPath);
143
147
  return {
144
148
  taskSlug,
145
149
  removedWorktreePath: task.worktreePath,
146
150
  removedStatePaths,
147
151
  deletedBranch: task.branch,
148
- cleanedAt
152
+ cleanedAt,
153
+ warnings: warnings.length > 0 ? warnings : undefined
149
154
  };
150
155
  }
151
156
  };
152
157
  }
158
+ async function removeTaskWorktreeIdempotent(fs, git, repoRoot, worktreePath, force, warnings) {
159
+ const wasRegistered = await git.isWorktreeRegistered(repoRoot, worktreePath);
160
+ if (wasRegistered) {
161
+ try {
162
+ await git.removeWorktree(repoRoot, worktreePath, { force });
163
+ }
164
+ catch (error) {
165
+ await pruneWorktreesBestEffort(git, repoRoot, warnings);
166
+ if (await git.isWorktreeRegistered(repoRoot, worktreePath)) {
167
+ throw error;
168
+ }
169
+ warnings.push(`Git worktree metadata was already cleared for ${worktreePath}; continuing cleanup.`);
170
+ }
171
+ }
172
+ else {
173
+ await pruneWorktreesBestEffort(git, repoRoot, warnings);
174
+ }
175
+ if (await fs.pathExists(worktreePath)) {
176
+ try {
177
+ await fs.removePath?.(worktreePath, { recursive: true, force: true });
178
+ }
179
+ catch (error) {
180
+ warnings.push(`Unable to remove stale task worktree directory ${worktreePath}: ${describeError(error)}`);
181
+ }
182
+ }
183
+ }
184
+ async function deleteTaskBranchIdempotent(git, repoRoot, branch, force) {
185
+ if (!(await git.branchExists(repoRoot, branch))) {
186
+ return;
187
+ }
188
+ try {
189
+ await git.deleteBranch(repoRoot, branch, { force });
190
+ }
191
+ catch (error) {
192
+ if (!(await git.branchExists(repoRoot, branch))) {
193
+ return;
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+ async function pruneWorktreesBestEffort(git, repoRoot, warnings) {
199
+ try {
200
+ await git.pruneWorktrees(repoRoot);
201
+ }
202
+ catch (error) {
203
+ warnings.push(`Unable to prune Git worktree metadata: ${describeError(error)}`);
204
+ }
205
+ }
206
+ async function removeWorktreeStatePathBestEffort(fs, statePath, removedStatePaths, warnings) {
207
+ try {
208
+ await fs.removePath?.(statePath, { recursive: true, force: true });
209
+ removedStatePaths.push(statePath);
210
+ }
211
+ catch (error) {
212
+ warnings.push(`Unable to remove stale task state path ${statePath}: ${describeError(error)}`);
213
+ }
214
+ }
215
+ function describeError(error) {
216
+ return error instanceof Error ? error.message : String(error);
217
+ }
153
218
  async function readStoredTasks(fs, taskStoreRoot) {
154
219
  const tasksDir = path.join(taskStoreRoot, "tasks");
155
220
  if (!(await fs.pathExists(tasksDir))) {