@claude-flow/cli 3.54.1 → 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/bin/cli.js +1 -1
- package/bin/mcp-server.js +1 -1
- 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/autopilot.js +2 -2
- package/dist/src/commands/daemon.js +15 -3
- package/dist/src/commands/doctor.js +65 -58
- package/dist/src/commands/hive-mind.js +7 -0
- package/dist/src/commands/mcp.js +35 -1
- 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/policy.js +10 -2
- package/dist/src/commands/task.js +12 -0
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-server.d.ts +33 -0
- package/dist/src/mcp-server.js +96 -4
- 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/hive-mind-tools.d.ts +2 -4
- package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
- 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/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +71 -13
- package/dist/src/plugins/store/discovery.d.ts +6 -0
- package/dist/src/plugins/store/discovery.js +7 -3
- package/dist/src/production/retry.js +1 -1
- package/dist/src/ruvector/neural-router.js +7 -2
- package/dist/src/services/bounded-worker-pool.js +3 -0
- 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/config-file-manager.d.ts +3 -0
- package/dist/src/services/config-file-manager.js +24 -7
- 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/policy-ledger-anchor.d.ts +42 -0
- package/dist/src/services/policy-ledger-anchor.js +163 -0
- package/dist/src/services/policy-runtime.d.ts +11 -5
- package/dist/src/services/policy-runtime.js +70 -15
- package/dist/src/services/worker-daemon.d.ts +5 -2
- package/dist/src/services/worker-daemon.js +26 -6
- package/dist/src/services/worker-queue.js +11 -2
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
- package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- 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/dist/policy/engine.d.ts +11 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +7 -7
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
- package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
- package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
- package/.claude/agents/tmp.json +0 -0
|
@@ -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
|
{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type AppendCondition = {
|
|
2
|
+
namespace: string;
|
|
3
|
+
} & ({
|
|
4
|
+
key: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
latestPrefix?: string;
|
|
7
|
+
} | {
|
|
8
|
+
absent: true;
|
|
9
|
+
latestPrefix: string;
|
|
10
|
+
} | {
|
|
11
|
+
keysSha256: string;
|
|
12
|
+
count: number;
|
|
13
|
+
});
|
|
14
|
+
export declare class AppendConditionFailed extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare function validateAppendConditions(input: unknown): AppendCondition[];
|
|
17
|
+
interface NativeReader {
|
|
18
|
+
prepare(sql: string): {
|
|
19
|
+
all(...parameters: string[]): Array<{
|
|
20
|
+
key: string;
|
|
21
|
+
content: string;
|
|
22
|
+
status: string | null;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function assertAppendConditions(db: NativeReader, conditions: AppendCondition[]): void;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=append-conditions.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Native append predicates checked in the SAME write transaction as an immutable insert. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
export class AppendConditionFailed extends Error {
|
|
4
|
+
}
|
|
5
|
+
const text = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
|
|
6
|
+
const digest = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
7
|
+
export function validateAppendConditions(input) {
|
|
8
|
+
if (!Array.isArray(input) || input.length < 1 || input.length > 8)
|
|
9
|
+
throw new TypeError('append conditions must contain 1-8 predicates');
|
|
10
|
+
return input.map(condition => {
|
|
11
|
+
if (!condition || typeof condition !== 'object' || Array.isArray(condition))
|
|
12
|
+
throw new TypeError('invalid append condition');
|
|
13
|
+
const row = condition;
|
|
14
|
+
const fields = Object.keys(row).sort().join(',');
|
|
15
|
+
const validRecord = ['key,namespace,sha256', 'key,latestPrefix,namespace,sha256'].includes(fields)
|
|
16
|
+
&& text(row.key, 1024) && digest(row.sha256) && (row.latestPrefix === undefined || text(row.latestPrefix, 1024));
|
|
17
|
+
const validAbsent = fields === 'absent,latestPrefix,namespace' && row.absent === true && text(row.latestPrefix, 1024);
|
|
18
|
+
const validKeys = fields === 'count,keysSha256,namespace' && digest(row.keysSha256)
|
|
19
|
+
&& Number.isSafeInteger(row.count) && row.count >= 0 && row.count <= 100_000;
|
|
20
|
+
if (!text(row.namespace, 256) || !(validRecord || validAbsent || validKeys))
|
|
21
|
+
throw new TypeError('invalid append condition');
|
|
22
|
+
return row;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
26
|
+
export function assertAppendConditions(db, conditions) {
|
|
27
|
+
for (const condition of conditions) {
|
|
28
|
+
if ('keysSha256' in condition) {
|
|
29
|
+
const rows = db.prepare("SELECT key FROM memory_entries WHERE namespace = ? AND (status = 'active' OR status IS NULL)")
|
|
30
|
+
.all(condition.namespace);
|
|
31
|
+
const keys = rows.map(row => row.key).sort();
|
|
32
|
+
if (keys.length !== condition.count || sha(JSON.stringify(keys)) !== condition.keysSha256) {
|
|
33
|
+
throw new AppendConditionFailed('immutable append rejected: namespace membership changed');
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if ('key' in condition) {
|
|
38
|
+
const rows = db.prepare('SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key = ?')
|
|
39
|
+
.all(condition.namespace, condition.key);
|
|
40
|
+
if (rows.length !== 1 || !['active', null].includes(rows[0].status)
|
|
41
|
+
|| typeof rows[0].content !== 'string' || sha(rows[0].content) !== condition.sha256) {
|
|
42
|
+
throw new AppendConditionFailed('immutable append rejected: expected parent/authority bytes changed or absent');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (condition.latestPrefix !== undefined) {
|
|
46
|
+
// Include tombstones: deleting a new authority slot must never rewind to an older authority.
|
|
47
|
+
const pattern = condition.latestPrefix.replace(/[\\%_]/g, character => `\\${character}`) + '%';
|
|
48
|
+
const latest = db.prepare("SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key LIKE ? ESCAPE '\\' ORDER BY key COLLATE BINARY DESC LIMIT 1")
|
|
49
|
+
.all(condition.namespace, pattern);
|
|
50
|
+
if ('absent' in condition ? latest.length !== 0 : latest.length !== 1 || latest[0].key !== condition.key) {
|
|
51
|
+
throw new AppendConditionFailed('immutable append rejected: newer parent/authority slot exists');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=append-conditions.js.map
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
*
|
|
30
30
|
* @module v3/cli/memory/graph-edge-writer
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* True iff the memory.db bridge at `customDbPath` (or the default location)
|
|
34
|
+
* is currently RFE1-encrypted-at-rest. Callers that receive `null` from
|
|
35
|
+
* getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
|
|
36
|
+
* destructive `memory init` command for this specific, non-corrupt case.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isBridgeDbEncryptedAtRest(customDbPath?: string): boolean;
|
|
39
|
+
/** Test-only: clear the once-per-path warning dedup set. */
|
|
40
|
+
export declare function _resetRfe1WarningFlag(): void;
|
|
32
41
|
/**
|
|
33
42
|
* Checkpoint and close the cached handle if it is open (optionally only if it
|
|
34
43
|
* is open on `dbPath`). Returns true if a handle was released. Never throws.
|