trekoon 0.2.0 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ npm i -g trekoon
40
40
 
41
41
  1. Make issue tracking fast enough for daily terminal use.
42
42
  2. Make issue data deterministic and machine-readable for AI automation.
43
- 3. Keep branch/worktree-aware state so parallel execution can be coordinated safely.
43
+ 3. Keep one repo-scoped state store that every worktree can coordinate through safely.
44
44
  4. Stay minimal in code size while preserving robustness and clear boundaries.
45
45
 
46
46
  ## Command surface
@@ -48,10 +48,11 @@ npm i -g trekoon
48
48
  - `trekoon init`
49
49
  - `trekoon help [command]`
50
50
  - `trekoon quickstart`
51
- - `trekoon epic <create|list|show|search|replace|update|delete>`
52
- - `trekoon task <create|list|show|ready|next|search|replace|update|delete>`
53
- - `trekoon subtask <create|list|search|replace|update|delete>`
54
- - `trekoon dep <add|remove|list|reverse>`
51
+ - `trekoon epic <create|expand|list|show|search|replace|update|delete>`
52
+ - `trekoon session`
53
+ - `trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete>`
54
+ - `trekoon subtask <create|create-many|list|search|replace|update|delete>`
55
+ - `trekoon dep <add|add-many|remove|list|reverse>`
55
56
  - `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
56
57
  - `trekoon migrate <status|rollback> [--to-version <n>]`
57
58
  - `trekoon sync <status|pull|resolve|conflicts>`
@@ -117,15 +118,23 @@ trekoon epic update --ids <epic-1>,<epic-2> --status done
117
118
 
118
119
  ## Quickstart
119
120
 
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`.
121
+ Trekoon is local-first, but in git repos and worktrees it is **repo-shared**:
122
+ every worktree for the same repository resolves to one shared `.trekoon`
123
+ directory and one shared `.trekoon/trekoon.db`.
124
+
125
+ - `worktreeRoot` identifies the current checkout.
126
+ - `sharedStorageRoot` identifies the repository root that owns `.trekoon`.
127
+ - `databaseFile` points at the shared SQLite database.
128
+ - `.trekoon` stays gitignored on purpose because the DB is operational state,
129
+ not source code.
130
+ - Committing `.trekoon/trekoon.db` is the wrong fix for drift because it bakes
131
+ machine-local state and stale snapshots into Git.
123
132
 
124
133
  Outside git repos, Trekoon falls back to the invocation cwd.
125
134
 
126
135
  When machine output is enabled (`--json`/`--toon`) and a command resolves
127
136
  storage from a non-canonical cwd, Trekoon emits
128
- `meta.storageRootDiagnostics` to make the divergence explicit for automation.
137
+ `meta.storageRootDiagnostics` so automation can verify the storage contract.
129
138
 
130
139
  ### 1) Initialize
131
140
 
@@ -134,6 +143,15 @@ trekoon init
134
143
  trekoon --version
135
144
  ```
136
145
 
146
+ Bootstrap expectations:
147
+
148
+ - Run `trekoon --toon init` once per repository to create or re-bootstrap the
149
+ shared storage root.
150
+ - Run `trekoon --toon sync status` before agent work to inspect diagnostics.
151
+ - If diagnostics report `recoveryRequired`, a tracked/ignored mismatch, or an
152
+ ambiguous recovery path, stop and repair setup before continuing.
153
+ - Do **not** continue with task selection after broken bootstrap warnings.
154
+
137
155
  ### 2) Create epic → task → subtask
138
156
 
139
157
  ```bash
@@ -146,6 +164,42 @@ trekoon task list --limit 25
146
164
  trekoon task list --all --view compact
147
165
  ```
148
166
 
167
+ ### 2a) Preferred one-shot epic creation
168
+
169
+ When you already know the epic tree, create the epic, tasks, subtasks, and
170
+ dependencies in one invocation.
171
+
172
+ ```bash
173
+ trekoon epic create \
174
+ --title "Batch command rollout" \
175
+ --description "Ship one-shot planning workflows" \
176
+ --task "task-a|First task|First description|todo" \
177
+ --task "task-b|Second task|Second description|todo" \
178
+ --subtask "@task-a|sub-a|First subtask|Subtask description|todo" \
179
+ --dep "@task-b|@task-a" \
180
+ --dep "@sub-a|@task-a"
181
+ ```
182
+
183
+ Use this when:
184
+
185
+ - the epic does not exist yet
186
+ - later records need to reference earlier created records via `@temp-key`
187
+ - you want one atomic create step and one machine response with mappings/counts
188
+
189
+ Compact machine output adds:
190
+
191
+ ```text
192
+ command: epic.create
193
+ data:
194
+ epic: created epic row
195
+ tasks[]: created tasks in input order
196
+ subtasks[]: created subtasks in input order
197
+ dependencies[]: created dependencies in input order
198
+ result:
199
+ mappings[]: { kind: task|subtask, tempKey, id }
200
+ counts: { tasks, subtasks, dependencies }
201
+ ```
202
+
149
203
  ### 3) Add dependencies
150
204
 
151
205
  ```bash
@@ -153,25 +207,229 @@ trekoon dep add <task-id> <depends-on-id>
153
207
  trekoon dep list <task-id>
154
208
  ```
155
209
 
210
+ ### 3a) Batch planning commands
211
+
212
+ Use compact batch commands when one invocation needs to create or link multiple
213
+ items atomically. Use the single-item commands when you already have persisted
214
+ UUIDs and only need one mutation.
215
+
216
+ #### `task create-many`
217
+
218
+ Create multiple tasks under one epic in declared order.
219
+
220
+ ```bash
221
+ trekoon task create-many \
222
+ --epic <epic-id> \
223
+ --task "seed-api|Design API|Define batch grammar|todo" \
224
+ --task "seed-cli|Wire CLI|Hook parser and output|in_progress"
225
+ ```
226
+
227
+ Compact spec:
228
+
229
+ - `--task <temp-key>|<title>|<description>|<status>`
230
+ - escape `\|`, `\\`, `\n`, `\r`, `\t`
231
+ - repeated `--task` flags are preserved in the exact order provided
232
+ - temp keys are local mapping labels, not persisted IDs
233
+
234
+ Rollback semantics:
235
+
236
+ - Trekoon validates the full batch before inserts
237
+ - duplicate temp keys, empty required fields, or invalid input fail the whole
238
+ command
239
+ - no partial task rows are kept on failure
240
+
241
+ Compact machine output:
242
+
243
+ ```text
244
+ command: task.create-many
245
+ data:
246
+ epicId: <epic-id>
247
+ tasks[]: created task rows in input order
248
+ result:
249
+ mappings[]: { kind: task, tempKey, id }
250
+ ```
251
+
252
+ #### `subtask create-many`
253
+
254
+ Create multiple subtasks under one existing task.
255
+
256
+ ```bash
257
+ trekoon subtask create-many <task-id> \
258
+ --subtask "seed-tests|Write tests|Cover happy path|todo" \
259
+ --subtask "seed-docs|Document flow|Add operator notes|todo"
260
+ ```
261
+
262
+ Equivalent explicit parent form:
263
+
264
+ ```bash
265
+ trekoon subtask create-many \
266
+ --task <task-id> \
267
+ --subtask "seed-tests|Write tests|Cover happy path|todo"
268
+ ```
269
+
270
+ Rules:
271
+
272
+ - positional `<task-id>` or `--task <task-id>` may be used
273
+ - if both are provided, they must be identical or the command fails
274
+ - repeated `--subtask` flags are applied in declared order
275
+
276
+ Rollback semantics:
277
+
278
+ - full batch prevalidation happens before inserts
279
+ - duplicate temp keys, conflicting task ids, or invalid specs abort the whole
280
+ command
281
+ - no partial subtasks are kept on failure
282
+
283
+ Compact machine output:
284
+
285
+ ```text
286
+ command: subtask.create-many
287
+ data:
288
+ taskId: <task-id>
289
+ subtasks[]: created subtask rows in input order
290
+ result:
291
+ mappings[]: { kind: subtask, tempKey, id }
292
+ ```
293
+
294
+ #### `dep add-many`
295
+
296
+ Create multiple dependency edges in one ordered, transactional operation.
297
+
298
+ ```bash
299
+ trekoon dep add-many \
300
+ --dep "<task-b>|<task-a>" \
301
+ --dep "<subtask-c>|<task-b>"
302
+ ```
303
+
304
+ Compact spec:
305
+
306
+ - `--dep <source-ref>|<depends-on-ref>`
307
+ - repeated `--dep` flags are applied in declared order
308
+ - standalone `dep add-many` resolves persisted IDs only
309
+ - `@temp-key` refs are **not** resolved from earlier commands; they are reserved
310
+ for same-invocation workflows such as `epic expand`
311
+
312
+ Rollback semantics:
313
+
314
+ - validation covers the full dependency set before insert
315
+ - missing ids, unresolved `@temp-key` refs, duplicates, or cycles fail the whole
316
+ batch
317
+ - no partial dependency edges are inserted on failure
318
+
319
+ Compact machine output:
320
+
321
+ ```text
322
+ command: dep.add-many
323
+ data:
324
+ dependencies[]: created dependency rows in input order
325
+ result:
326
+ mappings[]: []
327
+ ```
328
+
329
+ #### `epic expand`
330
+
331
+ Expand one existing epic by creating tasks, subtasks, and dependencies in one
332
+ transaction. Use this when the epic already exists and you want to add a linked
333
+ batch later.
334
+
335
+ ```bash
336
+ trekoon epic expand <epic-id> \
337
+ --task "task-api|Design API|Define compact grammar|todo" \
338
+ --task "task-cli|Wire CLI|Hook parser and output|todo" \
339
+ --subtask "@task-api|sub-tests|Write tests|Cover parser cases|todo" \
340
+ --dep "@task-cli|@task-api" \
341
+ --dep "@sub-tests|@task-api"
342
+ ```
343
+
344
+ Compact specs:
345
+
346
+ - `--task <temp-key>|<title>|<description>|<status>`
347
+ - `--subtask <parent-ref>|<temp-key>|<title>|<description>|<status>`
348
+ - `--dep <source-ref>|<depends-on-ref>`
349
+ - `@temp-key` refs may target tasks/subtasks declared earlier in the same
350
+ `epic expand` invocation
351
+
352
+ Background phases:
353
+
354
+ 1. validate all compact specs and duplicate temp keys
355
+ 2. create tasks transactionally
356
+ 3. resolve subtask parent temp keys and create subtasks
357
+ 4. resolve dependency refs and link dependencies
358
+ 5. append task, subtask, then dependency events
359
+ 6. roll back the full expansion if any phase fails
360
+
361
+ Compact machine output:
362
+
363
+ ```text
364
+ command: epic.expand
365
+ data:
366
+ epicId: <epic-id>
367
+ tasks[]: created tasks in input order
368
+ subtasks[]: created subtasks in input order
369
+ dependencies[]: created dependencies in input order
370
+ result:
371
+ mappings[]: { kind: task|subtask, tempKey, id }
372
+ counts: { tasks, subtasks, dependencies }
373
+ ```
374
+
375
+ When to choose which command:
376
+
377
+ - use `task create-many` for sibling tasks under one known epic
378
+ - use `subtask create-many` for sibling subtasks under one known task
379
+ - use `dep add-many` only when every endpoint already has a persisted ID
380
+ - use `epic create` with batch specs when the epic does not exist yet and the
381
+ whole graph is known up front
382
+ - use `epic expand` when the epic already exists and one batch must add linked
383
+ tasks/subtasks/dependencies with `@temp-key` references
384
+
156
385
  ### 4) AI execution loop for agents
157
386
 
158
- Run this loop each session to pick next work deterministically:
387
+ The primary loop is: **session work task done → repeat**.
388
+
389
+ Orient with a single call that returns diagnostics, sync status, next ready
390
+ task with subtasks, blocker list, and readiness counts:
391
+
392
+ ```bash
393
+ trekoon --toon session
394
+ ```
395
+
396
+ Claim work, then finish or report a block:
159
397
 
160
398
  ```bash
161
- trekoon --toon sync status
162
- trekoon --toon task ready --limit 5
163
- trekoon --toon task next
164
- trekoon --toon dep reverse <task-or-subtask-id>
165
399
  trekoon --toon task update <task-id> --status in_progress
400
+ trekoon --toon task done <task-id>
401
+ trekoon --toon task update <task-id> --append "Blocked by <reason>" --status blocked
166
402
  ```
167
403
 
168
- When done or blocked, append context and update final status:
404
+ `task done` marks the task done and returns the next ready task with
405
+ dependencies inline, replacing the old multi-step transition.
406
+
407
+ Fail-fast rules:
408
+
409
+ - Treat `meta.storageRootDiagnostics` as the source of truth for worktree
410
+ storage.
411
+ - In linked worktrees, `sharedStorageRoot` may differ from `worktreeRoot`; that
412
+ is expected.
413
+ - If `recoveryRequired` is `true`, stop and follow the reported bootstrap or
414
+ recovery action.
415
+ - Do not fall back to a separate per-worktree DB or continue after missing
416
+ shared storage.
417
+
418
+ <details>
419
+ <summary>Legacy manual bootstrap (use <code>session</code> instead)</summary>
169
420
 
170
421
  ```bash
422
+ trekoon --toon init
423
+ trekoon --toon sync status
424
+ trekoon --toon task ready --limit 5
425
+ trekoon --toon task next
426
+ trekoon --toon dep reverse <task-or-subtask-id>
427
+ trekoon --toon task update <task-id> --status in_progress
171
428
  trekoon --toon task update <task-id> --append "Completed implementation and checks" --status done
172
- trekoon --toon task update <task-id> --append "Blocked by <reason>" --status blocked
173
429
  ```
174
430
 
431
+ </details>
432
+
175
433
  ### 5) Use TOON output for agent workflows
176
434
 
177
435
  ```bash
@@ -290,6 +548,19 @@ react deterministically:
290
548
  - `diagnostics.conflictEvents`
291
549
  - `diagnostics.errorHints`
292
550
 
551
+ Worktree diagnostics and recovery:
552
+
553
+ - Inspect `storageMode`, `repoCommonDir`, `worktreeRoot`, `sharedStorageRoot`,
554
+ and `databaseFile` in machine output when debugging worktree behavior.
555
+ - If a worktree resolves shared storage outside its checkout, that is expected
556
+ for linked worktrees and should not be “fixed” by committing `.trekoon`.
557
+ - If Git contains a tracked `.trekoon/trekoon.db`, remove it from Git history or
558
+ the index as appropriate, keep `.trekoon` ignored, and re-run
559
+ `trekoon --toon init`.
560
+ - Use `trekoon wipe --yes` only for explicit destructive recovery; it deletes
561
+ the shared storage root for the entire repository, not just the current
562
+ worktree.
563
+
293
564
  Compatibility mode for legacy sync command IDs:
294
565
 
295
566
  ```bash
@@ -379,6 +650,7 @@ Trekoon does not mutate global editor config directories.
379
650
  - [ ] done tasks/subtasks are marked completed
380
651
  - [ ] dependency graph has no stale blockers
381
652
  - [ ] final AI check: `trekoon --toon epic show <epic-id>`
653
+ - [ ] no one tried to commit `.trekoon/trekoon.db` as a worktree fix
382
654
 
383
655
  ## Machine-contract recipes (--toon)
384
656
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.2.0",
3
+ "version": "0.2.4",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,11 +1,25 @@
1
+ import {
2
+ COMPACT_TEMP_KEY_PREFIX,
3
+ type CompactEntityRef,
4
+ type CompactEntityIdRef,
5
+ type CompactTempKey,
6
+ type CompactTempKeyRef,
7
+ } from "../domain/types";
8
+
1
9
  export interface ParsedArgs {
2
10
  readonly positional: readonly string[];
3
11
  readonly options: ReadonlyMap<string, string>;
12
+ readonly optionEntries: readonly ParsedOptionEntry[];
4
13
  readonly flags: ReadonlySet<string>;
5
14
  readonly missingOptionValues: ReadonlySet<string>;
6
15
  readonly providedOptions: readonly string[];
7
16
  }
8
17
 
18
+ export interface ParsedOptionEntry {
19
+ readonly key: string;
20
+ readonly value: string;
21
+ }
22
+
9
23
  export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
10
24
 
11
25
  export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
@@ -21,11 +35,18 @@ export interface PreviewApplyModeSelection {
21
35
  readonly conflict: boolean;
22
36
  }
23
37
 
38
+ export interface ParsedCompactFields {
39
+ readonly fields: readonly string[];
40
+ readonly invalidEscape: string | null;
41
+ readonly hasDanglingEscape: boolean;
42
+ }
43
+
24
44
  const LONG_PREFIX = "--";
25
45
 
26
46
  export function parseArgs(args: readonly string[]): ParsedArgs {
27
47
  const positional: string[] = [];
28
48
  const options = new Map<string, string>();
49
+ const optionEntries: ParsedOptionEntry[] = [];
29
50
  const flags = new Set<string>();
30
51
  const missingOptionValues = new Set<string>();
31
52
  const providedOptions: string[] = [];
@@ -51,12 +72,14 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
51
72
  }
52
73
 
53
74
  options.set(key, value);
75
+ optionEntries.push({ key, value });
54
76
  index += 1;
55
77
  }
56
78
 
57
79
  return {
58
80
  positional,
59
81
  options,
82
+ optionEntries,
60
83
  flags,
61
84
  missingOptionValues,
62
85
  providedOptions,
@@ -78,6 +101,11 @@ export function hasFlag(flags: ReadonlySet<string>, ...keys: string[]): boolean
78
101
  return keys.some((key) => flags.has(key));
79
102
  }
80
103
 
104
+ export function readOptions(optionEntries: readonly ParsedOptionEntry[], ...keys: string[]): string[] {
105
+ const allowedKeys = new Set<string>(keys);
106
+ return optionEntries.filter((entry) => allowedKeys.has(entry.key)).map((entry) => entry.value);
107
+ }
108
+
81
109
  export function readMissingOptionValue(
82
110
  missingOptionValues: ReadonlySet<string>,
83
111
  ...keys: string[]
@@ -178,6 +206,90 @@ export function resolvePreviewApplyMode(
178
206
  };
179
207
  }
180
208
 
209
+ export function isValidCompactTempKey(value: string): value is CompactTempKey {
210
+ return /^[A-Za-z][A-Za-z0-9._-]{0,63}$/u.test(value);
211
+ }
212
+
213
+ export function parseCompactFields(rawValue: string): ParsedCompactFields {
214
+ const fields: string[] = [];
215
+ let current = "";
216
+ let escaping = false;
217
+
218
+ for (const character of rawValue) {
219
+ if (escaping) {
220
+ switch (character) {
221
+ case "|":
222
+ current += "|";
223
+ break;
224
+ case "\\":
225
+ current += "\\";
226
+ break;
227
+ case "n":
228
+ current += "\n";
229
+ break;
230
+ case "r":
231
+ current += "\r";
232
+ break;
233
+ case "t":
234
+ current += "\t";
235
+ break;
236
+ default:
237
+ return {
238
+ fields,
239
+ invalidEscape: `\\${character}`,
240
+ hasDanglingEscape: false,
241
+ };
242
+ }
243
+
244
+ escaping = false;
245
+ continue;
246
+ }
247
+
248
+ if (character === "\\") {
249
+ escaping = true;
250
+ continue;
251
+ }
252
+
253
+ if (character === "|") {
254
+ fields.push(current);
255
+ current = "";
256
+ continue;
257
+ }
258
+
259
+ current += character;
260
+ }
261
+
262
+ if (escaping) {
263
+ return {
264
+ fields,
265
+ invalidEscape: null,
266
+ hasDanglingEscape: true,
267
+ };
268
+ }
269
+
270
+ fields.push(current);
271
+ return {
272
+ fields,
273
+ invalidEscape: null,
274
+ hasDanglingEscape: false,
275
+ };
276
+ }
277
+
278
+ export function parseCompactEntityRef(rawValue: string): CompactEntityRef {
279
+ if (rawValue.startsWith(COMPACT_TEMP_KEY_PREFIX)) {
280
+ const tempKey = rawValue.slice(COMPACT_TEMP_KEY_PREFIX.length);
281
+ return {
282
+ kind: "temp_key",
283
+ tempKey,
284
+ } satisfies CompactTempKeyRef;
285
+ }
286
+
287
+ return {
288
+ kind: "id",
289
+ id: rawValue,
290
+ } satisfies CompactEntityIdRef;
291
+ }
292
+
181
293
  function levenshteinDistance(source: string, target: string): number {
182
294
  const sourceLength = source.length;
183
295
  const targetLength = target.length;
@@ -268,3 +380,7 @@ export function readEnumOption<const T extends readonly string[]>(
268
380
 
269
381
  return allowed.includes(value) ? value : undefined;
270
382
  }
383
+
384
+ export function readUnexpectedPositionals(parsed: ParsedArgs, expectedCount: number): readonly string[] {
385
+ return parsed.positional.slice(expectedCount);
386
+ }