trekoon 0.4.1 → 0.4.2

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 (54) hide show
  1. package/.agents/skills/trekoon/SKILL.md +20 -577
  2. package/.agents/skills/trekoon/reference/execution-with-team.md +21 -9
  3. package/.agents/skills/trekoon/reference/execution.md +246 -7
  4. package/.agents/skills/trekoon/reference/planning.md +138 -1
  5. package/.agents/skills/trekoon/reference/status-machine.md +21 -0
  6. package/.agents/skills/trekoon/reference/sync.md +129 -0
  7. package/README.md +8 -1
  8. package/docs/ai-agents.md +17 -2
  9. package/docs/commands.md +147 -3
  10. package/docs/machine-contracts.md +123 -0
  11. package/docs/quickstart.md +52 -0
  12. package/package.json +1 -1
  13. package/src/board/assets/app.js +45 -13
  14. package/src/board/assets/components/Component.js +22 -8
  15. package/src/board/assets/components/Workspace.js +9 -3
  16. package/src/board/assets/components/helpers.js +4 -0
  17. package/src/board/assets/runtime/delegation.js +8 -0
  18. package/src/board/assets/runtime/focus-trap.js +48 -0
  19. package/src/board/assets/state/actions.js +42 -4
  20. package/src/board/assets/state/api.js +284 -11
  21. package/src/board/assets/state/store.js +79 -11
  22. package/src/board/assets/state/url.js +10 -0
  23. package/src/board/assets/state/utils.js +2 -1
  24. package/src/board/event-bus.ts +72 -0
  25. package/src/board/routes.ts +412 -33
  26. package/src/board/server.ts +77 -8
  27. package/src/board/wal-watcher.ts +302 -0
  28. package/src/commands/board.ts +52 -17
  29. package/src/commands/epic.ts +7 -9
  30. package/src/commands/error-utils.ts +54 -1
  31. package/src/commands/help.ts +69 -4
  32. package/src/commands/migrate.ts +153 -24
  33. package/src/commands/quickstart.ts +7 -0
  34. package/src/commands/subtask.ts +71 -10
  35. package/src/commands/suggest.ts +6 -13
  36. package/src/commands/task.ts +137 -88
  37. package/src/domain/batch-validation.ts +329 -0
  38. package/src/domain/cascade-planner.ts +412 -0
  39. package/src/domain/dependency-rules.ts +15 -0
  40. package/src/domain/mutation-service.ts +828 -192
  41. package/src/domain/search.ts +113 -0
  42. package/src/domain/tracker-domain.ts +150 -680
  43. package/src/domain/types.ts +53 -2
  44. package/src/index.ts +37 -0
  45. package/src/runtime/cli-shell.ts +44 -0
  46. package/src/runtime/daemon.ts +639 -0
  47. package/src/storage/backup.ts +166 -0
  48. package/src/storage/database.ts +261 -4
  49. package/src/storage/migrations.ts +422 -20
  50. package/src/storage/path.ts +8 -0
  51. package/src/storage/schema.ts +5 -1
  52. package/src/sync/event-writes.ts +38 -11
  53. package/src/sync/git-context.ts +226 -8
  54. package/src/sync/service.ts +650 -147
@@ -255,14 +255,65 @@ export interface StatusCascadePlan {
255
255
  readonly counts: StatusCascadeCounts;
256
256
  }
257
257
 
258
+ export const ERROR_CODES = {
259
+ ALREADY_DONE: "already_done",
260
+ ALREADY_RESOLVED: "already_resolved",
261
+ AMBIGUOUS_LEGACY_STATE: "ambiguous_legacy_state",
262
+ BACKPRESSURE: "backpressure",
263
+ BACKUP_ALREADY_EXISTS: "backup_already_exists",
264
+ BACKUP_DATABASE_MISSING: "backup_database_missing",
265
+ BACKUP_FAILED: "backup_failed",
266
+ CANCELLED: "cancelled",
267
+ CONFIRMATION_REQUIRED: "confirmation_required",
268
+ CONFLICT_SET_CHANGED: "conflict_set_changed",
269
+ DAEMON_START_FAILED: "daemon_start_failed",
270
+ DATABASE_BUSY: "database_busy",
271
+ DEPENDENCY_BLOCKED: "dependency_blocked",
272
+ DISALLOWED_FIELD: "disallowed_field",
273
+ EVENTS_FAILED: "events_failed",
274
+ INSTALL_FAILED: "install_failed",
275
+ INTERNAL_ERROR: "internal_error",
276
+ INVALID_ARGS: "invalid_args",
277
+ INVALID_DEPENDENCY: "invalid_dependency",
278
+ INVALID_INPUT: "invalid_input",
279
+ INVALID_PATH: "invalid_path",
280
+ INVALID_SOURCE: "invalid_source",
281
+ INVALID_STATE: "invalid_state",
282
+ INVALID_SUBCOMMAND: "invalid_subcommand",
283
+ LEGACY_IMPORT_FAILED: "legacy_import_failed",
284
+ MIGRATE_FAILED: "migrate_failed",
285
+ MIGRATION_DOWN_UNSUPPORTED: "migration_down_unsupported",
286
+ MISSING_ASSET: "missing_asset",
287
+ NO_MATCHING_CONFLICTS: "no_matching_conflicts",
288
+ NOT_FOUND: "not_found",
289
+ ORPHANED_EXTERNAL_NODE: "orphaned_external_node",
290
+ OUTSIDE_REPO_TARGET: "outside_repo_target",
291
+ PERMISSION_DENIED: "permission_denied",
292
+ PRECONDITION_FAILED: "precondition_failed",
293
+ ROW_NOT_FOUND: "row_not_found",
294
+ STATUS_TRANSITION_INVALID: "status_transition_invalid",
295
+ STREAM_UNAVAILABLE: "stream_unavailable",
296
+ SYNC_FAILED: "sync_failed",
297
+ TRACKED_IGNORED_MISMATCH: "tracked_ignored_mismatch",
298
+ UNAUTHORIZED: "unauthorized",
299
+ UNHANDLED_COMMAND: "unhandled_command",
300
+ UNKNOWN_COMMAND: "unknown_command",
301
+ UNKNOWN_OPTION: "unknown_option",
302
+ UNSUPPORTED_ENTITY_KIND: "unsupported_entity_kind",
303
+ UPDATE_FAILED: "update_failed",
304
+ WRONG_ENTITY_TYPE: "wrong_entity_type",
305
+ } as const;
306
+
307
+ export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
308
+
258
309
  export interface DomainErrorShape {
259
- readonly code: string;
310
+ readonly code: ErrorCode;
260
311
  readonly message: string;
261
312
  readonly details?: Record<string, unknown>;
262
313
  }
263
314
 
264
315
  export class DomainError extends Error {
265
- readonly code: string;
316
+ readonly code: ErrorCode;
266
317
  readonly details?: Record<string, unknown>;
267
318
 
268
319
  constructor(input: DomainErrorShape) {
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
+ }