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.
@@ -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
@@ -101,6 +131,7 @@ Dependencies define what must be completed before a task can start. A task/subta
101
131
  trekoon dep add <source-id> <depends-on-id> --toon
102
132
  trekoon dep list <source-id> --toon
103
133
  trekoon dep remove <source-id> <depends-on-id> --toon
134
+ trekoon dep reverse <task-or-subtask-id> --toon
104
135
  ```
105
136
 
106
137
  - `<source-id>`: The task/subtask that has the dependency
@@ -127,16 +158,32 @@ The response `data.dependencies` array contains entries with:
127
158
 
128
159
  ## 3) Task Completion Flow
129
160
 
130
- ### Before Starting a Task
161
+ ### Canonical dependency-aware execution loop
131
162
 
132
- 1. Check if task has unmet dependencies:
163
+ Run this sequence every session:
164
+
165
+ 1. Sync branch/worktree status:
133
166
  ```bash
134
- trekoon dep list <task-id> --toon
167
+ trekoon sync status --toon
168
+ ```
169
+ 2. Pull deterministic ready candidates (or next candidate):
170
+ ```bash
171
+ trekoon task ready --limit 5 --toon
172
+ trekoon task next --toon
173
+ ```
174
+ 3. Inspect downstream impact before changes:
175
+ ```bash
176
+ trekoon dep reverse <task-or-subtask-id> --toon
177
+ ```
178
+ 4. Start work with explicit status updates:
179
+ ```bash
180
+ trekoon task update <task-id> --status in_progress --toon
181
+ ```
182
+ 5. Finish or block with appended context + final status:
183
+ ```bash
184
+ trekoon task update <task-id> --append "Completed implementation" --status done --toon
185
+ trekoon task update <task-id> --append "Blocked by <reason>" --status blocked --toon
135
186
  ```
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
187
 
141
188
  ### When Completing a Task
142
189
 
@@ -146,17 +193,19 @@ The response `data.dependencies` array contains entries with:
146
193
  ```
147
194
 
148
195
  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
196
+ - Inspect downstream nodes: `trekoon dep reverse <task-id> --toon`
197
+ - Pull ready queue: `trekoon task ready --limit 5 --toon`
198
+ - Pick one deterministically: `trekoon task next --toon`
152
199
 
153
200
  ### Finding Next Work
154
201
 
155
202
  ```bash
156
- trekoon task list --status todo --limit 20 --toon
203
+ trekoon task ready --limit 5 --toon
204
+ trekoon task next --toon
205
+ trekoon dep reverse <task-or-subtask-id> --toon
157
206
  ```
158
207
 
159
- Tasks are sorted with `in_progress` first, then `todo`. Look for tasks with no dependencies or all dependencies satisfied.
208
+ Use `task ready` for ranked candidates and `task next` for the top deterministic pick.
160
209
 
161
210
  ## 4) Load existing work first
162
211
 
@@ -173,6 +222,7 @@ trekoon task show <id> --all --toon
173
222
  - open work only (`in_progress`, `in-progress`, `todo`)
174
223
  - prioritized as `in_progress`/`in-progress` first, then `todo`
175
224
  - default limit `10`
225
+ - `--cursor <n>` is offset-like pagination for list endpoints
176
226
  - Filter list explicitly when needed:
177
227
 
178
228
  ```bash
@@ -182,9 +232,24 @@ trekoon task list --all --toon
182
232
  ```
183
233
 
184
234
  - `--all` cannot be combined with `--status` or `--limit`.
235
+ - `--all` cannot be combined with `--cursor`.
236
+ - Machine pagination contract is in `meta.pagination.hasMore` and
237
+ `meta.pagination.nextCursor`.
238
+ - Machine list/show responses may also include:
239
+ - `meta.defaults`
240
+ - `meta.filters`
241
+ - `meta.truncation`
185
242
  - `epic show <id> --all --toon`: full epic tree (tasks + subtasks)
186
243
  - `task show <id> --all --toon`: task plus its subtasks
187
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
+
188
253
  ### View Options
189
254
 
190
255
  | Command | `--view` options |
@@ -236,14 +301,15 @@ Rules:
236
301
  2. In the target repository/worktree, initialize tracker state:
237
302
 
238
303
  ```bash
239
- trekoon init
304
+ trekoon init --toon
240
305
  ```
241
306
 
242
- 3. You can always run `trekoon quickstart` or `trekoon --help` to get more information.
307
+ 3. You can always run `trekoon quickstart --toon` or `trekoon --help --toon` to
308
+ get more information.
243
309
 
244
310
  If `.trekoon/trekoon.db` is missing, initialize before any create/update commands.
245
311
 
246
312
  ## 8) Safety
247
313
 
248
314
  - Never edit `.trekoon/trekoon.db` directly.
249
- - `trekoon wipe --yes` is prohibited unless the user explicitly confirms they want a destructive wipe.
315
+ - `trekoon wipe --yes --toon` is prohibited unless the user explicitly confirms they want a destructive wipe.
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
- - `trekoon task <create|list|show|update|delete>`
52
+ - `trekoon task <create|list|show|ready|next|update|delete>`
52
53
  - `trekoon subtask <create|list|update|delete>`
53
- - `trekoon dep <add|remove|list>`
54
- - `trekoon sync <status|pull|resolve>`
54
+ - `trekoon dep <add|remove|list|reverse>`
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
 
@@ -87,8 +92,9 @@ List defaults and filters (`epic list`, `task list`, `subtask list`):
87
92
  - Default limit: `10`
88
93
  - Status filter: `--status in_progress,todo` (CSV)
89
94
  - Custom limit: `--limit <n>`
95
+ - Cursor pagination: `--cursor <n>` (offset-like start index for next page)
90
96
  - All rows and statuses: `--all`
91
- - `--all` is mutually exclusive with `--status` and `--limit`
97
+ - `--all` is mutually exclusive with `--status`, `--limit`, and `--cursor`
92
98
 
93
99
  Bulk updates (`epic update`, `task update`, `subtask update`):
94
100
 
@@ -111,8 +117,15 @@ trekoon epic update --ids <epic-1>,<epic-2> --status done
111
117
 
112
118
  ## Quickstart
113
119
 
114
- Trekoon is local-first: each worktree uses its own `.trekoon/trekoon.db`.
115
- 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.
116
129
 
117
130
  ### 1) Initialize
118
131
 
@@ -140,16 +153,40 @@ trekoon dep add <task-id> <depends-on-id>
140
153
  trekoon dep list <task-id>
141
154
  ```
142
155
 
143
- ### 4) Use JSON or TOON output for agents
156
+ ### 4) AI execution loop for agents
157
+
158
+ Run this loop each session to pick next work deterministically:
159
+
160
+ ```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
+ trekoon --toon task update <task-id> --status in_progress
166
+ ```
167
+
168
+ When done or blocked, append context and update final status:
169
+
170
+ ```bash
171
+ 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
+ ```
174
+
175
+ ### 5) Use TOON output for agent workflows
144
176
 
145
177
  ```bash
146
- trekoon --json epic show <epic-id>
147
- trekoon --json task show <task-id>
148
178
  trekoon --toon epic show <epic-id>
149
179
  trekoon --toon task show <task-id>
150
180
  ```
151
181
 
152
- ### 5) Sync workflow for worktrees
182
+ Optional alternative for integrations that explicitly require JSON:
183
+
184
+ ```bash
185
+ trekoon --json epic show <epic-id>
186
+ trekoon --json task show <task-id>
187
+ ```
188
+
189
+ ### 6) Sync workflow for worktrees
153
190
 
154
191
  - Run `trekoon sync status` at session start and before PR/merge.
155
192
  - Run `trekoon sync pull --from main` before merge to align tracker state.
@@ -158,6 +195,8 @@ trekoon --toon task show <task-id>
158
195
  ```bash
159
196
  trekoon sync status
160
197
  trekoon sync pull --from main
198
+ trekoon sync conflicts list
199
+ trekoon sync conflicts show <conflict-id>
161
200
  trekoon sync resolve <conflict-id> --use ours
162
201
  ```
163
202
 
@@ -170,10 +209,31 @@ react deterministically:
170
209
  - `diagnostics.conflictEvents`
171
210
  - `diagnostics.errorHints`
172
211
 
173
- ### 6) Install project-local Trekoon skill for agents
212
+ Compatibility mode for legacy sync command IDs:
174
213
 
175
- `trekoon skills install` always writes the bundled skill file into the current
176
- repository at:
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
+
233
+ ### 7) Install project-local Trekoon skill for agents
234
+
235
+ `trekoon skills install` always writes the bundled skill file under the current
236
+ working directory at:
177
237
 
178
238
  - `.agents/skills/trekoon/SKILL.md`
179
239
 
@@ -195,7 +255,7 @@ Path behavior:
195
255
  - Default pi link path: `.pi/skills/trekoon`
196
256
  - `--to <path>` overrides the editor root for link creation only.
197
257
  - `--to` does **not** move or copy `SKILL.md` to that path.
198
- - By default, link targets must resolve inside the repository root.
258
+ - By default, link targets must resolve inside the current working directory root.
199
259
  - Use `--allow-outside-repo` only for intentional external links.
200
260
  - When override is used, install prints a warning and includes confirmation
201
261
  fields in machine output.
@@ -212,11 +272,11 @@ Path behavior:
212
272
  How `--to` works (step-by-step):
213
273
 
214
274
  1. Trekoon always installs/copies to:
215
- - `<repo>/.agents/skills/trekoon/SKILL.md`
275
+ - `<cwd>/.agents/skills/trekoon/SKILL.md`
216
276
  2. If `--link` is present, Trekoon creates a `trekoon` symlink directory entry.
217
277
  3. `--to <path>` sets the symlink root directory.
218
278
  4. Final link path is:
219
- - `<resolved-to-path>/trekoon -> <repo>/.agents/skills/trekoon`
279
+ - `<resolved-to-path>/trekoon -> <cwd>/.agents/skills/trekoon`
220
280
 
221
281
  Example:
222
282
 
@@ -226,19 +286,119 @@ trekoon skills install --link --editor opencode --to ./.custom-editor/skills
226
286
 
227
287
  This produces:
228
288
 
229
- - `<repo>/.agents/skills/trekoon/SKILL.md` (copied file)
230
- - `<repo>/.custom-editor/skills/trekoon` (symlink)
231
- - 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`
232
292
 
233
293
  Trekoon does not mutate global editor config directories.
234
294
 
235
- ### 7) Pre-merge checklist
295
+ ### 8) Pre-merge checklist
236
296
 
237
297
  - [ ] `trekoon sync status` shows no unresolved conflicts
238
298
  - [ ] done tasks/subtasks are marked completed
239
299
  - [ ] dependency graph has no stale blockers
240
300
  - [ ] final AI check: `trekoon --toon epic show <epic-id>`
241
301
 
302
+ ## Machine-contract recipes (--toon)
303
+
304
+ Use `--toon` for production agent loops. The examples below show command +
305
+ expected envelope fields.
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
+
328
+ ### Ready queue (deterministic candidates)
329
+
330
+ ```bash
331
+ trekoon --toon task ready --limit 3
332
+ ```
333
+
334
+ Payload fields:
335
+
336
+ ```text
337
+ ok: true
338
+ command: task.ready
339
+ data:
340
+ candidates[]:
341
+ task: { id, epicId, title, status, ... }
342
+ readiness: { isReady, reason }
343
+ blockerSummary: { blockedByCount, totalDependencies, blockedBy[] }
344
+ ranking: { rank, blockerCount, statusPriority }
345
+ blocked[]: (same shape, non-ready items)
346
+ summary: {
347
+ totalOpenTasks,
348
+ readyCount,
349
+ returnedCount,
350
+ appliedLimit,
351
+ blockedCount,
352
+ unresolvedDependencyCount,
353
+ }
354
+ ```
355
+
356
+ ### Reverse dependency walk (blocker impact)
357
+
358
+ ```bash
359
+ trekoon --toon dep reverse <task-or-subtask-id>
360
+ ```
361
+
362
+ Payload fields:
363
+
364
+ ```text
365
+ ok: true
366
+ command: dep.reverse
367
+ data:
368
+ targetId: <id>
369
+ targetKind: task|subtask
370
+ blockedNodes[]: { id, kind, distance, isDirect }
371
+ ```
372
+
373
+ ### Pagination contract for machine list calls
374
+
375
+ ```bash
376
+ trekoon --toon task list --status todo --limit 2
377
+ trekoon --toon task list --status todo --limit 2 --cursor 2
378
+ ```
379
+
380
+ Cursor semantics:
381
+
382
+ - `--cursor <n>` is offset-like pagination for list endpoints (`epic list`,
383
+ `task list`, `subtask list`).
384
+ - Do not combine `--all` with `--cursor`.
385
+ - Machine consumers should page using `meta.pagination.hasMore` and
386
+ `meta.pagination.nextCursor`.
387
+
388
+ Payload fields:
389
+
390
+ ```text
391
+ ok: true
392
+ command: task.list
393
+ data:
394
+ tasks[]: ...
395
+ metadata:
396
+ contractVersion: "1.0.0"
397
+ requestId: req-<stable-id>
398
+ meta:
399
+ pagination: { hasMore, nextCursor }
400
+ ```
401
+
242
402
  ## Implementation principles
243
403
 
244
404
  - Minimal, composable modules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.7",
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
 
@@ -79,6 +83,97 @@ export function parseStrictPositiveInt(rawValue: string | undefined): number | u
79
83
  return parsed;
80
84
  }
81
85
 
86
+ export function parseStrictNonNegativeInt(rawValue: string | undefined): number | undefined {
87
+ if (rawValue === undefined) {
88
+ return undefined;
89
+ }
90
+
91
+ const parsed = Number.parseInt(rawValue, 10);
92
+ if (!Number.isInteger(parsed) || parsed < 0 || `${parsed}` !== rawValue.trim()) {
93
+ return Number.NaN;
94
+ }
95
+
96
+ return parsed;
97
+ }
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
+
82
177
  export function readEnumOption<const T extends readonly string[]>(
83
178
  options: ReadonlyMap<string, string>,
84
179
  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
  },