trekoon 0.3.7 → 0.3.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.
@@ -85,6 +85,15 @@ export interface BoardSnapshot {
85
85
  };
86
86
  }
87
87
 
88
+ interface SnapshotDeltaSelection {
89
+ readonly epicIds?: readonly string[];
90
+ readonly taskIds?: readonly string[];
91
+ readonly subtaskIds?: readonly string[];
92
+ readonly dependencyIds?: readonly string[];
93
+ readonly deletedSubtaskIds?: readonly string[];
94
+ readonly deletedDependencyIds?: readonly string[];
95
+ }
96
+
88
97
  function normalizeStatusBucket(status: string): keyof Omit<StatusCounts, "total"> {
89
98
  if (status === "todo") return "todo";
90
99
  if (status === "blocked") return "blocked";
@@ -125,56 +134,32 @@ function mapDependency(record: DependencyRecord): BoardSnapshotDependency {
125
134
  };
126
135
  }
127
136
 
128
- export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
129
- const generatedAt = Date.now();
130
- const epics = domain.listEpics();
131
- const tasks = domain.listTasks();
132
- const subtasks = domain.listSubtasks();
133
- const sourceIds = [...tasks.map((task) => task.id), ...subtasks.map((subtask) => subtask.id)];
134
- const dependenciesBySourceId = domain.listDependenciesBySourceIds(sourceIds);
135
- const subtasksByTaskId = new Map<string, SubtaskRecord[]>();
136
- const tasksByEpicId = new Map<string, TaskRecord[]>();
137
+ function uniqueIds(ids: readonly string[]): string[] {
138
+ return [...new Set(ids.filter((id) => id.length > 0))];
139
+ }
140
+
141
+ function buildDependencyIndexes(dependenciesBySourceId: Map<string, readonly DependencyRecord[]>, sourceIds: readonly string[]) {
137
142
  const dependencyIdsBySource = new Map<string, string[]>();
138
143
  const blockedByIdsBySource = new Map<string, string[]>();
139
144
  const dependentIdsByTarget = new Map<string, string[]>();
140
145
  const blocksByTarget = new Map<string, string[]>();
141
146
  const dependencies: BoardSnapshotDependency[] = [];
142
147
 
143
- for (const task of tasks) {
144
- const existing = tasksByEpicId.get(task.epicId) ?? [];
145
- existing.push(task);
146
- tasksByEpicId.set(task.epicId, existing);
147
- }
148
-
149
- for (const subtask of subtasks) {
150
- const existing = subtasksByTaskId.get(subtask.taskId) ?? [];
151
- existing.push(subtask);
152
- subtasksByTaskId.set(subtask.taskId, existing);
153
- }
154
-
155
148
  for (const sourceId of sourceIds) {
156
149
  for (const dependency of dependenciesBySourceId.get(sourceId) ?? []) {
157
150
  dependencies.push(mapDependency(dependency));
158
-
159
- const sourceDependencyIds = dependencyIdsBySource.get(dependency.sourceId) ?? [];
160
- sourceDependencyIds.push(dependency.id);
161
- dependencyIdsBySource.set(dependency.sourceId, sourceDependencyIds);
162
-
163
- const sourceBlockedBy = blockedByIdsBySource.get(dependency.sourceId) ?? [];
164
- sourceBlockedBy.push(dependency.dependsOnId);
165
- blockedByIdsBySource.set(dependency.sourceId, sourceBlockedBy);
166
-
167
- const targetDependentIds = dependentIdsByTarget.get(dependency.dependsOnId) ?? [];
168
- targetDependentIds.push(dependency.id);
169
- dependentIdsByTarget.set(dependency.dependsOnId, targetDependentIds);
170
-
171
- const targetBlocks = blocksByTarget.get(dependency.dependsOnId) ?? [];
172
- targetBlocks.push(dependency.sourceId);
173
- blocksByTarget.set(dependency.dependsOnId, targetBlocks);
151
+ (dependencyIdsBySource.get(dependency.sourceId) ?? dependencyIdsBySource.set(dependency.sourceId, []).get(dependency.sourceId) ?? []).push(dependency.id);
152
+ (blockedByIdsBySource.get(dependency.sourceId) ?? blockedByIdsBySource.set(dependency.sourceId, []).get(dependency.sourceId) ?? []).push(dependency.dependsOnId);
153
+ (dependentIdsByTarget.get(dependency.dependsOnId) ?? dependentIdsByTarget.set(dependency.dependsOnId, []).get(dependency.dependsOnId) ?? []).push(dependency.id);
154
+ (blocksByTarget.get(dependency.dependsOnId) ?? blocksByTarget.set(dependency.dependsOnId, []).get(dependency.dependsOnId) ?? []).push(dependency.sourceId);
174
155
  }
175
156
  }
176
157
 
177
- const snapshotSubtasks: BoardSnapshotSubtask[] = subtasks.map((subtask) => ({
158
+ return { dependencies, dependencyIdsBySource, blockedByIdsBySource, dependentIdsByTarget, blocksByTarget };
159
+ }
160
+
161
+ function mapSnapshotSubtask(subtask: SubtaskRecord, indexes: ReturnType<typeof buildDependencyIndexes>): BoardSnapshotSubtask {
162
+ return {
178
163
  id: subtask.id,
179
164
  kind: "subtask",
180
165
  taskId: subtask.taskId,
@@ -184,12 +169,113 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
184
169
  owner: subtask.owner ?? null,
185
170
  createdAt: subtask.createdAt,
186
171
  updatedAt: subtask.updatedAt,
187
- blockedBy: blockedByIdsBySource.get(subtask.id) ?? [],
188
- blocks: blocksByTarget.get(subtask.id) ?? [],
189
- dependencyIds: dependencyIdsBySource.get(subtask.id) ?? [],
190
- dependentIds: dependentIdsByTarget.get(subtask.id) ?? [],
172
+ blockedBy: indexes.blockedByIdsBySource.get(subtask.id) ?? [],
173
+ blocks: indexes.blocksByTarget.get(subtask.id) ?? [],
174
+ dependencyIds: indexes.dependencyIdsBySource.get(subtask.id) ?? [],
175
+ dependentIds: indexes.dependentIdsByTarget.get(subtask.id) ?? [],
191
176
  searchText: [subtask.title, subtask.description, subtask.status].join(" ").toLowerCase(),
192
- }));
177
+ };
178
+ }
179
+
180
+ function mapSnapshotTask(task: TaskRecord, taskSubtasks: readonly BoardSnapshotSubtask[], indexes: ReturnType<typeof buildDependencyIndexes>): BoardSnapshotTask {
181
+ return {
182
+ id: task.id,
183
+ kind: "task",
184
+ epicId: task.epicId,
185
+ title: task.title,
186
+ description: task.description,
187
+ status: task.status,
188
+ owner: task.owner ?? null,
189
+ createdAt: task.createdAt,
190
+ updatedAt: task.updatedAt,
191
+ blockedBy: indexes.blockedByIdsBySource.get(task.id) ?? [],
192
+ blocks: indexes.blocksByTarget.get(task.id) ?? [],
193
+ dependencyIds: indexes.dependencyIdsBySource.get(task.id) ?? [],
194
+ dependentIds: indexes.dependentIdsByTarget.get(task.id) ?? [],
195
+ subtasks: taskSubtasks,
196
+ searchText: [task.title, task.description, task.status, ...taskSubtasks.map((subtask) => `${subtask.title} ${subtask.description} ${subtask.status}`)].join(" ").toLowerCase(),
197
+ };
198
+ }
199
+
200
+ function mapSnapshotEpic(epic: EpicRecord, epicTasks: readonly BoardSnapshotTask[]): BoardSnapshotEpic {
201
+ return {
202
+ id: epic.id,
203
+ title: epic.title,
204
+ description: epic.description,
205
+ status: epic.status,
206
+ createdAt: epic.createdAt,
207
+ updatedAt: epic.updatedAt,
208
+ taskIds: epicTasks.map((task) => task.id),
209
+ counts: deriveFlatCounts(epicTasks),
210
+ searchText: [epic.title, epic.description, ...epicTasks.map((task) => task.searchText)].join(" ").toLowerCase(),
211
+ };
212
+ }
213
+
214
+ export function buildBoardSnapshotDelta(domain: TrackerDomain, selection: SnapshotDeltaSelection): Record<string, unknown> {
215
+ const epicIds = uniqueIds(selection.epicIds ?? []);
216
+ const requestedTaskIds = uniqueIds(selection.taskIds ?? []);
217
+ const requestedSubtaskIds = uniqueIds(selection.subtaskIds ?? []);
218
+ const requestedDependencyIds = new Set(selection.dependencyIds ?? []);
219
+ const relatedTaskIds = uniqueIds([
220
+ ...requestedTaskIds,
221
+ ...requestedSubtaskIds.map((subtaskId) => domain.getSubtask(subtaskId)?.taskId ?? ""),
222
+ ]);
223
+ const tasks = relatedTaskIds.map((taskId) => domain.getTask(taskId)).filter((task): task is TaskRecord => task !== null);
224
+ const subtasksByTaskId = new Map<string, readonly SubtaskRecord[]>();
225
+ for (const task of tasks) {
226
+ subtasksByTaskId.set(task.id, domain.listSubtasks(task.id));
227
+ }
228
+ const allSubtasks = uniqueIds([
229
+ ...requestedSubtaskIds,
230
+ ...[...subtasksByTaskId.values()].flatMap((taskSubtasks) => taskSubtasks.map((subtask) => subtask.id)),
231
+ ]).map((subtaskId) => domain.getSubtask(subtaskId)).filter((subtask): subtask is SubtaskRecord => subtask !== null);
232
+ const sourceIds = uniqueIds([...tasks.map((task) => task.id), ...allSubtasks.map((subtask) => subtask.id)]);
233
+ const indexes = buildDependencyIndexes(domain.listDependenciesBySourceIds(sourceIds), sourceIds);
234
+ const snapshotSubtasksByTaskId = new Map<string, BoardSnapshotSubtask[]>();
235
+ for (const subtask of allSubtasks) {
236
+ const mappedSubtask = mapSnapshotSubtask(subtask, indexes);
237
+ const taskSubtasks = snapshotSubtasksByTaskId.get(subtask.taskId) ?? [];
238
+ taskSubtasks.push(mappedSubtask);
239
+ snapshotSubtasksByTaskId.set(subtask.taskId, taskSubtasks);
240
+ }
241
+ const snapshotTasks = tasks.map((task) => mapSnapshotTask(task, snapshotSubtasksByTaskId.get(task.id) ?? [], indexes));
242
+ const snapshotEpics = epicIds.map((epicId) => domain.getEpic(epicId)).filter((epic): epic is EpicRecord => epic !== null).map((epic) => mapSnapshotEpic(epic, snapshotTasks.filter((task) => task.epicId === epic.id)));
243
+
244
+ return {
245
+ generatedAt: Date.now(),
246
+ epics: snapshotEpics,
247
+ tasks: snapshotTasks.filter((task) => requestedTaskIds.includes(task.id)),
248
+ subtasks: allSubtasks.map((subtask) => mapSnapshotSubtask(subtask, indexes)).filter((subtask) => requestedSubtaskIds.includes(subtask.id)),
249
+ dependencies: indexes.dependencies.filter((dependency) => requestedDependencyIds.has(dependency.id)),
250
+ deletedSubtaskIds: [...(selection.deletedSubtaskIds ?? [])],
251
+ deletedDependencyIds: [...(selection.deletedDependencyIds ?? [])],
252
+ };
253
+ }
254
+
255
+ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
256
+ const generatedAt = Date.now();
257
+ const epics = domain.listEpics();
258
+ const tasks = domain.listTasks();
259
+ const subtasks = domain.listSubtasks();
260
+ const sourceIds = [...tasks.map((task) => task.id), ...subtasks.map((subtask) => subtask.id)];
261
+ const dependenciesBySourceId = domain.listDependenciesBySourceIds(sourceIds);
262
+ const subtasksByTaskId = new Map<string, SubtaskRecord[]>();
263
+ const tasksByEpicId = new Map<string, TaskRecord[]>();
264
+ const indexes = buildDependencyIndexes(dependenciesBySourceId, sourceIds);
265
+
266
+ for (const task of tasks) {
267
+ const existing = tasksByEpicId.get(task.epicId) ?? [];
268
+ existing.push(task);
269
+ tasksByEpicId.set(task.epicId, existing);
270
+ }
271
+
272
+ for (const subtask of subtasks) {
273
+ const existing = subtasksByTaskId.get(subtask.taskId) ?? [];
274
+ existing.push(subtask);
275
+ subtasksByTaskId.set(subtask.taskId, existing);
276
+ }
277
+
278
+ const snapshotSubtasks: BoardSnapshotSubtask[] = subtasks.map((subtask) => mapSnapshotSubtask(subtask, indexes));
193
279
  const snapshotSubtasksByTaskId = new Map<string, BoardSnapshotSubtask[]>();
194
280
  for (const subtask of snapshotSubtasks) {
195
281
  const existing = snapshotSubtasksByTaskId.get(subtask.taskId) ?? [];
@@ -197,31 +283,7 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
197
283
  snapshotSubtasksByTaskId.set(subtask.taskId, existing);
198
284
  }
199
285
 
200
- const snapshotTasks: BoardSnapshotTask[] = tasks.map((task) => {
201
- const taskSubtasks = snapshotSubtasksByTaskId.get(task.id) ?? [];
202
- return {
203
- id: task.id,
204
- kind: "task",
205
- epicId: task.epicId,
206
- title: task.title,
207
- description: task.description,
208
- status: task.status,
209
- owner: task.owner ?? null,
210
- createdAt: task.createdAt,
211
- updatedAt: task.updatedAt,
212
- blockedBy: blockedByIdsBySource.get(task.id) ?? [],
213
- blocks: blocksByTarget.get(task.id) ?? [],
214
- dependencyIds: dependencyIdsBySource.get(task.id) ?? [],
215
- dependentIds: dependentIdsByTarget.get(task.id) ?? [],
216
- subtasks: taskSubtasks,
217
- searchText: [
218
- task.title,
219
- task.description,
220
- task.status,
221
- ...taskSubtasks.map((subtask) => `${subtask.title} ${subtask.description} ${subtask.status}`),
222
- ].join(" ").toLowerCase(),
223
- };
224
- });
286
+ const snapshotTasks: BoardSnapshotTask[] = tasks.map((task) => mapSnapshotTask(task, snapshotSubtasksByTaskId.get(task.id) ?? [], indexes));
225
287
  const taskSearchTextByEpicId = new Map<string, string[]>();
226
288
  for (const task of snapshotTasks) {
227
289
  const existing = taskSearchTextByEpicId.get(task.epicId) ?? [];
@@ -229,32 +291,19 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
229
291
  taskSearchTextByEpicId.set(task.epicId, existing);
230
292
  }
231
293
 
232
- const snapshotEpics: BoardSnapshotEpic[] = epics.map((epic) => {
233
- const epicTasks = tasksByEpicId.get(epic.id) ?? [];
234
- return {
235
- id: epic.id,
236
- title: epic.title,
237
- description: epic.description,
238
- status: epic.status,
239
- createdAt: epic.createdAt,
240
- updatedAt: epic.updatedAt,
241
- taskIds: epicTasks.map((task) => task.id),
242
- counts: deriveFlatCounts(epicTasks),
243
- searchText: [epic.title, epic.description, ...(taskSearchTextByEpicId.get(epic.id) ?? [])].join(" ").toLowerCase(),
244
- };
245
- });
294
+ const snapshotEpics: BoardSnapshotEpic[] = epics.map((epic) => mapSnapshotEpic(epic, snapshotTasks.filter((task) => task.epicId === epic.id)));
246
295
 
247
296
  return {
248
297
  generatedAt,
249
298
  epics: snapshotEpics,
250
299
  tasks: snapshotTasks,
251
300
  subtasks: snapshotSubtasks,
252
- dependencies,
301
+ dependencies: indexes.dependencies,
253
302
  counts: {
254
303
  epics: countStatuses(epics),
255
304
  tasks: countStatuses(tasks),
256
305
  subtasks: countStatuses(subtasks),
257
- dependencies: dependencies.length,
306
+ dependencies: indexes.dependencies.length,
258
307
  },
259
308
  };
260
309
  }
@@ -121,7 +121,7 @@ export async function runBoard(context: CliContext): Promise<CliResult> {
121
121
  return okResult({
122
122
  command: "board.open",
123
123
  human: [
124
- `Board ready at ${server.url}`,
124
+ `Board ready at ${server.fallbackUrl}`,
125
125
  launch.launched
126
126
  ? `Browser launched with ${launch.command}`
127
127
  : `Browser launch failed: ${launch.errorMessage ?? "unknown failure"}`,
@@ -36,7 +36,12 @@ import {
36
36
  import { formatHumanTable } from "../io/human-table";
37
37
  import { failResult, okResult } from "../io/output";
38
38
  import { type CliContext, type CliResult } from "../runtime/command-types";
39
+ import { buildEpicExportBundle } from "../export/build-epic-export-bundle";
40
+ import { resolveExportPath } from "../export/path";
41
+ import { renderMarkdown } from "../export/render-markdown";
42
+ import { atomicWrite, ExportWriteError } from "../export/write";
39
43
  import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
44
+ import { resolveStoragePaths } from "../storage/path";
40
45
 
41
46
  function formatEpic(epic: EpicRecord): string {
42
47
  return `${epic.id} | ${epic.title} | ${epic.status}`;
@@ -53,6 +58,7 @@ const SEARCH_OPTIONS = ["fields", "preview"] as const;
53
58
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
54
59
  const EXPAND_OPTIONS = ["task", "subtask", "dep"] as const;
55
60
  const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "t"] as const;
61
+ const EXPORT_OPTIONS = ["path", "overwrite"] as const;
56
62
  const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
57
63
 
58
64
  function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
@@ -1472,10 +1478,87 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
1472
1478
  data: { id: epicId },
1473
1479
  });
1474
1480
  }
1481
+ case "export": {
1482
+ const epicId: string = parsed.positional[1] ?? "";
1483
+ const exportUnknown = findUnknownOption(parsed, EXPORT_OPTIONS);
1484
+ if (exportUnknown !== undefined) {
1485
+ return unknownOption("epic.export", exportUnknown, EXPORT_OPTIONS);
1486
+ }
1487
+
1488
+ const missingExportOption = readMissingOptionValue(parsed.missingOptionValues, "path");
1489
+ if (missingExportOption !== undefined) {
1490
+ return failMissingOptionValue("epic.export", missingExportOption);
1491
+ }
1492
+
1493
+ if (!epicId) {
1494
+ return failResult({
1495
+ command: "epic.export",
1496
+ human: "Usage: trekoon epic export <epic-id> [--path <path>] [--overwrite]",
1497
+ data: {},
1498
+ error: { code: "invalid_input", message: "Missing epic ID" },
1499
+ });
1500
+ }
1501
+
1502
+ const bundle = buildEpicExportBundle(domain, epicId);
1503
+ const markdown = renderMarkdown(bundle);
1504
+ const storagePaths = resolveStoragePaths(context.cwd);
1505
+ const customPath = readOption(parsed.options, "path");
1506
+ const overwrite = hasFlag(parsed.flags, "overwrite");
1507
+
1508
+ const exportPath = resolveExportPath({
1509
+ customPath,
1510
+ epicId: bundle.epic.id,
1511
+ epicTitle: bundle.epic.title,
1512
+ worktreeRoot: storagePaths.worktreeRoot,
1513
+ cwd: context.cwd,
1514
+ });
1515
+
1516
+ try {
1517
+ const result = atomicWrite({ path: exportPath, content: markdown, overwrite });
1518
+ const action = result.overwritten ? "Updated" : "Exported";
1519
+ return okResult({
1520
+ command: "epic.export",
1521
+ human: `${action} epic to ${result.path}`,
1522
+ data: {
1523
+ epicId: bundle.epic.id,
1524
+ path: result.path,
1525
+ overwritten: result.overwritten,
1526
+ summary: bundle.summary,
1527
+ },
1528
+ });
1529
+ } catch (error: unknown) {
1530
+ if (error instanceof ExportWriteError) {
1531
+ return failResult({
1532
+ command: "epic.export",
1533
+ human: error.message,
1534
+ data: { path: exportPath, epicId: bundle.epic.id },
1535
+ error: { code: error.code, message: error.message },
1536
+ });
1537
+ }
1538
+ const fsCode = typeof error === "object" && error !== null && "code" in error ? (error as { code: string }).code : null;
1539
+ if (fsCode === "EACCES" || fsCode === "EPERM" || fsCode === "EROFS") {
1540
+ return failResult({
1541
+ command: "epic.export",
1542
+ human: `Permission denied: cannot write to ${exportPath}`,
1543
+ data: { path: exportPath, epicId: bundle.epic.id, fsError: fsCode },
1544
+ error: { code: "permission_denied", message: `Permission denied: ${exportPath}` },
1545
+ });
1546
+ }
1547
+ if (fsCode === "EISDIR") {
1548
+ return failResult({
1549
+ command: "epic.export",
1550
+ human: `Path is a directory, not a file: ${exportPath}`,
1551
+ data: { path: exportPath, epicId: bundle.epic.id, fsError: fsCode },
1552
+ error: { code: "invalid_path", message: `Path is a directory: ${exportPath}` },
1553
+ });
1554
+ }
1555
+ throw error;
1556
+ }
1557
+ }
1475
1558
  default:
1476
1559
  return failResult({
1477
1560
  command: "epic",
1478
- human: "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress>",
1561
+ human: "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export>",
1479
1562
  data: {
1480
1563
  args: context.args,
1481
1564
  },
@@ -103,7 +103,7 @@ const BOARD_HELP = [
103
103
  ].join("\n");
104
104
 
105
105
  const EPIC_HELP = [
106
- "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress> [options]",
106
+ "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export> [options]",
107
107
  "",
108
108
  "Create:",
109
109
  " trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
@@ -149,6 +149,24 @@ const EPIC_HELP = [
149
149
  "Progress:",
150
150
  " trekoon epic progress <epic-id>",
151
151
  " Returns done/in_progress/blocked/todo counts, readyCount, and nextCandidate.",
152
+ "",
153
+ "Export:",
154
+ " trekoon epic export <epic-id> [--path <path>] [--overwrite]",
155
+ " Writes a Markdown snapshot of the epic including tasks, subtasks, dependencies,",
156
+ " external node stubs, and warnings. The Markdown is a point-in-time artifact;",
157
+ " the database remains the source of truth.",
158
+ "",
159
+ " Default path: <worktree-root>/plans/<slugified-title>.md",
160
+ " --path <path> Custom output path (relative or absolute).",
161
+ " With extension (e.g. docs/plan.md): creates that file.",
162
+ " Without extension (e.g. docs/plans): creates the default-named file inside.",
163
+ " --overwrite Resave if the file already exists.",
164
+ "",
165
+ " Examples:",
166
+ " trekoon epic export abc-123",
167
+ " trekoon epic export abc-123 --path docs/plan.md # writes docs/plan.md",
168
+ " trekoon epic export abc-123 --path docs/plans # writes docs/plans/<title>.md",
169
+ " trekoon epic export abc-123 --overwrite",
152
170
  ].join("\n");
153
171
 
154
172
  const TASK_HELP = [
@@ -4,6 +4,16 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
4
4
  const QUICKSTART_TEXT = [
5
5
  "Trekoon quickstart",
6
6
  "",
7
+ "Human workflow:",
8
+ " 1. Gather context through discussion, brainstorming, or research.",
9
+ " 2. Run: trekoon plan <goal>",
10
+ " Use this when you want Trekoon to turn the goal into an execution-ready epic.",
11
+ " 3. Run: trekoon <epic-id>",
12
+ " Use this to inspect the epic, next ready work, and blockers before execution.",
13
+ " 4. Run: trekoon <epic-id> execute",
14
+ " Use this when you want the agent to keep working until the epic is done,",
15
+ " all remaining work is blocked, or it needs your input.",
16
+ "",
7
17
  "Agents: always use --toon on every command.",
8
18
  "Aligned with: .agents/skills/trekoon/SKILL.md",
9
19
  "",
@@ -49,6 +59,7 @@ const QUICKSTART_TEXT = [
49
59
  " Bulk update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
50
60
  " Ready queue: trekoon --toon task ready [--limit <n>] [--epic <id>]",
51
61
  " Next candidate: trekoon --toon task next [--epic <id>]",
62
+ " Export epic to MD: trekoon --toon epic export <epic-id> [--path <path>] [--overwrite]",
52
63
  "",
53
64
  "6) List and view defaults",
54
65
  " Default scope: open work (in_progress, todo), limit 10.",
@@ -142,6 +153,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
142
153
  "trekoon --toon task list --status in_progress,todo --limit 20",
143
154
  "trekoon --toon task list --cursor <n>",
144
155
  "trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
156
+ "trekoon --toon epic export <epic-id>",
145
157
  ],
146
158
  machineExamples: [
147
159
  "trekoon --toon quickstart",
@@ -156,6 +168,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
156
168
  "trekoon --toon task ready --limit 5",
157
169
  "trekoon --toon task next",
158
170
  "trekoon --toon dep reverse <task-or-subtask-id>",
171
+ "trekoon --toon epic export <epic-id>",
159
172
  ],
160
173
  wipeWarning: {
161
174
  command: "trekoon wipe --yes",