trekoon 0.1.8 → 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.
@@ -35,6 +35,9 @@ data:
35
35
  status: in_progress
36
36
  createdAt: 1700000001000
37
37
  updatedAt: 1700000001000
38
+ metadata:
39
+ contractVersion: 1.0.0
40
+ requestId: req-abc12345
38
41
  ```
39
42
 
40
43
  On error:
@@ -43,6 +46,9 @@ On error:
43
46
  ok: false
44
47
  command: task.show
45
48
  data: {}
49
+ metadata:
50
+ contractVersion: 1.0.0
51
+ requestId: req-def67890
46
52
  error:
47
53
  code: not_found
48
54
  message: task not found: invalid-id
@@ -55,10 +61,34 @@ error:
55
61
  | `ok` | `true` if command succeeded, `false` on error |
56
62
  | `command` | The command that was executed (e.g., `task.list`, `epic.create`) |
57
63
  | `data` | The response payload (tasks, epics, dependencies, etc.) |
64
+ | `metadata` | Contract metadata (`contractVersion`, `requestId`) |
65
+ | `meta` | Optional command-specific metadata (pagination/defaults/filters/diagnostics) |
58
66
  | `error` | Present only on failure, contains `code` and `message` |
59
67
 
60
68
  Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to every command.
61
69
 
70
+ ### Contract details to rely on
71
+
72
+ - Machine responses include `metadata.contractVersion` and `metadata.requestId`.
73
+ - Command IDs are stable and typically dot namespaced (`task.list`, `sync.status`).
74
+ - Some root commands use single-token IDs (`help`, `init`, `quickstart`, `wipe`, `version`).
75
+ - Unknown options fail fast with deterministic `unknown_option` errors and may include:
76
+ - `data.option`
77
+ - `data.allowedOptions`
78
+ - `data.suggestions`
79
+
80
+ ### Compatibility mode (legacy sync consumers)
81
+
82
+ Default behavior is strict canonical IDs (for example `sync.status`).
83
+
84
+ If a legacy consumer still expects underscore sync IDs, compatibility mode can be used:
85
+
86
+ ```bash
87
+ trekoon --toon --compat legacy-sync-command-ids sync status
88
+ ```
89
+
90
+ When enabled, output includes `metadata.compatibility` with migration/deprecation details.
91
+
62
92
  ## 1) Status Management
63
93
 
64
94
  ### Status values
@@ -205,9 +235,21 @@ trekoon task list --all --toon
205
235
  - `--all` cannot be combined with `--cursor`.
206
236
  - Machine pagination contract is in `meta.pagination.hasMore` and
207
237
  `meta.pagination.nextCursor`.
238
+ - Machine list/show responses may also include:
239
+ - `meta.defaults`
240
+ - `meta.filters`
241
+ - `meta.truncation`
208
242
  - `epic show <id> --all --toon`: full epic tree (tasks + subtasks)
209
243
  - `task show <id> --all --toon`: task plus its subtasks
210
244
 
245
+ ### Canonical storage root behavior
246
+
247
+ - In git repos/worktrees, Trekoon resolves storage from repository top-level so
248
+ nested cwd invocations use one canonical `.trekoon/trekoon.db`.
249
+ - In non-git directories, Trekoon falls back to invocation cwd.
250
+ - If invocation cwd differs from canonical root, machine output may include
251
+ `meta.storageRootDiagnostics`.
252
+
211
253
  ### View Options
212
254
 
213
255
  | Command | `--view` options |
package/README.md CHANGED
@@ -46,12 +46,15 @@ npm i -g trekoon
46
46
  ## Command surface
47
47
 
48
48
  - `trekoon init`
49
+ - `trekoon help [command]`
49
50
  - `trekoon quickstart`
50
51
  - `trekoon epic <create|list|show|update|delete>`
51
52
  - `trekoon task <create|list|show|ready|next|update|delete>`
52
53
  - `trekoon subtask <create|list|update|delete>`
53
54
  - `trekoon dep <add|remove|list|reverse>`
54
- - `trekoon sync <status|pull|resolve>`
55
+ - `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
56
+ - `trekoon migrate <status|rollback> [--to-version <n>]`
57
+ - `trekoon sync <status|pull|resolve|conflicts>`
55
58
  - `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]`
56
59
  - `trekoon skills update`
57
60
  - `trekoon wipe --yes`
@@ -60,6 +63,7 @@ Global output modes:
60
63
 
61
64
  - `--json` for structured JSON output
62
65
  - `--toon` for true TOON-encoded output (not JSON text)
66
+ - `--compat <mode>` for explicit machine compatibility behavior
63
67
  - `--help` for root and command help
64
68
  - `--version` for CLI version
65
69
 
@@ -72,7 +76,8 @@ trekoon --json quickstart
72
76
  trekoon quickstart --json
73
77
  ```
74
78
 
75
- Trekoon currently accepts long option form (`--option`).
79
+ Trekoon options use long form (`--option`) for command/subcommand flags.
80
+ Root help/version aliases `-h` and `-v` are also supported.
76
81
 
77
82
  Human view options:
78
83
 
@@ -112,8 +117,15 @@ trekoon epic update --ids <epic-1>,<epic-2> --status done
112
117
 
113
118
  ## Quickstart
114
119
 
115
- Trekoon is local-first: each worktree uses its own `.trekoon/trekoon.db`.
116
- Git does not merge this DB file; Trekoon sync commands merge tracker state.
120
+ Trekoon is local-first: in git repos/worktrees, Trekoon resolves state to one
121
+ canonical repository root (`git rev-parse --show-toplevel`) so nested
122
+ invocations share the same `.trekoon/trekoon.db`.
123
+
124
+ Outside git repos, Trekoon falls back to the invocation cwd.
125
+
126
+ When machine output is enabled (`--json`/`--toon`) and a command resolves
127
+ storage from a non-canonical cwd, Trekoon emits
128
+ `meta.storageRootDiagnostics` to make the divergence explicit for automation.
117
129
 
118
130
  ### 1) Initialize
119
131
 
@@ -183,6 +195,8 @@ trekoon --json task show <task-id>
183
195
  ```bash
184
196
  trekoon sync status
185
197
  trekoon sync pull --from main
198
+ trekoon sync conflicts list
199
+ trekoon sync conflicts show <conflict-id>
186
200
  trekoon sync resolve <conflict-id> --use ours
187
201
  ```
188
202
 
@@ -195,10 +209,31 @@ react deterministically:
195
209
  - `diagnostics.conflictEvents`
196
210
  - `diagnostics.errorHints`
197
211
 
212
+ Compatibility mode for legacy sync command IDs:
213
+
214
+ ```bash
215
+ trekoon --json --compat legacy-sync-command-ids sync status
216
+ trekoon --toon --compat legacy-sync-command-ids sync pull --from main
217
+ ```
218
+
219
+ Behavior:
220
+
221
+ - Default remains strict canonical IDs (`sync.status`, `sync.pull`, ...).
222
+ - Compatibility mode rewrites sync command IDs to legacy forms
223
+ (`sync_status`, `sync_pull`, ...).
224
+ - Compatibility mode is machine-only and valid only for `sync` commands.
225
+ - Machine output includes `metadata.compatibility` with:
226
+ - deprecation warning code
227
+ - migration guidance
228
+ - canonical + compatibility command IDs
229
+ - removal window (`removalAfter: 2026-09-30`)
230
+ - Migration path: remove `--compat legacy-sync-command-ids` and consume dotted
231
+ command IDs directly.
232
+
198
233
  ### 7) Install project-local Trekoon skill for agents
199
234
 
200
- `trekoon skills install` always writes the bundled skill file into the current
201
- repository at:
235
+ `trekoon skills install` always writes the bundled skill file under the current
236
+ working directory at:
202
237
 
203
238
  - `.agents/skills/trekoon/SKILL.md`
204
239
 
@@ -220,7 +255,7 @@ Path behavior:
220
255
  - Default pi link path: `.pi/skills/trekoon`
221
256
  - `--to <path>` overrides the editor root for link creation only.
222
257
  - `--to` does **not** move or copy `SKILL.md` to that path.
223
- - By default, link targets must resolve inside the repository root.
258
+ - By default, link targets must resolve inside the current working directory root.
224
259
  - Use `--allow-outside-repo` only for intentional external links.
225
260
  - When override is used, install prints a warning and includes confirmation
226
261
  fields in machine output.
@@ -237,11 +272,11 @@ Path behavior:
237
272
  How `--to` works (step-by-step):
238
273
 
239
274
  1. Trekoon always installs/copies to:
240
- - `<repo>/.agents/skills/trekoon/SKILL.md`
275
+ - `<cwd>/.agents/skills/trekoon/SKILL.md`
241
276
  2. If `--link` is present, Trekoon creates a `trekoon` symlink directory entry.
242
277
  3. `--to <path>` sets the symlink root directory.
243
278
  4. Final link path is:
244
- - `<resolved-to-path>/trekoon -> <repo>/.agents/skills/trekoon`
279
+ - `<resolved-to-path>/trekoon -> <cwd>/.agents/skills/trekoon`
245
280
 
246
281
  Example:
247
282
 
@@ -251,9 +286,9 @@ trekoon skills install --link --editor opencode --to ./.custom-editor/skills
251
286
 
252
287
  This produces:
253
288
 
254
- - `<repo>/.agents/skills/trekoon/SKILL.md` (copied file)
255
- - `<repo>/.custom-editor/skills/trekoon` (symlink)
256
- - symlink target: `<repo>/.agents/skills/trekoon`
289
+ - `<cwd>/.agents/skills/trekoon/SKILL.md` (copied file)
290
+ - `<cwd>/.custom-editor/skills/trekoon` (symlink)
291
+ - symlink target: `<cwd>/.agents/skills/trekoon`
257
292
 
258
293
  Trekoon does not mutate global editor config directories.
259
294
 
@@ -269,6 +304,27 @@ Trekoon does not mutate global editor config directories.
269
304
  Use `--toon` for production agent loops. The examples below show command +
270
305
  expected envelope fields.
271
306
 
307
+ Base envelope fields (all machine responses):
308
+
309
+ ```text
310
+ ok: true|false
311
+ command: <stable command id>
312
+ data: <payload>
313
+ metadata:
314
+ contractVersion: "1.0.0"
315
+ requestId: req-<stable-id>
316
+ ```
317
+
318
+ Most subcommand identifiers are dot-namespaced (`task.list`, `sync.pull`,
319
+ `epic.show`). Root-level commands may use single-token IDs (`help`, `init`,
320
+ `quickstart`, `wipe`, `version`).
321
+
322
+ Additional metadata can appear when relevant:
323
+
324
+ - `metadata.compatibility` when `--compat` mode is active
325
+ - `meta.storageRootDiagnostics` when a machine-readable command resolves
326
+ storage from a non-canonical cwd
327
+
272
328
  ### Ready queue (deterministic candidates)
273
329
 
274
330
  ```bash
@@ -336,6 +392,9 @@ ok: true
336
392
  command: task.list
337
393
  data:
338
394
  tasks[]: ...
395
+ metadata:
396
+ contractVersion: "1.0.0"
397
+ requestId: req-<stable-id>
339
398
  meta:
340
399
  pagination: { hasMore, nextCursor }
341
400
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -3,6 +3,7 @@ export interface ParsedArgs {
3
3
  readonly options: ReadonlyMap<string, string>;
4
4
  readonly flags: ReadonlySet<string>;
5
5
  readonly missingOptionValues: ReadonlySet<string>;
6
+ readonly providedOptions: readonly string[];
6
7
  }
7
8
 
8
9
  const LONG_PREFIX = "--";
@@ -12,6 +13,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
12
13
  const options = new Map<string, string>();
13
14
  const flags = new Set<string>();
14
15
  const missingOptionValues = new Set<string>();
16
+ const providedOptions: string[] = [];
15
17
 
16
18
  for (let index = 0; index < args.length; index += 1) {
17
19
  const token: string | undefined = args[index];
@@ -25,6 +27,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
25
27
  }
26
28
 
27
29
  const key = token.slice(LONG_PREFIX.length);
30
+ providedOptions.push(key);
28
31
  const value = args[index + 1];
29
32
  if (!value || value.startsWith(LONG_PREFIX)) {
30
33
  flags.add(key);
@@ -41,6 +44,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
41
44
  options,
42
45
  flags,
43
46
  missingOptionValues,
47
+ providedOptions,
44
48
  };
45
49
  }
46
50
 
@@ -92,6 +96,84 @@ export function parseStrictNonNegativeInt(rawValue: string | undefined): number
92
96
  return parsed;
93
97
  }
94
98
 
99
+ function levenshteinDistance(source: string, target: string): number {
100
+ const sourceLength = source.length;
101
+ const targetLength = target.length;
102
+ if (sourceLength === 0) {
103
+ return targetLength;
104
+ }
105
+
106
+ if (targetLength === 0) {
107
+ return sourceLength;
108
+ }
109
+
110
+ const previous: number[] = Array.from({ length: targetLength + 1 }, (_, index) => index);
111
+ const current: number[] = new Array<number>(targetLength + 1).fill(0);
112
+
113
+ for (let sourceIndex = 1; sourceIndex <= sourceLength; sourceIndex += 1) {
114
+ current[0] = sourceIndex;
115
+ for (let targetIndex = 1; targetIndex <= targetLength; targetIndex += 1) {
116
+ const replacementCost = source[sourceIndex - 1] === target[targetIndex - 1] ? 0 : 1;
117
+ const insertCost = (current[targetIndex - 1] ?? 0) + 1;
118
+ const deleteCost = (previous[targetIndex] ?? 0) + 1;
119
+ const replaceCost = (previous[targetIndex - 1] ?? 0) + replacementCost;
120
+ current[targetIndex] = Math.min(
121
+ insertCost,
122
+ deleteCost,
123
+ replaceCost,
124
+ );
125
+ }
126
+
127
+ for (let targetIndex = 0; targetIndex <= targetLength; targetIndex += 1) {
128
+ previous[targetIndex] = current[targetIndex] ?? previous[targetIndex] ?? 0;
129
+ }
130
+ }
131
+
132
+ return previous[targetLength] ?? 0;
133
+ }
134
+
135
+ function normalizeOption(option: string): string {
136
+ return option.startsWith(LONG_PREFIX) ? option.slice(LONG_PREFIX.length) : option;
137
+ }
138
+
139
+ export function findUnknownOption(parsed: ParsedArgs, allowedOptions: readonly string[]): string | undefined {
140
+ const allowed = new Set<string>(allowedOptions.map(normalizeOption));
141
+ for (const option of parsed.providedOptions) {
142
+ if (!allowed.has(option)) {
143
+ return option;
144
+ }
145
+ }
146
+
147
+ return undefined;
148
+ }
149
+
150
+ export function suggestOptions(option: string, allowedOptions: readonly string[], limit = 3): string[] {
151
+ const normalizedOption = normalizeOption(option);
152
+ const normalizedAllowed = allowedOptions.map(normalizeOption);
153
+ return normalizedAllowed
154
+ .map((candidate) => {
155
+ const distance =
156
+ candidate.startsWith(normalizedOption) || normalizedOption.startsWith(candidate)
157
+ ? 0
158
+ : levenshteinDistance(normalizedOption, candidate);
159
+ return {
160
+ candidate,
161
+ distance,
162
+ };
163
+ })
164
+ .sort((left, right) => {
165
+ const byDistance = left.distance - right.distance;
166
+ if (byDistance !== 0) {
167
+ return byDistance;
168
+ }
169
+
170
+ return left.candidate.localeCompare(right.candidate);
171
+ })
172
+ .filter((item) => item.distance <= Math.max(2, Math.floor(normalizedOption.length / 2)))
173
+ .slice(0, limit)
174
+ .map((item) => item.candidate);
175
+ }
176
+
95
177
  export function readEnumOption<const T extends readonly string[]>(
96
178
  options: ReadonlyMap<string, string>,
97
179
  allowed: T,
@@ -396,7 +396,28 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
396
396
  command: "epic.list",
397
397
  human,
398
398
  data: { epics },
399
- ...(context.mode === "human" ? {} : { meta: { pagination: listed.pagination } }),
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
+ }),
400
421
  });
401
422
  }
402
423
  case "show": {
@@ -430,6 +451,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
430
451
  command: "epic.show",
431
452
  human: formatEpic(epic),
432
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
+ }),
433
470
  });
434
471
  }
435
472
 
@@ -440,6 +477,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
440
477
  command: "epic.show",
441
478
  human: formatEpicShowCompact(tree),
442
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
+ }),
443
496
  });
444
497
  }
445
498
 
@@ -449,6 +502,22 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
449
502
  command: "epic.show",
450
503
  human: effectiveView === "table" ? formatEpicShowTable(tree) : formatEpicShowDetailed(tree),
451
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
+ }),
452
521
  });
453
522
  }
454
523
  case "update": {
@@ -12,10 +12,12 @@ const ROOT_HELP = [
12
12
  "Global options:",
13
13
  " --json Emit stable JSON machine output",
14
14
  " --toon Emit true TOON-encoded output",
15
+ " --compat <mode> Enable explicit machine compatibility mode",
15
16
  " --help Show root or command help",
16
17
  " --version Print CLI version",
17
18
  "",
18
19
  "Commands:",
20
+ " help Show root or command help",
19
21
  " init Initialize .trekoon storage and local DB",
20
22
  " quickstart Show AI execution loop + task detail workflow",
21
23
  " wipe Remove local Trekoon state (requires --yes)",
@@ -88,6 +90,7 @@ const EPIC_HELP = [
88
90
  "",
89
91
  "Show behavior:",
90
92
  " Views:",
93
+ " - table: default view",
91
94
  " - compact: epic summary",
92
95
  " - tree: hierarchy",
93
96
  " - detail: descriptions",
@@ -109,7 +112,7 @@ const TASK_HELP = [
109
112
  " - Open statuses only: in_progress, in-progress, todo",
110
113
  " - Limit: 10",
111
114
  " Flags:",
112
- " --status <csv> | --limit <n> | --cursor <n> | --all | --view table|compact",
115
+ " --epic <id> | --status <csv> | --limit <n> | --cursor <n> | --all | --view table|compact",
113
116
  " Pagination:",
114
117
  " - --cursor is offset-like",
115
118
  " - Machine modes expose meta.pagination.hasMore / nextCursor",
@@ -118,6 +121,7 @@ const TASK_HELP = [
118
121
  "",
119
122
  "Show behavior:",
120
123
  " Views:",
124
+ " - table: default view",
121
125
  " - compact: task summary",
122
126
  " - tree: hierarchy",
123
127
  " - detail: descriptions",
@@ -229,6 +233,13 @@ const SYNC_HELP = [
229
233
  " resolve <conflict-id> --use ours|theirs",
230
234
  " Resolve a pending conflict by selecting ours or theirs.",
231
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
+ "",
232
243
  "Examples:",
233
244
  " trekoon sync status",
234
245
  " trekoon sync status --from main",
@@ -323,7 +323,29 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
323
323
  command: "subtask.list",
324
324
  human,
325
325
  data: { subtasks },
326
- ...(context.mode === "human" ? {} : { meta: { pagination: listed.pagination } }),
326
+ ...(context.mode === "human"
327
+ ? {}
328
+ : {
329
+ meta: {
330
+ pagination: listed.pagination,
331
+ defaults: {
332
+ statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_SUBTASK_STATUSES] : null,
333
+ limit: !includeAll && parsedLimit === undefined ? DEFAULT_SUBTASK_LIST_LIMIT : null,
334
+ cursor: parsedCursor === undefined ? 0 : null,
335
+ view: view === undefined ? "table" : null,
336
+ },
337
+ filters: {
338
+ taskId: taskId ?? null,
339
+ statuses: selectedStatuses ?? null,
340
+ includeAll,
341
+ },
342
+ truncation: {
343
+ applied: listed.pagination.hasMore,
344
+ returned: subtasks.length,
345
+ limit: selectedLimit ?? null,
346
+ },
347
+ },
348
+ }),
327
349
  });
328
350
  }
329
351
  case "update": {