@lawrence369/loop-cli 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/dist/agent/activity.d.ts +64 -0
  5. package/dist/agent/activity.js +265 -0
  6. package/dist/agent/launcher.d.ts +42 -0
  7. package/dist/agent/launcher.js +243 -0
  8. package/dist/agent/pty-session.d.ts +113 -0
  9. package/dist/agent/pty-session.js +490 -0
  10. package/dist/agent/ready-detector.d.ts +46 -0
  11. package/dist/agent/ready-detector.js +86 -0
  12. package/dist/agent/wrapper.d.ts +18 -0
  13. package/dist/agent/wrapper.js +110 -0
  14. package/dist/bin/lclaude.d.ts +3 -0
  15. package/dist/bin/lclaude.js +7 -0
  16. package/dist/bin/lcodex.d.ts +3 -0
  17. package/dist/bin/lcodex.js +7 -0
  18. package/dist/bin/lgemini.d.ts +3 -0
  19. package/dist/bin/lgemini.js +7 -0
  20. package/dist/bus/daemon.d.ts +56 -0
  21. package/dist/bus/daemon.js +135 -0
  22. package/dist/bus/event-bus.d.ts +105 -0
  23. package/dist/bus/event-bus.js +157 -0
  24. package/dist/bus/message.d.ts +48 -0
  25. package/dist/bus/message.js +129 -0
  26. package/dist/bus/queue.d.ts +50 -0
  27. package/dist/bus/queue.js +100 -0
  28. package/dist/bus/store.d.ts +88 -0
  29. package/dist/bus/store.js +212 -0
  30. package/dist/bus/subscriber.d.ts +76 -0
  31. package/dist/bus/subscriber.js +187 -0
  32. package/dist/config/index.d.ts +8 -0
  33. package/dist/config/index.js +72 -0
  34. package/dist/config/schema.d.ts +18 -0
  35. package/dist/config/schema.js +58 -0
  36. package/dist/core/conversation.d.ts +34 -0
  37. package/dist/core/conversation.js +289 -0
  38. package/dist/core/engine.d.ts +40 -0
  39. package/dist/core/engine.js +288 -0
  40. package/dist/core/loop.d.ts +33 -0
  41. package/dist/core/loop.js +209 -0
  42. package/dist/core/protocol.d.ts +60 -0
  43. package/dist/core/protocol.js +162 -0
  44. package/dist/core/scoring.d.ts +34 -0
  45. package/dist/core/scoring.js +69 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +408 -0
  48. package/dist/orchestrator/daemon.d.ts +74 -0
  49. package/dist/orchestrator/daemon.js +294 -0
  50. package/dist/orchestrator/group.d.ts +73 -0
  51. package/dist/orchestrator/group.js +166 -0
  52. package/dist/orchestrator/ipc-server.d.ts +60 -0
  53. package/dist/orchestrator/ipc-server.js +166 -0
  54. package/dist/orchestrator/scheduler.d.ts +32 -0
  55. package/dist/orchestrator/scheduler.js +95 -0
  56. package/dist/plan/context.d.ts +8 -0
  57. package/dist/plan/context.js +42 -0
  58. package/dist/plan/decisions.d.ts +18 -0
  59. package/dist/plan/decisions.js +143 -0
  60. package/dist/plan/shared-plan.d.ts +33 -0
  61. package/dist/plan/shared-plan.js +211 -0
  62. package/dist/skills/executor.d.ts +7 -0
  63. package/dist/skills/executor.js +11 -0
  64. package/dist/skills/loader.d.ts +16 -0
  65. package/dist/skills/loader.js +80 -0
  66. package/dist/skills/registry.d.ts +13 -0
  67. package/dist/skills/registry.js +54 -0
  68. package/dist/terminal/adapter.d.ts +61 -0
  69. package/dist/terminal/adapter.js +42 -0
  70. package/dist/terminal/detect.d.ts +30 -0
  71. package/dist/terminal/detect.js +77 -0
  72. package/dist/terminal/iterm2-adapter.d.ts +19 -0
  73. package/dist/terminal/iterm2-adapter.js +120 -0
  74. package/dist/terminal/pty-adapter.d.ts +18 -0
  75. package/dist/terminal/pty-adapter.js +84 -0
  76. package/dist/terminal/terminal-adapter.d.ts +17 -0
  77. package/dist/terminal/terminal-adapter.js +94 -0
  78. package/dist/terminal/tmux-adapter.d.ts +18 -0
  79. package/dist/terminal/tmux-adapter.js +127 -0
  80. package/dist/ui/banner.d.ts +3 -0
  81. package/dist/ui/banner.js +145 -0
  82. package/dist/ui/colors.d.ts +41 -0
  83. package/dist/ui/colors.js +65 -0
  84. package/dist/ui/dashboard.d.ts +32 -0
  85. package/dist/ui/dashboard.js +138 -0
  86. package/dist/ui/input.d.ts +10 -0
  87. package/dist/ui/input.js +96 -0
  88. package/dist/ui/interactive.d.ts +13 -0
  89. package/dist/ui/interactive.js +230 -0
  90. package/dist/ui/renderer.d.ts +33 -0
  91. package/dist/ui/renderer.js +106 -0
  92. package/dist/utils/ansi.d.ts +11 -0
  93. package/dist/utils/ansi.js +16 -0
  94. package/dist/utils/fs.d.ts +34 -0
  95. package/dist/utils/fs.js +115 -0
  96. package/dist/utils/lock.d.ts +12 -0
  97. package/dist/utils/lock.js +116 -0
  98. package/dist/utils/process.d.ts +31 -0
  99. package/dist/utils/process.js +111 -0
  100. package/dist/utils/pty-filter.d.ts +31 -0
  101. package/dist/utils/pty-filter.js +187 -0
  102. package/package.json +71 -0
  103. package/skills/loop/SKILL.md +19 -0
  104. package/skills/plan/SKILL.md +9 -0
  105. package/skills/review/SKILL.md +14 -0
@@ -0,0 +1,74 @@
1
+ import { EventBus } from "../bus/event-bus.js";
2
+ import { type IpcRequest, type IpcResponse } from "./ipc-server.js";
3
+ /**
4
+ * Status information for the orchestrator daemon.
5
+ */
6
+ export interface DaemonStatus {
7
+ pid: number;
8
+ uptime: number;
9
+ agents: number;
10
+ busEvents: number;
11
+ }
12
+ /**
13
+ * The OrchestratorDaemon manages the lifecycle of:
14
+ * - EventBus (message routing)
15
+ * - BusDaemon (queue polling / delivery)
16
+ * - IpcServer (Unix socket for commands)
17
+ *
18
+ * It persists a PID file and logs to the run directory.
19
+ */
20
+ export declare class OrchestratorDaemon {
21
+ readonly projectRoot: string;
22
+ private readonly loopDir;
23
+ private readonly runDir;
24
+ private readonly pidPath;
25
+ private readonly logPath;
26
+ private readonly socketPath;
27
+ private eventBus;
28
+ private busDaemon;
29
+ private ipcServer;
30
+ private startTime;
31
+ private running;
32
+ constructor(projectRoot: string);
33
+ /**
34
+ * Start the daemon. Initializes bus, starts polling, opens IPC socket.
35
+ */
36
+ start(): Promise<void>;
37
+ /**
38
+ * Stop the daemon gracefully.
39
+ */
40
+ stop(): Promise<void>;
41
+ /**
42
+ * Ensure the daemon is running. If not, start it.
43
+ */
44
+ ensureRunning(): Promise<void>;
45
+ /**
46
+ * Check if the daemon is currently running.
47
+ */
48
+ isRunning(): boolean;
49
+ /**
50
+ * Get the current daemon status.
51
+ */
52
+ getStatus(): Promise<DaemonStatus>;
53
+ /**
54
+ * Handle an incoming IPC request.
55
+ */
56
+ handleRequest(req: IpcRequest): Promise<IpcResponse>;
57
+ /**
58
+ * Get the event bus instance.
59
+ */
60
+ getEventBus(): EventBus;
61
+ /**
62
+ * Get the PID file path (for external tools to check).
63
+ */
64
+ getPidPath(): string;
65
+ /**
66
+ * Get the socket path (for IPC clients).
67
+ */
68
+ getSocketPath(): string;
69
+ /**
70
+ * Get the log path.
71
+ */
72
+ getLogPath(): string;
73
+ }
74
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1,294 @@
1
+ import { join } from "node:path";
2
+ import { EventBus } from "../bus/event-bus.js";
3
+ import { BusDaemon } from "../bus/daemon.js";
4
+ import { IpcServer } from "./ipc-server.js";
5
+ import { isProcessAlive, writePidFile, readPidFile, removePidFile, setupSignalHandlers, } from "../utils/process.js";
6
+ import { ensureDir } from "../utils/fs.js";
7
+ /**
8
+ * The OrchestratorDaemon manages the lifecycle of:
9
+ * - EventBus (message routing)
10
+ * - BusDaemon (queue polling / delivery)
11
+ * - IpcServer (Unix socket for commands)
12
+ *
13
+ * It persists a PID file and logs to the run directory.
14
+ */
15
+ export class OrchestratorDaemon {
16
+ projectRoot;
17
+ loopDir;
18
+ runDir;
19
+ pidPath;
20
+ logPath;
21
+ socketPath;
22
+ eventBus;
23
+ busDaemon = null;
24
+ ipcServer = null;
25
+ startTime = 0;
26
+ running = false;
27
+ constructor(projectRoot) {
28
+ this.projectRoot = projectRoot;
29
+ this.loopDir = join(projectRoot, ".loop");
30
+ this.runDir = join(this.loopDir, "run");
31
+ this.pidPath = join(this.runDir, "loop-daemon.pid");
32
+ this.logPath = join(this.runDir, "loop-daemon.log");
33
+ this.socketPath = join(this.runDir, "loop.sock");
34
+ this.eventBus = new EventBus(projectRoot);
35
+ }
36
+ /**
37
+ * Start the daemon. Initializes bus, starts polling, opens IPC socket.
38
+ */
39
+ async start() {
40
+ if (this.running)
41
+ return;
42
+ // Ensure run directory exists
43
+ await ensureDir(this.runDir);
44
+ // Check if another daemon is already running
45
+ const existingPid = readPidFile(this.pidPath);
46
+ if (existingPid !== null && isProcessAlive(existingPid)) {
47
+ throw new Error(`Daemon already running (pid=${existingPid})`);
48
+ }
49
+ // Initialize the event bus
50
+ await this.eventBus.init();
51
+ // Write PID file
52
+ writePidFile(this.pidPath);
53
+ this.startTime = Date.now();
54
+ this.running = true;
55
+ // Start bus daemon
56
+ this.busDaemon = new BusDaemon(this.eventBus, { pollIntervalMs: 1000 });
57
+ await this.busDaemon.start();
58
+ // Start IPC server
59
+ this.ipcServer = new IpcServer(this.socketPath, (req) => this.handleRequest(req));
60
+ await this.ipcServer.start();
61
+ // Set up signal handlers
62
+ setupSignalHandlers(async () => {
63
+ await this.stop();
64
+ });
65
+ }
66
+ /**
67
+ * Stop the daemon gracefully.
68
+ */
69
+ async stop() {
70
+ if (!this.running)
71
+ return;
72
+ this.running = false;
73
+ // Stop IPC server
74
+ if (this.ipcServer) {
75
+ await this.ipcServer.stop();
76
+ this.ipcServer = null;
77
+ }
78
+ // Stop bus daemon
79
+ if (this.busDaemon) {
80
+ await this.busDaemon.stop();
81
+ this.busDaemon = null;
82
+ }
83
+ // Shut down event bus
84
+ await this.eventBus.shutdown();
85
+ // Remove PID file
86
+ removePidFile(this.pidPath);
87
+ }
88
+ /**
89
+ * Ensure the daemon is running. If not, start it.
90
+ */
91
+ async ensureRunning() {
92
+ if (this.running)
93
+ return;
94
+ const existingPid = readPidFile(this.pidPath);
95
+ if (existingPid !== null && isProcessAlive(existingPid)) {
96
+ // Another daemon is running, that's fine
97
+ return;
98
+ }
99
+ await this.start();
100
+ }
101
+ /**
102
+ * Check if the daemon is currently running.
103
+ */
104
+ isRunning() {
105
+ if (this.running)
106
+ return true;
107
+ // Also check via PID file (might be running in another process)
108
+ const pid = readPidFile(this.pidPath);
109
+ return pid !== null && isProcessAlive(pid);
110
+ }
111
+ /**
112
+ * Get the current daemon status.
113
+ */
114
+ async getStatus() {
115
+ const busStatus = await this.eventBus.status();
116
+ return {
117
+ pid: process.pid,
118
+ uptime: this.startTime > 0 ? Math.floor((Date.now() - this.startTime) / 1000) : 0,
119
+ agents: busStatus.agents,
120
+ busEvents: busStatus.events,
121
+ };
122
+ }
123
+ /**
124
+ * Handle an incoming IPC request.
125
+ */
126
+ async handleRequest(req) {
127
+ // Validate incoming request structure
128
+ if (!req || typeof req !== "object" || typeof req.type !== "string") {
129
+ return { success: false, type: "ERROR", error: "Invalid request: missing type" };
130
+ }
131
+ if (req.data !== undefined && (typeof req.data !== "object" || req.data === null)) {
132
+ return { success: false, type: "ERROR", error: "Invalid request: data must be an object" };
133
+ }
134
+ // Ensure data is always an object for safe property access
135
+ if (!req.data)
136
+ req.data = {};
137
+ try {
138
+ switch (req.type) {
139
+ case "STATUS": {
140
+ const status = await this.getStatus();
141
+ const busStatus = await this.eventBus.status();
142
+ return {
143
+ success: true,
144
+ type: "STATUS",
145
+ data: {
146
+ ...status,
147
+ agentList: busStatus.agentList,
148
+ },
149
+ };
150
+ }
151
+ case "REGISTER_AGENT": {
152
+ const agentType = String(req.data.agent_type ?? "claude");
153
+ const subscriberId = await this.eventBus.join(agentType, {
154
+ nickname: req.data.nickname,
155
+ pid: req.data.pid,
156
+ tty: req.data.tty,
157
+ launch_mode: req.data.launch_mode,
158
+ });
159
+ return {
160
+ success: true,
161
+ type: "REGISTER_AGENT",
162
+ data: { subscriber_id: subscriberId },
163
+ };
164
+ }
165
+ case "AGENT_READY": {
166
+ const subscriberId = String(req.data.subscriber_id ?? "");
167
+ await this.eventBus.getSubscriberManager().updateMetadata(subscriberId, {
168
+ activity_state: "idle",
169
+ last_seen: new Date().toISOString(),
170
+ });
171
+ return {
172
+ success: true,
173
+ type: "AGENT_READY",
174
+ data: { subscriber_id: subscriberId },
175
+ };
176
+ }
177
+ case "AGENT_REPORT": {
178
+ const subscriberId = String(req.data.subscriber_id ?? "");
179
+ await this.eventBus.getSubscriberManager().updateMetadata(subscriberId, {
180
+ last_seen: new Date().toISOString(),
181
+ activity_state: String(req.data.activity_state ?? "working"),
182
+ });
183
+ return {
184
+ success: true,
185
+ type: "AGENT_REPORT",
186
+ data: { subscriber_id: subscriberId },
187
+ };
188
+ }
189
+ case "BUS_SEND": {
190
+ const publisher = String(req.data.publisher ?? "unknown");
191
+ const target = String(req.data.target ?? "");
192
+ const message = String(req.data.message ?? "");
193
+ const event = await this.eventBus.send(publisher, target, message);
194
+ return {
195
+ success: true,
196
+ type: "BUS_SEND",
197
+ data: { seq: event.seq, target: event.target },
198
+ };
199
+ }
200
+ case "BUS_CHECK": {
201
+ const subscriberId = String(req.data.subscriber_id ?? "");
202
+ const events = await this.eventBus.check(subscriberId);
203
+ return {
204
+ success: true,
205
+ type: "BUS_CHECK",
206
+ data: {
207
+ subscriber_id: subscriberId,
208
+ count: events.length,
209
+ events: events,
210
+ },
211
+ };
212
+ }
213
+ case "LAUNCH_AGENT": {
214
+ // Placeholder - actual agent launching is handled by the agent launcher
215
+ return {
216
+ success: true,
217
+ type: "LAUNCH_AGENT",
218
+ data: { message: "Agent launch request received" },
219
+ };
220
+ }
221
+ case "CLOSE_AGENT": {
222
+ const subscriberId = String(req.data.subscriber_id ?? req.data.agent_id ?? "");
223
+ await this.eventBus.leave(subscriberId);
224
+ return {
225
+ success: true,
226
+ type: "CLOSE_AGENT",
227
+ data: { subscriber_id: subscriberId },
228
+ };
229
+ }
230
+ case "RESUME_AGENTS": {
231
+ return {
232
+ success: true,
233
+ type: "RESUME_AGENTS",
234
+ data: { message: "Resume request received" },
235
+ };
236
+ }
237
+ case "LAUNCH_GROUP": {
238
+ return {
239
+ success: true,
240
+ type: "LAUNCH_GROUP",
241
+ data: { message: "Group launch request received" },
242
+ };
243
+ }
244
+ case "STOP_GROUP": {
245
+ return {
246
+ success: true,
247
+ type: "STOP_GROUP",
248
+ data: { message: "Group stop request received" },
249
+ };
250
+ }
251
+ default: {
252
+ return {
253
+ success: false,
254
+ type: "ERROR",
255
+ error: `Unknown request type: ${req.type}`,
256
+ };
257
+ }
258
+ }
259
+ }
260
+ catch (err) {
261
+ const message = err instanceof Error ? err.message : String(err);
262
+ return {
263
+ success: false,
264
+ type: req.type,
265
+ error: message,
266
+ };
267
+ }
268
+ }
269
+ /**
270
+ * Get the event bus instance.
271
+ */
272
+ getEventBus() {
273
+ return this.eventBus;
274
+ }
275
+ /**
276
+ * Get the PID file path (for external tools to check).
277
+ */
278
+ getPidPath() {
279
+ return this.pidPath;
280
+ }
281
+ /**
282
+ * Get the socket path (for IPC clients).
283
+ */
284
+ getSocketPath() {
285
+ return this.socketPath;
286
+ }
287
+ /**
288
+ * Get the log path.
289
+ */
290
+ getLogPath() {
291
+ return this.logPath;
292
+ }
293
+ }
294
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,73 @@
1
+ import { OrchestratorDaemon } from "./daemon.js";
2
+ /**
3
+ * Describes a single agent within a group.
4
+ */
5
+ export interface GroupAgent {
6
+ /** Display name / nickname for this agent */
7
+ name: string;
8
+ /** Agent engine type (e.g. "claude", "gemini", "codex") */
9
+ engine: string;
10
+ /** Optional role description */
11
+ role?: string;
12
+ }
13
+ /**
14
+ * Describes a group of agents to be launched together.
15
+ */
16
+ export interface AgentGroup {
17
+ /** Unique group name */
18
+ name: string;
19
+ /** Agents in this group */
20
+ agents: GroupAgent[];
21
+ /** How agents are executed */
22
+ strategy: "parallel" | "sequential" | "pipeline";
23
+ }
24
+ /** Runtime state for a group member */
25
+ interface GroupMemberState {
26
+ name: string;
27
+ engine: string;
28
+ subscriberId: string;
29
+ status: "pending" | "active" | "stopped" | "failed";
30
+ launchedAt: string;
31
+ stoppedAt: string;
32
+ }
33
+ /** Runtime state for a group */
34
+ interface GroupState {
35
+ name: string;
36
+ status: "starting" | "active" | "stopped" | "failed";
37
+ strategy: "parallel" | "sequential" | "pipeline";
38
+ members: GroupMemberState[];
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }
42
+ /**
43
+ * Orchestrates groups of agents: launching them according to a strategy,
44
+ * tracking their lifecycle, and stopping them together.
45
+ */
46
+ export declare class GroupOrchestrator {
47
+ private readonly daemon;
48
+ private groups;
49
+ constructor(daemon: OrchestratorDaemon);
50
+ /**
51
+ * Launch a group of agents.
52
+ * Returns the subscriber IDs of all launched agents.
53
+ */
54
+ launchGroup(group: AgentGroup): Promise<string[]>;
55
+ /**
56
+ * Stop all agents in a group.
57
+ */
58
+ stopGroup(name: string): Promise<void>;
59
+ /**
60
+ * List all known groups and their current state.
61
+ */
62
+ listGroups(): Promise<AgentGroup[]>;
63
+ /**
64
+ * Get the state of a specific group.
65
+ */
66
+ getGroupState(name: string): GroupState | undefined;
67
+ /**
68
+ * Launch a single agent on the bus, returning its subscriber ID.
69
+ */
70
+ private launchSingleAgent;
71
+ }
72
+ export {};
73
+ //# sourceMappingURL=group.d.ts.map
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Orchestrates groups of agents: launching them according to a strategy,
3
+ * tracking their lifecycle, and stopping them together.
4
+ */
5
+ export class GroupOrchestrator {
6
+ daemon;
7
+ groups = new Map();
8
+ constructor(daemon) {
9
+ this.daemon = daemon;
10
+ }
11
+ /**
12
+ * Launch a group of agents.
13
+ * Returns the subscriber IDs of all launched agents.
14
+ */
15
+ async launchGroup(group) {
16
+ const eventBus = this.daemon.getEventBus();
17
+ const now = new Date().toISOString();
18
+ const state = {
19
+ name: group.name,
20
+ status: "starting",
21
+ strategy: group.strategy,
22
+ members: group.agents.map((agent) => ({
23
+ name: agent.name,
24
+ engine: agent.engine,
25
+ subscriberId: "",
26
+ status: "pending",
27
+ launchedAt: "",
28
+ stoppedAt: "",
29
+ })),
30
+ createdAt: now,
31
+ updatedAt: now,
32
+ };
33
+ this.groups.set(group.name, state);
34
+ const subscriberIds = [];
35
+ try {
36
+ switch (group.strategy) {
37
+ case "parallel": {
38
+ // Launch all agents concurrently
39
+ const promises = group.agents.map(async (agent, idx) => {
40
+ const subscriberId = await this.launchSingleAgent(eventBus, agent);
41
+ const member = state.members[idx];
42
+ if (member) {
43
+ member.subscriberId = subscriberId;
44
+ member.status = "active";
45
+ member.launchedAt = new Date().toISOString();
46
+ }
47
+ return subscriberId;
48
+ });
49
+ const results = await Promise.allSettled(promises);
50
+ for (let i = 0; i < results.length; i++) {
51
+ const result = results[i];
52
+ if (result.status === "fulfilled") {
53
+ subscriberIds.push(result.value);
54
+ }
55
+ else {
56
+ const member = state.members[i];
57
+ if (member) {
58
+ member.status = "failed";
59
+ }
60
+ }
61
+ }
62
+ break;
63
+ }
64
+ case "sequential":
65
+ case "pipeline": {
66
+ // Launch agents one at a time in order
67
+ for (let i = 0; i < group.agents.length; i++) {
68
+ const agent = group.agents[i];
69
+ const member = state.members[i];
70
+ try {
71
+ const subscriberId = await this.launchSingleAgent(eventBus, agent);
72
+ member.subscriberId = subscriberId;
73
+ member.status = "active";
74
+ member.launchedAt = new Date().toISOString();
75
+ subscriberIds.push(subscriberId);
76
+ }
77
+ catch {
78
+ member.status = "failed";
79
+ // For pipeline, stop on first failure
80
+ if (group.strategy === "pipeline") {
81
+ state.status = "failed";
82
+ state.updatedAt = new Date().toISOString();
83
+ this.groups.set(group.name, state);
84
+ return subscriberIds;
85
+ }
86
+ }
87
+ }
88
+ break;
89
+ }
90
+ }
91
+ // Determine overall group status
92
+ const allActive = state.members.every((m) => m.status === "active");
93
+ const anyFailed = state.members.some((m) => m.status === "failed");
94
+ state.status = allActive ? "active" : anyFailed ? "failed" : "active";
95
+ state.updatedAt = new Date().toISOString();
96
+ this.groups.set(group.name, state);
97
+ }
98
+ catch (err) {
99
+ state.status = "failed";
100
+ state.updatedAt = new Date().toISOString();
101
+ this.groups.set(group.name, state);
102
+ throw err;
103
+ }
104
+ return subscriberIds;
105
+ }
106
+ /**
107
+ * Stop all agents in a group.
108
+ */
109
+ async stopGroup(name) {
110
+ const state = this.groups.get(name);
111
+ if (!state) {
112
+ throw new Error(`Group "${name}" not found`);
113
+ }
114
+ const eventBus = this.daemon.getEventBus();
115
+ // Stop in reverse order
116
+ for (let i = state.members.length - 1; i >= 0; i--) {
117
+ const member = state.members[i];
118
+ if (member.status !== "active" || !member.subscriberId)
119
+ continue;
120
+ try {
121
+ await eventBus.leave(member.subscriberId);
122
+ member.status = "stopped";
123
+ member.stoppedAt = new Date().toISOString();
124
+ }
125
+ catch {
126
+ // Best effort - continue stopping others
127
+ }
128
+ }
129
+ state.status = "stopped";
130
+ state.updatedAt = new Date().toISOString();
131
+ this.groups.set(name, state);
132
+ }
133
+ /**
134
+ * List all known groups and their current state.
135
+ */
136
+ async listGroups() {
137
+ const groups = [];
138
+ for (const state of this.groups.values()) {
139
+ groups.push({
140
+ name: state.name,
141
+ strategy: state.strategy,
142
+ agents: state.members.map((m) => ({
143
+ name: m.name,
144
+ engine: m.engine,
145
+ })),
146
+ });
147
+ }
148
+ return groups;
149
+ }
150
+ /**
151
+ * Get the state of a specific group.
152
+ */
153
+ getGroupState(name) {
154
+ return this.groups.get(name);
155
+ }
156
+ /**
157
+ * Launch a single agent on the bus, returning its subscriber ID.
158
+ */
159
+ async launchSingleAgent(eventBus, agent) {
160
+ return eventBus.join(agent.engine, {
161
+ nickname: agent.name,
162
+ activity_state: "starting",
163
+ });
164
+ }
165
+ }
166
+ //# sourceMappingURL=group.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Request types the IPC server handles.
3
+ */
4
+ export type IpcRequestType = "REGISTER_AGENT" | "AGENT_READY" | "AGENT_REPORT" | "LAUNCH_AGENT" | "CLOSE_AGENT" | "RESUME_AGENTS" | "STATUS" | "BUS_SEND" | "BUS_CHECK" | "LAUNCH_GROUP" | "STOP_GROUP";
5
+ /**
6
+ * Incoming IPC request.
7
+ */
8
+ export interface IpcRequest {
9
+ type: IpcRequestType;
10
+ data: Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Outgoing IPC response.
14
+ */
15
+ export interface IpcResponse {
16
+ success: boolean;
17
+ type: string;
18
+ data?: Record<string, unknown>;
19
+ error?: string;
20
+ }
21
+ /** Handler function signature for processing requests. */
22
+ export type IpcRequestHandler = (req: IpcRequest) => Promise<IpcResponse>;
23
+ /**
24
+ * Unix domain socket IPC server.
25
+ *
26
+ * Protocol: newline-delimited JSON (each message is a JSON object
27
+ * followed by a newline character).
28
+ */
29
+ export declare class IpcServer {
30
+ private readonly socketPath;
31
+ private readonly handler;
32
+ private server;
33
+ private sockets;
34
+ constructor(socketPath: string, handler: IpcRequestHandler);
35
+ /**
36
+ * Start listening on the Unix socket.
37
+ */
38
+ start(): Promise<void>;
39
+ /**
40
+ * Stop the IPC server and close all connections.
41
+ */
42
+ stop(): Promise<void>;
43
+ /**
44
+ * Send a message to all connected clients.
45
+ */
46
+ broadcast(payload: IpcResponse): void;
47
+ /**
48
+ * Get the number of connected clients.
49
+ */
50
+ get clientCount(): number;
51
+ /**
52
+ * Handle a new socket connection.
53
+ */
54
+ private handleConnection;
55
+ /**
56
+ * Process a single JSON line from a client.
57
+ */
58
+ private processLine;
59
+ }
60
+ //# sourceMappingURL=ipc-server.d.ts.map