vibe-coding-master 0.6.11 → 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 -16
- package/dist/backend/services/task-service.js +72 -7
- package/dist-frontend/assets/{index-DIfaGVNJ.js → index-Cf5EOrjk.js} +46 -46
- 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);
|
|
@@ -671,7 +729,6 @@ export function createSessionService(deps) {
|
|
|
671
729
|
...current,
|
|
672
730
|
claudeSessionId: sessionIdentity.claudeSessionId,
|
|
673
731
|
transcriptPath: sessionIdentity.transcriptPath,
|
|
674
|
-
cwd: input.cwd ?? current.cwd,
|
|
675
732
|
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
676
733
|
lastHookEventAt: timestamp,
|
|
677
734
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
@@ -797,7 +854,6 @@ export function createSessionService(deps) {
|
|
|
797
854
|
...current,
|
|
798
855
|
claudeSessionId: sessionIdentity.claudeSessionId,
|
|
799
856
|
transcriptPath: sessionIdentity.transcriptPath,
|
|
800
|
-
cwd: input.cwd ?? current.cwd,
|
|
801
857
|
activityStatus: isTurnEnd ? "idle" : isCompact ? current.activityStatus : "running",
|
|
802
858
|
lastHookEventAt: timestamp,
|
|
803
859
|
lastTurnEndedAt: isTurnEnd ? timestamp : current.lastTurnEndedAt,
|
|
@@ -1118,6 +1174,33 @@ function getRecoverableStatus(record) {
|
|
|
1118
1174
|
}
|
|
1119
1175
|
return "resumable";
|
|
1120
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
|
+
}
|
|
1121
1204
|
function samePath(left, right) {
|
|
1122
1205
|
return path.resolve(left) === path.resolve(right);
|
|
1123
1206
|
}
|
|
@@ -1292,7 +1375,7 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
|
|
|
1292
1375
|
claudeSessionId: session.claudeSessionId,
|
|
1293
1376
|
transcriptPath: session.transcriptPath,
|
|
1294
1377
|
status: session.status,
|
|
1295
|
-
record
|
|
1378
|
+
record: withoutRuntimeOnlySessionFields(record)
|
|
1296
1379
|
}
|
|
1297
1380
|
}
|
|
1298
1381
|
});
|
|
@@ -1334,7 +1417,7 @@ async function persistTranslatorSession(fs, repoRoot, session) {
|
|
|
1334
1417
|
role: session.role,
|
|
1335
1418
|
updatedAt: session.updatedAt,
|
|
1336
1419
|
record: {
|
|
1337
|
-
...session,
|
|
1420
|
+
...withoutRuntimeOnlySessionFields(session),
|
|
1338
1421
|
taskSlug: PROJECT_TRANSLATOR_SCOPE
|
|
1339
1422
|
}
|
|
1340
1423
|
});
|
|
@@ -1351,7 +1434,7 @@ async function persistHarnessEngineerSession(fs, repoRoot, session) {
|
|
|
1351
1434
|
role: session.role,
|
|
1352
1435
|
updatedAt: session.updatedAt,
|
|
1353
1436
|
record: {
|
|
1354
|
-
...session,
|
|
1437
|
+
...withoutRuntimeOnlySessionFields(session),
|
|
1355
1438
|
taskSlug: PROJECT_HARNESS_ENGINEER_SCOPE
|
|
1356
1439
|
}
|
|
1357
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))) {
|