@katyella/legio 0.1.3 → 0.2.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 (112) hide show
  1. package/CHANGELOG.md +61 -3
  2. package/README.md +21 -10
  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 +87 -82
  25. package/src/commands/coordinator.ts +94 -48
  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 +43 -17
  33. package/src/commands/gateway.ts +101 -11
  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.test.ts +6 -1
  48. package/src/commands/sling.ts +42 -17
  49. package/src/commands/spec.test.ts +2 -5
  50. package/src/commands/status.test.ts +2 -4
  51. package/src/commands/stop.test.ts +2 -5
  52. package/src/commands/supervisor.ts +6 -6
  53. package/src/commands/trace.test.ts +2 -6
  54. package/src/commands/up.test.ts +43 -9
  55. package/src/commands/up.ts +15 -11
  56. package/src/commands/watchman.ts +327 -0
  57. package/src/commands/worktree.test.ts +2 -6
  58. package/src/config.test.ts +34 -104
  59. package/src/config.ts +120 -32
  60. package/src/doctor/agents.test.ts +52 -2
  61. package/src/doctor/agents.ts +4 -2
  62. package/src/doctor/config-check.test.ts +7 -2
  63. package/src/doctor/consistency.test.ts +7 -2
  64. package/src/doctor/databases.test.ts +6 -2
  65. package/src/doctor/dependencies.test.ts +18 -13
  66. package/src/doctor/dependencies.ts +23 -94
  67. package/src/doctor/logs.test.ts +7 -2
  68. package/src/doctor/merge-queue.test.ts +6 -2
  69. package/src/doctor/structure.test.ts +7 -2
  70. package/src/doctor/version.test.ts +7 -2
  71. package/src/e2e/init-sling-lifecycle.test.ts +2 -5
  72. package/src/index.ts +7 -7
  73. package/src/mail/pending.ts +120 -0
  74. package/src/mail/store.test.ts +89 -0
  75. package/src/mail/store.ts +11 -0
  76. package/src/merge/resolver.test.ts +518 -489
  77. package/src/server/index.ts +33 -2
  78. package/src/server/public/app.js +3 -3
  79. package/src/server/public/components/message-bubble.js +11 -1
  80. package/src/server/public/components/terminal-panel.js +66 -74
  81. package/src/server/public/views/chat.js +18 -2
  82. package/src/server/public/views/costs.js +5 -5
  83. package/src/server/public/views/dashboard.js +80 -51
  84. package/src/server/public/views/gateway-chat.js +37 -131
  85. package/src/server/public/views/inspect.js +16 -4
  86. package/src/server/public/views/issues.js +16 -12
  87. package/src/server/routes.test.ts +55 -39
  88. package/src/server/routes.ts +38 -26
  89. package/src/test-helpers.ts +6 -3
  90. package/src/tracker/beads.ts +159 -0
  91. package/src/tracker/exec.ts +44 -0
  92. package/src/tracker/factory.test.ts +283 -0
  93. package/src/tracker/factory.ts +59 -0
  94. package/src/tracker/seeds.ts +156 -0
  95. package/src/tracker/types.ts +46 -0
  96. package/src/types.ts +11 -2
  97. package/src/{watchdog → watchman}/daemon.test.ts +421 -515
  98. package/src/watchman/daemon.ts +940 -0
  99. package/src/worktree/tmux.test.ts +2 -1
  100. package/src/worktree/tmux.ts +4 -4
  101. package/templates/hooks.json.tmpl +17 -17
  102. package/src/beads/client.test.ts +0 -210
  103. package/src/commands/merge.test.ts +0 -676
  104. package/src/commands/watch.test.ts +0 -152
  105. package/src/commands/watch.ts +0 -238
  106. package/src/test-helpers.test.ts +0 -97
  107. package/src/watchdog/daemon.ts +0 -533
  108. package/src/watchdog/health.test.ts +0 -371
  109. package/src/watchdog/triage.test.ts +0 -162
  110. package/src/worktree/manager.test.ts +0 -444
  111. /package/src/{watchdog → watchman}/health.ts +0 -0
  112. /package/src/{watchdog → watchman}/triage.ts +0 -0
@@ -128,8 +128,9 @@ describe("createSession", () => {
128
128
  expect(args).toContain("my-session");
129
129
  expect(args).toContain("-c");
130
130
  expect(args).toContain("/work/dir");
131
- // The command is the last arg, wrapped with unset prefix
131
+ // The command is the last arg, wrapped with env -u prefix
132
132
  const wrappedCmd = args[args.length - 1] as string;
133
+ expect(wrappedCmd).toContain("env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT");
133
134
  expect(wrappedCmd).toContain("echo hello");
134
135
  // PATH is injected via -e flag, not shell export
135
136
  expect(args).toContain("-e");
@@ -99,10 +99,10 @@ export async function createSession(
99
99
  ): Promise<number> {
100
100
  // Clear Claude Code nesting detection so child Claude Code instances
101
101
  // don't refuse to start with "cannot be launched inside another session".
102
- // This MUST be part of the shell command (not tmux -e) because we need
103
- // to *unset* vars inherited from the parent process environment.
104
- const shellPrefix = "unset CLAUDECODE CLAUDE_CODE_ENTRYPOINT";
105
- const wrappedCommand = `${shellPrefix} && ${command}`;
102
+ // Use `env -u` instead of shell-builtin `unset` because tmux inherits the
103
+ // user's default shell which may be fish (where `unset` is invalid syntax).
104
+ // `env -u VAR cmd` is a standalone binary (/usr/bin/env) that works in any shell.
105
+ const wrappedCommand = `env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT ${command}`;
106
106
 
107
107
  // Build tmux args. Environment variables are passed via `-e KEY=VALUE`
108
108
  // flags (tmux 3.2+) instead of shell `export` commands to avoid
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio prime --agent {{AGENT_NAME}}"
9
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio prime --agent $LEGIO_AGENT_NAME"
10
10
  }
11
11
  ]
12
12
  }
@@ -17,7 +17,7 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio mail check --inject --agent {{AGENT_NAME}}"
20
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio mail check --inject --agent $LEGIO_AGENT_NAME"
21
21
  }
22
22
  ]
23
23
  }
@@ -60,35 +60,35 @@
60
60
  ]
61
61
  },
62
62
  {
63
- "matcher": "",
63
+ "matcher": "Bash",
64
64
  "hooks": [
65
65
  {
66
66
  "type": "command",
67
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log tool-start --agent {{AGENT_NAME}} --stdin"
67
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; read -r INPUT; CMD=$(echo \"$INPUT\" | sed 's/.*\"command\": *\"\\([^\"]*\\)\".*/\\1/'); if echo \"$CMD\" | grep -qE '\\bsleep\\b'; then echo '{\"decision\":\"block\",\"reason\":\"sleep is blocked in legio agents - mail delivery is automatic via hooks, no polling needed\"}'; exit 0; fi;"
68
68
  }
69
69
  ]
70
- }
71
- ],
72
- "PostToolUse": [
70
+ },
73
71
  {
74
72
  "matcher": "",
75
73
  "hooks": [
76
74
  {
77
75
  "type": "command",
78
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log tool-end --agent {{AGENT_NAME}} --stdin"
79
- },
80
- {
81
- "type": "command",
82
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio mail check --inject --agent {{AGENT_NAME}} --debounce 500"
76
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log tool-start --agent $LEGIO_AGENT_NAME --stdin"
83
77
  }
84
78
  ]
85
- },
79
+ }
80
+ ],
81
+ "PostToolUse": [
86
82
  {
87
83
  "matcher": "",
88
84
  "hooks": [
89
85
  {
90
86
  "type": "command",
91
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio mail check --inject --agent {{AGENT_NAME}} --debounce 30000"
87
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log tool-end --agent $LEGIO_AGENT_NAME --stdin"
88
+ },
89
+ {
90
+ "type": "command",
91
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio mail check --inject --agent $LEGIO_AGENT_NAME --signal"
92
92
  }
93
93
  ]
94
94
  },
@@ -97,7 +97,7 @@
97
97
  "hooks": [
98
98
  {
99
99
  "type": "command",
100
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; read -r INPUT; CMD=$(echo \"$INPUT\" | sed 's/.*\"command\": *\"\\([^\"]*\\)\".*/\\1/'); if echo \"$CMD\" | grep -qE '\\bgit\\s+commit\\b'; then mulch diff HEAD~1; fi;"
100
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; read -r INPUT; CMD=$(echo \"$INPUT\" | sed 's/.*\"command\": *\"\\([^\"]*\\)\".*/\\1/'); if echo \"$CMD\" | grep -qE '\\bgit\\s+commit\\b'; then git rev-parse HEAD~1 >/dev/null 2>&1 && mulch diff HEAD~1; fi;"
101
101
  }
102
102
  ]
103
103
  }
@@ -108,7 +108,7 @@
108
108
  "hooks": [
109
109
  {
110
110
  "type": "command",
111
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log session-end --agent {{AGENT_NAME}} --stdin"
111
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio log session-end --agent $LEGIO_AGENT_NAME --stdin"
112
112
  },
113
113
  {
114
114
  "type": "command",
@@ -123,7 +123,7 @@
123
123
  "hooks": [
124
124
  {
125
125
  "type": "command",
126
- "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio prime --agent {{AGENT_NAME}} --compact"
126
+ "command": "[ -z \"$LEGIO_AGENT_NAME\" ] && exit 0; legio prime --agent $LEGIO_AGENT_NAME --compact"
127
127
  }
128
128
  ]
129
129
  }
@@ -1,210 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { realpathSync } from "node:fs";
3
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
4
- import { AgentError } from "../errors.ts";
5
- import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
6
- import { type BeadsClient, createBeadsClient } from "./client.ts";
7
-
8
- /**
9
- * Check if the bd CLI is available on this machine (synchronous).
10
- * Uses spawnSync so the result is available at test registration time
11
- * for use with test.skipIf().
12
- */
13
- function isBdAvailable(): boolean {
14
- try {
15
- const result = spawnSync("bd", ["--version"], { stdio: "pipe" });
16
- return result.status === 0;
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- /**
23
- * Initialize beads in a git repo directory.
24
- */
25
- function initBeads(cwd: string): void {
26
- const result = spawnSync("bd", ["init"], { cwd, stdio: "pipe" });
27
- if (result.status !== 0) {
28
- const stderr = (result.stderr ?? Buffer.alloc(0)).toString();
29
- throw new Error(`bd init failed: ${stderr}`);
30
- }
31
- }
32
-
33
- const bdAvailable = isBdAvailable();
34
-
35
- /**
36
- * Optimized test suite: uses a single shared repo (beforeAll) instead of
37
- * creating a fresh repo per test. All 16 original tests share one repo
38
- * since they create issues with unique IDs and use toContain/not.toContain
39
- * assertions. This reduces setup from ~96 subprocess spawns to ~6.
40
- */
41
- describe("createBeadsClient (integration)", { timeout: 30_000 }, () => {
42
- let tempDir: string;
43
- let client: BeadsClient;
44
-
45
- // Pre-created issue IDs for tests that need existing issues
46
- let openIssueId: string;
47
- let claimedIssueId: string;
48
- let closedIssueId: string;
49
-
50
- beforeAll(async () => {
51
- if (!bdAvailable) return;
52
- // realpathSync resolves macOS /var -> /private/var symlink so paths match
53
- tempDir = realpathSync(await createTempGitRepo());
54
- initBeads(tempDir);
55
- client = createBeadsClient(tempDir);
56
-
57
- // Pre-create issues used by read-only tests (list, ready, show)
58
- openIssueId = await client.create("Pre-created open issue");
59
- claimedIssueId = await client.create("Pre-created claimed issue");
60
- await client.claim(claimedIssueId);
61
- closedIssueId = await client.create("Pre-created closed issue");
62
- await client.close(closedIssueId);
63
- });
64
-
65
- afterAll(async () => {
66
- if (!bdAvailable) return;
67
- await cleanupTempDir(tempDir);
68
- });
69
-
70
- describe("create", () => {
71
- test.skipIf(!bdAvailable)("returns an issue ID", async () => {
72
- const id = await client.create("Integration test issue");
73
-
74
- expect(typeof id).toBe("string");
75
- expect(id.length).toBeGreaterThan(0);
76
- });
77
-
78
- test.skipIf(!bdAvailable)("returns ID with type and priority options", async () => {
79
- const id = await client.create("Typed issue", {
80
- type: "bug",
81
- priority: 1,
82
- });
83
-
84
- expect(typeof id).toBe("string");
85
- expect(id.length).toBeGreaterThan(0);
86
- });
87
-
88
- test.skipIf(!bdAvailable)("returns ID with description option", async () => {
89
- const id = await client.create("Described issue", {
90
- description: "A detailed description",
91
- });
92
-
93
- expect(typeof id).toBe("string");
94
- expect(id.length).toBeGreaterThan(0);
95
- });
96
- });
97
-
98
- describe("show", () => {
99
- test.skipIf(!bdAvailable)("returns issue details for a valid ID", async () => {
100
- const id = await client.create("Show test issue", {
101
- type: "task",
102
- priority: 2,
103
- });
104
-
105
- const issue = await client.show(id);
106
-
107
- expect(issue.id).toBe(id);
108
- expect(issue.title).toBe("Show test issue");
109
- expect(issue.status).toBe("open");
110
- expect(issue.priority).toBe(2);
111
- expect(issue.type).toBe("task");
112
- });
113
- });
114
-
115
- describe("claim", () => {
116
- test.skipIf(!bdAvailable)("changes issue status to in_progress and returns void", async () => {
117
- const id = await client.create("Claim test issue");
118
-
119
- const result = await client.claim(id);
120
- expect(result).toBeUndefined();
121
-
122
- const issue = await client.show(id);
123
- expect(issue.status).toBe("in_progress");
124
- });
125
- });
126
-
127
- describe("close", () => {
128
- test.skipIf(!bdAvailable)("closes issues with and without reason", async () => {
129
- const id1 = await client.create("Close test issue");
130
- const id2 = await client.create("Close reason test");
131
-
132
- await client.close(id1);
133
- await client.close(id2, "Completed all acceptance criteria");
134
-
135
- const issue1 = await client.show(id1);
136
- expect(issue1.status).toBe("closed");
137
-
138
- const issue2 = await client.show(id2);
139
- expect(issue2.status).toBe("closed");
140
- });
141
- });
142
-
143
- describe("list", () => {
144
- test.skipIf(!bdAvailable)("returns all issues", async () => {
145
- const issues = await client.list();
146
-
147
- // Pre-created issues should be present (plus any from other tests)
148
- expect(issues.length).toBeGreaterThanOrEqual(3);
149
- const titles = issues.map((i) => i.title);
150
- expect(titles).toContain("Pre-created open issue");
151
- expect(titles).toContain("Pre-created claimed issue");
152
- });
153
-
154
- test.skipIf(!bdAvailable)("filters by status", async () => {
155
- const openIssues = await client.list({ status: "open" });
156
- const openIds = openIssues.map((i) => i.id);
157
- expect(openIds).toContain(openIssueId);
158
- expect(openIds).not.toContain(claimedIssueId);
159
- expect(openIds).not.toContain(closedIssueId);
160
-
161
- const inProgressIssues = await client.list({ status: "in_progress" });
162
- const inProgressIds = inProgressIssues.map((i) => i.id);
163
- expect(inProgressIds).toContain(claimedIssueId);
164
- expect(inProgressIds).not.toContain(openIssueId);
165
- });
166
-
167
- test.skipIf(!bdAvailable)("respects limit option", async () => {
168
- const limited = await client.list({ limit: 1 });
169
- expect(limited).toHaveLength(1);
170
- });
171
- });
172
-
173
- describe("ready", () => {
174
- test.skipIf(!bdAvailable)(
175
- "returns open unblocked issues but not claimed or closed",
176
- async () => {
177
- const readyIssues = await client.ready();
178
- const readyIds = readyIssues.map((i) => i.id);
179
-
180
- // Open issue should appear in ready
181
- expect(readyIds).toContain(openIssueId);
182
- // Claimed issue should not appear in ready
183
- expect(readyIds).not.toContain(claimedIssueId);
184
- // Closed issue should not appear in ready
185
- expect(readyIds).not.toContain(closedIssueId);
186
- },
187
- );
188
- });
189
-
190
- describe("error handling", () => {
191
- test.skipIf(!bdAvailable)("show throws AgentError for nonexistent ID", async () => {
192
- await expect(client.show("nonexistent-id")).rejects.toThrow(AgentError);
193
- });
194
-
195
- test.skipIf(!bdAvailable)(
196
- "throws AgentError when bd is run without beads initialized",
197
- async () => {
198
- // Create a git repo without bd init — independent from shared repo
199
- const bareDir = realpathSync(await createTempGitRepo());
200
- const bareClient = createBeadsClient(bareDir);
201
-
202
- try {
203
- await expect(bareClient.list()).rejects.toThrow(AgentError);
204
- } finally {
205
- await cleanupTempDir(bareDir);
206
- }
207
- },
208
- );
209
- });
210
- });