@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.
Files changed (37) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +208 -54
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/settings.json +2 -1
  4. package/README.md +2 -0
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/commands/daemon.js +20 -1
  7. package/dist/src/commands/embeddings.js +6 -6
  8. package/dist/src/commands/hive-mind-worker.d.ts +10 -0
  9. package/dist/src/commands/hive-mind-worker.js +20 -0
  10. package/dist/src/commands/hive-mind.js +27 -4
  11. package/dist/src/commands/init.js +4 -0
  12. package/dist/src/commands/session.js +10 -7
  13. package/dist/src/init/executor.js +15 -5
  14. package/dist/src/init/settings-generator.js +4 -0
  15. package/dist/src/init/types.d.ts +2 -0
  16. package/dist/src/init/types.js +4 -1
  17. package/dist/src/mcp-tools/agentdb-tools.js +3 -3
  18. package/dist/src/mcp-tools/claims-tools.js +23 -10
  19. package/dist/src/mcp-tools/daa-tools.js +22 -2
  20. package/dist/src/mcp-tools/embeddings-tools.js +2 -1
  21. package/dist/src/mcp-tools/hooks-tools.js +11 -1
  22. package/dist/src/mcp-tools/session-tools.js +42 -4
  23. package/dist/src/mcp-tools/task-tools.js +11 -0
  24. package/dist/src/mcp-tools/terminal-tools.js +5 -0
  25. package/dist/src/mcp-tools/workflow-tools.js +22 -3
  26. package/dist/src/memory/memory-bridge.js +29 -5
  27. package/dist/src/memory/memory-initializer.js +32 -10
  28. package/dist/src/missions/schemas.d.ts +10 -10
  29. package/dist/src/mods/headless-plugins.d.ts +16 -0
  30. package/dist/src/mods/headless-plugins.js +30 -1
  31. package/dist/src/services/claims-store.js +5 -2
  32. package/dist/src/services/daemon-autostart.d.ts +24 -0
  33. package/dist/src/services/daemon-autostart.js +55 -1
  34. package/dist/src/services/headless-worker-executor.d.ts +15 -0
  35. package/dist/src/services/headless-worker-executor.js +28 -0
  36. package/package.json +1 -1
  37. 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 restoreMemory = !ctx.flags['agents-only'] && !ctx.flags['tasks-only'];
346
- const restoreAgents = !ctx.flags['memory-only'] && !ctx.flags['tasks-only'];
347
- const restoreTasks = !ctx.flags['memory-only'] && !ctx.flags['agents-only'];
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
  },
@@ -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
  }
@@ -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: 'Synthesize context from stored memories for a given query 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.',
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: 'Query to synthesize context for' },
993
- maxEntries: { type: 'number', description: 'Maximum entries to include (default: 10)' },
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
- claim.context = stealableInfo.context;
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
- const claims = Object.values(store.claims);
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 claims) {
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: claims.filter(c => c.claimant.type === 'agent').length,
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
- const claims = Object.values(store.claims);
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 claims) {
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
- return JSON.parse(readFileSync(path, 'utf-8'));
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
- const threshold = input.threshold || 0.5;
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: 'Specific trajectories to learn from' },
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(outputPath, JSON.stringify(session, null, 2), 'utf-8');
710
- path = outputPath;
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
- return JSON.parse(data);
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
- // Include tags so export/list can round-trip them.
1544
- const stmt = ctx.db.prepare(`
1545
- SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at, tags
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
- const rows = stmt.all(...filterParams, options.includeEmbedding ? Math.min(safeLimit, MAX_LIST_EMBEDDINGS) : safeLimit, safeOffset);
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) {