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.
@@ -4,34 +4,69 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
4
4
  const QUICKSTART_TEXT = [
5
5
  "Trekoon quickstart",
6
6
  "",
7
+ "This quickstart is aligned with .agents/skills/trekoon/SKILL.md.",
8
+ "For agents: always use --toon for every command.",
9
+ "",
7
10
  "1) Local DB and worktree model",
8
11
  "- Every worktree stores tracker state at .trekoon/trekoon.db.",
9
12
  "- This DB stays local; it is not merged by Git automatically.",
10
13
  "",
11
- "2) Pre-merge sync flow",
12
- "- Run: trekoon sync status",
13
- "- Pull upstream tracker events: trekoon sync pull --from main",
14
- "- Resolve conflicts if needed: trekoon sync resolve <id> --use ours",
15
- "- Run sync status again before opening or merging a PR.",
14
+ "2) Agent session-start checklist (verify setup + current state)",
15
+ "- 1. Verify tracker exists (or initialize once): trekoon --toon init",
16
+ "- 2. Check sync baseline: trekoon --toon sync status",
17
+ "- 3. Load active context: trekoon --toon epic list",
18
+ "- 4. Load active tasks: trekoon --toon task list",
19
+ "- 5. Load deterministic candidates: trekoon --toon task ready --limit 5",
20
+ "- 6. If blocked, inspect downstream impact: trekoon --toon dep reverse <task-or-subtask-id>",
21
+ "",
22
+ "3) AI execution loop (deterministic, dependency-aware)",
23
+ "- 1. Sync branch/worktree state: trekoon --toon sync status",
24
+ "- 2. Select ready work: trekoon --toon task ready --limit 5",
25
+ "- 3. Pick top candidate when needed: trekoon --toon task next",
26
+ "- 4. Check downstream blockers: trekoon --toon dep reverse <task-or-subtask-id>",
27
+ "- 5. Claim work and update status: trekoon --toon task update <task-id> --status in_progress",
28
+ "- 6. Complete with context: trekoon --toon task update <task-id> --append \"Completed implementation\" --status done",
29
+ "- 7. Or report block: trekoon --toon task update <task-id> --append \"Blocked by <reason>\" --status blocked",
16
30
  "",
17
- "3) Task details and description",
31
+ "4) Power-user command patterns (aligned with skill)",
32
+ "- Inspect full epic tree: trekoon --toon epic show <epic-id> --all",
33
+ "- Inspect full task payload: trekoon --toon task show <task-id> --all",
34
+ "- Check direct dependencies before starting: trekoon --toon dep list <task-id>",
35
+ "- Find what this item unblocks: trekoon --toon dep reverse <task-or-subtask-id>",
36
+ "- Filter list explicitly: trekoon --toon task list --status in_progress,todo --limit 20",
37
+ "- Paginate deterministically: trekoon --toon task list --cursor <n>",
38
+ "- Bulk append/status update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
39
+ "",
40
+ "5) Task details and description",
18
41
  "- Human list and show views default to table format.",
19
42
  "- Alternate list view: add --view compact.",
20
43
  "- task/epic/subtask list defaults: open work only (in_progress/in-progress, todo), max 10.",
21
44
  "- Filter list by status: --status in_progress,todo (CSV).",
22
45
  "- Change page size: --limit <n>. Show all statuses and all rows with --all.",
23
- "- --all cannot be combined with --status or --limit.",
46
+ "- Continue pagination with --cursor <n> (offset-like list position).",
47
+ "- --all cannot be combined with --status, --limit, or --cursor.",
24
48
  "- Bulk update: use --all or --ids <csv> with --append and/or --status.",
25
49
  "- Bulk update rejects positional id, and --all/--ids cannot be combined.",
26
- "- Full tree + descriptions: trekoon epic show <epic-id> --all --json",
27
- "- For full task payload (including description), use --json:",
28
- " trekoon task show <task-id> --all --json",
29
- "",
30
- "4) Machine output examples",
31
- "- trekoon quickstart --json",
32
- "- trekoon task show <task-id> --all --json",
33
- "- trekoon epic show <epic-id> --all --json",
34
- "- trekoon sync status --toon",
50
+ "- Ready queue: trekoon task ready [--limit <n>] [--epic <id>] (deterministic order).",
51
+ "- Next execution candidate: trekoon task next [--epic <id>]",
52
+ "- Full tree + descriptions (canonical): trekoon --toon epic show <epic-id> --all",
53
+ "- Full task payload (including description): trekoon --toon task show <task-id> --all",
54
+ "- Optional integration format: trekoon --json task show <task-id> --all",
55
+ "",
56
+ "6) Pre-merge sync flow",
57
+ "- Run: trekoon --toon sync status",
58
+ "- Pull upstream tracker events: trekoon --toon sync pull --from main",
59
+ "- Resolve conflicts if needed: trekoon --toon sync resolve <id> --use ours",
60
+ "- Run sync status again before opening or merging a PR.",
61
+ "",
62
+ "7) Machine output examples",
63
+ "- trekoon --toon quickstart",
64
+ "- trekoon --toon task show <task-id> --all",
65
+ "- trekoon --toon epic show <epic-id> --all",
66
+ "- trekoon --toon sync status",
67
+ "- trekoon --toon task ready --limit 5",
68
+ "- trekoon --toon task next",
69
+ "- trekoon --toon dep reverse <task-or-subtask-id>",
35
70
  ].join("\n");
36
71
 
37
72
  export async function runQuickstart(_: CliContext): Promise<CliResult> {
@@ -44,17 +79,46 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
44
79
  databaseFile: ".trekoon/trekoon.db",
45
80
  mergeBehavior: "manual-sync",
46
81
  },
82
+ alignedSkill: ".agents/skills/trekoon/SKILL.md",
83
+ requiresToonForAgents: true,
84
+ agentStartupChecklist: [
85
+ "trekoon --toon init",
86
+ "trekoon --toon sync status",
87
+ "trekoon --toon epic list",
88
+ "trekoon --toon task list",
89
+ "trekoon --toon task ready --limit 5",
90
+ "trekoon --toon dep reverse <task-or-subtask-id>",
91
+ ],
47
92
  preMergeFlow: [
48
- "trekoon sync status",
49
- "trekoon sync pull --from main",
50
- "trekoon sync resolve <id> --use ours",
51
- "trekoon sync status",
93
+ "trekoon --toon sync status",
94
+ "trekoon --toon sync pull --from main",
95
+ "trekoon --toon sync resolve <id> --use ours",
96
+ "trekoon --toon sync status",
97
+ ],
98
+ executionLoop: [
99
+ "trekoon --toon sync status",
100
+ "trekoon --toon task ready --limit 5",
101
+ "trekoon --toon task next",
102
+ "trekoon --toon dep reverse <task-or-subtask-id>",
103
+ "trekoon --toon task update <task-id> --status in_progress",
104
+ ],
105
+ powerUserCommands: [
106
+ "trekoon --toon epic show <epic-id> --all",
107
+ "trekoon --toon task show <task-id> --all",
108
+ "trekoon --toon dep list <task-id>",
109
+ "trekoon --toon dep reverse <task-or-subtask-id>",
110
+ "trekoon --toon task list --status in_progress,todo --limit 20",
111
+ "trekoon --toon task list --cursor <n>",
112
+ "trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
52
113
  ],
53
114
  machineExamples: [
54
- "trekoon quickstart --json",
55
- "trekoon task show <task-id> --all --json",
56
- "trekoon epic show <epic-id> --all --json",
57
- "trekoon sync status --toon",
115
+ "trekoon --toon quickstart",
116
+ "trekoon --toon task show <task-id> --all",
117
+ "trekoon --toon epic show <epic-id> --all",
118
+ "trekoon --toon sync status",
119
+ "trekoon --toon task ready --limit 5",
120
+ "trekoon --toon task next",
121
+ "trekoon --toon dep reverse <task-or-subtask-id>",
58
122
  ],
59
123
  },
60
124
  });
@@ -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";
@@ -54,16 +62,44 @@ function filterSortAndLimitSubtasks(
54
62
  subtasks: readonly SubtaskRecord[],
55
63
  statuses: readonly string[] | undefined,
56
64
  limit: number | undefined,
57
- ): SubtaskRecord[] {
65
+ cursor: number,
66
+ ): { subtasks: SubtaskRecord[]; pagination: { hasMore: boolean; nextCursor: string | null } } {
58
67
  const allowedStatuses = statuses === undefined ? undefined : new Set(statuses);
59
68
  const filtered = allowedStatuses === undefined ? [...subtasks] : subtasks.filter((subtask) => allowedStatuses.has(subtask.status));
60
- const sorted = [...filtered].sort((left, right) => subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status));
69
+ const sorted = [...filtered].sort((left, right) => {
70
+ const byStatus = subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status);
71
+ if (byStatus !== 0) {
72
+ return byStatus;
73
+ }
74
+
75
+ const byCreatedAt = left.createdAt - right.createdAt;
76
+ if (byCreatedAt !== 0) {
77
+ return byCreatedAt;
78
+ }
79
+
80
+ return left.id.localeCompare(right.id);
81
+ });
61
82
 
62
83
  if (limit === undefined) {
63
- return sorted;
84
+ return {
85
+ subtasks: sorted,
86
+ pagination: {
87
+ hasMore: false,
88
+ nextCursor: null,
89
+ },
90
+ };
64
91
  }
65
92
 
66
- return sorted.slice(0, limit);
93
+ const pagedSubtasks = sorted.slice(cursor, cursor + limit);
94
+ const nextIndex = cursor + pagedSubtasks.length;
95
+ const hasMore = nextIndex < sorted.length;
96
+ return {
97
+ subtasks: pagedSubtasks,
98
+ pagination: {
99
+ hasMore,
100
+ nextCursor: hasMore ? `${nextIndex}` : null,
101
+ },
102
+ };
67
103
  }
68
104
 
69
105
  function appendLine(existing: string, line: string): string {
@@ -161,6 +197,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
161
197
  readMissingOptionValue(parsed.missingOptionValues, "view") ??
162
198
  readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
163
199
  readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
200
+ readMissingOptionValue(parsed.missingOptionValues, "cursor") ??
164
201
  readMissingOptionValue(parsed.missingOptionValues, "task", "t");
165
202
  if (missingListOption !== undefined) {
166
203
  return failMissingOptionValue("subtask.list", missingListOption);
@@ -171,6 +208,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
171
208
  const includeAll = hasFlag(parsed.flags, "all");
172
209
  const rawStatuses = readOption(parsed.options, "status", "s");
173
210
  const rawLimit = readOption(parsed.options, "limit", "l");
211
+ const rawCursor = readOption(parsed.options, "cursor");
174
212
 
175
213
  if (rawView !== undefined && view === undefined) {
176
214
  return failResult({
@@ -208,6 +246,18 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
208
246
  });
209
247
  }
210
248
 
249
+ if (includeAll && rawCursor !== undefined) {
250
+ return failResult({
251
+ command: "subtask.list",
252
+ human: "Use either --all or --cursor, not both.",
253
+ data: { code: "invalid_input", flags: ["all", "cursor"] },
254
+ error: {
255
+ code: "invalid_input",
256
+ message: "--all and --cursor are mutually exclusive",
257
+ },
258
+ });
259
+ }
260
+
211
261
  const statuses = parseStatusCsv(rawStatuses);
212
262
  if (rawStatuses !== undefined && statuses !== undefined && statuses.length === 0) {
213
263
  return failResult({
@@ -234,6 +284,19 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
234
284
  });
235
285
  }
236
286
 
287
+ const parsedCursor = parseStrictNonNegativeInt(rawCursor);
288
+ if (Number.isNaN(parsedCursor)) {
289
+ return failResult({
290
+ command: "subtask.list",
291
+ human: "Invalid --cursor value. Use an integer >= 0.",
292
+ data: { code: "invalid_input", cursor: rawCursor },
293
+ error: {
294
+ code: "invalid_input",
295
+ message: "Invalid --cursor value",
296
+ },
297
+ });
298
+ }
299
+
237
300
  const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
238
301
  const selectedStatuses = includeAll
239
302
  ? undefined
@@ -241,7 +304,13 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
241
304
  const selectedLimit = includeAll
242
305
  ? undefined
243
306
  : parsedLimit ?? DEFAULT_SUBTASK_LIST_LIMIT;
244
- const subtasks = filterSortAndLimitSubtasks(domain.listSubtasks(taskId), selectedStatuses, selectedLimit);
307
+ const listed = filterSortAndLimitSubtasks(
308
+ domain.listSubtasks(taskId),
309
+ selectedStatuses,
310
+ selectedLimit,
311
+ parsedCursor ?? 0,
312
+ );
313
+ const subtasks = listed.subtasks;
245
314
  const listView = view ?? "table";
246
315
  const human =
247
316
  subtasks.length === 0
@@ -254,6 +323,29 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
254
323
  command: "subtask.list",
255
324
  human,
256
325
  data: { subtasks },
326
+ ...(context.mode === "human"
327
+ ? {}
328
+ : {
329
+ meta: {
330
+ pagination: listed.pagination,
331
+ defaults: {
332
+ statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_SUBTASK_STATUSES] : null,
333
+ limit: !includeAll && parsedLimit === undefined ? DEFAULT_SUBTASK_LIST_LIMIT : null,
334
+ cursor: parsedCursor === undefined ? 0 : null,
335
+ view: view === undefined ? "table" : null,
336
+ },
337
+ filters: {
338
+ taskId: taskId ?? null,
339
+ statuses: selectedStatuses ?? null,
340
+ includeAll,
341
+ },
342
+ truncation: {
343
+ applied: listed.pagination.hasMore,
344
+ returned: subtasks.length,
345
+ limit: selectedLimit ?? null,
346
+ },
347
+ },
348
+ }),
257
349
  });
258
350
  }
259
351
  case "update": {
@@ -1,22 +1,48 @@
1
+ import { findUnknownOption, parseArgs, readMissingOptionValue, readOption, suggestOptions } from "./arg-parser";
2
+
1
3
  import { failResult, okResult } from "../io/output";
2
4
  import { type CliContext, type CliResult } from "../runtime/command-types";
3
5
  import { MissingBranchDatabaseError } from "../sync/branch-db";
4
6
  import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
5
- import { type SyncConflictMode, type SyncResolution } from "../sync/types";
7
+ import { type SyncResolution } from "../sync/types";
8
+
9
+ const STATUS_OPTIONS = ["from"] as const;
10
+ const PULL_OPTIONS = ["from"] as const;
11
+ const RESOLVE_OPTIONS = ["use"] as const;
12
+ const CONFLICTS_LIST_OPTIONS = ["mode"] as const;
13
+ const CONFLICTS_SHOW_OPTIONS: readonly string[] = [];
14
+
15
+ function resolveSyncCommandId(subcommand: string | undefined, conflictsSubcommand: string | undefined): string {
16
+ if (subcommand === "status") {
17
+ return "sync.status";
18
+ }
19
+
20
+ if (subcommand === "pull") {
21
+ return "sync.pull";
22
+ }
23
+
24
+ if (subcommand === "resolve") {
25
+ return "sync.resolve";
26
+ }
27
+
28
+ if (subcommand !== "conflicts") {
29
+ return "sync";
30
+ }
31
+
32
+ if (conflictsSubcommand === "list") {
33
+ return "sync.conflicts.list";
34
+ }
6
35
 
7
- function parseOption(args: readonly string[], option: string): string | null {
8
- const index: number = args.indexOf(option);
9
- if (index < 0) {
10
- return null;
36
+ if (conflictsSubcommand === "show") {
37
+ return "sync.conflicts.show";
11
38
  }
12
39
 
13
- const value: string | undefined = args[index + 1];
14
- return value && !value.startsWith("--") ? value : null;
40
+ return "sync.conflicts";
15
41
  }
16
42
 
17
- function usage(message: string): CliResult {
43
+ function usage(message: string, command = "sync"): CliResult {
18
44
  return failResult({
19
- command: "sync",
45
+ command,
20
46
  human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
21
47
  data: { message },
22
48
  error: {
@@ -26,6 +52,28 @@ function usage(message: string): CliResult {
26
52
  });
27
53
  }
28
54
 
55
+ function prefixedOptions(options: readonly string[]): string[] {
56
+ return options.map((option) => `--${option}`);
57
+ }
58
+
59
+ function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
60
+ const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
61
+ const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
62
+ return failResult({
63
+ command,
64
+ human: `Unknown option --${option}.${suggestionMessage}`,
65
+ data: {
66
+ option: `--${option}`,
67
+ allowedOptions: prefixedOptions(allowedOptions),
68
+ suggestions,
69
+ },
70
+ error: {
71
+ code: "unknown_option",
72
+ message: `Unknown option --${option}`,
73
+ },
74
+ });
75
+ }
76
+
29
77
  function statusMessage(sourceBranch: string, ahead: number, behind: number, conflicts: number): string {
30
78
  return [
31
79
  `Sync status against '${sourceBranch}'`,
@@ -61,26 +109,11 @@ function formatConflictList(
61
109
  .join("\n");
62
110
  }
63
111
 
64
- function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
65
- const modeIndex = args.indexOf("--mode");
66
- if (modeIndex < 0) {
67
- return "pending";
68
- }
69
-
70
- const explicitMode = args[modeIndex + 1];
71
- if (!explicitMode || explicitMode.startsWith("--")) {
72
- return null;
73
- }
74
-
75
- if (explicitMode === "pending" || explicitMode === "all") {
76
- return explicitMode;
77
- }
78
-
79
- return null;
80
- }
81
-
82
112
  export async function runSync(context: CliContext): Promise<CliResult> {
83
- const subcommand: string | undefined = context.args[0];
113
+ const parsed = parseArgs(context.args);
114
+ const subcommand: string | undefined = parsed.positional[0];
115
+ const conflictsSubcommand: string | undefined = subcommand === "conflicts" ? parsed.positional[1] : undefined;
116
+ const resolvedCommand: string = resolveSyncCommandId(subcommand, conflictsSubcommand);
84
117
 
85
118
  if (!subcommand) {
86
119
  return usage("Missing sync subcommand.");
@@ -88,26 +121,46 @@ export async function runSync(context: CliContext): Promise<CliResult> {
88
121
 
89
122
  try {
90
123
  if (subcommand === "status") {
91
- const sourceBranch: string = parseOption(context.args, "--from") ?? "main";
124
+ const statusUnknownOption = findUnknownOption(parsed, STATUS_OPTIONS);
125
+ if (statusUnknownOption !== undefined) {
126
+ return unknownOption("sync.status", statusUnknownOption, STATUS_OPTIONS);
127
+ }
128
+
129
+ const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
130
+ if (missingFromOption !== undefined) {
131
+ return usage("sync status requires --from <branch> when provided.", "sync.status");
132
+ }
133
+
134
+ const sourceBranch: string = readOption(parsed.options, "from") ?? "main";
92
135
  const summary = syncStatus(context.cwd, sourceBranch);
93
136
 
94
137
  return okResult({
95
- command: "sync status",
138
+ command: "sync.status",
96
139
  human: statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts),
97
140
  data: summary,
98
141
  });
99
142
  }
100
143
 
101
144
  if (subcommand === "pull") {
102
- const sourceBranch: string | null = parseOption(context.args, "--from");
103
- if (!sourceBranch) {
104
- return usage("sync pull requires --from <branch>.");
145
+ const pullUnknownOption = findUnknownOption(parsed, PULL_OPTIONS);
146
+ if (pullUnknownOption !== undefined) {
147
+ return unknownOption("sync.pull", pullUnknownOption, PULL_OPTIONS);
148
+ }
149
+
150
+ const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
151
+ if (missingFromOption !== undefined) {
152
+ return usage("sync pull requires --from <branch>.", "sync.pull");
153
+ }
154
+
155
+ const sourceBranch: string | undefined = readOption(parsed.options, "from");
156
+ if (sourceBranch === undefined) {
157
+ return usage("sync pull requires --from <branch>.", "sync.pull");
105
158
  }
106
159
 
107
160
  const summary = syncPull(context.cwd, sourceBranch);
108
161
 
109
162
  return okResult({
110
- command: "sync pull",
163
+ command: "sync.pull",
111
164
  human: [
112
165
  `Pulled from '${summary.sourceBranch}'`,
113
166
  `Scanned events: ${summary.scannedEvents}`,
@@ -123,42 +176,62 @@ export async function runSync(context: CliContext): Promise<CliResult> {
123
176
  }
124
177
 
125
178
  if (subcommand === "resolve") {
126
- const conflictId: string | undefined = context.args[1];
127
- const rawResolution: string | null = parseOption(context.args, "--use");
179
+ const resolveUnknownOption = findUnknownOption(parsed, RESOLVE_OPTIONS);
180
+ if (resolveUnknownOption !== undefined) {
181
+ return unknownOption("sync.resolve", resolveUnknownOption, RESOLVE_OPTIONS);
182
+ }
183
+
184
+ const conflictId: string | undefined = parsed.positional[1];
185
+ const missingResolutionOption = readMissingOptionValue(parsed.missingOptionValues, "use");
186
+ if (missingResolutionOption !== undefined) {
187
+ return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
188
+ }
189
+
190
+ const rawResolution: string | undefined = readOption(parsed.options, "use");
128
191
 
129
192
  if (!conflictId || !rawResolution) {
130
- return usage("sync resolve requires <conflict-id> --use ours|theirs.");
193
+ return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
131
194
  }
132
195
 
133
196
  if (rawResolution !== "ours" && rawResolution !== "theirs") {
134
- return usage("sync resolve --use only accepts ours|theirs.");
197
+ return usage("sync resolve --use only accepts ours|theirs.", "sync.resolve");
135
198
  }
136
199
 
137
200
  const summary = syncResolve(context.cwd, conflictId, rawResolution as SyncResolution);
138
201
 
139
202
  return okResult({
140
- command: "sync resolve",
203
+ command: "sync.resolve",
141
204
  human: `Resolved ${summary.conflictId} using ${summary.resolution}.`,
142
205
  data: summary,
143
206
  });
144
207
  }
145
208
 
146
209
  if (subcommand === "conflicts") {
147
- const conflictsCommand: string | undefined = context.args[1];
210
+ const conflictsCommand: string | undefined = parsed.positional[1];
148
211
  if (!conflictsCommand) {
149
- return usage("sync conflicts requires list|show.");
212
+ return usage("sync conflicts requires list|show.", "sync.conflicts");
150
213
  }
151
214
 
152
215
  if (conflictsCommand === "list") {
153
- const mode = parseConflictMode(context.args);
154
- if (!mode) {
155
- return usage("sync conflicts list --mode only accepts pending|all.");
216
+ const listUnknownOption = findUnknownOption(parsed, CONFLICTS_LIST_OPTIONS);
217
+ if (listUnknownOption !== undefined) {
218
+ return unknownOption("sync.conflicts.list", listUnknownOption, CONFLICTS_LIST_OPTIONS);
219
+ }
220
+
221
+ const missingModeOption = readMissingOptionValue(parsed.missingOptionValues, "mode");
222
+ if (missingModeOption !== undefined) {
223
+ return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
224
+ }
225
+
226
+ const mode = readOption(parsed.options, "mode") ?? "pending";
227
+ if (mode !== "pending" && mode !== "all") {
228
+ return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
156
229
  }
157
230
 
158
231
  const conflicts = listSyncConflicts(context.cwd, mode);
159
232
 
160
233
  return okResult({
161
- command: "sync conflicts list",
234
+ command: "sync.conflicts.list",
162
235
  human: formatConflictList(conflicts),
163
236
  data: {
164
237
  mode,
@@ -168,15 +241,20 @@ export async function runSync(context: CliContext): Promise<CliResult> {
168
241
  }
169
242
 
170
243
  if (conflictsCommand === "show") {
171
- const conflictId: string | undefined = context.args[2];
172
- if (!conflictId) {
173
- return usage("sync conflicts show requires <conflict-id>.");
244
+ const showUnknownOption = findUnknownOption(parsed, CONFLICTS_SHOW_OPTIONS);
245
+ if (showUnknownOption !== undefined) {
246
+ return unknownOption("sync.conflicts.show", showUnknownOption, CONFLICTS_SHOW_OPTIONS);
174
247
  }
175
248
 
249
+ const conflictId: string | undefined = parsed.positional[2];
250
+ if (!conflictId) {
251
+ return usage("sync conflicts show requires <conflict-id>.", "sync.conflicts.show");
252
+ }
253
+
176
254
  const conflict = getSyncConflict(context.cwd, conflictId);
177
255
 
178
256
  return okResult({
179
- command: "sync conflicts show",
257
+ command: "sync.conflicts.show",
180
258
  human: [
181
259
  `Conflict: ${conflict.id}`,
182
260
  `Entity: ${conflict.entityKind} ${conflict.entityId}`,
@@ -191,14 +269,14 @@ export async function runSync(context: CliContext): Promise<CliResult> {
191
269
  });
192
270
  }
193
271
 
194
- return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`);
272
+ return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`, "sync.conflicts");
195
273
  }
196
274
 
197
275
  return usage(`Unknown sync subcommand '${subcommand}'.`);
198
276
  } catch (error) {
199
277
  if (error instanceof MissingBranchDatabaseError) {
200
278
  return failResult({
201
- command: "sync",
279
+ command: resolvedCommand,
202
280
  human: error.message,
203
281
  data: {
204
282
  reason: "missing_branch_db",
@@ -213,7 +291,7 @@ export async function runSync(context: CliContext): Promise<CliResult> {
213
291
  const message = error instanceof Error ? error.message : "Unknown sync error.";
214
292
 
215
293
  return failResult({
216
- command: "sync",
294
+ command: resolvedCommand,
217
295
  human: message,
218
296
  data: {
219
297
  reason: "sync_failed",