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.
- package/dist/backend/adapters/git-adapter.js +44 -0
- package/dist/backend/gateway/gateway-service.js +6 -2
- package/dist/backend/services/session-service.js +99 -14
- package/dist/backend/services/task-service.js +72 -7
- package/dist-frontend/assets/{index-CyDq6KRw.js → index-Cf5EOrjk.js} +48 -48
- package/dist-frontend/assets/index-sTAVWdNl.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-D6vwKigt.css +0 -32
|
@@ -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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
395
|
-
|
|
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
|
|
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
|
|
138
|
-
await deps.git
|
|
139
|
-
for (const statePath of statePaths) {
|
|
140
|
-
await deps.fs
|
|
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))) {
|