knit-mcp 0.7.0 → 0.8.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.
@@ -1,20 +1,33 @@
1
+ import {
2
+ KNIT_INSTRUCTIONS
3
+ } from "./chunk-4PFEG4GQ.js";
4
+ import {
5
+ VERSION
6
+ } from "./chunk-UTVFELXS.js";
1
7
  import {
2
8
  appendSession,
9
+ getCachedLatestVersion,
3
10
  getRecentSessions,
4
11
  installAgentsForProject,
12
+ isNewerVersion,
13
+ loadAllSessions,
14
+ loadScanResult,
15
+ persistScanResult,
5
16
  pruneSessionsByAge,
17
+ scanIntegrations,
6
18
  searchSessions,
7
19
  sessionCount
8
- } from "./chunk-3XR77YJM.js";
20
+ } from "./chunk-N7R4P42P.js";
9
21
  import {
10
22
  scanProject
11
- } from "./chunk-7PPC6IG6.js";
23
+ } from "./chunk-JJ367RK5.js";
12
24
  import {
13
25
  appendGlobalLearning,
14
26
  buildGlobalLearning,
15
27
  getRecentGlobalLearnings,
28
+ loadAllGlobalLearnings,
16
29
  searchGlobalLearnings
17
- } from "./chunk-TRZ3LD6B.js";
30
+ } from "./chunk-TSIXVZCT.js";
18
31
  import {
19
32
  addEntry,
20
33
  getFalsePositives,
@@ -35,7 +48,10 @@ import {
35
48
  sessionsLogPath,
36
49
  teamsPath,
37
50
  worktreesRegistryPath
38
- } from "./chunk-HBMF62U4.js";
51
+ } from "./chunk-SBJMHDBM.js";
52
+ import {
53
+ notifyToolsListChanged
54
+ } from "./chunk-WMESQUZU.js";
39
55
 
40
56
  // src/mcp/handlers.ts
41
57
  import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, readdirSync, existsSync as existsSync4, renameSync as renameSync2, unlinkSync } from "fs";
@@ -723,11 +739,12 @@ var TOOL_REGISTRY = [
723
739
  // ── Tier 1 — Protocol Guard config (2) ──────────────────────────
724
740
  { tool: "knit_set_protocol_strictness", tier: 1, category: "protocol-config", rationale: "Universal \u2014 every install ships Protocol Guard" },
725
741
  { tool: "knit_get_protocol_strictness", tier: 1, category: "protocol-config", rationale: "Universal" },
726
- // ── Tier 1 — Diagnostics + meta (4) ─────────────────────────────
742
+ // ── Tier 1 — Diagnostics + meta (5) ─────────────────────────────
727
743
  { tool: "knit_brain_status", tier: 1, category: "diagnostics", rationale: "Health + token-accounting; universal" },
728
744
  { tool: "knit_list_features", tier: 1, category: "diagnostics", rationale: "The discoverability escape hatch itself" },
729
745
  { tool: "knit_enable_feature", tier: 1, category: "diagnostics", rationale: "Flip on a Tier-2/3 feature flag \u2014 must always be reachable so hidden tools are recoverable" },
730
746
  { tool: "knit_disable_feature", tier: 1, category: "diagnostics", rationale: "Flip off a previously-enabled feature flag" },
747
+ { tool: "knit_scan_integrations", tier: 1, category: "diagnostics", rationale: "Re-detect existing user workflow frameworks (Ruflo, gstack, CodeTour, custom CLAUDE.md) so Knit can integrate rather than overlap" },
731
748
  // ── Tier 2 — Team worktrees (9) ─────────────────────────────────
732
749
  { tool: "knit_spawn_team_worktree", tier: 2, category: "teams", rationale: "Multi-domain parallel write orchestration", enable_via: 'knit_enable_feature("teams") or auto-exposed when \u22653 domains detected' },
733
750
  { tool: "knit_finalize_team_worktree", tier: 2, category: "teams", rationale: "Merge/discard a team worktree", enable_via: 'knit_enable_feature("teams")' },
@@ -796,6 +813,225 @@ function isEnableableFeature(name) {
796
813
  return name === "teams" || name === "subagents" || name === "admin";
797
814
  }
798
815
 
816
+ // src/engine/retrieval/bm25.ts
817
+ var STOPWORDS = /* @__PURE__ */ new Set([
818
+ "a",
819
+ "an",
820
+ "and",
821
+ "are",
822
+ "as",
823
+ "at",
824
+ "be",
825
+ "by",
826
+ "for",
827
+ "from",
828
+ "has",
829
+ "have",
830
+ "i",
831
+ "in",
832
+ "is",
833
+ "it",
834
+ "its",
835
+ "of",
836
+ "on",
837
+ "or",
838
+ "that",
839
+ "the",
840
+ "this",
841
+ "to",
842
+ "was",
843
+ "were",
844
+ "will",
845
+ "with",
846
+ "s",
847
+ "t"
848
+ // possessives + contraction remnants after tokenization
849
+ ]);
850
+ function defaultTokenize(text) {
851
+ if (!text) return [];
852
+ const lower = text.toLowerCase();
853
+ const tokens = lower.split(/[^a-z0-9_]+/);
854
+ const out = [];
855
+ for (const t of tokens) {
856
+ if (t.length < 2) continue;
857
+ if (STOPWORDS.has(t)) continue;
858
+ out.push(t);
859
+ }
860
+ return out;
861
+ }
862
+ var BM25Index = class {
863
+ k1;
864
+ b;
865
+ tokenize;
866
+ docs = [];
867
+ /** Per-document token frequency map: docId → token → count. */
868
+ termFreq = /* @__PURE__ */ new Map();
869
+ /** Document lengths in tokens, indexed by docId. */
870
+ docLengths = /* @__PURE__ */ new Map();
871
+ /** Document frequency: token → number of docs containing it. */
872
+ docFreq = /* @__PURE__ */ new Map();
873
+ avgDocLength = 0;
874
+ constructor(documents = [], options = {}) {
875
+ this.k1 = options.k1 ?? 1.5;
876
+ this.b = options.b ?? 0.75;
877
+ this.tokenize = options.tokenize ?? defaultTokenize;
878
+ for (const doc of documents) this.addInternal(doc);
879
+ this.recomputeAvgDocLength();
880
+ }
881
+ /** Add a document. Triggers an avgDocLength recompute. For bulk additions
882
+ * during initial indexing, prefer constructor — same end state, single recompute. */
883
+ add(document) {
884
+ this.addInternal(document);
885
+ this.recomputeAvgDocLength();
886
+ }
887
+ /** Search the corpus. Returns up to `limit` documents ranked by BM25 score.
888
+ * Documents with zero score (no query terms match) are omitted entirely. */
889
+ search(query, limit = 10) {
890
+ const queryTerms = this.tokenize(query);
891
+ if (queryTerms.length === 0 || this.docs.length === 0) return [];
892
+ const scores = /* @__PURE__ */ new Map();
893
+ const N = this.docs.length;
894
+ for (const term of queryTerms) {
895
+ const df = this.docFreq.get(term) ?? 0;
896
+ if (df === 0) continue;
897
+ const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
898
+ for (const doc of this.docs) {
899
+ const tf = this.termFreq.get(doc.id)?.get(term) ?? 0;
900
+ if (tf === 0) continue;
901
+ const docLen = this.docLengths.get(doc.id) ?? 0;
902
+ const denom = tf + this.k1 * (1 - this.b + this.b * (docLen / (this.avgDocLength || 1)));
903
+ const tfNorm = tf * (this.k1 + 1) / denom;
904
+ scores.set(doc.id, (scores.get(doc.id) ?? 0) + idf * tfNorm);
905
+ }
906
+ }
907
+ const ranked = [];
908
+ for (const doc of this.docs) {
909
+ const score = scores.get(doc.id);
910
+ if (score === void 0 || score <= 0) continue;
911
+ ranked.push({ id: doc.id, score, document: doc });
912
+ }
913
+ ranked.sort((a, b) => b.score - a.score);
914
+ return ranked.slice(0, limit);
915
+ }
916
+ /** Number of indexed documents. */
917
+ size() {
918
+ return this.docs.length;
919
+ }
920
+ /** Number of unique tokens across the corpus — useful for diagnostics. */
921
+ vocabularySize() {
922
+ return this.docFreq.size;
923
+ }
924
+ // ── Internals ─────────────────────────────────────────────────
925
+ addInternal(doc) {
926
+ if (this.termFreq.has(doc.id)) {
927
+ this.removeInternal(doc.id);
928
+ }
929
+ this.docs.push(doc);
930
+ const tokens = this.tokenize(doc.text);
931
+ this.docLengths.set(doc.id, tokens.length);
932
+ const tfMap = /* @__PURE__ */ new Map();
933
+ for (const t of tokens) {
934
+ tfMap.set(t, (tfMap.get(t) ?? 0) + 1);
935
+ }
936
+ this.termFreq.set(doc.id, tfMap);
937
+ for (const t of tfMap.keys()) {
938
+ this.docFreq.set(t, (this.docFreq.get(t) ?? 0) + 1);
939
+ }
940
+ }
941
+ removeInternal(docId) {
942
+ const tfMap = this.termFreq.get(docId);
943
+ if (!tfMap) return;
944
+ for (const t of tfMap.keys()) {
945
+ const df = this.docFreq.get(t) ?? 0;
946
+ if (df <= 1) this.docFreq.delete(t);
947
+ else this.docFreq.set(t, df - 1);
948
+ }
949
+ this.termFreq.delete(docId);
950
+ this.docLengths.delete(docId);
951
+ const idx = this.docs.findIndex((d) => d.id === docId);
952
+ if (idx !== -1) this.docs.splice(idx, 1);
953
+ }
954
+ recomputeAvgDocLength() {
955
+ if (this.docs.length === 0) {
956
+ this.avgDocLength = 0;
957
+ return;
958
+ }
959
+ let total = 0;
960
+ for (const len of this.docLengths.values()) total += len;
961
+ this.avgDocLength = total / this.docs.length;
962
+ }
963
+ };
964
+
965
+ // src/engine/retrieval/rrf.ts
966
+ function rrfFuse(rankings, options = {}) {
967
+ const k = options.k ?? 60;
968
+ const acc = /* @__PURE__ */ new Map();
969
+ for (let rankerIdx = 0; rankerIdx < rankings.length; rankerIdx++) {
970
+ const ranking = rankings[rankerIdx];
971
+ for (const result of ranking) {
972
+ const contribution = 1 / (k + result.rank);
973
+ const entry = acc.get(result.id) ?? { score: 0, ranks: {} };
974
+ entry.score += contribution;
975
+ entry.ranks[rankerIdx] = result.rank;
976
+ acc.set(result.id, entry);
977
+ }
978
+ }
979
+ const fused = [];
980
+ for (const [id, { score, ranks }] of acc) {
981
+ fused.push({ id, score, ranks });
982
+ }
983
+ fused.sort((a, b) => b.score - a.score);
984
+ return options.limit !== void 0 ? fused.slice(0, options.limit) : fused;
985
+ }
986
+ function toRankedResults(scored) {
987
+ return scored.map((s, i) => ({ id: s.id, rank: i + 1 }));
988
+ }
989
+
990
+ // src/engine/retrieval/index.ts
991
+ function buildLearningsIndex(entries) {
992
+ const docs = entries.map((e) => ({
993
+ id: e.id,
994
+ text: [e.summary, e.lesson, e.approach ?? "", (e.tags ?? []).join(" "), (e.domains ?? []).join(" ")].filter(Boolean).join(" "),
995
+ metadata: { entry: e }
996
+ }));
997
+ return new BM25Index(docs);
998
+ }
999
+ function buildGlobalLearningsIndex(entries) {
1000
+ const docs = entries.map((e) => ({
1001
+ id: e.id,
1002
+ text: [e.summary, e.lesson, (e.tags ?? []).join(" "), e.projectName ?? ""].filter(Boolean).join(" "),
1003
+ metadata: { entry: e }
1004
+ }));
1005
+ return new BM25Index(docs);
1006
+ }
1007
+ function buildSessionsIndex(sessions) {
1008
+ const docs = sessions.map((s) => ({
1009
+ id: s.id,
1010
+ text: [
1011
+ s.summary ?? "",
1012
+ (s.tags ?? []).join(" "),
1013
+ s.branch ?? "",
1014
+ s.commits ?? "",
1015
+ (s.domainsTouched ?? []).join(" ")
1016
+ ].filter(Boolean).join(" "),
1017
+ metadata: { session: s }
1018
+ }));
1019
+ return new BM25Index(docs);
1020
+ }
1021
+ function diversifyByBranch(results, maxPerBranch = 2) {
1022
+ const counts = /* @__PURE__ */ new Map();
1023
+ const out = [];
1024
+ for (const r of results) {
1025
+ const session = r.document.metadata?.session;
1026
+ const branch = session?.branch ?? "(no-branch)";
1027
+ const c = counts.get(branch) ?? 0;
1028
+ if (c >= maxPerBranch) continue;
1029
+ counts.set(branch, c + 1);
1030
+ out.push(r);
1031
+ }
1032
+ return out;
1033
+ }
1034
+
799
1035
  // src/engine/teams.ts
800
1036
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
801
1037
  import { dirname as dirname2 } from "path";
@@ -1107,12 +1343,47 @@ function handleFindFanout(params, brain) {
1107
1343
  }
1108
1344
  function handleSearchLearnings(params, brain) {
1109
1345
  const domains = (params.domains || "").split(",").map((d) => d.trim()).filter(Boolean);
1110
- if (domains.length === 0) return JSON.stringify({ error: "domains parameter is required", query: [], results: [], count: 0 });
1111
- const results = queryByDomains(brain.knowledgeBase, domains);
1112
- if (results.length > 0) recordCacheHit(brain.knowledgeBase);
1346
+ const query = (params.query || "").trim();
1347
+ const limit = Math.max(1, Math.min(50, parseInt(params.limit || "10", 10) || 10));
1348
+ if (!query && domains.length === 0) {
1349
+ return JSON.stringify({
1350
+ error: "Provide either query (BM25 free-text) or domains (tag filter), or both. query=auth domains=#api filters BM25 results to entries tagged #api.",
1351
+ query: [],
1352
+ results: [],
1353
+ count: 0
1354
+ });
1355
+ }
1356
+ if (!query) {
1357
+ const results = queryByDomains(brain.knowledgeBase, domains);
1358
+ if (results.length > 0) recordCacheHit(brain.knowledgeBase);
1359
+ return JSON.stringify(buildLearningsResponse(results, domains, []));
1360
+ }
1361
+ const index = buildLearningsIndex(brain.knowledgeBase.entries);
1362
+ const bm25Hits = index.search(query, Math.min(limit * 3, 50));
1363
+ const fused = rrfFuse([toRankedResults(bm25Hits)], { k: 60 });
1364
+ const entryById = /* @__PURE__ */ new Map();
1365
+ for (const e of brain.knowledgeBase.entries) entryById.set(e.id, e);
1366
+ let entries = [];
1367
+ for (const f of fused) {
1368
+ const entry = entryById.get(f.id);
1369
+ if (!entry) continue;
1370
+ entries.push(entry);
1371
+ }
1372
+ if (domains.length > 0) {
1373
+ entries = entries.filter(
1374
+ (e) => e.tags.some((t) => domains.includes(t)) || e.domains.some((d) => domains.includes(d))
1375
+ );
1376
+ }
1377
+ entries = entries.slice(0, limit);
1378
+ if (entries.length > 0) recordCacheHit(brain.knowledgeBase);
1379
+ return JSON.stringify(buildLearningsResponse(entries, domains, [query]));
1380
+ }
1381
+ function buildLearningsResponse(results, domains, freeText) {
1113
1382
  const hasFailures = results.some((r) => r.outcome === "failure");
1114
- return JSON.stringify({
1115
- query: domains,
1383
+ const queryParts = [...freeText, ...domains];
1384
+ return {
1385
+ query: queryParts,
1386
+ retriever: freeText.length > 0 ? "bm25" : "tag-filter",
1116
1387
  results: results.map((r) => ({
1117
1388
  summary: r.summary,
1118
1389
  lesson: r.lesson,
@@ -1122,8 +1393,8 @@ function handleSearchLearnings(params, brain) {
1122
1393
  access_count: r.accessCount
1123
1394
  })),
1124
1395
  count: results.length,
1125
- instruction: results.length > 0 ? hasFailures ? `Found ${results.length} past learnings including FAILURES. Read the lessons carefully \u2014 avoid repeating past mistakes.` : `Found ${results.length} past learnings. Apply these lessons to your current task.` : "No past learnings for these domains. This is new territory \u2014 be thorough and record what you learn."
1126
- });
1396
+ instruction: results.length > 0 ? hasFailures ? `Found ${results.length} past learnings including FAILURES. Read the lessons carefully \u2014 avoid repeating past mistakes.` : `Found ${results.length} past learnings. Apply these lessons to your current task.` : freeText.length > 0 ? "No past learnings match this query. Try broader terms, or call knit_search_global_learnings to search across all your projects." : "No past learnings for these domains. This is new territory \u2014 be thorough and record what you learn."
1397
+ };
1127
1398
  }
1128
1399
  function handleGetFalsePositives(_params, brain) {
1129
1400
  const fps = getFalsePositives(brain.knowledgeBase);
@@ -1133,6 +1404,27 @@ function handleGetFalsePositives(_params, brain) {
1133
1404
  instruction: "Include these in review agent prompts as DO NOT FLAG items."
1134
1405
  });
1135
1406
  }
1407
+ var TOKEN_BUDGETS = {
1408
+ /** Generated CLAUDE.md block. v0.7 trim landed at ~2KB on typical projects;
1409
+ * 6.5KB target allows for projects with many domains / large project map. */
1410
+ claude_md_bytes: 6500,
1411
+ /** Tier-gated tools/list response. v0.7 typical: 26 active × ~280 bytes ≈ 7.3KB.
1412
+ * 8.5KB target allows Tier-2 team tools to come online on ≥3-domain projects
1413
+ * without crossing into warn. Full 38-tool exposure (everything enabled)
1414
+ * sits in warn range, surfacing the bloat without blocking it. */
1415
+ tool_registry_bytes: 8500,
1416
+ /** MCP server `instructions` field — sent at handshake. v0.7 ships at ~2KB. */
1417
+ instructions_bytes: 2500,
1418
+ /** Sum of the three above — the per-session fixed cost Knit imposes.
1419
+ * v0.7 typical: ~12KB; 17.5KB target covers the union with slack. */
1420
+ per_session_overhead_bytes: 17500
1421
+ };
1422
+ function verdict(actual, target) {
1423
+ if (actual <= target) return "healthy";
1424
+ if (actual <= target * 1.25) return "warn";
1425
+ return "over-budget";
1426
+ }
1427
+ var CHARS_PER_TOKEN = 4;
1136
1428
  function handleBrainStatus(_params, brain) {
1137
1429
  const summary = getKBSummary(brain.knowledgeBase);
1138
1430
  const claudeMdBytes = (() => {
@@ -1142,8 +1434,51 @@ function handleBrainStatus(_params, brain) {
1142
1434
  return 0;
1143
1435
  }
1144
1436
  })();
1437
+ const shape = detectProjectShape(brain);
1438
+ const listing = computeFeatureListing(shape);
1439
+ const activeToolCount = listing.totals.active;
1440
+ const totalToolCount = listing.totals.total;
1441
+ const AVG_TOOL_DEF_BYTES = 280;
1442
+ const toolRegistryBytes = activeToolCount * AVG_TOOL_DEF_BYTES;
1443
+ const instructionsBytes = KNIT_INSTRUCTIONS.length;
1444
+ const perSessionOverheadBytes = claudeMdBytes + toolRegistryBytes + instructionsBytes;
1145
1445
  const totalSessions = sessionCount(brain.rootPath);
1146
1446
  const hitRate = summary.totalEntries > 0 ? Math.round(summary.accessedEntries / summary.totalEntries * 100) : 0;
1447
+ const budgets = {
1448
+ claude_md: {
1449
+ bytes: claudeMdBytes,
1450
+ kb: Math.round(claudeMdBytes / 1024 * 10) / 10,
1451
+ target_bytes: TOKEN_BUDGETS.claude_md_bytes,
1452
+ verdict: verdict(claudeMdBytes, TOKEN_BUDGETS.claude_md_bytes)
1453
+ },
1454
+ tool_registry: {
1455
+ active_tool_count: activeToolCount,
1456
+ total_tool_count: totalToolCount,
1457
+ bytes: toolRegistryBytes,
1458
+ target_bytes: TOKEN_BUDGETS.tool_registry_bytes,
1459
+ verdict: verdict(toolRegistryBytes, TOKEN_BUDGETS.tool_registry_bytes)
1460
+ },
1461
+ instructions: {
1462
+ bytes: instructionsBytes,
1463
+ target_bytes: TOKEN_BUDGETS.instructions_bytes,
1464
+ verdict: verdict(instructionsBytes, TOKEN_BUDGETS.instructions_bytes)
1465
+ },
1466
+ per_session_overhead: {
1467
+ bytes: perSessionOverheadBytes,
1468
+ kb: Math.round(perSessionOverheadBytes / 1024 * 10) / 10,
1469
+ tokens_estimate: Math.round(perSessionOverheadBytes / CHARS_PER_TOKEN),
1470
+ target_bytes: TOKEN_BUDGETS.per_session_overhead_bytes,
1471
+ verdict: verdict(perSessionOverheadBytes, TOKEN_BUDGETS.per_session_overhead_bytes)
1472
+ }
1473
+ };
1474
+ const verdicts = [budgets.claude_md.verdict, budgets.tool_registry.verdict, budgets.instructions.verdict, budgets.per_session_overhead.verdict];
1475
+ const overall = verdicts.includes("over-budget") ? "over-budget" : verdicts.includes("warn") ? "warn" : "healthy";
1476
+ const compounding = {
1477
+ session_count: totalSessions,
1478
+ total_learnings: summary.totalEntries,
1479
+ learnings_hit_rate_pct: hitRate,
1480
+ note: totalSessions === 0 ? "Fresh brain \u2014 no sessions yet. Compounding kicks in around session 3." : hitRate >= 30 ? "Strong compounding \u2014 learnings are getting reused across sessions." : hitRate < 20 && summary.totalEntries > 10 ? "Low hit rate \u2014 many learnings unused. Consider pruning stale entries." : "Compounding building up."
1481
+ };
1147
1482
  return JSON.stringify({
1148
1483
  ...summary,
1149
1484
  knowledge_index: {
@@ -1152,14 +1487,45 @@ function handleBrainStatus(_params, brain) {
1152
1487
  import_edges: Object.keys(brain.knowledge.importGraph).length,
1153
1488
  exports_mapped: Object.keys(brain.knowledge.exports).length
1154
1489
  },
1490
+ // Back-compat: the flat token_accounting shape from pre-v0.7.2 is kept so
1491
+ // anything that hard-coded those field names still works.
1155
1492
  token_accounting: {
1156
1493
  claude_md_bytes: claudeMdBytes,
1157
- claude_md_kb: Math.round(claudeMdBytes / 1024 * 10) / 10,
1494
+ claude_md_kb: budgets.claude_md.kb,
1158
1495
  session_count: totalSessions,
1159
1496
  learnings_hit_rate_pct: hitRate,
1160
- note: claudeMdBytes > 3e4 ? "CLAUDE.md is large \u2014 consider trimming. Tax exceeds typical savings." : hitRate < 20 && summary.totalEntries > 10 ? "Low hit rate \u2014 many learnings unused. Consider pruning stale entries." : "Healthy."
1497
+ note: budgets.per_session_overhead.verdict === "over-budget" ? "Per-session overhead exceeds budget \u2014 see token_budget for the offending surface." : compounding.note
1498
+ },
1499
+ // v0.7.2 — structured per-surface budget with target ceilings + verdicts.
1500
+ token_budget: {
1501
+ budgets,
1502
+ overall_verdict: overall,
1503
+ compounding
1161
1504
  },
1162
1505
  cache_age_ms: Date.now() - brain.loadedAt,
1506
+ ...(() => {
1507
+ const latest = getCachedLatestVersion();
1508
+ if (!latest || !isNewerVersion(latest, VERSION)) return {};
1509
+ return {
1510
+ update_available: {
1511
+ current: VERSION,
1512
+ latest,
1513
+ upgrade: 'Restart Claude Code to spawn a fresh MCP \u2014 npx will auto-fetch the new version. If your ~/.claude.json pins a specific version, change it to "knit-mcp@latest".',
1514
+ changelog: "https://github.com/PDgit12/knit/blob/main/CHANGELOG.md"
1515
+ }
1516
+ };
1517
+ })(),
1518
+ ...(() => {
1519
+ const integrations = loadScanResult(brain.rootPath);
1520
+ if (!integrations) return {};
1521
+ return {
1522
+ integrations: {
1523
+ scanned_at: integrations.scannedAt,
1524
+ detected: integrations.detected,
1525
+ summary: integrations.summary
1526
+ }
1527
+ };
1528
+ })(),
1163
1529
  instruction: "Brain is ready. Next: call knit_classify_task with the files you plan to touch to get your tier and phases."
1164
1530
  });
1165
1531
  }
@@ -1199,7 +1565,7 @@ function saveEnabledFeatures(rootPath, enabled) {
1199
1565
  function detectProjectShape(brain) {
1200
1566
  return {
1201
1567
  hasAnalyzableCode: brain.knowledge.summary.totalFiles >= 10,
1202
- domainCount: brain.config.domains?.length ?? 0,
1568
+ domainCount: brain.config?.domains?.length ?? 0,
1203
1569
  hasInstalledSubagents: existsSync4(projectAgentsDir(brain.rootPath)),
1204
1570
  sessionCount: sessionCount(brain.rootPath),
1205
1571
  enabledFeatures: loadEnabledFeatures(brain.rootPath)
@@ -1233,14 +1599,31 @@ function handleEnableFeature(params, brain) {
1233
1599
  enabled.add(feature);
1234
1600
  if (!wasAlreadyOn) {
1235
1601
  saveEnabledFeatures(brain.rootPath, enabled);
1602
+ notifyToolsListChanged();
1236
1603
  }
1237
1604
  return JSON.stringify({
1238
1605
  status: wasAlreadyOn ? "already-enabled" : "enabled",
1239
1606
  feature,
1240
1607
  enabled_features: [...enabled].sort(),
1241
- instruction: "New tools may now appear in tools/list on the next request. Call knit_list_features to confirm."
1608
+ instruction: wasAlreadyOn ? "Already enabled. Call knit_list_features to see the active tool list." : "Tools list updated for this session. The newly-enabled tools should be available immediately \u2014 call knit_list_features to confirm."
1242
1609
  });
1243
1610
  }
1611
+ function handleScanIntegrations(_params, brain) {
1612
+ try {
1613
+ const result = scanIntegrations(brain.rootPath, { knitVersion: VERSION });
1614
+ persistScanResult(brain.rootPath, result);
1615
+ return JSON.stringify({
1616
+ status: "scanned",
1617
+ ...result,
1618
+ instruction: result.detected.ruflo.present || result.detected.gstack.present || result.detected.codetour.present || result.detected.conductor.present ? "Existing frameworks detected. v0.7.2 surfaces them under knit_brain_status; v0.8 will tailor server instructions to defer to them where appropriate." : "No existing workflow frameworks detected. Knit operates in full-protocol mode."
1619
+ });
1620
+ } catch (err) {
1621
+ return JSON.stringify({
1622
+ status: "error",
1623
+ error: err instanceof Error ? err.message : String(err)
1624
+ });
1625
+ }
1626
+ }
1244
1627
  function handleDisableFeature(params, brain) {
1245
1628
  const feature = (params.feature || "").trim().toLowerCase();
1246
1629
  if (!isEnableableFeature(feature)) {
@@ -1253,12 +1636,13 @@ function handleDisableFeature(params, brain) {
1253
1636
  const wasOn = enabled.delete(feature);
1254
1637
  if (wasOn) {
1255
1638
  saveEnabledFeatures(brain.rootPath, enabled);
1639
+ notifyToolsListChanged();
1256
1640
  }
1257
1641
  return JSON.stringify({
1258
1642
  status: wasOn ? "disabled" : "already-disabled",
1259
1643
  feature,
1260
1644
  enabled_features: [...enabled].sort(),
1261
- instruction: "Disabled tools will be filtered out of tools/list on the next request."
1645
+ instruction: wasOn ? "Tools list updated for this session. Opt-in-only tools for this feature are no longer visible." : "Already disabled. Auto-exposed tools (e.g. teams when \u22653 domains) stay visible regardless of this flag."
1262
1646
  });
1263
1647
  }
1264
1648
  function detectsInquiryIntent(description) {
@@ -1715,9 +2099,28 @@ function handleSearchGlobalLearnings(params, _brain) {
1715
2099
  if (!query) {
1716
2100
  return JSON.stringify({ error: "query is required", results: [] });
1717
2101
  }
1718
- const matches = searchGlobalLearnings(query, limit);
2102
+ const entries = loadAllGlobalLearnings();
2103
+ let matches = [];
2104
+ let retriever = "bm25";
2105
+ if (entries.length > 0) {
2106
+ const index = buildGlobalLearningsIndex(entries);
2107
+ const bm25Hits = index.search(query, Math.min(limit * 3, 50));
2108
+ const fused = rrfFuse([toRankedResults(bm25Hits)], { k: 60 });
2109
+ const byId = /* @__PURE__ */ new Map();
2110
+ for (const e of entries) byId.set(e.id, e);
2111
+ for (const f of fused) {
2112
+ const entry = byId.get(f.id);
2113
+ if (entry) matches.push(entry);
2114
+ if (matches.length >= limit) break;
2115
+ }
2116
+ }
2117
+ if (matches.length === 0) {
2118
+ matches = searchGlobalLearnings(query, limit);
2119
+ if (matches.length > 0) retriever = "substring-fallback";
2120
+ }
1719
2121
  return JSON.stringify({
1720
2122
  query,
2123
+ retriever,
1721
2124
  count: matches.length,
1722
2125
  results: matches.map((m) => ({
1723
2126
  id: m.id,
@@ -1965,14 +2368,33 @@ function handleFinalizeTeamWorktree(params, brain) {
1965
2368
  }
1966
2369
  }
1967
2370
  function handleSearchSessions(params, brain) {
1968
- const query = params.query || "";
2371
+ const query = (params.query || "").trim();
1969
2372
  const limit = Math.max(1, Math.min(50, parseInt(params.limit || "10", 10) || 10));
1970
- if (!query.trim()) {
2373
+ if (!query) {
1971
2374
  return JSON.stringify({ error: "query is required", results: [] });
1972
2375
  }
1973
- const matches = searchSessions(brain.rootPath, query, limit);
2376
+ const sessions = loadAllSessions(brain.rootPath);
2377
+ let matches = [];
2378
+ let retriever = "bm25";
2379
+ if (sessions.length > 0) {
2380
+ const index = buildSessionsIndex(sessions);
2381
+ const bm25Hits = index.search(query, Math.min(limit * 5, 50));
2382
+ const diversified = diversifyByBranch(bm25Hits, 2);
2383
+ const byId = /* @__PURE__ */ new Map();
2384
+ for (const s of sessions) byId.set(s.id, s);
2385
+ for (const r of diversified) {
2386
+ const session = byId.get(r.id);
2387
+ if (session) matches.push(session);
2388
+ if (matches.length >= limit) break;
2389
+ }
2390
+ }
2391
+ if (matches.length === 0) {
2392
+ matches = searchSessions(brain.rootPath, query, limit);
2393
+ if (matches.length > 0) retriever = "substring-fallback";
2394
+ }
1974
2395
  return JSON.stringify({
1975
2396
  query,
2397
+ retriever,
1976
2398
  count: matches.length,
1977
2399
  results: matches.map((s) => ({
1978
2400
  id: s.id,
@@ -2035,8 +2457,15 @@ function getToolDefinitions() {
2035
2457
  },
2036
2458
  {
2037
2459
  name: "knit_search_learnings",
2038
- description: "Search learnings by domain tag.",
2039
- inputSchema: { type: "object", properties: { domains: { type: "string", description: "Comma-separated domain tags." } }, required: ["domains"] }
2460
+ description: 'BM25 free-text + tag filter. Pass query="text" for BM25, domains="#tag" for tag filter, or both to combine.',
2461
+ inputSchema: {
2462
+ type: "object",
2463
+ properties: {
2464
+ query: { type: "string", description: "BM25 free-text query over summary/lesson/approach/tags." },
2465
+ domains: { type: "string", description: "Comma-separated tag filter; combines with query when both passed." },
2466
+ limit: { type: "string", description: "Max results (default 10, max 50)." }
2467
+ }
2468
+ }
2040
2469
  },
2041
2470
  {
2042
2471
  name: "knit_get_false_positives",
@@ -2292,6 +2721,11 @@ function getToolDefinitions() {
2292
2721
  properties: { feature: { type: "string", description: "One of: teams, subagents, admin." } },
2293
2722
  required: ["feature"]
2294
2723
  }
2724
+ },
2725
+ {
2726
+ name: "knit_scan_integrations",
2727
+ description: "Re-scan host for existing workflow frameworks (Ruflo, gstack, CodeTour). Runs implicitly at autoInit; this is the manual re-trigger.",
2728
+ inputSchema: { type: "object", properties: {} }
2295
2729
  }
2296
2730
  ];
2297
2731
  }
@@ -2344,7 +2778,8 @@ var handlers = {
2344
2778
  knit_get_protocol_strictness: handleGetProtocolStrictness,
2345
2779
  knit_list_features: handleListFeatures,
2346
2780
  knit_enable_feature: handleEnableFeature,
2347
- knit_disable_feature: handleDisableFeature
2781
+ knit_disable_feature: handleDisableFeature,
2782
+ knit_scan_integrations: handleScanIntegrations
2348
2783
  };
2349
2784
  function handleToolCall(toolName, params, brain) {
2350
2785
  if (params.file_path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knit-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Knit — second brain for Claude Code. MCP server giving any AI agent project-scoped memory, tiered workflow protocol, and parallel team worktrees.",
5
5
  "type": "module",
6
6
  "bin": {