claude-flow 3.47.0 → 3.47.1
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/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.d.ts +88 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind-worker.js +242 -0
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +24 -41
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +73 -53
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hive-mind worker execution: the MCP config handed to a spawned Claude, and
|
|
3
|
+
* `hive-mind work`, which actually runs a queued task.
|
|
4
|
+
*
|
|
5
|
+
* Before this, `hive-mind task` only wrote a `pending` row to the task store.
|
|
6
|
+
* Workers are records, not processes, and nothing ever claimed the row: the
|
|
7
|
+
* task stayed pending and the worker stayed idle forever. `work` is the missing
|
|
8
|
+
* consumer — claim one task, assign it to an idle hive worker, run it through
|
|
9
|
+
* `claude -p`, and record the outcome.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from '../types.js';
|
|
12
|
+
export interface WorkerMcpConfigOptions {
|
|
13
|
+
/** `--mcp-config` from the caller; used verbatim when given. */
|
|
14
|
+
explicit?: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
home: string;
|
|
17
|
+
/** Where the generated config is written (e.g. `.hive-mind/sessions`). */
|
|
18
|
+
outDir: string;
|
|
19
|
+
swarmId: string;
|
|
20
|
+
/** The ruflo MCP server to register when no source config provides one. */
|
|
21
|
+
rufloServer: {
|
|
22
|
+
command: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The MCP config file to pass to a spawned Claude.
|
|
28
|
+
*
|
|
29
|
+
* A source file is never passed through as-is. `~/.claude.json` is Claude
|
|
30
|
+
* Code's whole state file; on a machine where it has no top-level
|
|
31
|
+
* `mcpServers` (a fresh desktop), Claude exits at startup with "Invalid MCP
|
|
32
|
+
* configuration: mcpServers: Invalid input", before any task can run. And a
|
|
33
|
+
* config lacking a `ruflo` server leaves the worker without the tools its
|
|
34
|
+
* prompt names. So the servers are extracted, a `ruflo` server is guaranteed,
|
|
35
|
+
* and the result is written as a config Claude is known to accept.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveWorkerMcpConfig(opts: WorkerMcpConfigOptions): string;
|
|
38
|
+
/**
|
|
39
|
+
* The ruflo MCP server as launched by THIS cli, so a worker and the CLI that
|
|
40
|
+
* dispatched it share one version and one store format. `@latest` could
|
|
41
|
+
* resolve to a different release mid-session.
|
|
42
|
+
*/
|
|
43
|
+
export declare function currentRufloServer(): {
|
|
44
|
+
command: string;
|
|
45
|
+
args: string[];
|
|
46
|
+
};
|
|
47
|
+
export interface RunOutcome {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
output: string;
|
|
50
|
+
exitCode: number | null;
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
costUsd?: number;
|
|
53
|
+
durationMs: number;
|
|
54
|
+
}
|
|
55
|
+
export type TaskRunner = (prompt: string, opts: {
|
|
56
|
+
mcpConfig: string;
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
cwd: string;
|
|
59
|
+
}) => Promise<RunOutcome>;
|
|
60
|
+
/** Run one prompt through `claude -p` and parse its JSON result. */
|
|
61
|
+
export declare const runWithClaude: TaskRunner;
|
|
62
|
+
export interface WorkOnceOptions {
|
|
63
|
+
cwd: string;
|
|
64
|
+
mcpConfig: string;
|
|
65
|
+
runner: TaskRunner;
|
|
66
|
+
taskId?: string;
|
|
67
|
+
workerId?: string;
|
|
68
|
+
defaultTimeoutMs?: number;
|
|
69
|
+
}
|
|
70
|
+
export type WorkOnceResult = {
|
|
71
|
+
dispatched: false;
|
|
72
|
+
reason: string;
|
|
73
|
+
} | {
|
|
74
|
+
dispatched: true;
|
|
75
|
+
taskId: string;
|
|
76
|
+
workerId: string;
|
|
77
|
+
status: 'completed' | 'failed';
|
|
78
|
+
outcome: RunOutcome;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* One dispatch cycle: the oldest pending hive task goes to an idle hive worker,
|
|
82
|
+
* runs, and its outcome is recorded on the task. Every state change goes
|
|
83
|
+
* through the MCP task tools, so the store is the same one `hive-mind status`
|
|
84
|
+
* and `task status` read.
|
|
85
|
+
*/
|
|
86
|
+
export declare function workOnce(opts: WorkOnceOptions): Promise<WorkOnceResult>;
|
|
87
|
+
export declare const workCommand: Command;
|
|
88
|
+
//# sourceMappingURL=hive-mind-worker.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hive-mind worker execution: the MCP config handed to a spawned Claude, and
|
|
3
|
+
* `hive-mind work`, which actually runs a queued task.
|
|
4
|
+
*
|
|
5
|
+
* Before this, `hive-mind task` only wrote a `pending` row to the task store.
|
|
6
|
+
* Workers are records, not processes, and nothing ever claimed the row: the
|
|
7
|
+
* task stayed pending and the worker stayed idle forever. `work` is the missing
|
|
8
|
+
* consumer — claim one task, assign it to an idle hive worker, run it through
|
|
9
|
+
* `claude -p`, and record the outcome.
|
|
10
|
+
*/
|
|
11
|
+
import { output } from '../output.js';
|
|
12
|
+
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
13
|
+
import { spawn as childSpawn } from 'child_process';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
|
|
17
|
+
/** The server key the hive-mind prompt addresses (`mcp__ruflo__*`). */
|
|
18
|
+
const RUFLO_SERVER = 'ruflo';
|
|
19
|
+
function readServers(path, cwd) {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return null;
|
|
23
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
24
|
+
const isMap = (v) => !!v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0;
|
|
25
|
+
if (isMap(parsed.mcpServers))
|
|
26
|
+
return parsed.mcpServers;
|
|
27
|
+
// ~/.claude.json keeps project-scoped servers under projects[<cwd>].
|
|
28
|
+
const project = parsed.projects?.[cwd];
|
|
29
|
+
if (isMap(project?.mcpServers))
|
|
30
|
+
return project.mcpServers;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Unreadable or not JSON: not a usable config.
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The MCP config file to pass to a spawned Claude.
|
|
39
|
+
*
|
|
40
|
+
* A source file is never passed through as-is. `~/.claude.json` is Claude
|
|
41
|
+
* Code's whole state file; on a machine where it has no top-level
|
|
42
|
+
* `mcpServers` (a fresh desktop), Claude exits at startup with "Invalid MCP
|
|
43
|
+
* configuration: mcpServers: Invalid input", before any task can run. And a
|
|
44
|
+
* config lacking a `ruflo` server leaves the worker without the tools its
|
|
45
|
+
* prompt names. So the servers are extracted, a `ruflo` server is guaranteed,
|
|
46
|
+
* and the result is written as a config Claude is known to accept.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveWorkerMcpConfig(opts) {
|
|
49
|
+
if (opts.explicit)
|
|
50
|
+
return opts.explicit;
|
|
51
|
+
const candidates = [join(opts.cwd, '.mcp.json')];
|
|
52
|
+
if (opts.home)
|
|
53
|
+
candidates.push(join(opts.home, '.claude.json'), join(opts.home, '.claude', 'mcp.json'));
|
|
54
|
+
let servers = {};
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
const found = readServers(c, opts.cwd);
|
|
57
|
+
if (found) {
|
|
58
|
+
servers = found;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!servers[RUFLO_SERVER]) {
|
|
63
|
+
servers = {
|
|
64
|
+
...servers,
|
|
65
|
+
[RUFLO_SERVER]: {
|
|
66
|
+
command: opts.rufloServer.command,
|
|
67
|
+
args: opts.rufloServer.args,
|
|
68
|
+
// The MCP server keys its stores off this, so the worker's tool calls
|
|
69
|
+
// land in the same task/agent stores as the CLI that dispatched it.
|
|
70
|
+
env: { CLAUDE_FLOW_CWD: opts.cwd },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
75
|
+
const path = join(opts.outDir, `mcp-${opts.swarmId}.json`);
|
|
76
|
+
writeFileSync(path, JSON.stringify({ mcpServers: servers }, null, 2), 'utf-8');
|
|
77
|
+
return path;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The ruflo MCP server as launched by THIS cli, so a worker and the CLI that
|
|
81
|
+
* dispatched it share one version and one store format. `@latest` could
|
|
82
|
+
* resolve to a different release mid-session.
|
|
83
|
+
*/
|
|
84
|
+
export function currentRufloServer() {
|
|
85
|
+
const entry = process.argv[1];
|
|
86
|
+
return entry
|
|
87
|
+
? { command: process.execPath, args: [entry, 'mcp', 'start'] }
|
|
88
|
+
: { command: 'npx', args: ['-y', '@claude-flow/cli@latest', 'mcp', 'start'] };
|
|
89
|
+
}
|
|
90
|
+
/** Run one prompt through `claude -p` and parse its JSON result. */
|
|
91
|
+
export const runWithClaude = (prompt, opts) => new Promise((resolve) => {
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
const launch = resolveClaudeLaunchCommand();
|
|
94
|
+
if (!launch) {
|
|
95
|
+
resolve({ ok: false, output: 'Claude Code CLI not found in PATH', exitCode: null, durationMs: 0 });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// The prompt goes on stdin, not argv: a task description has no length
|
|
99
|
+
// bound and `--mcp-config` is variadic (#1780).
|
|
100
|
+
const child = childSpawn(launch.command, [...launch.argsPrefix, '-p', '--output-format', 'json', `--mcp-config=${opts.mcpConfig}`], { cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
101
|
+
let stdout = '';
|
|
102
|
+
let stderr = '';
|
|
103
|
+
child.stdout.on('data', (d) => (stdout += d));
|
|
104
|
+
child.stderr.on('data', (d) => (stderr += d));
|
|
105
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), opts.timeoutMs);
|
|
106
|
+
child.on('error', (e) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve({ ok: false, output: e.message, exitCode: null, durationMs: Date.now() - started });
|
|
109
|
+
});
|
|
110
|
+
child.on('close', (code, signal) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
const durationMs = Date.now() - started;
|
|
113
|
+
try {
|
|
114
|
+
const r = JSON.parse(stdout);
|
|
115
|
+
resolve({
|
|
116
|
+
ok: code === 0 && !r.is_error,
|
|
117
|
+
output: r.result ?? '',
|
|
118
|
+
exitCode: code,
|
|
119
|
+
sessionId: r.session_id,
|
|
120
|
+
costUsd: r.total_cost_usd,
|
|
121
|
+
durationMs,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
const why = signal === 'SIGTERM' ? `timed out after ${opts.timeoutMs}ms` : (stderr || stdout).trim().slice(-2000);
|
|
126
|
+
resolve({ ok: false, output: why, exitCode: code, durationMs });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
child.stdin.end(prompt);
|
|
130
|
+
});
|
|
131
|
+
function timeoutFromTags(tags, fallback) {
|
|
132
|
+
const tag = tags?.find((t) => /^timeout:\d+s$/.test(t));
|
|
133
|
+
return tag ? Number(tag.slice('timeout:'.length, -1)) * 1000 : fallback;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* One dispatch cycle: the oldest pending hive task goes to an idle hive worker,
|
|
137
|
+
* runs, and its outcome is recorded on the task. Every state change goes
|
|
138
|
+
* through the MCP task tools, so the store is the same one `hive-mind status`
|
|
139
|
+
* and `task status` read.
|
|
140
|
+
*/
|
|
141
|
+
export async function workOnce(opts) {
|
|
142
|
+
let taskId = opts.taskId;
|
|
143
|
+
if (!taskId) {
|
|
144
|
+
const listed = await callMCPTool('task_list', { status: 'pending', type: 'hive-mind', limit: 1000 });
|
|
145
|
+
const oldest = [...(listed.tasks ?? [])].sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
146
|
+
if (!oldest)
|
|
147
|
+
return { dispatched: false, reason: 'No pending hive-mind tasks' };
|
|
148
|
+
taskId = oldest.taskId;
|
|
149
|
+
}
|
|
150
|
+
const task = await callMCPTool('task_status', { taskId });
|
|
151
|
+
if (task.status !== 'pending') {
|
|
152
|
+
return { dispatched: false, reason: `Task ${taskId} is ${task.status}, not pending` };
|
|
153
|
+
}
|
|
154
|
+
let workerId = opts.workerId;
|
|
155
|
+
if (!workerId) {
|
|
156
|
+
const hive = await callMCPTool('hive-mind_status', {});
|
|
157
|
+
workerId = hive.workers?.find((w) => w.status === 'idle')?.id;
|
|
158
|
+
if (!workerId)
|
|
159
|
+
return { dispatched: false, reason: 'No idle hive worker — run "hive-mind spawn" first' };
|
|
160
|
+
}
|
|
161
|
+
await callMCPTool('task_assign', { taskId, agentIds: [workerId] });
|
|
162
|
+
const prompt = `You are hive-mind worker ${workerId}. Complete this task, then reply with ` +
|
|
163
|
+
`the result only.\n\nTask ${taskId}:\n${task.description}`;
|
|
164
|
+
const outcome = await opts.runner(prompt, {
|
|
165
|
+
mcpConfig: opts.mcpConfig,
|
|
166
|
+
timeoutMs: timeoutFromTags(task.tags, opts.defaultTimeoutMs ?? 300_000),
|
|
167
|
+
cwd: opts.cwd,
|
|
168
|
+
});
|
|
169
|
+
const result = {
|
|
170
|
+
worker: workerId,
|
|
171
|
+
output: outcome.output,
|
|
172
|
+
exitCode: outcome.exitCode,
|
|
173
|
+
durationMs: outcome.durationMs,
|
|
174
|
+
...(outcome.sessionId ? { sessionId: outcome.sessionId } : {}),
|
|
175
|
+
...(outcome.costUsd !== undefined ? { costUsd: outcome.costUsd } : {}),
|
|
176
|
+
};
|
|
177
|
+
if (outcome.ok) {
|
|
178
|
+
await callMCPTool('task_complete', { taskId, result });
|
|
179
|
+
return { dispatched: true, taskId, workerId, status: 'completed', outcome };
|
|
180
|
+
}
|
|
181
|
+
await callMCPTool('task_update', { taskId, status: 'failed', result });
|
|
182
|
+
return { dispatched: true, taskId, workerId, status: 'failed', outcome };
|
|
183
|
+
}
|
|
184
|
+
export const workCommand = {
|
|
185
|
+
name: 'work',
|
|
186
|
+
description: 'Run the next pending hive task on an idle worker via Claude Code and record the result',
|
|
187
|
+
options: [
|
|
188
|
+
{ name: 'task', description: 'Run this task id instead of the oldest pending one', type: 'string' },
|
|
189
|
+
{ name: 'worker', description: 'Assign to this worker id instead of the first idle one', type: 'string' },
|
|
190
|
+
{ name: 'mcp-config', description: 'MCP config for the worker (default: generated, with the ruflo server)', type: 'string' },
|
|
191
|
+
],
|
|
192
|
+
examples: [
|
|
193
|
+
{ command: 'claude-flow hive-mind work', description: 'Run the oldest pending hive task' },
|
|
194
|
+
{ command: 'claude-flow hive-mind work --task task-123', description: 'Run a specific task' },
|
|
195
|
+
],
|
|
196
|
+
action: async (ctx) => {
|
|
197
|
+
const cwd = ctx.cwd || process.cwd();
|
|
198
|
+
try {
|
|
199
|
+
const mcpConfig = resolveWorkerMcpConfig({
|
|
200
|
+
explicit: ctx.flags.mcpConfig,
|
|
201
|
+
cwd,
|
|
202
|
+
home: process.env.HOME || process.env.USERPROFILE || '',
|
|
203
|
+
outDir: join(cwd, '.hive-mind', 'sessions'),
|
|
204
|
+
swarmId: 'worker',
|
|
205
|
+
rufloServer: currentRufloServer(),
|
|
206
|
+
});
|
|
207
|
+
const r = await workOnce({
|
|
208
|
+
cwd,
|
|
209
|
+
mcpConfig,
|
|
210
|
+
runner: runWithClaude,
|
|
211
|
+
taskId: ctx.flags.task,
|
|
212
|
+
workerId: ctx.flags.worker,
|
|
213
|
+
});
|
|
214
|
+
if (ctx.flags.format === 'json') {
|
|
215
|
+
output.printJson(r);
|
|
216
|
+
}
|
|
217
|
+
else if (!r.dispatched) {
|
|
218
|
+
output.printInfo(r.reason);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
output.printBox([
|
|
222
|
+
`Task ID: ${r.taskId}`,
|
|
223
|
+
`Worker: ${r.workerId}`,
|
|
224
|
+
`Status: ${r.status}`,
|
|
225
|
+
`Duration: ${r.outcome.durationMs}ms`,
|
|
226
|
+
'',
|
|
227
|
+
r.outcome.output.slice(0, 2000),
|
|
228
|
+
].join('\n'), r.status === 'completed' ? 'Task Completed' : 'Task Failed');
|
|
229
|
+
output.writeln(output.dim(` Full record: claude-flow task status ${r.taskId}`));
|
|
230
|
+
}
|
|
231
|
+
// Nothing to do is not an error; a task that ran and failed is.
|
|
232
|
+
const failed = r.dispatched && r.status === 'failed';
|
|
233
|
+
return { success: !failed, exitCode: failed ? 1 : 0, data: r };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const msg = error instanceof MCPClientError ? error.message : String(error);
|
|
237
|
+
output.printError(`Work error: ${msg}`);
|
|
238
|
+
return { success: false, exitCode: 1 };
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=hive-mind-worker.js.map
|
|
@@ -10,7 +10,7 @@ import { select, confirm, input } from '../prompt.js';
|
|
|
10
10
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
11
11
|
import { spawn as childSpawn } from 'child_process';
|
|
12
12
|
import { mkdir, writeFile } from 'fs/promises';
|
|
13
|
-
import {
|
|
13
|
+
import { resolveWorkerMcpConfig, currentRufloServer, workCommand } from './hive-mind-worker.js';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
|
|
16
16
|
import { getHiveTokenForCli } from '../mcp-tools/hive-mind-tools.js';
|
|
@@ -202,44 +202,25 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
202
202
|
// Build arguments - flags first, then prompt
|
|
203
203
|
const claudeArgs = [];
|
|
204
204
|
// #1748 Issue 2 — pass --mcp-config so the spawned worker actually has
|
|
205
|
-
// mcp__ruflo__* tools registered.
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
mcpConfigPath = c;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
catch { /* continue */ }
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (mcpConfigPath) {
|
|
232
|
-
// #1780 — Claude Code's `--mcp-config` is variadic; passing it as two
|
|
233
|
-
// argv tokens (`--mcp-config`, `<path>`) lets a later positional (the
|
|
234
|
-
// hive-mind prompt) be slurped as a second config file, producing
|
|
235
|
-
// `ENAMETOOLONG: name too long, open` once the prompt exceeds PATH_MAX.
|
|
236
|
-
// Use `=`-syntax so the flag stays attached to its single value.
|
|
237
|
-
claudeArgs.push(`--mcp-config=${mcpConfigPath}`);
|
|
238
|
-
output.printInfo(`Spawned worker MCP config: ${mcpConfigPath}`);
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
output.printWarning('No .mcp.json or ~/.claude.json found — spawned worker will not have mcp__ruflo__* tools (#1748 Issue 2). Pass --mcp-config <path> or run "ruflo init" to generate one.');
|
|
242
|
-
}
|
|
205
|
+
// mcp__ruflo__* tools registered. The config is always a generated file
|
|
206
|
+
// that Claude accepts; see resolveWorkerMcpConfig for why a source
|
|
207
|
+
// config (notably ~/.claude.json) is never passed through as-is.
|
|
208
|
+
// The parser stores flags camelCased (#2269), so read both forms.
|
|
209
|
+
const mcpConfigPath = resolveWorkerMcpConfig({
|
|
210
|
+
explicit: (flags['mcp-config'] ?? flags.mcpConfig),
|
|
211
|
+
cwd: process.cwd(),
|
|
212
|
+
home: process.env.HOME || process.env.USERPROFILE || '',
|
|
213
|
+
outDir: sessionsDir,
|
|
214
|
+
swarmId,
|
|
215
|
+
rufloServer: currentRufloServer(),
|
|
216
|
+
});
|
|
217
|
+
// #1780 — Claude Code's `--mcp-config` is variadic; passing it as two
|
|
218
|
+
// argv tokens (`--mcp-config`, `<path>`) lets a later positional (the
|
|
219
|
+
// hive-mind prompt) be slurped as a second config file, producing
|
|
220
|
+
// `ENAMETOOLONG: name too long, open` once the prompt exceeds PATH_MAX.
|
|
221
|
+
// Use `=`-syntax so the flag stays attached to its single value.
|
|
222
|
+
claudeArgs.push(`--mcp-config=${mcpConfigPath}`);
|
|
223
|
+
output.printInfo(`Spawned worker MCP config: ${mcpConfigPath}`);
|
|
243
224
|
// Check for non-interactive mode
|
|
244
225
|
const isNonInteractive = flags['non-interactive'] || flags.nonInteractive;
|
|
245
226
|
if (isNonInteractive) {
|
|
@@ -882,7 +863,8 @@ const taskCommand = {
|
|
|
882
863
|
].join('\n'), 'Task Submitted');
|
|
883
864
|
output.writeln();
|
|
884
865
|
output.printSuccess('Task submitted to hive');
|
|
885
|
-
output.writeln(output.dim(`
|
|
866
|
+
output.writeln(output.dim(` Run it with: claude-flow hive-mind work --task ${result.taskId}`));
|
|
867
|
+
output.writeln(output.dim(` Track with: claude-flow task status ${result.taskId}`));
|
|
886
868
|
return { success: true, data: result };
|
|
887
869
|
}
|
|
888
870
|
catch (error) {
|
|
@@ -1210,7 +1192,7 @@ export const hiveMindCommand = {
|
|
|
1210
1192
|
name: 'hive-mind',
|
|
1211
1193
|
aliases: ['hive'],
|
|
1212
1194
|
description: 'Queen-led consensus-based multi-agent coordination',
|
|
1213
|
-
subcommands: [initCommand, spawnCommand, statusCommand, taskCommand, joinCommand, leaveCommand, consensusCommand, broadcastCommand, memorySubCommand, optimizeMemoryCommand, shutdownCommand],
|
|
1195
|
+
subcommands: [initCommand, spawnCommand, statusCommand, taskCommand, workCommand, joinCommand, leaveCommand, consensusCommand, broadcastCommand, memorySubCommand, optimizeMemoryCommand, shutdownCommand],
|
|
1214
1196
|
options: [],
|
|
1215
1197
|
examples: [
|
|
1216
1198
|
{ command: 'claude-flow hive-mind init -t hierarchical-mesh', description: 'Initialize hive' },
|
|
@@ -1230,6 +1212,7 @@ export const hiveMindCommand = {
|
|
|
1230
1212
|
`${output.highlight('spawn')} - Spawn worker agents (use --claude to launch Claude Code)`,
|
|
1231
1213
|
`${output.highlight('status')} - Show hive status`,
|
|
1232
1214
|
`${output.highlight('task')} - Submit task to hive`,
|
|
1215
|
+
`${output.highlight('work')} - Run the next pending task on an idle worker`,
|
|
1233
1216
|
`${output.highlight('join')} - Join an agent to the hive`,
|
|
1234
1217
|
`${output.highlight('leave')} - Remove an agent from the hive`,
|
|
1235
1218
|
`${output.highlight('consensus')} - Manage consensus proposals`,
|
|
@@ -40,6 +40,53 @@ function saveTaskStore(store) {
|
|
|
40
40
|
ensureTaskDir();
|
|
41
41
|
writeFileSync(getTaskPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Apply an agent-state change to every store that holds the agent.
|
|
45
|
+
*
|
|
46
|
+
* `hive-mind_spawn` keeps its workers in `.claude-flow/agents.json`, not the
|
|
47
|
+
* canonical `.claude-flow/agents/store.json` (#1916). Syncing only the
|
|
48
|
+
* canonical store meant assigning a task to a hive worker never marked it
|
|
49
|
+
* busy, and completing one never counted it: `hive-mind status` showed the
|
|
50
|
+
* worker idle with 0 completed while its task sat in the queue.
|
|
51
|
+
*/
|
|
52
|
+
function updateAgents(agentIds, mutate) {
|
|
53
|
+
if (agentIds.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const paths = [
|
|
56
|
+
join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json'),
|
|
57
|
+
join(getProjectCwd(), STORAGE_DIR, 'agents.json'),
|
|
58
|
+
];
|
|
59
|
+
for (const path of paths) {
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(path))
|
|
62
|
+
continue;
|
|
63
|
+
const store = JSON.parse(readFileSync(path, 'utf-8'));
|
|
64
|
+
if (!store.agents)
|
|
65
|
+
continue;
|
|
66
|
+
let touched = false;
|
|
67
|
+
for (const id of agentIds) {
|
|
68
|
+
if (store.agents[id]) {
|
|
69
|
+
mutate(store.agents[id]);
|
|
70
|
+
touched = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (touched)
|
|
74
|
+
writeFileSync(path, JSON.stringify(store, null, 2), 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Best-effort agent sync: a corrupt agent store must not fail the task op.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Return agents still holding `taskId` to idle. */
|
|
82
|
+
function releaseAgents(agentIds, taskId) {
|
|
83
|
+
updateAgents(agentIds, (agent) => {
|
|
84
|
+
if (agent.currentTask === taskId) {
|
|
85
|
+
agent.status = 'idle';
|
|
86
|
+
agent.currentTask = null;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
43
90
|
export const taskTools = [
|
|
44
91
|
{
|
|
45
92
|
name: 'task_create',
|
|
@@ -214,33 +261,22 @@ export const taskTools = [
|
|
|
214
261
|
const taskId = input.taskId;
|
|
215
262
|
const task = store.tasks[taskId];
|
|
216
263
|
if (task) {
|
|
264
|
+
if (task.status === 'completed') {
|
|
265
|
+
return { taskId: task.taskId, status: task.status, completedAt: task.completedAt, result: task.result };
|
|
266
|
+
}
|
|
217
267
|
task.status = 'completed';
|
|
218
268
|
task.progress = 100;
|
|
219
269
|
task.completedAt = new Date().toISOString();
|
|
220
270
|
task.result = input.result || {};
|
|
221
271
|
saveTaskStore(store);
|
|
222
272
|
// Sync assigned agents back to idle and increment taskCount
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (existsSync(agentStorePath)) {
|
|
228
|
-
agentStore = JSON.parse(readFileSync(agentStorePath, 'utf-8'));
|
|
229
|
-
}
|
|
230
|
-
for (const agentId of task.assignedTo) {
|
|
231
|
-
if (agentStore.agents[agentId]) {
|
|
232
|
-
agentStore.agents[agentId].status = 'idle';
|
|
233
|
-
agentStore.agents[agentId].currentTask = null;
|
|
234
|
-
agentStore.agents[agentId].taskCount =
|
|
235
|
-
(agentStore.agents[agentId].taskCount || 0) + 1;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
writeFileSync(agentStorePath, JSON.stringify(agentStore, null, 2), 'utf-8');
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
// Best-effort agent sync
|
|
273
|
+
updateAgents(task.assignedTo, (agent) => {
|
|
274
|
+
if (agent.currentTask === taskId) {
|
|
275
|
+
agent.status = 'idle';
|
|
276
|
+
agent.currentTask = null;
|
|
242
277
|
}
|
|
243
|
-
|
|
278
|
+
agent.taskCount = (agent.taskCount || 0) + 1;
|
|
279
|
+
});
|
|
244
280
|
return {
|
|
245
281
|
taskId: task.taskId,
|
|
246
282
|
status: task.status,
|
|
@@ -266,6 +302,7 @@ export const taskTools = [
|
|
|
266
302
|
status: { type: 'string', description: 'New status' },
|
|
267
303
|
progress: { type: 'number', description: 'Progress percentage (0-100)' },
|
|
268
304
|
assignTo: { type: 'array', items: { type: 'string' }, description: 'Agent IDs to assign' },
|
|
305
|
+
result: { type: 'object', description: 'Result data (e.g. the failure of a failed task)' },
|
|
269
306
|
},
|
|
270
307
|
required: ['taskId'],
|
|
271
308
|
},
|
|
@@ -284,6 +321,12 @@ export const taskTools = [
|
|
|
284
321
|
if (newStatus === 'in_progress' && !task.startedAt) {
|
|
285
322
|
task.startedAt = new Date().toISOString();
|
|
286
323
|
}
|
|
324
|
+
// A failed task frees its workers, as task_complete and task_cancel do;
|
|
325
|
+
// otherwise a worker whose run failed stays `busy` forever.
|
|
326
|
+
if (newStatus === 'failed') {
|
|
327
|
+
task.completedAt = new Date().toISOString();
|
|
328
|
+
releaseAgents(task.assignedTo, taskId);
|
|
329
|
+
}
|
|
287
330
|
}
|
|
288
331
|
if (typeof input.progress === 'number') {
|
|
289
332
|
task.progress = Math.min(100, Math.max(0, input.progress));
|
|
@@ -291,6 +334,9 @@ export const taskTools = [
|
|
|
291
334
|
if (input.assignTo) {
|
|
292
335
|
task.assignedTo = input.assignTo;
|
|
293
336
|
}
|
|
337
|
+
if (input.result && typeof input.result === 'object') {
|
|
338
|
+
task.result = input.result;
|
|
339
|
+
}
|
|
294
340
|
saveTaskStore(store);
|
|
295
341
|
return {
|
|
296
342
|
success: true,
|
|
@@ -332,41 +378,20 @@ export const taskTools = [
|
|
|
332
378
|
return { taskId, error: 'Task not found' };
|
|
333
379
|
}
|
|
334
380
|
const previouslyAssigned = [...task.assignedTo];
|
|
335
|
-
// Load agent store to sync worker state
|
|
336
|
-
const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json');
|
|
337
|
-
let agentStore = { agents: {} };
|
|
338
|
-
try {
|
|
339
|
-
if (existsSync(agentStorePath)) {
|
|
340
|
-
agentStore = JSON.parse(readFileSync(agentStorePath, 'utf-8'));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
catch { /* ignore */ }
|
|
344
381
|
if (input.unassign) {
|
|
345
382
|
// Revert previously assigned agents to idle
|
|
346
|
-
|
|
347
|
-
if (agentStore.agents[agentId]) {
|
|
348
|
-
agentStore.agents[agentId].status = 'idle';
|
|
349
|
-
agentStore.agents[agentId].currentTask = null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
383
|
+
releaseAgents(previouslyAssigned, taskId);
|
|
352
384
|
task.assignedTo = [];
|
|
353
385
|
}
|
|
354
386
|
else {
|
|
355
387
|
const agentIds = input.agentIds || [];
|
|
356
388
|
// Revert old agents to idle
|
|
357
|
-
|
|
358
|
-
if (!agentIds.includes(agentId) && agentStore.agents[agentId]) {
|
|
359
|
-
agentStore.agents[agentId].status = 'idle';
|
|
360
|
-
agentStore.agents[agentId].currentTask = null;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
389
|
+
releaseAgents(previouslyAssigned.filter((id) => !agentIds.includes(id)), taskId);
|
|
363
390
|
// Set new agents to active
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
}
|
|
391
|
+
updateAgents(agentIds, (agent) => {
|
|
392
|
+
agent.status = 'busy';
|
|
393
|
+
agent.currentTask = taskId;
|
|
394
|
+
});
|
|
370
395
|
task.assignedTo = agentIds;
|
|
371
396
|
// Auto-transition task to in_progress if pending
|
|
372
397
|
if (task.status === 'pending' && agentIds.length > 0) {
|
|
@@ -377,12 +402,6 @@ export const taskTools = [
|
|
|
377
402
|
}
|
|
378
403
|
}
|
|
379
404
|
saveTaskStore(store);
|
|
380
|
-
// Save agent store
|
|
381
|
-
const agentDir = join(getProjectCwd(), STORAGE_DIR, 'agents');
|
|
382
|
-
if (!existsSync(agentDir)) {
|
|
383
|
-
mkdirSync(agentDir, { recursive: true });
|
|
384
|
-
}
|
|
385
|
-
writeFileSync(agentStorePath, JSON.stringify(agentStore, null, 2), 'utf-8');
|
|
386
405
|
return {
|
|
387
406
|
taskId: task.taskId,
|
|
388
407
|
assignedTo: task.assignedTo,
|
|
@@ -421,6 +440,7 @@ export const taskTools = [
|
|
|
421
440
|
task.completedAt = new Date().toISOString();
|
|
422
441
|
task.result = { cancelReason: input.reason || 'Cancelled by user' };
|
|
423
442
|
saveTaskStore(store);
|
|
443
|
+
releaseAgents(task.assignedTo, taskId);
|
|
424
444
|
return {
|
|
425
445
|
success: true,
|
|
426
446
|
taskId: task.taskId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.47.
|
|
3
|
+
"version": "3.47.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|