trekoon 0.1.8 → 0.2.0
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/.agents/skills/trekoon/SKILL.md +117 -2
- package/README.md +158 -18
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +164 -0
- package/src/commands/epic.ts +256 -3
- package/src/commands/help.ts +45 -4
- package/src/commands/subtask.ts +209 -3
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +257 -3
- package/src/domain/mutation-service.ts +242 -1
- package/src/domain/tracker-domain.ts +171 -0
- package/src/domain/types.ts +27 -0
- package/src/index.ts +1 -1
- package/src/io/output.ts +98 -5
- package/src/runtime/cli-shell.ts +159 -22
- package/src/runtime/command-types.ts +18 -0
- package/src/storage/path.ts +58 -1
- package/src/sync/event-writes.ts +21 -1
|
@@ -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 |
|
|
@@ -253,7 +295,80 @@ Rules:
|
|
|
253
295
|
- In bulk mode, do not pass a positional ID.
|
|
254
296
|
- Bulk update supports `--append` and/or `--status`.
|
|
255
297
|
|
|
256
|
-
## 7)
|
|
298
|
+
## 7) Scoped search/replace recipes for agents
|
|
299
|
+
|
|
300
|
+
Use scoped search/replace instead of repeated `show` scans when you need to
|
|
301
|
+
locate or migrate repeated text inside one issue tree.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
trekoon epic search <epic-id> "path/to/somewhere" --toon
|
|
305
|
+
trekoon task search <task-id> "path/to/somewhere" --toon
|
|
306
|
+
trekoon subtask search <subtask-id> "path/to/somewhere" --toon
|
|
307
|
+
|
|
308
|
+
trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --toon
|
|
309
|
+
trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply --toon
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Guardrails:
|
|
313
|
+
|
|
314
|
+
- Use `search` first when you only need to confirm whether the text exists.
|
|
315
|
+
- Use preview `replace` next to confirm the exact candidate set.
|
|
316
|
+
- Use `--apply` only after preview matches the intended scope.
|
|
317
|
+
- Prefer the narrowest root that satisfies the task: `subtask` → `task` →
|
|
318
|
+
`epic`.
|
|
319
|
+
- Keep prompts deterministic: literal search text, explicit IDs, no regex
|
|
320
|
+
assumptions.
|
|
321
|
+
|
|
322
|
+
Agent contract for epic-scoped replace:
|
|
323
|
+
|
|
324
|
+
- Exact search command:
|
|
325
|
+
`trekoon epic search <epic-id> "path/to/somewhere" --toon`
|
|
326
|
+
- Exact replace command:
|
|
327
|
+
`trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --toon`
|
|
328
|
+
- Apply command:
|
|
329
|
+
`trekoon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply --toon`
|
|
330
|
+
- Epic scope includes the epic title/description plus every task and subtask
|
|
331
|
+
title/description in that epic tree.
|
|
332
|
+
|
|
333
|
+
Compact TOON fields to expect:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
ok: true
|
|
337
|
+
command: epic.search
|
|
338
|
+
data:
|
|
339
|
+
scope: epic
|
|
340
|
+
query: { search, fields[], mode: preview }
|
|
341
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
342
|
+
summary: { matchedEntities, matchedFields, totalMatches }
|
|
343
|
+
metadata:
|
|
344
|
+
contractVersion: 1.0.0
|
|
345
|
+
requestId: req-<id>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
ok: true
|
|
350
|
+
command: epic.replace
|
|
351
|
+
data:
|
|
352
|
+
scope: epic
|
|
353
|
+
query: { search, replace, fields[], mode: preview|apply }
|
|
354
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
355
|
+
summary: { matchedEntities, matchedFields, totalMatches, mode }
|
|
356
|
+
metadata:
|
|
357
|
+
contractVersion: 1.0.0
|
|
358
|
+
requestId: req-<id>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Background behavior to assume:
|
|
362
|
+
|
|
363
|
+
- Scope traversal is deterministic: epic first, then descendant tasks, then
|
|
364
|
+
descendant subtasks.
|
|
365
|
+
- Field traversal is deterministic: `title` before `description`.
|
|
366
|
+
- Preview reads and summarizes candidates without mutation.
|
|
367
|
+
- `--apply` reuses the same scoped traversal, mutates only rows with real text
|
|
368
|
+
changes, and returns matched rows with `query.mode` and `summary.mode` set
|
|
369
|
+
to `"apply"`.
|
|
370
|
+
|
|
371
|
+
## 8) Setup/install/init (if `trekoon` is unavailable)
|
|
257
372
|
|
|
258
373
|
1. Install Trekoon (or make sure it is on `PATH`).
|
|
259
374
|
2. In the target repository/worktree, initialize tracker state:
|
|
@@ -267,7 +382,7 @@ trekoon init --toon
|
|
|
267
382
|
|
|
268
383
|
If `.trekoon/trekoon.db` is missing, initialize before any create/update commands.
|
|
269
384
|
|
|
270
|
-
##
|
|
385
|
+
## 9) Safety
|
|
271
386
|
|
|
272
387
|
- Never edit `.trekoon/trekoon.db` directly.
|
|
273
388
|
- `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
|
-
- `trekoon epic <create|list|show|update|delete>`
|
|
51
|
-
- `trekoon task <create|list|show|ready|next|update|delete>`
|
|
52
|
-
- `trekoon subtask <create|list|update|delete>`
|
|
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>`
|
|
53
54
|
- `trekoon dep <add|remove|list|reverse>`
|
|
54
|
-
- `trekoon
|
|
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
|
|
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:
|
|
116
|
-
|
|
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
|
|
|
@@ -174,7 +186,88 @@ trekoon --json epic show <epic-id>
|
|
|
174
186
|
trekoon --json task show <task-id>
|
|
175
187
|
```
|
|
176
188
|
|
|
177
|
-
### 6)
|
|
189
|
+
### 6) Scoped search for repeated text
|
|
190
|
+
|
|
191
|
+
Use scoped search before manual tree reads when you need to locate repeated
|
|
192
|
+
paths, labels, or migration targets.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
196
|
+
trekoon --toon task search <task-id> "path/to/somewhere"
|
|
197
|
+
trekoon --toon subtask search <subtask-id> "path/to/somewhere"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Scope rules:
|
|
201
|
+
|
|
202
|
+
- `epic search` scans the epic title/description plus every task and subtask
|
|
203
|
+
title/description in that epic tree.
|
|
204
|
+
- `task search` scans the task title/description plus descendant subtask
|
|
205
|
+
title/description.
|
|
206
|
+
- `subtask search` scans only that subtask's title/description.
|
|
207
|
+
- Add `--fields title`, `--fields description`, or
|
|
208
|
+
`--fields title,description` when you need a narrower scan.
|
|
209
|
+
|
|
210
|
+
### 7) Preview first, then apply scoped replace
|
|
211
|
+
|
|
212
|
+
Use search first to confirm the scope, then run replace in preview mode, and
|
|
213
|
+
only use `--apply` after the preview matches the intended migration.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
trekoon --toon epic search <epic-id> "path/to/somewhere"
|
|
217
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path"
|
|
218
|
+
trekoon --toon epic replace <epic-id> --search "path/to/somewhere" --replace "path/to/new-path" --apply
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Use this loop for low-risk agent workflows:
|
|
222
|
+
|
|
223
|
+
1. `search` when you need the smallest possible read before deciding whether a
|
|
224
|
+
migration is needed.
|
|
225
|
+
2. preview `replace` to verify the exact candidate set and changed fields.
|
|
226
|
+
3. `replace --apply` only after the preview output matches the intended scope.
|
|
227
|
+
|
|
228
|
+
Epic-scoped replace applies across the epic title/description and every task and
|
|
229
|
+
subtask title/description in that epic tree.
|
|
230
|
+
|
|
231
|
+
Compact TOON expectations for agents:
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
ok: true
|
|
235
|
+
command: epic.search
|
|
236
|
+
data:
|
|
237
|
+
scope: epic
|
|
238
|
+
query: { search, fields[], mode: preview }
|
|
239
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
240
|
+
summary: { matchedEntities, matchedFields, totalMatches }
|
|
241
|
+
metadata:
|
|
242
|
+
contractVersion: 1.0.0
|
|
243
|
+
requestId: req-<id>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
ok: true
|
|
248
|
+
command: epic.replace
|
|
249
|
+
data:
|
|
250
|
+
scope: epic
|
|
251
|
+
query: { search, replace, fields[], mode: preview|apply }
|
|
252
|
+
matches[]: { kind, id, fields[]: { field, count, snippet } }
|
|
253
|
+
summary: { matchedEntities, matchedFields, totalMatches, mode }
|
|
254
|
+
metadata:
|
|
255
|
+
contractVersion: 1.0.0
|
|
256
|
+
requestId: req-<id>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Background behavior:
|
|
260
|
+
|
|
261
|
+
- `epic search` and preview `epic replace` traverse the epic first, then
|
|
262
|
+
descendant tasks, then descendant subtasks.
|
|
263
|
+
- Within each record, Trekoon checks `title` before `description` so output stays
|
|
264
|
+
deterministic and low-token.
|
|
265
|
+
- Preview reports the candidate set without mutating records.
|
|
266
|
+
- `--apply` reuses the same scoped traversal, updates only rows with real text
|
|
267
|
+
changes, and returns the matched rows with `query.mode` and `summary.mode`
|
|
268
|
+
set to `"apply"`.
|
|
269
|
+
|
|
270
|
+
### 8) Sync workflow for worktrees
|
|
178
271
|
|
|
179
272
|
- Run `trekoon sync status` at session start and before PR/merge.
|
|
180
273
|
- Run `trekoon sync pull --from main` before merge to align tracker state.
|
|
@@ -183,6 +276,8 @@ trekoon --json task show <task-id>
|
|
|
183
276
|
```bash
|
|
184
277
|
trekoon sync status
|
|
185
278
|
trekoon sync pull --from main
|
|
279
|
+
trekoon sync conflicts list
|
|
280
|
+
trekoon sync conflicts show <conflict-id>
|
|
186
281
|
trekoon sync resolve <conflict-id> --use ours
|
|
187
282
|
```
|
|
188
283
|
|
|
@@ -195,10 +290,31 @@ react deterministically:
|
|
|
195
290
|
- `diagnostics.conflictEvents`
|
|
196
291
|
- `diagnostics.errorHints`
|
|
197
292
|
|
|
198
|
-
|
|
293
|
+
Compatibility mode for legacy sync command IDs:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
trekoon --json --compat legacy-sync-command-ids sync status
|
|
297
|
+
trekoon --toon --compat legacy-sync-command-ids sync pull --from main
|
|
298
|
+
```
|
|
199
299
|
|
|
200
|
-
|
|
201
|
-
|
|
300
|
+
Behavior:
|
|
301
|
+
|
|
302
|
+
- Default remains strict canonical IDs (`sync.status`, `sync.pull`, ...).
|
|
303
|
+
- Compatibility mode rewrites sync command IDs to legacy forms
|
|
304
|
+
(`sync_status`, `sync_pull`, ...).
|
|
305
|
+
- Compatibility mode is machine-only and valid only for `sync` commands.
|
|
306
|
+
- Machine output includes `metadata.compatibility` with:
|
|
307
|
+
- deprecation warning code
|
|
308
|
+
- migration guidance
|
|
309
|
+
- canonical + compatibility command IDs
|
|
310
|
+
- removal window (`removalAfter: 2026-09-30`)
|
|
311
|
+
- Migration path: remove `--compat legacy-sync-command-ids` and consume dotted
|
|
312
|
+
command IDs directly.
|
|
313
|
+
|
|
314
|
+
### 9) Install project-local Trekoon skill for agents
|
|
315
|
+
|
|
316
|
+
`trekoon skills install` always writes the bundled skill file under the current
|
|
317
|
+
working directory at:
|
|
202
318
|
|
|
203
319
|
- `.agents/skills/trekoon/SKILL.md`
|
|
204
320
|
|
|
@@ -220,7 +336,7 @@ Path behavior:
|
|
|
220
336
|
- Default pi link path: `.pi/skills/trekoon`
|
|
221
337
|
- `--to <path>` overrides the editor root for link creation only.
|
|
222
338
|
- `--to` does **not** move or copy `SKILL.md` to that path.
|
|
223
|
-
- By default, link targets must resolve inside the
|
|
339
|
+
- By default, link targets must resolve inside the current working directory root.
|
|
224
340
|
- Use `--allow-outside-repo` only for intentional external links.
|
|
225
341
|
- When override is used, install prints a warning and includes confirmation
|
|
226
342
|
fields in machine output.
|
|
@@ -237,11 +353,11 @@ Path behavior:
|
|
|
237
353
|
How `--to` works (step-by-step):
|
|
238
354
|
|
|
239
355
|
1. Trekoon always installs/copies to:
|
|
240
|
-
- `<
|
|
356
|
+
- `<cwd>/.agents/skills/trekoon/SKILL.md`
|
|
241
357
|
2. If `--link` is present, Trekoon creates a `trekoon` symlink directory entry.
|
|
242
358
|
3. `--to <path>` sets the symlink root directory.
|
|
243
359
|
4. Final link path is:
|
|
244
|
-
- `<resolved-to-path>/trekoon -> <
|
|
360
|
+
- `<resolved-to-path>/trekoon -> <cwd>/.agents/skills/trekoon`
|
|
245
361
|
|
|
246
362
|
Example:
|
|
247
363
|
|
|
@@ -251,13 +367,13 @@ trekoon skills install --link --editor opencode --to ./.custom-editor/skills
|
|
|
251
367
|
|
|
252
368
|
This produces:
|
|
253
369
|
|
|
254
|
-
- `<
|
|
255
|
-
- `<
|
|
256
|
-
- symlink target: `<
|
|
370
|
+
- `<cwd>/.agents/skills/trekoon/SKILL.md` (copied file)
|
|
371
|
+
- `<cwd>/.custom-editor/skills/trekoon` (symlink)
|
|
372
|
+
- symlink target: `<cwd>/.agents/skills/trekoon`
|
|
257
373
|
|
|
258
374
|
Trekoon does not mutate global editor config directories.
|
|
259
375
|
|
|
260
|
-
###
|
|
376
|
+
### 10) Pre-merge checklist
|
|
261
377
|
|
|
262
378
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
263
379
|
- [ ] done tasks/subtasks are marked completed
|
|
@@ -269,6 +385,27 @@ Trekoon does not mutate global editor config directories.
|
|
|
269
385
|
Use `--toon` for production agent loops. The examples below show command +
|
|
270
386
|
expected envelope fields.
|
|
271
387
|
|
|
388
|
+
Base envelope fields (all machine responses):
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
ok: true|false
|
|
392
|
+
command: <stable command id>
|
|
393
|
+
data: <payload>
|
|
394
|
+
metadata:
|
|
395
|
+
contractVersion: "1.0.0"
|
|
396
|
+
requestId: req-<stable-id>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Most subcommand identifiers are dot-namespaced (`task.list`, `sync.pull`,
|
|
400
|
+
`epic.show`). Root-level commands may use single-token IDs (`help`, `init`,
|
|
401
|
+
`quickstart`, `wipe`, `version`).
|
|
402
|
+
|
|
403
|
+
Additional metadata can appear when relevant:
|
|
404
|
+
|
|
405
|
+
- `metadata.compatibility` when `--compat` mode is active
|
|
406
|
+
- `meta.storageRootDiagnostics` when a machine-readable command resolves
|
|
407
|
+
storage from a non-canonical cwd
|
|
408
|
+
|
|
272
409
|
### Ready queue (deterministic candidates)
|
|
273
410
|
|
|
274
411
|
```bash
|
|
@@ -336,6 +473,9 @@ ok: true
|
|
|
336
473
|
command: task.list
|
|
337
474
|
data:
|
|
338
475
|
tasks[]: ...
|
|
476
|
+
metadata:
|
|
477
|
+
contractVersion: "1.0.0"
|
|
478
|
+
requestId: req-<stable-id>
|
|
339
479
|
meta:
|
|
340
480
|
pagination: { hasMore, nextCursor }
|
|
341
481
|
```
|
package/package.json
CHANGED
|
@@ -3,6 +3,22 @@ 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[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const SEARCH_REPLACE_FIELDS = ["title", "description"] as const;
|
|
10
|
+
|
|
11
|
+
export type SearchReplaceField = (typeof SEARCH_REPLACE_FIELDS)[number];
|
|
12
|
+
|
|
13
|
+
export interface ParsedCsvEnumOption<T extends string> {
|
|
14
|
+
readonly values: readonly T[];
|
|
15
|
+
readonly invalidValues: readonly string[];
|
|
16
|
+
readonly empty: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PreviewApplyModeSelection {
|
|
20
|
+
readonly mode: "preview" | "apply";
|
|
21
|
+
readonly conflict: boolean;
|
|
6
22
|
}
|
|
7
23
|
|
|
8
24
|
const LONG_PREFIX = "--";
|
|
@@ -12,6 +28,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
12
28
|
const options = new Map<string, string>();
|
|
13
29
|
const flags = new Set<string>();
|
|
14
30
|
const missingOptionValues = new Set<string>();
|
|
31
|
+
const providedOptions: string[] = [];
|
|
15
32
|
|
|
16
33
|
for (let index = 0; index < args.length; index += 1) {
|
|
17
34
|
const token: string | undefined = args[index];
|
|
@@ -25,6 +42,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
const key = token.slice(LONG_PREFIX.length);
|
|
45
|
+
providedOptions.push(key);
|
|
28
46
|
const value = args[index + 1];
|
|
29
47
|
if (!value || value.startsWith(LONG_PREFIX)) {
|
|
30
48
|
flags.add(key);
|
|
@@ -41,6 +59,7 @@ export function parseArgs(args: readonly string[]): ParsedArgs {
|
|
|
41
59
|
options,
|
|
42
60
|
flags,
|
|
43
61
|
missingOptionValues,
|
|
62
|
+
providedOptions,
|
|
44
63
|
};
|
|
45
64
|
}
|
|
46
65
|
|
|
@@ -92,6 +111,151 @@ export function parseStrictNonNegativeInt(rawValue: string | undefined): number
|
|
|
92
111
|
return parsed;
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
export function parseCsvOption(rawValue: string | undefined): string[] | undefined {
|
|
115
|
+
if (rawValue === undefined) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rawValue
|
|
120
|
+
.split(",")
|
|
121
|
+
.map((value) => value.trim())
|
|
122
|
+
.filter((value) => value.length > 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseCsvEnumOption<const T extends readonly string[]>(
|
|
126
|
+
rawValue: string | undefined,
|
|
127
|
+
allowed: T,
|
|
128
|
+
): ParsedCsvEnumOption<T[number]> {
|
|
129
|
+
const values = parseCsvOption(rawValue);
|
|
130
|
+
if (values === undefined) {
|
|
131
|
+
return {
|
|
132
|
+
values: [...allowed],
|
|
133
|
+
invalidValues: [],
|
|
134
|
+
empty: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (values.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
values: [...allowed],
|
|
141
|
+
invalidValues: [],
|
|
142
|
+
empty: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const allowedValues = new Set<string>(allowed);
|
|
147
|
+
const validValues: T[number][] = [];
|
|
148
|
+
const invalidValues: string[] = [];
|
|
149
|
+
|
|
150
|
+
for (const value of values) {
|
|
151
|
+
if (!allowedValues.has(value)) {
|
|
152
|
+
invalidValues.push(value);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!validValues.includes(value as T[number])) {
|
|
157
|
+
validValues.push(value as T[number]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
values: validValues.length > 0 ? validValues : [...allowed],
|
|
163
|
+
invalidValues,
|
|
164
|
+
empty: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolvePreviewApplyMode(
|
|
169
|
+
flags: ReadonlySet<string>,
|
|
170
|
+
previewKey = "preview",
|
|
171
|
+
applyKey = "apply",
|
|
172
|
+
): PreviewApplyModeSelection {
|
|
173
|
+
const preview = flags.has(previewKey);
|
|
174
|
+
const apply = flags.has(applyKey);
|
|
175
|
+
return {
|
|
176
|
+
mode: apply ? "apply" : "preview",
|
|
177
|
+
conflict: preview && apply,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function levenshteinDistance(source: string, target: string): number {
|
|
182
|
+
const sourceLength = source.length;
|
|
183
|
+
const targetLength = target.length;
|
|
184
|
+
if (sourceLength === 0) {
|
|
185
|
+
return targetLength;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (targetLength === 0) {
|
|
189
|
+
return sourceLength;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const previous: number[] = Array.from({ length: targetLength + 1 }, (_, index) => index);
|
|
193
|
+
const current: number[] = new Array<number>(targetLength + 1).fill(0);
|
|
194
|
+
|
|
195
|
+
for (let sourceIndex = 1; sourceIndex <= sourceLength; sourceIndex += 1) {
|
|
196
|
+
current[0] = sourceIndex;
|
|
197
|
+
for (let targetIndex = 1; targetIndex <= targetLength; targetIndex += 1) {
|
|
198
|
+
const replacementCost = source[sourceIndex - 1] === target[targetIndex - 1] ? 0 : 1;
|
|
199
|
+
const insertCost = (current[targetIndex - 1] ?? 0) + 1;
|
|
200
|
+
const deleteCost = (previous[targetIndex] ?? 0) + 1;
|
|
201
|
+
const replaceCost = (previous[targetIndex - 1] ?? 0) + replacementCost;
|
|
202
|
+
current[targetIndex] = Math.min(
|
|
203
|
+
insertCost,
|
|
204
|
+
deleteCost,
|
|
205
|
+
replaceCost,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (let targetIndex = 0; targetIndex <= targetLength; targetIndex += 1) {
|
|
210
|
+
previous[targetIndex] = current[targetIndex] ?? previous[targetIndex] ?? 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return previous[targetLength] ?? 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeOption(option: string): string {
|
|
218
|
+
return option.startsWith(LONG_PREFIX) ? option.slice(LONG_PREFIX.length) : option;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function findUnknownOption(parsed: ParsedArgs, allowedOptions: readonly string[]): string | undefined {
|
|
222
|
+
const allowed = new Set<string>(allowedOptions.map(normalizeOption));
|
|
223
|
+
for (const option of parsed.providedOptions) {
|
|
224
|
+
if (!allowed.has(option)) {
|
|
225
|
+
return option;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function suggestOptions(option: string, allowedOptions: readonly string[], limit = 3): string[] {
|
|
233
|
+
const normalizedOption = normalizeOption(option);
|
|
234
|
+
const normalizedAllowed = allowedOptions.map(normalizeOption);
|
|
235
|
+
return normalizedAllowed
|
|
236
|
+
.map((candidate) => {
|
|
237
|
+
const distance =
|
|
238
|
+
candidate.startsWith(normalizedOption) || normalizedOption.startsWith(candidate)
|
|
239
|
+
? 0
|
|
240
|
+
: levenshteinDistance(normalizedOption, candidate);
|
|
241
|
+
return {
|
|
242
|
+
candidate,
|
|
243
|
+
distance,
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
.sort((left, right) => {
|
|
247
|
+
const byDistance = left.distance - right.distance;
|
|
248
|
+
if (byDistance !== 0) {
|
|
249
|
+
return byDistance;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return left.candidate.localeCompare(right.candidate);
|
|
253
|
+
})
|
|
254
|
+
.filter((item) => item.distance <= Math.max(2, Math.floor(normalizedOption.length / 2)))
|
|
255
|
+
.slice(0, limit)
|
|
256
|
+
.map((item) => item.candidate);
|
|
257
|
+
}
|
|
258
|
+
|
|
95
259
|
export function readEnumOption<const T extends readonly string[]>(
|
|
96
260
|
options: ReadonlyMap<string, string>,
|
|
97
261
|
allowed: T,
|