claude-flow 3.6.21 → 3.6.23

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.
@@ -1 +1 @@
1
- {"sessionId":"36428a63-dfb2-42a4-a159-cf8be916193e","pid":86429,"procStart":"Sun May 3 19:17:47 2026","acquiredAt":1777840775529}
1
+ {"sessionId":"36428a63-dfb2-42a4-a159-cf8be916193e","pid":71027,"procStart":"Sun May 3 23:00:23 2026","acquiredAt":1777849234814}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.6.21",
3
+ "version": "3.6.23",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared agent-execution core.
3
+ *
4
+ * Both the agent_execute MCP tool and the workflow runtime (G3) need
5
+ * to dispatch a prompt to an agent's configured Anthropic model. This
6
+ * module factors that path out so it's testable and reusable, and
7
+ * keeps the wire from agent_spawn → ProviderManager (real) in one
8
+ * place rather than duplicated.
9
+ */
10
+ type ClaudeModel = 'haiku' | 'sonnet' | 'opus' | 'inherit';
11
+ export interface AgentRecord {
12
+ agentId: string;
13
+ agentType: string;
14
+ status: 'idle' | 'busy' | 'terminated';
15
+ health: number;
16
+ taskCount: number;
17
+ config: Record<string, unknown>;
18
+ createdAt: string;
19
+ domain?: string;
20
+ model?: ClaudeModel;
21
+ modelRoutedBy?: 'explicit' | 'router' | 'agent-booster' | 'default';
22
+ lastResult?: Record<string, unknown>;
23
+ }
24
+ export interface AnthropicCallInput {
25
+ prompt: string;
26
+ systemPrompt?: string;
27
+ model?: string;
28
+ maxTokens?: number;
29
+ temperature?: number;
30
+ timeoutMs?: number;
31
+ }
32
+ export interface AnthropicCallResult {
33
+ success: boolean;
34
+ model?: string;
35
+ messageId?: string;
36
+ stopReason?: string;
37
+ output?: string;
38
+ usage?: {
39
+ inputTokens: number;
40
+ outputTokens: number;
41
+ totalTokens: number;
42
+ };
43
+ durationMs?: number;
44
+ error?: string;
45
+ }
46
+ /**
47
+ * Generic Anthropic Messages API call. No agent registry coupling — used
48
+ * by agent_execute (with the agent's configured model) and by the WASM
49
+ * agent runtime (G4) when the bundled WASM only echoes input.
50
+ */
51
+ export declare function callAnthropicMessages(input: AnthropicCallInput): Promise<AnthropicCallResult>;
52
+ /**
53
+ * Resolve a model identifier to an Anthropic model ID. Accepts:
54
+ * - logical names: 'haiku', 'sonnet', 'opus', 'inherit'
55
+ * - prefixed: 'anthropic:claude-3-5-sonnet-latest'
56
+ * - direct: 'claude-3-5-sonnet-latest'
57
+ */
58
+ export declare function resolveAnthropicModel(input: string | undefined): string;
59
+ export interface AgentExecuteInput {
60
+ agentId: string;
61
+ prompt: string;
62
+ systemPrompt?: string;
63
+ maxTokens?: number;
64
+ temperature?: number;
65
+ timeoutMs?: number;
66
+ }
67
+ export interface AgentExecuteResult {
68
+ success: boolean;
69
+ agentId: string;
70
+ model?: string;
71
+ messageId?: string;
72
+ stopReason?: string;
73
+ output?: string;
74
+ usage?: {
75
+ inputTokens: number;
76
+ outputTokens: number;
77
+ totalTokens: number;
78
+ };
79
+ durationMs?: number;
80
+ error?: string;
81
+ remediation?: string;
82
+ }
83
+ export declare function executeAgentTask(input: AgentExecuteInput): Promise<AgentExecuteResult>;
84
+ export {};
85
+ //# sourceMappingURL=agent-execute-core.d.ts.map
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Shared agent-execution core.
3
+ *
4
+ * Both the agent_execute MCP tool and the workflow runtime (G3) need
5
+ * to dispatch a prompt to an agent's configured Anthropic model. This
6
+ * module factors that path out so it's testable and reusable, and
7
+ * keeps the wire from agent_spawn → ProviderManager (real) in one
8
+ * place rather than duplicated.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { getProjectCwd } from './types.js';
13
+ const STORAGE_DIR = '.claude-flow';
14
+ const AGENT_DIR = 'agents';
15
+ const AGENT_FILE = 'store.json';
16
+ function getAgentDir() { return join(getProjectCwd(), STORAGE_DIR, AGENT_DIR); }
17
+ function getAgentPath() { return join(getAgentDir(), AGENT_FILE); }
18
+ function ensureAgentDir() {
19
+ const dir = getAgentDir();
20
+ if (!existsSync(dir))
21
+ mkdirSync(dir, { recursive: true });
22
+ }
23
+ function loadAgentStore() {
24
+ try {
25
+ if (existsSync(getAgentPath()))
26
+ return JSON.parse(readFileSync(getAgentPath(), 'utf-8'));
27
+ }
28
+ catch { /* fall through */ }
29
+ return { agents: {}, version: '3.0.0' };
30
+ }
31
+ function saveAgentStore(store) {
32
+ ensureAgentDir();
33
+ writeFileSync(getAgentPath(), JSON.stringify(store, null, 2), 'utf-8');
34
+ }
35
+ const MODEL_MAP = {
36
+ haiku: 'claude-3-5-haiku-latest',
37
+ sonnet: 'claude-3-5-sonnet-latest',
38
+ opus: 'claude-3-opus-latest',
39
+ inherit: 'claude-3-5-sonnet-latest',
40
+ };
41
+ /**
42
+ * Generic Anthropic Messages API call. No agent registry coupling — used
43
+ * by agent_execute (with the agent's configured model) and by the WASM
44
+ * agent runtime (G4) when the bundled WASM only echoes input.
45
+ */
46
+ export async function callAnthropicMessages(input) {
47
+ const apiKey = process.env.ANTHROPIC_API_KEY;
48
+ if (!apiKey) {
49
+ return { success: false, error: 'ANTHROPIC_API_KEY not set in environment' };
50
+ }
51
+ const model = input.model || 'claude-3-5-sonnet-latest';
52
+ const startedAt = Date.now();
53
+ try {
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), input.timeoutMs || 60000);
56
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
57
+ method: 'POST',
58
+ headers: {
59
+ 'x-api-key': apiKey,
60
+ 'anthropic-version': '2023-06-01',
61
+ 'content-type': 'application/json',
62
+ },
63
+ body: JSON.stringify({
64
+ model,
65
+ max_tokens: input.maxTokens || 1024,
66
+ temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
67
+ ...(input.systemPrompt ? { system: input.systemPrompt } : {}),
68
+ messages: [{ role: 'user', content: input.prompt }],
69
+ }),
70
+ signal: controller.signal,
71
+ });
72
+ clearTimeout(timer);
73
+ if (!res.ok) {
74
+ const errText = await res.text().catch(() => '<unreadable error body>');
75
+ return { success: false, model, error: `Anthropic API error ${res.status}: ${errText.slice(0, 400)}` };
76
+ }
77
+ const data = await res.json();
78
+ const textOut = data.content
79
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
80
+ .map(c => c.text)
81
+ .join('');
82
+ return {
83
+ success: true,
84
+ model: data.model,
85
+ messageId: data.id,
86
+ stopReason: data.stop_reason,
87
+ output: textOut,
88
+ usage: {
89
+ inputTokens: data.usage.input_tokens,
90
+ outputTokens: data.usage.output_tokens,
91
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens,
92
+ },
93
+ durationMs: Date.now() - startedAt,
94
+ };
95
+ }
96
+ catch (err) {
97
+ return {
98
+ success: false,
99
+ model,
100
+ error: err instanceof Error ? err.message : String(err),
101
+ durationMs: Date.now() - startedAt,
102
+ };
103
+ }
104
+ }
105
+ /**
106
+ * Resolve a model identifier to an Anthropic model ID. Accepts:
107
+ * - logical names: 'haiku', 'sonnet', 'opus', 'inherit'
108
+ * - prefixed: 'anthropic:claude-3-5-sonnet-latest'
109
+ * - direct: 'claude-3-5-sonnet-latest'
110
+ */
111
+ export function resolveAnthropicModel(input) {
112
+ if (!input)
113
+ return 'claude-3-5-sonnet-latest';
114
+ if (input in MODEL_MAP)
115
+ return MODEL_MAP[input];
116
+ if (input.startsWith('anthropic:'))
117
+ return input.slice('anthropic:'.length);
118
+ return input;
119
+ }
120
+ export async function executeAgentTask(input) {
121
+ const apiKey = process.env.ANTHROPIC_API_KEY;
122
+ if (!apiKey) {
123
+ return {
124
+ success: false,
125
+ agentId: input.agentId,
126
+ error: 'ANTHROPIC_API_KEY not set in environment',
127
+ remediation: 'Set the env var and re-run. The key is read at call time.',
128
+ };
129
+ }
130
+ const store = loadAgentStore();
131
+ const agent = store.agents[input.agentId];
132
+ if (!agent)
133
+ return { success: false, agentId: input.agentId, error: 'Agent not found' };
134
+ if (agent.status === 'terminated')
135
+ return { success: false, agentId: input.agentId, error: 'Agent has been terminated' };
136
+ const anthropicModel = MODEL_MAP[agent.model || 'sonnet'] || 'claude-3-5-sonnet-latest';
137
+ const systemPrompt = input.systemPrompt ||
138
+ `You are a ${agent.agentType} agent operating as part of a Ruflo swarm. ` +
139
+ `Agent ID: ${input.agentId}. Domain: ${agent.domain ?? 'general'}. ` +
140
+ `Respond directly and stay focused on the task. If you need information you don't have, state that explicitly.`;
141
+ agent.status = 'busy';
142
+ agent.taskCount = (agent.taskCount || 0) + 1;
143
+ saveAgentStore(store);
144
+ const startedAt = Date.now();
145
+ try {
146
+ const controller = new AbortController();
147
+ const timeoutMs = input.timeoutMs || 60000;
148
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
149
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
150
+ method: 'POST',
151
+ headers: {
152
+ 'x-api-key': apiKey,
153
+ 'anthropic-version': '2023-06-01',
154
+ 'content-type': 'application/json',
155
+ },
156
+ body: JSON.stringify({
157
+ model: anthropicModel,
158
+ max_tokens: input.maxTokens || 1024,
159
+ temperature: typeof input.temperature === 'number' ? input.temperature : 0.7,
160
+ system: systemPrompt,
161
+ messages: [{ role: 'user', content: input.prompt }],
162
+ }),
163
+ signal: controller.signal,
164
+ });
165
+ clearTimeout(timer);
166
+ if (!res.ok) {
167
+ const errText = await res.text().catch(() => '<unreadable error body>');
168
+ agent.status = 'idle';
169
+ saveAgentStore(store);
170
+ return {
171
+ success: false,
172
+ agentId: input.agentId,
173
+ model: anthropicModel,
174
+ error: `Anthropic API error ${res.status}: ${errText.slice(0, 400)}`,
175
+ };
176
+ }
177
+ const data = await res.json();
178
+ const textOut = data.content
179
+ .filter(c => c.type === 'text' && typeof c.text === 'string')
180
+ .map(c => c.text)
181
+ .join('');
182
+ const result = {
183
+ success: true,
184
+ agentId: input.agentId,
185
+ messageId: data.id,
186
+ model: data.model,
187
+ stopReason: data.stop_reason,
188
+ output: textOut,
189
+ usage: {
190
+ inputTokens: data.usage.input_tokens,
191
+ outputTokens: data.usage.output_tokens,
192
+ totalTokens: data.usage.input_tokens + data.usage.output_tokens,
193
+ },
194
+ durationMs: Date.now() - startedAt,
195
+ };
196
+ agent.status = 'idle';
197
+ agent.lastResult = result;
198
+ saveAgentStore(store);
199
+ return result;
200
+ }
201
+ catch (err) {
202
+ agent.status = 'idle';
203
+ saveAgentStore(store);
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ return {
206
+ success: false,
207
+ agentId: input.agentId,
208
+ model: anthropicModel,
209
+ error: `agent_execute failed: ${msg}`,
210
+ durationMs: Date.now() - startedAt,
211
+ };
212
+ }
213
+ }
214
+ //# sourceMappingURL=agent-execute-core.js.map
@@ -7,7 +7,8 @@
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { getProjectCwd } from './types.js';
10
- import { validateIdentifier, validateAgentSpawn } from './validate-input.js';
10
+ import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
11
+ import { executeAgentTask } from './agent-execute-core.js';
11
12
  // Storage paths
12
13
  const STORAGE_DIR = '.claude-flow';
13
14
  const AGENT_DIR = 'agents';
@@ -199,7 +200,10 @@ export const agentTools = [
199
200
  modelRoutedBy: routingResult.routedBy,
200
201
  status: 'registered',
201
202
  createdAt: agent.createdAt,
202
- note: 'Agent registered for coordination. Use Claude Code Task tool or claude -p to execute work.',
203
+ note: 'Agent registered for coordination. Three execution paths: ' +
204
+ '(1) call agent_execute(agentId, prompt) — direct LLM call via Anthropic Messages API (requires ANTHROPIC_API_KEY); ' +
205
+ '(2) Claude Code Task tool — spawns a real subagent; ' +
206
+ '(3) claude -p — headless background instance.',
203
207
  };
204
208
  // Add Agent Booster info if task can skip LLM
205
209
  if (routingResult.canSkipLLM) {
@@ -214,6 +218,47 @@ export const agentTools = [
214
218
  return response;
215
219
  },
216
220
  },
221
+ {
222
+ // ADR-095 G1: real LLM execution via the agent registry. Previously
223
+ // agent_spawn registered metadata but nothing dispatched work to a
224
+ // provider — the wire between AnthropicProvider and the agent
225
+ // registry was missing, as the April audit (@roman-rr) called out.
226
+ // agent_execute closes that wire by reading the agent's configured
227
+ // model, calling the Anthropic Messages API directly via fetch, and
228
+ // updating the agent record with lastResult / taskCount / status.
229
+ // No mock — actual HTTP request to api.anthropic.com.
230
+ name: 'agent_execute',
231
+ description: 'Execute a task on a spawned agent — calls the Anthropic Messages API with the agent\'s configured model. Requires ANTHROPIC_API_KEY in env.',
232
+ category: 'agent',
233
+ inputSchema: {
234
+ type: 'object',
235
+ properties: {
236
+ agentId: { type: 'string', description: 'ID of the spawned agent' },
237
+ prompt: { type: 'string', description: 'Task / prompt for the agent to execute' },
238
+ systemPrompt: { type: 'string', description: 'Optional system prompt (overrides agent default)' },
239
+ maxTokens: { type: 'number', description: 'Max output tokens (default 1024)' },
240
+ temperature: { type: 'number', description: 'Sampling temperature 0..1 (default 0.7)' },
241
+ },
242
+ required: ['agentId', 'prompt'],
243
+ },
244
+ handler: async (input) => {
245
+ const vId = validateIdentifier(input.agentId, 'agentId');
246
+ if (!vId.valid)
247
+ return { success: false, error: `Input validation failed: ${vId.error}` };
248
+ const vP = validateText(input.prompt, 'prompt');
249
+ if (!vP.valid)
250
+ return { success: false, error: `Input validation failed: ${vP.error}` };
251
+ // Delegate to the shared core (also used by the workflow runtime).
252
+ return executeAgentTask({
253
+ agentId: input.agentId,
254
+ prompt: input.prompt,
255
+ systemPrompt: input.systemPrompt,
256
+ maxTokens: input.maxTokens,
257
+ temperature: input.temperature,
258
+ timeoutMs: input.timeoutMs,
259
+ });
260
+ },
261
+ },
217
262
  {
218
263
  name: 'agent_terminate',
219
264
  description: 'Terminate an agent',
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { getProjectCwd } from './types.js';
9
9
  import { validateIdentifier, validatePath, validateText } from './validate-input.js';
10
+ import { executeAgentTask } from './agent-execute-core.js';
10
11
  // Storage paths
11
12
  const STORAGE_DIR = '.claude-flow';
12
13
  const WORKFLOW_DIR = 'workflows';
@@ -247,25 +248,156 @@ export const workflowTools = [
247
248
  workflow.status = 'running';
248
249
  workflow.startedAt = new Date().toISOString();
249
250
  workflow.currentStep = input.startFromStep || 0;
250
- // Set steps to pending — actual execution requires agent assignment via task tools
251
- const results = [];
252
- for (let i = workflow.currentStep; i < workflow.steps.length; i++) {
253
- const step = workflow.steps[i];
254
- step.status = 'pending';
255
- results.push({
256
- stepId: step.stepId,
257
- status: step.status,
258
- _note: 'Workflow execution tracks state. Actual step execution requires agent assignment via task tools.',
251
+ saveWorkflowStore(store);
252
+ // ADR-095 G3: real workflow runtime. Walk the steps in order;
253
+ // dispatch each by type. Persist progress after each step so a
254
+ // crash or pause can resume cleanly. No mock — task steps make
255
+ // real LLM calls via agent_execute (G1's wire).
256
+ const stepResults = [];
257
+ // Variable substitution: {{name}} → workflow.variables[name] OR steps[stepId].output
258
+ const interp = (text) => {
259
+ return text.replace(/\{\{\s*([a-zA-Z_][\w.]*)\s*\}\}/g, (_, key) => {
260
+ // Direct variable
261
+ if (key in workflow.variables)
262
+ return String(workflow.variables[key]);
263
+ // Step-output reference: stepId.output
264
+ const dot = key.indexOf('.');
265
+ if (dot > 0) {
266
+ const stepId = key.slice(0, dot);
267
+ const field = key.slice(dot + 1);
268
+ const prior = stepResults.find(s => s.stepId === stepId);
269
+ if (prior && field === 'output' && typeof prior.output === 'string')
270
+ return prior.output;
271
+ }
272
+ return '';
259
273
  });
274
+ };
275
+ const startedAt = Date.now();
276
+ let i = workflow.currentStep;
277
+ while (i < workflow.steps.length) {
278
+ // Honor pause/cancel signals between steps.
279
+ const live = loadWorkflowStore().workflows[workflowId];
280
+ if (!live || live.status === 'paused') {
281
+ workflow.status = 'paused';
282
+ workflow.currentStep = i;
283
+ saveWorkflowStore(store);
284
+ break;
285
+ }
286
+ if (live.status === 'failed') {
287
+ workflow.status = 'failed';
288
+ saveWorkflowStore(store);
289
+ break;
290
+ }
291
+ const step = workflow.steps[i];
292
+ step.status = 'running';
293
+ step.startedAt = new Date().toISOString();
294
+ const stepStart = Date.now();
295
+ saveWorkflowStore(store);
296
+ let stepEntry = { stepId: step.stepId, type: step.type, status: 'running' };
297
+ try {
298
+ if (step.type === 'task') {
299
+ const cfg = step.config;
300
+ const agentId = cfg.agentId || workflow.variables.defaultAgentId;
301
+ const promptTpl = cfg.prompt || step.name;
302
+ if (!agentId)
303
+ throw new Error(`task step ${step.stepId} requires config.agentId or workflow.variables.defaultAgentId`);
304
+ const prompt = interp(promptTpl);
305
+ const result = await executeAgentTask({
306
+ agentId,
307
+ prompt,
308
+ systemPrompt: cfg.systemPrompt ? interp(String(cfg.systemPrompt)) : undefined,
309
+ maxTokens: cfg.maxTokens,
310
+ temperature: cfg.temperature,
311
+ timeoutMs: cfg.timeoutMs,
312
+ });
313
+ if (!result.success)
314
+ throw new Error(result.error || 'agent_execute failed');
315
+ step.result = result;
316
+ workflow.variables[`${step.stepId}.output`] = result.output;
317
+ workflow.variables.lastStepOutput = result.output;
318
+ stepEntry = { stepId: step.stepId, type: 'task', status: 'completed', durationMs: result.durationMs, output: result.output };
319
+ }
320
+ else if (step.type === 'wait') {
321
+ const cfg = step.config;
322
+ const ms = Math.min(Math.max(0, cfg.ms || 0), 60000);
323
+ await new Promise(r => setTimeout(r, ms));
324
+ step.result = { waitedMs: ms };
325
+ stepEntry = { stepId: step.stepId, type: 'wait', status: 'completed', durationMs: ms };
326
+ }
327
+ else if (step.type === 'condition') {
328
+ // Simple condition: config.when is a JS expression evaluated against workflow.variables.
329
+ // For safety, we only support `var === 'value'` or `var === number`.
330
+ const cfg = step.config;
331
+ const expr = String(cfg.when || 'true').trim();
332
+ const m = expr.match(/^([a-zA-Z_][\w]*)\s*===?\s*(['\"])?([^'\"]*)\2?$/);
333
+ let truthy = false;
334
+ if (m) {
335
+ const v = workflow.variables[m[1]];
336
+ const expected = m[2] ? m[3] : Number(m[3]);
337
+ truthy = v === expected;
338
+ }
339
+ else if (expr === 'true')
340
+ truthy = true;
341
+ step.result = { conditionExpr: expr, truthy };
342
+ // condition can declare a target step index to jump to via cfg.thenStep / cfg.elseStep
343
+ if (typeof cfg.thenStep === 'number' && truthy)
344
+ i = cfg.thenStep - 1;
345
+ if (typeof cfg.elseStep === 'number' && !truthy)
346
+ i = cfg.elseStep - 1;
347
+ stepEntry = { stepId: step.stepId, type: 'condition', status: 'completed' };
348
+ }
349
+ else {
350
+ // parallel/loop are deferred — mark skipped honestly rather than mock-completing.
351
+ step.result = { _note: `step type '${step.type}' not yet implemented in runtime` };
352
+ stepEntry = { stepId: step.stepId, type: step.type, status: 'skipped' };
353
+ }
354
+ step.status = stepEntry.status === 'skipped' ? 'skipped' : 'completed';
355
+ step.completedAt = new Date().toISOString();
356
+ }
357
+ catch (err) {
358
+ const msg = err instanceof Error ? err.message : String(err);
359
+ step.status = 'failed';
360
+ step.result = { error: msg };
361
+ step.completedAt = new Date().toISOString();
362
+ stepEntry = { stepId: step.stepId, type: step.type, status: 'failed', durationMs: Date.now() - stepStart, error: msg };
363
+ stepResults.push(stepEntry);
364
+ workflow.status = 'failed';
365
+ workflow.error = msg;
366
+ workflow.completedAt = new Date().toISOString();
367
+ saveWorkflowStore(store);
368
+ return {
369
+ workflowId,
370
+ status: 'failed',
371
+ error: msg,
372
+ failedStep: step.stepId,
373
+ stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
374
+ results: stepResults,
375
+ durationMs: Date.now() - startedAt,
376
+ };
377
+ }
378
+ if (typeof stepEntry.durationMs !== 'number')
379
+ stepEntry.durationMs = Date.now() - stepStart;
380
+ stepResults.push(stepEntry);
381
+ workflow.currentStep = i + 1;
382
+ saveWorkflowStore(store);
383
+ i++;
384
+ }
385
+ if (workflow.status === 'running') {
386
+ workflow.status = 'completed';
387
+ workflow.completedAt = new Date().toISOString();
388
+ saveWorkflowStore(store);
260
389
  }
261
- saveWorkflowStore(store);
262
390
  return {
263
391
  workflowId,
264
392
  status: workflow.status,
265
- totalSteps: results.length,
266
- results,
393
+ totalSteps: workflow.steps.length,
394
+ stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
395
+ stepsSkipped: stepResults.filter(s => s.status === 'skipped').length,
396
+ stepsFailed: stepResults.filter(s => s.status === 'failed').length,
397
+ results: stepResults,
267
398
  startedAt: workflow.startedAt,
268
- _note: 'Workflow is now running. Steps are in pending state and must be executed via task tools.',
399
+ completedAt: workflow.completedAt,
400
+ durationMs: Date.now() - startedAt,
269
401
  };
270
402
  },
271
403
  },
@@ -186,20 +186,108 @@ async function getRegistry(dbPath) {
186
186
  }
187
187
  }
188
188
  catch { /* router optional */ }
189
+ // ADR-095 G7: load disabled-by-default controllers via direct
190
+ // file:// URLs from the bundled agentdb. agentdb's exports
191
+ // field doesn't expose these subpaths and we can't reliably
192
+ // patch it across pnpm-hoisted multi-version trees, so we
193
+ // sidestep the exports field entirely and import the file
194
+ // by absolute URL. Only loads controllers whose constructor
195
+ // is safe with no special prerequisites — others remain off
196
+ // pending per-controller activation ADRs.
197
+ try {
198
+ const { createRequire } = await import('node:module');
199
+ const { pathToFileURL } = await import('node:url');
200
+ const path = await import('node:path');
201
+ const fs = await import('node:fs');
202
+ const cjsRequire = createRequire(import.meta.url);
203
+ let adbPkgJsonPath = null;
204
+ try {
205
+ adbPkgJsonPath = cjsRequire.resolve('agentdb/package.json');
206
+ }
207
+ catch {
208
+ adbPkgJsonPath = null;
209
+ }
210
+ if (adbPkgJsonPath) {
211
+ const adbDir = path.dirname(adbPkgJsonPath);
212
+ const candidates = [
213
+ // GNNService and RVFOptimizer can construct with no args
214
+ // in current agentdb — safe to activate as-is.
215
+ { name: 'gnnService', relPath: 'dist/src/services/GNNService.js', configurable: false },
216
+ { name: 'rvfOptimizer', relPath: 'dist/src/optimizations/RVFOptimizer.js', configurable: false },
217
+ // ADR-095 G7 follow-up: MutationGuard constructs cleanly
218
+ // with no args and exposes WASM-backed proof generation.
219
+ // No external deps; safe-default activation.
220
+ { name: 'mutationGuard', relPath: 'dist/src/security/MutationGuard.js', configurable: false },
221
+ // AttestationLog needs a sqlite db handle — wired below
222
+ // separately because we have to construct a db too.
223
+ // GuardedVectorBackend needs key material — leave for
224
+ // follow-up ADR.
225
+ ];
226
+ for (const cand of candidates) {
227
+ if (reg.get(cand.name))
228
+ continue;
229
+ const abs = path.join(adbDir, cand.relPath);
230
+ if (!fs.existsSync(abs))
231
+ continue;
232
+ try {
233
+ const url = pathToFileURL(abs).href;
234
+ const mod = await import(url);
235
+ // Look for a default export, named export matching the
236
+ // file basename, or any class-typed export.
237
+ const baseName = path.basename(cand.relPath, '.js');
238
+ const Ctor = (mod[baseName] || mod.default ||
239
+ Object.values(mod).find(v => typeof v === 'function'));
240
+ if (typeof Ctor !== 'function')
241
+ continue;
242
+ const inst = new Ctor();
243
+ if (typeof reg.set === 'function')
244
+ reg.set(cand.name, inst);
245
+ else
246
+ reg._controllers = { ...(reg._controllers || {}), [cand.name]: inst };
247
+ }
248
+ catch { /* skip controllers that fail to construct */ }
249
+ }
250
+ // AttestationLog activation — needs a better-sqlite3
251
+ // database. We open a dedicated file at .swarm/attestation.db
252
+ // (separate from the main memory.db so the audit trail
253
+ // is isolated). Best-effort: if better-sqlite3 isn't
254
+ // resolvable in this env, skip cleanly.
255
+ if (!reg.get('attestationLog')) {
256
+ try {
257
+ const attestationFile = path.join(adbDir, 'dist/src/security/AttestationLog.js');
258
+ if (fs.existsSync(attestationFile)) {
259
+ const Database = cjsRequire('better-sqlite3');
260
+ const swarmDir = path.resolve(process.cwd(), '.swarm');
261
+ if (!fs.existsSync(swarmDir))
262
+ fs.mkdirSync(swarmDir, { recursive: true });
263
+ const dbPath = path.join(swarmDir, 'attestation.db');
264
+ const db = new Database(dbPath);
265
+ const url = pathToFileURL(attestationFile).href;
266
+ const mod = await import(url);
267
+ const Ctor = mod.AttestationLog;
268
+ if (typeof Ctor === 'function') {
269
+ const inst = new Ctor({ db });
270
+ if (typeof reg.set === 'function')
271
+ reg.set('attestationLog', inst);
272
+ else
273
+ reg._controllers = { ...(reg._controllers || {}), attestationLog: inst };
274
+ }
275
+ }
276
+ }
277
+ catch { /* better-sqlite3 missing or schema init failed — skip silently */ }
278
+ }
279
+ }
280
+ }
281
+ catch { /* G7 wiring optional */ }
189
282
  })();
190
283
  // Run both in parallel; settle either way so a single failing
191
284
  // path doesn't tear down the rest of the post-init wiring.
192
285
  await Promise.allSettled([intelligencePromise, agentdbPromise]);
193
- // Other disabled controllers remain disabled and tracked in
194
- // ADR-093 F9 for future enablement:
195
- // - mutationGuard (write protection needs config)
196
- // - attestationLog (needs sqlite db handle the registry does
197
- // not currently expose)
198
- // - gnnService (graph neural net — heavy deps, needs WASM/CUDA)
199
- // - guardedVectorBackend (secured vector backend variant —
200
- // needs key material)
201
- // - rvfOptimizer (RVF format optimizer — needs RVF storage)
202
- // - graphAdapter (graph DB adapter — needs graph DB)
286
+ // Remaining disabled controllers tracked in ADR-095 G7 for
287
+ // per-controller activation ADRs (each needs config / key
288
+ // material that we don't pass blindly):
289
+ // - guardedVectorBackend (secured backend needs key material)
290
+ // - graphAdapter (graph DB adapter — needs graph DB connection)
203
291
  }
204
292
  catch {
205
293
  // Top-level catch — registry stays usable even if post-init wiring fails wholesale.
@@ -84,7 +84,18 @@ export declare function initAgentWasm(): Promise<void>;
84
84
  */
85
85
  export declare function createWasmAgent(config?: WasmAgentConfig): Promise<WasmAgentInfo>;
86
86
  /**
87
- * Send a prompt to a WASM agent. Requires a model provider to be set.
87
+ * Send a prompt to a WASM agent.
88
+ *
89
+ * ADR-095 G4: the bundled @ruvector/rvagent-wasm doesn't actually run an
90
+ * LLM — its prompt() method echoes input back as `"echo: <input>"`. We
91
+ * detect that stub output and route the prompt through Anthropic's
92
+ * Messages API so users get a real response. The WASM agent's sandbox
93
+ * (virtual filesystem, tool execution) still works for non-LLM ops via
94
+ * executeWasmTool — we're just patching the "talk to a model" hole.
95
+ *
96
+ * If ANTHROPIC_API_KEY is not set, returns the stub output verbatim so
97
+ * the failure mode is obvious to the caller (matches the previous
98
+ * behaviour rather than throwing for users without keys configured).
88
99
  */
89
100
  export declare function promptWasmAgent(agentId: string, input: string): Promise<string>;
90
101
  export declare function executeWasmTool(agentId: string, toolCall: Record<string, unknown>): Promise<ToolResult>;
@@ -80,7 +80,18 @@ export async function createWasmAgent(config = {}) {
80
80
  return info;
81
81
  }
82
82
  /**
83
- * Send a prompt to a WASM agent. Requires a model provider to be set.
83
+ * Send a prompt to a WASM agent.
84
+ *
85
+ * ADR-095 G4: the bundled @ruvector/rvagent-wasm doesn't actually run an
86
+ * LLM — its prompt() method echoes input back as `"echo: <input>"`. We
87
+ * detect that stub output and route the prompt through Anthropic's
88
+ * Messages API so users get a real response. The WASM agent's sandbox
89
+ * (virtual filesystem, tool execution) still works for non-LLM ops via
90
+ * executeWasmTool — we're just patching the "talk to a model" hole.
91
+ *
92
+ * If ANTHROPIC_API_KEY is not set, returns the stub output verbatim so
93
+ * the failure mode is obvious to the caller (matches the previous
94
+ * behaviour rather than throwing for users without keys configured).
84
95
  */
85
96
  export async function promptWasmAgent(agentId, input) {
86
97
  const entry = agents.get(agentId);
@@ -88,10 +99,34 @@ export async function promptWasmAgent(agentId, input) {
88
99
  throw new Error(`WASM agent not found: ${agentId}`);
89
100
  entry.info.state = 'running';
90
101
  try {
91
- const result = await entry.agent.prompt(input);
102
+ const wasmResult = await entry.agent.prompt(input);
92
103
  entry.info.state = 'idle';
93
104
  syncAgentInfo(entry);
94
- return result;
105
+ // Detect the WASM echo stub.
106
+ const isEchoStub = typeof wasmResult === 'string' &&
107
+ (wasmResult === `echo: ${input}` || /^echo: /.test(wasmResult.slice(0, 12)));
108
+ if (!isEchoStub) {
109
+ return wasmResult;
110
+ }
111
+ // Echo stub detected — route through a real LLM call.
112
+ if (!process.env.ANTHROPIC_API_KEY) {
113
+ // No key configured; surface the stub honestly with a hint.
114
+ return `${wasmResult}\n[NOTE: bundled WASM agent has no LLM; set ANTHROPIC_API_KEY to enable real responses via Anthropic Messages API]`;
115
+ }
116
+ const { callAnthropicMessages, resolveAnthropicModel } = await import('../mcp-tools/agent-execute-core.js');
117
+ const model = resolveAnthropicModel(entry.info.config.model);
118
+ const systemPrompt = entry.info.config.instructions || 'You are a helpful coding assistant running in a Ruflo WASM agent sandbox.';
119
+ const result = await callAnthropicMessages({
120
+ prompt: input,
121
+ systemPrompt,
122
+ model,
123
+ maxTokens: 2048,
124
+ });
125
+ if (!result.success) {
126
+ return `${wasmResult}\n[NOTE: bundled WASM agent has no LLM; Anthropic fallback failed: ${result.error}]`;
127
+ }
128
+ // Return the real LLM output, not the echo stub.
129
+ return result.output ?? '';
95
130
  }
96
131
  catch (err) {
97
132
  entry.info.state = 'error';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.6.21",
3
+ "version": "3.6.23",
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",
@@ -76,6 +76,7 @@
76
76
  "files": [
77
77
  "dist",
78
78
  "bin",
79
+ "scripts",
79
80
  ".claude",
80
81
  "README.md"
81
82
  ],
@@ -84,7 +85,7 @@
84
85
  "test": "vitest run",
85
86
  "test:plugin-store": "npx tsx src/plugins/tests/standalone-test.ts",
86
87
  "test:pattern-store": "npx tsx src/transfer/store/tests/standalone-test.ts",
87
- "postinstall": "node -e \"const{existsSync,cpSync,readdirSync,statSync}=require('fs');const{join,dirname}=require('path');try{const r=require.resolve('agentdb');const base=r.includes('dist/src')?join(dirname(r),'..','..'):(r.includes('dist')?join(dirname(r),'..'):dirname(r));const srcDist=join(base,'dist','src');if(!existsSync(srcDist))process.exit(0);for(const e of readdirSync(srcDist)){const s=join(srcDist,e);const t=join(base,'dist',e);try{if(statSync(s).isDirectory()&&!existsSync(t)){cpSync(s,t,{recursive:true});}}catch{}}}catch{}\"",
88
+ "postinstall": "node ./scripts/postinstall.cjs",
88
89
  "prepublishOnly": "cp ../../../README.md ./README.md",
89
90
  "release": "npm version prerelease --preid=alpha && npm run publish:all",
90
91
  "publish:all": "./scripts/publish.sh"