trekoon 0.2.6 → 0.2.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.
Files changed (40) hide show
  1. package/README.md +102 -708
  2. package/docs/ai-agents.md +198 -0
  3. package/docs/commands.md +226 -0
  4. package/docs/machine-contracts.md +253 -0
  5. package/docs/plans/2026-03-15-trekoon-board-design.md +13 -0
  6. package/docs/quickstart.md +207 -0
  7. package/package.json +3 -1
  8. package/src/board/assets/app.js +1498 -0
  9. package/src/board/assets/components/AppShell.js +17 -0
  10. package/src/board/assets/components/BoardTopbar.js +78 -0
  11. package/src/board/assets/components/ClampedText.js +31 -0
  12. package/src/board/assets/components/EpicRow.js +62 -0
  13. package/src/board/assets/components/EpicsOverview.js +43 -0
  14. package/src/board/assets/components/WorkspaceHeader.js +70 -0
  15. package/src/board/assets/components/assetMap.js +65 -0
  16. package/src/board/assets/index.html +76 -0
  17. package/src/board/assets/main.js +27 -0
  18. package/src/board/assets/manifest.json +12 -0
  19. package/src/board/assets/state/actions.js +334 -0
  20. package/src/board/assets/state/api.js +126 -0
  21. package/src/board/assets/state/store.js +172 -0
  22. package/src/board/assets/styles/board.css +1127 -0
  23. package/src/board/assets/utils/dom.js +308 -0
  24. package/src/board/install.ts +196 -0
  25. package/src/board/open-browser.ts +131 -0
  26. package/src/board/routes.ts +299 -0
  27. package/src/board/server.ts +184 -0
  28. package/src/board/snapshot.ts +277 -0
  29. package/src/board/types.ts +43 -0
  30. package/src/commands/board.ts +158 -0
  31. package/src/commands/epic.ts +104 -3
  32. package/src/commands/help.ts +52 -13
  33. package/src/commands/init.ts +29 -0
  34. package/src/commands/subtask.ts +78 -1
  35. package/src/commands/task.ts +113 -7
  36. package/src/domain/mutation-service.ts +116 -0
  37. package/src/domain/tracker-domain.ts +261 -5
  38. package/src/domain/types.ts +51 -0
  39. package/src/runtime/cli-shell.ts +5 -0
  40. package/src/storage/path.ts +36 -0
@@ -0,0 +1,277 @@
1
+ import { TrackerDomain } from "../domain/tracker-domain";
2
+ import { type DependencyRecord, type EpicRecord, type SubtaskRecord, type TaskRecord } from "../domain/types";
3
+
4
+ interface SearchFields {
5
+ readonly title: string;
6
+ readonly description: string;
7
+ readonly text: string;
8
+ }
9
+
10
+ interface StatusCounts {
11
+ readonly total: number;
12
+ readonly todo: number;
13
+ readonly blocked: number;
14
+ readonly inProgress: number;
15
+ readonly done: number;
16
+ readonly other: number;
17
+ }
18
+
19
+ export interface BoardSnapshotEpic {
20
+ readonly id: string;
21
+ readonly title: string;
22
+ readonly description: string;
23
+ readonly status: string;
24
+ readonly createdAt: number;
25
+ readonly updatedAt: number;
26
+ readonly taskIds: readonly string[];
27
+ readonly counts: {
28
+ readonly tasks: StatusCounts;
29
+ readonly subtasks: StatusCounts;
30
+ };
31
+ readonly search: SearchFields;
32
+ }
33
+
34
+ export interface BoardSnapshotTask {
35
+ readonly id: string;
36
+ readonly epicId: string;
37
+ readonly title: string;
38
+ readonly description: string;
39
+ readonly status: string;
40
+ readonly createdAt: number;
41
+ readonly updatedAt: number;
42
+ readonly subtaskIds: readonly string[];
43
+ readonly dependencyIds: readonly string[];
44
+ readonly dependentIds: readonly string[];
45
+ readonly counts: {
46
+ readonly subtasks: StatusCounts;
47
+ readonly dependencies: number;
48
+ readonly dependents: number;
49
+ };
50
+ readonly search: SearchFields;
51
+ }
52
+
53
+ export interface BoardSnapshotSubtask {
54
+ readonly id: string;
55
+ readonly taskId: string;
56
+ readonly title: string;
57
+ readonly description: string;
58
+ readonly status: string;
59
+ readonly createdAt: number;
60
+ readonly updatedAt: number;
61
+ readonly dependencyIds: readonly string[];
62
+ readonly dependentIds: readonly string[];
63
+ readonly counts: {
64
+ readonly dependencies: number;
65
+ readonly dependents: number;
66
+ };
67
+ readonly search: SearchFields;
68
+ }
69
+
70
+ export interface BoardSnapshotDependency {
71
+ readonly id: string;
72
+ readonly sourceId: string;
73
+ readonly sourceKind: "task" | "subtask";
74
+ readonly dependsOnId: string;
75
+ readonly dependsOnKind: "task" | "subtask";
76
+ readonly createdAt: number;
77
+ readonly updatedAt: number;
78
+ }
79
+
80
+ export interface BoardSnapshot {
81
+ readonly generatedAt: number;
82
+ readonly epics: readonly BoardSnapshotEpic[];
83
+ readonly tasks: readonly BoardSnapshotTask[];
84
+ readonly subtasks: readonly BoardSnapshotSubtask[];
85
+ readonly dependencies: readonly BoardSnapshotDependency[];
86
+ readonly counts: {
87
+ readonly epics: StatusCounts;
88
+ readonly tasks: StatusCounts;
89
+ readonly subtasks: StatusCounts;
90
+ readonly dependencies: number;
91
+ };
92
+ }
93
+
94
+ function normalizeStatusBucket(status: string): keyof Omit<StatusCounts, "total"> {
95
+ if (status === "todo") {
96
+ return "todo";
97
+ }
98
+
99
+ if (status === "blocked") {
100
+ return "blocked";
101
+ }
102
+
103
+ if (status === "in_progress" || status === "in-progress") {
104
+ return "inProgress";
105
+ }
106
+
107
+ if (status === "done") {
108
+ return "done";
109
+ }
110
+
111
+ return "other";
112
+ }
113
+
114
+ function countStatuses(records: readonly { readonly status: string }[]): StatusCounts {
115
+ const counts: {
116
+ total: number;
117
+ todo: number;
118
+ blocked: number;
119
+ inProgress: number;
120
+ done: number;
121
+ other: number;
122
+ } = {
123
+ total: records.length,
124
+ todo: 0,
125
+ blocked: 0,
126
+ inProgress: 0,
127
+ done: 0,
128
+ other: 0,
129
+ };
130
+
131
+ for (const record of records) {
132
+ const bucket = normalizeStatusBucket(record.status);
133
+ counts[bucket] += 1;
134
+ }
135
+
136
+ return counts;
137
+ }
138
+
139
+ function buildSearchFields(title: string, description: string): SearchFields {
140
+ return {
141
+ title,
142
+ description,
143
+ text: `${title}\n${description}`.trim(),
144
+ };
145
+ }
146
+
147
+ function mapDependency(record: DependencyRecord): BoardSnapshotDependency {
148
+ return {
149
+ id: record.id,
150
+ sourceId: record.sourceId,
151
+ sourceKind: record.sourceKind,
152
+ dependsOnId: record.dependsOnId,
153
+ dependsOnKind: record.dependsOnKind,
154
+ createdAt: record.createdAt,
155
+ updatedAt: record.updatedAt,
156
+ };
157
+ }
158
+
159
+ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
160
+ const generatedAt: number = Date.now();
161
+ const epics: readonly EpicRecord[] = domain.listEpics();
162
+ const tasks: readonly TaskRecord[] = domain.listTasks();
163
+ const subtasks: readonly SubtaskRecord[] = domain.listSubtasks();
164
+ const dependencies: BoardSnapshotDependency[] = [];
165
+
166
+ const tasksByEpic = new Map<string, TaskRecord[]>();
167
+ for (const task of tasks) {
168
+ const existing = tasksByEpic.get(task.epicId) ?? [];
169
+ existing.push(task);
170
+ tasksByEpic.set(task.epicId, existing);
171
+ }
172
+
173
+ const subtasksByTask = new Map<string, SubtaskRecord[]>();
174
+ for (const subtask of subtasks) {
175
+ const existing = subtasksByTask.get(subtask.taskId) ?? [];
176
+ existing.push(subtask);
177
+ subtasksByTask.set(subtask.taskId, existing);
178
+ }
179
+
180
+ const dependencyIdsBySource = new Map<string, string[]>();
181
+ const dependentIdsByTarget = new Map<string, string[]>();
182
+ for (const task of tasks) {
183
+ for (const dependency of domain.listDependencies(task.id)) {
184
+ dependencies.push(mapDependency(dependency));
185
+ const sourceIds = dependencyIdsBySource.get(dependency.sourceId) ?? [];
186
+ sourceIds.push(dependency.id);
187
+ dependencyIdsBySource.set(dependency.sourceId, sourceIds);
188
+ const dependentIds = dependentIdsByTarget.get(dependency.dependsOnId) ?? [];
189
+ dependentIds.push(dependency.id);
190
+ dependentIdsByTarget.set(dependency.dependsOnId, dependentIds);
191
+ }
192
+ }
193
+
194
+ for (const subtask of subtasks) {
195
+ for (const dependency of domain.listDependencies(subtask.id)) {
196
+ dependencies.push(mapDependency(dependency));
197
+ const sourceIds = dependencyIdsBySource.get(dependency.sourceId) ?? [];
198
+ sourceIds.push(dependency.id);
199
+ dependencyIdsBySource.set(dependency.sourceId, sourceIds);
200
+ const dependentIds = dependentIdsByTarget.get(dependency.dependsOnId) ?? [];
201
+ dependentIds.push(dependency.id);
202
+ dependentIdsByTarget.set(dependency.dependsOnId, dependentIds);
203
+ }
204
+ }
205
+
206
+ return {
207
+ generatedAt,
208
+ epics: epics.map((epic) => {
209
+ const epicTasks = tasksByEpic.get(epic.id) ?? [];
210
+ const epicSubtasks = epicTasks.flatMap((task) => subtasksByTask.get(task.id) ?? []);
211
+ return {
212
+ id: epic.id,
213
+ title: epic.title,
214
+ description: epic.description,
215
+ status: epic.status,
216
+ createdAt: epic.createdAt,
217
+ updatedAt: epic.updatedAt,
218
+ taskIds: epicTasks.map((task) => task.id),
219
+ counts: {
220
+ tasks: countStatuses(epicTasks),
221
+ subtasks: countStatuses(epicSubtasks),
222
+ },
223
+ search: buildSearchFields(epic.title, epic.description),
224
+ };
225
+ }),
226
+ tasks: tasks.map((task) => {
227
+ const taskSubtasks = subtasksByTask.get(task.id) ?? [];
228
+ const dependencyIds = dependencyIdsBySource.get(task.id) ?? [];
229
+ const dependentIds = dependentIdsByTarget.get(task.id) ?? [];
230
+ return {
231
+ id: task.id,
232
+ epicId: task.epicId,
233
+ title: task.title,
234
+ description: task.description,
235
+ status: task.status,
236
+ createdAt: task.createdAt,
237
+ updatedAt: task.updatedAt,
238
+ subtaskIds: taskSubtasks.map((subtask) => subtask.id),
239
+ dependencyIds,
240
+ dependentIds,
241
+ counts: {
242
+ subtasks: countStatuses(taskSubtasks),
243
+ dependencies: dependencyIds.length,
244
+ dependents: dependentIds.length,
245
+ },
246
+ search: buildSearchFields(task.title, task.description),
247
+ };
248
+ }),
249
+ subtasks: subtasks.map((subtask) => {
250
+ const dependencyIds = dependencyIdsBySource.get(subtask.id) ?? [];
251
+ const dependentIds = dependentIdsByTarget.get(subtask.id) ?? [];
252
+ return {
253
+ id: subtask.id,
254
+ taskId: subtask.taskId,
255
+ title: subtask.title,
256
+ description: subtask.description,
257
+ status: subtask.status,
258
+ createdAt: subtask.createdAt,
259
+ updatedAt: subtask.updatedAt,
260
+ dependencyIds,
261
+ dependentIds,
262
+ counts: {
263
+ dependencies: dependencyIds.length,
264
+ dependents: dependentIds.length,
265
+ },
266
+ search: buildSearchFields(subtask.title, subtask.description),
267
+ };
268
+ }),
269
+ dependencies,
270
+ counts: {
271
+ epics: countStatuses(epics),
272
+ tasks: countStatuses(tasks),
273
+ subtasks: countStatuses(subtasks),
274
+ dependencies: dependencies.length,
275
+ },
276
+ };
277
+ }
@@ -0,0 +1,43 @@
1
+ export const BOARD_ASSET_CONTRACT_VERSION = "1.0.0";
2
+ export const BOARD_BUNDLED_ASSET_DIRNAME = "assets";
3
+
4
+ export interface BoardAssetManifest {
5
+ readonly contractVersion: string;
6
+ readonly assetVersion: string;
7
+ readonly entryFile: string;
8
+ readonly files: readonly string[];
9
+ readonly assetDigest: string;
10
+ }
11
+
12
+ export interface BoardAssetPaths {
13
+ readonly sourceRoot: string;
14
+ readonly runtimeRoot: string;
15
+ readonly entryFile: string;
16
+ readonly manifestFile: string;
17
+ }
18
+
19
+ export type BoardInstallAction = "installed" | "reinstalled" | "updated" | "unchanged";
20
+
21
+ export interface BoardInstallResult {
22
+ readonly action: BoardInstallAction;
23
+ readonly paths: BoardAssetPaths;
24
+ readonly manifest: BoardAssetManifest;
25
+ }
26
+
27
+ export interface EnsureBoardInstalledOptions {
28
+ readonly workingDirectory?: string;
29
+ readonly assetVersion?: string;
30
+ readonly bundledAssetRoot?: string;
31
+ }
32
+
33
+ export class BoardInstallError extends Error {
34
+ readonly code: string;
35
+ readonly details: Record<string, unknown>;
36
+
37
+ constructor(code: string, message: string, details: Record<string, unknown> = {}) {
38
+ super(message);
39
+ this.name = "BoardInstallError";
40
+ this.code = code;
41
+ this.details = details;
42
+ }
43
+ }
@@ -0,0 +1,158 @@
1
+ import { parseArgs, readUnexpectedPositionals } from "./arg-parser";
2
+
3
+ import { ensureBoardInstalled, updateBoardInstallation } from "../board/install";
4
+ import { openBoardInBrowser, type OpenBrowserResult } from "../board/open-browser";
5
+ import { startBoardServer, type BoardServerInfo } from "../board/server";
6
+ import { BoardInstallError, type EnsureBoardInstalledOptions } from "../board/types";
7
+ import { failResult, okResult } from "../io/output";
8
+ import { type CliContext, type CliResult } from "../runtime/command-types";
9
+
10
+ type EnsureBoardInstalledFn = (options: EnsureBoardInstalledOptions) => ReturnType<typeof ensureBoardInstalled>;
11
+ type StartBoardServerFn = (options: { cwd: string }) => BoardServerInfo;
12
+ type OpenBoardInBrowserFn = (url: string) => Promise<OpenBrowserResult> | OpenBrowserResult;
13
+
14
+ let ensureInstalledImpl: EnsureBoardInstalledFn = ensureBoardInstalled;
15
+ let updateInstalledImpl: EnsureBoardInstalledFn = updateBoardInstallation;
16
+ let startBoardServerImpl: StartBoardServerFn = (options) => startBoardServer(options);
17
+ let openBoardInBrowserImpl: OpenBoardInBrowserFn = openBoardInBrowser;
18
+
19
+ function usageResult(): CliResult {
20
+ return failResult({
21
+ command: "board",
22
+ human: "Usage: trekoon board <open|update>",
23
+ data: {},
24
+ error: {
25
+ code: "invalid_subcommand",
26
+ message: "Invalid board subcommand",
27
+ },
28
+ });
29
+ }
30
+
31
+ function boardInstallOptions(context: CliContext): EnsureBoardInstalledOptions {
32
+ const bundledAssetRoot: string | undefined = process.env.TREKOON_BOARD_ASSET_ROOT;
33
+ return {
34
+ workingDirectory: context.cwd,
35
+ ...(bundledAssetRoot === undefined ? {} : { bundledAssetRoot }),
36
+ };
37
+ }
38
+
39
+ function boardInstallFailure(command: string, error: BoardInstallError): CliResult {
40
+ return failResult({
41
+ command,
42
+ human: error.message,
43
+ data: {
44
+ code: error.code,
45
+ ...error.details,
46
+ },
47
+ error: {
48
+ code: error.code,
49
+ message: error.message,
50
+ },
51
+ });
52
+ }
53
+
54
+ export function setBoardCommandHooksForTests(hooks: {
55
+ ensureInstalled?: EnsureBoardInstalledFn;
56
+ updateInstalled?: EnsureBoardInstalledFn;
57
+ startBoardServer?: StartBoardServerFn;
58
+ openBoardInBrowser?: OpenBoardInBrowserFn;
59
+ } | null): void {
60
+ ensureInstalledImpl = hooks?.ensureInstalled ?? ensureBoardInstalled;
61
+ updateInstalledImpl = hooks?.updateInstalled ?? updateBoardInstallation;
62
+ startBoardServerImpl = hooks?.startBoardServer ?? ((options) => startBoardServer(options));
63
+ openBoardInBrowserImpl = hooks?.openBoardInBrowser ?? openBoardInBrowser;
64
+ }
65
+
66
+ export async function runBoard(context: CliContext): Promise<CliResult> {
67
+ const parsed = parseArgs(context.args);
68
+ const subcommand: string | undefined = parsed.positional[0];
69
+
70
+ if (parsed.options.size > 0 || parsed.flags.size > 0) {
71
+ return failResult({
72
+ command: subcommand ? `board.${subcommand}` : "board",
73
+ human: "Board commands do not accept options yet.",
74
+ data: {
75
+ options: [...parsed.providedOptions].map((option) => `--${option}`),
76
+ },
77
+ error: {
78
+ code: "invalid_input",
79
+ message: "Board commands do not accept options",
80
+ },
81
+ });
82
+ }
83
+
84
+ if (!subcommand) {
85
+ return usageResult();
86
+ }
87
+
88
+ const unexpectedPositionals = readUnexpectedPositionals(parsed, 1);
89
+ if (unexpectedPositionals.length > 0) {
90
+ return failResult({
91
+ command: `board.${subcommand}`,
92
+ human: `Unexpected positional arguments: ${unexpectedPositionals.join(", ")}.`,
93
+ data: {
94
+ unexpectedPositionals,
95
+ },
96
+ error: {
97
+ code: "invalid_input",
98
+ message: "Unexpected positional arguments",
99
+ },
100
+ });
101
+ }
102
+
103
+ try {
104
+ switch (subcommand) {
105
+ case "update": {
106
+ const install = updateInstalledImpl(boardInstallOptions(context));
107
+ return okResult({
108
+ command: "board.update",
109
+ human: `Board assets ${install.action} at ${install.paths.runtimeRoot}`,
110
+ data: {
111
+ action: install.action,
112
+ paths: install.paths,
113
+ manifest: install.manifest,
114
+ },
115
+ });
116
+ }
117
+ case "open": {
118
+ const install = ensureInstalledImpl(boardInstallOptions(context));
119
+ const server = startBoardServerImpl({ cwd: context.cwd });
120
+ const launch = await openBoardInBrowserImpl(server.url);
121
+ return okResult({
122
+ command: "board.open",
123
+ human: [
124
+ `Board ready at ${server.url}`,
125
+ launch.launched
126
+ ? `Browser launched with ${launch.command}`
127
+ : `Browser launch failed: ${launch.errorMessage ?? "unknown failure"}`,
128
+ `Open manually if needed: ${server.fallbackUrl}`,
129
+ ].join("\n"),
130
+ data: {
131
+ install: {
132
+ action: install.action,
133
+ paths: install.paths,
134
+ manifest: install.manifest,
135
+ },
136
+ server: {
137
+ origin: server.origin,
138
+ url: server.url,
139
+ fallbackUrl: server.fallbackUrl,
140
+ hostname: server.hostname,
141
+ port: server.port,
142
+ token: server.token,
143
+ },
144
+ launch,
145
+ },
146
+ });
147
+ }
148
+ default:
149
+ return usageResult();
150
+ }
151
+ } catch (error: unknown) {
152
+ if (error instanceof BoardInstallError) {
153
+ return boardInstallFailure(`board.${subcommand}`, error);
154
+ }
155
+
156
+ throw error;
157
+ }
158
+ }
@@ -30,6 +30,7 @@ import {
30
30
  type CompactTaskSpec,
31
31
  type EpicRecord,
32
32
  type SearchEntityMatch,
33
+ type StatusCascadePlan,
33
34
  } from "../domain/types";
34
35
  import { formatHumanTable } from "../io/human-table";
35
36
  import { failResult, okResult } from "../io/output";
@@ -45,9 +46,13 @@ const LIST_VIEW_MODES = ["table", "compact"] as const;
45
46
  const DEFAULT_LIST_LIMIT = 10;
46
47
  const DEFAULT_OPEN_STATUSES = ["in_progress", "in-progress", "todo"] as const;
47
48
  const CREATE_OPTIONS = ["title", "t", "description", "d", "status", "s", "task", "subtask", "dep"] as const;
49
+ const LIST_OPTIONS = ["status", "s", "limit", "l", "cursor", "all", "view"] as const;
50
+ const SHOW_OPTIONS = ["view", "all"] as const;
48
51
  const SEARCH_OPTIONS = ["fields", "preview"] as const;
49
52
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
50
53
  const EXPAND_OPTIONS = ["task", "subtask", "dep"] as const;
54
+ const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "t"] as const;
55
+ const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
51
56
 
52
57
  function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
53
58
  if (rawStatuses === undefined) {
@@ -218,6 +223,44 @@ function appendLine(existing: string, line: string): string {
218
223
  return existing.length > 0 ? `${existing}\n${line}` : line;
219
224
  }
220
225
 
226
+ function isStatusCascadeUpdateStatus(status: string | undefined): status is (typeof STATUS_CASCADE_UPDATE_STATUSES)[number] {
227
+ return status === "done" || status === "todo";
228
+ }
229
+
230
+ function buildStatusCascadeData(plan: StatusCascadePlan): Record<string, unknown> {
231
+ return {
232
+ mode: "descendants",
233
+ root: {
234
+ kind: plan.rootKind,
235
+ id: plan.rootId,
236
+ },
237
+ targetStatus: plan.targetStatus,
238
+ atomic: plan.atomic,
239
+ changedIds: plan.changedIds,
240
+ unchangedIds: plan.unchangedIds,
241
+ counts: plan.counts,
242
+ };
243
+ }
244
+
245
+ function formatStatusCascadeHuman(entityLabel: string, plan: StatusCascadePlan): string {
246
+ return `Cascade updated ${entityLabel} ${plan.rootId} to ${plan.targetStatus} (${plan.counts.changed} changed, ${plan.counts.unchanged} unchanged; epics=${plan.counts.changedEpics}, tasks=${plan.counts.changedTasks}, subtasks=${plan.counts.changedSubtasks})`;
247
+ }
248
+
249
+ function failCascadeStatusUpdate(command: string, entityLabel: string, data: Record<string, unknown>): CliResult {
250
+ return failResult({
251
+ command,
252
+ human: `${entityLabel} descendant cascade requires --status done or --status todo and does not support --append, --description, or --title.`,
253
+ data: {
254
+ code: "invalid_input",
255
+ ...data,
256
+ },
257
+ error: {
258
+ code: "invalid_input",
259
+ message: `${entityLabel} descendant cascade requires status-only done/todo mode`,
260
+ },
261
+ });
262
+ }
263
+
221
264
  function formatEpicListTable(epics: readonly EpicRecord[]): string {
222
265
  const rows = epics.map((epic) => [epic.id, epic.title, epic.status]);
223
266
  return formatHumanTable(["ID", "TITLE", "STATUS"], rows, { wrapColumns: [1] });
@@ -826,6 +869,16 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
826
869
  });
827
870
  }
828
871
  case "list": {
872
+ const listUnknownOption = findUnknownOption(parsed, LIST_OPTIONS);
873
+ if (listUnknownOption !== undefined) {
874
+ return unknownOption("epic.list", listUnknownOption, LIST_OPTIONS);
875
+ }
876
+
877
+ const unexpectedListPositionals = readUnexpectedPositionals(parsed, 1);
878
+ if (unexpectedListPositionals.length > 0) {
879
+ return failUnexpectedPositionals("epic.list", unexpectedListPositionals);
880
+ }
881
+
829
882
  const missingListOption =
830
883
  readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
831
884
  readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
@@ -836,8 +889,8 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
836
889
  }
837
890
 
838
891
  const includeAll: boolean = hasFlag(parsed.flags, "all");
839
- const rawStatuses: string | undefined = readOption(parsed.options, "status");
840
- const rawLimit: string | undefined = readOption(parsed.options, "limit");
892
+ const rawStatuses: string | undefined = readOption(parsed.options, "status", "s");
893
+ const rawLimit: string | undefined = readOption(parsed.options, "limit", "l");
841
894
  const rawCursor: string | undefined = readOption(parsed.options, "cursor");
842
895
  const rawView: string | undefined = readOption(parsed.options, "view");
843
896
  const view = readEnumOption(parsed.options, VIEW_MODES, "view");
@@ -939,6 +992,16 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
939
992
  });
940
993
  }
941
994
  case "show": {
995
+ const showUnknownOption = findUnknownOption(parsed, SHOW_OPTIONS);
996
+ if (showUnknownOption !== undefined) {
997
+ return unknownOption("epic.show", showUnknownOption, SHOW_OPTIONS);
998
+ }
999
+
1000
+ const unexpectedShowPositionals = readUnexpectedPositionals(parsed, 2);
1001
+ if (unexpectedShowPositionals.length > 0) {
1002
+ return failUnexpectedPositionals("epic.show", unexpectedShowPositionals);
1003
+ }
1004
+
942
1005
  const missingShowOption = readMissingOptionValue(parsed.missingOptionValues, "view");
943
1006
  if (missingShowOption !== undefined) {
944
1007
  return failMissingOptionValue("epic.show", missingShowOption);
@@ -1167,6 +1230,16 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
1167
1230
  });
1168
1231
  }
1169
1232
  case "update": {
1233
+ const updateUnknownOption = findUnknownOption(parsed, UPDATE_OPTIONS);
1234
+ if (updateUnknownOption !== undefined) {
1235
+ return unknownOption("epic.update", updateUnknownOption, UPDATE_OPTIONS);
1236
+ }
1237
+
1238
+ const unexpectedUpdatePositionals = readUnexpectedPositionals(parsed, 2);
1239
+ if (unexpectedUpdatePositionals.length > 0) {
1240
+ return failUnexpectedPositionals("epic.update", unexpectedUpdatePositionals);
1241
+ }
1242
+
1170
1243
  const missingUpdateOption =
1171
1244
  readMissingOptionValue(parsed.missingOptionValues, "ids") ??
1172
1245
  readMissingOptionValue(parsed.missingOptionValues, "append") ??
@@ -1209,7 +1282,35 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
1209
1282
  });
1210
1283
  }
1211
1284
 
1212
- const hasBulkTarget = updateAll || ids.length > 0;
1285
+ const cascadeMode = updateAll && epicId.length > 0;
1286
+ if (cascadeMode) {
1287
+ if (title !== undefined || description !== undefined || append !== undefined || !isStatusCascadeUpdateStatus(status)) {
1288
+ return failCascadeStatusUpdate("epic.update", "Epic", {
1289
+ id: epicId,
1290
+ status,
1291
+ allowedStatuses: [...STATUS_CASCADE_UPDATE_STATUSES],
1292
+ fields: {
1293
+ title: title !== undefined,
1294
+ description: description !== undefined,
1295
+ append: append !== undefined,
1296
+ },
1297
+ });
1298
+ }
1299
+
1300
+ const cascade = mutations.updateEpicStatusCascade(epicId, status);
1301
+ const epic = domain.getEpicOrThrow(epicId);
1302
+
1303
+ return okResult({
1304
+ command: "epic.update",
1305
+ human: formatStatusCascadeHuman("epic", cascade),
1306
+ data: {
1307
+ epic,
1308
+ cascade: buildStatusCascadeData(cascade),
1309
+ },
1310
+ });
1311
+ }
1312
+
1313
+ const hasBulkTarget = (updateAll && epicId.length === 0) || ids.length > 0;
1213
1314
  if (hasBulkTarget) {
1214
1315
  if (epicId.length > 0) {
1215
1316
  return failResult({