@claude-flow/cli 3.71.1 โ 3.71.3
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/auto-memory-hook.mjs +208 -54
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/settings.json +2 -1
- package/README.md +2 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/daemon.js +20 -1
- package/dist/src/commands/embeddings.js +6 -6
- package/dist/src/commands/hive-mind-worker.d.ts +10 -0
- package/dist/src/commands/hive-mind-worker.js +20 -0
- package/dist/src/commands/hive-mind.js +27 -4
- package/dist/src/commands/init.js +4 -0
- package/dist/src/commands/session.js +10 -7
- package/dist/src/init/executor.js +15 -5
- package/dist/src/init/settings-generator.js +4 -0
- package/dist/src/init/types.d.ts +2 -0
- package/dist/src/init/types.js +4 -1
- package/dist/src/mcp-tools/agentdb-tools.js +3 -3
- package/dist/src/mcp-tools/claims-tools.js +23 -10
- package/dist/src/mcp-tools/daa-tools.js +22 -2
- package/dist/src/mcp-tools/embeddings-tools.js +2 -1
- package/dist/src/mcp-tools/hooks-tools.js +11 -1
- package/dist/src/mcp-tools/session-tools.js +42 -4
- package/dist/src/mcp-tools/task-tools.js +11 -0
- package/dist/src/mcp-tools/terminal-tools.js +5 -0
- package/dist/src/mcp-tools/workflow-tools.js +22 -3
- package/dist/src/memory/memory-bridge.js +29 -5
- package/dist/src/memory/memory-initializer.js +32 -10
- package/dist/src/missions/schemas.d.ts +10 -10
- package/dist/src/mods/headless-plugins.d.ts +16 -0
- package/dist/src/mods/headless-plugins.js +30 -1
- package/dist/src/services/claims-store.js +5 -2
- package/dist/src/services/daemon-autostart.d.ts +24 -0
- package/dist/src/services/daemon-autostart.js +55 -1
- package/dist/src/services/headless-worker-executor.d.ts +15 -0
- package/dist/src/services/headless-worker-executor.js +28 -0
- package/package.json +1 -1
- package/plugins/ruflo-console/dist/tui.mjs +134 -134
|
@@ -10,7 +10,7 @@ import { select, confirm, input } from '../prompt.js';
|
|
|
10
10
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
11
11
|
import { spawn as childSpawn } from 'child_process';
|
|
12
12
|
import { mkdir, writeFile } from 'fs/promises';
|
|
13
|
-
import { resolveWorkerMcpConfig, currentRufloServer, workCommand } from './hive-mind-worker.js';
|
|
13
|
+
import { resolveWorkerMcpConfig, resolveWorkerModel, currentRufloServer, workCommand } from './hive-mind-worker.js';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
|
|
16
16
|
import { withHeadlessPlugins } from '../mods/headless-plugins.js';
|
|
@@ -168,6 +168,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
168
168
|
const workerGroups = groupWorkersByType(workers);
|
|
169
169
|
const hiveMindPrompt = generateHiveMindPrompt(swarmId, swarmName, objective, workers, workerGroups, flags);
|
|
170
170
|
spinner.succeed('Hive Mind coordination prompt ready!');
|
|
171
|
+
const { model: workerModel } = resolveWorkerModel(flags);
|
|
171
172
|
// Display coordination summary
|
|
172
173
|
output.writeln();
|
|
173
174
|
output.writeln(output.bold('๐ง Hive Mind Configuration'));
|
|
@@ -179,6 +180,7 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
179
180
|
`Worker Count: ${output.highlight(String(workers.length))}`,
|
|
180
181
|
`Worker Types: ${output.highlight(Object.keys(workerGroups).join(', '))}`,
|
|
181
182
|
`Consensus: ${output.highlight(flags.consensus || 'byzantine')}`,
|
|
183
|
+
`Model: ${output.highlight(workerModel ?? 'Claude Code default')}`,
|
|
182
184
|
`MCP Tools: ${output.success('Full Claude-Flow integration enabled')}`
|
|
183
185
|
]);
|
|
184
186
|
// Ensure sessions directory exists
|
|
@@ -249,6 +251,10 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
249
251
|
output.printWarning('Using --dangerously-skip-permissions for seamless hive-mind execution');
|
|
250
252
|
}
|
|
251
253
|
}
|
|
254
|
+
// #1870: without --model the argv is unchanged and Claude Code picks its default.
|
|
255
|
+
if (workerModel) {
|
|
256
|
+
claudeArgs.push('--model', workerModel);
|
|
257
|
+
}
|
|
252
258
|
// ADR-510 ยง2.2: a non-interactive (-p) run is a headless worker and loads
|
|
253
259
|
// no marketplace plugins on a fresh config, so stage the worker profile
|
|
254
260
|
// (default ruflo-mods) as --plugin-dir before the prompt. The interactive
|
|
@@ -344,11 +350,12 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
344
350
|
output.writeln();
|
|
345
351
|
output.writeln(output.bold('๐ Manual Execution Instructions:'));
|
|
346
352
|
output.writeln(output.dim('โ'.repeat(50)));
|
|
353
|
+
const modelArg = workerModel ? ` --model ${workerModel}` : '';
|
|
347
354
|
output.printList([
|
|
348
355
|
'Install Claude Code: npm install -g @anthropic-ai/claude-code',
|
|
349
|
-
`Run with saved prompt: claude < ${promptFile}`,
|
|
350
|
-
`Or copy manually: cat ${promptFile} | claude`,
|
|
351
|
-
`With auto-permissions: claude --dangerously-skip-permissions < ${promptFile}`
|
|
356
|
+
`Run with saved prompt: claude${modelArg} < ${promptFile}`,
|
|
357
|
+
`Or copy manually: cat ${promptFile} | claude${modelArg}`,
|
|
358
|
+
`With auto-permissions: claude${modelArg} --dangerously-skip-permissions < ${promptFile}`
|
|
352
359
|
]);
|
|
353
360
|
return { success: true, promptFile };
|
|
354
361
|
}
|
|
@@ -529,6 +536,12 @@ const spawnCommand = {
|
|
|
529
536
|
description: 'Objective for the hive mind (used with --claude)',
|
|
530
537
|
type: 'string'
|
|
531
538
|
},
|
|
539
|
+
{
|
|
540
|
+
name: 'model',
|
|
541
|
+
short: 'm',
|
|
542
|
+
description: 'Model for the Claude Code worker started by --claude, passed as claude --model (e.g. sonnet, opus, or a full model id; default: the Claude Code default)',
|
|
543
|
+
type: 'string'
|
|
544
|
+
},
|
|
532
545
|
{
|
|
533
546
|
name: 'dangerously-skip-permissions',
|
|
534
547
|
description: 'Skip permission prompts in Claude Code (use with caution)',
|
|
@@ -564,6 +577,7 @@ const spawnCommand = {
|
|
|
564
577
|
{ command: 'claude-flow hive-mind spawn -n 3 -r specialist', description: 'Spawn 3 specialists' },
|
|
565
578
|
{ command: 'claude-flow hive-mind spawn -t coder -p my-coder', description: 'Spawn coder with custom prefix' },
|
|
566
579
|
{ command: 'claude-flow hive-mind spawn --claude -o "Build a REST API"', description: 'Launch Claude Code with objective' },
|
|
580
|
+
{ command: 'claude-flow hive-mind spawn --claude -m sonnet -o "Build a REST API"', description: 'Launch Claude Code on a pinned model' },
|
|
567
581
|
{ command: 'claude-flow hive-mind spawn -n 5 --claude -o "Research AI patterns"', description: 'Spawn workers and launch Claude Code' }
|
|
568
582
|
],
|
|
569
583
|
action: async (ctx) => {
|
|
@@ -574,6 +588,15 @@ const spawnCommand = {
|
|
|
574
588
|
const prefix = ctx.flags.prefix || 'hive-worker';
|
|
575
589
|
const launchClaude = ctx.flags.claude;
|
|
576
590
|
let objective = ctx.flags.objective || ctx.args.join(' ');
|
|
591
|
+
// #1870: refuse a bad --model before any worker is spawned.
|
|
592
|
+
const workerModel = resolveWorkerModel(ctx.flags);
|
|
593
|
+
if (workerModel.error) {
|
|
594
|
+
output.printError(workerModel.error);
|
|
595
|
+
return { success: false, exitCode: 1 };
|
|
596
|
+
}
|
|
597
|
+
if (workerModel.model && !launchClaude) {
|
|
598
|
+
output.printWarning('--model only applies with --claude; ignoring it');
|
|
599
|
+
}
|
|
577
600
|
output.printInfo(`Spawning ${count} ${role} agent(s)...`);
|
|
578
601
|
try {
|
|
579
602
|
const result = await callMCPTool('hive-mind_spawn', {
|
|
@@ -1091,6 +1091,7 @@ const wizardCommand = {
|
|
|
1091
1091
|
{ value: 'github', label: 'GitHub', hint: 'GitHub integration skills', selected: true },
|
|
1092
1092
|
{ value: 'flowNexus', label: 'Flow Nexus', hint: 'Cloud platform skills', selected: false },
|
|
1093
1093
|
{ value: 'v3', label: 'V3', hint: 'V3 implementation skills', selected: true },
|
|
1094
|
+
{ value: 'legacy', label: 'Legacy', hint: 'Skills that document commands v3 does not ship', selected: false },
|
|
1094
1095
|
],
|
|
1095
1096
|
});
|
|
1096
1097
|
options.skills.core = skillSets.includes('core');
|
|
@@ -1098,6 +1099,7 @@ const wizardCommand = {
|
|
|
1098
1099
|
options.skills.github = skillSets.includes('github');
|
|
1099
1100
|
options.skills.flowNexus = skillSets.includes('flowNexus');
|
|
1100
1101
|
options.skills.v3 = skillSets.includes('v3');
|
|
1102
|
+
options.skills.legacy = skillSets.includes('legacy');
|
|
1101
1103
|
}
|
|
1102
1104
|
// Hooks selection
|
|
1103
1105
|
if (options.components.settings) {
|
|
@@ -1354,6 +1356,7 @@ const skillsCommand = {
|
|
|
1354
1356
|
{ name: 'agentdb', description: 'Install AgentDB skills', type: 'boolean', default: false },
|
|
1355
1357
|
{ name: 'github', description: 'Install GitHub skills', type: 'boolean', default: false },
|
|
1356
1358
|
{ name: 'v3', description: 'Install V3 skills', type: 'boolean', default: false },
|
|
1359
|
+
{ name: 'legacy', description: 'Install skills that document commands v3 does not ship (stream-chain, sparc)', type: 'boolean', default: false },
|
|
1357
1360
|
],
|
|
1358
1361
|
action: async (ctx) => {
|
|
1359
1362
|
const options = {
|
|
@@ -1380,6 +1383,7 @@ const skillsCommand = {
|
|
|
1380
1383
|
browser: false,
|
|
1381
1384
|
v3: ctx.flags.v3,
|
|
1382
1385
|
dualMode: false,
|
|
1386
|
+
legacy: ctx.flags.legacy,
|
|
1383
1387
|
},
|
|
1384
1388
|
};
|
|
1385
1389
|
const spinner = output.createSpinner({ text: 'Installing skills...' });
|
|
@@ -211,9 +211,9 @@ const saveCommand = {
|
|
|
211
211
|
const result = await callMCPTool('session_save', {
|
|
212
212
|
name: sessionName,
|
|
213
213
|
description,
|
|
214
|
-
includeMemory: ctx.flags['include-memory'] !== false,
|
|
215
|
-
includeAgents: ctx.flags['include-agents'] !== false,
|
|
216
|
-
includeTasks: ctx.flags['include-tasks'] !== false
|
|
214
|
+
includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false,
|
|
215
|
+
includeAgents: ctx.flags.includeAgents !== false && ctx.flags['include-agents'] !== false,
|
|
216
|
+
includeTasks: ctx.flags.includeTasks !== false && ctx.flags['include-tasks'] !== false
|
|
217
217
|
});
|
|
218
218
|
const failure = toolError(result);
|
|
219
219
|
if (failure) {
|
|
@@ -342,9 +342,12 @@ const restoreCommand = {
|
|
|
342
342
|
spinner.start();
|
|
343
343
|
try {
|
|
344
344
|
// Determine what to restore
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
345
|
+
const memoryOnly = ctx.flags.memoryOnly === true || ctx.flags['memory-only'] === true;
|
|
346
|
+
const agentsOnly = ctx.flags.agentsOnly === true || ctx.flags['agents-only'] === true;
|
|
347
|
+
const tasksOnly = ctx.flags.tasksOnly === true || ctx.flags['tasks-only'] === true;
|
|
348
|
+
const restoreMemory = !agentsOnly && !tasksOnly;
|
|
349
|
+
const restoreAgents = !memoryOnly && !tasksOnly;
|
|
350
|
+
const restoreTasks = !memoryOnly && !agentsOnly;
|
|
348
351
|
const result = await callMCPTool('session_restore', {
|
|
349
352
|
sessionId,
|
|
350
353
|
restoreMemory,
|
|
@@ -552,7 +555,7 @@ const exportCommand = {
|
|
|
552
555
|
try {
|
|
553
556
|
const result = await callMCPTool('session_export', {
|
|
554
557
|
sessionId,
|
|
555
|
-
includeMemory: ctx.flags['include-memory'] !== false
|
|
558
|
+
includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false
|
|
556
559
|
});
|
|
557
560
|
const failure = toolError(result);
|
|
558
561
|
if (failure || result.data === undefined) {
|
|
@@ -29,11 +29,9 @@ const SKILLS_MAP = {
|
|
|
29
29
|
core: [
|
|
30
30
|
'swarm-orchestration',
|
|
31
31
|
'swarm-advanced',
|
|
32
|
-
'sparc-methodology',
|
|
33
32
|
'hooks-automation',
|
|
34
33
|
'pair-programming',
|
|
35
34
|
'verification-quality',
|
|
36
|
-
'stream-chain',
|
|
37
35
|
'skill-builder',
|
|
38
36
|
],
|
|
39
37
|
browser: ['browser'], // agent-browser integration
|
|
@@ -47,12 +45,11 @@ const SKILLS_MAP = {
|
|
|
47
45
|
'reasoningbank-agentdb',
|
|
48
46
|
'reasoningbank-intelligence',
|
|
49
47
|
],
|
|
48
|
+
// github-code-review and github-project-management only use hooks, which v3
|
|
49
|
+
// ships; the other three drive `ruflo github`, so they live in skills.legacy.
|
|
50
50
|
github: [
|
|
51
51
|
'github-code-review',
|
|
52
|
-
'github-multi-repo',
|
|
53
52
|
'github-project-management',
|
|
54
|
-
'github-release-management',
|
|
55
|
-
'github-workflow-automation',
|
|
56
53
|
],
|
|
57
54
|
flowNexus: [
|
|
58
55
|
'flow-nexus-neural',
|
|
@@ -70,6 +67,17 @@ const SKILLS_MAP = {
|
|
|
70
67
|
'v3-security-overhaul',
|
|
71
68
|
'v3-swarm-coordination',
|
|
72
69
|
],
|
|
70
|
+
// ADR-128 / #4539: skills that document v2-era verbs v3 does not ship
|
|
71
|
+
// (`stream-chain`, `sparc`, `github`). Their examples fail with `Unknown
|
|
72
|
+
// command`, so they are opt-in like skills.v3 instead of being installed by
|
|
73
|
+
// default.
|
|
74
|
+
legacy: [
|
|
75
|
+
'github-multi-repo',
|
|
76
|
+
'github-release-management',
|
|
77
|
+
'github-workflow-automation',
|
|
78
|
+
'sparc-methodology',
|
|
79
|
+
'stream-chain',
|
|
80
|
+
],
|
|
73
81
|
};
|
|
74
82
|
/**
|
|
75
83
|
* Commands to copy based on configuration
|
|
@@ -1077,6 +1085,8 @@ async function copySkills(targetDir, options, result) {
|
|
|
1077
1085
|
skillsToCopy.push(...SKILLS_MAP.v3);
|
|
1078
1086
|
if (skillsConfig.dualMode)
|
|
1079
1087
|
skillsToCopy.push(...SKILLS_MAP.dualMode);
|
|
1088
|
+
if (skillsConfig.legacy)
|
|
1089
|
+
skillsToCopy.push(...SKILLS_MAP.legacy);
|
|
1080
1090
|
}
|
|
1081
1091
|
// Find source skills directory
|
|
1082
1092
|
const sourceSkillsDir = findSourceDir('skills', options.sourceBaseDir);
|
|
@@ -210,6 +210,10 @@ function generateHooksConfig(config, scope) {
|
|
|
210
210
|
type: 'command',
|
|
211
211
|
command: hookHandlerCmd('pre-bash', scope),
|
|
212
212
|
timeout: config.timeout,
|
|
213
|
+
// Claude Code 2.1.295+: if the safety gate can't start, times out or
|
|
214
|
+
// exits with an unexpected code, block the command instead of letting
|
|
215
|
+
// it run. Older versions ignore the key.
|
|
216
|
+
onFailure: 'block',
|
|
213
217
|
},
|
|
214
218
|
],
|
|
215
219
|
},
|
package/dist/src/init/types.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface SkillsConfig {
|
|
|
76
76
|
v3: boolean;
|
|
77
77
|
/** Include dual-mode skills (Claude Code + Codex hybrid) */
|
|
78
78
|
dualMode: boolean;
|
|
79
|
+
/** Include legacy skills that document commands v3 does not ship (opt-in) */
|
|
80
|
+
legacy: boolean;
|
|
79
81
|
/** Include all available skills */
|
|
80
82
|
all: boolean;
|
|
81
83
|
}
|
package/dist/src/init/types.js
CHANGED
|
@@ -73,11 +73,12 @@ export const DEFAULT_INIT_OPTIONS = {
|
|
|
73
73
|
skills: {
|
|
74
74
|
core: true,
|
|
75
75
|
agentdb: true,
|
|
76
|
-
github: true,
|
|
76
|
+
github: true, // #4539: only the hook-only GitHub skills remain; the rest are skills.legacy
|
|
77
77
|
flowNexus: false,
|
|
78
78
|
browser: true,
|
|
79
79
|
v3: false, // ADR-128: opt-in like agents.v3; use --full or `init skills --v3`
|
|
80
80
|
dualMode: false, // Optional: enable with --dual flag
|
|
81
|
+
legacy: false, // #4539: stream-chain/sparc document verbs v3 does not ship
|
|
81
82
|
all: false,
|
|
82
83
|
},
|
|
83
84
|
commands: {
|
|
@@ -186,6 +187,7 @@ export const MINIMAL_INIT_OPTIONS = {
|
|
|
186
187
|
browser: false,
|
|
187
188
|
v3: false,
|
|
188
189
|
dualMode: false,
|
|
190
|
+
legacy: false,
|
|
189
191
|
all: false,
|
|
190
192
|
},
|
|
191
193
|
agents: {
|
|
@@ -246,6 +248,7 @@ export const FULL_INIT_OPTIONS = {
|
|
|
246
248
|
browser: true,
|
|
247
249
|
v3: true,
|
|
248
250
|
dualMode: true, // Include in full init
|
|
251
|
+
legacy: true, // Include in full init
|
|
249
252
|
all: true,
|
|
250
253
|
},
|
|
251
254
|
commands: {
|
|
@@ -985,12 +985,12 @@ export const agentdbBatch = {
|
|
|
985
985
|
// ===== agentdb_context_synthesize โ Synthesize context from memories =====
|
|
986
986
|
export const agentdbContextSynthesize = {
|
|
987
987
|
name: 'agentdb_context-synthesize',
|
|
988
|
-
description: '
|
|
988
|
+
description: 'Summarize validated task episodes (task, reward, success, critique) that match a query: observed success rate, reward and critique-derived patterns. Ordinary documents, research notes and graph entities are not episodes, so for them it answers no-memories or no-eligible-episodes; read those with memory_search or memory_retrieve instead. Use when generic memory_* tools are wrong because you need AgentDB-specific controllers (HNSW vector search, hierarchical tiers, causal-graph links, pattern store/recall, RaBitQ quantization). For simple key-value persistence, memory_store/memory_retrieve are simpler. For unrelated file work, native Read/Write are fine.',
|
|
989
989
|
inputSchema: {
|
|
990
990
|
type: 'object',
|
|
991
991
|
properties: {
|
|
992
|
-
query: { type: 'string', description: '
|
|
993
|
-
maxEntries: { type: 'number', description: 'Maximum
|
|
992
|
+
query: { type: 'string', description: 'Task to find matching validated episodes for' },
|
|
993
|
+
maxEntries: { type: 'number', description: 'Maximum episodes to include (default: 10)' },
|
|
994
994
|
},
|
|
995
995
|
required: ['query'],
|
|
996
996
|
},
|
|
@@ -36,6 +36,10 @@ function formatClaimant(claimant) {
|
|
|
36
36
|
? `human:${claimant.userId}:${claimant.name}`
|
|
37
37
|
: `agent:${claimant.agentId}:${claimant.agentType}`;
|
|
38
38
|
}
|
|
39
|
+
/** An agent's claim that is still work in hand (not completed). */
|
|
40
|
+
function isOpenAgentClaim(claim) {
|
|
41
|
+
return claim.claimant.type === 'agent' && claim.status !== 'completed';
|
|
42
|
+
}
|
|
39
43
|
function parseClaimant(str) {
|
|
40
44
|
const parts = str.split(':');
|
|
41
45
|
if (parts[0] === 'human' && parts.length >= 3) {
|
|
@@ -274,6 +278,10 @@ export const claimsTools = [
|
|
|
274
278
|
if (progress !== undefined) {
|
|
275
279
|
claim.progress = Math.min(100, Math.max(0, progress));
|
|
276
280
|
}
|
|
281
|
+
// A claim being handed to a named claimant is no longer up for grabs:
|
|
282
|
+
// drop it from the list claims_stealable and claims_steal read, so it
|
|
283
|
+
// cannot be stolen while the handoff is pending or after it is accepted.
|
|
284
|
+
delete store.stealable[issueId];
|
|
277
285
|
store.claims[issueId] = claim;
|
|
278
286
|
saveClaims(store);
|
|
279
287
|
return {
|
|
@@ -402,6 +410,10 @@ export const claimsTools = [
|
|
|
402
410
|
if (progress !== undefined) {
|
|
403
411
|
claim.progress = Math.min(100, Math.max(0, progress));
|
|
404
412
|
}
|
|
413
|
+
// claims_status never sets 'stealable', so a claim it updates is no
|
|
414
|
+
// longer up for grabs: drop it from the list claims_stealable and
|
|
415
|
+
// claims_steal read.
|
|
416
|
+
delete store.stealable[issueId];
|
|
405
417
|
store.claims[issueId] = claim;
|
|
406
418
|
saveClaims(store);
|
|
407
419
|
return {
|
|
@@ -592,7 +604,9 @@ export const claimsTools = [
|
|
|
592
604
|
claim.claimant = stealer;
|
|
593
605
|
claim.status = 'active';
|
|
594
606
|
claim.statusChangedAt = now;
|
|
595
|
-
|
|
607
|
+
// The context given to claims_mark-stealable is the handoff note for the
|
|
608
|
+
// stealer; without one, keep the context recorded with the claim.
|
|
609
|
+
claim.context = stealableInfo.context ?? claim.context;
|
|
596
610
|
delete store.stealable[issueId];
|
|
597
611
|
store.claims[issueId] = claim;
|
|
598
612
|
saveClaims(store);
|
|
@@ -672,12 +686,12 @@ export const claimsTools = [
|
|
|
672
686
|
return { success: false, error: v.error };
|
|
673
687
|
}
|
|
674
688
|
const store = loadClaims();
|
|
675
|
-
|
|
689
|
+
// A completed claim stays in the store but is no longer work in hand,
|
|
690
|
+
// so it does not count toward an agent's load.
|
|
691
|
+
const openAgentClaims = Object.values(store.claims).filter(isOpenAgentClaim);
|
|
676
692
|
// Group claims by agent
|
|
677
693
|
const agentLoads = new Map();
|
|
678
|
-
for (const claim of
|
|
679
|
-
if (claim.claimant.type !== 'agent')
|
|
680
|
-
continue;
|
|
694
|
+
for (const claim of openAgentClaims) {
|
|
681
695
|
const key = claim.claimant.agentId;
|
|
682
696
|
if (!agentLoads.has(key)) {
|
|
683
697
|
agentLoads.set(key, {
|
|
@@ -717,7 +731,7 @@ export const claimsTools = [
|
|
|
717
731
|
success: true,
|
|
718
732
|
loads: result,
|
|
719
733
|
totalAgents: result.length,
|
|
720
|
-
totalClaims:
|
|
734
|
+
totalClaims: openAgentClaims.length,
|
|
721
735
|
avgUtilization: result.length > 0
|
|
722
736
|
? result.reduce((sum, l) => sum + l.utilization, 0) / result.length
|
|
723
737
|
: 0,
|
|
@@ -796,12 +810,11 @@ export const claimsTools = [
|
|
|
796
810
|
const dryRun = input.dryRun !== false;
|
|
797
811
|
const targetUtilization = input.targetUtilization || 0.7;
|
|
798
812
|
const store = loadClaims();
|
|
799
|
-
|
|
813
|
+
// Completed claims are not load, as in claims_load.
|
|
814
|
+
const openAgentClaims = Object.values(store.claims).filter(isOpenAgentClaim);
|
|
800
815
|
// Group by agent
|
|
801
816
|
const agentLoads = new Map();
|
|
802
|
-
for (const claim of
|
|
803
|
-
if (claim.claimant.type !== 'agent')
|
|
804
|
-
continue;
|
|
817
|
+
for (const claim of openAgentClaims) {
|
|
805
818
|
const key = claim.claimant.agentId;
|
|
806
819
|
if (!agentLoads.has(key)) {
|
|
807
820
|
agentLoads.set(key, { agentId: key, agentType: claim.claimant.agentType, claims: [] });
|
|
@@ -28,17 +28,37 @@ function ensureDAADir() {
|
|
|
28
28
|
mkdirSync(dir, { recursive: true });
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* SECURITY: record maps are keyed by ids taken from MCP tool input, and
|
|
33
|
+
* validateIdentifier accepts names like `__proto__` and `constructor`. On a
|
|
34
|
+
* plain object those resolve to Object.prototype or a built-in, which a handler
|
|
35
|
+
* would then write into. Null-prototype maps only ever return own records.
|
|
36
|
+
*/
|
|
37
|
+
function withRecordMaps(store, keys) {
|
|
38
|
+
const maps = store;
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const map = maps[key];
|
|
41
|
+
if (map && typeof map === 'object' && !Array.isArray(map))
|
|
42
|
+
Object.setPrototypeOf(map, null);
|
|
43
|
+
else
|
|
44
|
+
maps[key] = Object.create(null);
|
|
45
|
+
}
|
|
46
|
+
return store;
|
|
47
|
+
}
|
|
48
|
+
const DAA_MAPS = ['agents', 'workflows', 'knowledge'];
|
|
31
49
|
function loadDAAStore() {
|
|
32
50
|
try {
|
|
33
51
|
const path = getDAAPath();
|
|
34
52
|
if (existsSync(path)) {
|
|
35
|
-
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
54
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
55
|
+
return withRecordMaps(parsed, DAA_MAPS);
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
catch {
|
|
39
59
|
// Return empty store
|
|
40
60
|
}
|
|
41
|
-
return { agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' };
|
|
61
|
+
return withRecordMaps({ agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' }, DAA_MAPS);
|
|
42
62
|
}
|
|
43
63
|
function saveDAAStore(store) {
|
|
44
64
|
ensureDAADir();
|
|
@@ -425,7 +425,8 @@ export const embeddingsTools = [
|
|
|
425
425
|
}
|
|
426
426
|
const query = input.query;
|
|
427
427
|
const topK = input.topK || 5;
|
|
428
|
-
|
|
428
|
+
// `??`, not `||`: threshold 0 is a valid request for every match, as in memory_search.
|
|
429
|
+
const threshold = input.threshold ?? 0.5;
|
|
429
430
|
const namespace = input.namespace;
|
|
430
431
|
{
|
|
431
432
|
const v = validateText(query, 'query');
|
|
@@ -4096,11 +4096,21 @@ export const hooksIntelligenceLearn = {
|
|
|
4096
4096
|
inputSchema: {
|
|
4097
4097
|
type: 'object',
|
|
4098
4098
|
properties: {
|
|
4099
|
-
trajectoryIds: { type: 'array', items: { type: 'string' }, description: '
|
|
4099
|
+
trajectoryIds: { type: 'array', items: { type: 'string' }, description: 'Not supported: a learning cycle always covers every recorded trajectory. Supplying this returns an error and runs nothing, rather than learning from more than was asked (#4693).' },
|
|
4100
4100
|
consolidate: { type: 'boolean', description: 'Run EWC++ consolidation' },
|
|
4101
4101
|
},
|
|
4102
4102
|
},
|
|
4103
4103
|
handler: async (params) => {
|
|
4104
|
+
// #4693: distillLearning() and the background pass have no way to select
|
|
4105
|
+
// trajectories, so a request scoped to some must not trigger learning over all.
|
|
4106
|
+
// Refuse it before anything is imported or mutated; omitting the field is unchanged.
|
|
4107
|
+
if (params.trajectoryIds !== undefined) {
|
|
4108
|
+
return {
|
|
4109
|
+
success: false,
|
|
4110
|
+
learned: false,
|
|
4111
|
+
error: 'hooks_intelligence_learn: trajectoryIds is not supported; a learning cycle always covers every recorded trajectory. Omit trajectoryIds to run it.',
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4104
4114
|
const consolidate = params.consolidate !== false;
|
|
4105
4115
|
const startTime = Date.now();
|
|
4106
4116
|
// AUDIT FIX #5: actually TRIGGER a learning/consolidation cycle instead of
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tool definitions for session management with file persistence.
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { gunzipSync } from 'node:zlib';
|
|
9
|
-
import { join } from 'node:path';
|
|
9
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } 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';
|
|
@@ -351,6 +351,41 @@ function isSessionRecordLike(value) {
|
|
|
351
351
|
const v = value;
|
|
352
352
|
return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
|
|
353
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* SECURITY: `outputPath` comes from MCP tool input. Resolve it against the
|
|
356
|
+
* project root and require the real (symlink-resolved) parent directory to stay
|
|
357
|
+
* inside the real project root, and refuse an existing symlink as the target,
|
|
358
|
+
* so session_export cannot write files elsewhere on the host. The CLI's
|
|
359
|
+
* `session export` writes its own file and does not pass outputPath.
|
|
360
|
+
*/
|
|
361
|
+
function containedExportPath(outputPath) {
|
|
362
|
+
if (!outputPath || outputPath.includes('\0'))
|
|
363
|
+
return null;
|
|
364
|
+
try {
|
|
365
|
+
const root = realpathSync(getProjectCwd());
|
|
366
|
+
const abs = resolve(root, outputPath);
|
|
367
|
+
const parent = realpathSync(dirname(abs));
|
|
368
|
+
const target = join(parent, basename(abs));
|
|
369
|
+
const rel = relative(root, target);
|
|
370
|
+
if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
371
|
+
return null;
|
|
372
|
+
const existing = lstatOrNull(target);
|
|
373
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile()))
|
|
374
|
+
return null;
|
|
375
|
+
return target;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function lstatOrNull(p) {
|
|
382
|
+
try {
|
|
383
|
+
return lstatSync(p);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
354
389
|
export const sessionTools = [
|
|
355
390
|
{
|
|
356
391
|
name: 'session_save',
|
|
@@ -705,9 +740,12 @@ export const sessionTools = [
|
|
|
705
740
|
let path = null;
|
|
706
741
|
const outputPath = input.outputPath ? String(input.outputPath) : null;
|
|
707
742
|
if (outputPath) {
|
|
743
|
+
const target = containedExportPath(outputPath);
|
|
744
|
+
if (!target)
|
|
745
|
+
return { sessionId, error: 'outputPath must be a file inside the project directory (no symlinks)' };
|
|
708
746
|
try {
|
|
709
|
-
writeFileSync(
|
|
710
|
-
path =
|
|
747
|
+
writeFileSync(target, JSON.stringify(session, null, 2), 'utf-8');
|
|
748
|
+
path = target;
|
|
711
749
|
}
|
|
712
750
|
catch (e) {
|
|
713
751
|
return { sessionId, error: `Could not write ${outputPath}: ${e.message}` };
|
|
@@ -117,6 +117,10 @@ export const taskTools = [
|
|
|
117
117
|
const vDesc = validateText(input.description, 'description');
|
|
118
118
|
if (!vDesc.valid)
|
|
119
119
|
return { success: false, error: vDesc.error };
|
|
120
|
+
if (input.assignTo !== undefined && (!Array.isArray(input.assignTo)
|
|
121
|
+
|| !input.assignTo.every(id => validateIdentifier(id, 'assignTo').valid))) {
|
|
122
|
+
return { success: false, error: 'assignTo must be an array of agent ids' };
|
|
123
|
+
}
|
|
120
124
|
const store = loadTaskStore();
|
|
121
125
|
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
122
126
|
const task = {
|
|
@@ -134,6 +138,13 @@ export const taskTools = [
|
|
|
134
138
|
};
|
|
135
139
|
store.tasks[taskId] = task;
|
|
136
140
|
saveTaskStore(store);
|
|
141
|
+
// Assigning at creation must mark the workers busy, as task_assign and task_update do;
|
|
142
|
+
// otherwise they read idle while holding the task, and task_complete or task_cancel,
|
|
143
|
+
// which only release a worker whose currentTask is this task, never free them either.
|
|
144
|
+
updateAgents(task.assignedTo, (agent) => {
|
|
145
|
+
agent.status = 'busy';
|
|
146
|
+
agent.currentTask = taskId;
|
|
147
|
+
});
|
|
137
148
|
return {
|
|
138
149
|
taskId,
|
|
139
150
|
type: task.type,
|
|
@@ -132,6 +132,11 @@ export const terminalTools = [
|
|
|
132
132
|
const store = loadTerminalStore();
|
|
133
133
|
const sessionId = input.sessionId;
|
|
134
134
|
const command = input.command;
|
|
135
|
+
// An explicit sessionId must name a stored session; only fall back to
|
|
136
|
+
// (or create) the default session when no sessionId was given.
|
|
137
|
+
if (sessionId && !store.sessions[sessionId]) {
|
|
138
|
+
return { success: false, error: 'Session not found' };
|
|
139
|
+
}
|
|
135
140
|
// Find or create default session
|
|
136
141
|
let session = sessionId ? store.sessions[sessionId] : Object.values(store.sessions).find(s => s.status === 'active');
|
|
137
142
|
if (!session) {
|
|
@@ -55,18 +55,37 @@ function ensureWorkflowDir() {
|
|
|
55
55
|
mkdirSync(dir, { recursive: true });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* SECURITY: record maps are keyed by ids taken from MCP tool input, and
|
|
60
|
+
* validateIdentifier accepts names like `__proto__` and `constructor`. On a
|
|
61
|
+
* plain object those resolve to Object.prototype or a built-in, which a handler
|
|
62
|
+
* would then write into. Null-prototype maps only ever return own records.
|
|
63
|
+
*/
|
|
64
|
+
function withRecordMaps(store, keys) {
|
|
65
|
+
const maps = store;
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const map = maps[key];
|
|
68
|
+
if (map && typeof map === 'object' && !Array.isArray(map))
|
|
69
|
+
Object.setPrototypeOf(map, null);
|
|
70
|
+
else
|
|
71
|
+
maps[key] = Object.create(null);
|
|
72
|
+
}
|
|
73
|
+
return store;
|
|
74
|
+
}
|
|
75
|
+
const WORKFLOW_MAPS = ['workflows', 'templates'];
|
|
58
76
|
function loadWorkflowStore() {
|
|
59
77
|
try {
|
|
60
78
|
const path = getWorkflowPath();
|
|
61
79
|
if (existsSync(path)) {
|
|
62
|
-
const data = readFileSync(path, 'utf-8');
|
|
63
|
-
|
|
80
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
81
|
+
if (data && typeof data === 'object' && !Array.isArray(data))
|
|
82
|
+
return withRecordMaps(data, WORKFLOW_MAPS);
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
catch {
|
|
67
86
|
// Return default store on error
|
|
68
87
|
}
|
|
69
|
-
return { workflows: {}, templates: {}, version: '3.0.0' };
|
|
88
|
+
return withRecordMaps({ workflows: {}, templates: {}, version: '3.0.0' }, WORKFLOW_MAPS);
|
|
70
89
|
}
|
|
71
90
|
function saveWorkflowStore(store) {
|
|
72
91
|
ensureWorkflowDir();
|
|
@@ -1540,15 +1540,39 @@ export async function bridgeListEntries(options) {
|
|
|
1540
1540
|
// List
|
|
1541
1541
|
const entries = [];
|
|
1542
1542
|
try {
|
|
1543
|
-
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1543
|
+
const columns = 'id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags';
|
|
1544
|
+
const listLimit = options.includeEmbedding ? Math.min(safeLimit, MAX_LIST_EMBEDDINGS) : safeLimit;
|
|
1545
|
+
// #4686: sort only row identities first, then fetch the payload for the
|
|
1546
|
+
// page. The old query sorted full `content`/`embedding` payloads for
|
|
1547
|
+
// every row and discarded all but the requested page.
|
|
1548
|
+
const pagedSql = `
|
|
1549
|
+
SELECT ${columns}
|
|
1550
|
+
FROM memory_entries
|
|
1551
|
+
WHERE _rowid_ IN (
|
|
1552
|
+
SELECT _rowid_
|
|
1553
|
+
FROM memory_entries
|
|
1554
|
+
WHERE ${statusFilter} ${extraFilter}
|
|
1555
|
+
ORDER BY updated_at DESC
|
|
1556
|
+
LIMIT ? OFFSET ?
|
|
1557
|
+
)
|
|
1558
|
+
ORDER BY updated_at DESC
|
|
1559
|
+
`;
|
|
1560
|
+
const originalSql = `
|
|
1561
|
+
SELECT ${columns}
|
|
1546
1562
|
FROM memory_entries
|
|
1547
1563
|
WHERE ${statusFilter} ${extraFilter}
|
|
1548
1564
|
ORDER BY updated_at DESC
|
|
1549
1565
|
LIMIT ? OFFSET ?
|
|
1550
|
-
|
|
1551
|
-
|
|
1566
|
+
`;
|
|
1567
|
+
let rows;
|
|
1568
|
+
try {
|
|
1569
|
+
rows = ctx.db.prepare(pagedSql).all(...filterParams, listLimit, safeOffset);
|
|
1570
|
+
}
|
|
1571
|
+
catch {
|
|
1572
|
+
// WITHOUT ROWID/legacy tables have no _rowid_: preserve the original
|
|
1573
|
+
// query rather than failing the page.
|
|
1574
|
+
rows = ctx.db.prepare(originalSql).all(...filterParams, listLimit, safeOffset);
|
|
1575
|
+
}
|
|
1552
1576
|
for (const row of rows) {
|
|
1553
1577
|
let tags = [];
|
|
1554
1578
|
if (row.tags) {
|