trekoon 0.1.9 → 0.2.1

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
@@ -48,10 +48,10 @@ npm i -g trekoon
48
48
  - `trekoon init`
49
49
  - `trekoon help [command]`
50
50
  - `trekoon quickstart`
51
- - `trekoon epic <create|list|show|update|delete>`
52
- - `trekoon task <create|list|show|ready|next|update|delete>`
53
- - `trekoon subtask <create|list|update|delete>`
54
- - `trekoon dep <add|remove|list|reverse>`
51
+ - `trekoon epic <create|expand|list|show|search|replace|update|delete>`
52
+ - `trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete>`
53
+ - `trekoon subtask <create|create-many|list|search|replace|update|delete>`
54
+ - `trekoon dep <add|add-many|remove|list|reverse>`
55
55
  - `trekoon events prune [--dry-run] [--archive] [--retention-days <n>]`
56
56
  - `trekoon migrate <status|rollback> [--to-version <n>]`
57
57
  - `trekoon sync <status|pull|resolve|conflicts>`
@@ -146,6 +146,42 @@ trekoon task list --limit 25
146
146
  trekoon task list --all --view compact
147
147
  ```
148
148
 
149
+ ### 2a) Preferred one-shot epic creation
150
+
151
+ When you already know the epic tree, create the epic, tasks, subtasks, and
152
+ dependencies in one invocation.
153
+
154
+ ```bash
155
+ trekoon epic create \
156
+ --title "Batch command rollout" \
157
+ --description "Ship one-shot planning workflows" \
158
+ --task "task-a|First task|First description|todo" \
159
+ --task "task-b|Second task|Second description|todo" \
160
+ --subtask "@task-a|sub-a|First subtask|Subtask description|todo" \
161
+ --dep "@task-b|@task-a" \
162
+ --dep "@sub-a|@task-a"
163
+ ```
164
+
165
+ Use this when:
166
+
167
+ - the epic does not exist yet
168
+ - later records need to reference earlier created records via `@temp-key`
169
+ - you want one atomic create step and one machine response with mappings/counts
170
+
171
+ Compact machine output adds:
172
+
173
+ ```text
174
+ command: epic.create
175
+ data:
176
+ epic: created epic row
177
+ tasks[]: created tasks in input order
178
+ subtasks[]: created subtasks in input order
179
+ dependencies[]: created dependencies in input order
180
+ result:
181
+ mappings[]: { kind: task|subtask, tempKey, id }
182
+ counts: { tasks, subtasks, dependencies }
183
+ ```
184
+
149
185
  ### 3) Add dependencies
150
186
 
151
187
  ```bash
@@ -153,6 +189,181 @@ trekoon dep add <task-id> <depends-on-id>
153
189
  trekoon dep list <task-id>
154
190
  ```
155
191
 
192
+ ### 3a) Batch planning commands
193
+
194
+ Use compact batch commands when one invocation needs to create or link multiple
195
+ items atomically. Use the single-item commands when you already have persisted
196
+ UUIDs and only need one mutation.
197
+
198
+ #### `task create-many`
199
+
200
+ Create multiple tasks under one epic in declared order.
201
+
202
+ ```bash
203
+ trekoon task create-many \
204
+ --epic <epic-id> \
205
+ --task "seed-api|Design API|Define batch grammar|todo" \
206
+ --task "seed-cli|Wire CLI|Hook parser and output|in_progress"
207
+ ```
208
+
209
+ Compact spec:
210
+
211
+ - `--task <temp-key>|<title>|<description>|<status>`
212
+ - escape `\|`, `\\`, `\n`, `\r`, `\t`
213
+ - repeated `--task` flags are preserved in the exact order provided
214
+ - temp keys are local mapping labels, not persisted IDs
215
+
216
+ Rollback semantics:
217
+
218
+ - Trekoon validates the full batch before inserts
219
+ - duplicate temp keys, empty required fields, or invalid input fail the whole
220
+ command
221
+ - no partial task rows are kept on failure
222
+
223
+ Compact machine output:
224
+
225
+ ```text
226
+ command: task.create-many
227
+ data:
228
+ epicId: <epic-id>
229
+ tasks[]: created task rows in input order
230
+ result:
231
+ mappings[]: { kind: task, tempKey, id }
232
+ ```
233
+
234
+ #### `subtask create-many`
235
+
236
+ Create multiple subtasks under one existing task.
237
+
238
+ ```bash
239
+ trekoon subtask create-many <task-id> \
240
+ --subtask "seed-tests|Write tests|Cover happy path|todo" \
241
+ --subtask "seed-docs|Document flow|Add operator notes|todo"
242
+ ```
243
+
244
+ Equivalent explicit parent form:
245
+
246
+ ```bash
247
+ trekoon subtask create-many \
248
+ --task <task-id> \
249
+ --subtask "seed-tests|Write tests|Cover happy path|todo"
250
+ ```
251
+
252
+ Rules:
253
+
254
+ - positional `<task-id>` or `--task <task-id>` may be used
255
+ - if both are provided, they must be identical or the command fails
256
+ - repeated `--subtask` flags are applied in declared order
257
+
258
+ Rollback semantics:
259
+
260
+ - full batch prevalidation happens before inserts
261
+ - duplicate temp keys, conflicting task ids, or invalid specs abort the whole
262
+ command
263
+ - no partial subtasks are kept on failure
264
+
265
+ Compact machine output:
266
+
267
+ ```text
268
+ command: subtask.create-many
269
+ data:
270
+ taskId: <task-id>
271
+ subtasks[]: created subtask rows in input order
272
+ result:
273
+ mappings[]: { kind: subtask, tempKey, id }
274
+ ```
275
+
276
+ #### `dep add-many`
277
+
278
+ Create multiple dependency edges in one ordered, transactional operation.
279
+
280
+ ```bash
281
+ trekoon dep add-many \
282
+ --dep "<task-b>|<task-a>" \
283
+ --dep "<subtask-c>|<task-b>"
284
+ ```
285
+
286
+ Compact spec:
287
+
288
+ - `--dep <source-ref>|<depends-on-ref>`
289
+ - repeated `--dep` flags are applied in declared order
290
+ - standalone `dep add-many` resolves persisted IDs only
291
+ - `@temp-key` refs are **not** resolved from earlier commands; they are reserved
292
+ for same-invocation workflows such as `epic expand`
293
+
294
+ Rollback semantics:
295
+
296
+ - validation covers the full dependency set before insert
297
+ - missing ids, unresolved `@temp-key` refs, duplicates, or cycles fail the whole
298
+ batch
299
+ - no partial dependency edges are inserted on failure
300
+
301
+ Compact machine output:
302
+
303
+ ```text
304
+ command: dep.add-many
305
+ data:
306
+ dependencies[]: created dependency rows in input order
307
+ result:
308
+ mappings[]: []
309
+ ```
310
+
311
+ #### `epic expand`
312
+
313
+ Expand one existing epic by creating tasks, subtasks, and dependencies in one
314
+ transaction. Use this when the epic already exists and you want to add a linked
315
+ batch later.
316
+
317
+ ```bash
318
+ trekoon epic expand <epic-id> \
319
+ --task "task-api|Design API|Define compact grammar|todo" \
320
+ --task "task-cli|Wire CLI|Hook parser and output|todo" \
321
+ --subtask "@task-api|sub-tests|Write tests|Cover parser cases|todo" \
322
+ --dep "@task-cli|@task-api" \
323
+ --dep "@sub-tests|@task-api"
324
+ ```
325
+
326
+ Compact specs:
327
+
328
+ - `--task <temp-key>|<title>|<description>|<status>`
329
+ - `--subtask <parent-ref>|<temp-key>|<title>|<description>|<status>`
330
+ - `--dep <source-ref>|<depends-on-ref>`
331
+ - `@temp-key` refs may target tasks/subtasks declared earlier in the same
332
+ `epic expand` invocation
333
+
334
+ Background phases:
335
+
336
+ 1. validate all compact specs and duplicate temp keys
337
+ 2. create tasks transactionally
338
+ 3. resolve subtask parent temp keys and create subtasks
339
+ 4. resolve dependency refs and link dependencies
340
+ 5. append task, subtask, then dependency events
341
+ 6. roll back the full expansion if any phase fails
342
+
343
+ Compact machine output:
344
+
345
+ ```text
346
+ command: epic.expand
347
+ data:
348
+ epicId: <epic-id>
349
+ tasks[]: created tasks in input order
350
+ subtasks[]: created subtasks in input order
351
+ dependencies[]: created dependencies in input order
352
+ result:
353
+ mappings[]: { kind: task|subtask, tempKey, id }
354
+ counts: { tasks, subtasks, dependencies }
355
+ ```
356
+
357
+ When to choose which command:
358
+
359
+ - use `task create-many` for sibling tasks under one known epic
360
+ - use `subtask create-many` for sibling subtasks under one known task
361
+ - use `dep add-many` only when every endpoint already has a persisted ID
362
+ - use `epic create` with batch specs when the epic does not exist yet and the
363
+ whole graph is known up front
364
+ - use `epic expand` when the epic already exists and one batch must add linked
365
+ tasks/subtasks/dependencies with `@temp-key` references
366
+
156
367
  ### 4) AI execution loop for agents
157
368
 
158
369
  Run this loop each session to pick next work deterministically:
@@ -186,7 +397,88 @@ trekoon --json epic show <epic-id>
186
397
  trekoon --json task show <task-id>
187
398
  ```
188
399
 
189
- ### 6) Sync workflow for worktrees
400
+ ### 6) Scoped search for repeated text
401
+
402
+ Use scoped search before manual tree reads when you need to locate repeated
403
+ paths, labels, or migration targets.
404
+
405
+ ```bash
406
+ trekoon --toon epic search <epic-id> "path/to/somewhere"
407
+ trekoon --toon task search <task-id> "path/to/somewhere"
408
+ trekoon --toon subtask search <subtask-id> "path/to/somewhere"
409
+ ```
410
+
411
+ Scope rules:
412
+
413
+ - `epic search` scans the epic title/description plus every task and subtask
414
+ title/description in that epic tree.
415
+ - `task search` scans the task title/description plus descendant subtask
416
+ title/description.
417
+ - `subtask search` scans only that subtask's title/description.
418
+ - Add `--fields title`, `--fields description`, or
419
+ `--fields title,description` when you need a narrower scan.
420
+
421
+ ### 7) Preview first, then apply scoped replace
422
+
423
+ Use search first to confirm the scope, then run replace in preview mode, and
424
+ only use `--apply` after the preview matches the intended migration.
425
+
426
+ ```bash
427
+ trekoon --toon epic search <epic-id> "path/to/somewhere"
428
+ trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path"
429
+ trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply
430
+ ```
431
+
432
+ Use this loop for low-risk agent workflows:
433
+
434
+ 1. `search` when you need the smallest possible read before deciding whether a
435
+ migration is needed.
436
+ 2. preview `replace` to verify the exact candidate set and changed fields.
437
+ 3. `replace --apply` only after the preview output matches the intended scope.
438
+
439
+ Epic-scoped replace applies across the epic title/description and every task and
440
+ subtask title/description in that epic tree.
441
+
442
+ Compact TOON expectations for agents:
443
+
444
+ ```text
445
+ ok: true
446
+ command: epic.search
447
+ data:
448
+ scope: epic
449
+ query: { search, fields[], mode: preview }
450
+ matches[]: { kind, id, fields[]: { field, count, snippet } }
451
+ summary: { matchedEntities, matchedFields, totalMatches }
452
+ metadata:
453
+ contractVersion: 1.0.0
454
+ requestId: req-<id>
455
+ ```
456
+
457
+ ```text
458
+ ok: true
459
+ command: epic.replace
460
+ data:
461
+ scope: epic
462
+ query: { search, replace, fields[], mode: preview|apply }
463
+ matches[]: { kind, id, fields[]: { field, count, snippet } }
464
+ summary: { matchedEntities, matchedFields, totalMatches, mode }
465
+ metadata:
466
+ contractVersion: 1.0.0
467
+ requestId: req-<id>
468
+ ```
469
+
470
+ Background behavior:
471
+
472
+ - `epic search` and preview `epic replace` traverse the epic first, then
473
+ descendant tasks, then descendant subtasks.
474
+ - Within each record, Trekoon checks `title` before `description` so output stays
475
+ deterministic and low-token.
476
+ - Preview reports the candidate set without mutating records.
477
+ - `--apply` reuses the same scoped traversal, updates only rows with real text
478
+ changes, and returns the matched rows with `query.mode` and `summary.mode`
479
+ set to `"apply"`.
480
+
481
+ ### 8) Sync workflow for worktrees
190
482
 
191
483
  - Run `trekoon sync status` at session start and before PR/merge.
192
484
  - Run `trekoon sync pull --from main` before merge to align tracker state.
@@ -230,7 +522,7 @@ Behavior:
230
522
  - Migration path: remove `--compat legacy-sync-command-ids` and consume dotted
231
523
  command IDs directly.
232
524
 
233
- ### 7) Install project-local Trekoon skill for agents
525
+ ### 9) Install project-local Trekoon skill for agents
234
526
 
235
527
  `trekoon skills install` always writes the bundled skill file under the current
236
528
  working directory at:
@@ -292,7 +584,7 @@ This produces:
292
584
 
293
585
  Trekoon does not mutate global editor config directories.
294
586
 
295
- ### 8) Pre-merge checklist
587
+ ### 10) Pre-merge checklist
296
588
 
297
589
  - [ ] `trekoon sync status` shows no unresolved conflicts
298
590
  - [ ] done tasks/subtasks are marked completed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,16 +1,52 @@
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
+
23
+ export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
24
+
25
+ export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
26
+
27
+ export interface ParsedCsvEnumOption<T extends string> {
28
+ readonly values: readonly T[];
29
+ readonly invalidValues: readonly string[];
30
+ readonly empty: boolean;
31
+ }
32
+
33
+ export interface PreviewApplyModeSelection {
34
+ readonly mode: "preview" | "apply";
35
+ readonly conflict: boolean;
36
+ }
37
+
38
+ export interface ParsedCompactFields {
39
+ readonly fields: readonly string[];
40
+ readonly invalidEscape: string | null;
41
+ readonly hasDanglingEscape: boolean;
42
+ }
43
+
9
44
  const LONG_PREFIX = "--";
10
45
 
11
46
  export function parseArgs(args: readonly string[]): ParsedArgs {
12
47
  const positional: string[] = [];
13
48
  const options = new Map<string, string>();
49
+ const optionEntries: ParsedOptionEntry[] = [];
14
50
  const flags = new Set<string>();
15
51
  const missingOptionValues = new Set<string>();
16
52
  const providedOptions: string[] = [];
@@ -36,12 +72,14 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
36
72
  }
37
73
 
38
74
  options.set(key, value);
75
+ optionEntries.push({ key, value });
39
76
  index += 1;
40
77
  }
41
78
 
42
79
  return {
43
80
  positional,
44
81
  options,
82
+ optionEntries,
45
83
  flags,
46
84
  missingOptionValues,
47
85
  providedOptions,
@@ -63,6 +101,11 @@ export function hasFlag(flags: ReadonlySet<string>, ...keys: string[]): boolean
63
101
  return keys.some((key) => flags.has(key));
64
102
  }
65
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
+
66
109
  export function readMissingOptionValue(
67
110
  missingOptionValues: ReadonlySet<string>,
68
111
  ...keys: string[]
@@ -96,6 +139,157 @@ export function parseStrictNonNegativeInt(rawValue: string | undefined): number
96
139
  return parsed;
97
140
  }
98
141
 
142
+ export function parseCsvOption(rawValue: string | undefined): string[] | undefined {
143
+ if (rawValue === undefined) {
144
+ return undefined;
145
+ }
146
+
147
+ return rawValue
148
+ .split(",")
149
+ .map((value) => value.trim())
150
+ .filter((value) => value.length > 0);
151
+ }
152
+
153
+ export function parseCsvEnumOption<const T extends readonly string[]>(
154
+ rawValue: string | undefined,
155
+ allowed: T,
156
+ ): ParsedCsvEnumOption<T[number]> {
157
+ const values = parseCsvOption(rawValue);
158
+ if (values === undefined) {
159
+ return {
160
+ values: [...allowed],
161
+ invalidValues: [],
162
+ empty: false,
163
+ };
164
+ }
165
+
166
+ if (values.length === 0) {
167
+ return {
168
+ values: [...allowed],
169
+ invalidValues: [],
170
+ empty: true,
171
+ };
172
+ }
173
+
174
+ const allowedValues = new Set<string>(allowed);
175
+ const validValues: T[number][] = [];
176
+ const invalidValues: string[] = [];
177
+
178
+ for (const value of values) {
179
+ if (!allowedValues.has(value)) {
180
+ invalidValues.push(value);
181
+ continue;
182
+ }
183
+
184
+ if (!validValues.includes(value as T[number])) {
185
+ validValues.push(value as T[number]);
186
+ }
187
+ }
188
+
189
+ return {
190
+ values: validValues.length > 0 ? validValues : [...allowed],
191
+ invalidValues,
192
+ empty: false,
193
+ };
194
+ }
195
+
196
+ export function resolvePreviewApplyMode(
197
+ flags: ReadonlySet<string>,
198
+ previewKey = "preview",
199
+ applyKey = "apply",
200
+ ): PreviewApplyModeSelection {
201
+ const preview = flags.has(previewKey);
202
+ const apply = flags.has(applyKey);
203
+ return {
204
+ mode: apply ? "apply" : "preview",
205
+ conflict: preview && apply,
206
+ };
207
+ }
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
+
99
293
  function levenshteinDistance(source: string, target: string): number {
100
294
  const sourceLength = source.length;
101
295
  const targetLength = target.length;
@@ -186,3 +380,7 @@ export function readEnumOption<const T extends readonly string[]>(
186
380
 
187
381
  return allowed.includes(value) ? value : undefined;
188
382
  }
383
+
384
+ export function readUnexpectedPositionals(parsed: ParsedArgs, expectedCount: number): readonly string[] {
385
+ return parsed.positional.slice(expectedCount);
386
+ }