trekoon 0.4.0 → 0.4.2
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 +20 -577
- package/.agents/skills/trekoon/reference/execution-with-team.md +21 -9
- package/.agents/skills/trekoon/reference/execution.md +246 -7
- package/.agents/skills/trekoon/reference/planning.md +138 -1
- package/.agents/skills/trekoon/reference/status-machine.md +21 -0
- package/.agents/skills/trekoon/reference/sync.md +129 -0
- package/README.md +8 -7
- package/docs/ai-agents.md +17 -2
- package/docs/commands.md +147 -3
- package/docs/machine-contracts.md +123 -0
- package/docs/quickstart.md +52 -0
- package/package.json +1 -1
- package/src/board/assets/app.js +49 -16
- package/src/board/assets/components/Component.js +22 -8
- package/src/board/assets/components/Workspace.js +9 -3
- package/src/board/assets/components/helpers.js +5 -1
- package/src/board/assets/runtime/delegation.js +8 -0
- package/src/board/assets/runtime/focus-trap.js +48 -0
- package/src/board/assets/state/actions.js +47 -4
- package/src/board/assets/state/api.js +284 -11
- package/src/board/assets/state/store.js +87 -11
- package/src/board/assets/state/url.js +10 -0
- package/src/board/assets/state/utils.js +2 -1
- package/src/board/event-bus.ts +72 -0
- package/src/board/routes.ts +412 -33
- package/src/board/server.ts +77 -8
- package/src/board/wal-watcher.ts +302 -0
- package/src/commands/board.ts +52 -17
- package/src/commands/epic.ts +7 -9
- package/src/commands/error-utils.ts +54 -1
- package/src/commands/help.ts +69 -4
- package/src/commands/migrate.ts +153 -24
- package/src/commands/quickstart.ts +7 -0
- package/src/commands/subtask.ts +71 -10
- package/src/commands/suggest.ts +6 -13
- package/src/commands/task.ts +137 -88
- package/src/domain/batch-validation.ts +329 -0
- package/src/domain/cascade-planner.ts +412 -0
- package/src/domain/dependency-rules.ts +15 -0
- package/src/domain/mutation-service.ts +828 -192
- package/src/domain/search.ts +113 -0
- package/src/domain/tracker-domain.ts +150 -680
- package/src/domain/types.ts +53 -2
- package/src/index.ts +37 -0
- package/src/runtime/cli-shell.ts +44 -0
- package/src/runtime/daemon.ts +639 -0
- package/src/storage/backup.ts +166 -0
- package/src/storage/database.ts +261 -4
- package/src/storage/migrations.ts +422 -20
- package/src/storage/path.ts +8 -0
- package/src/storage/schema.ts +5 -1
- package/src/sync/event-writes.ts +38 -11
- package/src/sync/git-context.ts +226 -8
- package/src/sync/service.ts +650 -147
package/src/commands/help.ts
CHANGED
|
@@ -13,6 +13,7 @@ const ROOT_HELP = [
|
|
|
13
13
|
" --json Structured JSON output",
|
|
14
14
|
" --toon TOON-encoded output (preferred for agents)",
|
|
15
15
|
" --compat <mode> Machine compatibility mode",
|
|
16
|
+
" --daemon (experimental) Route the call through trekoon serve over a Unix socket",
|
|
16
17
|
" --help Show help for root or a command",
|
|
17
18
|
" --version Print CLI version",
|
|
18
19
|
"",
|
|
@@ -27,12 +28,13 @@ const ROOT_HELP = [
|
|
|
27
28
|
" subtask Create, list, update, and search subtasks",
|
|
28
29
|
" dep Manage dependency edges between tasks and subtasks",
|
|
29
30
|
" events Prune old sync event log rows",
|
|
30
|
-
" migrate Check schema version
|
|
31
|
+
" migrate Check schema version, roll back migrations, or snapshot a backup",
|
|
31
32
|
" session Agent orientation (diagnostics + sync + next task)",
|
|
32
33
|
" suggest Priority-ranked next-action suggestions",
|
|
33
34
|
" sync Pull events and resolve conflicts across branches",
|
|
34
35
|
" skills Install, link, or update the Trekoon skill",
|
|
35
36
|
" update Alias for skills update",
|
|
37
|
+
" serve (experimental) Run a long-lived daemon over a Unix socket",
|
|
36
38
|
].join("\n");
|
|
37
39
|
|
|
38
40
|
const INIT_HELP = [
|
|
@@ -94,11 +96,18 @@ const BOARD_HELP = [
|
|
|
94
96
|
" and open the browser. Returns the board URL and a fallback URL.",
|
|
95
97
|
" update Refresh board runtime assets only. No server, no browser.",
|
|
96
98
|
"",
|
|
99
|
+
"Token visibility:",
|
|
100
|
+
" By default the board token is redacted from machine output (shown as ****).",
|
|
101
|
+
" Pass --reveal-token to print the raw token value.",
|
|
102
|
+
" trekoon board open --reveal-token",
|
|
103
|
+
" Treat the token like a password: it grants full board access over loopback.",
|
|
104
|
+
"",
|
|
97
105
|
"Environment:",
|
|
98
106
|
" TREKOON_BOARD_ASSET_ROOT Override the bundled asset source (tests/dev only).",
|
|
99
107
|
"",
|
|
100
108
|
"Examples:",
|
|
101
109
|
" trekoon board open",
|
|
110
|
+
" trekoon board open --reveal-token",
|
|
102
111
|
" trekoon --json board update",
|
|
103
112
|
].join("\n");
|
|
104
113
|
|
|
@@ -170,7 +179,7 @@ const EPIC_HELP = [
|
|
|
170
179
|
].join("\n");
|
|
171
180
|
|
|
172
181
|
const TASK_HELP = [
|
|
173
|
-
"Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete> [options]",
|
|
182
|
+
"Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete|claim> [options]",
|
|
174
183
|
"",
|
|
175
184
|
"Create-many:",
|
|
176
185
|
" trekoon task create-many --epic <epic-id> --task <spec> [--task <spec> ...]",
|
|
@@ -216,10 +225,18 @@ const TASK_HELP = [
|
|
|
216
225
|
" Cascades atomically through descendant subtasks.",
|
|
217
226
|
" Blocked descendants abort the whole update. Only --status done|todo is supported.",
|
|
218
227
|
" Don't combine positional ID + --all with --ids, --append, --description, or --title.",
|
|
228
|
+
"",
|
|
229
|
+
"Claim:",
|
|
230
|
+
" trekoon task claim <task-id> --owner <owner>",
|
|
231
|
+
" Atomically claim a task using SQL compare-and-swap.",
|
|
232
|
+
" Sets status=in_progress and owner=<owner> only when the task is todo or blocked",
|
|
233
|
+
" and the owner field is NULL or already set to <owner>.",
|
|
234
|
+
" Returns claimed (true|false), currentOwner, currentStatus, and the full task record on success.",
|
|
235
|
+
" Two concurrent claim calls return exactly one claimed=true.",
|
|
219
236
|
].join("\n");
|
|
220
237
|
|
|
221
238
|
const SUBTASK_HELP = [
|
|
222
|
-
"Usage: trekoon subtask <create|create-many|list|search|replace|update|delete> [options]",
|
|
239
|
+
"Usage: trekoon subtask <create|create-many|list|search|replace|update|delete|claim> [options]",
|
|
223
240
|
"",
|
|
224
241
|
"Create-many:",
|
|
225
242
|
" trekoon subtask create-many [<task-id>] [--task <task-id>] --subtask <spec> [--subtask <spec> ...]",
|
|
@@ -250,6 +267,14 @@ const SUBTASK_HELP = [
|
|
|
250
267
|
" trekoon subtask update <subtask-id> --all --status done|todo",
|
|
251
268
|
" Accepted for consistency, but just updates the one subtask (no descendants).",
|
|
252
269
|
" Don't combine positional ID + --all with --ids, --append, --description, or --title.",
|
|
270
|
+
"",
|
|
271
|
+
"Claim:",
|
|
272
|
+
" trekoon subtask claim <subtask-id> --owner <owner>",
|
|
273
|
+
" Atomically claim a subtask using SQL compare-and-swap.",
|
|
274
|
+
" Sets status=in_progress and owner=<owner> only when the subtask is todo or blocked",
|
|
275
|
+
" and the owner field is NULL or already set to <owner>.",
|
|
276
|
+
" Returns claimed (true|false), currentOwner, currentStatus, and the full subtask record on success.",
|
|
277
|
+
" Two concurrent claim calls return exactly one claimed=true.",
|
|
253
278
|
].join("\n");
|
|
254
279
|
|
|
255
280
|
const DEP_HELP = [
|
|
@@ -292,16 +317,27 @@ const EVENTS_HELP = [
|
|
|
292
317
|
].join("\n");
|
|
293
318
|
|
|
294
319
|
const MIGRATE_HELP = [
|
|
295
|
-
"Usage: trekoon migrate <status|rollback> [--to-version <n>]",
|
|
320
|
+
"Usage: trekoon migrate <status|rollback|backup> [--to-version <n>] [--retain <n>]",
|
|
296
321
|
"",
|
|
297
322
|
"Subcommands:",
|
|
298
323
|
" status Show current schema version, latest version, and pending count.",
|
|
299
324
|
" rollback [--to-version <n>] Roll back migrations. Defaults to one version back.",
|
|
325
|
+
" backup [--retain <n>] Snapshot .trekoon/trekoon.db to a timestamped sibling file",
|
|
326
|
+
" before any manual migration recovery.",
|
|
327
|
+
" --retain keeps the last n timestamped backups",
|
|
328
|
+
" (default 10); older siblings are pruned.",
|
|
329
|
+
"",
|
|
330
|
+
"Notes:",
|
|
331
|
+
" Migrations 0004, 0005, and 0006 are irreversible (ALTER TABLE / data cleanup).",
|
|
332
|
+
" Rolling back below those versions errors with code migration_down_unsupported.",
|
|
333
|
+
" Take a backup first; restore by copying the backup over .trekoon/trekoon.db.",
|
|
300
334
|
"",
|
|
301
335
|
"Examples:",
|
|
302
336
|
" trekoon migrate status",
|
|
303
337
|
" trekoon migrate rollback",
|
|
304
338
|
" trekoon migrate rollback --to-version 1",
|
|
339
|
+
" trekoon migrate backup",
|
|
340
|
+
" trekoon migrate backup --retain 5",
|
|
305
341
|
].join("\n");
|
|
306
342
|
|
|
307
343
|
const SYNC_HELP = [
|
|
@@ -441,6 +477,34 @@ const SKILLS_HELP = [
|
|
|
441
477
|
" trekoon update",
|
|
442
478
|
].join("\n");
|
|
443
479
|
|
|
480
|
+
const SERVE_HELP = [
|
|
481
|
+
"Usage: trekoon serve [--json|--toon]",
|
|
482
|
+
"",
|
|
483
|
+
"Status: EXPERIMENTAL spike. Not on by default. The default one-shot CLI",
|
|
484
|
+
"behavior is unchanged when this command is not running.",
|
|
485
|
+
"",
|
|
486
|
+
"Starts a foreground Trekoon daemon on a Unix-domain socket inside the",
|
|
487
|
+
"shared .trekoon directory. The daemon holds the SQLite connection and the",
|
|
488
|
+
"CLI shell in memory, so subsequent invocations skip Bun startup, module",
|
|
489
|
+
"load, and database open.",
|
|
490
|
+
"",
|
|
491
|
+
"Activate the client side with one of:",
|
|
492
|
+
" TREKOON_DAEMON=1 trekoon session",
|
|
493
|
+
" trekoon --daemon session",
|
|
494
|
+
"If the socket is missing or unreachable, the client transparently falls",
|
|
495
|
+
"back to the in-process one-shot path.",
|
|
496
|
+
"",
|
|
497
|
+
"Security:",
|
|
498
|
+
" - Socket file mode is 0o600.",
|
|
499
|
+
" - Parent .trekoon directory is forced to 0o700.",
|
|
500
|
+
" - Stale sockets from prior crashes are cleaned up on start.",
|
|
501
|
+
" - On Ctrl-C / SIGTERM the socket is unlinked.",
|
|
502
|
+
"",
|
|
503
|
+
"Examples:",
|
|
504
|
+
" trekoon serve",
|
|
505
|
+
" trekoon --daemon session",
|
|
506
|
+
].join("\n");
|
|
507
|
+
|
|
444
508
|
const COMMAND_HELP: Record<string, string> = {
|
|
445
509
|
init: INIT_HELP,
|
|
446
510
|
board: BOARD_HELP,
|
|
@@ -457,6 +521,7 @@ const COMMAND_HELP: Record<string, string> = {
|
|
|
457
521
|
suggest: SUGGEST_HELP,
|
|
458
522
|
skills: SKILLS_HELP,
|
|
459
523
|
update: "Usage: trekoon update [--json|--toon]\n\nAlias for: trekoon skills update\n\nProbes and repairs all installed global and local skill symlinks.",
|
|
524
|
+
serve: SERVE_HELP,
|
|
460
525
|
help: "Usage: trekoon help [command] [--json|--toon]",
|
|
461
526
|
};
|
|
462
527
|
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
|
|
1
|
+
import { findUnknownOption, parseArgs, readMissingOptionValue, readOption } 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";
|
|
7
|
+
import { createMigrationBackup, DEFAULT_BACKUP_RETENTION } from "../storage/backup";
|
|
6
8
|
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
7
9
|
import { describeMigrations, rollbackDatabase } from "../storage/migrations";
|
|
8
10
|
|
|
9
|
-
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback> [--to-version <n>]";
|
|
11
|
+
const MIGRATE_USAGE = "Usage: trekoon migrate <status|rollback|backup> [--to-version <n>] [--retain <n>]";
|
|
12
|
+
|
|
13
|
+
const STATUS_OPTIONS: readonly string[] = [];
|
|
14
|
+
const ROLLBACK_OPTIONS: readonly string[] = ["to-version"];
|
|
15
|
+
const BACKUP_OPTIONS: readonly string[] = ["retain"];
|
|
10
16
|
|
|
11
17
|
function usage(message: string): CliResult {
|
|
12
18
|
return failResult({
|
|
@@ -20,6 +26,19 @@ function usage(message: string): CliResult {
|
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function unknownOptionResult(command: string, option: string): CliResult {
|
|
30
|
+
const message = `Unknown option --${option}.`;
|
|
31
|
+
return failResult({
|
|
32
|
+
command,
|
|
33
|
+
human: message,
|
|
34
|
+
data: { option: `--${option}` },
|
|
35
|
+
error: {
|
|
36
|
+
code: "unknown_option",
|
|
37
|
+
message,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
function parseVersion(rawValue: string | undefined): number | null {
|
|
24
43
|
if (rawValue === undefined) {
|
|
25
44
|
return null;
|
|
@@ -40,19 +59,107 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
40
59
|
return usage("Missing migrate subcommand.");
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
for (const optionName of ["to-version", "retain"] as const) {
|
|
63
|
+
const missingOption = readMissingOptionValue(parsed.missingOptionValues, optionName);
|
|
64
|
+
if (missingOption !== undefined) {
|
|
65
|
+
return failResult({
|
|
66
|
+
command: "migrate",
|
|
67
|
+
human: `Option --${missingOption} requires a value.`,
|
|
68
|
+
data: {
|
|
69
|
+
option: missingOption,
|
|
70
|
+
},
|
|
71
|
+
error: {
|
|
72
|
+
code: "invalid_input",
|
|
73
|
+
message: `Option --${missingOption} requires a value.`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The backup subcommand never opens the live DB through the standard
|
|
80
|
+
// openTrekoonDatabase pathway: backups must work even when the DB is
|
|
81
|
+
// in a partially-migrated or otherwise diagnostic-blocked state.
|
|
82
|
+
if (subcommand === "backup") {
|
|
83
|
+
const unknown = findUnknownOption(parsed, BACKUP_OPTIONS);
|
|
84
|
+
if (unknown !== undefined) {
|
|
85
|
+
return unknownOptionResult("migrate.backup", unknown);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const retainRaw: string | undefined = readOption(parsed.options, "retain");
|
|
89
|
+
let retain: number = DEFAULT_BACKUP_RETENTION;
|
|
90
|
+
if (retainRaw !== undefined) {
|
|
91
|
+
if (!/^\d+$/.test(retainRaw)) {
|
|
92
|
+
return failResult({
|
|
93
|
+
command: "migrate.backup",
|
|
94
|
+
human: "--retain must be a positive integer.",
|
|
95
|
+
data: { option: "retain" },
|
|
96
|
+
error: {
|
|
97
|
+
code: "invalid_input",
|
|
98
|
+
message: "--retain must be a positive integer.",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parsedRetain: number = Number.parseInt(retainRaw, 10);
|
|
104
|
+
if (parsedRetain < 1) {
|
|
105
|
+
return failResult({
|
|
106
|
+
command: "migrate.backup",
|
|
107
|
+
human: "--retain must be at least 1 (the new backup itself).",
|
|
108
|
+
data: { option: "retain" },
|
|
109
|
+
error: {
|
|
110
|
+
code: "invalid_input",
|
|
111
|
+
message: "--retain must be at least 1 (the new backup itself).",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
retain = parsedRetain;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = createMigrationBackup({ cwd: context.cwd, retain });
|
|
121
|
+
return okResult({
|
|
122
|
+
command: "migrate.backup",
|
|
123
|
+
human: [
|
|
124
|
+
`Backed up Trekoon database to ${result.backupPath}`,
|
|
125
|
+
`Bytes: ${result.bytes}`,
|
|
126
|
+
`Schema version at backup: ${result.migrationVersion} of ${result.latestVersion}`,
|
|
127
|
+
`Retained backups: ${result.retainedCount} (pruned ${result.prunedPaths.length})`,
|
|
128
|
+
].join("\n"),
|
|
129
|
+
data: {
|
|
130
|
+
backupPath: result.backupPath,
|
|
131
|
+
bytes: result.bytes,
|
|
132
|
+
migrationVersion: result.migrationVersion,
|
|
133
|
+
latestVersion: result.latestVersion,
|
|
134
|
+
timestamp: result.timestamp,
|
|
135
|
+
retain,
|
|
136
|
+
retainedCount: result.retainedCount,
|
|
137
|
+
prunedPaths: result.prunedPaths,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch (error: unknown) {
|
|
141
|
+
if (error instanceof DomainError) {
|
|
142
|
+
return failResult({
|
|
143
|
+
command: "migrate.backup",
|
|
144
|
+
human: error.message,
|
|
145
|
+
data: { code: error.code, ...(error.details ?? {}) },
|
|
146
|
+
error: { code: error.code, message: error.message },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const busyFailure = sqliteBusyFailure("migrate.backup", error);
|
|
151
|
+
if (busyFailure !== null) {
|
|
152
|
+
return busyFailure;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const message = safeErrorMessage(error, "Unknown backup failure.");
|
|
156
|
+
return failResult({
|
|
157
|
+
command: "migrate.backup",
|
|
158
|
+
human: message,
|
|
159
|
+
data: { reason: "backup_failed" },
|
|
160
|
+
error: { code: "backup_failed", message },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
56
163
|
}
|
|
57
164
|
|
|
58
165
|
let storage: TrekoonDatabase | undefined;
|
|
@@ -60,6 +167,11 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
60
167
|
try {
|
|
61
168
|
storage = openTrekoonDatabase(context.cwd, { autoMigrate: false });
|
|
62
169
|
if (subcommand === "status") {
|
|
170
|
+
const unknown = findUnknownOption(parsed, STATUS_OPTIONS);
|
|
171
|
+
if (unknown !== undefined) {
|
|
172
|
+
return unknownOptionResult("migrate.status", unknown);
|
|
173
|
+
}
|
|
174
|
+
|
|
63
175
|
const status = describeMigrations(storage.db);
|
|
64
176
|
|
|
65
177
|
return okResult({
|
|
@@ -74,6 +186,11 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
74
186
|
}
|
|
75
187
|
|
|
76
188
|
if (subcommand === "rollback") {
|
|
189
|
+
const unknown = findUnknownOption(parsed, ROLLBACK_OPTIONS);
|
|
190
|
+
if (unknown !== undefined) {
|
|
191
|
+
return unknownOptionResult("migrate.rollback", unknown);
|
|
192
|
+
}
|
|
193
|
+
|
|
77
194
|
const status = describeMigrations(storage.db);
|
|
78
195
|
const parsedVersion: number | null = parseVersion(readOption(parsed.options, "to-version"));
|
|
79
196
|
|
|
@@ -92,16 +209,28 @@ export async function runMigrate(context: CliContext): Promise<CliResult> {
|
|
|
92
209
|
}
|
|
93
210
|
|
|
94
211
|
const targetVersion: number = parsedVersion ?? Math.max(0, status.currentVersion - 1);
|
|
95
|
-
|
|
212
|
+
try {
|
|
213
|
+
const summary = rollbackDatabase(storage.db, targetVersion);
|
|
96
214
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
215
|
+
return okResult({
|
|
216
|
+
command: "migrate.rollback",
|
|
217
|
+
human: [
|
|
218
|
+
`Rolled back ${summary.rolledBack} migration(s).`,
|
|
219
|
+
`From version ${summary.fromVersion} to ${summary.toVersion}.`,
|
|
220
|
+
].join("\n"),
|
|
221
|
+
data: summary,
|
|
222
|
+
});
|
|
223
|
+
} catch (error: unknown) {
|
|
224
|
+
if (error instanceof DomainError) {
|
|
225
|
+
return failResult({
|
|
226
|
+
command: "migrate.rollback",
|
|
227
|
+
human: error.message,
|
|
228
|
+
data: { code: error.code, ...(error.details ?? {}) },
|
|
229
|
+
error: { code: error.code, message: error.message },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
105
234
|
}
|
|
106
235
|
|
|
107
236
|
return usage(`Unknown migrate subcommand '${subcommand}'.`);
|
|
@@ -57,9 +57,11 @@ const QUICKSTART_TEXT = [
|
|
|
57
57
|
" Filtered list: trekoon --toon task list --status in_progress,todo --limit 20",
|
|
58
58
|
" Paginate: trekoon --toon task list --cursor <n>",
|
|
59
59
|
" Bulk update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
60
|
+
" Atomic claim: trekoon --toon task claim <task-id> --owner <owner>",
|
|
60
61
|
" Ready queue: trekoon --toon task ready [--limit <n>] [--epic <id>]",
|
|
61
62
|
" Next candidate: trekoon --toon task next [--epic <id>]",
|
|
62
63
|
" Export epic to MD: trekoon --toon epic export <epic-id> [--path <path>] [--overwrite]",
|
|
64
|
+
" Snapshot the DB: trekoon --toon migrate backup",
|
|
63
65
|
"",
|
|
64
66
|
"6) List and view defaults",
|
|
65
67
|
" Default scope: open work (in_progress, todo), limit 10.",
|
|
@@ -153,7 +155,9 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
153
155
|
"trekoon --toon task list --status in_progress,todo --limit 20",
|
|
154
156
|
"trekoon --toon task list --cursor <n>",
|
|
155
157
|
"trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
158
|
+
"trekoon --toon task claim <task-id> --owner <owner>",
|
|
156
159
|
"trekoon --toon epic export <epic-id>",
|
|
160
|
+
"trekoon --toon migrate backup",
|
|
157
161
|
],
|
|
158
162
|
machineExamples: [
|
|
159
163
|
"trekoon --toon quickstart",
|
|
@@ -162,6 +166,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
162
166
|
"trekoon --toon suggest",
|
|
163
167
|
"trekoon --toon epic progress <epic-id>",
|
|
164
168
|
"trekoon --toon task done <task-id>",
|
|
169
|
+
"trekoon --toon task claim <task-id> --owner <owner>",
|
|
165
170
|
"trekoon --toon task show <task-id> --all",
|
|
166
171
|
"trekoon --toon epic show <epic-id> --all",
|
|
167
172
|
"trekoon --toon sync status",
|
|
@@ -169,6 +174,8 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
169
174
|
"trekoon --toon task next",
|
|
170
175
|
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
171
176
|
"trekoon --toon epic export <epic-id>",
|
|
177
|
+
"trekoon --toon migrate status",
|
|
178
|
+
"trekoon --toon migrate backup",
|
|
172
179
|
],
|
|
173
180
|
wipeWarning: {
|
|
174
181
|
command: "trekoon wipe --yes",
|
package/src/commands/subtask.ts
CHANGED
|
@@ -39,6 +39,7 @@ const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
|
39
39
|
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
40
40
|
const CREATE_MANY_OPTIONS = ["task", "t", "subtask"] as const;
|
|
41
41
|
const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "owner"] as const;
|
|
42
|
+
const CLAIM_OPTIONS = ["owner"] as const;
|
|
42
43
|
const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
|
|
43
44
|
|
|
44
45
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
@@ -892,10 +893,9 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
892
893
|
|
|
893
894
|
const targets = updateAll ? [...domain.listSubtasks()] : ids.map((id) => domain.getSubtaskOrThrow(id));
|
|
894
895
|
const subtasks = targets.map((target) =>
|
|
895
|
-
|
|
896
|
-
status
|
|
897
|
-
|
|
898
|
-
}),
|
|
896
|
+
append !== undefined
|
|
897
|
+
? mutations.appendToSubtaskDescription({ subtaskId: target.id, append, status })
|
|
898
|
+
: mutations.updateSubtask(target.id, { status }),
|
|
899
899
|
);
|
|
900
900
|
|
|
901
901
|
return okResult({
|
|
@@ -921,11 +921,10 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
921
921
|
});
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
-
const
|
|
925
|
-
append
|
|
926
|
-
?
|
|
927
|
-
:
|
|
928
|
-
const subtask = mutations.updateSubtask(subtaskId, { title, description: nextDescription, status, owner });
|
|
924
|
+
const subtask =
|
|
925
|
+
append !== undefined
|
|
926
|
+
? mutations.appendToSubtaskDescription({ subtaskId, append, status, owner })
|
|
927
|
+
: mutations.updateSubtask(subtaskId, { title, description, status, owner });
|
|
929
928
|
|
|
930
929
|
return okResult({
|
|
931
930
|
command: "subtask.update",
|
|
@@ -933,6 +932,68 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
933
932
|
data: { subtask },
|
|
934
933
|
});
|
|
935
934
|
}
|
|
935
|
+
case "claim": {
|
|
936
|
+
const claimUnknownOption = findUnknownOption(parsed, CLAIM_OPTIONS);
|
|
937
|
+
if (claimUnknownOption !== undefined) {
|
|
938
|
+
return unknownOption("subtask.claim", claimUnknownOption, CLAIM_OPTIONS);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const missingClaimOption = readMissingOptionValue(parsed.missingOptionValues, "owner");
|
|
942
|
+
if (missingClaimOption !== undefined) {
|
|
943
|
+
return failMissingOptionValue("subtask.claim", missingClaimOption);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const subtaskId: string = parsed.positional[1] ?? "";
|
|
947
|
+
if (subtaskId.length === 0) {
|
|
948
|
+
return failResult({
|
|
949
|
+
command: "subtask.claim",
|
|
950
|
+
human: "Provide a subtask id. Usage: trekoon subtask claim <id> --owner <owner>",
|
|
951
|
+
data: { code: "invalid_input" },
|
|
952
|
+
error: {
|
|
953
|
+
code: "invalid_input",
|
|
954
|
+
message: "Missing subtask id",
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const owner: string | undefined = readOption(parsed.options, "owner");
|
|
960
|
+
if (owner === undefined || owner.trim().length === 0) {
|
|
961
|
+
return failResult({
|
|
962
|
+
command: "subtask.claim",
|
|
963
|
+
human: "--owner is required. Usage: trekoon subtask claim <id> --owner <owner>",
|
|
964
|
+
data: { code: "invalid_input", option: "owner" },
|
|
965
|
+
error: {
|
|
966
|
+
code: "invalid_input",
|
|
967
|
+
message: "Missing required option --owner",
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const claimResult = mutations.claimSubtask({ subtaskId, owner });
|
|
973
|
+
|
|
974
|
+
if (claimResult.claimed) {
|
|
975
|
+
return okResult({
|
|
976
|
+
command: "subtask.claim",
|
|
977
|
+
human: `Claimed subtask ${subtaskId} for ${owner}`,
|
|
978
|
+
data: {
|
|
979
|
+
claimed: true,
|
|
980
|
+
currentOwner: claimResult.currentOwner,
|
|
981
|
+
currentStatus: claimResult.currentStatus,
|
|
982
|
+
subtask: claimResult.subtask,
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return okResult({
|
|
988
|
+
command: "subtask.claim",
|
|
989
|
+
human: `Subtask ${subtaskId} not claimed: status=${claimResult.currentStatus}, owner=${claimResult.currentOwner ?? "none"}`,
|
|
990
|
+
data: {
|
|
991
|
+
claimed: false,
|
|
992
|
+
currentOwner: claimResult.currentOwner,
|
|
993
|
+
currentStatus: claimResult.currentStatus,
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
}
|
|
936
997
|
case "delete": {
|
|
937
998
|
const subtaskId: string = parsed.positional[1] ?? "";
|
|
938
999
|
const result = mutations.deleteSubtask(subtaskId);
|
|
@@ -946,7 +1007,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
946
1007
|
default:
|
|
947
1008
|
return failResult({
|
|
948
1009
|
command: "subtask",
|
|
949
|
-
human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete>",
|
|
1010
|
+
human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete|claim>",
|
|
950
1011
|
data: {
|
|
951
1012
|
args: context.args,
|
|
952
1013
|
},
|
package/src/commands/suggest.ts
CHANGED
|
@@ -44,14 +44,7 @@ function resolveActiveEpic(domain: TrackerDomain, epicId: string | undefined): E
|
|
|
44
44
|
return domain.getEpic(epicId);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const inProgress = epics.find((epic) => epic.status === "in_progress");
|
|
49
|
-
if (inProgress) {
|
|
50
|
-
return inProgress;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const todo = epics.find((epic) => epic.status === "todo");
|
|
54
|
-
return todo ?? epics[0] ?? null;
|
|
47
|
+
return domain.findActiveEpic();
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
function findInProgressTasks(readiness: TaskReadinessResult): { count: number; first: { id: string; title: string } | null } {
|
|
@@ -76,7 +69,7 @@ function buildSuggestions(
|
|
|
76
69
|
recoveryRequired: boolean,
|
|
77
70
|
syncSummary: SyncStatusSummary,
|
|
78
71
|
readiness: TaskReadinessResult,
|
|
79
|
-
|
|
72
|
+
epicCount: number,
|
|
80
73
|
activeEpic: EpicRecord | null,
|
|
81
74
|
): readonly Suggestion[] {
|
|
82
75
|
const suggestions: Suggestion[] = [];
|
|
@@ -186,7 +179,7 @@ function buildSuggestions(
|
|
|
186
179
|
}
|
|
187
180
|
|
|
188
181
|
// Priority 8: No epics exist
|
|
189
|
-
if (suggestions.length < MAX_SUGGESTIONS &&
|
|
182
|
+
if (suggestions.length < MAX_SUGGESTIONS && epicCount === 0) {
|
|
190
183
|
suggestions.push({
|
|
191
184
|
priority: suggestions.length + 1,
|
|
192
185
|
action: "quickstart",
|
|
@@ -241,7 +234,7 @@ export async function runSuggest(context: CliContext): Promise<CliResult> {
|
|
|
241
234
|
|
|
242
235
|
const syncSummary = resolveSyncStatus(database, context.cwd, DEFAULT_SOURCE_BRANCH);
|
|
243
236
|
const domain = new TrackerDomain(database.db);
|
|
244
|
-
const
|
|
237
|
+
const epicCount = domain.countEpics();
|
|
245
238
|
const activeEpic = resolveActiveEpic(domain, epicId);
|
|
246
239
|
|
|
247
240
|
const readiness = buildTaskReadiness(domain, epicId ?? activeEpic?.id);
|
|
@@ -250,14 +243,14 @@ export async function runSuggest(context: CliContext): Promise<CliResult> {
|
|
|
250
243
|
diagnostics.recoveryRequired,
|
|
251
244
|
syncSummary,
|
|
252
245
|
readiness,
|
|
253
|
-
|
|
246
|
+
epicCount,
|
|
254
247
|
activeEpic,
|
|
255
248
|
);
|
|
256
249
|
|
|
257
250
|
const result: SuggestResult = {
|
|
258
251
|
suggestions,
|
|
259
252
|
context: {
|
|
260
|
-
totalEpics:
|
|
253
|
+
totalEpics: epicCount,
|
|
261
254
|
activeEpic: activeEpic?.id ?? null,
|
|
262
255
|
readyTasks: readiness.summary.readyCount,
|
|
263
256
|
blockedTasks: readiness.summary.blockedCount,
|