trekoon 0.1.7 → 0.1.9

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,4 +1,12 @@
1
- import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
1
+ import {
2
+ hasFlag,
3
+ parseArgs,
4
+ parseStrictNonNegativeInt,
5
+ parseStrictPositiveInt,
6
+ readEnumOption,
7
+ readMissingOptionValue,
8
+ readOption,
9
+ } from "./arg-parser";
2
10
 
3
11
  import { MutationService } from "../domain/mutation-service";
4
12
  import { TrackerDomain } from "../domain/tracker-domain";
@@ -41,21 +49,58 @@ function getStatusPriority(status: string): number {
41
49
  }
42
50
 
43
51
  function sortByStatusPriority(epics: readonly EpicRecord[]): EpicRecord[] {
44
- return [...epics].sort((left, right) => getStatusPriority(left.status) - getStatusPriority(right.status));
52
+ return [...epics].sort((left, right) => {
53
+ const byStatus = getStatusPriority(left.status) - getStatusPriority(right.status);
54
+ if (byStatus !== 0) {
55
+ return byStatus;
56
+ }
57
+
58
+ const byCreatedAt = left.createdAt - right.createdAt;
59
+ if (byCreatedAt !== 0) {
60
+ return byCreatedAt;
61
+ }
62
+
63
+ return left.id.localeCompare(right.id);
64
+ });
45
65
  }
46
66
 
47
- function filterSortAndLimitEpics(epics: readonly EpicRecord[], options: { includeAll: boolean; statuses: readonly string[] | undefined; limit: number | undefined }): EpicRecord[] {
48
- const { includeAll, statuses, limit } = options;
67
+ interface PaginationMeta {
68
+ readonly hasMore: boolean;
69
+ readonly nextCursor: string | null;
70
+ }
71
+
72
+ function filterSortAndLimitEpics(epics: readonly EpicRecord[], options: {
73
+ includeAll: boolean;
74
+ statuses: readonly string[] | undefined;
75
+ limit: number | undefined;
76
+ cursor: number;
77
+ }): { epics: EpicRecord[]; pagination: PaginationMeta } {
78
+ const { includeAll, statuses, limit, cursor } = options;
49
79
  const selectedStatuses = includeAll ? undefined : (statuses ?? DEFAULT_OPEN_STATUSES);
50
80
  const selectedEpics = selectedStatuses === undefined ? [...epics] : epics.filter((epic) => selectedStatuses.includes(epic.status));
51
81
  const sortedEpics = sortByStatusPriority(selectedEpics);
52
82
 
53
83
  if (includeAll) {
54
- return sortedEpics;
84
+ return {
85
+ epics: sortedEpics,
86
+ pagination: {
87
+ hasMore: false,
88
+ nextCursor: null,
89
+ },
90
+ };
55
91
  }
56
92
 
57
93
  const effectiveLimit = limit ?? DEFAULT_LIST_LIMIT;
58
- return sortedEpics.slice(0, effectiveLimit);
94
+ const pagedEpics = sortedEpics.slice(cursor, cursor + effectiveLimit);
95
+ const nextIndex = cursor + pagedEpics.length;
96
+ const hasMore = nextIndex < sortedEpics.length;
97
+ return {
98
+ epics: pagedEpics,
99
+ pagination: {
100
+ hasMore,
101
+ nextCursor: hasMore ? `${nextIndex}` : null,
102
+ },
103
+ };
59
104
  }
60
105
 
61
106
  function invalidEpicListInput(human: string, message: string, data: Record<string, unknown>): CliResult {
@@ -266,6 +311,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
266
311
  const missingListOption =
267
312
  readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
268
313
  readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
314
+ readMissingOptionValue(parsed.missingOptionValues, "cursor") ??
269
315
  readMissingOptionValue(parsed.missingOptionValues, "view");
270
316
  if (missingListOption !== undefined) {
271
317
  return failMissingOptionValue("epic.list", missingListOption);
@@ -274,6 +320,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
274
320
  const includeAll: boolean = hasFlag(parsed.flags, "all");
275
321
  const rawStatuses: string | undefined = readOption(parsed.options, "status");
276
322
  const rawLimit: string | undefined = readOption(parsed.options, "limit");
323
+ const rawCursor: string | undefined = readOption(parsed.options, "cursor");
277
324
  const rawView: string | undefined = readOption(parsed.options, "view");
278
325
  const view = readEnumOption(parsed.options, VIEW_MODES, "view");
279
326
  if (rawView !== undefined && view === undefined) {
@@ -320,11 +367,28 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
320
367
  });
321
368
  }
322
369
 
323
- const epics = filterSortAndLimitEpics(domain.listEpics(), {
370
+ const cursor = parseStrictNonNegativeInt(rawCursor) ?? 0;
371
+ if (Number.isNaN(cursor)) {
372
+ return invalidEpicListInput("Invalid --cursor value. Use an integer >= 0.", "Invalid --cursor value", {
373
+ code: "invalid_input",
374
+ cursor: rawCursor,
375
+ });
376
+ }
377
+
378
+ if (includeAll && rawCursor !== undefined) {
379
+ return invalidEpicListInput("Use either --all or --cursor, not both.", "--all and --cursor are mutually exclusive", {
380
+ code: "invalid_input",
381
+ flags: ["all", "cursor"],
382
+ });
383
+ }
384
+
385
+ const listed = filterSortAndLimitEpics(domain.listEpics(), {
324
386
  includeAll,
325
387
  statuses,
326
388
  limit,
389
+ cursor,
327
390
  });
391
+ const epics = listed.epics;
328
392
  const listView = view ?? "table";
329
393
  const human = epics.length === 0 ? "No epics found." : listView === "compact" ? epics.map(formatEpic).join("\n") : formatEpicListTable(epics);
330
394
 
@@ -332,6 +396,28 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
332
396
  command: "epic.list",
333
397
  human,
334
398
  data: { epics },
399
+ ...(context.mode === "human"
400
+ ? {}
401
+ : {
402
+ meta: {
403
+ pagination: listed.pagination,
404
+ defaults: {
405
+ statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_STATUSES] : null,
406
+ limit: !includeAll && limit === undefined ? DEFAULT_LIST_LIMIT : null,
407
+ cursor: rawCursor === undefined ? 0 : null,
408
+ view: view === undefined ? "table" : null,
409
+ },
410
+ filters: {
411
+ statuses: includeAll ? null : (statuses ?? [...DEFAULT_OPEN_STATUSES]),
412
+ includeAll,
413
+ },
414
+ truncation: {
415
+ applied: listed.pagination.hasMore,
416
+ returned: epics.length,
417
+ limit: includeAll ? null : (limit ?? DEFAULT_LIST_LIMIT),
418
+ },
419
+ },
420
+ }),
335
421
  });
336
422
  }
337
423
  case "show": {
@@ -365,6 +451,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
365
451
  command: "epic.show",
366
452
  human: formatEpic(epic),
367
453
  data: { epic, includeAll: false },
454
+ ...(context.mode === "human"
455
+ ? {}
456
+ : {
457
+ meta: {
458
+ defaults: {
459
+ view: view === undefined ? effectiveView : null,
460
+ },
461
+ filters: {
462
+ includeAll: false,
463
+ },
464
+ truncation: {
465
+ applied: true,
466
+ scope: "compact",
467
+ },
468
+ },
469
+ }),
368
470
  });
369
471
  }
370
472
 
@@ -375,6 +477,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
375
477
  command: "epic.show",
376
478
  human: formatEpicShowCompact(tree),
377
479
  data: { tree, includeAll: false },
480
+ ...(context.mode === "human"
481
+ ? {}
482
+ : {
483
+ meta: {
484
+ defaults: {
485
+ view: view === undefined ? effectiveView : null,
486
+ },
487
+ filters: {
488
+ includeAll: false,
489
+ },
490
+ truncation: {
491
+ applied: true,
492
+ scope: "tree",
493
+ },
494
+ },
495
+ }),
378
496
  });
379
497
  }
380
498
 
@@ -384,6 +502,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
384
502
  command: "epic.show",
385
503
  human: effectiveView === "table" ? formatEpicShowTable(tree) : formatEpicShowDetailed(tree),
386
504
  data: { tree, includeAll: true },
505
+ ...(context.mode === "human"
506
+ ? {}
507
+ : {
508
+ meta: {
509
+ defaults: {
510
+ view: view === undefined ? effectiveView : null,
511
+ },
512
+ filters: {
513
+ includeAll: true,
514
+ },
515
+ truncation: {
516
+ applied: false,
517
+ scope: "full",
518
+ },
519
+ },
520
+ }),
387
521
  });
388
522
  }
389
523
  case "update": {
@@ -1,8 +1,10 @@
1
1
  import { okResult } from "../io/output";
2
2
  import { type CliContext, type CliResult } from "../runtime/command-types";
3
+ import { CLI_VERSION } from "../runtime/version";
3
4
 
4
5
  const ROOT_HELP = [
5
6
  "Trekoon - AI-first local issue tracker",
7
+ `Version: ${CLI_VERSION}`,
6
8
  "",
7
9
  "Usage:",
8
10
  " trekoon [global-options] <command> [command-options]",
@@ -10,12 +12,14 @@ const ROOT_HELP = [
10
12
  "Global options:",
11
13
  " --json Emit stable JSON machine output",
12
14
  " --toon Emit true TOON-encoded output",
15
+ " --compat <mode> Enable explicit machine compatibility mode",
13
16
  " --help Show root or command help",
14
17
  " --version Print CLI version",
15
18
  "",
16
19
  "Commands:",
20
+ " help Show root or command help",
17
21
  " init Initialize .trekoon storage and local DB",
18
- " quickstart Show workflow + where to see task descriptions",
22
+ " quickstart Show AI execution loop + task detail workflow",
19
23
  " wipe Remove local Trekoon state (requires --yes)",
20
24
  " epic Epic lifecycle commands",
21
25
  " task Task lifecycle commands",
@@ -24,25 +28,269 @@ const ROOT_HELP = [
24
28
  " events Event retention and cleanup commands",
25
29
  " migrate Migration status and rollback commands",
26
30
  " sync Cross-branch sync commands",
27
- " skills Project-local skill install/link commands",
31
+ " skills Project-local skill install/update/link",
32
+ ].join("\n");
33
+
34
+ const INIT_HELP = [
35
+ "Usage: trekoon init [--json|--toon]",
36
+ "",
37
+ "Purpose:",
38
+ " Initialize local Trekoon storage (.trekoon) and database.",
39
+ "",
40
+ "Examples:",
41
+ " trekoon init",
42
+ " trekoon --json init",
43
+ ].join("\n");
44
+
45
+ const QUICKSTART_HELP = [
46
+ "Usage: trekoon quickstart [--json|--toon]",
47
+ "",
48
+ "Purpose:",
49
+ " Show the canonical Trekoon AI execution loop and task-detail workflow.",
50
+ "",
51
+ "Flow:",
52
+ " 1) trekoon --toon sync status",
53
+ " 2) trekoon --toon task ready --limit 5",
54
+ " 3) trekoon --toon task next",
55
+ " 4) trekoon --toon dep reverse <task-or-subtask-id>",
56
+ " 5) trekoon --toon task update <task-id> --status in_progress",
57
+ "",
58
+ "Examples:",
59
+ " trekoon quickstart",
60
+ " trekoon --toon quickstart",
61
+ ].join("\n");
62
+
63
+ const WIPE_HELP = [
64
+ "Usage: trekoon wipe --yes [--json|--toon]",
65
+ "",
66
+ "Purpose:",
67
+ " Remove local Trekoon state for the current repository.",
68
+ "",
69
+ "Options:",
70
+ " --yes Required safety confirmation.",
71
+ "",
72
+ "Examples:",
73
+ " trekoon wipe --yes",
74
+ ].join("\n");
75
+
76
+ const EPIC_HELP = [
77
+ "Usage: trekoon epic <create|list|show|update|delete> [options]",
78
+ "",
79
+ "List behavior:",
80
+ " Defaults:",
81
+ " - Open statuses only: in_progress, in-progress, todo",
82
+ " - Limit: 10",
83
+ " Flags:",
84
+ " --status <csv> | --limit <n> | --cursor <n> | --all | --view table|compact",
85
+ " Pagination:",
86
+ " - --cursor is offset-like",
87
+ " - Machine modes expose meta.pagination.hasMore / nextCursor",
88
+ " Constraints:",
89
+ " - --all is mutually exclusive with --status, --limit, and --cursor",
90
+ "",
91
+ "Show behavior:",
92
+ " Views:",
93
+ " - table: default view",
94
+ " - compact: epic summary",
95
+ " - tree: hierarchy",
96
+ " - detail: descriptions",
97
+ " Machine default:",
98
+ " - With --all, machine modes default to detail",
99
+ "",
100
+ "Update behavior:",
101
+ " Bulk target flags:",
102
+ " --all | --ids <csv>",
103
+ " Bulk fields:",
104
+ " --append <text> and/or --status <status>",
105
+ ].join("\n");
106
+
107
+ const TASK_HELP = [
108
+ "Usage: trekoon task <create|list|show|ready|next|update|delete> [options]",
109
+ "",
110
+ "List behavior:",
111
+ " Defaults:",
112
+ " - Open statuses only: in_progress, in-progress, todo",
113
+ " - Limit: 10",
114
+ " Flags:",
115
+ " --epic <id> | --status <csv> | --limit <n> | --cursor <n> | --all | --view table|compact",
116
+ " Pagination:",
117
+ " - --cursor is offset-like",
118
+ " - Machine modes expose meta.pagination.hasMore / nextCursor",
119
+ " Constraints:",
120
+ " - --all is mutually exclusive with --status, --limit, and --cursor",
121
+ "",
122
+ "Show behavior:",
123
+ " Views:",
124
+ " - table: default view",
125
+ " - compact: task summary",
126
+ " - tree: hierarchy",
127
+ " - detail: descriptions",
128
+ " Machine default:",
129
+ " - With --all, machine modes default to detail",
130
+ "",
131
+ "Ready/Next behavior:",
132
+ " ready:",
133
+ " - Returns deterministic unblocked candidates",
134
+ " - Sort order: status, blockers, createdAt, id",
135
+ " - Options: --limit <n>, --epic <id>",
136
+ " next:",
137
+ " - Returns top ready candidate",
138
+ " - Option: --epic <id>",
139
+ "",
140
+ "Update behavior:",
141
+ " Bulk target flags:",
142
+ " --all | --ids <csv>",
143
+ " Bulk fields:",
144
+ " --append <text> and/or --status <status>",
145
+ ].join("\n");
146
+
147
+ const SUBTASK_HELP = [
148
+ "Usage: trekoon subtask <create|list|update|delete> [options]",
149
+ "",
150
+ "List behavior:",
151
+ " Defaults:",
152
+ " - Open statuses only: in_progress, in-progress, todo",
153
+ " - Limit: 10",
154
+ " Flags:",
155
+ " --task <id> | --status <csv> | --limit <n> | --cursor <n> | --all | --view table|compact",
156
+ " Pagination:",
157
+ " - --cursor is offset-like",
158
+ " - Machine modes expose meta.pagination.hasMore / nextCursor",
159
+ " Constraints:",
160
+ " - --all is mutually exclusive with --status, --limit, and --cursor",
161
+ "",
162
+ "Update behavior:",
163
+ " Bulk target flags:",
164
+ " --all | --ids <csv>",
165
+ " Bulk fields:",
166
+ " --append <text> and/or --status <status>",
167
+ ].join("\n");
168
+
169
+ const DEP_HELP = [
170
+ "Usage: trekoon dep <add|remove|list|reverse> [options]",
171
+ "",
172
+ "Subcommands:",
173
+ " add <source-id> <depends-on-id>",
174
+ " Create dependency edge: source depends on depends-on.",
175
+ " remove <source-id> <depends-on-id>",
176
+ " Remove one dependency edge if it exists.",
177
+ " list <source-id>",
178
+ " Show direct dependencies for a node.",
179
+ " reverse <target-id>",
180
+ " Show downstream nodes blocked by target (with distance).",
181
+ "",
182
+ "Examples:",
183
+ " trekoon dep add <task-a> <task-b>",
184
+ " trekoon dep remove <task-a> <task-b>",
185
+ " trekoon dep list <task-a>",
186
+ " trekoon dep reverse <task-b>",
187
+ ].join("\n");
188
+
189
+ const EVENTS_HELP = [
190
+ "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]",
191
+ "",
192
+ "Purpose:",
193
+ " Manage retention for internal sync event log rows.",
194
+ "",
195
+ "Options:",
196
+ " --dry-run Preview candidate/archive/delete counts only.",
197
+ " --archive Copy pruned rows to event_archive before delete.",
198
+ " --retention-days <n> Keep last n days (positive integer, default 90).",
199
+ "",
200
+ "Examples:",
201
+ " trekoon events prune --dry-run",
202
+ " trekoon events prune --retention-days 30",
203
+ " trekoon events prune --archive",
204
+ ].join("\n");
205
+
206
+ const MIGRATE_HELP = [
207
+ "Usage: trekoon migrate <status|rollback> [--to-version <n>]",
208
+ "",
209
+ "Subcommands:",
210
+ " status",
211
+ " Show current schema version, latest version, and pending count.",
212
+ " rollback [--to-version <n>]",
213
+ " Roll back migrations; default target is one version back.",
214
+ "",
215
+ "Examples:",
216
+ " trekoon migrate status",
217
+ " trekoon migrate rollback",
218
+ " trekoon migrate rollback --to-version 1",
219
+ ].join("\n");
220
+
221
+ const SYNC_HELP = [
222
+ "Usage: trekoon sync <status|pull|resolve|conflicts> [options]",
223
+ "",
224
+ "Subcommands:",
225
+ " status [--from <branch>]",
226
+ " Show ahead/behind counts and pending conflicts vs source branch (default: main).",
227
+ " pull --from <branch>",
228
+ " Pull and apply upstream tracker events from a source branch.",
229
+ " conflicts list [--mode pending|all]",
230
+ " List sync conflicts (default mode: pending).",
231
+ " conflicts show <conflict-id>",
232
+ " Show full details for one conflict.",
233
+ " resolve <conflict-id> --use ours|theirs",
234
+ " Resolve a pending conflict by selecting ours or theirs.",
235
+ "",
236
+ "Compatibility mode:",
237
+ " --compat legacy-sync-command-ids",
238
+ " Emits legacy sync command IDs (sync_status, sync_pull, ...)",
239
+ " in machine output only and includes deprecation metadata.",
240
+ " Migration: remove --compat and consume dotted IDs (sync.status).",
241
+ " Planned compatibility window closes after 2026-09-30.",
242
+ "",
243
+ "Examples:",
244
+ " trekoon sync status",
245
+ " trekoon sync status --from main",
246
+ " trekoon sync pull --from main",
247
+ " trekoon sync conflicts list",
248
+ " trekoon sync conflicts list --mode all",
249
+ " trekoon sync conflicts show <conflict-id>",
250
+ " trekoon sync resolve <conflict-id> --use ours",
251
+ ].join("\n");
252
+
253
+ const SKILLS_HELP = [
254
+ "Usage:",
255
+ " trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]",
256
+ " trekoon skills update",
257
+ "",
258
+ "Purpose:",
259
+ " Install or refresh the project-local Trekoon skill asset.",
260
+ "",
261
+ "Install behavior:",
262
+ " - Always installs canonical file to:",
263
+ " <cwd>/.agents/skills/trekoon/SKILL.md",
264
+ " - Use --link to also create an editor symlink named 'trekoon'.",
265
+ " - --editor is required when --link is used (opencode|claude|pi).",
266
+ " - --to overrides the symlink root for --link only.",
267
+ " - Without --allow-outside-repo, link targets must resolve inside repo.",
268
+ " - --allow-outside-repo requires --link and disables that boundary check.",
269
+ "",
270
+ "Update behavior:",
271
+ " - Refreshes canonical SKILL file in the install path above.",
272
+ " - Reports default link states for opencode/claude/pi.",
273
+ "",
274
+ "Examples:",
275
+ " trekoon skills install",
276
+ " trekoon skills install --link --editor opencode",
277
+ " trekoon skills install --link --editor claude --to .claude/skills",
278
+ " trekoon skills install --link --editor pi --to ../shared/skills --allow-outside-repo",
279
+ " trekoon skills update",
28
280
  ].join("\n");
29
281
 
30
282
  const COMMAND_HELP: Record<string, string> = {
31
- init: "Usage: trekoon init [--json|--toon]",
32
- quickstart: "Usage: trekoon quickstart [--json|--toon]",
33
- wipe: "Usage: trekoon wipe --yes [--json|--toon]",
34
- epic:
35
- "Usage: trekoon epic <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --status <csv> | --limit <n> | --all | --view table|compact; show: compact=epic summary, tree=hierarchy, detail=descriptions, and --all defaults to detail in machine modes; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
36
- task:
37
- "Usage: trekoon task <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --status <csv> | --limit <n> | --all | --view table|compact; show: compact=task summary, tree=hierarchy, detail=descriptions, and --all defaults to detail in machine modes; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
38
- subtask:
39
- "Usage: trekoon subtask <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --task <id> | --status <csv> | --limit <n> | --all | --view table|compact; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
40
- dep: "Usage: trekoon dep <subcommand> [options]",
41
- events: "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]",
42
- migrate: "Usage: trekoon migrate <status|rollback> [--to-version <n>]",
43
- sync: "Usage: trekoon sync <subcommand> [options]",
44
- skills:
45
- "Usage: trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo] | trekoon skills update (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md; links must resolve inside repo unless --allow-outside-repo is set; update refreshes canonical SKILL and reports default link states)",
283
+ init: INIT_HELP,
284
+ quickstart: QUICKSTART_HELP,
285
+ wipe: WIPE_HELP,
286
+ epic: EPIC_HELP,
287
+ task: TASK_HELP,
288
+ subtask: SUBTASK_HELP,
289
+ dep: DEP_HELP,
290
+ events: EVENTS_HELP,
291
+ migrate: MIGRATE_HELP,
292
+ sync: SYNC_HELP,
293
+ skills: SKILLS_HELP,
46
294
  help: "Usage: trekoon help [command] [--json|--toon]",
47
295
  };
48
296
 
@@ -64,6 +312,7 @@ export async function runHelp(context: CliContext): Promise<CliResult> {
64
312
  data: {
65
313
  topic,
66
314
  text,
315
+ version: CLI_VERSION,
67
316
  },
68
317
  });
69
318
  }