claude-flow 3.47.1 → 3.49.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.
Files changed (84) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/auto-memory-hook.mjs +6 -2
  3. package/.claude/helpers/hook-handler.cjs +7 -4
  4. package/.claude/helpers/learning-service.mjs +4 -2
  5. package/.claude/helpers/memory.cjs +1 -1
  6. package/.claude/helpers/metrics-db.mjs +4 -2
  7. package/.claude/helpers/router.cjs +1 -1
  8. package/.claude/helpers/session.cjs +1 -1
  9. package/.claude/proven-config.json +42 -0
  10. package/.claude-plugin/marketplace.json +16 -1
  11. package/README.md +1 -53
  12. package/README.zh-CN.md +1 -53
  13. package/node_modules/@claude-flow/codex/package.json +1 -1
  14. package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
  15. package/node_modules/@claude-flow/mcp/dist/tool-registry.js +11 -3
  16. package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
  17. package/node_modules/@claude-flow/mcp/package.json +6 -3
  18. package/node_modules/@claude-flow/plugin-agent-federation/package.json +3 -3
  19. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
  20. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
  22. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  23. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
  24. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  25. package/node_modules/@claude-flow/security/package.json +2 -3
  26. package/package.json +7 -7
  27. package/v3/@claude-flow/cli/README.md +1 -53
  28. package/v3/@claude-flow/cli/bin/cli.js +4 -1
  29. package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
  30. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/config.js +5 -7
  32. package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
  33. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +69 -8
  34. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +22 -9
  35. package/v3/@claude-flow/cli/dist/src/commands/memory.js +119 -34
  36. package/v3/@claude-flow/cli/dist/src/commands/metaharness.js +1 -0
  37. package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
  38. package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
  39. package/v3/@claude-flow/cli/dist/src/commands/security.js +113 -55
  40. package/v3/@claude-flow/cli/dist/src/commands/session.js +132 -25
  41. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
  42. package/v3/@claude-flow/cli/dist/src/commands/task.js +5 -4
  43. package/v3/@claude-flow/cli/dist/src/index.js +10 -1
  44. package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
  45. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
  46. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
  47. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
  48. package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
  49. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
  50. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
  51. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +14 -12
  52. package/v3/@claude-flow/cli/dist/src/mcp-server.js +10 -3
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +29 -62
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -7
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +226 -51
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +131 -40
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.d.ts +8 -1
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-enforcer.js +80 -8
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +270 -58
  62. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.d.ts +13 -0
  63. package/v3/@claude-flow/cli/dist/src/memory/feedback-patterns.js +14 -0
  64. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.d.ts +3 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/live-memory-row.js +5 -0
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +11 -20
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +348 -134
  68. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +9 -1
  69. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +47 -34
  70. package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
  71. package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
  72. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
  73. package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
  74. package/v3/@claude-flow/cli/dist/src/ruvector/graph-analyzer.js +4 -1
  75. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +30 -8
  76. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +1 -1
  77. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +67 -27
  78. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
  79. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +27 -2
  80. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +2 -2
  81. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +23 -14
  82. package/v3/@claude-flow/cli/package.json +9 -9
  83. package/v3/@claude-flow/guidance/package.json +5 -6
  84. package/v3/@claude-flow/shared/package.json +6 -3
@@ -1,3 +1,4 @@
1
+ import { feedbackPatternsSchema, validateFeedbackPatterns } from '../memory/feedback-patterns.js';
1
2
  /**
2
3
  * Hooks MCP Tools
3
4
  * Provides intelligent hooks functionality via MCP protocol
@@ -410,8 +411,75 @@ async function getLoRAAdapter() {
410
411
  }
411
412
  return loraAdapter;
412
413
  }
413
- // In-memory trajectory tracking (persisted on end)
414
+ // Cache pending trajectories; the existing memory store also persists checkpoints.
414
415
  const activeTrajectories = new Map();
416
+ const dirtyTrajectories = new Set();
417
+ const trajectoryOperations = new Map();
418
+ /** Serialize read/modify/checkpoint operations for a trajectory in this process. */
419
+ async function withTrajectoryOperation(id, operation) {
420
+ const previous = trajectoryOperations.get(id) ?? Promise.resolve();
421
+ let release;
422
+ const gate = new Promise(resolve => { release = resolve; });
423
+ const current = previous.then(() => gate);
424
+ trajectoryOperations.set(id, current);
425
+ await previous;
426
+ try {
427
+ return await operation();
428
+ }
429
+ finally {
430
+ release();
431
+ if (trajectoryOperations.get(id) === current)
432
+ trajectoryOperations.delete(id);
433
+ }
434
+ }
435
+ async function persistPendingTrajectory(trajectory) {
436
+ dirtyTrajectories.add(trajectory.id);
437
+ try {
438
+ const store = await getRealStoreFunction();
439
+ const result = await store?.({
440
+ key: `trajectory-pending-${trajectory.id}`, value: JSON.stringify(trajectory),
441
+ namespace: 'trajectories', tags: [trajectory.agent, 'pending', 'sona-trajectory'], upsert: true,
442
+ });
443
+ if (result?.success === true)
444
+ dirtyTrajectories.delete(trajectory.id);
445
+ return result?.success === true;
446
+ }
447
+ catch {
448
+ return false;
449
+ }
450
+ }
451
+ async function loadPendingTrajectory(id) {
452
+ try {
453
+ const { getEntry } = await import('../memory/memory-initializer.js');
454
+ // A completed record is authoritative even if pending-record cleanup failed
455
+ // or another process still has an old cache entry.
456
+ const completed = await getEntry({ key: `trajectory-${id}`, namespace: 'trajectories' });
457
+ if (completed.success && completed.found) {
458
+ activeTrajectories.delete(id);
459
+ dirtyTrajectories.delete(id);
460
+ return undefined;
461
+ }
462
+ // Failed checkpoints must not erase already-acknowledged live steps.
463
+ // Clean caches still reload so another process's later checkpoint is seen.
464
+ if (dirtyTrajectories.has(id) && activeTrajectories.has(id))
465
+ return activeTrajectories.get(id);
466
+ const pending = await getEntry({ key: `trajectory-pending-${id}`, namespace: 'trajectories' });
467
+ if (pending.success && pending.found) {
468
+ const value = JSON.parse(pending.entry?.content ?? 'null');
469
+ if (!value || value.id !== id || typeof value.task !== 'string' || typeof value.agent !== 'string'
470
+ || typeof value.startedAt !== 'string' || !Number.isFinite(Date.parse(value.startedAt))
471
+ || !Array.isArray(value.steps) || !value.steps.every((step) => step
472
+ && typeof step.action === 'string' && typeof step.result === 'string'
473
+ && typeof step.quality === 'number' && Number.isFinite(step.quality)
474
+ && typeof step.timestamp === 'string'))
475
+ return undefined;
476
+ activeTrajectories.set(id, value);
477
+ return value;
478
+ }
479
+ }
480
+ catch { /* store unavailable: retain same-process best-effort tracking */ }
481
+ return activeTrajectories.get(id);
482
+ }
415
483
  const MEMORY_DIR = '.claude-flow/memory';
416
484
  const MEMORY_FILE = 'store.json';
417
485
  function getMemoryPath() {
@@ -654,12 +722,41 @@ const KEYWORD_MATCHERS = Object.entries(KEYWORD_PATTERNS).map(([keyword, result]
654
722
  const body = /[\s/]/.test(keyword) ? escaped : `${escaped}(?:s|es|ing|ed)?`;
655
723
  return { regex: new RegExp(`\\b${body}\\b`, 'i'), result };
656
724
  });
725
+ /**
726
+ * Confidence for a task nothing matched (#3567). It must rank below every real
727
+ * signal: keyword hits are >= 0.8, learned outcomes >= 0.7, and a semantic
728
+ * match is only eligible above 0.4. Matches the helper router's 0.3 fall-through.
729
+ */
730
+ export const NO_MATCH_CONFIDENCE = 0.3;
731
+ /** True when the task has at least one letter or digit in any script. */
732
+ export function hasRoutableText(task) {
733
+ return /[\p{L}\p{N}]/u.test(task);
734
+ }
735
+ // Keyword matchers are English regexes; letters outside Latin script can never hit them.
736
+ const NON_LATIN_LETTER = /(?![A-Za-zÀ-ɏ])\p{L}/u;
737
+ function noMatchNote(task) {
738
+ if (!hasRoutableText(task))
739
+ return 'Task has no words to match.';
740
+ if (NON_LATIN_LETTER.test(task)) {
741
+ return 'Keyword matchers are English-only, so non-English text cannot match them.';
742
+ }
743
+ return 'No keyword or learned pattern matched.';
744
+ }
657
745
  /** Exported for tests. */
658
746
  export function suggestAgentsForTask(task) {
747
+ const noMatch = () => ({
748
+ agents: ['coder', 'researcher', 'tester'],
749
+ confidence: NO_MATCH_CONFIDENCE,
750
+ matched: false,
751
+ reason: 'no-match-default',
752
+ note: noMatchNote(task),
753
+ });
754
+ if (!hasRoutableText(task))
755
+ return noMatch();
659
756
  // Check static keyword patterns first
660
757
  for (const { regex, result } of KEYWORD_MATCHERS) {
661
758
  if (regex.test(task)) {
662
- return result;
759
+ return { ...result, matched: true };
663
760
  }
664
761
  }
665
762
  // Check runtime-learned patterns from successful task outcomes
@@ -679,11 +776,10 @@ export function suggestAgentsForTask(task) {
679
776
  }
680
777
  // Require at least 2 keyword overlap to prevent false positives
681
778
  if (bestAgent && bestOverlap >= 2) {
682
- return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85) };
779
+ return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85), matched: true };
683
780
  }
684
781
  }
685
- // Default fallback
686
- return { agents: ['coder', 'researcher', 'tester'], confidence: 0.7 };
782
+ return noMatch();
687
783
  }
688
784
  function assessCommandRisk(command) {
689
785
  const warnings = [];
@@ -897,13 +993,19 @@ export const hooksPostCommand = {
897
993
  properties: {
898
994
  command: { type: 'string', description: 'Executed command' },
899
995
  exitCode: { type: 'number', description: 'Command exit code' },
996
+ success: { type: 'boolean', description: 'Explicit execution outcome; false records a failure even without a nonzero exit code' },
997
+ ttl: { type: 'integer', minimum: 1, maximum: 2147483647, description: 'Command history lifetime in seconds (default: 30 days)' },
900
998
  },
901
999
  required: ['command'],
902
1000
  },
903
1001
  handler: async (params) => {
904
1002
  const command = params.command;
905
1003
  const exitCode = params.exitCode || 0;
906
- const success = exitCode === 0;
1004
+ const success = exitCode === 0 && params.success !== false;
1005
+ const ttl = params.ttl ?? 30 * 24 * 60 * 60;
1006
+ if (typeof ttl !== 'number' || !Number.isSafeInteger(ttl) || ttl < 1 || ttl > 2147483647) {
1007
+ return { success: false, recorded: false, error: 'ttl must be a positive integer number of seconds (at most 2147483647)' };
1008
+ }
907
1009
  {
908
1010
  const v = validateText(command, 'command');
909
1011
  if (!v.valid)
@@ -916,21 +1018,35 @@ export const hooksPostCommand = {
916
1018
  let _storedIn = 'none';
917
1019
  try {
918
1020
  const bridge = await import('../memory/memory-bridge.js');
919
- await bridge.bridgeStoreEntry({
1021
+ const stored = await bridge.bridgeStoreEntry({
920
1022
  key: `cmd-${Date.now()}`,
921
1023
  value: JSON.stringify({ command, exitCode, success }),
922
1024
  namespace: 'commands',
923
1025
  tags: [success ? 'success' : 'error'],
1026
+ ttl,
1027
+ generateEmbeddingFlag: false,
924
1028
  });
1029
+ if (!stored?.success)
1030
+ throw new Error(stored?.error || 'Native bridge did not store the command');
925
1031
  _storedIn = 'agentdb';
926
1032
  }
927
1033
  catch {
928
1034
  // AgentDB not available — store in JSON
929
1035
  try {
930
1036
  const store = loadMemoryStore();
931
- const key = `cmd-${Date.now()}`;
932
- store.entries[key] = { key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', createdAt: new Date().toISOString() };
933
- const memDir = resolve(MEMORY_DIR);
1037
+ const now = Date.now();
1038
+ for (const [entryKey, entry] of Object.entries(store.entries)) {
1039
+ if (entry.namespace === 'commands' && entry.expiresAt && Date.parse(entry.expiresAt) <= now) {
1040
+ delete store.entries[entryKey];
1041
+ }
1042
+ }
1043
+ const key = `cmd-${now}`;
1044
+ store.entries[key] = {
1045
+ key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands',
1046
+ storedAt: new Date(now).toISOString(), lastAccessed: new Date(now).toISOString(), accessCount: 0,
1047
+ expiresAt: new Date(now + ttl * 1000).toISOString(),
1048
+ };
1049
+ const memDir = dirname(getMemoryPath());
934
1050
  if (!existsSync(memDir))
935
1051
  mkdirSync(memDir, { recursive: true });
936
1052
  writeFileSync(getMemoryPath(), JSON.stringify(store, null, 2), 'utf-8');
@@ -1117,6 +1233,7 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
1117
1233
  let agents;
1118
1234
  let confidence;
1119
1235
  let matchedPattern = '';
1236
+ let noMatchDetail = null;
1120
1237
  // Both static and learned patterns are gated on the same similarity
1121
1238
  // score. Learned patterns additionally require support/reliability as a
1122
1239
  // quality guard, but do NOT need a higher score bar — a learned pattern
@@ -1124,7 +1241,9 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
1124
1241
  // (#2864: a 25pp higher threshold made a top-scoring learned-researcher
1125
1242
  // match at 0.57 lose to a static match at 0.52, discarding the learned
1126
1243
  // store's output on the majority of routes).
1127
- const eligibleSemantic = semanticResult.find((match) => {
1244
+ // A task with no letters or digits has nothing for the embedder to mean;
1245
+ // any similarity it scores is noise, so it never becomes a semantic match.
1246
+ const eligibleSemantic = !hasRoutableText(task) ? undefined : semanticResult.find((match) => {
1128
1247
  if (match.score <= 0.4)
1129
1248
  return false;
1130
1249
  const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
@@ -1144,9 +1263,16 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
1144
1263
  const suggestion = suggestAgentsForTask(task);
1145
1264
  agents = suggestion.agents;
1146
1265
  confidence = suggestion.confidence;
1147
- matchedPattern = 'keyword-fallback';
1148
1266
  routingMethod = 'keyword';
1149
- backendInfo = 'keyword matching';
1267
+ if (suggestion.matched) {
1268
+ matchedPattern = 'keyword-fallback';
1269
+ backendInfo = 'keyword matching';
1270
+ }
1271
+ else {
1272
+ matchedPattern = 'no-match-default';
1273
+ backendInfo = 'keyword matching (no match)';
1274
+ noMatchDetail = suggestion.note ?? 'No pattern matched.';
1275
+ }
1150
1276
  }
1151
1277
  // Determine complexity
1152
1278
  const taskLower = task.toLowerCase();
@@ -1164,6 +1290,9 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
1164
1290
  throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A',
1165
1291
  },
1166
1292
  matchedPattern,
1293
+ // #3567: callers branch on `matched`; a no-match result is a default, not a decision.
1294
+ matched: noMatchDetail === null,
1295
+ ...(noMatchDetail !== null ? { reason: 'no-match-default', note: noMatchDetail } : {}),
1167
1296
  semanticMatches: semanticResult.slice(0, 3).map(r => ({
1168
1297
  pattern: r.intent,
1169
1298
  score: Math.round(r.score * 100) / 100,
@@ -1173,15 +1302,18 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
1173
1302
  confidence: Math.round(confidence * 100) / 100,
1174
1303
  reason: routingMethod.startsWith('semantic')
1175
1304
  ? `Semantic similarity to "${matchedPattern}" pattern (${Math.round(confidence * 100)}%)`
1176
- : `Task contains keywords matching ${agents[0]} specialization`,
1305
+ : noMatchDetail !== null
1306
+ ? `Nothing matched; default suggestion only. ${noMatchDetail}`
1307
+ : `Task contains keywords matching ${agents[0]} specialization`,
1177
1308
  },
1178
1309
  alternativeAgents: agents.slice(1).map((agent, i) => ({
1179
1310
  type: agent,
1180
- confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100,
1181
- reason: `Alternative agent for ${agent} capabilities`,
1311
+ confidence: Math.max(0, Math.round((confidence - (0.1 * (i + 1))) * 100) / 100),
1312
+ reason: noMatchDetail !== null ? 'Default suggestion (nothing matched)' : `Alternative agent for ${agent} capabilities`,
1182
1313
  })),
1183
1314
  estimatedMetrics: {
1184
- successProbability: Math.round(confidence * 100) / 100,
1315
+ // No pattern means no basis for a success estimate.
1316
+ successProbability: noMatchDetail !== null ? null : Math.round(confidence * 100) / 100,
1185
1317
  estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
1186
1318
  complexity,
1187
1319
  },
@@ -1418,12 +1550,15 @@ export const hooksPreTask = {
1418
1550
  return {
1419
1551
  taskId,
1420
1552
  description,
1553
+ agentsMatched: suggestion.matched,
1421
1554
  suggestedAgents: suggestion.agents.map((agent, i) => ({
1422
1555
  type: agent,
1423
1556
  confidence: suggestion.confidence - (0.05 * i),
1424
- reason: i === 0
1425
- ? `Primary agent for ${agent} tasks based on learned patterns`
1426
- : `Alternative agent with ${agent} capabilities`,
1557
+ reason: !suggestion.matched
1558
+ ? 'Default suggestion (nothing matched)'
1559
+ : i === 0
1560
+ ? `Primary agent for ${agent} tasks based on learned patterns`
1561
+ : `Alternative agent with ${agent} capabilities`,
1427
1562
  })),
1428
1563
  complexity,
1429
1564
  estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
@@ -1444,9 +1579,10 @@ export const hooksPostTask = {
1444
1579
  type: 'object',
1445
1580
  properties: {
1446
1581
  taskId: { type: 'string', description: 'Task identifier' },
1582
+ patterns: feedbackPatternsSchema,
1447
1583
  success: { type: 'boolean', description: 'Whether task was successful' },
1448
1584
  agent: { type: 'string', description: 'Agent that completed the task' },
1449
- quality: { type: 'number', description: 'Quality score (0-1)' },
1585
+ quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
1450
1586
  task: { type: 'string', description: 'Task description text (used for learning keyword extraction)' },
1451
1587
  duration: { type: 'number', description: 'Observed task duration in milliseconds (used by pheromone-adaptive topology)' },
1452
1588
  latencyBudgetMs: { type: 'number', description: 'Latency budget used to normalize duration (default 60000ms)' },
@@ -1463,8 +1599,19 @@ export const hooksPostTask = {
1463
1599
  const taskId = params.taskId;
1464
1600
  const success = params.success !== false;
1465
1601
  const agent = params.agent;
1466
- const quality = params.quality || (success ? 0.85 : 0.3);
1602
+ if (params.quality !== undefined && (typeof params.quality !== 'number'
1603
+ || !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
1604
+ return { success: false, error: 'quality must be a finite number between 0 and 1' };
1605
+ }
1606
+ const quality = params.quality === undefined ? (success ? 0.85 : 0.3) : params.quality;
1467
1607
  const startTime = Date.now();
1608
+ let patterns;
1609
+ try {
1610
+ patterns = validateFeedbackPatterns(params.patterns);
1611
+ }
1612
+ catch (error) {
1613
+ return { success: false, error: error.message };
1614
+ }
1468
1615
  {
1469
1616
  const v = validateIdentifier(taskId, 'taskId');
1470
1617
  if (!v.valid)
@@ -1502,7 +1649,7 @@ export const hooksPostTask = {
1502
1649
  quality,
1503
1650
  agent,
1504
1651
  duration: params.duration || undefined,
1505
- patterns: params.patterns || undefined,
1652
+ patterns,
1506
1653
  // ADR-147 P2: forward spawn-tree lineage so it lands in feedback + memory
1507
1654
  parentAgentId,
1508
1655
  depth,
@@ -1756,8 +1903,34 @@ export const hooksExplain = {
1756
1903
  catch {
1757
1904
  // File unreadable; leave as null
1758
1905
  }
1906
+ // #3567: when nothing matched, say so instead of explaining a match that did not happen.
1907
+ if (!suggestion.matched) {
1908
+ return {
1909
+ task,
1910
+ matched: false,
1911
+ reason: 'no-match-default',
1912
+ explanation: `No keyword or learned pattern matched this task. ${suggestion.note ?? ''} ` +
1913
+ `"${suggestion.agents[0]}" is a default suggestion, not a routing decision.`.trim(),
1914
+ factors: [
1915
+ { factor: 'Keyword Match', weight: 0.4, value: null, impact: 'No keyword matched' },
1916
+ { factor: 'Historical Success', weight: 0.3, value: historicalSuccess, impact: historicalNote },
1917
+ { factor: 'Agent Availability', weight: 0.2, value: null, impact: 'Agent availability tracking not implemented' },
1918
+ { factor: 'Task Complexity', weight: 0.1, value: task.length > 100 ? 0.8 : 0.3, impact: 'Complexity assessment' },
1919
+ ],
1920
+ patterns: matchedPatterns,
1921
+ decision: {
1922
+ agent: suggestion.agents[0],
1923
+ confidence: suggestion.confidence,
1924
+ reasoning: [
1925
+ 'No pattern matched; the agent is a default suggestion',
1926
+ `Confidence ${(suggestion.confidence * 100).toFixed(0)}% is below every real match`,
1927
+ ],
1928
+ },
1929
+ };
1930
+ }
1759
1931
  return {
1760
1932
  task,
1933
+ matched: true,
1761
1934
  explanation: `The routing decision was made based on keyword analysis of the task description. ` +
1762
1935
  `The task contains keywords that match the "${suggestion.agents[0]}" specialization with ${(suggestion.confidence * 100).toFixed(0)}% confidence.`,
1763
1936
  factors: [
@@ -2754,9 +2927,8 @@ export const hooksIntelligenceReset = {
2754
2927
  // Directory read failed
2755
2928
  }
2756
2929
  }
2757
- // Clear in-memory trajectories
2758
- cleared.trajectories = activeTrajectories.size;
2759
- activeTrajectories.clear();
2930
+ // Reset learned state without abandoning tasks still recording outcomes.
2931
+ cleared.trajectories = 0;
2760
2932
  return {
2761
2933
  reset: true,
2762
2934
  cleared,
@@ -2815,27 +2987,15 @@ export const hooksTrajectoryStart = {
2815
2987
  getTrajectoryTree().openTrajectory({ sessionId, trajectoryId, task, agent });
2816
2988
  }
2817
2989
  catch { /* prototype path — never blocks trajectory recording */ }
2818
- // Persist pending trajectory to disk so it survives MCP restarts
2819
- const storeFn = await getRealStoreFunction();
2820
- if (storeFn) {
2821
- try {
2822
- await storeFn({
2823
- key: `trajectory-pending-${trajectoryId}`,
2824
- value: JSON.stringify(trajectory),
2825
- namespace: 'trajectories',
2826
- tags: [agent, 'pending', 'sona-trajectory'],
2827
- });
2828
- }
2829
- catch {
2830
- // Best-effort persistence — trajectory still lives in-memory
2831
- }
2832
- }
2990
+ // Checkpoint before handing the id to another process.
2991
+ const persisted = await persistPendingTrajectory(trajectory);
2833
2992
  return {
2834
2993
  trajectoryId,
2835
2994
  task,
2836
2995
  agent,
2837
2996
  started: startedAt,
2838
2997
  status: 'recording',
2998
+ persisted,
2839
2999
  implementation: 'real-trajectory-tracking',
2840
3000
  activeCount: activeTrajectories.size,
2841
3001
  };
@@ -2850,17 +3010,21 @@ export const hooksTrajectoryStep = {
2850
3010
  trajectoryId: { type: 'string', description: 'Trajectory ID' },
2851
3011
  action: { type: 'string', description: 'Action taken' },
2852
3012
  result: { type: 'string', description: 'Action result' },
2853
- quality: { type: 'number', description: 'Quality score (0-1)' },
3013
+ quality: { type: 'number', minimum: 0, maximum: 1, description: 'Quality score (0-1)' },
2854
3014
  },
2855
3015
  required: ['trajectoryId', 'action'],
2856
3016
  },
2857
- handler: async (params) => {
3017
+ handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
2858
3018
  const trajectoryId = params.trajectoryId;
2859
3019
  // #14: scrub extended-thinking blocks so reasoning tokens don't contaminate
2860
3020
  // the learning signal (DISTILL embeds this text).
2861
3021
  const action = scrubReasoningBlocks(params.action);
2862
3022
  const result = scrubReasoningBlocks(params.result || 'success');
2863
- const quality = params.quality || 0.85;
3023
+ if (params.quality !== undefined && (typeof params.quality !== 'number'
3024
+ || !Number.isFinite(params.quality) || params.quality < 0 || params.quality > 1)) {
3025
+ return { success: false, error: 'quality must be a finite number between 0 and 1' };
3026
+ }
3027
+ const quality = params.quality === undefined ? 0.85 : params.quality;
2864
3028
  const timestamp = new Date().toISOString();
2865
3029
  const stepId = `step-${Date.now()}`;
2866
3030
  {
@@ -2874,7 +3038,7 @@ export const hooksTrajectoryStep = {
2874
3038
  return { success: false, error: v.error };
2875
3039
  }
2876
3040
  // Add step to real trajectory if it exists
2877
- const trajectory = activeTrajectories.get(trajectoryId);
3041
+ const trajectory = await loadPendingTrajectory(trajectoryId);
2878
3042
  if (trajectory) {
2879
3043
  trajectory.steps.push({
2880
3044
  action,
@@ -2883,6 +3047,7 @@ export const hooksTrajectoryStep = {
2883
3047
  timestamp,
2884
3048
  });
2885
3049
  }
3050
+ const persisted = trajectory ? await persistPendingTrajectory(trajectory) : false;
2886
3051
  // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2887
3052
  try {
2888
3053
  const { getTrajectoryTree } = await import('../ruvector/trajectory-tree.js');
@@ -2914,11 +3079,12 @@ export const hooksTrajectoryStep = {
2914
3079
  result,
2915
3080
  quality,
2916
3081
  recorded: !!trajectory,
3082
+ persisted,
2917
3083
  timestamp,
2918
3084
  totalSteps: trajectory?.steps.length || 0,
2919
3085
  implementation: trajectory ? 'real-step-recording' : 'trajectory-not-found',
2920
3086
  };
2921
- },
3087
+ }),
2922
3088
  };
2923
3089
  export const hooksTrajectoryEnd = {
2924
3090
  name: 'hooks_intelligence_trajectory-end',
@@ -2932,7 +3098,7 @@ export const hooksTrajectoryEnd = {
2932
3098
  },
2933
3099
  required: ['trajectoryId'],
2934
3100
  },
2935
- handler: async (params) => {
3101
+ handler: async (params) => withTrajectoryOperation(params.trajectoryId, async () => {
2936
3102
  const trajectoryId = params.trajectoryId;
2937
3103
  {
2938
3104
  const v = validateIdentifier(trajectoryId, 'trajectoryId');
@@ -2944,7 +3110,7 @@ export const hooksTrajectoryEnd = {
2944
3110
  const endedAt = new Date().toISOString();
2945
3111
  const startTime = Date.now();
2946
3112
  // Get and finalize real trajectory
2947
- const trajectory = activeTrajectories.get(trajectoryId);
3113
+ const trajectory = await loadPendingTrajectory(trajectoryId);
2948
3114
  let persistResult = { success: false };
2949
3115
  if (trajectory) {
2950
3116
  trajectory.success = success;
@@ -2970,8 +3136,17 @@ export const hooksTrajectoryEnd = {
2970
3136
  persistResult = { success: false, error: error instanceof Error ? error.message : String(error) };
2971
3137
  }
2972
3138
  }
2973
- // Remove from active trajectories
2974
- activeTrajectories.delete(trajectoryId);
3139
+ // Failed writes remain retryable. A completed row prevents stale pending
3140
+ // data from being replayed even if cleanup itself fails.
3141
+ if (persistResult.success) {
3142
+ activeTrajectories.delete(trajectoryId);
3143
+ dirtyTrajectories.delete(trajectoryId);
3144
+ try {
3145
+ const { deleteEntry } = await import('../memory/memory-initializer.js');
3146
+ await deleteEntry({ key: `trajectory-pending-${trajectoryId}`, namespace: 'trajectories' });
3147
+ }
3148
+ catch { /* completed record is the replay guard */ }
3149
+ }
2975
3150
  }
2976
3151
  // MAGE-style execution-state-tree mirror (prototype, non-fatal)
2977
3152
  try {
@@ -3187,7 +3362,7 @@ export const hooksTrajectoryEnd = {
3187
3362
  ? `SONA learned pattern "${sonaResult.patternKey}" with ${(sonaResult.confidence * 100).toFixed(1)}% confidence`
3188
3363
  : (persistResult.success ? 'Trajectory persisted for future learning' : (persistResult.error || 'Trajectory not found')),
3189
3364
  };
3190
- },
3365
+ }),
3191
3366
  };
3192
3367
  // Pattern store/search hooks - REAL implementation using storeEntry
3193
3368
  export const hooksPatternStore = {
@@ -10,5 +10,12 @@
10
10
  * @module v3/cli/mcp-tools/memory-tools
11
11
  */
12
12
  import type { MCPTool } from './types.js';
13
+ export declare const DANGEROUS_KEY_PATTERN: RegExp;
14
+ /**
15
+ * #3570: the one key rule for every memory write path (MCP store, CLI store,
16
+ * import). Plain `/` stays legal (`probe/x`); traversal and shell metacharacters
17
+ * do not. Returns the error message, or null when the key is acceptable.
18
+ */
19
+ export declare function memoryKeyError(key: string): string | null;
13
20
  export declare const memoryTools: MCPTool[];
14
21
  //# sourceMappingURL=memory-tools.d.ts.map