@cleocode/core 2026.4.35 → 2026.4.37

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 (91) hide show
  1. package/dist/config.d.ts.map +1 -1
  2. package/dist/config.js +7 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/hooks/handlers/conduit-hooks.d.ts +72 -0
  5. package/dist/hooks/handlers/conduit-hooks.d.ts.map +1 -0
  6. package/dist/hooks/handlers/conduit-hooks.js +229 -0
  7. package/dist/hooks/handlers/conduit-hooks.js.map +1 -0
  8. package/dist/hooks/handlers/index.d.ts +2 -0
  9. package/dist/hooks/handlers/index.d.ts.map +1 -1
  10. package/dist/hooks/handlers/index.js +3 -0
  11. package/dist/hooks/handlers/index.js.map +1 -1
  12. package/dist/hooks/handlers/session-hooks.d.ts +14 -0
  13. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  14. package/dist/hooks/handlers/session-hooks.js +33 -0
  15. package/dist/hooks/handlers/session-hooks.js.map +1 -1
  16. package/dist/hooks/handlers/task-hooks.d.ts +2 -0
  17. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  18. package/dist/hooks/handlers/task-hooks.js +14 -0
  19. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  20. package/dist/index.js +54928 -46853
  21. package/dist/index.js.map +4 -4
  22. package/dist/internal.d.ts +2 -0
  23. package/dist/internal.d.ts.map +1 -1
  24. package/dist/internal.js +1 -0
  25. package/dist/internal.js.map +1 -1
  26. package/dist/memory/anthropic-key-resolver.d.ts +35 -0
  27. package/dist/memory/anthropic-key-resolver.d.ts.map +1 -0
  28. package/dist/memory/anthropic-key-resolver.js +105 -0
  29. package/dist/memory/anthropic-key-resolver.js.map +1 -0
  30. package/dist/memory/auto-extract.d.ts +38 -42
  31. package/dist/memory/auto-extract.d.ts.map +1 -1
  32. package/dist/memory/auto-extract.js +38 -57
  33. package/dist/memory/auto-extract.js.map +1 -1
  34. package/dist/memory/brain-retrieval.d.ts +6 -0
  35. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  36. package/dist/memory/brain-retrieval.js +145 -13
  37. package/dist/memory/brain-retrieval.js.map +1 -1
  38. package/dist/memory/brain-search.d.ts +82 -15
  39. package/dist/memory/brain-search.d.ts.map +1 -1
  40. package/dist/memory/brain-search.js +178 -93
  41. package/dist/memory/brain-search.js.map +1 -1
  42. package/dist/memory/engine-compat.d.ts +16 -1
  43. package/dist/memory/engine-compat.d.ts.map +1 -1
  44. package/dist/memory/engine-compat.js +0 -3
  45. package/dist/memory/engine-compat.js.map +1 -1
  46. package/dist/memory/learnings.d.ts.map +1 -1
  47. package/dist/memory/learnings.js +4 -3
  48. package/dist/memory/learnings.js.map +1 -1
  49. package/dist/memory/llm-extraction.d.ts +107 -0
  50. package/dist/memory/llm-extraction.d.ts.map +1 -0
  51. package/dist/memory/llm-extraction.js +425 -0
  52. package/dist/memory/llm-extraction.js.map +1 -0
  53. package/dist/memory/memory-bridge.js +23 -11
  54. package/dist/memory/memory-bridge.js.map +1 -1
  55. package/dist/memory/observer-reflector.d.ts +157 -0
  56. package/dist/memory/observer-reflector.d.ts.map +1 -0
  57. package/dist/memory/observer-reflector.js +626 -0
  58. package/dist/memory/observer-reflector.js.map +1 -0
  59. package/dist/store/brain-schema.d.ts +131 -0
  60. package/dist/store/brain-schema.d.ts.map +1 -1
  61. package/dist/store/brain-schema.js +30 -0
  62. package/dist/store/brain-schema.js.map +1 -1
  63. package/dist/store/brain-sqlite.js +41 -1
  64. package/dist/store/brain-sqlite.js.map +1 -1
  65. package/dist/tasks/complete.d.ts.map +1 -1
  66. package/dist/tasks/complete.js +7 -8
  67. package/dist/tasks/complete.js.map +1 -1
  68. package/package.json +13 -12
  69. package/src/config.ts +7 -0
  70. package/src/hooks/handlers/__tests__/conduit-hooks.test.ts +356 -0
  71. package/src/hooks/handlers/conduit-hooks.ts +258 -0
  72. package/src/hooks/handlers/index.ts +7 -0
  73. package/src/hooks/handlers/session-hooks.ts +37 -0
  74. package/src/hooks/handlers/task-hooks.ts +14 -0
  75. package/src/internal.ts +8 -0
  76. package/src/memory/__tests__/auto-extract.test.ts +43 -114
  77. package/src/memory/__tests__/brain-automation.test.ts +16 -39
  78. package/src/memory/__tests__/brain-rrf.test.ts +431 -0
  79. package/src/memory/__tests__/llm-extraction.test.ts +342 -0
  80. package/src/memory/__tests__/observer-reflector.test.ts +475 -0
  81. package/src/memory/anthropic-key-resolver.ts +113 -0
  82. package/src/memory/auto-extract.ts +40 -72
  83. package/src/memory/brain-retrieval.ts +187 -18
  84. package/src/memory/brain-search.ts +196 -128
  85. package/src/memory/engine-compat.ts +16 -4
  86. package/src/memory/learnings.ts +4 -3
  87. package/src/memory/llm-extraction.ts +524 -0
  88. package/src/memory/memory-bridge.ts +29 -12
  89. package/src/memory/observer-reflector.ts +829 -0
  90. package/src/store/brain-schema.ts +44 -0
  91. package/src/tasks/complete.ts +7 -10
@@ -17,7 +17,6 @@ import type {
17
17
  BrainPatternRow,
18
18
  } from '../store/brain-schema.js';
19
19
  import { typedAll } from '../store/typed-query.js';
20
- import type { BrainSearchHit } from './brain-row-types.js';
21
20
  import type { SimilarityResult } from './brain-similarity.js';
22
21
  import { searchSimilar } from './brain-similarity.js';
23
22
  import { QUALITY_SCORE_THRESHOLD } from './quality-scoring.js';
@@ -598,44 +597,174 @@ export function resetFts5Cache(): void {
598
597
  }
599
598
 
600
599
  // ============================================================================
601
- // Hybrid Search (FTS5 + Vector + Graph)
600
+ // Reciprocal Rank Fusion (RRF) Hybrid Retrieval
601
+ // ============================================================================
602
+
603
+ /**
604
+ * The RRF smoothing constant (research-proven at 60).
605
+ *
606
+ * Balances noise vs. signal: small values amplify top-rank differences;
607
+ * large values compress ranks toward a flat distribution. 60 is the
608
+ * standard value from Cormack, Clarke & Buettcher (SIGIR 2009).
609
+ */
610
+ export const RRF_K = 60;
611
+
612
+ /** A single ranked hit from one retrieval source before fusion. */
613
+ export interface RrfHit {
614
+ id: string;
615
+ type: string;
616
+ title: string;
617
+ text: string;
618
+ }
619
+
620
+ /** Fused result produced by reciprocalRankFusion. */
621
+ export interface RrfResult {
622
+ id: string;
623
+ /** Combined RRF score: sum of 1/(rank+RRF_K) across all source lists. */
624
+ rrfScore: number;
625
+ type: string;
626
+ title: string;
627
+ text: string;
628
+ /** Which retrieval sources contributed to this result. */
629
+ sources: Array<'fts' | 'vec' | 'graph'>;
630
+ /** BM25-derived FTS rank (0-based) — undefined if not in FTS results. */
631
+ ftsRank?: number;
632
+ /** Vector distance rank (0-based) — undefined if not in vector results. */
633
+ vecRank?: number;
634
+ }
635
+
636
+ /**
637
+ * Fuse ranked lists from multiple retrieval sources using Reciprocal Rank Fusion.
638
+ *
639
+ * Implements the RRF algorithm from Cormack, Clarke & Buettcher (SIGIR 2009):
640
+ *
641
+ * score(d) = Σ 1 / (k + rank(d, list)) for each list containing d
642
+ *
643
+ * where k=60 is the research-proven smoothing constant.
644
+ *
645
+ * Properties:
646
+ * - Rank-based: actual scores from each source are ignored (only rank matters).
647
+ * - Additive: items appearing in multiple lists accumulate higher scores.
648
+ * - Robust: the +60 constant prevents rank-1 items from dominating.
649
+ *
650
+ * @param sources - Named arrays of ranked hits (order = rank, index 0 = best)
651
+ * @param k - RRF smoothing constant (default: RRF_K = 60)
652
+ * @returns Array of fused results sorted by rrfScore descending
653
+ *
654
+ * @example
655
+ * ```ts
656
+ * const fused = reciprocalRankFusion([
657
+ * { source: 'fts', hits: ftsHits },
658
+ * { source: 'vec', hits: vecHits },
659
+ * ]);
660
+ * ```
661
+ */
662
+ export function reciprocalRankFusion(
663
+ sources: Array<{
664
+ source: 'fts' | 'vec' | 'graph';
665
+ hits: RrfHit[];
666
+ }>,
667
+ k: number = RRF_K,
668
+ ): RrfResult[] {
669
+ // Accumulator: id -> mutable result record
670
+ const accum = new Map<
671
+ string,
672
+ {
673
+ rrfScore: number;
674
+ type: string;
675
+ title: string;
676
+ text: string;
677
+ sources: Set<'fts' | 'vec' | 'graph'>;
678
+ ftsRank?: number;
679
+ vecRank?: number;
680
+ }
681
+ >();
682
+
683
+ for (const { source, hits } of sources) {
684
+ for (let rank = 0; rank < hits.length; rank++) {
685
+ const hit = hits[rank]!;
686
+ const contribution = 1 / (k + rank);
687
+
688
+ const existing = accum.get(hit.id);
689
+ if (existing) {
690
+ existing.rrfScore += contribution;
691
+ existing.sources.add(source);
692
+ if (source === 'fts') existing.ftsRank = rank;
693
+ if (source === 'vec') existing.vecRank = rank;
694
+ } else {
695
+ accum.set(hit.id, {
696
+ rrfScore: contribution,
697
+ type: hit.type,
698
+ title: hit.title,
699
+ text: hit.text,
700
+ sources: new Set([source]),
701
+ ftsRank: source === 'fts' ? rank : undefined,
702
+ vecRank: source === 'vec' ? rank : undefined,
703
+ });
704
+ }
705
+ }
706
+ }
707
+
708
+ return [...accum.entries()]
709
+ .map(([id, data]) => ({
710
+ id,
711
+ rrfScore: data.rrfScore,
712
+ type: data.type,
713
+ title: data.title,
714
+ text: data.text,
715
+ sources: [...data.sources] as Array<'fts' | 'vec' | 'graph'>,
716
+ ftsRank: data.ftsRank,
717
+ vecRank: data.vecRank,
718
+ }))
719
+ .sort((a, b) => b.rrfScore - a.rrfScore);
720
+ }
721
+
722
+ // ============================================================================
723
+ // Hybrid Search (FTS5 + Vector + Graph) — RRF-powered
602
724
  // ============================================================================
603
725
 
604
726
  /** Result from hybridSearch combining multiple search signals. */
605
727
  export interface HybridResult {
606
728
  id: string;
729
+ /** RRF-fused score: sum of 1/(rank+60) across all source lists. */
607
730
  score: number;
608
731
  type: string;
609
732
  title: string;
610
733
  text: string;
611
734
  sources: Array<'fts' | 'vec' | 'graph'>;
735
+ /** Raw FTS rank (0-based) for transparency — undefined if FTS did not return this item. */
736
+ ftsRank?: number;
737
+ /** Raw vector rank (0-based) for transparency — undefined if vector did not return this item. */
738
+ vecRank?: number;
612
739
  }
613
740
 
614
- /** Options for hybridSearch weighting and limits. */
741
+ /** Options for hybridSearch. */
615
742
  export interface HybridSearchOptions {
616
- ftsWeight?: number;
617
- vecWeight?: number;
618
- graphWeight?: number;
619
743
  limit?: number;
744
+ /**
745
+ * RRF smoothing constant k. Default: 60 (research-proven).
746
+ * Larger k flattens rank differences; smaller k amplifies top-rank advantage.
747
+ */
748
+ rrfK?: number;
620
749
  }
621
750
 
622
751
  /**
623
- * Hybrid search across FTS5, vector similarity, and graph neighbors.
752
+ * Hybrid search across FTS5, vector similarity, and graph neighbors using
753
+ * Reciprocal Rank Fusion (RRF) for result combination.
624
754
  *
625
- * 1. Runs FTS5 search via existing searchBrain.
626
- * 2. Runs vector similarity via searchSimilar (if available).
627
- * 3. Runs graph neighbor expansion via getNeighbors (if query matches a node).
628
- * 4. Normalizes scores to 0-1 using min-max normalization.
629
- * 5. Combines with configurable weights.
630
- * 6. Deduplicates by ID, keeping highest combined score.
631
- * 7. Returns top-N sorted by score descending.
755
+ * Algorithm:
756
+ * 1. Run FTS5 search and vector similarity search in parallel.
757
+ * 2. Optionally expand via graph neighbors (best-effort).
758
+ * 3. Fuse all ranked lists with RRF: score = Σ 1/(rank+60).
759
+ * 4. Return top-N sorted by fused RRF score.
632
760
  *
633
- * Graceful fallback: if vec unavailable, redistributes weight to FTS5.
761
+ * Graceful degradation: vector and graph sources are silently skipped when
762
+ * unavailable — RRF naturally handles partial source lists.
634
763
  *
635
764
  * @param query - Search query text
636
765
  * @param projectRoot - Project root directory
637
- * @param options - Weight and limit configuration
638
- * @returns Array of hybrid results ranked by combined score
766
+ * @param options - Limit and RRF tuning
767
+ * @returns Array of hybrid results ranked by RRF score descending
639
768
  */
640
769
  export async function hybridSearch(
641
770
  query: string,
@@ -645,52 +774,21 @@ export async function hybridSearch(
645
774
  if (!query?.trim()) return [];
646
775
 
647
776
  const maxResults = options?.limit ?? 10;
648
- let ftsWeight = options?.ftsWeight ?? 0.5;
649
- let vecWeight = options?.vecWeight ?? 0.4;
650
- const graphWeight = options?.graphWeight ?? 0.1;
651
-
652
- // Score accumulators: id -> { score, sources, type, title, text }
653
- const scoreMap = new Map<
654
- string,
655
- {
656
- score: number;
657
- type: string;
658
- title: string;
659
- text: string;
660
- sources: Set<'fts' | 'vec' | 'graph'>;
661
- }
662
- >();
663
-
664
- const addScore = (
665
- id: string,
666
- normalizedScore: number,
667
- weight: number,
668
- source: 'fts' | 'vec' | 'graph',
669
- type: string,
670
- title: string,
671
- text: string,
672
- ) => {
673
- const existing = scoreMap.get(id);
674
- if (existing) {
675
- existing.score += normalizedScore * weight;
676
- existing.sources.add(source);
677
- } else {
678
- scoreMap.set(id, {
679
- score: normalizedScore * weight,
680
- type,
681
- title,
682
- text,
683
- sources: new Set([source]),
684
- });
685
- }
686
- };
687
-
688
- // --- 1. FTS5 search ---
689
- const ftsResults = await searchBrain(projectRoot, query, { limit: maxResults * 2 });
690
-
691
- // Collect all FTS hits into a flat list with position-based scores
692
- const ftsHits: BrainSearchHit[] = [];
693
-
777
+ const rrfK = options?.rrfK ?? RRF_K;
778
+
779
+ // --- 1. Run FTS5 and vector in parallel ---
780
+ const [ftsResults, vecResults] = await Promise.all([
781
+ searchBrain(projectRoot, query, { limit: maxResults * 3 }).catch(() => ({
782
+ decisions: [],
783
+ patterns: [],
784
+ learnings: [],
785
+ observations: [],
786
+ })),
787
+ searchSimilar(query, projectRoot, maxResults * 3).catch(() => [] as SimilarityResult[]),
788
+ ]);
789
+
790
+ // --- 2. Project FTS results into ranked RrfHit list ---
791
+ const ftsHits: RrfHit[] = [];
694
792
  for (const d of ftsResults.decisions) {
695
793
  ftsHits.push({
696
794
  id: d.id,
@@ -719,88 +817,58 @@ export async function hybridSearch(
719
817
  ftsHits.push({ id: o.id, type: 'observation', title: o.title, text: o.narrative ?? o.title });
720
818
  }
721
819
 
722
- // Normalize FTS: position-based (first result = 1.0, last = near 0)
723
- for (let i = 0; i < ftsHits.length; i++) {
724
- const hit = ftsHits[i]!;
725
- const normalizedScore = ftsHits.length > 1 ? 1.0 - i / (ftsHits.length - 1) : 1.0;
726
- addScore(hit.id, normalizedScore, ftsWeight, 'fts', hit.type, hit.title, hit.text);
727
- }
728
-
729
- // --- 2. Vector similarity search ---
730
- let vecResults: SimilarityResult[] = [];
731
- try {
732
- vecResults = await searchSimilar(query, projectRoot, maxResults * 2);
733
- } catch {
734
- // Vector search unavailable
735
- }
820
+ // --- 3. Project vector results into ranked RrfHit list (ascending distance = descending quality) ---
821
+ const vecHits: RrfHit[] = vecResults.map((r) => ({
822
+ id: r.id,
823
+ type: r.type,
824
+ title: r.title,
825
+ text: r.text,
826
+ }));
736
827
 
737
- if (vecResults.length > 0) {
738
- // Normalize vector: distance-based (smaller distance = higher score)
739
- const maxDist = Math.max(...vecResults.map((r) => r.distance), 0.001);
740
- for (const r of vecResults) {
741
- const normalizedScore = 1.0 - r.distance / maxDist;
742
- addScore(r.id, normalizedScore, vecWeight, 'vec', r.type, r.title, r.text);
743
- }
744
- } else {
745
- // Redistribute vec weight to FTS if vector unavailable
746
- ftsWeight += vecWeight;
747
- vecWeight = 0;
748
-
749
- // Re-score FTS hits with updated weight
750
- scoreMap.clear();
751
- for (let i = 0; i < ftsHits.length; i++) {
752
- const hit = ftsHits[i]!;
753
- const normalizedScore = ftsHits.length > 1 ? 1.0 - i / (ftsHits.length - 1) : 1.0;
754
- addScore(hit.id, normalizedScore, ftsWeight, 'fts', hit.type, hit.title, hit.text);
755
- }
756
- }
828
+ // --- 4. Build source list for RRF ---
829
+ const rrfSources: Array<{ source: 'fts' | 'vec' | 'graph'; hits: RrfHit[] }> = [];
830
+ if (ftsHits.length > 0) rrfSources.push({ source: 'fts', hits: ftsHits });
831
+ if (vecHits.length > 0) rrfSources.push({ source: 'vec', hits: vecHits });
757
832
 
758
- // --- 3. Graph neighbor expansion ---
833
+ // --- 5. Graph neighbor expansion (best-effort) ---
759
834
  try {
760
835
  const accessor = await getBrainAccessor(projectRoot);
761
-
762
- // Check if query matches a known graph node ID pattern
763
836
  const possibleNodeIds = [
764
837
  `concept:${query.toLowerCase().replace(/\s+/g, '-')}`,
765
838
  `task:${query}`,
766
839
  `doc:${query}`,
767
840
  ];
768
841
 
842
+ const graphHits: RrfHit[] = [];
769
843
  for (const nodeId of possibleNodeIds) {
770
844
  const node = await accessor.getPageNode(nodeId);
771
845
  if (!node) continue;
772
-
773
846
  const neighbors = await accessor.getNeighbors(nodeId);
774
- for (let i = 0; i < neighbors.length; i++) {
775
- const neighbor = neighbors[i]!;
776
- const normalizedScore = neighbors.length > 1 ? 1.0 - i / (neighbors.length - 1) : 1.0;
777
- addScore(
778
- neighbor.id,
779
- normalizedScore,
780
- graphWeight,
781
- 'graph',
782
- neighbor.nodeType,
783
- neighbor.label,
784
- neighbor.label,
785
- );
847
+ for (const neighbor of neighbors) {
848
+ graphHits.push({
849
+ id: neighbor.id,
850
+ type: neighbor.nodeType,
851
+ title: neighbor.label,
852
+ text: neighbor.label,
853
+ });
786
854
  }
787
855
  }
856
+ if (graphHits.length > 0) rrfSources.push({ source: 'graph', hits: graphHits });
788
857
  } catch {
789
- // Graph search unavailable — no redistribution needed (small weight)
858
+ // Graph unavailable — RRF handles gracefully with remaining sources
790
859
  }
791
860
 
792
- // --- 4. Sort and return top-N ---
793
- const sorted = [...scoreMap.entries()]
794
- .map(([id, data]) => ({
795
- id,
796
- score: data.score,
797
- type: data.type,
798
- title: data.title,
799
- text: data.text,
800
- sources: [...data.sources] as Array<'fts' | 'vec' | 'graph'>,
801
- }))
802
- .sort((a, b) => b.score - a.score)
803
- .slice(0, maxResults);
804
-
805
- return sorted;
861
+ // --- 6. Fuse with RRF and return top-N ---
862
+ const fused = reciprocalRankFusion(rrfSources, rrfK);
863
+
864
+ return fused.slice(0, maxResults).map((r) => ({
865
+ id: r.id,
866
+ score: r.rrfScore,
867
+ type: r.type,
868
+ title: r.title,
869
+ text: r.text,
870
+ sources: r.sources,
871
+ ftsRank: r.ftsRank,
872
+ vecRank: r.vecRank,
873
+ }));
806
874
  }
@@ -1569,10 +1569,25 @@ export async function memoryReasonSimilar(
1569
1569
  export async function memorySearchHybrid(
1570
1570
  params: {
1571
1571
  query: string;
1572
+ limit?: number;
1573
+ /**
1574
+ * @deprecated Weight parameters are unused — hybrid search now uses
1575
+ * Reciprocal Rank Fusion (RRF) which is rank-based and does not require
1576
+ * per-source weights. This field is accepted but silently ignored.
1577
+ */
1572
1578
  ftsWeight?: number;
1579
+ /**
1580
+ * @deprecated Weight parameters are unused — hybrid search now uses
1581
+ * Reciprocal Rank Fusion (RRF) which is rank-based and does not require
1582
+ * per-source weights. This field is accepted but silently ignored.
1583
+ */
1573
1584
  vecWeight?: number;
1585
+ /**
1586
+ * @deprecated Weight parameters are unused — hybrid search now uses
1587
+ * Reciprocal Rank Fusion (RRF) which is rank-based and does not require
1588
+ * per-source weights. This field is accepted but silently ignored.
1589
+ */
1574
1590
  graphWeight?: number;
1575
- limit?: number;
1576
1591
  },
1577
1592
  projectRoot?: string,
1578
1593
  ): Promise<EngineResult> {
@@ -1584,9 +1599,6 @@ export async function memorySearchHybrid(
1584
1599
  const root = resolveRoot(projectRoot);
1585
1600
  const { hybridSearch } = await import('./brain-search.js');
1586
1601
  const results = await hybridSearch(params.query, root, {
1587
- ftsWeight: params.ftsWeight,
1588
- vecWeight: params.vecWeight,
1589
- graphWeight: params.graphWeight,
1590
1602
  limit: params.limit,
1591
1603
  });
1592
1604
  return { success: true, data: { results, total: results.length } };
@@ -105,8 +105,9 @@ export async function storeLearning(projectRoot: string, params: StoreLearningPa
105
105
  // memoryType routing (spec §4.1 Decision Tree for memoryType):
106
106
  // - source contains 'transcript:ses_' → 'episodic' (event-specific insight)
107
107
  // - otherwise → 'semantic' (declarative factual learning)
108
- // verified = false (learnings need corroboration or manual verify gate)
109
- const isManual = params.source.includes('manual');
108
+ // Owner-stated learnings are ground truth (auto-verified).
109
+ // Transcript-extracted and agent-inferred start unverified — consolidator promotes.
110
+ const isManual = params.source.includes('manual') || params.source.includes('owner');
110
111
  const isTranscript = params.source.includes('transcript:ses_');
111
112
  const sourceConfidence = isManual
112
113
  ? ('owner' as const)
@@ -115,7 +116,7 @@ export async function storeLearning(projectRoot: string, params: StoreLearningPa
115
116
  : ('agent' as const);
116
117
  const memoryTier = isManual ? ('medium' as const) : ('short' as const);
117
118
  const memoryType = isTranscript ? ('episodic' as const) : ('semantic' as const);
118
- const verified = false;
119
+ const verified = isManual;
119
120
 
120
121
  // Compute quality score from confidence, actionability, content richness,
121
122
  // and T549 source multiplier.