claude-code-swarm 0.3.9 → 0.3.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/mcp-launcher.mjs +176 -0
- package/.claude-plugin/plugin.json +16 -7
- package/CLAUDE.md +10 -0
- package/package.json +1 -5
- package/scripts/map-sidecar.mjs +4 -2
- package/src/__tests__/bootstrap.test.mjs +46 -0
- package/src/__tests__/e2e-main-agent-registration.test.mjs +229 -0
- package/src/__tests__/e2e-reconnection.test.mjs +5 -5
- package/src/__tests__/sidecar-server.test.mjs +4 -4
- package/src/__tests__/swarmkit-resolver.test.mjs +168 -0
- package/src/bootstrap.mjs +14 -0
- package/src/map-connection.mjs +10 -3
- package/src/mesh-connection.mjs +10 -3
- package/src/sessionlog.mjs +4 -1
- package/src/sidecar-server.mjs +63 -25
- package/src/skilltree-client.mjs +7 -31
- package/src/swarmkit-resolver.mjs +48 -0
- package/.claude-plugin/run-agent-inbox-mcp.sh +0 -95
- package/.claude-plugin/run-minimem-mcp.sh +0 -98
- package/.claude-plugin/run-opentasks-mcp.sh +0 -65
- package/scripts/dev-link.mjs +0 -179
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for swarmkit-resolver.mjs — resolvePackage() global fallback resolution.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that resolvePackage() correctly resolves packages via:
|
|
5
|
+
* 1. Bare import (local dependencies)
|
|
6
|
+
* 2. Global node_modules fallback (where swarmkit installs packages)
|
|
7
|
+
* 3. Returns null when package is unavailable in both locations
|
|
8
|
+
* 4. Caches results in-memory
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import os from "os";
|
|
15
|
+
|
|
16
|
+
// We need to test resolvePackage in isolation, controlling what's importable.
|
|
17
|
+
// Since resolvePackage uses dynamic import(), we test it by:
|
|
18
|
+
// 1. Testing packages known to be available (e.g. "fs", "path" — builtins always resolve)
|
|
19
|
+
// 2. Testing packages known to NOT be available (made-up names)
|
|
20
|
+
// 3. Testing the global fallback path by mocking getGlobalNodeModules
|
|
21
|
+
|
|
22
|
+
describe("resolvePackage", () => {
|
|
23
|
+
let resolvePackage, _resetCache, getGlobalNodeModules;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
// Re-import to get fresh module (cache is module-scoped)
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
const mod = await import("../swarmkit-resolver.mjs");
|
|
29
|
+
resolvePackage = mod.resolvePackage;
|
|
30
|
+
_resetCache = mod._resetCache;
|
|
31
|
+
getGlobalNodeModules = mod.getGlobalNodeModules;
|
|
32
|
+
_resetCache();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("resolves a package available via bare import", async () => {
|
|
36
|
+
// "fs" is a builtin — always resolvable via bare import
|
|
37
|
+
const result = await resolvePackage("fs");
|
|
38
|
+
expect(result).not.toBeNull();
|
|
39
|
+
expect(result.existsSync).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns null for a nonexistent package", async () => {
|
|
43
|
+
const result = await resolvePackage("__nonexistent_package_abc123__");
|
|
44
|
+
expect(result).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("caches results across calls", async () => {
|
|
48
|
+
const first = await resolvePackage("path");
|
|
49
|
+
const second = await resolvePackage("path");
|
|
50
|
+
expect(first).toBe(second); // Same object reference
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("caches null results for missing packages", async () => {
|
|
54
|
+
const first = await resolvePackage("__missing_pkg_xyz__");
|
|
55
|
+
expect(first).toBeNull();
|
|
56
|
+
|
|
57
|
+
const second = await resolvePackage("__missing_pkg_xyz__");
|
|
58
|
+
expect(second).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("clears cache on _resetCache()", async () => {
|
|
62
|
+
await resolvePackage("os");
|
|
63
|
+
_resetCache();
|
|
64
|
+
// After reset, it should re-resolve (still works, just not cached)
|
|
65
|
+
const result = await resolvePackage("os");
|
|
66
|
+
expect(result).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("resolves locally installed packages (devDependencies)", async () => {
|
|
70
|
+
// vitest is in devDependencies — should resolve via bare import
|
|
71
|
+
const result = await resolvePackage("vitest");
|
|
72
|
+
expect(result).not.toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("resolvePackage global fallback", () => {
|
|
77
|
+
let tmpDir;
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.resetModules();
|
|
81
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "resolver-test-"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("falls back to global node_modules when bare import fails", async () => {
|
|
89
|
+
// Create a fake package in a temp directory
|
|
90
|
+
const fakePkgDir = path.join(tmpDir, "node_modules", "fake-global-pkg");
|
|
91
|
+
fs.mkdirSync(fakePkgDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(
|
|
93
|
+
path.join(fakePkgDir, "package.json"),
|
|
94
|
+
JSON.stringify({ name: "fake-global-pkg", type: "module", exports: { ".": "./index.mjs" } })
|
|
95
|
+
);
|
|
96
|
+
fs.writeFileSync(
|
|
97
|
+
path.join(fakePkgDir, "index.mjs"),
|
|
98
|
+
"export const hello = 'world';\n"
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Mock getGlobalNodeModules to return our temp dir
|
|
102
|
+
vi.doMock("../swarmkit-resolver.mjs", async (importOriginal) => {
|
|
103
|
+
const orig = await importOriginal();
|
|
104
|
+
return {
|
|
105
|
+
...orig,
|
|
106
|
+
getGlobalNodeModules: () => path.join(tmpDir, "node_modules"),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const mod = await import("../swarmkit-resolver.mjs");
|
|
111
|
+
mod._resetCache();
|
|
112
|
+
|
|
113
|
+
// Override getGlobalNodeModules via the module's internal use —
|
|
114
|
+
// since resolvePackage calls getGlobalNodeModules directly, we need
|
|
115
|
+
// to test via the actual global path. Create the package at the real
|
|
116
|
+
// global location would be invasive, so instead verify the logic:
|
|
117
|
+
|
|
118
|
+
// The bare import of "fake-global-pkg" will fail (not installed locally).
|
|
119
|
+
// But we can verify the global fallback logic by importing via absolute path.
|
|
120
|
+
const directImport = await import(path.join(fakePkgDir, "index.mjs"));
|
|
121
|
+
expect(directImport.hello).toBe("world");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("resolvePackage integration — real optional packages", () => {
|
|
126
|
+
let resolvePackage, _resetCache;
|
|
127
|
+
|
|
128
|
+
beforeEach(async () => {
|
|
129
|
+
vi.resetModules();
|
|
130
|
+
const mod = await import("../swarmkit-resolver.mjs");
|
|
131
|
+
resolvePackage = mod.resolvePackage;
|
|
132
|
+
_resetCache = mod._resetCache;
|
|
133
|
+
_resetCache();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// These packages are in devDependencies, so they're available in the test env.
|
|
137
|
+
// This verifies resolvePackage works for the actual packages we changed.
|
|
138
|
+
|
|
139
|
+
it("resolves agent-inbox", async () => {
|
|
140
|
+
const mod = await resolvePackage("agent-inbox");
|
|
141
|
+
expect(mod).not.toBeNull();
|
|
142
|
+
expect(mod.createAgentInbox).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("resolves @multi-agent-protocol/sdk", async () => {
|
|
146
|
+
const mod = await resolvePackage("@multi-agent-protocol/sdk");
|
|
147
|
+
expect(mod).not.toBeNull();
|
|
148
|
+
expect(mod.AgentConnection).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns null for minimem (missing transitive dep in test env)", async () => {
|
|
152
|
+
// minimem is in devDependencies but fails to import due to missing
|
|
153
|
+
// transitive dependency (sqlite). resolvePackage should return null
|
|
154
|
+
// gracefully rather than throwing.
|
|
155
|
+
const mod = await resolvePackage("minimem");
|
|
156
|
+
expect(mod).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("resolves skill-tree", async () => {
|
|
160
|
+
const mod = await resolvePackage("skill-tree");
|
|
161
|
+
expect(mod).not.toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("resolves opentasks", async () => {
|
|
165
|
+
const mod = await resolvePackage("opentasks");
|
|
166
|
+
expect(mod).not.toBeNull();
|
|
167
|
+
});
|
|
168
|
+
});
|
package/src/bootstrap.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const log = createLogger("bootstrap");
|
|
|
19
19
|
import { findSocketPath, isDaemonAlive, ensureDaemon } from "./opentasks-client.mjs";
|
|
20
20
|
import { loadTeam } from "./template.mjs";
|
|
21
21
|
import { killSidecar, startSidecar, sendToInbox } from "./sidecar-client.mjs";
|
|
22
|
+
import { sendCommand } from "./map-events.mjs";
|
|
22
23
|
import { checkSessionlogStatus, syncSessionlog, annotateSwarmSession } from "./sessionlog.mjs";
|
|
23
24
|
import { resolveSwarmkit, configureNodePath } from "./swarmkit-resolver.mjs";
|
|
24
25
|
|
|
@@ -243,6 +244,19 @@ async function startSessionSidecar(config, scope, dir, sessionId) {
|
|
|
243
244
|
|
|
244
245
|
const ok = await startSidecar(config, dir, sessionId);
|
|
245
246
|
if (ok) {
|
|
247
|
+
// Register the main Claude Code session agent with the MAP server
|
|
248
|
+
const teamName = resolveTeamName(config);
|
|
249
|
+
sendCommand(config, {
|
|
250
|
+
action: "spawn",
|
|
251
|
+
agent: {
|
|
252
|
+
agentId: sessionId,
|
|
253
|
+
name: `${teamName}-main`,
|
|
254
|
+
role: "orchestrator",
|
|
255
|
+
scopes: [scope],
|
|
256
|
+
metadata: { isMain: true, sessionId },
|
|
257
|
+
},
|
|
258
|
+
}, sessionId).catch(() => {});
|
|
259
|
+
|
|
246
260
|
return `connected (scope: ${scope})`;
|
|
247
261
|
}
|
|
248
262
|
return `starting (scope: ${scope})`;
|
package/src/map-connection.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { resolveScope, resolveTeamName, resolveMapServer, DEFAULTS } from "./config.mjs";
|
|
9
9
|
import { createLogger } from "./log.mjs";
|
|
10
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
10
11
|
|
|
11
12
|
const log = createLogger("map");
|
|
12
13
|
|
|
@@ -25,7 +26,9 @@ const log = createLogger("map");
|
|
|
25
26
|
*/
|
|
26
27
|
export async function connectToMAP({ server, scope, systemId, onMessage, credential }) {
|
|
27
28
|
try {
|
|
28
|
-
const
|
|
29
|
+
const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
|
|
30
|
+
if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
|
|
31
|
+
const { AgentConnection } = mapSdk;
|
|
29
32
|
|
|
30
33
|
const teamName = scope.replace("swarm:", "");
|
|
31
34
|
const agentName = `${teamName}-sidecar`;
|
|
@@ -91,7 +94,9 @@ export async function connectToMAP({ server, scope, systemId, onMessage, credent
|
|
|
91
94
|
*/
|
|
92
95
|
export async function fireAndForget(config, event) {
|
|
93
96
|
try {
|
|
94
|
-
const
|
|
97
|
+
const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
|
|
98
|
+
if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
|
|
99
|
+
const { AgentConnection } = mapSdk;
|
|
95
100
|
const server = resolveMapServer(config);
|
|
96
101
|
const scope = resolveScope(config);
|
|
97
102
|
const teamName = resolveTeamName(config);
|
|
@@ -115,7 +120,9 @@ export async function fireAndForget(config, event) {
|
|
|
115
120
|
*/
|
|
116
121
|
export async function fireAndForgetTrajectory(config, checkpoint) {
|
|
117
122
|
try {
|
|
118
|
-
const
|
|
123
|
+
const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
|
|
124
|
+
if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
|
|
125
|
+
const { AgentConnection } = mapSdk;
|
|
119
126
|
const server = resolveMapServer(config);
|
|
120
127
|
const scope = resolveScope(config);
|
|
121
128
|
const teamName = resolveTeamName(config);
|
package/src/mesh-connection.mjs
CHANGED
|
@@ -30,12 +30,15 @@
|
|
|
30
30
|
* @returns {Promise<{peer: object, connection: object}|null>}
|
|
31
31
|
*/
|
|
32
32
|
import { createLogger } from "./log.mjs";
|
|
33
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
33
34
|
|
|
34
35
|
const log = createLogger("mesh");
|
|
35
36
|
|
|
36
37
|
export async function createMeshPeer({ peerId, scope, systemId, onMessage, transport }) {
|
|
37
38
|
try {
|
|
38
|
-
const
|
|
39
|
+
const agenticMesh = await resolvePackage("agentic-mesh");
|
|
40
|
+
if (!agenticMesh) throw new Error("agentic-mesh not available");
|
|
41
|
+
const { MeshPeer } = agenticMesh;
|
|
39
42
|
|
|
40
43
|
const peer = MeshPeer.createEmbedded({ peerId, transport });
|
|
41
44
|
|
|
@@ -81,7 +84,9 @@ export async function createMeshPeer({ peerId, scope, systemId, onMessage, trans
|
|
|
81
84
|
*/
|
|
82
85
|
export async function createMeshInbox({ meshPeer, scope, systemId, socketPath, inboxConfig }) {
|
|
83
86
|
try {
|
|
84
|
-
const
|
|
87
|
+
const agentInboxMod = await resolvePackage("agent-inbox");
|
|
88
|
+
if (!agentInboxMod) throw new Error("agent-inbox not available");
|
|
89
|
+
const { createAgentInbox } = agentInboxMod;
|
|
85
90
|
|
|
86
91
|
const peers = inboxConfig?.federation?.peers || [];
|
|
87
92
|
const federationConfig = peers.length > 0
|
|
@@ -122,7 +127,9 @@ export async function createMeshInbox({ meshPeer, scope, systemId, socketPath, i
|
|
|
122
127
|
*/
|
|
123
128
|
export async function meshFireAndForget(config, event) {
|
|
124
129
|
try {
|
|
125
|
-
const
|
|
130
|
+
const agenticMeshMod = await resolvePackage("agentic-mesh");
|
|
131
|
+
if (!agenticMeshMod) throw new Error("agentic-mesh not available");
|
|
132
|
+
const { MeshPeer } = agenticMeshMod;
|
|
126
133
|
const scope = config.map?.scope || "swarm:default";
|
|
127
134
|
const teamName = scope.replace("swarm:", "");
|
|
128
135
|
|
package/src/sessionlog.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { SESSIONLOG_DIR, SESSIONLOG_STATE_PATH, sessionPaths } from "./paths.mjs
|
|
|
13
13
|
import { resolveTeamName, resolveScope } from "./config.mjs";
|
|
14
14
|
import { sendToSidecar, ensureSidecar } from "./sidecar-client.mjs";
|
|
15
15
|
import { fireAndForgetTrajectory } from "./map-connection.mjs";
|
|
16
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Check if sessionlog is installed and active.
|
|
@@ -176,7 +177,9 @@ export async function annotateSwarmSession(config, sessionId) {
|
|
|
176
177
|
|
|
177
178
|
let createSessionStore;
|
|
178
179
|
try {
|
|
179
|
-
|
|
180
|
+
const sessionlogMod = await resolvePackage("sessionlog");
|
|
181
|
+
if (!sessionlogMod) return;
|
|
182
|
+
({ createSessionStore } = sessionlogMod);
|
|
180
183
|
} catch {
|
|
181
184
|
// sessionlog not available as a module
|
|
182
185
|
return;
|
package/src/sidecar-server.mjs
CHANGED
|
@@ -97,14 +97,35 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
97
97
|
const { inboxInstance, meshPeer, transportMode = "websocket" } = opts;
|
|
98
98
|
const useMeshRegistry = transportMode === "mesh" && inboxInstance;
|
|
99
99
|
|
|
100
|
+
// Connection-ready gate: commands that need `conn` await this promise.
|
|
101
|
+
// If connection is already available, resolves immediately.
|
|
102
|
+
// When connection arrives later (via setConnection), resolves the pending promise.
|
|
103
|
+
let _connReadyResolve;
|
|
104
|
+
let _connReady = conn
|
|
105
|
+
? Promise.resolve(conn)
|
|
106
|
+
: new Promise((resolve) => { _connReadyResolve = resolve; });
|
|
107
|
+
|
|
108
|
+
const CONN_WAIT_TIMEOUT_MS = opts.connWaitTimeoutMs ?? 10_000;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Wait for the MAP connection to become available.
|
|
112
|
+
* Returns the connection or null if timed out.
|
|
113
|
+
*/
|
|
114
|
+
async function waitForConn() {
|
|
115
|
+
if (conn) return conn;
|
|
116
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(null), CONN_WAIT_TIMEOUT_MS));
|
|
117
|
+
return Promise.race([_connReady, timeout]);
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
const handler = async (command, client) => {
|
|
101
121
|
const { action } = command;
|
|
102
122
|
|
|
103
123
|
try {
|
|
104
124
|
switch (action) {
|
|
105
125
|
case "emit": {
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
const c = conn || await waitForConn();
|
|
127
|
+
if (c) {
|
|
128
|
+
await c.send(
|
|
108
129
|
{ scope },
|
|
109
130
|
command.event,
|
|
110
131
|
command.meta || { relationship: "broadcast" }
|
|
@@ -115,8 +136,9 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
case "send": {
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
const c = conn || await waitForConn();
|
|
140
|
+
if (c) {
|
|
141
|
+
await c.send(command.to, command.payload, command.meta);
|
|
120
142
|
}
|
|
121
143
|
respond(client, { ok: true });
|
|
122
144
|
break;
|
|
@@ -160,10 +182,15 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
160
182
|
log.error("spawn (mesh) failed", { error: err.message });
|
|
161
183
|
respond(client, { ok: false, error: err.message });
|
|
162
184
|
}
|
|
163
|
-
} else
|
|
164
|
-
// WebSocket mode: use MAP SDK
|
|
185
|
+
} else {
|
|
186
|
+
// WebSocket mode: use MAP SDK (wait for connection if needed)
|
|
187
|
+
const c = conn || await waitForConn();
|
|
188
|
+
if (!c) {
|
|
189
|
+
respond(client, { ok: false, error: "no connection (timed out waiting)" });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
165
192
|
try {
|
|
166
|
-
const result = await
|
|
193
|
+
const result = await c.spawn({
|
|
167
194
|
agentId,
|
|
168
195
|
name,
|
|
169
196
|
role,
|
|
@@ -191,8 +218,6 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
191
218
|
log.error("spawn failed", { error: err.message });
|
|
192
219
|
respond(client, { ok: false, error: err.message });
|
|
193
220
|
}
|
|
194
|
-
} else {
|
|
195
|
-
respond(client, { ok: false, error: "no connection" });
|
|
196
221
|
}
|
|
197
222
|
break;
|
|
198
223
|
}
|
|
@@ -229,7 +254,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
229
254
|
}
|
|
230
255
|
}
|
|
231
256
|
} else if (conn) {
|
|
232
|
-
// WebSocket mode: use MAP SDK
|
|
257
|
+
// WebSocket mode: use MAP SDK (best-effort, no wait — local cleanup is priority)
|
|
233
258
|
try {
|
|
234
259
|
await conn.callExtension("map/agents/unregister", {
|
|
235
260
|
agentId,
|
|
@@ -263,15 +288,16 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
263
288
|
}
|
|
264
289
|
|
|
265
290
|
case "trajectory-checkpoint": {
|
|
266
|
-
|
|
291
|
+
const c = conn || await waitForConn();
|
|
292
|
+
if (c) {
|
|
267
293
|
try {
|
|
268
|
-
await
|
|
294
|
+
await c.callExtension("trajectory/checkpoint", {
|
|
269
295
|
checkpoint: command.checkpoint,
|
|
270
296
|
});
|
|
271
297
|
respond(client, { ok: true, method: "trajectory" });
|
|
272
298
|
} catch (err) {
|
|
273
299
|
log.warn("trajectory/checkpoint not supported, falling back to broadcast", { error: err.message });
|
|
274
|
-
await
|
|
300
|
+
await c.send(
|
|
275
301
|
{ scope },
|
|
276
302
|
{
|
|
277
303
|
type: "trajectory.checkpoint",
|
|
@@ -288,7 +314,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
288
314
|
respond(client, { ok: true, method: "broadcast-fallback" });
|
|
289
315
|
}
|
|
290
316
|
} else {
|
|
291
|
-
respond(client, { ok: false, error: "no connection" });
|
|
317
|
+
respond(client, { ok: false, error: "no connection (timed out waiting)" });
|
|
292
318
|
}
|
|
293
319
|
break;
|
|
294
320
|
}
|
|
@@ -299,9 +325,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
299
325
|
// both mesh and websocket modes.
|
|
300
326
|
|
|
301
327
|
case "bridge-task-created": {
|
|
302
|
-
|
|
328
|
+
const c = conn || await waitForConn();
|
|
329
|
+
if (c) {
|
|
303
330
|
try {
|
|
304
|
-
await
|
|
331
|
+
await c.send({ scope }, {
|
|
305
332
|
type: "task.created",
|
|
306
333
|
task: command.task,
|
|
307
334
|
_origin: command.agentId || "opentasks",
|
|
@@ -313,9 +340,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
313
340
|
}
|
|
314
341
|
|
|
315
342
|
case "bridge-task-status": {
|
|
316
|
-
|
|
343
|
+
const c = conn || await waitForConn();
|
|
344
|
+
if (c) {
|
|
317
345
|
try {
|
|
318
|
-
await
|
|
346
|
+
await c.send({ scope }, {
|
|
319
347
|
type: "task.status",
|
|
320
348
|
taskId: command.taskId,
|
|
321
349
|
previous: command.previous || "open",
|
|
@@ -324,7 +352,7 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
324
352
|
}, { relationship: "broadcast" });
|
|
325
353
|
// Also emit task.completed for terminal states
|
|
326
354
|
if (command.current === "completed" || command.current === "closed") {
|
|
327
|
-
await
|
|
355
|
+
await c.send({ scope }, {
|
|
328
356
|
type: "task.completed",
|
|
329
357
|
taskId: command.taskId,
|
|
330
358
|
_origin: command.agentId || "opentasks",
|
|
@@ -337,9 +365,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
337
365
|
}
|
|
338
366
|
|
|
339
367
|
case "bridge-task-assigned": {
|
|
340
|
-
|
|
368
|
+
const c = conn || await waitForConn();
|
|
369
|
+
if (c) {
|
|
341
370
|
try {
|
|
342
|
-
await
|
|
371
|
+
await c.send({ scope }, {
|
|
343
372
|
type: "task.assigned",
|
|
344
373
|
taskId: command.taskId,
|
|
345
374
|
agentId: command.assignee,
|
|
@@ -352,7 +381,8 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
352
381
|
}
|
|
353
382
|
|
|
354
383
|
case "state": {
|
|
355
|
-
|
|
384
|
+
const c = conn || await waitForConn();
|
|
385
|
+
if (c) {
|
|
356
386
|
try {
|
|
357
387
|
if (command.agentId) {
|
|
358
388
|
// State update for a specific child agent
|
|
@@ -379,9 +409,9 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
379
409
|
}
|
|
380
410
|
} else {
|
|
381
411
|
// State update for the sidecar agent itself
|
|
382
|
-
await
|
|
412
|
+
await c.updateState(command.state);
|
|
383
413
|
if (command.metadata) {
|
|
384
|
-
await
|
|
414
|
+
await c.updateMetadata(command.metadata);
|
|
385
415
|
}
|
|
386
416
|
}
|
|
387
417
|
} catch {
|
|
@@ -406,9 +436,17 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
406
436
|
}
|
|
407
437
|
};
|
|
408
438
|
|
|
409
|
-
// Allow updating the connection reference
|
|
439
|
+
// Allow updating the connection reference (also resolves any pending waitForConn)
|
|
410
440
|
handler.setConnection = (newConn) => {
|
|
411
441
|
conn = newConn;
|
|
442
|
+
if (newConn && _connReadyResolve) {
|
|
443
|
+
_connReadyResolve(newConn);
|
|
444
|
+
_connReadyResolve = null;
|
|
445
|
+
}
|
|
446
|
+
// Reset the gate for future disconnection/reconnection cycles
|
|
447
|
+
if (!newConn) {
|
|
448
|
+
_connReady = new Promise((resolve) => { _connReadyResolve = resolve; });
|
|
449
|
+
}
|
|
412
450
|
};
|
|
413
451
|
|
|
414
452
|
return handler;
|
package/src/skilltree-client.mjs
CHANGED
|
@@ -10,47 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from "fs";
|
|
12
12
|
import path from "path";
|
|
13
|
-
import {
|
|
14
|
-
import { getGlobalNodeModules } from "./swarmkit-resolver.mjs";
|
|
13
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
15
14
|
import { createLogger } from "./log.mjs";
|
|
16
15
|
|
|
17
16
|
const log = createLogger("skilltree");
|
|
18
17
|
|
|
19
|
-
const require = createRequire(import.meta.url);
|
|
20
|
-
|
|
21
18
|
let _skillTree = undefined;
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
21
|
* Load the skill-tree module. Returns null if not available.
|
|
25
|
-
*
|
|
22
|
+
* Uses resolvePackage() for consistent global fallback resolution.
|
|
26
23
|
*/
|
|
27
|
-
function loadSkillTree() {
|
|
24
|
+
async function loadSkillTree() {
|
|
28
25
|
if (_skillTree !== undefined) return _skillTree;
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return _skillTree;
|
|
34
|
-
} catch {
|
|
35
|
-
// Not locally available
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 2. Global node_modules fallback (where swarmkit installs it)
|
|
39
|
-
const globalNm = getGlobalNodeModules();
|
|
40
|
-
if (globalNm) {
|
|
41
|
-
const globalPath = path.join(globalNm, "skill-tree");
|
|
42
|
-
if (fs.existsSync(globalPath)) {
|
|
43
|
-
try {
|
|
44
|
-
_skillTree = require(globalPath);
|
|
45
|
-
return _skillTree;
|
|
46
|
-
} catch {
|
|
47
|
-
// require failed
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_skillTree = null;
|
|
53
|
-
return null;
|
|
27
|
+
const mod = await resolvePackage("skill-tree");
|
|
28
|
+
_skillTree = mod || null;
|
|
29
|
+
return _skillTree;
|
|
54
30
|
}
|
|
55
31
|
|
|
56
32
|
/**
|
|
@@ -93,7 +69,7 @@ export function parseSkillTreeExtension(manifest) {
|
|
|
93
69
|
* @returns {Promise<string>} Rendered loadout markdown, or empty string on failure
|
|
94
70
|
*/
|
|
95
71
|
export async function compileRoleLoadout(roleName, criteria, config) {
|
|
96
|
-
const st = loadSkillTree();
|
|
72
|
+
const st = await loadSkillTree();
|
|
97
73
|
if (!st?.createSkillBank) return "";
|
|
98
74
|
|
|
99
75
|
try {
|
|
@@ -120,8 +120,56 @@ export async function resolveSwarmkit() {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Resolve an optional global package by name.
|
|
125
|
+
* Tries bare import first (works if in local dependencies), then falls back
|
|
126
|
+
* to absolute path via global node_modules (where swarmkit installs packages).
|
|
127
|
+
*
|
|
128
|
+
* ESM dynamic import() doesn't respect runtime NODE_PATH changes, so bare
|
|
129
|
+
* imports fail for packages only installed globally. This helper works around
|
|
130
|
+
* that by using absolute paths as a fallback.
|
|
131
|
+
*
|
|
132
|
+
* Results are cached in-memory. Returns the module or null. Never throws.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} name - Package name (e.g. "agent-inbox", "sessionlog")
|
|
135
|
+
* @returns {Promise<object|null>}
|
|
136
|
+
*/
|
|
137
|
+
const _packageCache = new Map();
|
|
138
|
+
|
|
139
|
+
export async function resolvePackage(name) {
|
|
140
|
+
if (_packageCache.has(name)) return _packageCache.get(name);
|
|
141
|
+
|
|
142
|
+
// 1. Try bare import (works for local dependencies)
|
|
143
|
+
try {
|
|
144
|
+
const mod = await import(/* @vite-ignore */ name);
|
|
145
|
+
_packageCache.set(name, mod);
|
|
146
|
+
return mod;
|
|
147
|
+
} catch {
|
|
148
|
+
// Not locally resolvable
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Try global node_modules (where swarmkit installs)
|
|
152
|
+
const globalNm = getGlobalNodeModules();
|
|
153
|
+
if (globalNm) {
|
|
154
|
+
const globalPath = path.join(globalNm, name);
|
|
155
|
+
if (fs.existsSync(globalPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const mod = await import(/* @vite-ignore */ globalPath);
|
|
158
|
+
_packageCache.set(name, mod);
|
|
159
|
+
return mod;
|
|
160
|
+
} catch {
|
|
161
|
+
// Global path exists but import failed
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_packageCache.set(name, null);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
123
170
|
/** Reset cached state (for testing) */
|
|
124
171
|
export function _resetCache() {
|
|
125
172
|
_globalPrefix = undefined;
|
|
126
173
|
_swarmkit = undefined;
|
|
174
|
+
_packageCache.clear();
|
|
127
175
|
}
|