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.
- package/README.md +21 -0
- package/dist/{cache-WGZ2C7BZ.js → cache-RRKF2IZ5.js} +5 -5
- package/dist/{instructions-33TUHLTK.js → chunk-4PFEG4GQ.js} +1 -0
- package/dist/{chunk-7PPC6IG6.js → chunk-JJ367RK5.js} +163 -1
- package/dist/{chunk-SLN5ABF5.js → chunk-MOOVNMIN.js} +1 -160
- package/dist/{chunk-3XR77YJM.js → chunk-N7R4P42P.js} +265 -9
- package/dist/{chunk-HBMF62U4.js → chunk-SBJMHDBM.js} +4 -0
- package/dist/{chunk-TRZ3LD6B.js → chunk-TSIXVZCT.js} +11 -1
- package/dist/chunk-UTVFELXS.js +9 -0
- package/dist/chunk-WMESQUZU.js +25 -0
- package/dist/{chunk-QMH2DT6K.js → chunk-WN4AQFMO.js} +25 -11
- package/dist/cli.js +19 -16
- package/dist/{export-IKPBLZOO.js → export-KALLYKWG.js} +2 -2
- package/dist/{install-agents-AXT6DMYM.js → install-agents-DLSH3FNC.js} +6 -6
- package/dist/instructions-YPXOZZHI.js +6 -0
- package/dist/notifier-4L27HKHI.js +10 -0
- package/dist/{refresh-MSN5YNPS.js → refresh-N4O2QLYS.js} +7 -7
- package/dist/{status-XN6VHO66.js → status-YK2KBG57.js} +1 -1
- package/dist/{tools-KRPOUYNT.js → tools-GN6GM55U.js} +459 -24
- package/package.json +1 -1
|
@@ -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-
|
|
20
|
+
} from "./chunk-N7R4P42P.js";
|
|
9
21
|
import {
|
|
10
22
|
scanProject
|
|
11
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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 (
|
|
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
|
-
|
|
1111
|
-
const
|
|
1112
|
-
if (
|
|
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
|
-
|
|
1115
|
-
|
|
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:
|
|
1494
|
+
claude_md_kb: budgets.claude_md.kb,
|
|
1158
1495
|
session_count: totalSessions,
|
|
1159
1496
|
learnings_hit_rate_pct: hitRate,
|
|
1160
|
-
note:
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
2373
|
+
if (!query) {
|
|
1971
2374
|
return JSON.stringify({ error: "query is required", results: [] });
|
|
1972
2375
|
}
|
|
1973
|
-
const
|
|
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: "
|
|
2039
|
-
inputSchema: {
|
|
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