@katyella/legio 0.1.3 → 0.2.0

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 (110) hide show
  1. package/CHANGELOG.md +40 -3
  2. package/README.md +15 -8
  3. package/agents/builder.md +11 -10
  4. package/agents/coordinator.md +36 -27
  5. package/agents/cto.md +9 -8
  6. package/agents/gateway.md +28 -12
  7. package/agents/lead.md +45 -30
  8. package/agents/merger.md +4 -4
  9. package/agents/monitor.md +10 -9
  10. package/agents/reviewer.md +8 -8
  11. package/agents/scout.md +10 -10
  12. package/agents/supervisor.md +60 -45
  13. package/package.json +2 -2
  14. package/src/agents/hooks-deployer.test.ts +46 -41
  15. package/src/agents/hooks-deployer.ts +10 -9
  16. package/src/agents/manifest.test.ts +6 -2
  17. package/src/agents/overlay.test.ts +9 -7
  18. package/src/agents/overlay.ts +29 -7
  19. package/src/commands/agents.test.ts +1 -5
  20. package/src/commands/clean.test.ts +2 -5
  21. package/src/commands/clean.ts +25 -1
  22. package/src/commands/completions.test.ts +1 -1
  23. package/src/commands/completions.ts +26 -7
  24. package/src/commands/coordinator.test.ts +78 -78
  25. package/src/commands/coordinator.ts +92 -47
  26. package/src/commands/costs.test.ts +2 -6
  27. package/src/commands/dashboard.test.ts +2 -5
  28. package/src/commands/doctor.test.ts +2 -6
  29. package/src/commands/down.ts +3 -3
  30. package/src/commands/errors.test.ts +2 -6
  31. package/src/commands/feed.test.ts +2 -6
  32. package/src/commands/gateway.test.ts +39 -13
  33. package/src/commands/gateway.ts +95 -7
  34. package/src/commands/hooks.test.ts +2 -5
  35. package/src/commands/init.test.ts +4 -13
  36. package/src/commands/inspect.test.ts +2 -6
  37. package/src/commands/log.test.ts +2 -6
  38. package/src/commands/logs.test.ts +2 -9
  39. package/src/commands/mail.test.ts +76 -215
  40. package/src/commands/mail.ts +43 -187
  41. package/src/commands/metrics.test.ts +3 -10
  42. package/src/commands/nudge.ts +15 -0
  43. package/src/commands/prime.test.ts +4 -11
  44. package/src/commands/replay.test.ts +2 -6
  45. package/src/commands/server.test.ts +1 -5
  46. package/src/commands/server.ts +1 -1
  47. package/src/commands/sling.ts +40 -16
  48. package/src/commands/spec.test.ts +2 -5
  49. package/src/commands/status.test.ts +2 -4
  50. package/src/commands/stop.test.ts +2 -5
  51. package/src/commands/supervisor.ts +6 -6
  52. package/src/commands/trace.test.ts +2 -6
  53. package/src/commands/up.test.ts +43 -9
  54. package/src/commands/up.ts +15 -11
  55. package/src/commands/watchman.ts +327 -0
  56. package/src/commands/worktree.test.ts +2 -6
  57. package/src/config.test.ts +34 -104
  58. package/src/config.ts +120 -32
  59. package/src/doctor/agents.test.ts +7 -2
  60. package/src/doctor/config-check.test.ts +7 -2
  61. package/src/doctor/consistency.test.ts +7 -2
  62. package/src/doctor/databases.test.ts +6 -2
  63. package/src/doctor/dependencies.test.ts +35 -10
  64. package/src/doctor/dependencies.ts +16 -92
  65. package/src/doctor/logs.test.ts +7 -2
  66. package/src/doctor/merge-queue.test.ts +6 -2
  67. package/src/doctor/structure.test.ts +7 -2
  68. package/src/doctor/version.test.ts +7 -2
  69. package/src/e2e/init-sling-lifecycle.test.ts +2 -5
  70. package/src/index.ts +7 -7
  71. package/src/mail/pending.ts +120 -0
  72. package/src/mail/store.test.ts +89 -0
  73. package/src/mail/store.ts +11 -0
  74. package/src/merge/resolver.test.ts +518 -489
  75. package/src/server/index.ts +33 -2
  76. package/src/server/public/app.js +3 -3
  77. package/src/server/public/components/message-bubble.js +11 -1
  78. package/src/server/public/components/terminal-panel.js +66 -74
  79. package/src/server/public/views/chat.js +18 -2
  80. package/src/server/public/views/costs.js +5 -5
  81. package/src/server/public/views/dashboard.js +80 -51
  82. package/src/server/public/views/gateway-chat.js +37 -131
  83. package/src/server/public/views/inspect.js +16 -4
  84. package/src/server/public/views/issues.js +16 -12
  85. package/src/server/routes.test.ts +55 -39
  86. package/src/server/routes.ts +38 -26
  87. package/src/test-helpers.ts +6 -3
  88. package/src/tracker/beads.ts +159 -0
  89. package/src/tracker/exec.ts +44 -0
  90. package/src/tracker/factory.test.ts +283 -0
  91. package/src/tracker/factory.ts +59 -0
  92. package/src/tracker/seeds.ts +156 -0
  93. package/src/tracker/types.ts +46 -0
  94. package/src/types.ts +11 -2
  95. package/src/{watchdog → watchman}/daemon.test.ts +421 -515
  96. package/src/watchman/daemon.ts +940 -0
  97. package/src/worktree/tmux.test.ts +2 -1
  98. package/src/worktree/tmux.ts +4 -4
  99. package/templates/hooks.json.tmpl +17 -17
  100. package/src/beads/client.test.ts +0 -210
  101. package/src/commands/merge.test.ts +0 -676
  102. package/src/commands/watch.test.ts +0 -152
  103. package/src/commands/watch.ts +0 -238
  104. package/src/test-helpers.test.ts +0 -97
  105. package/src/watchdog/daemon.ts +0 -533
  106. package/src/watchdog/health.test.ts +0 -371
  107. package/src/watchdog/triage.test.ts +0 -162
  108. package/src/worktree/manager.test.ts +0 -444
  109. /package/src/{watchdog → watchman}/health.ts +0 -0
  110. /package/src/{watchdog → watchman}/triage.ts +0 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tracker factory with auto-detection.
3
+ *
4
+ * Detects which tracker backend is present in the project root and
5
+ * returns the appropriate TrackerClient.
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { createBeadsTrackerClient } from "./beads.ts";
11
+ import { createSeedsTrackerClient } from "./seeds.ts";
12
+ import type { TrackerBackend, TrackerClient } from "./types.ts";
13
+
14
+ // Re-export types for convenience
15
+ export type { TrackerBackend, TrackerClient, TrackerIssue } from "./types.ts";
16
+
17
+ /**
18
+ * Auto-detect which tracker backend to use.
19
+ * Checks .seeds/ first, then .beads/. Defaults to seeds.
20
+ *
21
+ * @param cwd - Directory to search for tracker markers
22
+ */
23
+ export function resolveBackend(cwd: string): "seeds" | "beads" {
24
+ if (existsSync(join(cwd, ".seeds"))) return "seeds";
25
+ if (existsSync(join(cwd, ".beads"))) return "beads";
26
+ return "seeds"; // default
27
+ }
28
+
29
+ /**
30
+ * Create a TrackerClient for the given backend.
31
+ *
32
+ * @param backend - The backend to use ("auto" detects from filesystem)
33
+ * @param cwd - Working directory for CLI commands and backend detection
34
+ */
35
+ export function createTrackerClient(backend: TrackerBackend, cwd: string): TrackerClient {
36
+ const resolved = backend === "auto" ? resolveBackend(cwd) : backend;
37
+ switch (resolved) {
38
+ case "seeds":
39
+ return createSeedsTrackerClient(cwd);
40
+ case "beads":
41
+ return createBeadsTrackerClient(cwd);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Return the CLI tool name for a resolved backend.
47
+ * Used by the overlay generator to inject {{TRACKER_CLI}} into agent definitions.
48
+ */
49
+ export function trackerCliName(backend: "seeds" | "beads"): string {
50
+ return backend === "seeds" ? "sd" : "bd";
51
+ }
52
+
53
+ /**
54
+ * Return the human-readable tracker name for a resolved backend.
55
+ * Used by the overlay generator to inject {{TRACKER_NAME}} into agent definitions.
56
+ */
57
+ export function trackerDisplayName(backend: "seeds" | "beads"): string {
58
+ return backend === "seeds" ? "seeds" : "beads";
59
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Seeds adapter for the tracker abstraction layer.
3
+ *
4
+ * Wraps the `sd` CLI to implement TrackerClient using the seeds backend.
5
+ * Follows the same patterns as the beads adapter.
6
+ */
7
+
8
+ import { AgentError } from "../errors.ts";
9
+ import { runTrackerCommand } from "./exec.ts";
10
+ import type { TrackerClient, TrackerIssue } from "./types.ts";
11
+
12
+ /**
13
+ * Parse JSON output from an sd command.
14
+ */
15
+ function parseJsonOutput<T>(stdout: string, context: string): T {
16
+ const trimmed = stdout.trim();
17
+ if (trimmed === "") {
18
+ throw new AgentError(`Empty output from sd ${context}`);
19
+ }
20
+ try {
21
+ return JSON.parse(trimmed) as T;
22
+ } catch {
23
+ throw new AgentError(
24
+ `Failed to parse JSON output from sd ${context}: ${trimmed.slice(0, 200)}`,
25
+ );
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Raw issue shape from the sd CLI (snake_case fields).
31
+ */
32
+ interface RawSeedIssue {
33
+ id: string;
34
+ title: string;
35
+ status: string;
36
+ priority: number;
37
+ issue_type?: string;
38
+ type?: string;
39
+ owner?: string;
40
+ assignee?: string;
41
+ description?: string;
42
+ blocks?: string[];
43
+ blocked_by?: string[];
44
+ blockedBy?: string[];
45
+ closed_at?: string;
46
+ close_reason?: string;
47
+ created_at?: string;
48
+ }
49
+
50
+ /**
51
+ * Normalize a raw sd issue into a TrackerIssue (camelCase).
52
+ */
53
+ export function normalizeIssue(raw: RawSeedIssue): TrackerIssue {
54
+ return {
55
+ id: raw.id,
56
+ title: raw.title,
57
+ status: raw.status,
58
+ priority: raw.priority,
59
+ type: raw.issue_type ?? raw.type ?? "unknown",
60
+ assignee: raw.owner ?? raw.assignee,
61
+ description: raw.description,
62
+ blocks: raw.blocks,
63
+ blockedBy: raw.blocked_by ?? raw.blockedBy,
64
+ closedAt: raw.closed_at,
65
+ closeReason: raw.close_reason,
66
+ createdAt: raw.created_at,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Create a TrackerClient backed by the seeds (sd) CLI.
72
+ *
73
+ * @param cwd - Working directory where sd commands should run
74
+ */
75
+ export function createSeedsTrackerClient(cwd: string): TrackerClient {
76
+ async function runSd(
77
+ args: string[],
78
+ context: string,
79
+ ): Promise<{ stdout: string; stderr: string }> {
80
+ const { stdout, stderr, exitCode } = await runTrackerCommand(["sd", ...args], cwd);
81
+ if (exitCode !== 0) {
82
+ throw new AgentError(`sd ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
83
+ }
84
+ return { stdout, stderr };
85
+ }
86
+
87
+ return {
88
+ async ready(options): Promise<TrackerIssue[]> {
89
+ const args = ["ready", "--json"];
90
+ if (options?.mol) {
91
+ args.push("--mol", options.mol);
92
+ }
93
+ const { stdout } = await runSd(args, "ready");
94
+ const raw = parseJsonOutput<RawSeedIssue[]>(stdout, "ready");
95
+ return raw.map(normalizeIssue);
96
+ },
97
+
98
+ async show(id): Promise<TrackerIssue> {
99
+ const { stdout } = await runSd(["show", id, "--json"], `show ${id}`);
100
+ const raw = parseJsonOutput<RawSeedIssue[]>(stdout, `show ${id}`);
101
+ const first = raw[0];
102
+ if (!first) {
103
+ throw new AgentError(`sd show ${id} returned empty array`);
104
+ }
105
+ return normalizeIssue(first);
106
+ },
107
+
108
+ async create(title, options): Promise<string> {
109
+ const args = ["create", title, "--json"];
110
+ if (options?.type) {
111
+ args.push("--type", options.type);
112
+ }
113
+ if (options?.priority !== undefined) {
114
+ args.push("--priority", String(options.priority));
115
+ }
116
+ if (options?.description) {
117
+ args.push("--description", options.description);
118
+ }
119
+ const { stdout } = await runSd(args, "create");
120
+ const result = parseJsonOutput<{ id: string }>(stdout, "create");
121
+ return result.id;
122
+ },
123
+
124
+ async claim(id): Promise<void> {
125
+ await runSd(["update", id, "--status", "in_progress"], `claim ${id}`);
126
+ },
127
+
128
+ async close(id, reason): Promise<void> {
129
+ const args = ["close", id];
130
+ if (reason) {
131
+ args.push("--reason", reason);
132
+ }
133
+ await runSd(args, `close ${id}`);
134
+ },
135
+
136
+ async list(options): Promise<TrackerIssue[]> {
137
+ const args = ["list", "--json"];
138
+ if (options?.status) {
139
+ args.push("--status", options.status);
140
+ }
141
+ if (options?.limit !== undefined) {
142
+ args.push("--limit", String(options.limit));
143
+ }
144
+ if (options?.all) {
145
+ args.push("--all");
146
+ }
147
+ const { stdout } = await runSd(args, "list");
148
+ const raw = parseJsonOutput<RawSeedIssue[]>(stdout, "list");
149
+ return raw.map(normalizeIssue);
150
+ },
151
+
152
+ async sync(): Promise<void> {
153
+ await runSd(["sync"], "sync");
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tracker abstraction layer types.
3
+ *
4
+ * Defines a pluggable TrackerClient interface that works with any backend
5
+ * (beads via bd CLI, or seeds via sd CLI).
6
+ */
7
+
8
+ /** A tracker issue as returned by any backend. */
9
+ export interface TrackerIssue {
10
+ id: string;
11
+ title: string;
12
+ status: string;
13
+ priority: number;
14
+ type: string;
15
+ assignee?: string;
16
+ description?: string;
17
+ blocks?: string[];
18
+ blockedBy?: string[];
19
+ closedAt?: string;
20
+ closeReason?: string;
21
+ createdAt?: string;
22
+ }
23
+
24
+ /** Supported tracker backends. */
25
+ export type TrackerBackend = "auto" | "seeds" | "beads";
26
+
27
+ /** Pluggable tracker client interface. */
28
+ export interface TrackerClient {
29
+ /** List issues that are ready for work (open, unblocked). */
30
+ ready(options?: { mol?: string }): Promise<TrackerIssue[]>;
31
+ /** Show details for a specific issue. */
32
+ show(id: string): Promise<TrackerIssue>;
33
+ /** Create a new issue. Returns the new issue ID. */
34
+ create(
35
+ title: string,
36
+ options?: { type?: string; priority?: number; description?: string },
37
+ ): Promise<string>;
38
+ /** Claim an issue (mark as in_progress). */
39
+ claim(id: string): Promise<void>;
40
+ /** Close an issue with an optional reason. */
41
+ close(id: string, reason?: string): Promise<void>;
42
+ /** List issues with optional filters. */
43
+ list(options?: { status?: string; limit?: number; all?: boolean }): Promise<TrackerIssue[]>;
44
+ /** Sync tracker state (e.g., bd sync or sd sync). */
45
+ sync(): Promise<void>;
46
+ }
package/src/types.ts CHANGED
@@ -33,7 +33,12 @@ export interface LegioConfig {
33
33
  worktrees: {
34
34
  baseDir: string; // Where worktrees live
35
35
  };
36
- beads: {
36
+ /** @deprecated Use taskTracker instead. Kept for backward compatibility with inline fixtures. */
37
+ beads?: {
38
+ enabled: boolean;
39
+ };
40
+ taskTracker: {
41
+ backend: "auto" | "seeds" | "beads";
37
42
  enabled: boolean;
38
43
  };
39
44
  mulch: {
@@ -46,13 +51,17 @@ export interface LegioConfig {
46
51
  aiResolveEnabled: boolean;
47
52
  reimagineEnabled: boolean;
48
53
  };
49
- watchdog: {
54
+ watchman: {
50
55
  tier0Enabled: boolean; // Tier 0: Mechanical daemon (heartbeat, tmux/pid liveness)
51
56
  tier0IntervalMs: number; // Default 30_000
52
57
  tier1Enabled: boolean; // Tier 1: Triage agent (ephemeral AI analysis)
53
58
  tier2Enabled: boolean; // Tier 2: Monitor agent (continuous patrol — not yet implemented)
54
59
  zombieThresholdMs: number; // When to kill
55
60
  nudgeIntervalMs: number; // Time between progressive nudge stages (default 60_000)
61
+ mailIntervalMs: number; // Mail polling interval (default 5_000)
62
+ reNudgeIntervalMs: number; // Time between re-nudges for the same agent (default 10_000)
63
+ warnAfterMs: number; // Warn after unread mail sits this long (default 60_000)
64
+ beaconNudgeMs: number; // Time before sending follow-up Enter to stuck beacon (default 20_000)
56
65
  };
57
66
  models: Partial<Record<string, ModelAlias>>;
58
67
  logging: {