trekoon 0.4.1 → 0.4.3

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 (58) hide show
  1. package/.agents/skills/trekoon/SKILL.md +97 -765
  2. package/.agents/skills/trekoon/reference/execution-with-team.md +91 -141
  3. package/.agents/skills/trekoon/reference/execution.md +188 -159
  4. package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
  5. package/.agents/skills/trekoon/reference/planning.md +213 -213
  6. package/.agents/skills/trekoon/reference/status-machine.md +21 -0
  7. package/.agents/skills/trekoon/reference/sync.md +82 -0
  8. package/README.md +29 -8
  9. package/docs/ai-agents.md +65 -6
  10. package/docs/commands.md +149 -5
  11. package/docs/machine-contracts.md +123 -0
  12. package/docs/quickstart.md +55 -3
  13. package/package.json +1 -1
  14. package/src/board/assets/app.js +47 -13
  15. package/src/board/assets/components/Component.js +20 -8
  16. package/src/board/assets/components/Workspace.js +9 -3
  17. package/src/board/assets/components/helpers.js +4 -0
  18. package/src/board/assets/runtime/delegation.js +8 -0
  19. package/src/board/assets/runtime/focus-trap.js +48 -0
  20. package/src/board/assets/state/actions.js +45 -4
  21. package/src/board/assets/state/api.js +304 -17
  22. package/src/board/assets/state/store.js +82 -11
  23. package/src/board/assets/state/url.js +10 -0
  24. package/src/board/assets/state/utils.js +2 -1
  25. package/src/board/event-bus.ts +81 -0
  26. package/src/board/routes.ts +430 -40
  27. package/src/board/server.ts +86 -10
  28. package/src/board/snapshot.ts +6 -0
  29. package/src/board/wal-watcher.ts +313 -0
  30. package/src/commands/board.ts +52 -17
  31. package/src/commands/epic.ts +7 -9
  32. package/src/commands/error-utils.ts +54 -1
  33. package/src/commands/help.ts +75 -10
  34. package/src/commands/migrate.ts +153 -24
  35. package/src/commands/quickstart.ts +7 -0
  36. package/src/commands/skills.ts +17 -5
  37. package/src/commands/subtask.ts +71 -10
  38. package/src/commands/suggest.ts +6 -13
  39. package/src/commands/task.ts +137 -88
  40. package/src/domain/batch-validation.ts +329 -0
  41. package/src/domain/cascade-planner.ts +412 -0
  42. package/src/domain/dependency-rules.ts +15 -0
  43. package/src/domain/mutation-service.ts +842 -187
  44. package/src/domain/search.ts +113 -0
  45. package/src/domain/tracker-domain.ts +167 -693
  46. package/src/domain/types.ts +56 -2
  47. package/src/export/render-markdown.ts +1 -2
  48. package/src/index.ts +37 -0
  49. package/src/runtime/cli-shell.ts +44 -0
  50. package/src/runtime/daemon.ts +700 -0
  51. package/src/storage/backup.ts +166 -0
  52. package/src/storage/database.ts +268 -4
  53. package/src/storage/migrations.ts +441 -22
  54. package/src/storage/path.ts +8 -0
  55. package/src/storage/schema.ts +5 -1
  56. package/src/sync/event-writes.ts +38 -11
  57. package/src/sync/git-context.ts +226 -8
  58. package/src/sync/service.ts +679 -156
@@ -97,6 +97,7 @@ export interface EpicRecord {
97
97
  readonly status: string;
98
98
  readonly createdAt: number;
99
99
  readonly updatedAt: number;
100
+ readonly version: number;
100
101
  }
101
102
 
102
103
  export interface TaskRecord {
@@ -108,6 +109,7 @@ export interface TaskRecord {
108
109
  readonly owner: string | null;
109
110
  readonly createdAt: number;
110
111
  readonly updatedAt: number;
112
+ readonly version: number;
111
113
  }
112
114
 
113
115
  export interface SubtaskRecord {
@@ -119,6 +121,7 @@ export interface SubtaskRecord {
119
121
  readonly owner: string | null;
120
122
  readonly createdAt: number;
121
123
  readonly updatedAt: number;
124
+ readonly version: number;
122
125
  }
123
126
 
124
127
  export interface DependencyRecord {
@@ -255,14 +258,65 @@ export interface StatusCascadePlan {
255
258
  readonly counts: StatusCascadeCounts;
256
259
  }
257
260
 
261
+ export const ERROR_CODES = {
262
+ ALREADY_DONE: "already_done",
263
+ ALREADY_RESOLVED: "already_resolved",
264
+ AMBIGUOUS_LEGACY_STATE: "ambiguous_legacy_state",
265
+ BACKPRESSURE: "backpressure",
266
+ BACKUP_ALREADY_EXISTS: "backup_already_exists",
267
+ BACKUP_DATABASE_MISSING: "backup_database_missing",
268
+ BACKUP_FAILED: "backup_failed",
269
+ CANCELLED: "cancelled",
270
+ CONFIRMATION_REQUIRED: "confirmation_required",
271
+ CONFLICT_SET_CHANGED: "conflict_set_changed",
272
+ DAEMON_START_FAILED: "daemon_start_failed",
273
+ DATABASE_BUSY: "database_busy",
274
+ DEPENDENCY_BLOCKED: "dependency_blocked",
275
+ DISALLOWED_FIELD: "disallowed_field",
276
+ EVENTS_FAILED: "events_failed",
277
+ INSTALL_FAILED: "install_failed",
278
+ INTERNAL_ERROR: "internal_error",
279
+ INVALID_ARGS: "invalid_args",
280
+ INVALID_DEPENDENCY: "invalid_dependency",
281
+ INVALID_INPUT: "invalid_input",
282
+ INVALID_PATH: "invalid_path",
283
+ INVALID_SOURCE: "invalid_source",
284
+ INVALID_STATE: "invalid_state",
285
+ INVALID_SUBCOMMAND: "invalid_subcommand",
286
+ LEGACY_IMPORT_FAILED: "legacy_import_failed",
287
+ MIGRATE_FAILED: "migrate_failed",
288
+ MIGRATION_DOWN_UNSUPPORTED: "migration_down_unsupported",
289
+ MISSING_ASSET: "missing_asset",
290
+ NO_MATCHING_CONFLICTS: "no_matching_conflicts",
291
+ NOT_FOUND: "not_found",
292
+ ORPHANED_EXTERNAL_NODE: "orphaned_external_node",
293
+ OUTSIDE_REPO_TARGET: "outside_repo_target",
294
+ PERMISSION_DENIED: "permission_denied",
295
+ PRECONDITION_FAILED: "precondition_failed",
296
+ ROW_NOT_FOUND: "row_not_found",
297
+ STATUS_TRANSITION_INVALID: "status_transition_invalid",
298
+ STREAM_UNAVAILABLE: "stream_unavailable",
299
+ SYNC_FAILED: "sync_failed",
300
+ TRACKED_IGNORED_MISMATCH: "tracked_ignored_mismatch",
301
+ UNAUTHORIZED: "unauthorized",
302
+ UNHANDLED_COMMAND: "unhandled_command",
303
+ UNKNOWN_COMMAND: "unknown_command",
304
+ UNKNOWN_OPTION: "unknown_option",
305
+ UNSUPPORTED_ENTITY_KIND: "unsupported_entity_kind",
306
+ UPDATE_FAILED: "update_failed",
307
+ WRONG_ENTITY_TYPE: "wrong_entity_type",
308
+ } as const;
309
+
310
+ export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
311
+
258
312
  export interface DomainErrorShape {
259
- readonly code: string;
313
+ readonly code: ErrorCode;
260
314
  readonly message: string;
261
315
  readonly details?: Record<string, unknown>;
262
316
  }
263
317
 
264
318
  export class DomainError extends Error {
265
- readonly code: string;
319
+ readonly code: ErrorCode;
266
320
  readonly details?: Record<string, unknown>;
267
321
 
268
322
  constructor(input: DomainErrorShape) {
@@ -124,8 +124,7 @@ function renderTaskIndex(lines: string[], bundle: ExportBundle): void {
124
124
  lines.push("| # | Title | Status | Subtasks |");
125
125
  lines.push("|---|-------|--------|----------|");
126
126
 
127
- for (let i = 0; i < bundle.tasks.length; i++) {
128
- const task = bundle.tasks[i];
127
+ for (const [i, task] of bundle.tasks.entries()) {
129
128
  const subtaskCount = bundle.subtasks.filter((s) => s.taskId === task.id).length;
130
129
  const anchor = taskAnchor(task);
131
130
  lines.push(`| ${i + 1} | [${escapeTableCell(escapeInlineText(task.title))}](#${anchor}) | ${task.status} | ${subtaskCount} |`);
package/src/index.ts CHANGED
@@ -4,6 +4,43 @@ import { executeShell, parseInvocation, renderShellResult } from "./runtime/cli-
4
4
 
5
5
  export async function run(argv: readonly string[] = process.argv.slice(2)): Promise<void> {
6
6
  const parsed = parseInvocation(argv);
7
+
8
+ // Daemon path is opt-in (TREKOON_DAEMON=1 or --daemon). The `serve`
9
+ // subcommand always runs in-process so it can host the daemon.
10
+ const daemonRequested: boolean = parsed.wantsDaemon || process.env.TREKOON_DAEMON === "1";
11
+ if (daemonRequested && parsed.command !== "serve") {
12
+ const { tryDaemonDispatch, PostWriteError } = await import("./runtime/daemon");
13
+ // Strip --daemon from the argv we forward (the server has its own dispatch).
14
+ const forwarded: readonly string[] = argv.filter((token: string): boolean => token !== "--daemon");
15
+ try {
16
+ const daemonResult = await tryDaemonDispatch(forwarded);
17
+ if (daemonResult !== null) {
18
+ if (daemonResult.stdout.length > 0) {
19
+ process.stdout.write(daemonResult.stdout);
20
+ }
21
+ if (daemonResult.stderr.length > 0) {
22
+ process.stderr.write(daemonResult.stderr);
23
+ }
24
+ process.exitCode = daemonResult.exitCode;
25
+ return;
26
+ }
27
+ // Fall through to one-shot CLI when no daemon is reachable (pre-write
28
+ // transport failure — request never made it onto the wire).
29
+ } catch (error: unknown) {
30
+ // Post-write failure: the request bytes were already flushed to the
31
+ // daemon. The mutation may have committed. Refuse to silently re-run
32
+ // the command in-process; exit non-zero so the caller can decide.
33
+ if (error instanceof PostWriteError) {
34
+ process.stderr.write(
35
+ `trekoon: daemon may have committed; do not retry: ${error.message}\n`,
36
+ );
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+
7
44
  const result = await executeShell(parsed);
8
45
  const rendered: string = renderShellResult(result, parsed.mode, parsed.compatibilityMode, { compact: parsed.compact });
9
46
 
@@ -36,6 +36,7 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
36
36
  "suggest",
37
37
  "update",
38
38
  "wipe",
39
+ "serve",
39
40
  ];
40
41
 
41
42
  export interface ParsedInvocation {
@@ -48,6 +49,7 @@ export interface ParsedInvocation {
48
49
  readonly args: readonly string[];
49
50
  readonly wantsHelp: boolean;
50
51
  readonly wantsVersion: boolean;
52
+ readonly wantsDaemon: boolean;
51
53
  }
52
54
 
53
55
  export interface ParseInvocationOptions {
@@ -62,6 +64,7 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
62
64
  let compatibilityModeMissingValue = false;
63
65
  let wantsHelp = false;
64
66
  let wantsVersion = false;
67
+ let wantsDaemon = false;
65
68
  const positionals: string[] = [];
66
69
 
67
70
  for (let index = 0; index < argv.length; index += 1) {
@@ -95,6 +98,11 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
95
98
  continue;
96
99
  }
97
100
 
101
+ if (token === "--daemon") {
102
+ wantsDaemon = true;
103
+ continue;
104
+ }
105
+
98
106
  if (token === "--compat") {
99
107
  const maybeValue: string | undefined = argv[index + 1];
100
108
  if (!maybeValue || maybeValue.startsWith("--")) {
@@ -123,6 +131,7 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
123
131
  args: positionals.slice(1),
124
132
  wantsHelp,
125
133
  wantsVersion,
134
+ wantsDaemon,
126
135
  };
127
136
  }
128
137
 
@@ -403,6 +412,11 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
403
412
  // Route `trekoon update` to `trekoon skills update` internally.
404
413
  result = await runSkills({ ...context, args: ["update", ...context.args] });
405
414
  break;
415
+ case "serve":
416
+ // Experimental: run the daemon in the foreground. Loaded lazily so the
417
+ // default cold path stays free of node:net imports.
418
+ result = await runServe(context);
419
+ break;
406
420
  default:
407
421
  result = failResult({
408
422
  command: "shell",
@@ -418,3 +432,33 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
418
432
 
419
433
  return withStorageRootDiagnostics(result, cwd);
420
434
  }
435
+
436
+ async function runServe(context: CliContext): Promise<CliResult> {
437
+ // Lazy import keeps node:net out of the normal one-shot dispatch path.
438
+ const { runDaemonForeground, resolveDaemonSocketPath } = await import("./daemon");
439
+ const socketPath: string = resolveDaemonSocketPath(context.cwd);
440
+
441
+ try {
442
+ await runDaemonForeground({ cwd: context.cwd, silent: context.mode !== "human" });
443
+ return okResult({
444
+ command: "serve",
445
+ human: `Daemon stopped (socket: ${socketPath})`,
446
+ data: {
447
+ socketPath,
448
+ status: "stopped",
449
+ experimental: true,
450
+ },
451
+ });
452
+ } catch (error: unknown) {
453
+ const message: string = error instanceof Error ? error.message : String(error);
454
+ return failResult({
455
+ command: "serve",
456
+ human: `Daemon failed to start: ${message}`,
457
+ data: { socketPath },
458
+ error: {
459
+ code: "daemon_start_failed",
460
+ message,
461
+ },
462
+ });
463
+ }
464
+ }