@katyella/legio 0.2.0 → 0.2.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.
- package/CHANGELOG.md +32 -1
- package/README.md +6 -2
- package/package.json +1 -1
- package/src/commands/coordinator.test.ts +9 -4
- package/src/commands/coordinator.ts +2 -1
- package/src/commands/gateway.test.ts +38 -6
- package/src/commands/gateway.ts +32 -26
- package/src/commands/sling.test.ts +6 -1
- package/src/commands/sling.ts +2 -1
- package/src/doctor/agents.test.ts +45 -0
- package/src/doctor/agents.ts +4 -2
- package/src/doctor/dependencies.test.ts +8 -28
- package/src/doctor/dependencies.ts +15 -10
- package/src/index.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.3] - 2026-03-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Coordinator and sling beacons now explain what legio is — previously Claude Code on fresh machines rejected thin beacons as "unrecognized" foreign content (same fix as gateway in v0.2.1)
|
|
14
|
+
- Coordinator beacon startup instructions use `legio status` instead of `bd ready` / `legio group status` (doesn't assume beads is installed)
|
|
15
|
+
- `legio doctor` no longer flags persistent agent identity files (coordinator, gateway, monitor) as stale — they legitimately exist outside the agent manifest
|
|
16
|
+
- Gateway greeting mail ("Gateway is online and ready") is only sent after confirmed beacon delivery — previously it was sent even when the beacon was rejected
|
|
17
|
+
- 2399 tests across 79 test files (up from 2397 across 79)
|
|
18
|
+
|
|
19
|
+
## [0.2.2] - 2026-03-02
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Coordinator and sling beacons now explain what legio is — previously Claude Code on fresh machines rejected thin beacons as "unrecognized" foreign content (same fix as gateway in v0.2.1)
|
|
23
|
+
- Coordinator beacon startup instructions use `legio status` instead of `bd ready` / `legio group status` (doesn't assume beads is installed)
|
|
24
|
+
- `legio doctor` no longer flags persistent agent identity files (coordinator, gateway, monitor) as stale — they legitimately exist outside the agent manifest
|
|
25
|
+
- 2398 tests across 79 test files (up from 2397 across 79)
|
|
26
|
+
|
|
27
|
+
## [0.2.1] - 2026-03-02
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Gateway beacon now explains what legio is — previously Claude Code on fresh machines rejected the thin beacon as "unrecognized" foreign content
|
|
31
|
+
- Gateway tmux session now receives provider env vars (`collectProviderEnv()`) — was the only agent type missing them
|
|
32
|
+
- Removed hardcoded `bd create` from gateway beacon (not all users have beads)
|
|
33
|
+
- `legio doctor` checks for bun availability with install instructions (required runtime for seeds and mulch)
|
|
34
|
+
- `legio doctor` marks sd, mulch, and bd as optional dependencies (warn instead of fail)
|
|
35
|
+
- Doctor install hints explain what each tool does and note bun requirement
|
|
36
|
+
- README requirements section separates required (Node, Claude Code, git, tmux) from optional (sd, mulch, bd) dependencies
|
|
37
|
+
|
|
10
38
|
## [0.2.0] - 2026-03-02
|
|
11
39
|
|
|
12
40
|
### Added
|
|
@@ -182,7 +210,10 @@ Initial public release on npm as [`@katyella/legio`](https://www.npmjs.com/packa
|
|
|
182
210
|
- E2E lifecycle tests via Playwright
|
|
183
211
|
- Vitest test runner with forks pool for CI compatibility
|
|
184
212
|
|
|
185
|
-
[Unreleased]: https://github.com/katyella/legio/compare/v0.2.
|
|
213
|
+
[Unreleased]: https://github.com/katyella/legio/compare/v0.2.3...HEAD
|
|
214
|
+
[0.2.3]: https://github.com/katyella/legio/compare/v0.2.2...v0.2.3
|
|
215
|
+
[0.2.2]: https://github.com/katyella/legio/compare/v0.2.1...v0.2.2
|
|
216
|
+
[0.2.1]: https://github.com/katyella/legio/compare/v0.2.0...v0.2.1
|
|
186
217
|
[0.2.0]: https://github.com/katyella/legio/compare/v0.1.3...v0.2.0
|
|
187
218
|
[0.1.3]: https://github.com/katyella/legio/compare/v0.1.2...v0.1.3
|
|
188
219
|
[0.1.2]: https://github.com/katyella/legio/compare/v0.1.1...v0.1.2
|
package/README.md
CHANGED
|
@@ -116,8 +116,12 @@ npm link
|
|
|
116
116
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
|
117
117
|
- git
|
|
118
118
|
- tmux
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
|
|
120
|
+
### Optional
|
|
121
|
+
|
|
122
|
+
- [sd (seeds)](https://github.com/jayminwest/seeds) — issue tracking CLI (`bun install -g @os-eco/seeds-cli`) — requires [Bun](https://bun.sh)
|
|
123
|
+
- [mulch](https://github.com/jayminwest/mulch) — structured expertise management CLI (`bun install -g @os-eco/mulch-cli`) — requires [Bun](https://bun.sh)
|
|
124
|
+
- [bd (beads)](https://github.com/steveyegge/beads) — legacy issue tracker (alternative to seeds)
|
|
121
125
|
|
|
122
126
|
## Documentation
|
|
123
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@katyella/legio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
|
|
5
5
|
"author": "Matthew Wojtowicz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -875,8 +875,7 @@ describe("buildCoordinatorBeacon", () => {
|
|
|
875
875
|
const beacon = buildCoordinatorBeacon();
|
|
876
876
|
expect(beacon).toContain("mulch prime");
|
|
877
877
|
expect(beacon).toContain("legio mail check --agent coordinator");
|
|
878
|
-
expect(beacon).toContain("
|
|
879
|
-
expect(beacon).toContain("legio group status");
|
|
878
|
+
expect(beacon).toContain("legio status");
|
|
880
879
|
});
|
|
881
880
|
|
|
882
881
|
test("includes hierarchy enforcement instruction", () => {
|
|
@@ -891,11 +890,17 @@ describe("buildCoordinatorBeacon", () => {
|
|
|
891
890
|
expect(beacon).toContain("spawn a lead who will spawn scouts");
|
|
892
891
|
});
|
|
893
892
|
|
|
893
|
+
test("explains what legio is", () => {
|
|
894
|
+
const beacon = buildCoordinatorBeacon();
|
|
895
|
+
expect(beacon).toContain("legio multi-agent orchestration system");
|
|
896
|
+
expect(beacon).toContain("CLI tool installed on this machine");
|
|
897
|
+
});
|
|
898
|
+
|
|
894
899
|
test("parts are joined with em-dash separator", () => {
|
|
895
900
|
const beacon = buildCoordinatorBeacon();
|
|
896
|
-
// Should have exactly
|
|
901
|
+
// Should have exactly 5 " — " separators (6 parts)
|
|
897
902
|
const dashes = beacon.split(" — ");
|
|
898
|
-
expect(dashes).toHaveLength(
|
|
903
|
+
expect(dashes).toHaveLength(6);
|
|
899
904
|
});
|
|
900
905
|
});
|
|
901
906
|
|
|
@@ -281,10 +281,11 @@ export function buildCoordinatorBeacon(): string {
|
|
|
281
281
|
const timestamp = new Date().toISOString();
|
|
282
282
|
const parts = [
|
|
283
283
|
`[LEGIO] ${COORDINATOR_NAME} (coordinator) ${timestamp}`,
|
|
284
|
+
"You are a coordinator agent in the legio multi-agent orchestration system. legio is a CLI tool installed on this machine that coordinates multiple Claude Code agents via tmux, SQLite mail, and git worktrees.",
|
|
284
285
|
"Depth: 0 | Parent: none | Role: persistent orchestrator",
|
|
285
286
|
"HIERARCHY: You ONLY spawn leads (legio sling --capability lead). Leads spawn scouts, builders, reviewers. NEVER spawn non-lead agents directly.",
|
|
286
287
|
"DELEGATION: For any exploration/scouting, spawn a lead who will spawn scouts. Do NOT explore the codebase yourself beyond initial planning.",
|
|
287
|
-
`Startup: run mulch prime, check mail (legio mail check --agent ${COORDINATOR_NAME}), check
|
|
288
|
+
`Startup: run mulch prime, check mail (legio mail check --agent ${COORDINATOR_NAME}), check legio status, then begin work`,
|
|
288
289
|
];
|
|
289
290
|
return parts.join(" — ");
|
|
290
291
|
}
|
|
@@ -518,8 +518,26 @@ describe("startGateway", () => {
|
|
|
518
518
|
expect(enterCalls).toHaveLength(2); // follow-up Enter + retry Enter
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
-
test("sends greeting mail to human after beacon delivery", async () => {
|
|
522
|
-
const {
|
|
521
|
+
test("sends greeting mail to human after confirmed beacon delivery", async () => {
|
|
522
|
+
const { tmux } = makeFakeTmux();
|
|
523
|
+
// Simulate hooks transitioning session out of "booting" after a few sleep calls
|
|
524
|
+
let sleepCount = 0;
|
|
525
|
+
const deps: GatewayDeps = {
|
|
526
|
+
_tmux: tmux,
|
|
527
|
+
_sleep: async () => {
|
|
528
|
+
sleepCount++;
|
|
529
|
+
// After 3 sleeps (waitForTuiReady + follow-up Enter delay + first poll),
|
|
530
|
+
// transition session to "working" like hooks would
|
|
531
|
+
if (sleepCount === 3) {
|
|
532
|
+
const { store } = openSessionStore(legioDir);
|
|
533
|
+
try {
|
|
534
|
+
store.updateState("gateway", "working");
|
|
535
|
+
} finally {
|
|
536
|
+
store.close();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
};
|
|
523
541
|
await captureStdout(() => gatewayCommand(["start", "--no-attach"], deps));
|
|
524
542
|
// Verify mail.db has the greeting
|
|
525
543
|
const { createMailStore } = await import("../mail/store.ts");
|
|
@@ -535,6 +553,20 @@ describe("startGateway", () => {
|
|
|
535
553
|
mailDb.close();
|
|
536
554
|
}
|
|
537
555
|
});
|
|
556
|
+
|
|
557
|
+
test("does not send greeting mail when beacon delivery fails", async () => {
|
|
558
|
+
const { deps } = makeDeps();
|
|
559
|
+
await captureStdout(() => gatewayCommand(["start", "--no-attach"], deps));
|
|
560
|
+
// verifyBeaconDelivery returns false (session stays in "booting"), so no greeting mail
|
|
561
|
+
const { createMailStore } = await import("../mail/store.ts");
|
|
562
|
+
const mailDb = createMailStore(join(legioDir, "mail.db"));
|
|
563
|
+
try {
|
|
564
|
+
const msgs = mailDb.getAll({ from: "gateway", to: "human" });
|
|
565
|
+
expect(msgs).toHaveLength(0);
|
|
566
|
+
} finally {
|
|
567
|
+
mailDb.close();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
538
570
|
});
|
|
539
571
|
|
|
540
572
|
describe("stopGateway", () => {
|
|
@@ -713,21 +745,21 @@ describe("buildGatewayBeacon", () => {
|
|
|
713
745
|
|
|
714
746
|
test("includes ISSUES notice", () => {
|
|
715
747
|
const beacon = buildGatewayBeacon();
|
|
716
|
-
expect(beacon).toContain("ISSUES:
|
|
748
|
+
expect(beacon).toContain("ISSUES:");
|
|
749
|
+
expect(beacon).toContain("legio status");
|
|
717
750
|
});
|
|
718
751
|
|
|
719
752
|
test("includes startup instructions", () => {
|
|
720
753
|
const beacon = buildGatewayBeacon();
|
|
721
|
-
expect(beacon).toContain("mulch prime");
|
|
722
754
|
expect(beacon).toContain("legio mail check --agent gateway");
|
|
723
755
|
expect(beacon).toContain("respond to user via BOTH terminal AND mail");
|
|
724
756
|
});
|
|
725
757
|
|
|
726
758
|
test("parts are joined with em-dash separator", () => {
|
|
727
759
|
const beacon = buildGatewayBeacon();
|
|
728
|
-
// Should have exactly
|
|
760
|
+
// Should have exactly 5 " — " separators (6 parts)
|
|
729
761
|
const dashes = beacon.split(" — ");
|
|
730
|
-
expect(dashes).toHaveLength(
|
|
762
|
+
expect(dashes).toHaveLength(6);
|
|
731
763
|
});
|
|
732
764
|
|
|
733
765
|
test("default (no args) does not include FIRST_RUN", () => {
|
package/src/commands/gateway.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
19
19
|
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
20
20
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
21
|
-
import { loadConfig } from "../config.ts";
|
|
21
|
+
import { collectProviderEnv, loadConfig } from "../config.ts";
|
|
22
22
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
23
23
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
24
24
|
import type { SessionStore } from "../sessions/store.ts";
|
|
@@ -82,10 +82,11 @@ export function buildGatewayBeacon(isFirstRun = false): string {
|
|
|
82
82
|
const timestamp = new Date().toISOString();
|
|
83
83
|
const parts = [
|
|
84
84
|
`[LEGIO] ${GATEWAY_NAME} (gateway) ${timestamp}`,
|
|
85
|
-
"
|
|
86
|
-
"READONLY: No Write/Edit",
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
"You are a gateway agent in the legio multi-agent orchestration system. legio is a CLI tool installed on this machine that coordinates multiple Claude Code agents via tmux, SQLite mail, and git worktrees.",
|
|
86
|
+
"Depth: 0 | Role: planning companion | READONLY: No Write/Edit tool access",
|
|
87
|
+
'COMMUNICATION: Use legio mail for all inter-agent messaging. Check your inbox with: legio mail check --agent gateway. Send replies via: legio mail send --to human --subject "chat" --body "..." --type status --agent gateway',
|
|
88
|
+
"ISSUES: If a task tracker is configured, use it to create issues for work decomposition. Check legio status for current agent fleet.",
|
|
89
|
+
`Startup: check mail (legio mail check --agent ${GATEWAY_NAME}), respond to user via BOTH terminal AND mail so replies appear in dashboard chat`,
|
|
89
90
|
];
|
|
90
91
|
if (isFirstRun) {
|
|
91
92
|
parts.push("FIRST_RUN: true — Follow the First Run workflow in your agent definition");
|
|
@@ -117,7 +118,7 @@ async function verifyBeaconDelivery(
|
|
|
117
118
|
tmuxSession: string,
|
|
118
119
|
beacon: string,
|
|
119
120
|
sleep: (ms: number) => Promise<void>,
|
|
120
|
-
): Promise<
|
|
121
|
+
): Promise<boolean> {
|
|
121
122
|
const MAX_CHECKS = 10;
|
|
122
123
|
const INTERVAL_MS = 2_000;
|
|
123
124
|
|
|
@@ -126,7 +127,7 @@ async function verifyBeaconDelivery(
|
|
|
126
127
|
const session = store.getByName(GATEWAY_NAME);
|
|
127
128
|
if (session && session.state !== "booting") {
|
|
128
129
|
// Beacon confirmed — gateway transitioned out of booting
|
|
129
|
-
return;
|
|
130
|
+
return true;
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -141,7 +142,9 @@ async function verifyBeaconDelivery(
|
|
|
141
142
|
const finalSession = store.getByName(GATEWAY_NAME);
|
|
142
143
|
if (!finalSession || finalSession.state === "booting") {
|
|
143
144
|
process.stderr.write("[legio] Warning: gateway beacon delivery could not be confirmed\n");
|
|
145
|
+
return false;
|
|
144
146
|
}
|
|
147
|
+
return true;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/**
|
|
@@ -238,6 +241,7 @@ async function startGateway(args: string[], deps: GatewayDeps = {}): Promise<voi
|
|
|
238
241
|
await writeFile(settingsPath, JSON.stringify(settings), "utf-8");
|
|
239
242
|
const claudeCmd = `claude --model ${model} --dangerously-skip-permissions --settings ${settingsPath}`;
|
|
240
243
|
const pid = await tmux.createSession(tmuxSession, projectRoot, claudeCmd, {
|
|
244
|
+
...collectProviderEnv(),
|
|
241
245
|
LEGIO_AGENT_NAME: GATEWAY_NAME,
|
|
242
246
|
});
|
|
243
247
|
|
|
@@ -303,25 +307,27 @@ async function startGateway(args: string[], deps: GatewayDeps = {}): Promise<voi
|
|
|
303
307
|
|
|
304
308
|
// Verify delivery: poll store until state transitions out of booting.
|
|
305
309
|
// Must complete before the finally block closes the store.
|
|
306
|
-
await verifyBeaconDelivery(store, tmux, tmuxSession, beacon, sleep);
|
|
307
|
-
|
|
308
|
-
// Send greeting mail to human
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
310
|
+
const confirmed = await verifyBeaconDelivery(store, tmux, tmuxSession, beacon, sleep);
|
|
311
|
+
|
|
312
|
+
// Send greeting mail to human only after confirmed beacon delivery
|
|
313
|
+
if (confirmed) {
|
|
314
|
+
const { createMailStore } = await import("../mail/store.ts");
|
|
315
|
+
const mailDb = createMailStore(join(legioDir, "mail.db"));
|
|
316
|
+
try {
|
|
317
|
+
mailDb.insert({
|
|
318
|
+
id: "",
|
|
319
|
+
from: GATEWAY_NAME,
|
|
320
|
+
to: "human",
|
|
321
|
+
subject: "Gateway online",
|
|
322
|
+
body: "Gateway is online and ready. Send a message to start chatting.",
|
|
323
|
+
type: "status",
|
|
324
|
+
priority: "normal",
|
|
325
|
+
threadId: null,
|
|
326
|
+
audience: "human",
|
|
327
|
+
});
|
|
328
|
+
} finally {
|
|
329
|
+
mailDb.close();
|
|
330
|
+
}
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
if (shouldAttach) {
|
|
@@ -399,12 +399,17 @@ describe("buildBeacon", () => {
|
|
|
399
399
|
expect(beacon).toContain("Depth: 0 | Parent: none");
|
|
400
400
|
});
|
|
401
401
|
|
|
402
|
+
test("explains what legio is", () => {
|
|
403
|
+
const beacon = buildBeacon(makeBeaconOpts());
|
|
404
|
+
expect(beacon).toContain("legio multi-agent orchestration system");
|
|
405
|
+
expect(beacon).toContain("CLI tool installed on this machine");
|
|
406
|
+
});
|
|
407
|
+
|
|
402
408
|
test("includes startup instructions with agent name and task ID", () => {
|
|
403
409
|
const opts = makeBeaconOpts({ agentName: "scout-1", taskId: "legio-xyz" });
|
|
404
410
|
const beacon = buildBeacon(opts);
|
|
405
411
|
|
|
406
412
|
expect(beacon).toContain("read .claude/CLAUDE.md");
|
|
407
|
-
expect(beacon).toContain("mulch prime");
|
|
408
413
|
expect(beacon).toContain("legio mail check --agent scout-1");
|
|
409
414
|
expect(beacon).toContain("begin task legio-xyz");
|
|
410
415
|
});
|
package/src/commands/sling.ts
CHANGED
|
@@ -115,8 +115,9 @@ export function buildBeacon(opts: BeaconOptions): string {
|
|
|
115
115
|
const parent = opts.parentAgent ?? "none";
|
|
116
116
|
const parts = [
|
|
117
117
|
`[LEGIO] ${opts.agentName} (${opts.capability}) ${timestamp} task:${opts.taskId}`,
|
|
118
|
+
"You are an agent in the legio multi-agent orchestration system. legio is a CLI tool installed on this machine that coordinates multiple Claude Code agents via tmux, SQLite mail, and git worktrees.",
|
|
118
119
|
`Depth: ${opts.depth} | Parent: ${parent}`,
|
|
119
|
-
`Startup: read .claude/CLAUDE.md,
|
|
120
|
+
`Startup: read .claude/CLAUDE.md, check mail (legio mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
|
|
120
121
|
];
|
|
121
122
|
return parts.join(" — ");
|
|
122
123
|
}
|
|
@@ -375,6 +375,51 @@ sessionsCompleted: -5
|
|
|
375
375
|
expect(identityCheck?.details?.some((d) => d.includes("sessionsCompleted"))).toBe(true);
|
|
376
376
|
});
|
|
377
377
|
|
|
378
|
+
test("does not flag persistent agent identities as stale", async () => {
|
|
379
|
+
const manifest = {
|
|
380
|
+
version: "1.0",
|
|
381
|
+
agents: {
|
|
382
|
+
scout: {
|
|
383
|
+
file: "scout.md",
|
|
384
|
+
model: "haiku",
|
|
385
|
+
tools: ["Read"],
|
|
386
|
+
capabilities: ["explore"],
|
|
387
|
+
canSpawn: false,
|
|
388
|
+
constraints: [],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
capabilityIndex: {
|
|
392
|
+
explore: ["scout"],
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
await mkdir(join(legioDir, "agent-defs"), { recursive: true });
|
|
397
|
+
await mkdir(join(legioDir, "agents", "gateway"), { recursive: true });
|
|
398
|
+
await mkdir(join(legioDir, "agents", "coordinator"), { recursive: true });
|
|
399
|
+
await writeFile(join(legioDir, "agent-manifest.json"), JSON.stringify(manifest, null, 2));
|
|
400
|
+
await writeFile(join(legioDir, "agent-defs", "scout.md"), "# Scout");
|
|
401
|
+
|
|
402
|
+
const identity = `name: gateway
|
|
403
|
+
capability: gateway
|
|
404
|
+
created: "2024-01-01T00:00:00Z"
|
|
405
|
+
sessionsCompleted: 3
|
|
406
|
+
`;
|
|
407
|
+
await writeFile(join(legioDir, "agents", "gateway", "identity.yaml"), identity);
|
|
408
|
+
|
|
409
|
+
const coordIdentity = `name: coordinator
|
|
410
|
+
capability: coordinator
|
|
411
|
+
created: "2024-01-01T00:00:00Z"
|
|
412
|
+
sessionsCompleted: 10
|
|
413
|
+
`;
|
|
414
|
+
await writeFile(join(legioDir, "agents", "coordinator", "identity.yaml"), coordIdentity);
|
|
415
|
+
|
|
416
|
+
const checks = await checkAgents(mockConfig, legioDir);
|
|
417
|
+
|
|
418
|
+
const staleCheck = checks.find((c) => c.name === "Stale identities");
|
|
419
|
+
// Should not warn — persistent agents are expected to have identity files
|
|
420
|
+
expect(staleCheck?.status).not.toBe("warn");
|
|
421
|
+
});
|
|
422
|
+
|
|
378
423
|
test("warns about stale identity files", async () => {
|
|
379
424
|
const manifest = {
|
|
380
425
|
version: "1.0",
|
package/src/doctor/agents.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
|
5
5
|
|
|
6
6
|
const VALID_MODELS = new Set(["sonnet", "opus", "haiku"]);
|
|
7
7
|
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
8
|
+
/** Persistent agents create identity files but are not registered in the agent manifest. */
|
|
9
|
+
const PERSISTENT_AGENTS = new Set(["coordinator", "gateway", "monitor"]);
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Check if a path exists.
|
|
@@ -313,8 +315,8 @@ export const checkAgents: DoctorCheckFn = async (_config, legioDir): Promise<Doc
|
|
|
313
315
|
|
|
314
316
|
identityFileCount++;
|
|
315
317
|
|
|
316
|
-
// Check if agent still exists in manifest
|
|
317
|
-
if (!manifest.agents[agentName]) {
|
|
318
|
+
// Check if agent still exists in manifest (persistent agents are not manifest-registered)
|
|
319
|
+
if (!manifest.agents[agentName] && !PERSISTENT_AGENTS.has(agentName)) {
|
|
318
320
|
staleIdentities.push(agentName);
|
|
319
321
|
continue;
|
|
320
322
|
}
|
|
@@ -56,46 +56,26 @@ describe("checkDependencies", () => {
|
|
|
56
56
|
const checks = await checkDependencies(mockConfig, "/tmp/.legio");
|
|
57
57
|
|
|
58
58
|
expect(Array.isArray(checks)).toBe(true);
|
|
59
|
-
expect(checks.length).toBeGreaterThanOrEqual(
|
|
59
|
+
expect(checks.length).toBeGreaterThanOrEqual(6);
|
|
60
60
|
|
|
61
61
|
// Verify we have checks for each tool
|
|
62
62
|
const toolNames = checks.map((c) => c.name);
|
|
63
63
|
expect(toolNames).toContain("git availability");
|
|
64
64
|
expect(toolNames).toContain("node availability");
|
|
65
65
|
expect(toolNames).toContain("tmux availability");
|
|
66
|
+
expect(toolNames).toContain("bun availability");
|
|
66
67
|
expect(toolNames).toContain("sd availability");
|
|
67
68
|
expect(toolNames).toContain("mulch availability");
|
|
68
69
|
expect(toolNames).toContain("bd availability");
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
test("sd
|
|
72
|
+
test("bun, sd, mulch, and bd are all optional (warn if missing)", async () => {
|
|
72
73
|
const checks = await checkDependencies(mockConfig, "/tmp/.legio");
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("bd is optional when backend is auto", async () => {
|
|
81
|
-
const checks = await checkDependencies(mockConfig, "/tmp/.legio");
|
|
82
|
-
const bdCheck = checks.find((c) => c.name === "bd availability");
|
|
83
|
-
// bd should be optional (warn if missing) when backend is "auto"
|
|
84
|
-
if (bdCheck?.status !== "pass") {
|
|
85
|
-
expect(bdCheck?.status).toBe("warn");
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("bd is required when backend is beads", async () => {
|
|
90
|
-
const beadsConfig = {
|
|
91
|
-
...mockConfig,
|
|
92
|
-
taskTracker: { backend: "beads" as const, enabled: true },
|
|
93
|
-
};
|
|
94
|
-
const checks = await checkDependencies(beadsConfig, "/tmp/.legio");
|
|
95
|
-
const bdCheck = checks.find((c) => c.name === "bd availability");
|
|
96
|
-
// bd should be required (fail if missing) when backend is "beads"
|
|
97
|
-
if (bdCheck?.status !== "pass") {
|
|
98
|
-
expect(bdCheck?.status).toBe("fail");
|
|
74
|
+
for (const name of ["bun", "sd", "mulch", "bd"]) {
|
|
75
|
+
const check = checks.find((c) => c.name === `${name} availability`);
|
|
76
|
+
if (check?.status !== "pass") {
|
|
77
|
+
expect(check?.status).toBe("warn");
|
|
78
|
+
}
|
|
99
79
|
}
|
|
100
80
|
});
|
|
101
81
|
|
|
@@ -8,33 +8,38 @@ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
|
8
8
|
* bd is checked as optional (legacy tracker backend).
|
|
9
9
|
*/
|
|
10
10
|
export const checkDependencies: DoctorCheckFn = async (
|
|
11
|
-
|
|
11
|
+
_config: LegioConfig,
|
|
12
12
|
_legioDir,
|
|
13
13
|
): Promise<DoctorCheck[]> => {
|
|
14
|
-
const backend = config.taskTracker.backend;
|
|
15
|
-
|
|
16
14
|
const requiredTools = [
|
|
17
15
|
{ name: "git", versionFlag: "--version", required: true },
|
|
18
16
|
{ name: "node", versionFlag: "--version", required: true },
|
|
19
17
|
{ name: "tmux", versionFlag: "-V", required: true },
|
|
18
|
+
{
|
|
19
|
+
name: "bun",
|
|
20
|
+
versionFlag: "--version",
|
|
21
|
+
required: false,
|
|
22
|
+
installHint: "curl -fsSL https://bun.sh/install | bash — required runtime for sd and mulch",
|
|
23
|
+
},
|
|
20
24
|
{
|
|
21
25
|
name: "sd",
|
|
22
26
|
versionFlag: "--version",
|
|
23
|
-
required:
|
|
24
|
-
installHint:
|
|
27
|
+
required: false,
|
|
28
|
+
installHint:
|
|
29
|
+
"bun install -g @os-eco/seeds-cli — issue tracker for agent task dispatch (requires Bun)",
|
|
25
30
|
},
|
|
26
31
|
{
|
|
27
32
|
name: "mulch",
|
|
28
33
|
versionFlag: "--version",
|
|
29
|
-
required:
|
|
30
|
-
installHint:
|
|
34
|
+
required: false,
|
|
35
|
+
installHint:
|
|
36
|
+
"bun install -g @os-eco/mulch-cli — structured expertise/memory across sessions (requires Bun)",
|
|
31
37
|
},
|
|
32
38
|
{
|
|
33
39
|
name: "bd",
|
|
34
40
|
versionFlag: "--version",
|
|
35
|
-
required:
|
|
36
|
-
installHint:
|
|
37
|
-
"https://github.com/steveyegge/beads (legacy — consider migrating to seeds: sd migrate-from-beads)",
|
|
41
|
+
required: false,
|
|
42
|
+
installHint: "https://github.com/steveyegge/beads — legacy issue tracker (alternative to sd)",
|
|
38
43
|
},
|
|
39
44
|
];
|
|
40
45
|
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { worktreeCommand } from "./commands/worktree.ts";
|
|
|
45
45
|
import { LegioError, WorktreeError } from "./errors.ts";
|
|
46
46
|
import { setQuiet } from "./logging/color.ts";
|
|
47
47
|
|
|
48
|
-
const VERSION = "0.2.
|
|
48
|
+
const VERSION = "0.2.3";
|
|
49
49
|
|
|
50
50
|
const HELP = `legio v${VERSION} — Multi-agent orchestration for Claude Code
|
|
51
51
|
|