trekoon 0.1.4 → 0.1.6

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.
@@ -61,7 +61,11 @@ Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to
61
61
 
62
62
  ## 1) Status Management
63
63
 
64
- ### Valid Statuses
64
+ ### Status values
65
+
66
+ Trekoon accepts any non-empty status string.
67
+
68
+ Recommended statuses for consistent workflows:
65
69
 
66
70
  | Status | Meaning |
67
71
  |--------|---------|
@@ -69,7 +73,7 @@ Use long flags (`--status`, `--description`, etc.) and ALWAYS append `--toon` to
69
73
  | `in_progress` | Actively being worked on |
70
74
  | `done` | Completed successfully |
71
75
 
72
- Note: `in-progress` (hyphenated) is equivalent to `in_progress`.
76
+ Note: `in-progress` (hyphenated) is treated the same as `in_progress` for default list ordering/filtering.
73
77
 
74
78
  ### When to Change Status
75
79
 
@@ -165,7 +169,7 @@ trekoon epic show <id> --all --toon
165
169
  trekoon task show <id> --all --toon
166
170
  ```
167
171
 
168
- - `epic list` / `task list` defaults:
172
+ - `epic list` / `task list` / `subtask list` defaults:
169
173
  - open work only (`in_progress`, `in-progress`, `todo`)
170
174
  - prioritized as `in_progress`/`in-progress` first, then `todo`
171
175
  - default limit `10`
package/README.md CHANGED
@@ -52,7 +52,8 @@ npm i -g trekoon
52
52
  - `trekoon subtask <create|list|update|delete>`
53
53
  - `trekoon dep <add|remove|list>`
54
54
  - `trekoon sync <status|pull|resolve>`
55
- - `trekoon skills install [--link --editor opencode|claude] [--to <path>]`
55
+ - `trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]`
56
+ - `trekoon skills update`
56
57
  - `trekoon wipe --yes`
57
58
 
58
59
  Global output modes:
@@ -77,10 +78,10 @@ Human view options:
77
78
 
78
79
  - List and show commands default to table output in human mode.
79
80
  - Use `--view compact` to restore compact pipe output.
80
- - `epic list` and `task list` support `--view table|compact`.
81
+ - `epic list`, `task list`, and `subtask list` support `--view table|compact`.
81
82
  - `epic show` and `task show` support `--view table|compact|tree|detail`.
82
83
 
83
- List defaults and filters (`epic list`, `task list`):
84
+ List defaults and filters (`epic list`, `task list`, `subtask list`):
84
85
 
85
86
  - Default scope: open work only (`in_progress`, `in-progress`, `todo`)
86
87
  - Default limit: `10`
@@ -173,17 +174,25 @@ You can also create a project-local editor link:
173
174
  trekoon skills install
174
175
  trekoon skills install --link --editor opencode
175
176
  trekoon skills install --link --editor claude
177
+ trekoon skills install --link --editor pi
176
178
  trekoon skills install --link --editor opencode --to ./.custom-editor/skills
179
+ trekoon skills update
177
180
  ```
178
181
 
179
182
  Path behavior:
180
183
 
181
184
  - Default opencode link path: `.opencode/skills/trekoon`
182
185
  - Default claude link path: `.claude/skills/trekoon`
186
+ - Default pi link path: `.pi/skills/trekoon`
183
187
  - `--to <path>` overrides the editor root for link creation only.
184
188
  - `--to` does **not** move or copy `SKILL.md` to that path.
185
189
  - Re-running install is idempotent: it refreshes `SKILL.md` and reuses/replaces
186
190
  the same symlink target.
191
+ - `trekoon skills update` is idempotent: it refreshes canonical
192
+ `.agents/skills/trekoon/SKILL.md` and reports default link states for
193
+ opencode/claude/pi as `missing`, `valid`, or `conflict`.
194
+ - Update does not mutate default links; conflicts are reported with actionable
195
+ path context.
187
196
  - If the link destination exists as a non-link path, install fails with an
188
197
  actionable conflict error.
189
198
 
@@ -215,7 +224,7 @@ Trekoon does not mutate global editor config directories.
215
224
  - [ ] `trekoon sync status` shows no unresolved conflicts
216
225
  - [ ] done tasks/subtasks are marked completed
217
226
  - [ ] dependency graph has no stale blockers
218
- - [ ] final AI check: `trekoon --json epic show <epic-id>`
227
+ - [ ] final AI check: `trekoon --toon epic show <epic-id>`
219
228
 
220
229
  ## Implementation principles
221
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "AI-first local issue tracker CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,7 +1,8 @@
1
1
  import { parseArgs } from "./arg-parser";
2
2
 
3
- import { DomainError } from "../domain/types";
3
+ import { MutationService } from "../domain/mutation-service";
4
4
  import { TrackerDomain } from "../domain/tracker-domain";
5
+ import { DomainError } from "../domain/types";
5
6
  import { failResult, okResult } from "../io/output";
6
7
  import { type CliContext, type CliResult } from "../runtime/command-types";
7
8
  import { openTrekoonDatabase } from "../storage/database";
@@ -42,10 +43,11 @@ export async function runDep(context: CliContext): Promise<CliResult> {
42
43
  const sourceId: string = parsed.positional[1] ?? "";
43
44
  const dependsOnId: string = parsed.positional[2] ?? "";
44
45
  const domain = new TrackerDomain(database.db);
46
+ const mutations = new MutationService(database.db, context.cwd);
45
47
 
46
48
  switch (subcommand) {
47
49
  case "add": {
48
- const dependency = domain.addDependency(sourceId, dependsOnId);
50
+ const dependency = mutations.addDependency(sourceId, dependsOnId);
49
51
 
50
52
  return okResult({
51
53
  command: "dep.add",
@@ -54,7 +56,7 @@ export async function runDep(context: CliContext): Promise<CliResult> {
54
56
  });
55
57
  }
56
58
  case "remove": {
57
- const removed: number = domain.removeDependency(sourceId, dependsOnId);
59
+ const removed: number = mutations.removeDependency(sourceId, dependsOnId);
58
60
 
59
61
  return okResult({
60
62
  command: "dep.remove",
@@ -1,7 +1,8 @@
1
1
  import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
2
2
 
3
- import { DomainError, type EpicRecord } from "../domain/types";
3
+ import { MutationService } from "../domain/mutation-service";
4
4
  import { TrackerDomain } from "../domain/tracker-domain";
5
+ import { DomainError, type EpicRecord } from "../domain/types";
5
6
  import { formatHumanTable } from "../io/human-table";
6
7
  import { failResult, okResult } from "../io/output";
7
8
  import { type CliContext, type CliResult } from "../runtime/command-types";
@@ -235,6 +236,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
235
236
  const parsed = parseArgs(context.args);
236
237
  const subcommand: string | undefined = parsed.positional[0];
237
238
  const domain = new TrackerDomain(database.db);
239
+ const mutations = new MutationService(database.db, context.cwd);
238
240
 
239
241
  switch (subcommand) {
240
242
  case "create": {
@@ -248,7 +250,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
248
250
  const title: string | undefined = readOption(parsed.options, "title", "t");
249
251
  const description: string | undefined = readOption(parsed.options, "description", "d");
250
252
  const status: string | undefined = readOption(parsed.options, "status", "s");
251
- const epic = domain.createEpic({
253
+ const epic = mutations.createEpic({
252
254
  title: title ?? "",
253
255
  description: description ?? "",
254
256
  status,
@@ -467,7 +469,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
467
469
 
468
470
  const targets = updateAll ? [...domain.listEpics()] : ids.map((id) => domain.getEpicOrThrow(id));
469
471
  const epics = targets.map((target) =>
470
- domain.updateEpic(target.id, {
472
+ mutations.updateEpic(target.id, {
471
473
  status,
472
474
  description: append === undefined ? undefined : appendLine(target.description, append),
473
475
  }),
@@ -500,7 +502,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
500
502
  append === undefined
501
503
  ? description
502
504
  : appendLine(domain.getEpicOrThrow(epicId).description, append);
503
- const epic = domain.updateEpic(epicId, { title, description: nextDescription, status });
505
+ const epic = mutations.updateEpic(epicId, { title, description: nextDescription, status });
504
506
 
505
507
  return okResult({
506
508
  command: "epic.update",
@@ -510,7 +512,7 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
510
512
  }
511
513
  case "delete": {
512
514
  const epicId: string = parsed.positional[1] ?? "";
513
- domain.deleteEpic(epicId);
515
+ mutations.deleteEpic(epicId);
514
516
 
515
517
  return okResult({
516
518
  command: "epic.delete",
@@ -36,13 +36,13 @@ const COMMAND_HELP: Record<string, string> = {
36
36
  task:
37
37
  "Usage: trekoon task <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --status <csv> | --limit <n> | --all | --view table|compact; show: compact=task summary, tree=hierarchy, detail=descriptions, and --all defaults to detail in machine modes; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
38
38
  subtask:
39
- "Usage: trekoon subtask <subcommand> [options] (list supports --view table|compact; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
39
+ "Usage: trekoon subtask <subcommand> [options] (list defaults: open statuses + limit 10; list flags: --task <id> | --status <csv> | --limit <n> | --all | --view table|compact; update bulk flags: --all | --ids <csv> with --append <text> and/or --status <status>)",
40
40
  dep: "Usage: trekoon dep <subcommand> [options]",
41
41
  events: "Usage: trekoon events prune [--dry-run] [--archive] [--retention-days <n>]",
42
42
  migrate: "Usage: trekoon migrate <status|rollback> [--to-version <n>]",
43
43
  sync: "Usage: trekoon sync <subcommand> [options]",
44
44
  skills:
45
- "Usage: trekoon skills install [--link --editor opencode|claude] [--to <path>] (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md)",
45
+ "Usage: trekoon skills install [--link --editor opencode|claude|pi] [--to <path>] | trekoon skills update (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md; update refreshes canonical SKILL and reports default link states)",
46
46
  help: "Usage: trekoon help [command] [--json|--toon]",
47
47
  };
48
48
 
@@ -17,7 +17,7 @@ const QUICKSTART_TEXT = [
17
17
  "3) Task details and description",
18
18
  "- Human list and show views default to table format.",
19
19
  "- Alternate list view: add --view compact.",
20
- "- task/epic list defaults: open work only (in_progress/in-progress, todo), max 10.",
20
+ "- task/epic/subtask list defaults: open work only (in_progress/in-progress, todo), max 10.",
21
21
  "- Filter list by status: --status in_progress,todo (CSV).",
22
22
  "- Change page size: --limit <n>. Show all statuses and all rows with --all.",
23
23
  "- --all cannot be combined with --status or --limit.",
@@ -7,10 +7,15 @@ import { hasFlag, parseArgs, readMissingOptionValue, readOption } from "./arg-pa
7
7
  import { failResult, okResult } from "../io/output";
8
8
  import { type CliContext, type CliResult } from "../runtime/command-types";
9
9
 
10
- const SKILLS_USAGE = "Usage: trekoon skills install [--link --editor opencode|claude] [--to <path>]";
11
- const EDITOR_NAMES = ["opencode", "claude"] as const;
10
+ const SKILLS_USAGE = [
11
+ "Usage:",
12
+ " trekoon skills install [--link --editor opencode|claude|pi] [--to <path>]",
13
+ " trekoon skills update",
14
+ ].join("\n");
15
+ const EDITOR_NAMES = ["opencode", "claude", "pi"] as const;
12
16
 
13
17
  type EditorName = (typeof EDITOR_NAMES)[number];
18
+ type LinkStateStatus = "missing" | "valid" | "conflict";
14
19
 
15
20
  interface InstallOutcome {
16
21
  readonly sourcePath: string;
@@ -20,6 +25,22 @@ interface InstallOutcome {
20
25
  readonly linkTarget: string | null;
21
26
  }
22
27
 
28
+ interface LinkState {
29
+ readonly editor: EditorName;
30
+ readonly linkPath: string;
31
+ readonly expectedTarget: string;
32
+ readonly status: LinkStateStatus;
33
+ readonly existingTarget: string | null;
34
+ readonly conflictCode: "non_link" | "wrong_target" | null;
35
+ }
36
+
37
+ interface UpdateOutcome {
38
+ readonly sourcePath: string;
39
+ readonly installedPath: string;
40
+ readonly installedDir: string;
41
+ readonly links: readonly LinkState[];
42
+ }
43
+
23
44
  function invalidArgs(message: string): CliResult {
24
45
  return failResult({
25
46
  command: "skills",
@@ -68,7 +89,61 @@ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | u
68
89
  return join(cwd, ".opencode", "skills");
69
90
  }
70
91
 
71
- return join(cwd, ".claude", "skills");
92
+ if (editor === "claude") {
93
+ return join(cwd, ".claude", "skills");
94
+ }
95
+
96
+ return join(cwd, ".pi", "skills");
97
+ }
98
+
99
+ function resolveDefaultLinkPath(cwd: string, editor: EditorName): string {
100
+ return join(resolveLinkRoot(cwd, editor, undefined), "trekoon");
101
+ }
102
+
103
+ function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; installedPath: string; installedDir: string } {
104
+ const sourcePath: string = resolveBundledSkillFilePath();
105
+ if (!existsSync(sourcePath)) {
106
+ return failResult({
107
+ command: "skills.install",
108
+ human: `Bundled skill asset not found at ${sourcePath}`,
109
+ data: {
110
+ code: "missing_asset",
111
+ sourcePath,
112
+ },
113
+ error: {
114
+ code: "missing_asset",
115
+ message: "Bundled skill asset not found",
116
+ },
117
+ });
118
+ }
119
+
120
+ const installedPath: string = join(cwd, ".agents", "skills", "trekoon", "SKILL.md");
121
+ const installedDir: string = dirname(installedPath);
122
+
123
+ try {
124
+ mkdirSync(installedDir, { recursive: true });
125
+ copyFileSync(sourcePath, installedPath);
126
+ } catch (error: unknown) {
127
+ const message = error instanceof Error ? error.message : "Unknown skills install failure";
128
+ return failResult({
129
+ command: "skills.install",
130
+ human: `Failed to install skill: ${message}`,
131
+ data: {
132
+ code: "install_failed",
133
+ message,
134
+ },
135
+ error: {
136
+ code: "install_failed",
137
+ message,
138
+ },
139
+ });
140
+ }
141
+
142
+ return {
143
+ sourcePath,
144
+ installedPath,
145
+ installedDir,
146
+ };
72
147
  }
73
148
 
74
149
  function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult | null {
@@ -120,6 +195,56 @@ function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult
120
195
  return null;
121
196
  }
122
197
 
198
+ function inspectDefaultLink(cwd: string, editor: EditorName, installedDir: string): LinkState {
199
+ const linkPath: string = resolveDefaultLinkPath(cwd, editor);
200
+ const expectedTarget: string = resolve(installedDir);
201
+
202
+ if (!existsSync(linkPath)) {
203
+ return {
204
+ editor,
205
+ linkPath,
206
+ expectedTarget,
207
+ status: "missing",
208
+ existingTarget: null,
209
+ conflictCode: null,
210
+ };
211
+ }
212
+
213
+ const entry = lstatSync(linkPath);
214
+ if (!entry.isSymbolicLink()) {
215
+ return {
216
+ editor,
217
+ linkPath,
218
+ expectedTarget,
219
+ status: "conflict",
220
+ existingTarget: null,
221
+ conflictCode: "non_link",
222
+ };
223
+ }
224
+
225
+ const existingRawTarget: string = readlinkSync(linkPath);
226
+ const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
227
+ if (existingTarget !== expectedTarget) {
228
+ return {
229
+ editor,
230
+ linkPath,
231
+ expectedTarget,
232
+ status: "conflict",
233
+ existingTarget,
234
+ conflictCode: "wrong_target",
235
+ };
236
+ }
237
+
238
+ return {
239
+ editor,
240
+ linkPath,
241
+ expectedTarget,
242
+ status: "valid",
243
+ existingTarget,
244
+ conflictCode: null,
245
+ };
246
+ }
247
+
123
248
  function runSkillsInstall(context: CliContext): CliResult {
124
249
  const parsed = parseArgs(context.args);
125
250
  const missingValue = readMissingOptionValue(parsed.missingOptionValues, "editor", "to");
@@ -150,11 +275,11 @@ function runSkillsInstall(context: CliContext): CliResult {
150
275
  }
151
276
 
152
277
  if (wantsLink && rawEditor === undefined) {
153
- return invalidArgs("skills install --link requires --editor opencode|claude.");
278
+ return invalidArgs("skills install --link requires --editor opencode|claude|pi.");
154
279
  }
155
280
 
156
281
  if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
157
- return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude", {
282
+ return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude, pi", {
158
283
  editor: rawEditor,
159
284
  allowedEditors: EDITOR_NAMES,
160
285
  });
@@ -162,38 +287,21 @@ function runSkillsInstall(context: CliContext): CliResult {
162
287
 
163
288
  const editor: EditorName | undefined = rawEditor as EditorName | undefined;
164
289
 
165
- const sourcePath: string = resolveBundledSkillFilePath();
166
- if (!existsSync(sourcePath)) {
167
- return failResult({
168
- command: "skills.install",
169
- human: `Bundled skill asset not found at ${sourcePath}`,
170
- data: {
171
- code: "missing_asset",
172
- sourcePath,
173
- },
174
- error: {
175
- code: "missing_asset",
176
- message: "Bundled skill asset not found",
177
- },
178
- });
290
+ const installResult = installCanonicalSkill(context.cwd);
291
+ if ("ok" in installResult) {
292
+ return installResult;
179
293
  }
180
294
 
181
- const installPath = join(context.cwd, ".agents", "skills", "trekoon", "SKILL.md");
182
- const installDir = dirname(installPath);
183
-
184
295
  let outcome: InstallOutcome;
185
296
 
186
297
  try {
187
- mkdirSync(installDir, { recursive: true });
188
- copyFileSync(sourcePath, installPath);
189
-
190
298
  let linkPath: string | null = null;
191
299
  let linkTarget: string | null = null;
192
300
 
193
301
  if (wantsLink && editor !== undefined) {
194
302
  const linkRoot: string = resolveLinkRoot(context.cwd, editor, rawTo);
195
303
  linkPath = join(linkRoot, "trekoon");
196
- linkTarget = installDir;
304
+ linkTarget = installResult.installedDir;
197
305
  const linkFailure = replaceOrCreateSymlink(linkPath, linkTarget);
198
306
  if (linkFailure) {
199
307
  return linkFailure;
@@ -201,9 +309,9 @@ function runSkillsInstall(context: CliContext): CliResult {
201
309
  }
202
310
 
203
311
  outcome = {
204
- sourcePath,
205
- installedPath: installPath,
206
- installedDir: installDir,
312
+ sourcePath: installResult.sourcePath,
313
+ installedPath: installResult.installedPath,
314
+ installedDir: installResult.installedDir,
207
315
  linkPath,
208
316
  linkTarget,
209
317
  };
@@ -249,6 +357,77 @@ function runSkillsInstall(context: CliContext): CliResult {
249
357
  });
250
358
  }
251
359
 
360
+ function runSkillsUpdate(context: CliContext): CliResult {
361
+ const parsed = parseArgs(context.args);
362
+ if (parsed.positional.length > 1) {
363
+ return invalidArgs("Unexpected positional arguments for skills update.");
364
+ }
365
+
366
+ if (parsed.flags.size > 0 || parsed.options.size > 0) {
367
+ return invalidArgs("skills update takes no options.");
368
+ }
369
+
370
+ const installResult = installCanonicalSkill(context.cwd);
371
+ if ("ok" in installResult) {
372
+ return failResult({
373
+ command: "skills.update",
374
+ human: installResult.human,
375
+ data: installResult.data,
376
+ error:
377
+ installResult.error ?? {
378
+ code: "install_failed",
379
+ message: "Failed to refresh canonical skill",
380
+ },
381
+ });
382
+ }
383
+
384
+ const links: readonly LinkState[] = EDITOR_NAMES.map((editor) =>
385
+ inspectDefaultLink(context.cwd, editor, installResult.installedDir),
386
+ );
387
+
388
+ const outcome: UpdateOutcome = {
389
+ sourcePath: installResult.sourcePath,
390
+ installedPath: installResult.installedPath,
391
+ installedDir: installResult.installedDir,
392
+ links,
393
+ };
394
+
395
+ const linkSummary: string = outcome.links
396
+ .map((entry) => {
397
+ if (entry.status === "missing") {
398
+ return `- ${entry.editor}: missing (${entry.linkPath})`;
399
+ }
400
+
401
+ if (entry.status === "valid") {
402
+ return `- ${entry.editor}: valid (${entry.linkPath} -> ${entry.expectedTarget})`;
403
+ }
404
+
405
+ if (entry.conflictCode === "non_link") {
406
+ return `- ${entry.editor}: conflict (non-link path at ${entry.linkPath})`;
407
+ }
408
+
409
+ return `- ${entry.editor}: conflict (points to ${entry.existingTarget})`;
410
+ })
411
+ .join("\n");
412
+
413
+ return okResult({
414
+ command: "skills.update",
415
+ human: [
416
+ "Updated Trekoon skill in canonical path.",
417
+ `Source: ${outcome.sourcePath}`,
418
+ `Installed file: ${outcome.installedPath}`,
419
+ "Default link states:",
420
+ linkSummary,
421
+ ].join("\n"),
422
+ data: {
423
+ sourcePath: outcome.sourcePath,
424
+ installedPath: outcome.installedPath,
425
+ installedDir: outcome.installedDir,
426
+ links: outcome.links,
427
+ },
428
+ });
429
+ }
430
+
252
431
  export async function runSkills(context: CliContext): Promise<CliResult> {
253
432
  const parsed = parseArgs(context.args);
254
433
  const subcommand: string | undefined = parsed.positional[0];
@@ -259,6 +438,8 @@ export async function runSkills(context: CliContext): Promise<CliResult> {
259
438
  switch (subcommand) {
260
439
  case "install":
261
440
  return runSkillsInstall(context);
441
+ case "update":
442
+ return runSkillsUpdate(context);
262
443
  default:
263
444
  return invalidArgs(`Unknown skills subcommand '${subcommand}'.`);
264
445
  }