trekoon 0.4.1 → 0.4.3

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.
Files changed (58) hide show
  1. package/.agents/skills/trekoon/SKILL.md +97 -765
  2. package/.agents/skills/trekoon/reference/execution-with-team.md +91 -141
  3. package/.agents/skills/trekoon/reference/execution.md +188 -159
  4. package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
  5. package/.agents/skills/trekoon/reference/planning.md +213 -213
  6. package/.agents/skills/trekoon/reference/status-machine.md +21 -0
  7. package/.agents/skills/trekoon/reference/sync.md +82 -0
  8. package/README.md +29 -8
  9. package/docs/ai-agents.md +65 -6
  10. package/docs/commands.md +149 -5
  11. package/docs/machine-contracts.md +123 -0
  12. package/docs/quickstart.md +55 -3
  13. package/package.json +1 -1
  14. package/src/board/assets/app.js +47 -13
  15. package/src/board/assets/components/Component.js +20 -8
  16. package/src/board/assets/components/Workspace.js +9 -3
  17. package/src/board/assets/components/helpers.js +4 -0
  18. package/src/board/assets/runtime/delegation.js +8 -0
  19. package/src/board/assets/runtime/focus-trap.js +48 -0
  20. package/src/board/assets/state/actions.js +45 -4
  21. package/src/board/assets/state/api.js +304 -17
  22. package/src/board/assets/state/store.js +82 -11
  23. package/src/board/assets/state/url.js +10 -0
  24. package/src/board/assets/state/utils.js +2 -1
  25. package/src/board/event-bus.ts +81 -0
  26. package/src/board/routes.ts +430 -40
  27. package/src/board/server.ts +86 -10
  28. package/src/board/snapshot.ts +6 -0
  29. package/src/board/wal-watcher.ts +313 -0
  30. package/src/commands/board.ts +52 -17
  31. package/src/commands/epic.ts +7 -9
  32. package/src/commands/error-utils.ts +54 -1
  33. package/src/commands/help.ts +75 -10
  34. package/src/commands/migrate.ts +153 -24
  35. package/src/commands/quickstart.ts +7 -0
  36. package/src/commands/skills.ts +17 -5
  37. package/src/commands/subtask.ts +71 -10
  38. package/src/commands/suggest.ts +6 -13
  39. package/src/commands/task.ts +137 -88
  40. package/src/domain/batch-validation.ts +329 -0
  41. package/src/domain/cascade-planner.ts +412 -0
  42. package/src/domain/dependency-rules.ts +15 -0
  43. package/src/domain/mutation-service.ts +842 -187
  44. package/src/domain/search.ts +113 -0
  45. package/src/domain/tracker-domain.ts +167 -693
  46. package/src/domain/types.ts +56 -2
  47. package/src/export/render-markdown.ts +1 -2
  48. package/src/index.ts +37 -0
  49. package/src/runtime/cli-shell.ts +44 -0
  50. package/src/runtime/daemon.ts +700 -0
  51. package/src/storage/backup.ts +166 -0
  52. package/src/storage/database.ts +268 -4
  53. package/src/storage/migrations.ts +441 -22
  54. package/src/storage/path.ts +8 -0
  55. package/src/storage/schema.ts +5 -1
  56. package/src/sync/event-writes.ts +38 -11
  57. package/src/sync/git-context.ts +226 -8
  58. package/src/sync/service.ts +679 -156
@@ -29,8 +29,46 @@ function readErrorMessage(error: unknown): string | null {
29
29
  return null;
30
30
  }
31
31
 
32
+ // Keys whose values must never appear in surfaced error output.
33
+ // Handles formats: key=val, key: val, key="val", 'key':'val', "key":"val",
34
+ // Authorization: Bearer val, Authorization: Basic val.
35
+ const SENSITIVE_KEY_PATTERN =
36
+ /(["']?)(token|secret|password|bearer|authorization|api[_-]?key|client[_-]?secret|private[_-]?key|cookie|session[_-]?id)(["']?\s*[:=]\s*(?:Bearer\s+|Basic\s+)?["']?)([^\s"',;&\]}{)<>]+)/giu;
37
+
38
+ // Tag-style sensitive values: <key>value</key>.
39
+ const SENSITIVE_TAG_PATTERN =
40
+ /(<\s*(token|secret|password|bearer|authorization|api[_-]?key|client[_-]?secret|private[_-]?key|cookie|session[_-]?id)\s*>)([^<]+)/giu;
41
+
42
+ // Standalone "Bearer xyz" / "Basic xyz" anywhere in the message.
43
+ // SENSITIVE_KEY_PATTERN runs first and consumes Authorization: Bearer/Basic forms; this
44
+ // catches bare occurrences that remain (e.g. "got Bearer eyJ..." or "auth: Basic dXNl...").
45
+ const STANDALONE_AUTH_SCHEME_PATTERN = /\b(Bearer|Basic)\s+([A-Za-z0-9._\-+/=]+)/giu;
46
+
47
+ // JWT shape heuristic: three base64url segments separated by dots, each starting
48
+ // with a base64url-encoded JSON header/payload/signature. The first two segments
49
+ // of any JWT begin with "eyJ" because they encode JSON objects (`{"...`).
50
+ // Catches bare JWTs that slip past the keyed and Bearer/Basic patterns above
51
+ // (e.g. raw token pasted into an error message without an "Authorization:" prefix).
52
+ const JWT_PATTERN = /\beyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/gu;
53
+
54
+ export function redactSensitive(input: string): string {
55
+ const keyRedacted = input.replace(
56
+ SENSITIVE_KEY_PATTERN,
57
+ (_match, open, key, sep) => `${open}${key}${sep}REDACTED`,
58
+ );
59
+ const tagRedacted = keyRedacted.replace(
60
+ SENSITIVE_TAG_PATTERN,
61
+ (_match, openTag) => `${openTag}REDACTED`,
62
+ );
63
+ const authRedacted = tagRedacted.replace(
64
+ STANDALONE_AUTH_SCHEME_PATTERN,
65
+ (_match, scheme) => `${scheme} REDACTED`,
66
+ );
67
+ return authRedacted.replace(JWT_PATTERN, "REDACTED");
68
+ }
69
+
32
70
  function sanitizeErrorMessage(message: string): string {
33
- const normalized = message.replace(/\s+/gu, " ").trim();
71
+ const normalized = redactSensitive(message.replace(/\s+/gu, " ").trim());
34
72
  if (normalized.length <= 240) {
35
73
  return normalized;
36
74
  }
@@ -109,3 +147,18 @@ export function safeErrorMessage(error: unknown, fallback: string): string {
109
147
  const message = readErrorMessage(error);
110
148
  return message === null ? fallback : sanitizeErrorMessage(message);
111
149
  }
150
+
151
+ /**
152
+ * Redact a stack trace before logging. Routes the input through
153
+ * `redactSensitive` (the canonical secret-stripping pass) so absolute paths
154
+ * and any inline credentials are scrubbed. The function is intentionally
155
+ * shallow — additional heuristics (e.g. JWT shape detection) live in
156
+ * `redactSensitive` itself so future contributors only need to extend that
157
+ * single regex pipeline.
158
+ */
159
+ export function redactStack(stack: string | undefined): string {
160
+ if (typeof stack !== "string" || stack.length === 0) {
161
+ return "";
162
+ }
163
+ return redactSensitive(stack);
164
+ }
@@ -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 or roll back migrations",
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
 
@@ -106,13 +115,13 @@ const EPIC_HELP = [
106
115
  "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export> [options]",
107
116
  "",
108
117
  "Create:",
109
- " trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
110
- " trekoon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
118
+ " trekoon --toon epic create --title \"...\" --description \"...\" [--status <status>]",
119
+ " trekoon --toon epic create --title \"...\" --description \"...\" [--task <spec>] [--subtask <spec>] [--dep <spec>]",
111
120
  " When the full tree is known, the second form creates everything in one shot",
112
121
  " and returns mappings/counts. Same compact spec grammar as epic expand.",
113
122
  "",
114
123
  "Expand:",
115
- " trekoon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
124
+ " trekoon --toon epic expand <epic-id> [--task <spec>] [--subtask <spec>] [--dep <spec>]",
116
125
  " --task <temp-key>|<title>|<description>|<status>",
117
126
  ` --subtask <parent-ref>|<temp-key>|<title>|<description>|<status> (${"@"}<temp-key> for new parents)`,
118
127
  ` --dep <source-ref>|<depends-on-ref> (refs can be IDs or ${"@"}<temp-key>)`,
@@ -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 = [
@@ -405,8 +441,8 @@ const SUGGEST_HELP = [
405
441
 
406
442
  const SKILLS_HELP = [
407
443
  "Usage:",
408
- " trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
409
- " trekoon skills install -g|--global [--editor opencode|claude|pi]",
444
+ " trekoon skills install [--link --editor opencode|claude|codex|pi] [--to <path>] [--allow-outside-repo]",
445
+ " trekoon skills install -g|--global [--editor opencode|claude|codex|pi]",
410
446
  " trekoon skills update",
411
447
  "",
412
448
  "Installs or refreshes the Trekoon skill so AI agents can plan and execute.",
@@ -415,7 +451,7 @@ const SKILLS_HELP = [
415
451
  " Creates a symlink at .agents/skills/trekoon pointing to the bundled source,",
416
452
  " so the skill always matches the installed CLI version.",
417
453
  " --link Also create an editor symlink named 'trekoon'.",
418
- " --editor <name> Required with --link (opencode|claude|pi).",
454
+ " --editor <name> Required with --link (opencode|claude|codex|pi).",
419
455
  " --to <path> Override the symlink root for --link only.",
420
456
  " --allow-outside-repo Allow links outside the repo (requires --link).",
421
457
  "",
@@ -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
 
@@ -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 missingOption = readMissingOptionValue(parsed.missingOptionValues, "to-version");
44
- if (missingOption !== undefined) {
45
- return failResult({
46
- command: "migrate",
47
- human: `Option --${missingOption} requires a value.`,
48
- data: {
49
- option: missingOption,
50
- },
51
- error: {
52
- code: "invalid_input",
53
- message: `Option --${missingOption} requires a value.`,
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
- const summary = rollbackDatabase(storage.db, targetVersion);
212
+ try {
213
+ const summary = rollbackDatabase(storage.db, targetVersion);
96
214
 
97
- return okResult({
98
- command: "migrate.rollback",
99
- human: [
100
- `Rolled back ${summary.rolledBack} migration(s).`,
101
- `From version ${summary.fromVersion} to ${summary.toVersion}.`,
102
- ].join("\n"),
103
- data: summary,
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",
@@ -10,11 +10,11 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
10
10
 
11
11
  const SKILLS_USAGE = [
12
12
  "Usage:",
13
- " trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
14
- " trekoon skills install -g|--global [--editor opencode|claude|pi]",
13
+ " trekoon skills install [--link --editor opencode|claude|codex|pi] [--to <path>] [--allow-outside-repo]",
14
+ " trekoon skills install -g|--global [--editor opencode|claude|codex|pi]",
15
15
  " trekoon skills update",
16
16
  ].join("\n");
17
- const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
17
+ const EDITOR_NAMES = ["opencode", "claude", "codex", "pi"] as const;
18
18
  const ALLOW_OUTSIDE_REPO_FLAG = "allow-outside-repo";
19
19
 
20
20
  type EditorName = (typeof EDITOR_NAMES)[number];
@@ -88,6 +88,10 @@ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | u
88
88
  return join(cwd, ".claude", "skills");
89
89
  }
90
90
 
91
+ if (editor === "codex") {
92
+ return join(cwd, ".codex", "skills");
93
+ }
94
+
91
95
  return join(cwd, ".pi", "skills");
92
96
  }
93
97
 
@@ -222,6 +226,10 @@ function resolveEditorConfigDir(cwd: string, editor: EditorName): string {
222
226
  return join(cwd, ".claude");
223
227
  }
224
228
 
229
+ if (editor === "codex") {
230
+ return join(cwd, ".codex");
231
+ }
232
+
225
233
  return join(cwd, ".pi");
226
234
  }
227
235
 
@@ -235,6 +243,10 @@ function resolveGlobalEditorSkillsDir(editor: EditorName): string {
235
243
  return join(home, ".claude", "skills");
236
244
  }
237
245
 
246
+ if (editor === "codex") {
247
+ return join(home, ".codex", "skills");
248
+ }
249
+
238
250
  return join(home, ".pi", "skills");
239
251
  }
240
252
 
@@ -542,7 +554,7 @@ function runSkillsInstall(context: CliContext): CliResult {
542
554
 
543
555
  // Validate editor early (shared by both modes).
544
556
  if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
545
- return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
557
+ return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, codex, pi", {
546
558
  editor: rawEditor,
547
559
  allowedEditors: EDITOR_NAMES,
548
560
  });
@@ -588,7 +600,7 @@ function runSkillsInstall(context: CliContext): CliResult {
588
600
  }
589
601
 
590
602
  if (wantsLink && rawEditor === undefined) {
591
- return invalidArgs("skills install --link requires --editor opencode|claude|pi.");
603
+ return invalidArgs("skills install --link requires --editor opencode|claude|codex|pi.");
592
604
  }
593
605
 
594
606
  const editor: EditorName | undefined = rawEditor as EditorName | undefined;