@claude-flow/cli 3.55.0 → 3.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/.claude/settings.json +9 -9
- package/catalog-manifest.json +2 -2
- package/dist/src/auth/client.js +40 -24
- package/dist/src/commands/agent.js +12 -0
- package/dist/src/commands/analyze.js +18 -14
- package/dist/src/commands/daemon.js +11 -19
- package/dist/src/commands/doctor.js +65 -58
- package/dist/src/commands/memory-backup.d.ts +1 -1
- package/dist/src/commands/memory-backup.js +23 -9
- package/dist/src/commands/memory.d.ts +0 -4
- package/dist/src/commands/memory.js +10 -2
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/commands/task.js +12 -0
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-tools/agent-execute-core.js +244 -188
- package/dist/src/mcp-tools/agent-tools.js +15 -16
- package/dist/src/mcp-tools/agentdb-tools.js +22 -3
- package/dist/src/mcp-tools/claims-tools.js +3 -11
- package/dist/src/mcp-tools/hooks-tools.js +47 -2
- package/dist/src/mcp-tools/memory-tools.js +29 -10
- package/dist/src/mcp-tools/record-store.d.ts +3 -0
- package/dist/src/mcp-tools/record-store.js +21 -0
- package/dist/src/mcp-tools/session-tools.js +47 -13
- package/dist/src/mcp-tools/task-tools.js +5 -11
- package/dist/src/mcp-tools/workflow-tools.js +78 -29
- package/dist/src/memory/append-conditions.d.ts +28 -0
- package/dist/src/memory/append-conditions.js +56 -0
- package/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/dist/src/memory/graph-edge-writer.js +90 -0
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +50 -9
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +95 -61
- package/dist/src/memory/memory-root.d.ts +2 -0
- package/dist/src/memory/memory-root.js +28 -0
- package/dist/src/memory/sona-optimizer.d.ts +2 -0
- package/dist/src/memory/sona-optimizer.js +4 -0
- package/dist/src/parser.js +7 -0
- package/dist/src/plugins/store/discovery.d.ts +6 -0
- package/dist/src/plugins/store/discovery.js +7 -3
- package/dist/src/ruvector/neural-router.js +7 -2
- package/dist/src/services/claim-service.js +39 -18
- package/dist/src/services/claims-store.d.ts +3 -0
- package/dist/src/services/claims-store.js +40 -0
- package/dist/src/services/distill-tuning.js +17 -10
- package/dist/src/services/memory-backup.d.ts +7 -0
- package/dist/src/services/memory-backup.js +29 -2
- package/dist/src/services/memory-distillation.js +2 -2
- package/dist/src/services/worker-daemon.d.ts +4 -2
- package/dist/src/services/worker-daemon.js +14 -6
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
- package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +4 -4
- package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
- package/.claude/agents/tmp.json +0 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { readRecordStore } from './record-store.js';
|
|
9
10
|
import { getProjectCwd } from './types.js';
|
|
10
11
|
import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
|
|
11
12
|
import { executeAgentTask } from './agent-execute-core.js';
|
|
@@ -32,17 +33,7 @@ function ensureAgentDir() {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
function loadAgentStore() {
|
|
35
|
-
|
|
36
|
-
const path = getAgentPath();
|
|
37
|
-
if (existsSync(path)) {
|
|
38
|
-
const data = readFileSync(path, 'utf-8');
|
|
39
|
-
return JSON.parse(data);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
// Return empty store on error
|
|
44
|
-
}
|
|
45
|
-
return { agents: {}, version: '3.0.0' };
|
|
36
|
+
return readRecordStore(getAgentPath(), 'agents', () => ({ agents: {}, version: '3.0.0' }));
|
|
46
37
|
}
|
|
47
38
|
function saveAgentStore(store) {
|
|
48
39
|
ensureAgentDir();
|
|
@@ -247,7 +238,6 @@ export const agentTools = [
|
|
|
247
238
|
if (!validation.valid) {
|
|
248
239
|
return { success: false, error: `Input validation failed: ${validation.errors.join('; ')}` };
|
|
249
240
|
}
|
|
250
|
-
const store = loadAgentStore();
|
|
251
241
|
const agentId = input.agentId || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
252
242
|
const agentType = input.agentType;
|
|
253
243
|
const config = input.config || {};
|
|
@@ -285,6 +275,7 @@ export const agentTools = [
|
|
|
285
275
|
: routingResult.provider ? { provider: routingResult.provider } : {}),
|
|
286
276
|
...(routingResult.openrouterModel ? { openrouterModel: routingResult.openrouterModel } : {}),
|
|
287
277
|
};
|
|
278
|
+
const store = loadAgentStore();
|
|
288
279
|
store.agents[agentId] = agent;
|
|
289
280
|
saveAgentStore(store);
|
|
290
281
|
// #2085 — also push to the swarm store's agents array so that
|
|
@@ -337,8 +328,13 @@ export const agentTools = [
|
|
|
337
328
|
memoryBranch = br.branchPath;
|
|
338
329
|
agent.memoryBranch = br.branchPath;
|
|
339
330
|
agent.memoryBase = br.basePath;
|
|
340
|
-
|
|
341
|
-
|
|
331
|
+
const latest = loadAgentStore();
|
|
332
|
+
const current = latest.agents[agentId];
|
|
333
|
+
if (current && current.status !== 'terminated') {
|
|
334
|
+
current.memoryBranch = br.branchPath;
|
|
335
|
+
current.memoryBase = br.basePath;
|
|
336
|
+
saveAgentStore(latest);
|
|
337
|
+
}
|
|
342
338
|
}
|
|
343
339
|
// else: degraded (agenticow missing / kill-switched) — agent stands
|
|
344
340
|
// without an isolated branch; callers see no memoryBranch field.
|
|
@@ -629,7 +625,10 @@ export const agentTools = [
|
|
|
629
625
|
};
|
|
630
626
|
}
|
|
631
627
|
if (action === 'scale') {
|
|
632
|
-
const targetSize = input.targetSize
|
|
628
|
+
const targetSize = input.targetSize === undefined ? 5 : input.targetSize;
|
|
629
|
+
if (typeof targetSize !== 'number' || !Number.isSafeInteger(targetSize) || targetSize < 0) {
|
|
630
|
+
return { action, error: 'targetSize must be a non-negative safe integer' };
|
|
631
|
+
}
|
|
633
632
|
const agentType = input.agentType || 'worker';
|
|
634
633
|
const currentSize = agents.filter(a => a.agentType === agentType).length;
|
|
635
634
|
const delta = targetSize - currentSize;
|
|
@@ -663,7 +662,7 @@ export const agentTools = [
|
|
|
663
662
|
agentType,
|
|
664
663
|
previousSize: currentSize,
|
|
665
664
|
targetSize,
|
|
666
|
-
newSize: currentSize +
|
|
665
|
+
newSize: currentSize + added.length - removed.length,
|
|
667
666
|
added,
|
|
668
667
|
removed,
|
|
669
668
|
};
|
|
@@ -61,6 +61,25 @@ async function getGraphEdgeWriter() {
|
|
|
61
61
|
graphEdgeWriterMod = await import('../memory/graph-edge-writer.js');
|
|
62
62
|
return graphEdgeWriterMod;
|
|
63
63
|
}
|
|
64
|
+
// #2889 — a null graph_edges DB handle can mean several unrelated things
|
|
65
|
+
// (missing native module, permissions, genuine corruption, or an RFE1
|
|
66
|
+
// encrypted-at-rest store). Recommending `memory init` for the encrypted
|
|
67
|
+
// case is actively harmful — it creates a fresh EMPTY store instead of
|
|
68
|
+
// using the existing encrypted data — so distinguish it before choosing
|
|
69
|
+
// a hint.
|
|
70
|
+
async function bridgeUnavailableResult() {
|
|
71
|
+
const { isBridgeDbEncryptedAtRest } = await getGraphEdgeWriter();
|
|
72
|
+
if (isBridgeDbEncryptedAtRest()) {
|
|
73
|
+
return {
|
|
74
|
+
error: 'graph_edges DB is encrypted at rest (RFE1) — the native bridge cannot open it directly.',
|
|
75
|
+
hint: 'Your existing data is intact; this is a known limitation, not corruption. Do NOT run `memory init` — see https://github.com/ruvnet/ruflo/issues/2889.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
error: 'graph_edges DB unavailable (sql.js could not load)',
|
|
80
|
+
hint: 'Check Node version + try `ruflo memory init` to initialize manually.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
64
83
|
let memInitMod = null;
|
|
65
84
|
async function getMemInit() {
|
|
66
85
|
if (!memInitMod)
|
|
@@ -1152,7 +1171,7 @@ export const agentdbGraphQuery = {
|
|
|
1152
1171
|
// fresh environments work without a pre-existing memory init.
|
|
1153
1172
|
const db = await getBridgeDb(undefined, { createIfMissing: true });
|
|
1154
1173
|
if (!db)
|
|
1155
|
-
return { success: false,
|
|
1174
|
+
return { success: false, ...(await bridgeUnavailableResult()), mode, nodeId };
|
|
1156
1175
|
// Load all rows with embedding_ref and score by cosine.
|
|
1157
1176
|
// better-sqlite3 API — `db.exec(sql, params)` (sql.js) silently
|
|
1158
1177
|
// throws "datatype mismatch" because exec ignores params, so `?`
|
|
@@ -1194,7 +1213,7 @@ export const agentdbGraphQuery = {
|
|
|
1194
1213
|
// fresh environments work without a pre-existing memory init.
|
|
1195
1214
|
const db = await getBridgeDb(undefined, { createIfMissing: true });
|
|
1196
1215
|
if (!db)
|
|
1197
|
-
return { success: false,
|
|
1216
|
+
return { success: false, ...(await bridgeUnavailableResult()), mode, nodeId };
|
|
1198
1217
|
// better-sqlite3 API — see semantic-mode comment above.
|
|
1199
1218
|
const edges = db.prepare(`SELECT source_id, target_id, weight FROM graph_edges LIMIT ?`).raw().all(budget.maxNodesVisited);
|
|
1200
1219
|
if (edges.length === 0) {
|
|
@@ -1380,7 +1399,7 @@ export const agentdbGraphPathfinder = {
|
|
|
1380
1399
|
// #2246 fix: lazy-create memory.db on first pathfinder call.
|
|
1381
1400
|
const db = await getBridgeDb(undefined, { createIfMissing: true });
|
|
1382
1401
|
if (!db)
|
|
1383
|
-
return { success: false,
|
|
1402
|
+
return { success: false, ...(await bridgeUnavailableResult()), seedNodeId };
|
|
1384
1403
|
const colsSql = algorithm === 'witness-chain-divergence'
|
|
1385
1404
|
? 'source_id, target_id, weight, last_reinforced, witness_id'
|
|
1386
1405
|
: algorithm === 'temporal-centrality'
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module @claude-flow/cli/mcp-tools/claims
|
|
8
8
|
*/
|
|
9
|
+
import { readClaimsStore } from '../services/claims-store.js';
|
|
9
10
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
10
11
|
// File-based persistence
|
|
11
|
-
import { existsSync,
|
|
12
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
13
|
import { join, resolve } from 'path';
|
|
13
14
|
const CLAIMS_DIR = '.claude-flow/claims';
|
|
14
15
|
const CLAIMS_FILE = 'claims.json';
|
|
@@ -22,16 +23,7 @@ function ensureClaimsDir() {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
function loadClaims() {
|
|
25
|
-
|
|
26
|
-
const path = getClaimsPath();
|
|
27
|
-
if (existsSync(path)) {
|
|
28
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Return empty store on error
|
|
33
|
-
}
|
|
34
|
-
return { claims: {}, stealable: {}, contests: {} };
|
|
26
|
+
return readClaimsStore(getClaimsPath());
|
|
35
27
|
}
|
|
36
28
|
function saveClaims(store) {
|
|
37
29
|
ensureClaimsDir();
|
|
@@ -1154,14 +1154,59 @@ export const hooksRoute = {
|
|
|
1154
1154
|
catch {
|
|
1155
1155
|
// AgentDB router not available — fall through to local routing
|
|
1156
1156
|
}
|
|
1157
|
+
// Trajectory learning persists SONA outcomes separately from the local
|
|
1158
|
+
// semantic index. Only consume an eligible learned pattern here; native,
|
|
1159
|
+
// Q-learning and keyword suggestions retain their existing routing paths.
|
|
1160
|
+
try {
|
|
1161
|
+
const sonaStart = performance.now();
|
|
1162
|
+
const sona = await getSONAOptimizer();
|
|
1163
|
+
const suggestion = await sona?.getRoutingSuggestion(task);
|
|
1164
|
+
const { isSupportedSonaAgent } = await import('../memory/sona-optimizer.js');
|
|
1165
|
+
if (suggestion?.source === 'sona-pattern'
|
|
1166
|
+
&& Number.isFinite(suggestion.confidence)
|
|
1167
|
+
&& suggestion.confidence >= 0.6 && suggestion.confidence <= 1
|
|
1168
|
+
&& isSupportedSonaAgent(suggestion.agent)) {
|
|
1169
|
+
const confidence = Math.round(suggestion.confidence * 100) / 100;
|
|
1170
|
+
const complexity = task.length > 200 ? 'high' : task.length < 50 ? 'low' : 'medium';
|
|
1171
|
+
return {
|
|
1172
|
+
task,
|
|
1173
|
+
routing: { method: 'sona-pattern', backend: 'SONA persisted keyword patterns', latencyMs: performance.now() - sonaStart, throughput: 'N/A' },
|
|
1174
|
+
matched: true,
|
|
1175
|
+
matchedPattern: 'sona-pattern',
|
|
1176
|
+
semanticMatches: [],
|
|
1177
|
+
primaryAgent: {
|
|
1178
|
+
type: suggestion.agent,
|
|
1179
|
+
confidence,
|
|
1180
|
+
reason: `Persisted SONA outcome pattern (${Math.round(suggestion.confidence * 100)}% confidence)`,
|
|
1181
|
+
},
|
|
1182
|
+
alternativeAgents: suggestion.alternatives
|
|
1183
|
+
.filter(alternative => isSupportedSonaAgent(alternative.agent) && Number.isFinite(alternative.score))
|
|
1184
|
+
.map(alternative => ({
|
|
1185
|
+
type: alternative.agent,
|
|
1186
|
+
confidence: Math.round(alternative.score * 100) / 100,
|
|
1187
|
+
reason: 'Alternative from persisted SONA patterns',
|
|
1188
|
+
})),
|
|
1189
|
+
estimatedMetrics: {
|
|
1190
|
+
successProbability: confidence,
|
|
1191
|
+
estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
|
|
1192
|
+
complexity,
|
|
1193
|
+
},
|
|
1194
|
+
swarmRecommendation: null,
|
|
1195
|
+
learning: { source: suggestion.source, matchedKeywords: suggestion.matchedKeywords },
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
// Optional SONA state unavailable — preserve the existing local route.
|
|
1201
|
+
}
|
|
1157
1202
|
}
|
|
1158
1203
|
return routeTaskLocal(task, context, useSemanticRouter);
|
|
1159
1204
|
}),
|
|
1160
1205
|
};
|
|
1161
1206
|
/**
|
|
1162
1207
|
* hooks_route's local routing (semantic index + keyword fallback), run after the
|
|
1163
|
-
* AgentDB pre-
|
|
1164
|
-
* measures
|
|
1208
|
+
* AgentDB and eligible SONA pre-routes. Shared with {@link routeTaskForBench}
|
|
1209
|
+
* so the benchmark measures this local path. (Body kept at its original
|
|
1165
1210
|
* indentation to keep this refactor's diff small.)
|
|
1166
1211
|
*/
|
|
1167
1212
|
async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride) {
|
|
@@ -1500,7 +1500,21 @@ export const memoryTools = [
|
|
|
1500
1500
|
// entry body. Without this, `value` is always null because listEntries
|
|
1501
1501
|
// strips content by default (callers pay for the JSON parse only when
|
|
1502
1502
|
// they need it).
|
|
1503
|
-
|
|
1503
|
+
// A successful export must cover the whole reported store. Read all
|
|
1504
|
+
// pages before touching an existing snapshot, including on read failure.
|
|
1505
|
+
const entries = [];
|
|
1506
|
+
let total;
|
|
1507
|
+
do {
|
|
1508
|
+
const page = await listEntries({ limit: 10000, offset: entries.length, namespace, includeContent: true });
|
|
1509
|
+
if (!page.success)
|
|
1510
|
+
return { error: page.error || 'Could not read memory for export' };
|
|
1511
|
+
total ??= page.total;
|
|
1512
|
+
if (page.entries.length === 0 && entries.length < total) {
|
|
1513
|
+
return { error: `Incomplete memory export: read ${entries.length} of ${total} entries` };
|
|
1514
|
+
}
|
|
1515
|
+
entries.push(...page.entries);
|
|
1516
|
+
} while (entries.length < total);
|
|
1517
|
+
const all = { entries };
|
|
1504
1518
|
const payload = {
|
|
1505
1519
|
schema: 'ruflo-memory-export/v1',
|
|
1506
1520
|
exportedAt: new Date().toISOString(),
|
|
@@ -1595,6 +1609,8 @@ export const memoryTools = [
|
|
|
1595
1609
|
}
|
|
1596
1610
|
let imported = 0;
|
|
1597
1611
|
let skipped = 0;
|
|
1612
|
+
let failed = 0;
|
|
1613
|
+
const errors = [];
|
|
1598
1614
|
for (const e of entries) {
|
|
1599
1615
|
if (!e || typeof e.key !== 'string') {
|
|
1600
1616
|
skipped++;
|
|
@@ -1603,19 +1619,22 @@ export const memoryTools = [
|
|
|
1603
1619
|
const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
|
|
1604
1620
|
try {
|
|
1605
1621
|
const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
|
|
1606
|
-
if (result.success)
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
else
|
|
1612
|
-
skipped++;
|
|
1622
|
+
if (!result.success)
|
|
1623
|
+
throw new Error(result.error || 'Memory write was rejected');
|
|
1624
|
+
imported++;
|
|
1625
|
+
if (result.embedding)
|
|
1626
|
+
vectors++;
|
|
1613
1627
|
}
|
|
1614
|
-
catch {
|
|
1615
|
-
|
|
1628
|
+
catch (error) {
|
|
1629
|
+
failed++;
|
|
1630
|
+
if (errors.length < 10)
|
|
1631
|
+
errors.push(`${e.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1616
1632
|
}
|
|
1617
1633
|
}
|
|
1618
1634
|
return {
|
|
1635
|
+
success: failed === 0,
|
|
1636
|
+
error: failed > 0 ? `${failed} memory write(s) failed: ${errors.join('; ')}` : undefined,
|
|
1637
|
+
failed,
|
|
1619
1638
|
inputPath,
|
|
1620
1639
|
imported: { entries: imported, vectors, patterns: 0 },
|
|
1621
1640
|
skipped,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/** Only an absent store is empty. Failed reads must never authorize replacement. */
|
|
3
|
+
export function readRecordStore(path, collection, empty) {
|
|
4
|
+
let text;
|
|
5
|
+
try {
|
|
6
|
+
text = readFileSync(path, 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error.code === 'ENOENT')
|
|
10
|
+
return empty();
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
const store = JSON.parse(text);
|
|
14
|
+
const records = store?.[collection];
|
|
15
|
+
if (!store || typeof store !== 'object' || Array.isArray(store)
|
|
16
|
+
|| !records || typeof records !== 'object' || Array.isArray(records)) {
|
|
17
|
+
throw new Error(`Invalid ${collection} store at ${path}; existing data was preserved`);
|
|
18
|
+
}
|
|
19
|
+
return store;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=record-store.js.map
|
|
@@ -10,6 +10,33 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { getProjectCwd } from './types.js';
|
|
11
11
|
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
12
12
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
// Imported snapshots become live stores on restore. Validate their containers
|
|
17
|
+
// before registering them, while allowing older metadata-only snapshots.
|
|
18
|
+
function validateImportedSession(value) {
|
|
19
|
+
if (!isRecord(value) || (typeof value.name !== 'string' && !isSessionRecordLike(value))) {
|
|
20
|
+
return 'Invalid session: Not a session record; expected a named snapshot or snapshot data';
|
|
21
|
+
}
|
|
22
|
+
if (value.name !== undefined && typeof value.name !== 'string') {
|
|
23
|
+
return 'Invalid session: name must be a string';
|
|
24
|
+
}
|
|
25
|
+
if (value.data === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (!isRecord(value.data))
|
|
28
|
+
return 'Invalid session: data must be an object';
|
|
29
|
+
for (const [component, collection] of [['tasks', 'tasks'], ['agents', 'agents'], ['memory', 'entries']]) {
|
|
30
|
+
const store = value.data[component];
|
|
31
|
+
if (store === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (!isRecord(store) || !isRecord(store[collection]) ||
|
|
34
|
+
Object.values(store[collection]).some(record => !isRecord(record))) {
|
|
35
|
+
return `Invalid session: data.${component}.${collection} must be a record map`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
13
40
|
// Storage paths
|
|
14
41
|
const STORAGE_DIR = '.claude-flow';
|
|
15
42
|
const SESSION_DIR = 'sessions';
|
|
@@ -306,9 +333,9 @@ export const sessionTools = [
|
|
|
306
333
|
properties: {
|
|
307
334
|
name: { type: 'string', description: 'Session name' },
|
|
308
335
|
description: { type: 'string', description: 'Session description' },
|
|
309
|
-
includeMemory: { type: 'boolean', description: 'Include memory in session' },
|
|
310
|
-
includeTasks: { type: 'boolean', description: 'Include tasks in session' },
|
|
311
|
-
includeAgents: { type: 'boolean', description: 'Include agents in session' },
|
|
336
|
+
includeMemory: { type: 'boolean', default: true, description: 'Include memory in session' },
|
|
337
|
+
includeTasks: { type: 'boolean', default: true, description: 'Include tasks in session' },
|
|
338
|
+
includeAgents: { type: 'boolean', default: true, description: 'Include agents in session' },
|
|
312
339
|
},
|
|
313
340
|
required: ['name'],
|
|
314
341
|
},
|
|
@@ -325,9 +352,9 @@ export const sessionTools = [
|
|
|
325
352
|
const sessionId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
326
353
|
// Load related data based on options
|
|
327
354
|
const { data, memoryCapture } = await loadRelatedStores({
|
|
328
|
-
includeMemory: input.includeMemory,
|
|
329
|
-
includeTasks: input.includeTasks,
|
|
330
|
-
includeAgents: input.includeAgents,
|
|
355
|
+
includeMemory: input.includeMemory !== false,
|
|
356
|
+
includeTasks: input.includeTasks !== false,
|
|
357
|
+
includeAgents: input.includeAgents !== false,
|
|
331
358
|
});
|
|
332
359
|
// Calculate stats
|
|
333
360
|
const stats = {
|
|
@@ -692,19 +719,26 @@ export const sessionTools = [
|
|
|
692
719
|
return { error: `Invalid session JSON: ${e.message}` };
|
|
693
720
|
}
|
|
694
721
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
722
|
+
const validationError = validateImportedSession(parsed);
|
|
723
|
+
if (validationError)
|
|
724
|
+
return { error: validationError };
|
|
725
|
+
const record = parsed;
|
|
698
726
|
const newId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
699
|
-
const stats = {
|
|
727
|
+
const stats = {
|
|
728
|
+
tasks: Object.keys(record.data?.tasks?.tasks || {}).length,
|
|
729
|
+
agents: Object.keys(record.data?.agents?.agents || {}).length,
|
|
730
|
+
memoryEntries: countMemoryEntries(record.data?.memory),
|
|
731
|
+
totalSize: 0,
|
|
732
|
+
};
|
|
700
733
|
const session = {
|
|
701
734
|
sessionId: newId,
|
|
702
|
-
name: input.name ? String(input.name) : (
|
|
703
|
-
description:
|
|
735
|
+
name: input.name ? String(input.name) : (record.name || 'imported-session'),
|
|
736
|
+
description: record.description,
|
|
704
737
|
savedAt: new Date().toISOString(),
|
|
705
738
|
stats,
|
|
706
|
-
data:
|
|
739
|
+
data: record.data,
|
|
707
740
|
};
|
|
741
|
+
session.stats.totalSize = Buffer.byteLength(JSON.stringify(session), 'utf-8');
|
|
708
742
|
saveSession(session);
|
|
709
743
|
let activated = false;
|
|
710
744
|
if (input.activate === true) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { readRecordStore } from './record-store.js';
|
|
8
9
|
import { getProjectCwd } from './types.js';
|
|
9
10
|
import { validateIdentifier, validateText } from './validate-input.js';
|
|
10
11
|
// Storage paths
|
|
@@ -24,17 +25,7 @@ function ensureTaskDir() {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
function loadTaskStore() {
|
|
27
|
-
|
|
28
|
-
const path = getTaskPath();
|
|
29
|
-
if (existsSync(path)) {
|
|
30
|
-
const data = readFileSync(path, 'utf-8');
|
|
31
|
-
return JSON.parse(data);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// Return empty store on error
|
|
36
|
-
}
|
|
37
|
-
return { tasks: {}, version: '3.0.0' };
|
|
28
|
+
return readRecordStore(getTaskPath(), 'tasks', () => ({ tasks: {}, version: '3.0.0' }));
|
|
38
29
|
}
|
|
39
30
|
function saveTaskStore(store) {
|
|
40
31
|
ensureTaskDir();
|
|
@@ -378,6 +369,9 @@ export const taskTools = [
|
|
|
378
369
|
return { taskId, error: 'Task not found' };
|
|
379
370
|
}
|
|
380
371
|
const previouslyAssigned = [...task.assignedTo];
|
|
372
|
+
// Load agent store to sync worker state
|
|
373
|
+
const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json');
|
|
374
|
+
readRecordStore(agentStorePath, 'agents', () => ({ agents: {} }));
|
|
381
375
|
if (input.unassign) {
|
|
382
376
|
// Revert previously assigned agents to idle
|
|
383
377
|
releaseAgents(previouslyAssigned, taskId);
|
|
@@ -41,6 +41,22 @@ function saveWorkflowStore(store) {
|
|
|
41
41
|
ensureWorkflowDir();
|
|
42
42
|
writeFileSync(getWorkflowPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
43
43
|
}
|
|
44
|
+
// A paused in-flight step still owns its executor. Resume joins that execution
|
|
45
|
+
// instead of dispatching the same side effect again (within this process).
|
|
46
|
+
const workflowExecutions = new Map();
|
|
47
|
+
const workflowExecutionKey = (workflowId) => JSON.stringify([getWorkflowPath(), workflowId]);
|
|
48
|
+
function withWorkflowExecution(workflowId, execute) {
|
|
49
|
+
const key = workflowExecutionKey(workflowId);
|
|
50
|
+
if (workflowExecutions.has(key)) {
|
|
51
|
+
return Promise.resolve({ workflowId, error: 'Workflow already executing' });
|
|
52
|
+
}
|
|
53
|
+
const execution = execute();
|
|
54
|
+
workflowExecutions.set(key, execution);
|
|
55
|
+
return execution.finally(() => {
|
|
56
|
+
if (workflowExecutions.get(key) === execution)
|
|
57
|
+
workflowExecutions.delete(key);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
44
60
|
export const workflowTools = [
|
|
45
61
|
{
|
|
46
62
|
name: 'workflow_run',
|
|
@@ -227,7 +243,7 @@ export const workflowTools = [
|
|
|
227
243
|
},
|
|
228
244
|
required: ['workflowId'],
|
|
229
245
|
},
|
|
230
|
-
handler: async (input) => {
|
|
246
|
+
handler: async (input) => withWorkflowExecution(input.workflowId, async () => {
|
|
231
247
|
// Validate user-provided input (#1425)
|
|
232
248
|
const vId = validateIdentifier(input.workflowId, 'workflowId');
|
|
233
249
|
if (!vId.valid)
|
|
@@ -241,13 +257,50 @@ export const workflowTools = [
|
|
|
241
257
|
if (workflow.status === 'running') {
|
|
242
258
|
return { workflowId, error: 'Workflow already running' };
|
|
243
259
|
}
|
|
260
|
+
// Indices may name a step or the end boundary (used when resuming
|
|
261
|
+
// after the final step). Reject invalid control flow before any writes.
|
|
262
|
+
const validIndex = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= workflow.steps.length;
|
|
263
|
+
const startFromStep = input.startFromStep === undefined ? 0 : input.startFromStep;
|
|
264
|
+
if (!validIndex(startFromStep)) {
|
|
265
|
+
return { workflowId, error: `startFromStep must be an integer between 0 and ${workflow.steps.length}` };
|
|
266
|
+
}
|
|
267
|
+
for (const step of workflow.steps) {
|
|
268
|
+
if (step.type !== 'condition')
|
|
269
|
+
continue;
|
|
270
|
+
for (const target of ['thenStep', 'elseStep']) {
|
|
271
|
+
if (step.config[target] !== undefined && !validIndex(step.config[target])) {
|
|
272
|
+
return { workflowId, error: `${step.stepId}.${target} must be an integer between 0 and ${workflow.steps.length}` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const persistProgress = () => {
|
|
277
|
+
// An awaited step may overlap pause/stop/delete or a sibling workflow
|
|
278
|
+
// mutation. Merge only this workflow into the latest store snapshot.
|
|
279
|
+
const latest = loadWorkflowStore();
|
|
280
|
+
const current = latest.workflows[workflowId];
|
|
281
|
+
if (!current) {
|
|
282
|
+
workflow.status = 'failed';
|
|
283
|
+
workflow.error = 'Workflow was deleted during execution';
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (current.status === 'failed') {
|
|
287
|
+
Object.assign(workflow, current);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (current.status === 'paused' && workflow.status !== 'failed')
|
|
291
|
+
workflow.status = 'paused';
|
|
292
|
+
latest.workflows[workflowId] = workflow;
|
|
293
|
+
saveWorkflowStore(latest);
|
|
294
|
+
};
|
|
244
295
|
// Inject runtime variables
|
|
245
296
|
if (input.variables) {
|
|
246
297
|
workflow.variables = { ...workflow.variables, ...input.variables };
|
|
247
298
|
}
|
|
248
299
|
workflow.status = 'running';
|
|
249
300
|
workflow.startedAt = new Date().toISOString();
|
|
250
|
-
workflow.
|
|
301
|
+
delete workflow.error;
|
|
302
|
+
delete workflow.completedAt;
|
|
303
|
+
workflow.currentStep = startFromStep;
|
|
251
304
|
saveWorkflowStore(store);
|
|
252
305
|
// ADR-095 G3: real workflow runtime. Walk the steps in order;
|
|
253
306
|
// dispatch each by type. Persist progress after each step so a
|
|
@@ -277,22 +330,26 @@ export const workflowTools = [
|
|
|
277
330
|
while (i < workflow.steps.length) {
|
|
278
331
|
// Honor pause/cancel signals between steps.
|
|
279
332
|
const live = loadWorkflowStore().workflows[workflowId];
|
|
280
|
-
if (!live
|
|
333
|
+
if (!live) {
|
|
334
|
+
workflow.status = 'failed';
|
|
335
|
+
workflow.error = 'Workflow was deleted during execution';
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (live.status === 'paused') {
|
|
281
339
|
workflow.status = 'paused';
|
|
282
340
|
workflow.currentStep = i;
|
|
283
|
-
|
|
341
|
+
persistProgress();
|
|
284
342
|
break;
|
|
285
343
|
}
|
|
286
344
|
if (live.status === 'failed') {
|
|
287
|
-
workflow
|
|
288
|
-
saveWorkflowStore(store);
|
|
345
|
+
Object.assign(workflow, live);
|
|
289
346
|
break;
|
|
290
347
|
}
|
|
291
348
|
const step = workflow.steps[i];
|
|
292
349
|
step.status = 'running';
|
|
293
350
|
step.startedAt = new Date().toISOString();
|
|
294
351
|
const stepStart = Date.now();
|
|
295
|
-
|
|
352
|
+
persistProgress();
|
|
296
353
|
let stepEntry = { stepId: step.stepId, type: step.type, status: 'running' };
|
|
297
354
|
try {
|
|
298
355
|
if (step.type === 'task') {
|
|
@@ -364,7 +421,7 @@ export const workflowTools = [
|
|
|
364
421
|
workflow.status = 'failed';
|
|
365
422
|
workflow.error = msg;
|
|
366
423
|
workflow.completedAt = new Date().toISOString();
|
|
367
|
-
|
|
424
|
+
persistProgress();
|
|
368
425
|
return {
|
|
369
426
|
workflowId,
|
|
370
427
|
status: 'failed',
|
|
@@ -379,17 +436,18 @@ export const workflowTools = [
|
|
|
379
436
|
stepEntry.durationMs = Date.now() - stepStart;
|
|
380
437
|
stepResults.push(stepEntry);
|
|
381
438
|
workflow.currentStep = i + 1;
|
|
382
|
-
|
|
439
|
+
persistProgress();
|
|
383
440
|
i++;
|
|
384
441
|
}
|
|
385
442
|
if (workflow.status === 'running') {
|
|
386
443
|
workflow.status = 'completed';
|
|
387
444
|
workflow.completedAt = new Date().toISOString();
|
|
388
|
-
|
|
445
|
+
persistProgress();
|
|
389
446
|
}
|
|
390
447
|
return {
|
|
391
448
|
workflowId,
|
|
392
449
|
status: workflow.status,
|
|
450
|
+
...(workflow.error ? { error: workflow.error } : {}),
|
|
393
451
|
totalSteps: workflow.steps.length,
|
|
394
452
|
stepsCompleted: stepResults.filter(s => s.status === 'completed').length,
|
|
395
453
|
stepsSkipped: stepResults.filter(s => s.status === 'skipped').length,
|
|
@@ -399,7 +457,7 @@ export const workflowTools = [
|
|
|
399
457
|
completedAt: workflow.completedAt,
|
|
400
458
|
durationMs: Date.now() - startedAt,
|
|
401
459
|
};
|
|
402
|
-
},
|
|
460
|
+
}),
|
|
403
461
|
},
|
|
404
462
|
{
|
|
405
463
|
name: 'workflow_status',
|
|
@@ -560,24 +618,15 @@ export const workflowTools = [
|
|
|
560
618
|
if (workflow.status !== 'paused') {
|
|
561
619
|
return { workflowId, error: 'Workflow not paused' };
|
|
562
620
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
})
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
workflowId,
|
|
574
|
-
status: workflow.status,
|
|
575
|
-
resumed: true,
|
|
576
|
-
currentStep: workflow.currentStep,
|
|
577
|
-
remainingSteps,
|
|
578
|
-
steps: stepStates,
|
|
579
|
-
_note: 'Workflow resumed. Steps remain in their current state and must be executed via task tools.',
|
|
580
|
-
};
|
|
621
|
+
const active = workflowExecutions.get(workflowExecutionKey(workflowId));
|
|
622
|
+
if (active) {
|
|
623
|
+
workflow.status = 'running';
|
|
624
|
+
saveWorkflowStore(store);
|
|
625
|
+
return { ...(await active), resumed: true };
|
|
626
|
+
}
|
|
627
|
+
const execute = workflowTools.find(tool => tool.name === 'workflow_execute');
|
|
628
|
+
const result = await execute.handler({ workflowId, startFromStep: workflow.currentStep });
|
|
629
|
+
return { ...result, resumed: true };
|
|
581
630
|
},
|
|
582
631
|
},
|
|
583
632
|
{
|