@cachly-dev/mcp-server 0.10.61 → 0.10.64
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/CHANGELOG.md.tmp +0 -0
- package/PROGRESS.md +137 -0
- package/README.md +210 -180
- package/VISION_10X.md +133 -0
- package/dist/src/__tests__/brain-flow.test.js +102 -1
- package/dist/src/__tests__/brain-flow.test.js.map +1 -1
- package/dist/src/__tests__/utils.test.d.ts +1 -0
- package/dist/src/__tests__/utils.test.js +83 -0
- package/dist/src/__tests__/utils.test.js.map +1 -0
- package/dist/src/handlers/brain.js +118 -64
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +4 -2
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/tools.d.ts +17 -0
- package/dist/src/tools.js +16 -0
- package/dist/src/tools.js.map +1 -1
- package/dist/src/utils.d.ts +33 -0
- package/dist/src/utils.js +76 -0
- package/dist/src/utils.js.map +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
- package/src/__tests__/brain-flow.test.ts +119 -1
- package/src/__tests__/utils.test.ts +81 -0
- package/src/handlers/brain.ts +130 -60
- package/src/handlers/fedbrain.ts +3 -2
- package/src/index.ts +3 -3
- package/src/tools.ts +17 -0
- package/src/utils.ts +80 -0
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
3
3
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode } from '../ckg.js';
|
|
4
|
-
import { safeJsonParse } from '../utils.js';
|
|
4
|
+
import { safeJsonParse, scanKeys } from '../utils.js';
|
|
5
5
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
6
6
|
import { rerankByQuality } from '../rerank.js';
|
|
7
7
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
8
8
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
9
|
-
const MCP_VERSION = '0.10.
|
|
9
|
+
const MCP_VERSION = '0.10.64';
|
|
10
10
|
const WHATS_NEW = {
|
|
11
|
+
'0.10.64': [
|
|
12
|
+
`📈 **The three decisive metrics, now measurable**`,
|
|
13
|
+
` ⏱️ \`brain_metrics()\` — time-to-first-recall, recall-lift, team-knowledge-reuse in one view`,
|
|
14
|
+
` 👥 Cross-author reuse tracked: recall a teammate's lesson → counted + surfaced inline`,
|
|
15
|
+
` 🛡️ Stability: scans now capped + timed out (a huge keyspace can't hang the agent)`,
|
|
16
|
+
],
|
|
11
17
|
'0.10.61': [
|
|
12
18
|
`🎯 **Phase 3C: 100 MCP tools milestone — zero-setup knowledge graph**`,
|
|
13
19
|
` 🔄 \`brain_from_git\` now auto-builds Person+File nodes from git history (zero setup!)`,
|
|
@@ -69,7 +75,7 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
69
75
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
70
76
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
71
77
|
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
72
|
-
'skill_gaps', 'brain_coverage',
|
|
78
|
+
'skill_gaps', 'brain_coverage', 'brain_metrics',
|
|
73
79
|
]);
|
|
74
80
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
75
81
|
switch (name) {
|
|
@@ -201,6 +207,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
201
207
|
version: 3,
|
|
202
208
|
};
|
|
203
209
|
const lesson = JSON.stringify(lessonObj);
|
|
210
|
+
// Metric 1 (time-to-first-recall): mark the moment the Brain first gained
|
|
211
|
+
// knowledge. SET NX so only the very first learn wins. Fire-and-forget.
|
|
212
|
+
redis.set(`cachly:stats:born_at:${instance_id}`, ts, 'EX', 365 * 86400, 'NX').catch(() => { });
|
|
204
213
|
// Always append to the history list (audit log); keep last 100 entries, 90-day TTL
|
|
205
214
|
const listKey = `cachly:lessons:${topic}`;
|
|
206
215
|
await redis.rpush(listKey, lesson);
|
|
@@ -510,7 +519,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
510
519
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
511
520
|
}
|
|
512
521
|
case 'smart_recall': {
|
|
513
|
-
const { instance_id, query, threshold = 0.78, } = args;
|
|
522
|
+
const { instance_id, query, threshold = 0.78, author: requester = '', } = args;
|
|
514
523
|
const redis = await getConnection(instance_id);
|
|
515
524
|
// ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
|
|
516
525
|
const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 10);
|
|
@@ -519,6 +528,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
519
528
|
const kwMatches = rerankByQuality(rawMatches);
|
|
520
529
|
const savedHere = [];
|
|
521
530
|
const lessonMatches = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
531
|
+
let crossAuthorThisCall = 0;
|
|
522
532
|
for (const m of lessonMatches.slice(0, 5)) {
|
|
523
533
|
const existing = await redis.get(m.key).catch(() => null);
|
|
524
534
|
if (existing) {
|
|
@@ -529,6 +539,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
529
539
|
const sev = lesson.severity;
|
|
530
540
|
const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
|
|
531
541
|
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
542
|
+
// Metric 3 (team-knowledge-reuse): a recall of a lesson authored by
|
|
543
|
+
// someone OTHER than the requester is cross-author reuse — the value
|
|
544
|
+
// only cachly delivers. Track total proven recalls + cross-author ones.
|
|
545
|
+
redis.incr(`cachly:stats:recalls_total:${instance_id}`).catch(() => { });
|
|
546
|
+
if (lesson.author && requester && lesson.author !== requester) {
|
|
547
|
+
crossAuthorThisCall++;
|
|
548
|
+
redis.incr(`cachly:stats:cross_author_recalls:${instance_id}`).catch(() => { });
|
|
549
|
+
redis.sadd(`cachly:stats:reuse_pairs:${instance_id}`, `${requester}<-${lesson.author}`).catch(() => { });
|
|
550
|
+
}
|
|
532
551
|
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
533
552
|
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
|
|
534
553
|
savedHere.push({
|
|
@@ -541,6 +560,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
541
560
|
}
|
|
542
561
|
}
|
|
543
562
|
}
|
|
563
|
+
// Metric 1 (time-to-first-recall): stamp the first time recall returned a
|
|
564
|
+
// proven lesson. SET NX so only the first successful recall wins.
|
|
565
|
+
if (savedHere.length > 0) {
|
|
566
|
+
redis.set(`cachly:stats:first_recall_at:${instance_id}`, new Date().toISOString(), 'EX', 365 * 86400, 'NX').catch(() => { });
|
|
567
|
+
}
|
|
544
568
|
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
545
569
|
const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
|
|
546
570
|
const semHits = [];
|
|
@@ -666,6 +690,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
666
690
|
if (topLesson) {
|
|
667
691
|
lines.push(`> 💡 **Brain saved you ~${topLesson.savedMins}m here** — ${topLesson.severity} issue from \`${topLesson.topic}\` recalled ${topLesson.recall_count}× proven\n`);
|
|
668
692
|
}
|
|
693
|
+
if (crossAuthorThisCall > 0) {
|
|
694
|
+
lines.push(`> 👥 **Team knowledge reuse** — ${crossAuthorThisCall} of these lesson${crossAuthorThisCall !== 1 ? 's were' : ' was'} written by a teammate. This is the value only a shared brain delivers.\n`);
|
|
695
|
+
}
|
|
669
696
|
// Show sub-query info if multi-topic was detected
|
|
670
697
|
const subQueries = splitMultiQuery(query);
|
|
671
698
|
if (subQueries.length > 1) {
|
|
@@ -1843,6 +1870,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1843
1870
|
// whose authored edges in the CKG match the query topic.
|
|
1844
1871
|
case 'brain_who_knows': {
|
|
1845
1872
|
const { instance_id, topic, limit = 10, } = args;
|
|
1873
|
+
if (!topic || typeof topic !== 'string' || !topic.trim()) {
|
|
1874
|
+
return '⚠️ `brain_who_knows` requires a non-empty `topic`. Example: `brain_who_knows(topic="kubernetes deployment")`.';
|
|
1875
|
+
}
|
|
1876
|
+
const safeLimit = Math.max(1, Math.min(Number(limit) || 10, 50));
|
|
1846
1877
|
const redis = await getConnection(instance_id);
|
|
1847
1878
|
// Step 1: Find candidate concept node IDs matching the query
|
|
1848
1879
|
const querySlug = ckgSlug(topic);
|
|
@@ -1917,7 +1948,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1917
1948
|
lastActive: s.lastActive,
|
|
1918
1949
|
}))
|
|
1919
1950
|
.sort((a, b) => (b.lessonCount * b.avgConfidence) - (a.lessonCount * a.avgConfidence))
|
|
1920
|
-
.slice(0,
|
|
1951
|
+
.slice(0, safeLimit);
|
|
1921
1952
|
const lines = [
|
|
1922
1953
|
`## 👥 Who Knows About \`${topic}\`?`,
|
|
1923
1954
|
``,
|
|
@@ -1938,8 +1969,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1938
1969
|
// Phase 3B: "What do we know about these files?" — experts + lessons per file.
|
|
1939
1970
|
case 'brain_file_map': {
|
|
1940
1971
|
const { instance_id, file_paths = [], } = args;
|
|
1941
|
-
|
|
1942
|
-
|
|
1972
|
+
// Defensive: accept only a non-empty array of non-empty strings.
|
|
1973
|
+
const cleanPaths = (Array.isArray(file_paths) ? file_paths : [])
|
|
1974
|
+
.filter((p) => typeof p === 'string' && p.trim().length > 0)
|
|
1975
|
+
.map(p => p.trim());
|
|
1976
|
+
if (!cleanPaths.length)
|
|
1977
|
+
return '⚠️ Pass at least one file path. Example: `brain_file_map(file_paths=["src/auth/jwt.ts"])`.';
|
|
1943
1978
|
const redis = await getConnection(instance_id);
|
|
1944
1979
|
const lines = [
|
|
1945
1980
|
`## 📁 File Knowledge Map`,
|
|
@@ -1947,7 +1982,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1947
1982
|
`_What cachly knows about the files you're about to touch:_`,
|
|
1948
1983
|
``,
|
|
1949
1984
|
];
|
|
1950
|
-
for (const fp of
|
|
1985
|
+
for (const fp of cleanPaths.slice(0, 10)) {
|
|
1951
1986
|
lines.push(`### \`${fp}\``);
|
|
1952
1987
|
// ── Experts via CKG file nodes ─────────────────────────────────────
|
|
1953
1988
|
const fileId = `file:${ckgSlug(fp)}`;
|
|
@@ -2003,14 +2038,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2003
2038
|
}
|
|
2004
2039
|
// Also: exact file_paths match via lesson scan
|
|
2005
2040
|
if (relatedLessons.length < 2) {
|
|
2006
|
-
const
|
|
2007
|
-
const
|
|
2008
|
-
await new Promise((res, rej) => {
|
|
2009
|
-
stream.on('data', (b) => scanKeys.push(...b));
|
|
2010
|
-
stream.on('end', res);
|
|
2011
|
-
stream.on('error', rej);
|
|
2012
|
-
});
|
|
2013
|
-
for (const k of scanKeys.slice(0, 300)) {
|
|
2041
|
+
const allLessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 1500 });
|
|
2042
|
+
for (const k of allLessonKeys.slice(0, 300)) {
|
|
2014
2043
|
const raw = await redis.get(k);
|
|
2015
2044
|
if (!raw)
|
|
2016
2045
|
continue;
|
|
@@ -2045,14 +2074,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2045
2074
|
case 'team_expertise_map': {
|
|
2046
2075
|
const { instance_id, top_n = 20, } = args;
|
|
2047
2076
|
const redis = await getConnection(instance_id);
|
|
2048
|
-
// Scan all person nodes
|
|
2049
|
-
const personKeys =
|
|
2050
|
-
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2051
|
-
await new Promise((res, rej) => {
|
|
2052
|
-
pStream.on('data', (b) => personKeys.push(...b));
|
|
2053
|
-
pStream.on('end', res);
|
|
2054
|
-
pStream.on('error', rej);
|
|
2055
|
-
});
|
|
2077
|
+
// Scan all person nodes (capped + timed out so a huge keyspace can't hang the agent)
|
|
2078
|
+
const personKeys = await scanKeys(redis, 'cachly:ckg:node:person:*', { max: 2000 });
|
|
2056
2079
|
if (personKeys.length === 0) {
|
|
2057
2080
|
return [
|
|
2058
2081
|
`## 🗺️ Team Expertise Map`,
|
|
@@ -2113,14 +2136,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2113
2136
|
case 'skill_gaps': {
|
|
2114
2137
|
const { instance_id, min_failures = 1, } = args;
|
|
2115
2138
|
const redis = await getConnection(instance_id);
|
|
2116
|
-
// Scan all best lessons
|
|
2117
|
-
const lessonKeys =
|
|
2118
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 500 });
|
|
2119
|
-
await new Promise((res, rej) => {
|
|
2120
|
-
lStream.on('data', (b) => lessonKeys.push(...b));
|
|
2121
|
-
lStream.on('end', res);
|
|
2122
|
-
lStream.on('error', rej);
|
|
2123
|
-
});
|
|
2139
|
+
// Scan all best lessons (capped + timed out)
|
|
2140
|
+
const lessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 3000 });
|
|
2124
2141
|
const domainMap = new Map();
|
|
2125
2142
|
let totalAttributed = 0;
|
|
2126
2143
|
let totalPrivate = 0;
|
|
@@ -2161,14 +2178,6 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2161
2178
|
});
|
|
2162
2179
|
}
|
|
2163
2180
|
}
|
|
2164
|
-
// Check for domains with person nodes but no lessons (contributors without lessons)
|
|
2165
|
-
const personKeys = [];
|
|
2166
|
-
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2167
|
-
await new Promise((res, rej) => {
|
|
2168
|
-
pStream.on('data', (b) => personKeys.push(...b));
|
|
2169
|
-
pStream.on('end', res);
|
|
2170
|
-
pStream.on('error', rej);
|
|
2171
|
-
});
|
|
2172
2181
|
const lines = [
|
|
2173
2182
|
`## 🔍 Skill Gaps — Knowledge Blind Spots`,
|
|
2174
2183
|
``,
|
|
@@ -2201,30 +2210,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2201
2210
|
case 'brain_coverage': {
|
|
2202
2211
|
const { instance_id, repo_path = '.', } = args;
|
|
2203
2212
|
const redis = await getConnection(instance_id);
|
|
2204
|
-
// Count lessons
|
|
2205
|
-
const lessonKeys = [
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
lStream.on('error', rej);
|
|
2211
|
-
});
|
|
2212
|
-
// Count person nodes
|
|
2213
|
-
const personKeys = [];
|
|
2214
|
-
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2215
|
-
await new Promise((res, rej) => {
|
|
2216
|
-
pStream.on('data', (b) => personKeys.push(...b));
|
|
2217
|
-
pStream.on('end', res);
|
|
2218
|
-
pStream.on('error', rej);
|
|
2219
|
-
});
|
|
2220
|
-
// Count file nodes in CKG
|
|
2221
|
-
const fileKeys = [];
|
|
2222
|
-
const fStream = redis.scanStream({ match: 'cachly:ckg:node:file:*', count: 500 });
|
|
2223
|
-
await new Promise((res, rej) => {
|
|
2224
|
-
fStream.on('data', (b) => fileKeys.push(...b));
|
|
2225
|
-
fStream.on('end', res);
|
|
2226
|
-
fStream.on('error', rej);
|
|
2227
|
-
});
|
|
2213
|
+
// Count lessons, person + file nodes (each capped + timed out, gathered in parallel)
|
|
2214
|
+
const [lessonKeys, personKeys, fileKeys] = await Promise.all([
|
|
2215
|
+
scanKeys(redis, 'cachly:lesson:best:*', { max: 5000 }),
|
|
2216
|
+
scanKeys(redis, 'cachly:ckg:node:person:*', { max: 2000 }),
|
|
2217
|
+
scanKeys(redis, 'cachly:ckg:node:file:*', { max: 5000 }),
|
|
2218
|
+
]);
|
|
2228
2219
|
// Get git file count for comparison (optional — non-critical if not a git repo)
|
|
2229
2220
|
let gitFileCount = 0;
|
|
2230
2221
|
let _gitFileCountLabel = 'n/a';
|
|
@@ -2290,6 +2281,69 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2290
2281
|
`_Boost score: run \`brain_from_git\` · add \`author\` to \`learn_from_attempts\` · fix gaps with \`skill_gaps\`_`,
|
|
2291
2282
|
].join('\n');
|
|
2292
2283
|
}
|
|
2284
|
+
// ── brain_metrics ─────────────────────────────────────────────────────────
|
|
2285
|
+
// The three decisive metrics (PROGRESS.md §3 / VISION_10X.md §4):
|
|
2286
|
+
// 1. Time-to-first-recall — onboarding friction
|
|
2287
|
+
// 2. Recall-lift — the moat proof (vs. raw BM25)
|
|
2288
|
+
// 3. Team-knowledge-reuse — the value only a shared brain delivers
|
|
2289
|
+
case 'brain_metrics': {
|
|
2290
|
+
const { instance_id } = args;
|
|
2291
|
+
const redis = await getConnection(instance_id);
|
|
2292
|
+
const [bornAt, firstRecallAt, recallsTotalRaw, crossAuthorRaw, timeSavedRaw, reusePairs] = await Promise.all([
|
|
2293
|
+
redis.get(`cachly:stats:born_at:${instance_id}`).catch(() => null),
|
|
2294
|
+
redis.get(`cachly:stats:first_recall_at:${instance_id}`).catch(() => null),
|
|
2295
|
+
redis.get(`cachly:stats:recalls_total:${instance_id}`).catch(() => null),
|
|
2296
|
+
redis.get(`cachly:stats:cross_author_recalls:${instance_id}`).catch(() => null),
|
|
2297
|
+
redis.get(`cachly:stats:time_saved_mins:${instance_id}`).catch(() => null),
|
|
2298
|
+
redis.smembers(`cachly:stats:reuse_pairs:${instance_id}`).catch(() => []),
|
|
2299
|
+
]);
|
|
2300
|
+
// ── Metric 1: Time-to-first-recall ──────────────────────────────────────
|
|
2301
|
+
let ttfrLine;
|
|
2302
|
+
if (bornAt && firstRecallAt) {
|
|
2303
|
+
const ms = new Date(firstRecallAt).getTime() - new Date(bornAt).getTime();
|
|
2304
|
+
const mins = Math.max(0, Math.round(ms / 60000));
|
|
2305
|
+
const human = mins < 1 ? '<1 min' : mins < 60 ? `${mins} min` : `${(mins / 60).toFixed(1)} h`;
|
|
2306
|
+
const target = ms <= 2 * 60000 ? '🟢 under 2 min target' : ms <= 60 * 60000 ? '🟡 over 2 min' : '🔴 over 1 h';
|
|
2307
|
+
ttfrLine = `**${human}** from first lesson → first proven recall · ${target}`;
|
|
2308
|
+
}
|
|
2309
|
+
else if (bornAt && !firstRecallAt) {
|
|
2310
|
+
ttfrLine = `🟡 Brain has knowledge but no proven recall yet — try \`smart_recall\``;
|
|
2311
|
+
}
|
|
2312
|
+
else {
|
|
2313
|
+
ttfrLine = `⚪ Not enough data yet — store a lesson, then recall it`;
|
|
2314
|
+
}
|
|
2315
|
+
// ── Metric 2: Recall-lift (the moat proof) ──────────────────────────────
|
|
2316
|
+
// Published headline from Cachly-Bench (CI-defended in rerank.test.ts).
|
|
2317
|
+
const recallLiftLine = `**+22.2 % Precision@1**, **+10.9 % MRR**, **+8.1 % nDCG@5** vs. raw BM25`;
|
|
2318
|
+
// ── Metric 3: Team-knowledge-reuse ──────────────────────────────────────
|
|
2319
|
+
const recallsTotal = Number(recallsTotalRaw ?? 0);
|
|
2320
|
+
const crossAuthor = Number(crossAuthorRaw ?? 0);
|
|
2321
|
+
const reusePct = recallsTotal > 0 ? Math.round((crossAuthor / recallsTotal) * 100) : 0;
|
|
2322
|
+
const reuseTarget = reusePct >= 30 ? '🟢 above 30 % target' : recallsTotal === 0 ? '⚪ no recalls yet' : '🟡 below 30 % target';
|
|
2323
|
+
const timeSaved = Math.round(Number(timeSavedRaw ?? 0));
|
|
2324
|
+
const timeSavedHuman = timeSaved < 60 ? `${timeSaved} min` : `${(timeSaved / 60).toFixed(1)} h`;
|
|
2325
|
+
const lines = [
|
|
2326
|
+
`## 📈 Brain Metrics — the three that decide everything`,
|
|
2327
|
+
``,
|
|
2328
|
+
`### 1. Time-to-first-recall _(onboarding friction)_`,
|
|
2329
|
+
ttfrLine,
|
|
2330
|
+
``,
|
|
2331
|
+
`### 2. Recall-lift _(the moat proof)_`,
|
|
2332
|
+
recallLiftLine,
|
|
2333
|
+
`_Reproduce: \`npm run bench\` · CI-defended in \`rerank.test.ts\` · details in BENCH.md_`,
|
|
2334
|
+
``,
|
|
2335
|
+
`### 3. Team-knowledge-reuse _(the value only a shared brain delivers)_`,
|
|
2336
|
+
`**${reusePct}%** of proven recalls used a teammate's lesson (${crossAuthor}/${recallsTotal}) · ${reuseTarget}`,
|
|
2337
|
+
reusePairs.length > 0 ? `**${reusePairs.length}** distinct reuse relationship${reusePairs.length !== 1 ? 's' : ''} across the team` : '',
|
|
2338
|
+
``,
|
|
2339
|
+
`---`,
|
|
2340
|
+
`⏱️ **${timeSavedHuman}** total saved not re-researching known fixes.`,
|
|
2341
|
+
recallsTotal === 0
|
|
2342
|
+
? `_Tip: pass \`author="your-handle"\` to \`smart_recall\` so cross-author reuse can be tracked._`
|
|
2343
|
+
: `_These numbers compound: every learned lesson and every teammate raises all three._`,
|
|
2344
|
+
].filter(Boolean);
|
|
2345
|
+
return lines.join('\n');
|
|
2346
|
+
}
|
|
2293
2347
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
2294
2348
|
default:
|
|
2295
2349
|
return null;
|