trekoon 0.2.1 → 0.2.4

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.
@@ -1,9 +1,11 @@
1
1
  import { findUnknownOption, parseArgs, readMissingOptionValue, readOption, suggestOptions } from "./arg-parser";
2
2
  import { safeErrorMessage, sqliteBusyFailure } from "./error-utils";
3
3
 
4
+ import { DomainError } from "../domain/types";
4
5
  import { failResult, okResult } from "../io/output";
5
6
  import { type CliContext, type CliResult } from "../runtime/command-types";
6
- import { MissingBranchDatabaseError } from "../sync/branch-db";
7
+ import { resolveStorageResolutionDiagnostics } from "../storage/database";
8
+ import { assertValidSourceRef } from "../sync/branch-db";
7
9
  import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
8
10
  import { type SyncResolution } from "../sync/types";
9
11
 
@@ -110,6 +112,15 @@ function formatConflictList(
110
112
  .join("\n");
111
113
  }
112
114
 
115
+ function formatDomainErrorHuman(message: string, details: Record<string, unknown> | undefined): string {
116
+ const operatorAction = typeof details?.operatorAction === "string" ? details.operatorAction : null;
117
+ return operatorAction ? `${message}\n${operatorAction}` : message;
118
+ }
119
+
120
+ function isStorageBootstrapError(code: string): boolean {
121
+ return code === "tracked_ignored_mismatch" || code === "ambiguous_legacy_state" || code === "legacy_import_failed";
122
+ }
123
+
113
124
  export async function runSync(context: CliContext): Promise<CliResult> {
114
125
  const parsed = parseArgs(context.args);
115
126
  const subcommand: string | undefined = parsed.positional[0];
@@ -133,11 +144,17 @@ export async function runSync(context: CliContext): Promise<CliResult> {
133
144
  }
134
145
 
135
146
  const sourceBranch: string = readOption(parsed.options, "from") ?? "main";
147
+ assertValidSourceRef(context.cwd, sourceBranch);
136
148
  const summary = syncStatus(context.cwd, sourceBranch);
137
149
 
150
+ const humanLines = [statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts)];
151
+ if (summary.sameBranch) {
152
+ humanLines.push(`Same-branch mode: already on '${summary.sourceBranch}', no sync needed`);
153
+ }
154
+
138
155
  return okResult({
139
156
  command: "sync.status",
140
- human: statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts),
157
+ human: humanLines.join("\n"),
141
158
  data: summary,
142
159
  });
143
160
  }
@@ -158,20 +175,26 @@ export async function runSync(context: CliContext): Promise<CliResult> {
158
175
  return usage("sync pull requires --from <branch>.", "sync.pull");
159
176
  }
160
177
 
178
+ assertValidSourceRef(context.cwd, sourceBranch);
161
179
  const summary = syncPull(context.cwd, sourceBranch);
162
180
 
181
+ const humanLines = [
182
+ `Pulled from '${summary.sourceBranch}'`,
183
+ `Scanned events: ${summary.scannedEvents}`,
184
+ `Applied events: ${summary.appliedEvents}`,
185
+ `Created conflicts: ${summary.createdConflicts}`,
186
+ `Malformed payloads: ${summary.diagnostics.malformedPayloadEvents}`,
187
+ `Quarantined events: ${summary.diagnostics.quarantinedEvents}`,
188
+ `Conflict events: ${summary.diagnostics.conflictEvents}`,
189
+ ...summary.diagnostics.errorHints,
190
+ ];
191
+ if (summary.sameBranch) {
192
+ humanLines.push(`Same-branch mode: already on '${summary.sourceBranch}', no sync needed`);
193
+ }
194
+
163
195
  return okResult({
164
196
  command: "sync.pull",
165
- human: [
166
- `Pulled from '${summary.sourceBranch}'`,
167
- `Scanned events: ${summary.scannedEvents}`,
168
- `Applied events: ${summary.appliedEvents}`,
169
- `Created conflicts: ${summary.createdConflicts}`,
170
- `Malformed payloads: ${summary.diagnostics.malformedPayloadEvents}`,
171
- `Quarantined events: ${summary.diagnostics.quarantinedEvents}`,
172
- `Conflict events: ${summary.diagnostics.conflictEvents}`,
173
- ...summary.diagnostics.errorHints,
174
- ].join("\n"),
197
+ human: humanLines.join("\n"),
175
198
  data: summary,
176
199
  });
177
200
  }
@@ -275,25 +298,43 @@ export async function runSync(context: CliContext): Promise<CliResult> {
275
298
 
276
299
  return usage(`Unknown sync subcommand '${subcommand}'.`);
277
300
  } catch (error) {
278
- if (error instanceof MissingBranchDatabaseError) {
301
+ const busyFailure = sqliteBusyFailure(resolvedCommand, error);
302
+ if (busyFailure !== null) {
303
+ return busyFailure;
304
+ }
305
+
306
+ if (error instanceof DomainError) {
307
+ if (isStorageBootstrapError(error.code)) {
308
+ const storageDiagnostics = resolveStorageResolutionDiagnostics(context.cwd);
309
+
310
+ return failResult({
311
+ command: resolvedCommand,
312
+ human: formatDomainErrorHuman(error.message, error.details),
313
+ data: {
314
+ reason: "storage_bootstrap_blocked",
315
+ ...storageDiagnostics,
316
+ },
317
+ error: {
318
+ code: error.code,
319
+ message: error.message,
320
+ },
321
+ });
322
+ }
323
+
279
324
  return failResult({
280
325
  command: resolvedCommand,
281
- human: error.message,
326
+ human: formatDomainErrorHuman(error.message, error.details),
282
327
  data: {
283
- reason: "missing_branch_db",
328
+ ...(error.details ?? {}),
329
+ reason: error.code,
284
330
  },
285
331
  error: {
286
- code: "missing_branch_db",
332
+ code: error.code,
287
333
  message: error.message,
288
334
  },
289
335
  });
290
336
  }
291
337
 
292
- const busyFailure = sqliteBusyFailure(resolvedCommand, error);
293
- if (busyFailure !== null) {
294
- return busyFailure;
295
- }
296
-
297
338
  const message = safeErrorMessage(error, "Unknown sync error.");
298
339
 
299
340
  return failResult({
@@ -0,0 +1,147 @@
1
+ import { TrackerDomain } from "../domain/tracker-domain";
2
+ import { type TaskRecord } from "../domain/types";
3
+
4
+ export const DEFAULT_OPEN_TASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
5
+ export const READY_REASON_READY = "all_dependencies_done";
6
+ export const READY_REASON_BLOCKED = "blocked_by_dependencies";
7
+
8
+ export interface DependencyBlocker {
9
+ readonly id: string;
10
+ readonly kind: "task" | "subtask";
11
+ readonly status: string;
12
+ }
13
+
14
+ export interface TaskReadyCandidate {
15
+ readonly task: TaskRecord;
16
+ readonly readiness: {
17
+ readonly isReady: boolean;
18
+ readonly reason: typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
19
+ };
20
+ readonly blockerSummary: {
21
+ readonly totalDependencies: number;
22
+ readonly blockedByCount: number;
23
+ readonly blockedBy: ReadonlyArray<DependencyBlocker>;
24
+ };
25
+ readonly ranking: {
26
+ readonly statusPriority: number;
27
+ readonly blockerCount: number;
28
+ readonly createdAt: number;
29
+ readonly id: string;
30
+ readonly rank: number;
31
+ };
32
+ }
33
+
34
+ export type ReadyReason = typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
35
+
36
+ export interface TaskReadinessSummary {
37
+ readonly totalOpenTasks: number;
38
+ readonly readyCount: number;
39
+ readonly returnedCount: number;
40
+ readonly appliedLimit: number | null;
41
+ readonly blockedCount: number;
42
+ readonly unresolvedDependencyCount: number;
43
+ }
44
+
45
+ export interface TaskReadinessResult {
46
+ readonly candidates: readonly TaskReadyCandidate[];
47
+ readonly blocked: readonly TaskReadyCandidate[];
48
+ readonly summary: TaskReadinessSummary;
49
+ }
50
+
51
+ export function taskStatusPriority(status: string): number {
52
+ if (status === "in_progress" || status === "in-progress") {
53
+ return 0;
54
+ }
55
+
56
+ if (status === "todo") {
57
+ return 1;
58
+ }
59
+
60
+ return 2;
61
+ }
62
+
63
+ export function buildTaskReadiness(domain: TrackerDomain, epicId: string | undefined): TaskReadinessResult {
64
+ const openStatuses = new Set<string>(DEFAULT_OPEN_TASK_STATUSES);
65
+ const openTasks = domain.listTasks(epicId).filter((task) => openStatuses.has(task.status));
66
+ const assessed = openTasks
67
+ .map((task) => {
68
+ const blockers: DependencyBlocker[] = [];
69
+ const dependencies = domain.listDependencies(task.id);
70
+ for (const dependency of dependencies) {
71
+ const dependencyStatus =
72
+ dependency.dependsOnKind === "task"
73
+ ? domain.getTaskOrThrow(dependency.dependsOnId).status
74
+ : domain.getSubtaskOrThrow(dependency.dependsOnId).status;
75
+
76
+ if (dependencyStatus !== "done") {
77
+ blockers.push({
78
+ id: dependency.dependsOnId,
79
+ kind: dependency.dependsOnKind,
80
+ status: dependencyStatus,
81
+ });
82
+ }
83
+ }
84
+
85
+ const blockerCount = blockers.length;
86
+ const readinessReason: ReadyReason = blockerCount === 0 ? READY_REASON_READY : READY_REASON_BLOCKED;
87
+ return {
88
+ task,
89
+ readiness: {
90
+ isReady: blockerCount === 0,
91
+ reason: readinessReason,
92
+ },
93
+ blockerSummary: {
94
+ totalDependencies: dependencies.length,
95
+ blockedByCount: blockerCount,
96
+ blockedBy: blockers,
97
+ },
98
+ ranking: {
99
+ statusPriority: taskStatusPriority(task.status),
100
+ blockerCount,
101
+ createdAt: task.createdAt,
102
+ id: task.id,
103
+ rank: 0,
104
+ },
105
+ };
106
+ })
107
+ .sort((left, right) => {
108
+ const byStatus = left.ranking.statusPriority - right.ranking.statusPriority;
109
+ if (byStatus !== 0) {
110
+ return byStatus;
111
+ }
112
+
113
+ const byBlockers = left.ranking.blockerCount - right.ranking.blockerCount;
114
+ if (byBlockers !== 0) {
115
+ return byBlockers;
116
+ }
117
+
118
+ const byCreatedAt = left.ranking.createdAt - right.ranking.createdAt;
119
+ if (byCreatedAt !== 0) {
120
+ return byCreatedAt;
121
+ }
122
+
123
+ return left.ranking.id.localeCompare(right.ranking.id);
124
+ })
125
+ .map((item, index) => ({
126
+ ...item,
127
+ ranking: {
128
+ ...item.ranking,
129
+ rank: index + 1,
130
+ },
131
+ }));
132
+
133
+ const candidates = assessed.filter((item) => item.readiness.isReady);
134
+ const blocked = assessed.filter((item) => !item.readiness.isReady);
135
+ return {
136
+ candidates,
137
+ blocked,
138
+ summary: {
139
+ totalOpenTasks: assessed.length,
140
+ readyCount: candidates.length,
141
+ returnedCount: candidates.length,
142
+ appliedLimit: null,
143
+ blockedCount: blocked.length,
144
+ unresolvedDependencyCount: blocked.reduce((total, item) => total + item.blockerSummary.blockedByCount, 0),
145
+ },
146
+ };
147
+ }
@@ -17,6 +17,18 @@ import {
17
17
  suggestOptions,
18
18
  } from "./arg-parser";
19
19
  import { unexpectedFailureResult } from "./error-utils";
20
+ import {
21
+ buildTaskReadiness,
22
+ DEFAULT_OPEN_TASK_STATUSES,
23
+ type DependencyBlocker,
24
+ READY_REASON_BLOCKED,
25
+ READY_REASON_READY,
26
+ type ReadyReason,
27
+ taskStatusPriority,
28
+ type TaskReadinessResult,
29
+ type TaskReadinessSummary,
30
+ type TaskReadyCandidate,
31
+ } from "./task-readiness";
20
32
 
21
33
  import { MutationService } from "../domain/mutation-service";
22
34
  import { TrackerDomain } from "../domain/tracker-domain";
@@ -33,56 +45,10 @@ function formatTask(task: TaskRecord): string {
33
45
  const VIEW_MODES = ["table", "compact", "tree", "detail"] as const;
34
46
  const LIST_VIEW_MODES = ["table", "compact"] as const;
35
47
  const DEFAULT_TASK_LIST_LIMIT = 10;
36
- const DEFAULT_OPEN_TASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
37
- const READY_REASON_READY = "all_dependencies_done";
38
- const READY_REASON_BLOCKED = "blocked_by_dependencies";
39
48
  const SEARCH_OPTIONS = ["fields", "preview"] as const;
40
49
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
41
50
  const CREATE_MANY_OPTIONS = ["epic", "e", "task"] as const;
42
51
 
43
- interface DependencyBlocker {
44
- readonly id: string;
45
- readonly kind: "task" | "subtask";
46
- readonly status: string;
47
- }
48
-
49
- interface TaskReadyCandidate {
50
- readonly task: TaskRecord;
51
- readonly readiness: {
52
- readonly isReady: boolean;
53
- readonly reason: typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
54
- };
55
- readonly blockerSummary: {
56
- readonly totalDependencies: number;
57
- readonly blockedByCount: number;
58
- readonly blockedBy: ReadonlyArray<DependencyBlocker>;
59
- };
60
- readonly ranking: {
61
- readonly statusPriority: number;
62
- readonly blockerCount: number;
63
- readonly createdAt: number;
64
- readonly id: string;
65
- readonly rank: number;
66
- };
67
- }
68
-
69
- type ReadyReason = typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
70
-
71
- interface TaskReadinessSummary {
72
- readonly totalOpenTasks: number;
73
- readonly readyCount: number;
74
- readonly returnedCount: number;
75
- readonly appliedLimit: number | null;
76
- readonly blockedCount: number;
77
- readonly unresolvedDependencyCount: number;
78
- }
79
-
80
- interface TaskReadinessResult {
81
- readonly candidates: readonly TaskReadyCandidate[];
82
- readonly blocked: readonly TaskReadyCandidate[];
83
- readonly summary: TaskReadinessSummary;
84
- }
85
-
86
52
  function parseIdsOption(rawIds: string | undefined): string[] {
87
53
  if (rawIds === undefined) {
88
54
  return [];
@@ -154,102 +120,6 @@ function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
154
120
  .filter((value) => value.length > 0);
155
121
  }
156
122
 
157
- function taskStatusPriority(status: string): number {
158
- if (status === "in_progress" || status === "in-progress") {
159
- return 0;
160
- }
161
-
162
- if (status === "todo") {
163
- return 1;
164
- }
165
-
166
- return 2;
167
- }
168
-
169
- function buildTaskReadiness(domain: TrackerDomain, epicId: string | undefined): TaskReadinessResult {
170
- const openStatuses = new Set<string>(DEFAULT_OPEN_TASK_STATUSES);
171
- const openTasks = domain.listTasks(epicId).filter((task) => openStatuses.has(task.status));
172
- const assessed = openTasks
173
- .map((task) => {
174
- const blockers: DependencyBlocker[] = [];
175
- const dependencies = domain.listDependencies(task.id);
176
- for (const dependency of dependencies) {
177
- const dependencyStatus =
178
- dependency.dependsOnKind === "task"
179
- ? domain.getTaskOrThrow(dependency.dependsOnId).status
180
- : domain.getSubtaskOrThrow(dependency.dependsOnId).status;
181
-
182
- if (dependencyStatus !== "done") {
183
- blockers.push({
184
- id: dependency.dependsOnId,
185
- kind: dependency.dependsOnKind,
186
- status: dependencyStatus,
187
- });
188
- }
189
- }
190
-
191
- const blockerCount = blockers.length;
192
- const readinessReason: ReadyReason = blockerCount === 0 ? READY_REASON_READY : READY_REASON_BLOCKED;
193
- return {
194
- task,
195
- readiness: {
196
- isReady: blockerCount === 0,
197
- reason: readinessReason,
198
- },
199
- blockerSummary: {
200
- totalDependencies: dependencies.length,
201
- blockedByCount: blockerCount,
202
- blockedBy: blockers,
203
- },
204
- ranking: {
205
- statusPriority: taskStatusPriority(task.status),
206
- blockerCount,
207
- createdAt: task.createdAt,
208
- id: task.id,
209
- },
210
- };
211
- })
212
- .sort((left, right) => {
213
- const byStatus = left.ranking.statusPriority - right.ranking.statusPriority;
214
- if (byStatus !== 0) {
215
- return byStatus;
216
- }
217
-
218
- const byBlockers = left.ranking.blockerCount - right.ranking.blockerCount;
219
- if (byBlockers !== 0) {
220
- return byBlockers;
221
- }
222
-
223
- const byCreatedAt = left.ranking.createdAt - right.ranking.createdAt;
224
- if (byCreatedAt !== 0) {
225
- return byCreatedAt;
226
- }
227
-
228
- return left.ranking.id.localeCompare(right.ranking.id);
229
- })
230
- .map((item, index) => ({
231
- ...item,
232
- ranking: {
233
- ...item.ranking,
234
- rank: index + 1,
235
- },
236
- }));
237
-
238
- const candidates = assessed.filter((item) => item.readiness.isReady);
239
- const blocked = assessed.filter((item) => !item.readiness.isReady);
240
- return {
241
- candidates,
242
- blocked,
243
- summary: {
244
- totalOpenTasks: assessed.length,
245
- readyCount: candidates.length,
246
- returnedCount: candidates.length,
247
- appliedLimit: null,
248
- blockedCount: blocked.length,
249
- unresolvedDependencyCount: blocked.reduce((total, item) => total + item.blockerSummary.blockedByCount, 0),
250
- },
251
- };
252
- }
253
123
 
254
124
  function formatTaskReadyCandidateLine(candidate: TaskReadyCandidate): string {
255
125
  return `${candidate.ranking.rank}. ${formatTask(candidate.task)} | reason=${candidate.readiness.reason} | blockers=${candidate.blockerSummary.blockedByCount}/${candidate.blockerSummary.totalDependencies}`;
@@ -1242,6 +1112,74 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1242
1112
  data: { task },
1243
1113
  });
1244
1114
  }
1115
+ case "done": {
1116
+ const taskId: string = parsed.positional[1] ?? "";
1117
+ if (taskId.length === 0) {
1118
+ return failResult({
1119
+ command: "task.done",
1120
+ human: "Provide a task id. Usage: trekoon task done <id>",
1121
+ data: { code: "invalid_input" },
1122
+ error: {
1123
+ code: "invalid_input",
1124
+ message: "Missing task id",
1125
+ },
1126
+ });
1127
+ }
1128
+
1129
+ const existingTask = domain.getTask(taskId);
1130
+ if (!existingTask) {
1131
+ return failResult({
1132
+ command: "task.done",
1133
+ human: `Task not found: ${taskId}`,
1134
+ data: { code: "not_found", id: taskId },
1135
+ error: {
1136
+ code: "not_found",
1137
+ message: `Task not found: ${taskId}`,
1138
+ },
1139
+ });
1140
+ }
1141
+
1142
+ if (existingTask.status === "done") {
1143
+ return failResult({
1144
+ command: "task.done",
1145
+ human: "Task is already done",
1146
+ data: { code: "already_done", id: taskId },
1147
+ error: {
1148
+ code: "already_done",
1149
+ message: "Task is already done",
1150
+ },
1151
+ });
1152
+ }
1153
+
1154
+ const completed = mutations.updateTask(taskId, { status: "done" });
1155
+ const readiness = buildTaskReadiness(domain, completed.epicId);
1156
+ const nextCandidate = readiness.candidates[0] ?? null;
1157
+
1158
+ const nextTree = nextCandidate !== null ? domain.buildTaskTreeDetailed(nextCandidate.task.id) : null;
1159
+ const nextDeps = nextCandidate?.blockerSummary.blockedBy ?? [];
1160
+
1161
+ const readinessStats = {
1162
+ readyCount: readiness.summary.readyCount,
1163
+ blockedCount: readiness.summary.blockedCount,
1164
+ };
1165
+
1166
+ let human = `Task ${completed.title} marked done.`;
1167
+ if (nextTree !== null && nextCandidate !== null) {
1168
+ human += `\nNext: ${formatTask(nextCandidate.task)}`;
1169
+ }
1170
+ human += `\nReadiness: ready=${readinessStats.readyCount}, blocked=${readinessStats.blockedCount}.`;
1171
+
1172
+ return okResult({
1173
+ command: "task.done",
1174
+ human,
1175
+ data: {
1176
+ completed,
1177
+ next: nextTree,
1178
+ nextDeps,
1179
+ readiness: readinessStats,
1180
+ },
1181
+ });
1182
+ }
1245
1183
  case "delete": {
1246
1184
  const taskId: string = parsed.positional[1] ?? "";
1247
1185
  mutations.deleteTask(taskId);
@@ -1255,7 +1193,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1255
1193
  default:
1256
1194
  return failResult({
1257
1195
  command: "task",
1258
- human: "Usage: trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete>",
1196
+ human: "Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete>",
1259
1197
  data: {
1260
1198
  args: context.args,
1261
1199
  },
@@ -6,22 +6,29 @@ import { resolveStoragePaths } from "../storage/path";
6
6
 
7
7
  export async function runWipe(context: CliContext): Promise<CliResult> {
8
8
  const confirmed: boolean = context.args.includes("--yes");
9
+ const paths = resolveStoragePaths(context.cwd);
10
+ const repoScoped: boolean = paths.storageMode === "git_common_dir";
11
+ const sharedAcrossWorktrees: boolean = repoScoped && paths.sharedStorageRoot !== paths.worktreeRoot;
12
+ const scopeLabel: string = repoScoped ? "repo-wide Trekoon state" : "local Trekoon state";
9
13
 
10
14
  if (!confirmed) {
11
15
  return failResult({
12
16
  command: "wipe",
13
- human: "Refusing to wipe local state without --yes.",
17
+ human: `Refusing to wipe ${scopeLabel} without --yes. This deletes ${paths.storageDir}${repoScoped ? " for the entire repository, including any linked worktrees that share this storage" : " for this working directory"}.`,
14
18
  data: {
15
19
  confirmed,
20
+ storageDir: paths.storageDir,
21
+ worktreeRoot: paths.worktreeRoot,
22
+ sharedStorageRoot: paths.sharedStorageRoot,
23
+ repoScoped,
16
24
  },
17
25
  error: {
18
26
  code: "confirmation_required",
19
- message: "Wipe requires --yes",
27
+ message: `Wipe requires --yes to remove ${scopeLabel}`,
20
28
  },
21
29
  });
22
30
  }
23
31
 
24
- const paths = resolveStoragePaths(context.cwd);
25
32
  const existed: boolean = existsSync(paths.storageDir);
26
33
 
27
34
  rmSync(paths.storageDir, { recursive: true, force: true });
@@ -29,10 +36,13 @@ export async function runWipe(context: CliContext): Promise<CliResult> {
29
36
  return okResult({
30
37
  command: "wipe",
31
38
  human: existed
32
- ? `Removed local Trekoon state at ${paths.storageDir}`
33
- : `No local Trekoon state found at ${paths.storageDir}`,
39
+ ? `Removed ${scopeLabel} at ${paths.storageDir}${repoScoped ? ` for repository ${paths.sharedStorageRoot}` : ""}${sharedAcrossWorktrees ? ", which is shared with linked worktrees" : ""}.`
40
+ : `No ${scopeLabel} found at ${paths.storageDir}${repoScoped ? ` for repository ${paths.sharedStorageRoot}` : ""}${sharedAcrossWorktrees ? ", which is shared with linked worktrees" : ""}.`,
34
41
  data: {
35
42
  storageDir: paths.storageDir,
43
+ worktreeRoot: paths.worktreeRoot,
44
+ sharedStorageRoot: paths.sharedStorageRoot,
45
+ repoScoped,
36
46
  wiped: existed,
37
47
  },
38
48
  });