trekoon 0.2.0 → 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.
- package/.agents/skills/trekoon/SKILL.md +232 -297
- package/README.md +288 -16
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +116 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +490 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +83 -17
- package/src/commands/init.ts +115 -9
- package/src/commands/migrate.ts +11 -4
- package/src/commands/quickstart.ts +76 -30
- package/src/commands/session.ts +223 -0
- package/src/commands/skills.ts +100 -63
- package/src/commands/subtask.ts +224 -26
- package/src/commands/sync.ts +64 -17
- package/src/commands/task-readiness.ts +147 -0
- package/src/commands/task.ts +277 -168
- package/src/commands/wipe.ts +15 -5
- package/src/domain/mutation-service.ts +152 -0
- package/src/domain/tracker-domain.ts +503 -0
- package/src/domain/types.ts +80 -0
- package/src/runtime/cli-shell.ts +83 -5
- package/src/storage/database.ts +86 -0
- package/src/storage/migrations.ts +48 -0
- package/src/storage/path.ts +70 -21
- package/src/storage/schema.ts +9 -2
- package/src/storage/worktree-recovery.ts +376 -0
- package/src/sync/branch-db.ts +87 -35
- package/src/sync/git-context.ts +7 -2
- package/src/sync/service.ts +131 -95
- package/src/sync/types.ts +2 -0
package/src/commands/task.ts
CHANGED
|
@@ -2,24 +2,41 @@ import {
|
|
|
2
2
|
SEARCH_REPLACE_FIELDS,
|
|
3
3
|
findUnknownOption,
|
|
4
4
|
hasFlag,
|
|
5
|
+
isValidCompactTempKey,
|
|
5
6
|
parseArgs,
|
|
7
|
+
parseCompactFields,
|
|
6
8
|
parseCsvEnumOption,
|
|
7
9
|
parseStrictNonNegativeInt,
|
|
8
10
|
parseStrictPositiveInt,
|
|
9
11
|
readEnumOption,
|
|
10
12
|
readMissingOptionValue,
|
|
11
13
|
readOption,
|
|
14
|
+
readOptions,
|
|
15
|
+
readUnexpectedPositionals,
|
|
12
16
|
resolvePreviewApplyMode,
|
|
13
17
|
suggestOptions,
|
|
14
18
|
} from "./arg-parser";
|
|
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";
|
|
15
32
|
|
|
16
33
|
import { MutationService } from "../domain/mutation-service";
|
|
17
34
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
18
|
-
import {
|
|
35
|
+
import { type CompactBatchResultContract, type CompactTaskSpec, type SearchEntityMatch, type TaskRecord } from "../domain/types";
|
|
19
36
|
import { formatHumanTable } from "../io/human-table";
|
|
20
37
|
import { failResult, okResult } from "../io/output";
|
|
21
38
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
22
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
39
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
23
40
|
|
|
24
41
|
function formatTask(task: TaskRecord): string {
|
|
25
42
|
return `${task.id} | epic=${task.epicId} | ${task.title} | ${task.status}`;
|
|
@@ -28,54 +45,9 @@ function formatTask(task: TaskRecord): string {
|
|
|
28
45
|
const VIEW_MODES = ["table", "compact", "tree", "detail"] as const;
|
|
29
46
|
const LIST_VIEW_MODES = ["table", "compact"] as const;
|
|
30
47
|
const DEFAULT_TASK_LIST_LIMIT = 10;
|
|
31
|
-
const DEFAULT_OPEN_TASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
32
|
-
const READY_REASON_READY = "all_dependencies_done";
|
|
33
|
-
const READY_REASON_BLOCKED = "blocked_by_dependencies";
|
|
34
48
|
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
35
49
|
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
36
|
-
|
|
37
|
-
interface DependencyBlocker {
|
|
38
|
-
readonly id: string;
|
|
39
|
-
readonly kind: "task" | "subtask";
|
|
40
|
-
readonly status: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface TaskReadyCandidate {
|
|
44
|
-
readonly task: TaskRecord;
|
|
45
|
-
readonly readiness: {
|
|
46
|
-
readonly isReady: boolean;
|
|
47
|
-
readonly reason: typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
|
|
48
|
-
};
|
|
49
|
-
readonly blockerSummary: {
|
|
50
|
-
readonly totalDependencies: number;
|
|
51
|
-
readonly blockedByCount: number;
|
|
52
|
-
readonly blockedBy: ReadonlyArray<DependencyBlocker>;
|
|
53
|
-
};
|
|
54
|
-
readonly ranking: {
|
|
55
|
-
readonly statusPriority: number;
|
|
56
|
-
readonly blockerCount: number;
|
|
57
|
-
readonly createdAt: number;
|
|
58
|
-
readonly id: string;
|
|
59
|
-
readonly rank: number;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
type ReadyReason = typeof READY_REASON_READY | typeof READY_REASON_BLOCKED;
|
|
64
|
-
|
|
65
|
-
interface TaskReadinessSummary {
|
|
66
|
-
readonly totalOpenTasks: number;
|
|
67
|
-
readonly readyCount: number;
|
|
68
|
-
readonly returnedCount: number;
|
|
69
|
-
readonly appliedLimit: number | null;
|
|
70
|
-
readonly blockedCount: number;
|
|
71
|
-
readonly unresolvedDependencyCount: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface TaskReadinessResult {
|
|
75
|
-
readonly candidates: readonly TaskReadyCandidate[];
|
|
76
|
-
readonly blocked: readonly TaskReadyCandidate[];
|
|
77
|
-
readonly summary: TaskReadinessSummary;
|
|
78
|
-
}
|
|
50
|
+
const CREATE_MANY_OPTIONS = ["epic", "e", "task"] as const;
|
|
79
51
|
|
|
80
52
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
81
53
|
if (rawIds === undefined) {
|
|
@@ -148,102 +120,6 @@ function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
|
148
120
|
.filter((value) => value.length > 0);
|
|
149
121
|
}
|
|
150
122
|
|
|
151
|
-
function taskStatusPriority(status: string): number {
|
|
152
|
-
if (status === "in_progress" || status === "in-progress") {
|
|
153
|
-
return 0;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (status === "todo") {
|
|
157
|
-
return 1;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return 2;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function buildTaskReadiness(domain: TrackerDomain, epicId: string | undefined): TaskReadinessResult {
|
|
164
|
-
const openStatuses = new Set<string>(DEFAULT_OPEN_TASK_STATUSES);
|
|
165
|
-
const openTasks = domain.listTasks(epicId).filter((task) => openStatuses.has(task.status));
|
|
166
|
-
const assessed = openTasks
|
|
167
|
-
.map((task) => {
|
|
168
|
-
const blockers: DependencyBlocker[] = [];
|
|
169
|
-
const dependencies = domain.listDependencies(task.id);
|
|
170
|
-
for (const dependency of dependencies) {
|
|
171
|
-
const dependencyStatus =
|
|
172
|
-
dependency.dependsOnKind === "task"
|
|
173
|
-
? domain.getTaskOrThrow(dependency.dependsOnId).status
|
|
174
|
-
: domain.getSubtaskOrThrow(dependency.dependsOnId).status;
|
|
175
|
-
|
|
176
|
-
if (dependencyStatus !== "done") {
|
|
177
|
-
blockers.push({
|
|
178
|
-
id: dependency.dependsOnId,
|
|
179
|
-
kind: dependency.dependsOnKind,
|
|
180
|
-
status: dependencyStatus,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const blockerCount = blockers.length;
|
|
186
|
-
const readinessReason: ReadyReason = blockerCount === 0 ? READY_REASON_READY : READY_REASON_BLOCKED;
|
|
187
|
-
return {
|
|
188
|
-
task,
|
|
189
|
-
readiness: {
|
|
190
|
-
isReady: blockerCount === 0,
|
|
191
|
-
reason: readinessReason,
|
|
192
|
-
},
|
|
193
|
-
blockerSummary: {
|
|
194
|
-
totalDependencies: dependencies.length,
|
|
195
|
-
blockedByCount: blockerCount,
|
|
196
|
-
blockedBy: blockers,
|
|
197
|
-
},
|
|
198
|
-
ranking: {
|
|
199
|
-
statusPriority: taskStatusPriority(task.status),
|
|
200
|
-
blockerCount,
|
|
201
|
-
createdAt: task.createdAt,
|
|
202
|
-
id: task.id,
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
})
|
|
206
|
-
.sort((left, right) => {
|
|
207
|
-
const byStatus = left.ranking.statusPriority - right.ranking.statusPriority;
|
|
208
|
-
if (byStatus !== 0) {
|
|
209
|
-
return byStatus;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const byBlockers = left.ranking.blockerCount - right.ranking.blockerCount;
|
|
213
|
-
if (byBlockers !== 0) {
|
|
214
|
-
return byBlockers;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const byCreatedAt = left.ranking.createdAt - right.ranking.createdAt;
|
|
218
|
-
if (byCreatedAt !== 0) {
|
|
219
|
-
return byCreatedAt;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return left.ranking.id.localeCompare(right.ranking.id);
|
|
223
|
-
})
|
|
224
|
-
.map((item, index) => ({
|
|
225
|
-
...item,
|
|
226
|
-
ranking: {
|
|
227
|
-
...item.ranking,
|
|
228
|
-
rank: index + 1,
|
|
229
|
-
},
|
|
230
|
-
}));
|
|
231
|
-
|
|
232
|
-
const candidates = assessed.filter((item) => item.readiness.isReady);
|
|
233
|
-
const blocked = assessed.filter((item) => !item.readiness.isReady);
|
|
234
|
-
return {
|
|
235
|
-
candidates,
|
|
236
|
-
blocked,
|
|
237
|
-
summary: {
|
|
238
|
-
totalOpenTasks: assessed.length,
|
|
239
|
-
readyCount: candidates.length,
|
|
240
|
-
returnedCount: candidates.length,
|
|
241
|
-
appliedLimit: null,
|
|
242
|
-
blockedCount: blocked.length,
|
|
243
|
-
unresolvedDependencyCount: blocked.reduce((total, item) => total + item.blockerSummary.blockedByCount, 0),
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
123
|
|
|
248
124
|
function formatTaskReadyCandidateLine(candidate: TaskReadyCandidate): string {
|
|
249
125
|
return `${candidate.ranking.rank}. ${formatTask(candidate.task)} | reason=${candidate.readiness.reason} | blockers=${candidate.blockerSummary.blockedByCount}/${candidate.blockerSummary.totalDependencies}`;
|
|
@@ -383,29 +259,9 @@ function formatTaskShowTable(taskTree: {
|
|
|
383
259
|
}
|
|
384
260
|
|
|
385
261
|
function failFromError(error: unknown): CliResult {
|
|
386
|
-
|
|
387
|
-
return failResult({
|
|
388
|
-
command: "task",
|
|
389
|
-
human: error.message,
|
|
390
|
-
data: {
|
|
391
|
-
code: error.code,
|
|
392
|
-
...(error.details ?? {}),
|
|
393
|
-
},
|
|
394
|
-
error: {
|
|
395
|
-
code: error.code,
|
|
396
|
-
message: error.message,
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return failResult({
|
|
262
|
+
return unexpectedFailureResult(error, {
|
|
402
263
|
command: "task",
|
|
403
264
|
human: "Unexpected task command failure",
|
|
404
|
-
data: {},
|
|
405
|
-
error: {
|
|
406
|
-
code: "internal_error",
|
|
407
|
-
message: "Unexpected task command failure",
|
|
408
|
-
},
|
|
409
265
|
});
|
|
410
266
|
}
|
|
411
267
|
|
|
@@ -424,10 +280,145 @@ function failMissingOptionValue(command: string, option: string): CliResult {
|
|
|
424
280
|
});
|
|
425
281
|
}
|
|
426
282
|
|
|
283
|
+
function failBatchSpec(command: string, human: string, data: Record<string, unknown>): CliResult {
|
|
284
|
+
return failResult({
|
|
285
|
+
command,
|
|
286
|
+
human,
|
|
287
|
+
data,
|
|
288
|
+
error: {
|
|
289
|
+
code: "invalid_input",
|
|
290
|
+
message: human,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function failUnexpectedPositionals(command: string, unexpected: readonly string[]): CliResult {
|
|
296
|
+
return failBatchSpec(command, `Unexpected positional arguments: ${unexpected.join(", ")}.`, {
|
|
297
|
+
unexpectedPositionals: unexpected,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
|
|
302
|
+
return failBatchSpec(command, `${option === "task" ? "Task" : "Spec"} spec ${index + 1} is missing a ${field}.`, {
|
|
303
|
+
option,
|
|
304
|
+
index,
|
|
305
|
+
rawSpec,
|
|
306
|
+
field,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseTaskCreateManySpecs(rawSpecs: readonly string[]): { specs: CompactTaskSpec[]; error?: CliResult } {
|
|
311
|
+
const specs: CompactTaskSpec[] = [];
|
|
312
|
+
const seenTempKeys = new Set<string>();
|
|
313
|
+
|
|
314
|
+
for (const [index, rawSpec] of rawSpecs.entries()) {
|
|
315
|
+
const parsed = parseCompactFields(rawSpec);
|
|
316
|
+
if (parsed.invalidEscape !== null) {
|
|
317
|
+
return {
|
|
318
|
+
specs: [],
|
|
319
|
+
error: failBatchSpec("task.create-many", `Invalid escape sequence ${parsed.invalidEscape} in --task spec ${index + 1}.`, {
|
|
320
|
+
option: "task",
|
|
321
|
+
index,
|
|
322
|
+
rawSpec,
|
|
323
|
+
invalidEscape: parsed.invalidEscape,
|
|
324
|
+
}),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (parsed.hasDanglingEscape) {
|
|
329
|
+
return {
|
|
330
|
+
specs: [],
|
|
331
|
+
error: failBatchSpec("task.create-many", `Trailing escape in --task spec ${index + 1}.`, {
|
|
332
|
+
option: "task",
|
|
333
|
+
index,
|
|
334
|
+
rawSpec,
|
|
335
|
+
}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (parsed.fields.length !== 4) {
|
|
340
|
+
return {
|
|
341
|
+
specs: [],
|
|
342
|
+
error: failBatchSpec("task.create-many", `Task specs must use <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}.`, {
|
|
343
|
+
option: "task",
|
|
344
|
+
index,
|
|
345
|
+
rawSpec,
|
|
346
|
+
fields: parsed.fields,
|
|
347
|
+
}),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const tempKey = parsed.fields[0] ?? "";
|
|
352
|
+
const title = parsed.fields[1] ?? "";
|
|
353
|
+
const description = parsed.fields[2] ?? "";
|
|
354
|
+
const status = parsed.fields[3] ?? "";
|
|
355
|
+
if (!tempKey || !isValidCompactTempKey(tempKey)) {
|
|
356
|
+
return {
|
|
357
|
+
specs: [],
|
|
358
|
+
error: failBatchSpec("task.create-many", `Task spec ${index + 1} must start with a temp key like seed-1.`, {
|
|
359
|
+
option: "task",
|
|
360
|
+
index,
|
|
361
|
+
rawSpec,
|
|
362
|
+
tempKey,
|
|
363
|
+
}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (seenTempKeys.has(tempKey)) {
|
|
368
|
+
return {
|
|
369
|
+
specs: [],
|
|
370
|
+
error: failBatchSpec("task.create-many", `Duplicate temp key '${tempKey}' in --task specs.`, {
|
|
371
|
+
option: "task",
|
|
372
|
+
index,
|
|
373
|
+
rawSpec,
|
|
374
|
+
tempKey,
|
|
375
|
+
}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!title || title.trim().length === 0) {
|
|
380
|
+
return {
|
|
381
|
+
specs: [],
|
|
382
|
+
error: failBatchSpec("task.create-many", `Task spec ${index + 1} is missing a title.`, {
|
|
383
|
+
option: "task",
|
|
384
|
+
index,
|
|
385
|
+
rawSpec,
|
|
386
|
+
}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (description.trim().length === 0) {
|
|
391
|
+
return {
|
|
392
|
+
specs: [],
|
|
393
|
+
error: failEmptyCompactField("task.create-many", "task", index, rawSpec, "description"),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
seenTempKeys.add(tempKey);
|
|
398
|
+
const spec: CompactTaskSpec = status.length > 0
|
|
399
|
+
? {
|
|
400
|
+
tempKey,
|
|
401
|
+
title,
|
|
402
|
+
description,
|
|
403
|
+
status,
|
|
404
|
+
}
|
|
405
|
+
: {
|
|
406
|
+
tempKey,
|
|
407
|
+
title,
|
|
408
|
+
description,
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
specs.push(spec);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { specs };
|
|
415
|
+
}
|
|
416
|
+
|
|
427
417
|
export async function runTask(context: CliContext): Promise<CliResult> {
|
|
428
|
-
|
|
418
|
+
let database: TrekoonDatabase | undefined;
|
|
429
419
|
|
|
430
420
|
try {
|
|
421
|
+
database = openTrekoonDatabase(context.cwd);
|
|
431
422
|
const parsed = parseArgs(context.args);
|
|
432
423
|
const subcommand: string | undefined = parsed.positional[0];
|
|
433
424
|
const domain = new TrackerDomain(database.db);
|
|
@@ -460,6 +451,56 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
460
451
|
data: { task },
|
|
461
452
|
});
|
|
462
453
|
}
|
|
454
|
+
case "create-many": {
|
|
455
|
+
const createManyUnknownOption = findUnknownOption(parsed, CREATE_MANY_OPTIONS);
|
|
456
|
+
if (createManyUnknownOption !== undefined) {
|
|
457
|
+
return unknownOption("task.create-many", createManyUnknownOption, CREATE_MANY_OPTIONS);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const missingCreateManyOption = readMissingOptionValue(parsed.missingOptionValues, "epic", "e", "task");
|
|
461
|
+
if (missingCreateManyOption !== undefined) {
|
|
462
|
+
return failMissingOptionValue("task.create-many", missingCreateManyOption);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const unexpectedPositionals = readUnexpectedPositionals(parsed, 1);
|
|
466
|
+
if (unexpectedPositionals.length > 0) {
|
|
467
|
+
return failUnexpectedPositionals("task.create-many", unexpectedPositionals);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const epicId = readOption(parsed.options, "epic", "e");
|
|
471
|
+
if (epicId === undefined || epicId.trim().length === 0) {
|
|
472
|
+
return failBatchSpec("task.create-many", "Provide --epic for task create-many.", {
|
|
473
|
+
option: "epic",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const rawSpecs = readOptions(parsed.optionEntries, "task");
|
|
478
|
+
if (rawSpecs.length === 0) {
|
|
479
|
+
return failBatchSpec("task.create-many", "Provide at least one --task spec.", {
|
|
480
|
+
option: "task",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const specResult = parseTaskCreateManySpecs(rawSpecs);
|
|
485
|
+
if (specResult.error !== undefined) {
|
|
486
|
+
return specResult.error;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const created = mutations.createTaskBatch({
|
|
490
|
+
epicId,
|
|
491
|
+
specs: specResult.specs,
|
|
492
|
+
});
|
|
493
|
+
const result: CompactBatchResultContract = created.result;
|
|
494
|
+
return okResult({
|
|
495
|
+
command: "task.create-many",
|
|
496
|
+
human: `Created ${created.tasks.length} task(s): ${created.tasks.map(formatTask).join("\n")}`,
|
|
497
|
+
data: {
|
|
498
|
+
epicId,
|
|
499
|
+
tasks: created.tasks,
|
|
500
|
+
result,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
463
504
|
case "list": {
|
|
464
505
|
const missingListOption =
|
|
465
506
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
@@ -1071,6 +1112,74 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
1071
1112
|
data: { task },
|
|
1072
1113
|
});
|
|
1073
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
|
+
}
|
|
1074
1183
|
case "delete": {
|
|
1075
1184
|
const taskId: string = parsed.positional[1] ?? "";
|
|
1076
1185
|
mutations.deleteTask(taskId);
|
|
@@ -1084,7 +1193,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
1084
1193
|
default:
|
|
1085
1194
|
return failResult({
|
|
1086
1195
|
command: "task",
|
|
1087
|
-
human: "Usage: trekoon task <create|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>",
|
|
1088
1197
|
data: {
|
|
1089
1198
|
args: context.args,
|
|
1090
1199
|
},
|
|
@@ -1097,6 +1206,6 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
1097
1206
|
} catch (error: unknown) {
|
|
1098
1207
|
return failFromError(error);
|
|
1099
1208
|
} finally {
|
|
1100
|
-
database
|
|
1209
|
+
database?.close();
|
|
1101
1210
|
}
|
|
1102
1211
|
}
|
package/src/commands/wipe.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
33
|
-
: `No
|
|
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
|
});
|