trekoon 0.1.6 → 0.1.8

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.
@@ -101,6 +101,7 @@ Dependencies define what must be completed before a task can start. A task/subta
101
101
  trekoon dep add <source-id> <depends-on-id> --toon
102
102
  trekoon dep list <source-id> --toon
103
103
  trekoon dep remove <source-id> <depends-on-id> --toon
104
+ trekoon dep reverse <task-or-subtask-id> --toon
104
105
  ```
105
106
 
106
107
  - `<source-id>`: The task/subtask that has the dependency
@@ -127,16 +128,32 @@ The response `data.dependencies` array contains entries with:
127
128
 
128
129
  ## 3) Task Completion Flow
129
130
 
130
- ### Before Starting a Task
131
+ ### Canonical dependency-aware execution loop
131
132
 
132
- 1. Check if task has unmet dependencies:
133
+ Run this sequence every session:
134
+
135
+ 1. Sync branch/worktree status:
133
136
  ```bash
134
- trekoon dep list <task-id> --toon
137
+ trekoon sync status --toon
138
+ ```
139
+ 2. Pull deterministic ready candidates (or next candidate):
140
+ ```bash
141
+ trekoon task ready --limit 5 --toon
142
+ trekoon task next --toon
143
+ ```
144
+ 3. Inspect downstream impact before changes:
145
+ ```bash
146
+ trekoon dep reverse <task-or-subtask-id> --toon
147
+ ```
148
+ 4. Start work with explicit status updates:
149
+ ```bash
150
+ trekoon task update <task-id> --status in_progress --toon
151
+ ```
152
+ 5. Finish or block with appended context + final status:
153
+ ```bash
154
+ trekoon task update <task-id> --append "Completed implementation" --status done --toon
155
+ trekoon task update <task-id> --append "Blocked by <reason>" --status blocked --toon
135
156
  ```
136
-
137
- 2. If dependencies exist and are not `done`, complete those first
138
-
139
- 3. Only mark `in_progress` when all dependencies are `done`
140
157
 
141
158
  ### When Completing a Task
142
159
 
@@ -146,17 +163,19 @@ The response `data.dependencies` array contains entries with:
146
163
  ```
147
164
 
148
165
  2. To find the next task that was blocked by this one:
149
- - List all tasks: `trekoon task list --all --toon`
150
- - Check which tasks have dependencies on the completed task
151
- - The task(s) with all dependencies now satisfied are ready to start
166
+ - Inspect downstream nodes: `trekoon dep reverse <task-id> --toon`
167
+ - Pull ready queue: `trekoon task ready --limit 5 --toon`
168
+ - Pick one deterministically: `trekoon task next --toon`
152
169
 
153
170
  ### Finding Next Work
154
171
 
155
172
  ```bash
156
- trekoon task list --status todo --limit 20 --toon
173
+ trekoon task ready --limit 5 --toon
174
+ trekoon task next --toon
175
+ trekoon dep reverse <task-or-subtask-id> --toon
157
176
  ```
158
177
 
159
- Tasks are sorted with `in_progress` first, then `todo`. Look for tasks with no dependencies or all dependencies satisfied.
178
+ Use `task ready` for ranked candidates and `task next` for the top deterministic pick.
160
179
 
161
180
  ## 4) Load existing work first
162
181
 
@@ -173,6 +192,7 @@ trekoon task show <id> --all --toon
173
192
  - open work only (`in_progress`, `in-progress`, `todo`)
174
193
  - prioritized as `in_progress`/`in-progress` first, then `todo`
175
194
  - default limit `10`
195
+ - `--cursor <n>` is offset-like pagination for list endpoints
176
196
  - Filter list explicitly when needed:
177
197
 
178
198
  ```bash
@@ -182,6 +202,9 @@ trekoon task list --all --toon
182
202
  ```
183
203
 
184
204
  - `--all` cannot be combined with `--status` or `--limit`.
205
+ - `--all` cannot be combined with `--cursor`.
206
+ - Machine pagination contract is in `meta.pagination.hasMore` and
207
+ `meta.pagination.nextCursor`.
185
208
  - `epic show <id> --all --toon`: full epic tree (tasks + subtasks)
186
209
  - `task show <id> --all --toon`: task plus its subtasks
187
210
 
@@ -236,14 +259,15 @@ Rules:
236
259
  2. In the target repository/worktree, initialize tracker state:
237
260
 
238
261
  ```bash
239
- trekoon init
262
+ trekoon init --toon
240
263
  ```
241
264
 
242
- 3. You can always run `trekoon quickstart` or `trekoon --help` to get more information.
265
+ 3. You can always run `trekoon quickstart --toon` or `trekoon --help --toon` to
266
+ get more information.
243
267
 
244
268
  If `.trekoon/trekoon.db` is missing, initialize before any create/update commands.
245
269
 
246
270
  ## 8) Safety
247
271
 
248
272
  - Never edit `.trekoon/trekoon.db` directly.
249
- - `trekoon wipe --yes` is prohibited unless the user explicitly confirms they want a destructive wipe.
273
+ - `trekoon wipe --yes --toon` is prohibited unless the user explicitly confirms they want a destructive wipe.
package/README.md CHANGED
@@ -48,11 +48,11 @@ npm i -g trekoon
48
48
  - `trekoon init`
49
49
  - `trekoon quickstart`
50
50
  - `trekoon epic <create|list|show|update|delete>`
51
- - `trekoon task <create|list|show|update|delete>`
51
+ - `trekoon task <create|list|show|ready|next|update|delete>`
52
52
  - `trekoon subtask <create|list|update|delete>`
53
- - `trekoon dep <add|remove|list>`
53
+ - `trekoon dep <add|remove|list|reverse>`
54
54
  - `trekoon sync <status|pull|resolve>`
55
- - `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]`
55
+ - `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] [--allow-outside-repo]`
56
56
  - `trekoon skills update`
57
57
  - `trekoon wipe --yes`
58
58
 
@@ -87,8 +87,9 @@ List defaults and filters (`epic list`, `task list`, `subtask list`):
87
87
  - Default limit: `10`
88
88
  - Status filter: `--status in_progress,todo` (CSV)
89
89
  - Custom limit: `--limit <n>`
90
+ - Cursor pagination: `--cursor <n>` (offset-like start index for next page)
90
91
  - All rows and statuses: `--all`
91
- - `--all` is mutually exclusive with `--status` and `--limit`
92
+ - `--all` is mutually exclusive with `--status`, `--limit`, and `--cursor`
92
93
 
93
94
  Bulk updates (`epic update`, `task update`, `subtask update`):
94
95
 
@@ -140,16 +141,40 @@ trekoon dep add <task-id> <depends-on-id>
140
141
  trekoon dep list <task-id>
141
142
  ```
142
143
 
143
- ### 4) Use JSON or TOON output for agents
144
+ ### 4) AI execution loop for agents
145
+
146
+ Run this loop each session to pick next work deterministically:
147
+
148
+ ```bash
149
+ trekoon --toon sync status
150
+ trekoon --toon task ready --limit 5
151
+ trekoon --toon task next
152
+ trekoon --toon dep reverse <task-or-subtask-id>
153
+ trekoon --toon task update <task-id> --status in_progress
154
+ ```
155
+
156
+ When done or blocked, append context and update final status:
157
+
158
+ ```bash
159
+ trekoon --toon task update <task-id> --append "Completed implementation and checks" --status done
160
+ trekoon --toon task update <task-id> --append "Blocked by <reason>" --status blocked
161
+ ```
162
+
163
+ ### 5) Use TOON output for agent workflows
144
164
 
145
165
  ```bash
146
- trekoon --json epic show <epic-id>
147
- trekoon --json task show <task-id>
148
166
  trekoon --toon epic show <epic-id>
149
167
  trekoon --toon task show <task-id>
150
168
  ```
151
169
 
152
- ### 5) Sync workflow for worktrees
170
+ Optional alternative for integrations that explicitly require JSON:
171
+
172
+ ```bash
173
+ trekoon --json epic show <epic-id>
174
+ trekoon --json task show <task-id>
175
+ ```
176
+
177
+ ### 6) Sync workflow for worktrees
153
178
 
154
179
  - Run `trekoon sync status` at session start and before PR/merge.
155
180
  - Run `trekoon sync pull --from main` before merge to align tracker state.
@@ -161,7 +186,16 @@ trekoon sync pull --from main
161
186
  trekoon sync resolve <conflict-id> --use ours
162
187
  ```
163
188
 
164
- ### 6) Install project-local Trekoon skill for agents
189
+ `sync pull` machine output includes diagnostics counters and hints so agents can
190
+ react deterministically:
191
+
192
+ - `diagnostics.malformedPayloadEvents`
193
+ - `diagnostics.applyRejectedEvents`
194
+ - `diagnostics.quarantinedEvents`
195
+ - `diagnostics.conflictEvents`
196
+ - `diagnostics.errorHints`
197
+
198
+ ### 7) Install project-local Trekoon skill for agents
165
199
 
166
200
  `trekoon skills install` always writes the bundled skill file into the current
167
201
  repository at:
@@ -186,6 +220,10 @@ Path behavior:
186
220
  - Default pi link path: `.pi/skills/trekoon`
187
221
  - `--to <path>` overrides the editor root for link creation only.
188
222
  - `--to` does **not** move or copy `SKILL.md` to that path.
223
+ - By default, link targets must resolve inside the repository root.
224
+ - Use `--allow-outside-repo` only for intentional external links.
225
+ - When override is used, install prints a warning and includes confirmation
226
+ fields in machine output.
189
227
  - Re-running install is idempotent: it refreshes `SKILL.md` and reuses/replaces
190
228
  the same symlink target.
191
229
  - `trekoon skills update` is idempotent: it refreshes canonical
@@ -219,13 +257,89 @@ This produces:
219
257
 
220
258
  Trekoon does not mutate global editor config directories.
221
259
 
222
- ### 7) Pre-merge checklist
260
+ ### 8) Pre-merge checklist
223
261
 
224
262
  - [ ] `trekoon sync status` shows no unresolved conflicts
225
263
  - [ ] done tasks/subtasks are marked completed
226
264
  - [ ] dependency graph has no stale blockers
227
265
  - [ ] final AI check: `trekoon --toon epic show <epic-id>`
228
266
 
267
+ ## Machine-contract recipes (--toon)
268
+
269
+ Use `--toon` for production agent loops. The examples below show command +
270
+ expected envelope fields.
271
+
272
+ ### Ready queue (deterministic candidates)
273
+
274
+ ```bash
275
+ trekoon --toon task ready --limit 3
276
+ ```
277
+
278
+ Payload fields:
279
+
280
+ ```text
281
+ ok: true
282
+ command: task.ready
283
+ data:
284
+ candidates[]:
285
+ task: { id, epicId, title, status, ... }
286
+ readiness: { isReady, reason }
287
+ blockerSummary: { blockedByCount, totalDependencies, blockedBy[] }
288
+ ranking: { rank, blockerCount, statusPriority }
289
+ blocked[]: (same shape, non-ready items)
290
+ summary: {
291
+ totalOpenTasks,
292
+ readyCount,
293
+ returnedCount,
294
+ appliedLimit,
295
+ blockedCount,
296
+ unresolvedDependencyCount,
297
+ }
298
+ ```
299
+
300
+ ### Reverse dependency walk (blocker impact)
301
+
302
+ ```bash
303
+ trekoon --toon dep reverse <task-or-subtask-id>
304
+ ```
305
+
306
+ Payload fields:
307
+
308
+ ```text
309
+ ok: true
310
+ command: dep.reverse
311
+ data:
312
+ targetId: <id>
313
+ targetKind: task|subtask
314
+ blockedNodes[]: { id, kind, distance, isDirect }
315
+ ```
316
+
317
+ ### Pagination contract for machine list calls
318
+
319
+ ```bash
320
+ trekoon --toon task list --status todo --limit 2
321
+ trekoon --toon task list --status todo --limit 2 --cursor 2
322
+ ```
323
+
324
+ Cursor semantics:
325
+
326
+ - `--cursor <n>` is offset-like pagination for list endpoints (`epic list`,
327
+ `task list`, `subtask list`).
328
+ - Do not combine `--all` with `--cursor`.
329
+ - Machine consumers should page using `meta.pagination.hasMore` and
330
+ `meta.pagination.nextCursor`.
331
+
332
+ Payload fields:
333
+
334
+ ```text
335
+ ok: true
336
+ command: task.list
337
+ data:
338
+ tasks[]: ...
339
+ meta:
340
+ pagination: { hasMore, nextCursor }
341
+ ```
342
+
229
343
  ## Implementation principles
230
344
 
231
345
  - Minimal, composable modules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -79,6 +79,19 @@ export function parseStrictPositiveInt(rawValue: string | undefined): number | u
79
79
  return parsed;
80
80
  }
81
81
 
82
+ export function parseStrictNonNegativeInt(rawValue: string | undefined): number | undefined {
83
+ if (rawValue === undefined) {
84
+ return undefined;
85
+ }
86
+
87
+ const parsed = Number.parseInt(rawValue, 10);
88
+ if (!Number.isInteger(parsed) || parsed < 0 || `${parsed}` !== rawValue.trim()) {
89
+ return Number.NaN;
90
+ }
91
+
92
+ return parsed;
93
+ }
94
+
82
95
  export function readEnumOption<const T extends readonly string[]>(
83
96
  options: ReadonlyMap<string, string>,
84
97
  allowed: T,
@@ -86,10 +86,29 @@ export async function runDep(context: CliContext): Promise<CliResult> {
86
86
  },
87
87
  });
88
88
  }
89
+ case "reverse": {
90
+ const targetKind = domain.resolveNodeKind(sourceId);
91
+ const blockedNodes = domain.listReverseDependencies(sourceId);
92
+
93
+ return okResult({
94
+ command: "dep.reverse",
95
+ human:
96
+ blockedNodes.length === 0
97
+ ? `No downstream blockers for ${sourceId}`
98
+ : blockedNodes
99
+ .map((item) => `${item.id} (${item.kind}, distance=${item.distance})`)
100
+ .join("\n"),
101
+ data: {
102
+ targetId: sourceId,
103
+ targetKind,
104
+ blockedNodes,
105
+ },
106
+ });
107
+ }
89
108
  default:
90
109
  return failResult({
91
110
  command: "dep",
92
- human: "Usage: trekoon dep <add|remove|list>",
111
+ human: "Usage: trekoon dep <add|remove|list|reverse>",
93
112
  data: {
94
113
  args: context.args,
95
114
  },
@@ -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,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
332
396
  command: "epic.list",
333
397
  human,
334
398
  data: { epics },
399
+ ...(context.mode === "human" ? {} : { meta: { pagination: listed.pagination } }),
335
400
  });
336
401
  }
337
402
  case "show": {