trekoon 0.2.6 → 0.2.7

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.
@@ -0,0 +1,134 @@
1
+ # Quickstart
2
+
3
+ Use this guide for the shortest path from an empty repo to an active Trekoon
4
+ workflow.
5
+
6
+ ## Understand the storage model first
7
+
8
+ Trekoon is local-first, but inside git repos and worktrees it is **repo-shared**.
9
+ Every worktree for the same repository resolves to one shared `.trekoon`
10
+ directory and one shared `.trekoon/trekoon.db` database.
11
+
12
+ - `worktreeRoot` identifies the current checkout.
13
+ - `sharedStorageRoot` identifies the repository root that owns `.trekoon`.
14
+ - `databaseFile` points at the shared SQLite database.
15
+ - `.trekoon` stays gitignored because the DB is operational state, not source
16
+ code.
17
+
18
+ Outside git repos, Trekoon falls back to the current working directory.
19
+
20
+ ## Initialize
21
+
22
+ ```bash
23
+ trekoon init
24
+ trekoon --version
25
+ ```
26
+
27
+ If an agent is driving the workflow, use the machine-readable form:
28
+
29
+ ```bash
30
+ trekoon --toon init
31
+ trekoon --toon sync status
32
+ ```
33
+
34
+ Bootstrap rules:
35
+
36
+ - Run `trekoon --toon init` once per repository to create or re-bootstrap the
37
+ shared storage root.
38
+ - Run `trekoon --toon sync status` before agent work to inspect diagnostics.
39
+ - If diagnostics report `recoveryRequired`, a tracked or ignored mismatch, or an
40
+ ambiguous recovery path, stop and repair setup before continuing.
41
+
42
+ ## Create an epic, task, and subtask
43
+
44
+ ```bash
45
+ trekoon epic create --title "Agent backlog stabilization" --description "Track stabilization work" --status todo
46
+ trekoon task create --title "Implement sync status" --description "Add status reporting" --epic <epic-id> --status todo
47
+ trekoon subtask create --task <task-id> --title "Add cursor model" --status todo
48
+ ```
49
+
50
+ Useful follow-up reads:
51
+
52
+ ```bash
53
+ trekoon task list
54
+ trekoon task list --status done
55
+ trekoon task list --limit 25
56
+ trekoon task list --all --view compact
57
+ ```
58
+
59
+ ## Prefer one-shot planning when the graph is already known
60
+
61
+ If you already know the epic tree, create the epic, tasks, subtasks, and
62
+ dependencies in one call:
63
+
64
+ ```bash
65
+ trekoon epic create \
66
+ --title "Batch command rollout" \
67
+ --description "Ship one-shot planning workflows" \
68
+ --task "task-a|First task|First description|todo" \
69
+ --task "task-b|Second task|Second description|todo" \
70
+ --subtask "@task-a|sub-a|First subtask|Subtask description|todo" \
71
+ --dep "@task-b|@task-a" \
72
+ --dep "@sub-a|@task-a"
73
+ ```
74
+
75
+ Use this when:
76
+
77
+ - the epic does not exist yet
78
+ - later records need to reference earlier records with `@temp-key`
79
+ - you want one atomic create step and one machine response with mappings and
80
+ counts
81
+
82
+ ## Add dependencies
83
+
84
+ ```bash
85
+ trekoon dep add <task-id> <depends-on-id>
86
+ trekoon dep list <task-id>
87
+ ```
88
+
89
+ ## Use batch commands for larger updates
90
+
91
+ When one call needs to create or link multiple records, prefer the transactional
92
+ batch commands:
93
+
94
+ | Need | Command |
95
+ | --- | --- |
96
+ | Create multiple tasks under one epic | `trekoon task create-many --epic <epic-id> --task ...` |
97
+ | Create multiple subtasks under one task | `trekoon subtask create-many <task-id> --subtask ...` |
98
+ | Add multiple dependency edges | `trekoon dep add-many --dep ...` |
99
+ | Expand an existing epic with linked records | `trekoon epic expand <epic-id> ...` |
100
+
101
+ These commands validate the whole batch before applying changes, so a bad input
102
+ fails the whole operation instead of leaving partial state behind.
103
+
104
+ ## Close or reopen a whole tree in one update
105
+
106
+ When you need to manually finish or reopen an entire epic or task tree, use the
107
+ positional-ID cascade form of `update --all`:
108
+
109
+ ```bash
110
+ trekoon epic update <epic-id> --all --status done
111
+ trekoon epic update <epic-id> --all --status todo
112
+ trekoon task update <task-id> --all --status done
113
+ trekoon task update <task-id> --all --status todo
114
+ trekoon subtask update <subtask-id> --all --status done
115
+ ```
116
+
117
+ Rules:
118
+
119
+ - `epic update <id> --all --status done|todo` updates the epic and all
120
+ descendants atomically
121
+ - `task update <id> --all --status done|todo` updates the task and all
122
+ descendant subtasks atomically
123
+ - `subtask update <id> --all --status done|todo` is accepted for consistency,
124
+ but it only updates that one subtask
125
+ - Positional-ID cascade mode is status-only; do not combine it with `--append`,
126
+ `--description`, `--title`, or `--ids`
127
+ - If any epic/task descendant is blocked by an unresolved external dependency,
128
+ the whole cascade fails with no partial writes
129
+
130
+ ## What to read next
131
+
132
+ - [Command reference](commands.md)
133
+ - [AI agents and the Trekoon skill](ai-agents.md)
134
+ - [Machine contracts](machine-contracts.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "bin",
12
+ "docs",
12
13
  "src",
13
14
  ".agents/skills/trekoon/SKILL.md",
14
15
  "README.md",
@@ -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({
@@ -131,14 +131,20 @@ const EPIC_HELP = [
131
131
  " - --preview and --apply are mutually exclusive",
132
132
  "",
133
133
  "Update behavior:",
134
- " Bulk target flags:",
135
- " --all | --ids <csv>",
136
- " Bulk fields:",
137
- " --append <text> and/or --status <status>",
134
+ " Repo-wide bulk mode:",
135
+ " trekoon epic update --all --append <text> [--status <status>]",
136
+ " trekoon epic update --ids <csv> --append <text> [--status <status>]",
137
+ " - preserves existing per-row bulk behavior; not one atomic multi-row update",
138
+ " Descendant cascade mode:",
139
+ " trekoon epic update <epic-id> --all --status done|todo",
140
+ " - cascades atomically through descendant tasks and subtasks",
141
+ " - blocked descendants abort the whole update",
142
+ " - cascade mode supports only --status done|todo",
143
+ " - do not combine positional id + --all with --ids, --append, --description, or --title",
138
144
  ].join("\n");
139
145
 
140
146
  const TASK_HELP = [
141
- "Usage: trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete> [options]",
147
+ "Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete> [options]",
142
148
  "",
143
149
  "Create-many behavior:",
144
150
  " trekoon task create-many --epic <epic-id> --task <spec> [--task <spec> ...]",
@@ -188,10 +194,16 @@ const TASK_HELP = [
188
194
  " - --preview and --apply are mutually exclusive",
189
195
  "",
190
196
  "Update behavior:",
191
- " Bulk target flags:",
192
- " --all | --ids <csv>",
193
- " Bulk fields:",
194
- " --append <text> and/or --status <status>",
197
+ " Repo-wide bulk mode:",
198
+ " trekoon task update --all --append <text> [--status <status>]",
199
+ " trekoon task update --ids <csv> --append <text> [--status <status>]",
200
+ " - preserves existing per-row bulk behavior; not one atomic multi-row update",
201
+ " Descendant cascade mode:",
202
+ " trekoon task update <task-id> --all --status done|todo",
203
+ " - cascades atomically through descendant subtasks",
204
+ " - blocked descendants abort the whole update",
205
+ " - cascade mode supports only --status done|todo",
206
+ " - do not combine positional id + --all with --ids, --append, --description, or --title",
195
207
  ].join("\n");
196
208
 
197
209
  const SUBTASK_HELP = [
@@ -228,10 +240,16 @@ const SUBTASK_HELP = [
228
240
  " - --preview and --apply are mutually exclusive",
229
241
  "",
230
242
  "Update behavior:",
231
- " Bulk target flags:",
232
- " --all | --ids <csv>",
233
- " Bulk fields:",
234
- " --append <text> and/or --status <status>",
243
+ " Repo-wide bulk mode:",
244
+ " trekoon subtask update --all --append <text> [--status <status>]",
245
+ " trekoon subtask update --ids <csv> --append <text> [--status <status>]",
246
+ " - preserves existing per-row bulk behavior; not one atomic multi-row update",
247
+ " Positional-id cascade syntax:",
248
+ " trekoon subtask update <subtask-id> --all --status done|todo",
249
+ " - accepted for contract consistency",
250
+ " - behaves like a normal single-subtask status update",
251
+ " - positional id + --all supports only --status done|todo",
252
+ " - do not combine positional id + --all with --ids, --append, --description, or --title",
235
253
  ].join("\n");
236
254
 
237
255
  const DEP_HELP = [
@@ -33,9 +33,13 @@ function formatSubtask(subtask: SubtaskRecord): string {
33
33
  const VIEW_MODES = ["table", "compact"] as const;
34
34
  const DEFAULT_SUBTASK_LIST_LIMIT = 10;
35
35
  const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
36
+ const CREATE_OPTIONS = ["task", "t", "title", "description", "d", "status", "s"] as const;
37
+ const LIST_OPTIONS = ["task", "t", "status", "s", "limit", "l", "cursor", "all", "view"] as const;
36
38
  const SEARCH_OPTIONS = ["fields", "preview"] as const;
37
39
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
38
40
  const CREATE_MANY_OPTIONS = ["task", "t", "subtask"] as const;
41
+ const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title"] as const;
42
+ const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
39
43
 
40
44
  function parseIdsOption(rawIds: string | undefined): string[] {
41
45
  if (rawIds === undefined) {
@@ -168,6 +172,25 @@ function appendLine(existing: string, line: string): string {
168
172
  return existing.length > 0 ? `${existing}\n${line}` : line;
169
173
  }
170
174
 
175
+ function isStatusCascadeUpdateStatus(status: string | undefined): status is (typeof STATUS_CASCADE_UPDATE_STATUSES)[number] {
176
+ return status === "done" || status === "todo";
177
+ }
178
+
179
+ function failCascadeStatusUpdate(command: string, entityLabel: string, data: Record<string, unknown>): CliResult {
180
+ return failResult({
181
+ command,
182
+ human: `${entityLabel} cascade mode requires --status done or --status todo and does not support --append, --description, or --title.`,
183
+ data: {
184
+ code: "invalid_input",
185
+ ...data,
186
+ },
187
+ error: {
188
+ code: "invalid_input",
189
+ message: `${entityLabel} cascade mode requires status-only done/todo input`,
190
+ },
191
+ });
192
+ }
193
+
171
194
  function formatSubtaskListTable(subtasks: readonly SubtaskRecord[]): string {
172
195
  return formatHumanTable(
173
196
  ["ID", "TASK", "TITLE", "STATUS"],
@@ -359,6 +382,16 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
359
382
 
360
383
  switch (subcommand) {
361
384
  case "create": {
385
+ const createUnknownOption = findUnknownOption(parsed, CREATE_OPTIONS);
386
+ if (createUnknownOption !== undefined) {
387
+ return unknownOption("subtask.create", createUnknownOption, CREATE_OPTIONS);
388
+ }
389
+
390
+ const unexpectedCreatePositionals = readUnexpectedPositionals(parsed, 3);
391
+ if (unexpectedCreatePositionals.length > 0) {
392
+ return failUnexpectedPositionals("subtask.create", unexpectedCreatePositionals);
393
+ }
394
+
362
395
  const missingCreateOption =
363
396
  readMissingOptionValue(parsed.missingOptionValues, "task", "t") ??
364
397
  readMissingOptionValue(parsed.missingOptionValues, "description", "d") ??
@@ -447,6 +480,16 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
447
480
  });
448
481
  }
449
482
  case "list": {
483
+ const listUnknownOption = findUnknownOption(parsed, LIST_OPTIONS);
484
+ if (listUnknownOption !== undefined) {
485
+ return unknownOption("subtask.list", listUnknownOption, LIST_OPTIONS);
486
+ }
487
+
488
+ const unexpectedListPositionals = readUnexpectedPositionals(parsed, 2);
489
+ if (unexpectedListPositionals.length > 0) {
490
+ return failUnexpectedPositionals("subtask.list", unexpectedListPositionals);
491
+ }
492
+
450
493
  const missingListOption =
451
494
  readMissingOptionValue(parsed.missingOptionValues, "view") ??
452
495
  readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
@@ -731,6 +774,16 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
731
774
  });
732
775
  }
733
776
  case "update": {
777
+ const updateUnknownOption = findUnknownOption(parsed, UPDATE_OPTIONS);
778
+ if (updateUnknownOption !== undefined) {
779
+ return unknownOption("subtask.update", updateUnknownOption, UPDATE_OPTIONS);
780
+ }
781
+
782
+ const unexpectedUpdatePositionals = readUnexpectedPositionals(parsed, 2);
783
+ if (unexpectedUpdatePositionals.length > 0) {
784
+ return failUnexpectedPositionals("subtask.update", unexpectedUpdatePositionals);
785
+ }
786
+
734
787
  const missingUpdateOption =
735
788
  readMissingOptionValue(parsed.missingOptionValues, "ids") ??
736
789
  readMissingOptionValue(parsed.missingOptionValues, "append") ??
@@ -773,7 +826,31 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
773
826
  });
774
827
  }
775
828
 
776
- const hasBulkTarget = updateAll || ids.length > 0;
829
+ const cascadeMode = updateAll && subtaskId.length > 0;
830
+ if (cascadeMode) {
831
+ if (title !== undefined || description !== undefined || append !== undefined || !isStatusCascadeUpdateStatus(status)) {
832
+ return failCascadeStatusUpdate("subtask.update", "Subtask", {
833
+ id: subtaskId,
834
+ status,
835
+ allowedStatuses: [...STATUS_CASCADE_UPDATE_STATUSES],
836
+ fields: {
837
+ title: title !== undefined,
838
+ description: description !== undefined,
839
+ append: append !== undefined,
840
+ },
841
+ });
842
+ }
843
+
844
+ const subtask = mutations.updateSubtask(subtaskId, { status });
845
+
846
+ return okResult({
847
+ command: "subtask.update",
848
+ human: `Updated subtask ${formatSubtask(subtask)}`,
849
+ data: { subtask },
850
+ });
851
+ }
852
+
853
+ const hasBulkTarget = (updateAll && subtaskId.length === 0) || ids.length > 0;
777
854
  if (hasBulkTarget) {
778
855
  if (subtaskId.length > 0) {
779
856
  return failResult({