@katyella/legio 0.1.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.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: legio sling <task-id>
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL PATH. Orchestrates a full agent spawn:
|
|
5
|
+
* 1. Load config + manifest
|
|
6
|
+
* 2. Validate (depth limit, hierarchy)
|
|
7
|
+
* 3. Load manifest + validate capability
|
|
8
|
+
* 4. Resolve or create run_id (current-run.txt)
|
|
9
|
+
* 5. Check name uniqueness + concurrency limit
|
|
10
|
+
* 6. Validate bead exists
|
|
11
|
+
* 7. Create worktree
|
|
12
|
+
* 8. Generate + write overlay CLAUDE.md
|
|
13
|
+
* 9. Deploy hooks config
|
|
14
|
+
* 10. Claim beads issue
|
|
15
|
+
* 11. Create agent identity
|
|
16
|
+
* 12. Create tmux session running claude
|
|
17
|
+
* 13. Record session in SessionStore + increment run agent count
|
|
18
|
+
* 14. Return AgentSession
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
23
|
+
import { join, resolve } from "node:path";
|
|
24
|
+
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
25
|
+
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
26
|
+
import { createManifestLoader } from "../agents/manifest.ts";
|
|
27
|
+
import { writeOverlay } from "../agents/overlay.ts";
|
|
28
|
+
import type { BeadIssue } from "../beads/client.ts";
|
|
29
|
+
import { createBeadsClient } from "../beads/client.ts";
|
|
30
|
+
import { collectProviderEnv, loadConfig } from "../config.ts";
|
|
31
|
+
import { AgentError, HierarchyError, isRunningAsRoot, ValidationError } from "../errors.ts";
|
|
32
|
+
import { createMailStore } from "../mail/store.ts";
|
|
33
|
+
import { createMulchClient, inferDomainsFromFiles } from "../mulch/client.ts";
|
|
34
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
35
|
+
import { createRunStore } from "../sessions/store.ts";
|
|
36
|
+
import type { AgentSession, OverlayConfig } from "../types.ts";
|
|
37
|
+
import { createWorktree } from "../worktree/manager.ts";
|
|
38
|
+
import { createSession, sendKeys, startPipePane, waitForTuiReady } from "../worktree/tmux.ts";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculate how many milliseconds to sleep before spawning a new agent,
|
|
42
|
+
* based on the configured stagger delay and when the most recent active
|
|
43
|
+
* session was started.
|
|
44
|
+
*
|
|
45
|
+
* Returns 0 if no sleep is needed (no active sessions, delay is 0, or
|
|
46
|
+
* enough time has already elapsed).
|
|
47
|
+
*
|
|
48
|
+
* @param staggerDelayMs - The configured minimum delay between spawns
|
|
49
|
+
* @param activeSessions - Currently active (non-zombie) sessions
|
|
50
|
+
* @param now - Current timestamp in ms (defaults to Date.now(), injectable for testing)
|
|
51
|
+
*/
|
|
52
|
+
export function calculateStaggerDelay(
|
|
53
|
+
staggerDelayMs: number,
|
|
54
|
+
activeSessions: ReadonlyArray<{ startedAt: string }>,
|
|
55
|
+
now: number = Date.now(),
|
|
56
|
+
): number {
|
|
57
|
+
if (staggerDelayMs <= 0 || activeSessions.length === 0) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const mostRecent = activeSessions.reduce((latest, s) => {
|
|
62
|
+
return new Date(s.startedAt).getTime() > new Date(latest.startedAt).getTime() ? s : latest;
|
|
63
|
+
});
|
|
64
|
+
const elapsed = now - new Date(mostRecent.startedAt).getTime();
|
|
65
|
+
const remaining = staggerDelayMs - elapsed;
|
|
66
|
+
return remaining > 0 ? remaining : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse a named flag value from an args array.
|
|
71
|
+
*/
|
|
72
|
+
function getFlag(args: string[], flag: string): string | undefined {
|
|
73
|
+
const idx = args.indexOf(flag);
|
|
74
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return args[idx + 1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Options for building the structured startup beacon.
|
|
82
|
+
*/
|
|
83
|
+
export interface BeaconOptions {
|
|
84
|
+
agentName: string;
|
|
85
|
+
capability: string;
|
|
86
|
+
taskId: string;
|
|
87
|
+
parentAgent: string | null;
|
|
88
|
+
depth: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a structured startup beacon for an agent.
|
|
93
|
+
*
|
|
94
|
+
* The beacon is the first user message sent to a Claude Code agent via
|
|
95
|
+
* tmux send-keys. It provides identity context and a numbered startup
|
|
96
|
+
* protocol so the agent knows exactly what to do on boot.
|
|
97
|
+
*
|
|
98
|
+
* Format:
|
|
99
|
+
* [LEGIO] <agent-name> (<capability>) <ISO timestamp> task:<bead-id>
|
|
100
|
+
* Depth: <n> | Parent: <parent-name|none>
|
|
101
|
+
* Startup protocol:
|
|
102
|
+
* 1. Read your assignment in .claude/CLAUDE.md
|
|
103
|
+
* 2. Load expertise: mulch prime
|
|
104
|
+
* 3. Check mail: legio mail check --agent <name>
|
|
105
|
+
* 4. Begin working on task <bead-id>
|
|
106
|
+
*/
|
|
107
|
+
export function buildBeacon(opts: BeaconOptions): string {
|
|
108
|
+
const timestamp = new Date().toISOString();
|
|
109
|
+
const parent = opts.parentAgent ?? "none";
|
|
110
|
+
const parts = [
|
|
111
|
+
`[LEGIO] ${opts.agentName} (${opts.capability}) ${timestamp} task:${opts.taskId}`,
|
|
112
|
+
`Depth: ${opts.depth} | Parent: ${parent}`,
|
|
113
|
+
`Startup: read .claude/CLAUDE.md, run mulch prime, check mail (legio mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
|
|
114
|
+
];
|
|
115
|
+
return parts.join(" — ");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the auto-dispatch mail message object that sling writes to mail.db
|
|
120
|
+
* before creating the tmux session. This guarantees the dispatch mail exists
|
|
121
|
+
* when the agent's SessionStart hook fires `legio mail check`.
|
|
122
|
+
*
|
|
123
|
+
* Pure function — no side effects, easily testable.
|
|
124
|
+
*/
|
|
125
|
+
export function buildAutoDispatch(opts: {
|
|
126
|
+
parentAgent: string | null;
|
|
127
|
+
agentName: string;
|
|
128
|
+
taskId: string;
|
|
129
|
+
specPath: string | null;
|
|
130
|
+
branchName: string;
|
|
131
|
+
}): { from: string; to: string; subject: string; body: string; type: string; priority: string } {
|
|
132
|
+
const from = opts.parentAgent ?? "orchestrator";
|
|
133
|
+
const specDisplay = opts.specPath ?? "none";
|
|
134
|
+
return {
|
|
135
|
+
from,
|
|
136
|
+
to: opts.agentName,
|
|
137
|
+
subject: `dispatch: ${opts.taskId}`,
|
|
138
|
+
body: `Assigned task ${opts.taskId}. Spec: ${specDisplay}. Branch: ${opts.branchName}. Begin immediately.`,
|
|
139
|
+
type: "dispatch",
|
|
140
|
+
priority: "normal",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a parent agent has spawned any scouts.
|
|
146
|
+
* Returns true if the parent has at least one scout child in the session history.
|
|
147
|
+
*/
|
|
148
|
+
export function parentHasScouts(
|
|
149
|
+
sessions: ReadonlyArray<{ parentAgent: string | null; capability: string }>,
|
|
150
|
+
parentAgent: string,
|
|
151
|
+
): boolean {
|
|
152
|
+
return sessions.some((s) => s.parentAgent === parentAgent && s.capability === "scout");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate hierarchy constraints: the coordinator (no parent) may only spawn leads and scouts.
|
|
157
|
+
*
|
|
158
|
+
* When parentAgent is null, the caller is the coordinator or a human.
|
|
159
|
+
* Only "lead" and "scout" capabilities are allowed in that case. All other capabilities
|
|
160
|
+
* (builder, scout, reviewer, merger) must be spawned by a lead or supervisor
|
|
161
|
+
* that passes --parent.
|
|
162
|
+
*
|
|
163
|
+
* @param parentAgent - The --parent flag value (null = coordinator/human)
|
|
164
|
+
* @param capability - The requested agent capability
|
|
165
|
+
* @param name - The agent name (for error context)
|
|
166
|
+
* @param depth - The requested hierarchy depth
|
|
167
|
+
* @param forceHierarchy - If true, bypass the check (for debugging)
|
|
168
|
+
* @throws HierarchyError if the constraint is violated
|
|
169
|
+
*/
|
|
170
|
+
export function validateHierarchy(
|
|
171
|
+
parentAgent: string | null,
|
|
172
|
+
capability: string,
|
|
173
|
+
name: string,
|
|
174
|
+
_depth: number,
|
|
175
|
+
forceHierarchy: boolean,
|
|
176
|
+
): void {
|
|
177
|
+
if (forceHierarchy) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (parentAgent === null && capability !== "lead" && capability !== "scout") {
|
|
182
|
+
throw new HierarchyError(
|
|
183
|
+
`Coordinator cannot spawn "${capability}" directly. Only "lead" and "scout" are allowed without --parent. Use a lead as intermediary, or pass --force-hierarchy to bypass.`,
|
|
184
|
+
{ agentName: name, requestedCapability: capability },
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if a parent agent has reached its child agent budget ceiling.
|
|
191
|
+
* @throws AgentError if the parent already has maxAgentsPerLead active children.
|
|
192
|
+
*/
|
|
193
|
+
export function checkParentAgentLimit(
|
|
194
|
+
sessions: ReadonlyArray<{ parentAgent: string | null; state: string }>,
|
|
195
|
+
parentAgent: string,
|
|
196
|
+
maxAgentsPerLead: number,
|
|
197
|
+
agentName: string,
|
|
198
|
+
): void {
|
|
199
|
+
const activeChildren = sessions.filter(
|
|
200
|
+
(s) => s.parentAgent === parentAgent && s.state !== "zombie" && s.state !== "completed",
|
|
201
|
+
);
|
|
202
|
+
if (activeChildren.length >= maxAgentsPerLead) {
|
|
203
|
+
throw new AgentError(
|
|
204
|
+
`Parent "${parentAgent}" has reached its child agent limit: ${activeChildren.length}/${maxAgentsPerLead} active children`,
|
|
205
|
+
{ agentName },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if a lead agent is already active for the given task ID.
|
|
212
|
+
* Prevents two leads from concurrently working the same issue.
|
|
213
|
+
* @throws AgentError if a lead is already active for this task.
|
|
214
|
+
*/
|
|
215
|
+
export function checkDuplicateLead(
|
|
216
|
+
sessions: ReadonlyArray<{ beadId: string; capability: string; state: string; agentName: string }>,
|
|
217
|
+
taskId: string,
|
|
218
|
+
capability: string,
|
|
219
|
+
agentName: string,
|
|
220
|
+
): void {
|
|
221
|
+
if (capability !== "lead") return; // Only applies to leads
|
|
222
|
+
|
|
223
|
+
const existingLead = sessions.find(
|
|
224
|
+
(s) =>
|
|
225
|
+
s.beadId === taskId &&
|
|
226
|
+
s.capability === "lead" &&
|
|
227
|
+
s.state !== "zombie" &&
|
|
228
|
+
s.state !== "completed",
|
|
229
|
+
);
|
|
230
|
+
if (existingLead) {
|
|
231
|
+
throw new AgentError(
|
|
232
|
+
`Lead already active for task "${taskId}": ${existingLead.agentName}. Cannot spawn duplicate lead "${agentName}".`,
|
|
233
|
+
{ agentName },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Entry point for `legio sling <task-id> [flags]`.
|
|
240
|
+
*
|
|
241
|
+
* Flags:
|
|
242
|
+
* --capability <type> builder | scout | reviewer | lead | merger
|
|
243
|
+
* --name <name> Unique agent name
|
|
244
|
+
* --spec <path> Path to task spec file
|
|
245
|
+
* --files <f1,f2,...> Exclusive file scope
|
|
246
|
+
* --parent <agent-name> Parent agent (for hierarchy tracking)
|
|
247
|
+
* --depth <n> Current hierarchy depth (default 0)
|
|
248
|
+
* --force-hierarchy Bypass hierarchy validation (debugging only)
|
|
249
|
+
*/
|
|
250
|
+
const SLING_HELP = `legio sling — Spawn a worker agent
|
|
251
|
+
|
|
252
|
+
Usage: legio sling <task-id> [flags]
|
|
253
|
+
|
|
254
|
+
Arguments:
|
|
255
|
+
<task-id> Beads task ID to assign
|
|
256
|
+
|
|
257
|
+
Options:
|
|
258
|
+
--capability <type> Agent type: builder | scout | reviewer | lead | merger | cto (default: builder)
|
|
259
|
+
--name <name> Unique agent name (required)
|
|
260
|
+
--spec <path> Path to task spec file
|
|
261
|
+
--files <f1,f2,...> Exclusive file scope (comma-separated)
|
|
262
|
+
--parent <agent-name> Parent agent for hierarchy tracking
|
|
263
|
+
--depth <n> Current hierarchy depth (default: 0)
|
|
264
|
+
--force-hierarchy Bypass hierarchy validation (debugging only)
|
|
265
|
+
--skip-review Skip reviewer spawn (lead self-verifies)
|
|
266
|
+
--json Output result as JSON
|
|
267
|
+
--help, -h Show this help`;
|
|
268
|
+
|
|
269
|
+
export async function slingCommand(args: string[]): Promise<void> {
|
|
270
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
271
|
+
process.stdout.write(`${SLING_HELP}\n`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (isRunningAsRoot()) {
|
|
276
|
+
throw new ValidationError(
|
|
277
|
+
"legio must not run as root — agent processes execute arbitrary code",
|
|
278
|
+
{
|
|
279
|
+
field: "uid",
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const taskId = args.find((a) => !a.startsWith("--"));
|
|
285
|
+
if (!taskId) {
|
|
286
|
+
throw new ValidationError("Task ID is required: legio sling <task-id>", {
|
|
287
|
+
field: "taskId",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const capability = getFlag(args, "--capability") ?? "builder";
|
|
292
|
+
const name = getFlag(args, "--name");
|
|
293
|
+
const specPath = getFlag(args, "--spec") ?? null;
|
|
294
|
+
const filesRaw = getFlag(args, "--files");
|
|
295
|
+
const parentAgent = getFlag(args, "--parent") ?? null;
|
|
296
|
+
const depthStr = getFlag(args, "--depth");
|
|
297
|
+
const depth = depthStr !== undefined ? Number.parseInt(depthStr, 10) : 0;
|
|
298
|
+
const forceHierarchy = args.includes("--force-hierarchy");
|
|
299
|
+
const skipReview = args.includes("--skip-review");
|
|
300
|
+
|
|
301
|
+
if (!name || name.trim().length === 0) {
|
|
302
|
+
throw new ValidationError("--name is required for sling", { field: "name" });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (Number.isNaN(depth) || depth < 0) {
|
|
306
|
+
throw new ValidationError("--depth must be a non-negative integer", {
|
|
307
|
+
field: "depth",
|
|
308
|
+
value: depthStr,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate that spec file exists if provided, and resolve to absolute path
|
|
313
|
+
// so agents in worktrees can access it (worktrees don't have .legio/)
|
|
314
|
+
let absoluteSpecPath: string | null = null;
|
|
315
|
+
if (specPath !== null) {
|
|
316
|
+
absoluteSpecPath = resolve(specPath);
|
|
317
|
+
let specExists = false;
|
|
318
|
+
try {
|
|
319
|
+
await access(absoluteSpecPath);
|
|
320
|
+
specExists = true;
|
|
321
|
+
} catch {
|
|
322
|
+
specExists = false;
|
|
323
|
+
}
|
|
324
|
+
if (!specExists) {
|
|
325
|
+
throw new ValidationError(`Spec file not found: ${specPath}`, {
|
|
326
|
+
field: "spec",
|
|
327
|
+
value: specPath,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const fileScope = filesRaw
|
|
333
|
+
? filesRaw
|
|
334
|
+
.split(",")
|
|
335
|
+
.map((f) => f.trim())
|
|
336
|
+
.filter((f) => f.length > 0)
|
|
337
|
+
: [];
|
|
338
|
+
|
|
339
|
+
// 1. Load config
|
|
340
|
+
const cwd = process.cwd();
|
|
341
|
+
const config = await loadConfig(cwd);
|
|
342
|
+
|
|
343
|
+
// 2. Validate depth limit
|
|
344
|
+
// Hierarchy: orchestrator(0) -> lead(1) -> specialist(2)
|
|
345
|
+
// With maxDepth=2, depth=2 is the deepest allowed leaf, so reject only depth > maxDepth
|
|
346
|
+
if (depth > config.agents.maxDepth) {
|
|
347
|
+
throw new AgentError(
|
|
348
|
+
`Depth limit exceeded: depth ${depth} > maxDepth ${config.agents.maxDepth}`,
|
|
349
|
+
{ agentName: name },
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2b. Validate hierarchy: coordinator (no --parent) can only spawn leads and scouts
|
|
354
|
+
validateHierarchy(parentAgent, capability, name, depth, forceHierarchy);
|
|
355
|
+
|
|
356
|
+
// 3. Load manifest and validate capability
|
|
357
|
+
const manifestLoader = createManifestLoader(
|
|
358
|
+
join(config.project.root, config.agents.manifestPath),
|
|
359
|
+
join(config.project.root, config.agents.baseDir),
|
|
360
|
+
);
|
|
361
|
+
const manifest = await manifestLoader.load();
|
|
362
|
+
|
|
363
|
+
const agentDef = manifest.agents[capability];
|
|
364
|
+
if (!agentDef) {
|
|
365
|
+
throw new AgentError(
|
|
366
|
+
`Unknown capability "${capability}". Available: ${Object.keys(manifest.agents).join(", ")}`,
|
|
367
|
+
{ agentName: name, capability },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 4. Resolve or create run_id for this spawn
|
|
372
|
+
const legioDir = join(config.project.root, ".legio");
|
|
373
|
+
const currentRunPath = join(legioDir, "current-run.txt");
|
|
374
|
+
let runId: string;
|
|
375
|
+
|
|
376
|
+
let currentRunExists = false;
|
|
377
|
+
try {
|
|
378
|
+
await access(currentRunPath);
|
|
379
|
+
currentRunExists = true;
|
|
380
|
+
} catch {
|
|
381
|
+
currentRunExists = false;
|
|
382
|
+
}
|
|
383
|
+
if (currentRunExists) {
|
|
384
|
+
runId = (await readFile(currentRunPath, "utf-8")).trim();
|
|
385
|
+
} else {
|
|
386
|
+
runId = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
387
|
+
const runStore = createRunStore(join(legioDir, "sessions.db"));
|
|
388
|
+
try {
|
|
389
|
+
runStore.createRun({
|
|
390
|
+
id: runId,
|
|
391
|
+
startedAt: new Date().toISOString(),
|
|
392
|
+
coordinatorSessionId: null,
|
|
393
|
+
status: "active",
|
|
394
|
+
});
|
|
395
|
+
} finally {
|
|
396
|
+
runStore.close();
|
|
397
|
+
}
|
|
398
|
+
await writeFile(currentRunPath, runId);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 5. Check name uniqueness and concurrency limit against active sessions
|
|
402
|
+
const { store } = openSessionStore(legioDir);
|
|
403
|
+
try {
|
|
404
|
+
const activeSessions = store.getActive();
|
|
405
|
+
if (activeSessions.length >= config.agents.maxConcurrent) {
|
|
406
|
+
throw new AgentError(
|
|
407
|
+
`Max concurrent agent limit reached: ${activeSessions.length}/${config.agents.maxConcurrent} active agents`,
|
|
408
|
+
{ agentName: name },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const existing = store.getByName(name);
|
|
413
|
+
if (existing && existing.state !== "zombie" && existing.state !== "completed") {
|
|
414
|
+
throw new AgentError(`Agent name "${name}" is already in use (state: ${existing.state})`, {
|
|
415
|
+
agentName: name,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 5b. Enforce stagger delay between agent spawns
|
|
420
|
+
const staggerMs = calculateStaggerDelay(config.agents.staggerDelayMs, activeSessions);
|
|
421
|
+
if (staggerMs > 0) {
|
|
422
|
+
await new Promise<void>((resolve) => setTimeout(resolve, staggerMs));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 5c. Structural enforcement: warn when a lead spawns a builder without prior scouts.
|
|
426
|
+
// This is a non-blocking warning — it does not prevent the spawn, but surfaces
|
|
427
|
+
// the scout-skip pattern so agents and operators can see it happening.
|
|
428
|
+
if (capability === "builder" && parentAgent && !parentHasScouts(store.getAll(), parentAgent)) {
|
|
429
|
+
process.stderr.write(
|
|
430
|
+
`⚠️ Warning: "${parentAgent}" is spawning builder "${name}" without having spawned any scouts.\n`,
|
|
431
|
+
);
|
|
432
|
+
process.stderr.write(
|
|
433
|
+
" Leads should spawn scouts in Phase 1 before building. See agents/lead.md.\n",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 5d. Enforce per-lead agent budget ceiling
|
|
438
|
+
if (parentAgent !== null) {
|
|
439
|
+
checkParentAgentLimit(activeSessions, parentAgent, config.agents.maxAgentsPerLead ?? 5, name);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 5e. Prevent duplicate leads on the same task
|
|
443
|
+
checkDuplicateLead(activeSessions, taskId, capability, name);
|
|
444
|
+
|
|
445
|
+
// 6. Validate bead exists and is in a workable state (if beads enabled)
|
|
446
|
+
const beads = createBeadsClient(config.project.root);
|
|
447
|
+
if (config.beads.enabled) {
|
|
448
|
+
let issue: BeadIssue;
|
|
449
|
+
try {
|
|
450
|
+
issue = await beads.show(taskId);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
throw new AgentError(`Bead task "${taskId}" not found or inaccessible`, {
|
|
453
|
+
agentName: name,
|
|
454
|
+
cause: err instanceof Error ? err : undefined,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const workableStatuses = ["open", "in_progress"];
|
|
459
|
+
if (!workableStatuses.includes(issue.status)) {
|
|
460
|
+
throw new ValidationError(
|
|
461
|
+
`Bead task "${taskId}" is not workable (status: ${issue.status}). Only open or in_progress issues can be assigned.`,
|
|
462
|
+
{ field: "taskId", value: taskId },
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 7. Create worktree
|
|
468
|
+
const worktreeBaseDir = join(config.project.root, config.worktrees.baseDir);
|
|
469
|
+
await mkdir(worktreeBaseDir, { recursive: true });
|
|
470
|
+
|
|
471
|
+
const { path: worktreePath, branch: branchName } = await createWorktree({
|
|
472
|
+
repoRoot: config.project.root,
|
|
473
|
+
baseDir: worktreeBaseDir,
|
|
474
|
+
agentName: name,
|
|
475
|
+
baseBranch: config.project.canonicalBranch,
|
|
476
|
+
beadId: taskId,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// 8. Generate + write overlay CLAUDE.md
|
|
480
|
+
const agentDefPath = join(config.project.root, config.agents.baseDir, agentDef.file);
|
|
481
|
+
const baseDefinition = await readFile(agentDefPath, "utf-8");
|
|
482
|
+
|
|
483
|
+
// 8a. Infer mulch domains from file scope and fetch expertise
|
|
484
|
+
const customDomainMap = config.mulch.domainMap;
|
|
485
|
+
const inferredDomains = inferDomainsFromFiles(
|
|
486
|
+
fileScope,
|
|
487
|
+
customDomainMap && Object.keys(customDomainMap).length > 0 ? customDomainMap : undefined,
|
|
488
|
+
);
|
|
489
|
+
let mulchExpertise: string | undefined;
|
|
490
|
+
if (config.mulch.enabled && fileScope.length > 0) {
|
|
491
|
+
try {
|
|
492
|
+
const mulch = createMulchClient(config.project.root);
|
|
493
|
+
if (inferredDomains.length > 0) {
|
|
494
|
+
mulchExpertise = await mulch.prime(inferredDomains);
|
|
495
|
+
} else {
|
|
496
|
+
mulchExpertise = await mulch.prime(undefined, undefined, { files: fileScope });
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
// Non-fatal: mulch expertise is supplementary context
|
|
500
|
+
mulchExpertise = undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const overlayConfig: OverlayConfig = {
|
|
505
|
+
agentName: name,
|
|
506
|
+
beadId: taskId,
|
|
507
|
+
specPath: absoluteSpecPath,
|
|
508
|
+
branchName,
|
|
509
|
+
worktreePath,
|
|
510
|
+
fileScope,
|
|
511
|
+
mulchDomains: config.mulch.enabled
|
|
512
|
+
? inferredDomains.length > 0
|
|
513
|
+
? inferredDomains
|
|
514
|
+
: config.mulch.domains
|
|
515
|
+
: [],
|
|
516
|
+
parentAgent: parentAgent,
|
|
517
|
+
depth,
|
|
518
|
+
canSpawn: agentDef.canSpawn,
|
|
519
|
+
capability,
|
|
520
|
+
baseDefinition,
|
|
521
|
+
mulchExpertise,
|
|
522
|
+
canonicalRoot: config.project.root,
|
|
523
|
+
skipReview,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
await writeOverlay(worktreePath, overlayConfig, config.project.root);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
// Clean up the orphaned worktree created in step 7 (legio-p4st)
|
|
530
|
+
try {
|
|
531
|
+
await new Promise<void>((res) => {
|
|
532
|
+
const p = spawn("git", ["worktree", "remove", "--force", worktreePath], {
|
|
533
|
+
cwd: config.project.root,
|
|
534
|
+
stdio: "ignore",
|
|
535
|
+
});
|
|
536
|
+
p.on("close", () => res());
|
|
537
|
+
p.on("error", () => res());
|
|
538
|
+
});
|
|
539
|
+
} catch {
|
|
540
|
+
// Best-effort cleanup; the original error is more important
|
|
541
|
+
}
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 9. Deploy hooks config (capability-specific guards)
|
|
546
|
+
await deployHooks(worktreePath, name, capability);
|
|
547
|
+
|
|
548
|
+
// 10. Claim beads issue
|
|
549
|
+
if (config.beads.enabled) {
|
|
550
|
+
try {
|
|
551
|
+
await beads.claim(taskId);
|
|
552
|
+
} catch {
|
|
553
|
+
// Non-fatal: issue may already be claimed
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 11. Create agent identity (if new)
|
|
558
|
+
const identityBaseDir = join(config.project.root, ".legio", "agents");
|
|
559
|
+
const existingIdentity = await loadIdentity(identityBaseDir, name);
|
|
560
|
+
if (!existingIdentity) {
|
|
561
|
+
await createIdentity(identityBaseDir, {
|
|
562
|
+
name,
|
|
563
|
+
capability,
|
|
564
|
+
created: new Date().toISOString(),
|
|
565
|
+
sessionsCompleted: 0,
|
|
566
|
+
expertiseDomains: config.mulch.enabled ? config.mulch.domains : [],
|
|
567
|
+
recentTasks: [],
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 11b. Write dispatch mail BEFORE creating the tmux session so the mail
|
|
572
|
+
// exists when the agent's SessionStart hook fires `legio mail check`.
|
|
573
|
+
// Without this, there is a race: the agent boots, checks mail, finds nothing,
|
|
574
|
+
// and idles without an assignment.
|
|
575
|
+
const dispatchMsg = buildAutoDispatch({
|
|
576
|
+
parentAgent,
|
|
577
|
+
agentName: name,
|
|
578
|
+
taskId,
|
|
579
|
+
specPath: absoluteSpecPath,
|
|
580
|
+
branchName,
|
|
581
|
+
});
|
|
582
|
+
const mailStore = createMailStore(join(legioDir, "mail.db"));
|
|
583
|
+
try {
|
|
584
|
+
mailStore.insert({
|
|
585
|
+
id: `dispatch-${Date.now()}-${name}`,
|
|
586
|
+
from: dispatchMsg.from,
|
|
587
|
+
to: dispatchMsg.to,
|
|
588
|
+
subject: dispatchMsg.subject,
|
|
589
|
+
body: dispatchMsg.body,
|
|
590
|
+
type: dispatchMsg.type as "dispatch",
|
|
591
|
+
priority: dispatchMsg.priority as "normal",
|
|
592
|
+
threadId: null,
|
|
593
|
+
});
|
|
594
|
+
} finally {
|
|
595
|
+
mailStore.close();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 12. Create tmux session running claude in interactive mode
|
|
599
|
+
// Write a settings JSON file to skip the bypass-permissions dialog.
|
|
600
|
+
const settingsPath = join(legioDir, `settings-${name}.json`);
|
|
601
|
+
await writeFile(
|
|
602
|
+
settingsPath,
|
|
603
|
+
JSON.stringify({ skipDangerousModePermissionPrompt: true }),
|
|
604
|
+
"utf-8",
|
|
605
|
+
);
|
|
606
|
+
const tmuxSessionName = `legio-${config.project.name}-${name}`;
|
|
607
|
+
const claudeCmd = `claude --model ${agentDef.model} --dangerously-skip-permissions --settings ${settingsPath}`;
|
|
608
|
+
const pid = await createSession(tmuxSessionName, worktreePath, claudeCmd, {
|
|
609
|
+
...collectProviderEnv(),
|
|
610
|
+
LEGIO_AGENT_NAME: name,
|
|
611
|
+
LEGIO_WORKTREE_PATH: worktreePath,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// 13. Record session BEFORE sending the beacon so that hook-triggered
|
|
615
|
+
// updateLastActivity() can find the entry and transition booting->working.
|
|
616
|
+
// Without this, a race exists: hooks fire before the session is persisted,
|
|
617
|
+
// leaving the agent stuck in "booting" (legio-036f).
|
|
618
|
+
const terminalLogPath = join(legioDir, "logs", name, "terminal.log");
|
|
619
|
+
const session: AgentSession = {
|
|
620
|
+
id: `session-${Date.now()}-${name}`,
|
|
621
|
+
agentName: name,
|
|
622
|
+
capability,
|
|
623
|
+
worktreePath,
|
|
624
|
+
branchName,
|
|
625
|
+
beadId: taskId,
|
|
626
|
+
tmuxSession: tmuxSessionName,
|
|
627
|
+
state: "booting",
|
|
628
|
+
pid,
|
|
629
|
+
parentAgent: parentAgent,
|
|
630
|
+
depth,
|
|
631
|
+
runId,
|
|
632
|
+
startedAt: new Date().toISOString(),
|
|
633
|
+
lastActivity: new Date().toISOString(),
|
|
634
|
+
escalationLevel: 0,
|
|
635
|
+
stalledSince: null,
|
|
636
|
+
terminalLogPath,
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
store.upsert(session);
|
|
640
|
+
|
|
641
|
+
// Increment agent count for the run
|
|
642
|
+
const runStore = createRunStore(join(legioDir, "sessions.db"));
|
|
643
|
+
try {
|
|
644
|
+
runStore.incrementAgentCount(runId);
|
|
645
|
+
} finally {
|
|
646
|
+
runStore.close();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 13b. Send beacon prompt via tmux send-keys
|
|
650
|
+
// Wait for Claude Code's TUI to render before sending input.
|
|
651
|
+
// Polls pane content until non-empty (replaces hardcoded 3s sleep).
|
|
652
|
+
await waitForTuiReady(tmuxSessionName);
|
|
653
|
+
|
|
654
|
+
// 13c. Start pipe-pane streaming to terminal log (non-fatal)
|
|
655
|
+
try {
|
|
656
|
+
await startPipePane(tmuxSessionName, terminalLogPath);
|
|
657
|
+
} catch {
|
|
658
|
+
// Non-fatal: pipe-pane unavailable or session not yet ready
|
|
659
|
+
}
|
|
660
|
+
const beacon = buildBeacon({
|
|
661
|
+
agentName: name,
|
|
662
|
+
capability,
|
|
663
|
+
taskId,
|
|
664
|
+
parentAgent,
|
|
665
|
+
depth,
|
|
666
|
+
});
|
|
667
|
+
await sendKeys(tmuxSessionName, beacon);
|
|
668
|
+
|
|
669
|
+
// 13d. Send a follow-up Enter after a short delay to ensure submission.
|
|
670
|
+
// Claude Code's TUI may consume the first Enter during initialization,
|
|
671
|
+
// leaving the beacon text visible but unsubmitted (legio-yhv6).
|
|
672
|
+
// A redundant Enter on an empty input line is harmless.
|
|
673
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 500));
|
|
674
|
+
await sendKeys(tmuxSessionName, "");
|
|
675
|
+
|
|
676
|
+
// 14. Output result
|
|
677
|
+
const output = {
|
|
678
|
+
agentName: name,
|
|
679
|
+
capability,
|
|
680
|
+
taskId,
|
|
681
|
+
branch: branchName,
|
|
682
|
+
worktree: worktreePath,
|
|
683
|
+
tmuxSession: tmuxSessionName,
|
|
684
|
+
pid,
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
if (args.includes("--json")) {
|
|
688
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
689
|
+
} else {
|
|
690
|
+
process.stdout.write(`🚀 Agent "${name}" launched!\n`);
|
|
691
|
+
process.stdout.write(` Task: ${taskId}\n`);
|
|
692
|
+
process.stdout.write(` Branch: ${branchName}\n`);
|
|
693
|
+
process.stdout.write(` Worktree: ${worktreePath}\n`);
|
|
694
|
+
process.stdout.write(` Tmux: ${tmuxSessionName}\n`);
|
|
695
|
+
process.stdout.write(` PID: ${pid}\n`);
|
|
696
|
+
}
|
|
697
|
+
} finally {
|
|
698
|
+
store.close();
|
|
699
|
+
}
|
|
700
|
+
}
|