knit-mcp 0.9.0 → 0.11.2
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 +368 -215
- package/dist/cache-3LPETDUT.js +19 -0
- package/dist/{chunk-XFS2XGZI.js → chunk-27TA2ZQZ.js} +24 -0
- package/dist/{chunk-KLNUEE3O.js → chunk-7UFS67HP.js} +9 -1
- package/dist/{chunk-4K4FHOKE.js → chunk-HROSQ5MS.js} +256 -344
- package/dist/{chunk-5NCSZYRJ.js → chunk-LV73YTVN.js} +5 -0
- package/dist/{chunk-B2KZ5UR7.js → chunk-ORKWLA33.js} +1 -1
- package/dist/{chunk-BU3VHX3W.js → chunk-RZOVZYTF.js} +12 -4
- package/dist/{chunk-7PPC6IG6.js → chunk-ST4X7LZT.js} +60 -2
- package/dist/chunk-TWHNYJAJ.js +328 -0
- package/dist/{chunk-FLNV2IQC.js → chunk-VB2TIR6L.js} +2 -2
- package/dist/{chunk-BAUQEFYY.js → chunk-WKQHCLLO.js} +45 -10
- package/dist/cli.js +18 -9
- package/dist/doctor-4DN2P2JR.js +179 -0
- package/dist/{export-I5Y26WUL.js → export-CGSEUYZA.js} +3 -3
- package/dist/{install-agents-D2KJQUH3.js → install-agents-OBDCWCPB.js} +8 -7
- package/dist/{instructions-4FI32YZU.js → instructions-JARSXQPO.js} +1 -1
- package/dist/{integration-scanner-PS47AHGM.js → integration-scanner-LBD2PIZ3.js} +3 -3
- package/dist/{refresh-BXN32CNA.js → refresh-SMJ2NGIW.js} +3 -3
- package/dist/{status-RQWRIM2Y.js → status-VJDB75X2.js} +2 -2
- package/dist/{tools-EISDGPS5.js → tools-MIROTK2A.js} +1146 -91
- package/package.json +2 -1
- package/dist/cache-JSN6ETUF.js +0 -18
|
@@ -1,79 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
prewarmLatestVersion,
|
|
4
|
-
pruneSessionsByAge
|
|
5
|
-
} from "./chunk-BU3VHX3W.js";
|
|
6
|
-
import {
|
|
7
|
-
buildKnowledge,
|
|
8
|
-
buildReverseDependencies
|
|
9
|
-
} from "./chunk-MOOVNMIN.js";
|
|
10
|
-
import {
|
|
11
|
-
scanProject
|
|
12
|
-
} from "./chunk-7PPC6IG6.js";
|
|
13
|
-
import {
|
|
14
|
-
readLearnings
|
|
15
|
-
} from "./chunk-M3YZOJNW.js";
|
|
16
|
-
import {
|
|
17
|
-
persistScanResult,
|
|
18
|
-
scanIntegrations
|
|
19
|
-
} from "./chunk-FLNV2IQC.js";
|
|
20
|
-
import {
|
|
21
|
-
KNIT_MARKER_START,
|
|
22
|
-
generateClaudeMd,
|
|
23
|
-
spliceKnitBlock
|
|
24
|
-
} from "./chunk-KLNUEE3O.js";
|
|
25
|
-
import {
|
|
26
|
-
importFromMarkdown,
|
|
27
|
-
loadKnowledgeBase,
|
|
28
|
-
saveKnowledgeBase
|
|
29
|
-
} from "./chunk-BAUQEFYY.js";
|
|
30
|
-
import {
|
|
2
|
+
claimMarkerPath,
|
|
31
3
|
classificationMarkerPath,
|
|
32
4
|
knowledgePath,
|
|
33
5
|
knowledgebasePath,
|
|
34
6
|
learningsDir,
|
|
35
7
|
learningsFilePath,
|
|
36
|
-
legacyClaudeDir,
|
|
37
|
-
legacyKnowledgePath,
|
|
38
|
-
legacyKnowledgebasePath,
|
|
39
|
-
legacyLearningsDir,
|
|
40
|
-
legacyTeamsPath,
|
|
41
|
-
migrationBreadcrumbPath,
|
|
42
8
|
projectDataDir,
|
|
43
9
|
protocolConfigPath,
|
|
44
10
|
searchMarkerPath,
|
|
45
11
|
sessionMarkerPath,
|
|
46
12
|
sessionsJsonlPath,
|
|
47
13
|
sessionsLogPath,
|
|
48
|
-
|
|
49
|
-
} from "./chunk-
|
|
50
|
-
|
|
51
|
-
// src/mcp/cache.ts
|
|
52
|
-
import { execSync } from "child_process";
|
|
53
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
54
|
-
import { join, basename, dirname } from "path";
|
|
55
|
-
|
|
56
|
-
// src/generators/learnings.ts
|
|
57
|
-
function generateLearningsContent(config) {
|
|
58
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
59
|
-
return `# Project Learnings \u2014 ${config.name}
|
|
60
|
-
|
|
61
|
-
> Recursive learning log. Check this BEFORE starting any task.
|
|
62
|
-
> Grep by \`#tag\` to find relevant lessons for the domain you're working in.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## ${date} Project initialized with Knit workflow
|
|
67
|
-
**Domain(s):** All \u2014 workflow infrastructure
|
|
68
|
-
**Approach:** Auto-detected stack (${config.stack.language}${config.stack.framework ? " + " + config.stack.framework : ""}), generated ${config.domains.length} domains, wired hooks for ${config.targetAgent}.
|
|
69
|
-
**Outcome:** Success \u2014 workflow infrastructure in place
|
|
70
|
-
**Lesson:** This learnings file is the institutional memory. Every task should append an entry. Every session should check relevant tags before starting work. The LEARN phase is a hard exit gate \u2014 no task completes without updating this file.
|
|
71
|
-
**Tags:** #workflow #all #bootstrap
|
|
72
|
-
`;
|
|
73
|
-
}
|
|
14
|
+
turnEditLogPath
|
|
15
|
+
} from "./chunk-27TA2ZQZ.js";
|
|
74
16
|
|
|
75
17
|
// src/generators/settings.ts
|
|
76
|
-
var HOOKS_VERSION =
|
|
18
|
+
var HOOKS_VERSION = 11;
|
|
77
19
|
function generateSettings(config, rootPath) {
|
|
78
20
|
return {
|
|
79
21
|
mcpServers: {
|
|
@@ -120,6 +62,8 @@ function generateHooks(config, rootPath) {
|
|
|
120
62
|
const CLASSIFIED_MARKER = classificationMarkerPath(rootPath);
|
|
121
63
|
const SESSION_MARKER = sessionMarkerPath(rootPath);
|
|
122
64
|
const SEARCH_MARKER = searchMarkerPath(rootPath);
|
|
65
|
+
const CLAIM_MARKER = claimMarkerPath(rootPath);
|
|
66
|
+
const TURN_EDIT_LOG = turnEditLogPath(rootPath);
|
|
123
67
|
const CLAUDE_MD = `${rootPath}/CLAUDE.md`;
|
|
124
68
|
void knowledgePath;
|
|
125
69
|
const hooks = {
|
|
@@ -164,6 +108,14 @@ function generateHooks(config, rootPath) {
|
|
|
164
108
|
// so the next non-trivial task has to call knit_search_learnings again.
|
|
165
109
|
const sm = ${jsLit(SEARCH_MARKER)};
|
|
166
110
|
if (fs.existsSync(sm)) fs.rmSync(sm, { force: true });
|
|
111
|
+
// v0.11 slice 1: per-turn claim-verified marker \u2014 Stop-hook gate
|
|
112
|
+
// requires fresh verify_claim per non-trivial task.
|
|
113
|
+
const cm = ${jsLit(CLAIM_MARKER)};
|
|
114
|
+
if (fs.existsSync(cm)) fs.rmSync(cm, { force: true });
|
|
115
|
+
// v0.11 slice 3: per-turn edit log \u2014 the Stop-hook drift detector
|
|
116
|
+
// re-classifies the actual touched set and surfaces scope creep.
|
|
117
|
+
const tl = ${jsLit(TURN_EDIT_LOG)};
|
|
118
|
+
if (fs.existsSync(tl)) fs.rmSync(tl, { force: true });
|
|
167
119
|
} catch (e) {}
|
|
168
120
|
`),
|
|
169
121
|
timeout: 5
|
|
@@ -309,6 +261,155 @@ function generateHooks(config, rootPath) {
|
|
|
309
261
|
PostToolUse: [],
|
|
310
262
|
Stop: []
|
|
311
263
|
};
|
|
264
|
+
hooks.PostToolUse.push({
|
|
265
|
+
_knitOwned: true,
|
|
266
|
+
matcher: "Write|Edit|MultiEdit",
|
|
267
|
+
hooks: [
|
|
268
|
+
{
|
|
269
|
+
type: "command",
|
|
270
|
+
command: nodeHook(`
|
|
271
|
+
let d = "";
|
|
272
|
+
process.stdin.on("data", (c) => d += c);
|
|
273
|
+
process.stdin.on("end", () => {
|
|
274
|
+
try {
|
|
275
|
+
const fs = require("fs");
|
|
276
|
+
const path = require("path");
|
|
277
|
+
const i = JSON.parse(d);
|
|
278
|
+
const ti = i.tool_input || {};
|
|
279
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
280
|
+
if (!f) return;
|
|
281
|
+
const logPath = ${jsLit(TURN_EDIT_LOG)};
|
|
282
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
283
|
+
fs.appendFileSync(logPath, JSON.stringify({ file: f, ts: new Date().toISOString() }) + "\\n");
|
|
284
|
+
} catch (e) { try { process.stderr.write('[knit] turn-edit appender hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
285
|
+
});
|
|
286
|
+
`),
|
|
287
|
+
timeout: 5
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
});
|
|
291
|
+
hooks.PostToolUse.push({
|
|
292
|
+
_knitOwned: true,
|
|
293
|
+
matcher: "Write|Edit|MultiEdit",
|
|
294
|
+
hooks: [
|
|
295
|
+
{
|
|
296
|
+
type: "command",
|
|
297
|
+
command: nodeHook(`
|
|
298
|
+
let d = "";
|
|
299
|
+
process.stdin.on("data", (c) => d += c);
|
|
300
|
+
process.stdin.on("end", () => {
|
|
301
|
+
try {
|
|
302
|
+
const fs = require("fs");
|
|
303
|
+
const i = JSON.parse(d);
|
|
304
|
+
const toolName = i.tool_name || "";
|
|
305
|
+
const ti = i.tool_input || {};
|
|
306
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
307
|
+
if (!f || !fs.existsSync(f)) return;
|
|
308
|
+
const cur = fs.readFileSync(f, "utf-8");
|
|
309
|
+
if (toolName === "Write") {
|
|
310
|
+
const intended = ti.content || "";
|
|
311
|
+
if (cur === intended) {
|
|
312
|
+
console.error("[knit] verify: write landed \u2014 " + f);
|
|
313
|
+
} else {
|
|
314
|
+
const lenDelta = cur.length - intended.length;
|
|
315
|
+
console.error("[knit] verify: write DRIFTED \u2014 " + f + " differs by " + lenDelta + " char(s) from intent. Something modified the file after Write.");
|
|
316
|
+
}
|
|
317
|
+
} else if (toolName === "Edit") {
|
|
318
|
+
const newStr = ti.new_string || "";
|
|
319
|
+
const oldStr = ti.old_string || "";
|
|
320
|
+
if (newStr && cur.indexOf(newStr) !== -1) {
|
|
321
|
+
console.error("[knit] verify: edit landed \u2014 " + f);
|
|
322
|
+
} else if (oldStr && cur.indexOf(oldStr) !== -1) {
|
|
323
|
+
console.error("[knit] verify: edit DRIFTED \u2014 " + f + " still contains the old_string. Edit may have silently failed.");
|
|
324
|
+
} else {
|
|
325
|
+
console.error("[knit] verify: edit ambiguous \u2014 " + f + " contains neither new_string nor old_string. Inspect manually.");
|
|
326
|
+
}
|
|
327
|
+
} else if (toolName === "MultiEdit") {
|
|
328
|
+
const edits = Array.isArray(ti.edits) ? ti.edits : [];
|
|
329
|
+
let landed = 0;
|
|
330
|
+
let drifted = 0;
|
|
331
|
+
for (const e of edits) {
|
|
332
|
+
if (e && e.new_string && cur.indexOf(e.new_string) !== -1) landed++;
|
|
333
|
+
else drifted++;
|
|
334
|
+
}
|
|
335
|
+
if (drifted === 0) {
|
|
336
|
+
console.error("[knit] verify: all " + landed + " edits landed \u2014 " + f);
|
|
337
|
+
} else {
|
|
338
|
+
console.error("[knit] verify: " + drifted + " of " + (landed + drifted) + " edits DRIFTED \u2014 " + f + ". Some new_string values not found in file post-edit.");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (e) { try { process.stderr.write('[knit] diff verifier hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
342
|
+
});
|
|
343
|
+
`),
|
|
344
|
+
timeout: 5,
|
|
345
|
+
statusMessage: "Knit: verifying edit landed..."
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
});
|
|
349
|
+
hooks.PostToolUse.push({
|
|
350
|
+
_knitOwned: true,
|
|
351
|
+
matcher: "Write|Edit|MultiEdit",
|
|
352
|
+
hooks: [
|
|
353
|
+
{
|
|
354
|
+
type: "command",
|
|
355
|
+
command: nodeHook(`
|
|
356
|
+
let d = "";
|
|
357
|
+
process.stdin.on("data", (c) => d += c);
|
|
358
|
+
process.stdin.on("end", () => {
|
|
359
|
+
try {
|
|
360
|
+
const fs = require("fs");
|
|
361
|
+
const path = require("path");
|
|
362
|
+
const cp = require("child_process");
|
|
363
|
+
const i = JSON.parse(d);
|
|
364
|
+
const ti = i.tool_input || {};
|
|
365
|
+
const f = ti.file_path || (i.tool_response && i.tool_response.filePath) || "";
|
|
366
|
+
if (!/\\.(?:ts|tsx|mts|cts)$/.test(f)) return;
|
|
367
|
+
// Walk up to find tsconfig.json (project root).
|
|
368
|
+
let dir = path.dirname(f);
|
|
369
|
+
let projectRoot = null;
|
|
370
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
371
|
+
if (fs.existsSync(path.join(dir, "tsconfig.json"))) { projectRoot = dir; break; }
|
|
372
|
+
const parent = path.dirname(dir);
|
|
373
|
+
if (parent === dir) break;
|
|
374
|
+
dir = parent;
|
|
375
|
+
}
|
|
376
|
+
if (!projectRoot) return;
|
|
377
|
+
// Prefer local tsc; fall back to npx.
|
|
378
|
+
const localTsc = path.join(projectRoot, "node_modules", ".bin", "tsc");
|
|
379
|
+
const tscBin = fs.existsSync(localTsc) ? localTsc : "npx";
|
|
380
|
+
const tscArgs = fs.existsSync(localTsc) ? ["--noEmit", "--pretty", "false"] : ["--no-install", "tsc", "--noEmit", "--pretty", "false"];
|
|
381
|
+
let out = "";
|
|
382
|
+
let failed = false;
|
|
383
|
+
try {
|
|
384
|
+
cp.execFileSync(tscBin, tscArgs, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"], timeout: 15000, encoding: "utf-8" });
|
|
385
|
+
} catch (err) {
|
|
386
|
+
failed = true;
|
|
387
|
+
out = (err && (err.stdout || err.stderr) || "").toString();
|
|
388
|
+
}
|
|
389
|
+
if (!failed) {
|
|
390
|
+
console.error("[knit] tsc check: clean \u2014 " + f);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Filter tsc output to errors mentioning the touched file (or its dir).
|
|
394
|
+
const touched = path.basename(f);
|
|
395
|
+
const lines = out.split("\\n").map((s) => s.trim()).filter(Boolean);
|
|
396
|
+
const relevant = lines.filter((l) => l.indexOf(touched) !== -1 || l.indexOf(f) !== -1);
|
|
397
|
+
const errorCount = lines.filter((l) => /error TS\\d+:/.test(l)).length;
|
|
398
|
+
if (relevant.length > 0) {
|
|
399
|
+
console.error("[knit] tsc check: " + relevant.length + " error(s) referencing " + touched + ":");
|
|
400
|
+
for (const l of relevant.slice(0, 6)) console.error(" " + l);
|
|
401
|
+
if (relevant.length > 6) console.error(" ... and " + (relevant.length - 6) + " more");
|
|
402
|
+
} else if (errorCount > 0) {
|
|
403
|
+
console.error("[knit] tsc check: project has " + errorCount + " type error(s) (none in " + touched + " directly \u2014 likely a cross-file ripple). Run \`npx tsc --noEmit\` for full output.");
|
|
404
|
+
}
|
|
405
|
+
} catch (e) { try { process.stderr.write('[knit] tsc check hook failed: ' + (e && e.message ? e.message : e) + '\\n'); } catch {} }
|
|
406
|
+
});
|
|
407
|
+
`),
|
|
408
|
+
timeout: 20,
|
|
409
|
+
statusMessage: "Knit: tsc on edit..."
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
});
|
|
312
413
|
hooks.PostToolUse.push({
|
|
313
414
|
_knitOwned: true,
|
|
314
415
|
matcher: "Write|Edit|MultiEdit",
|
|
@@ -352,32 +453,6 @@ function generateHooks(config, rootPath) {
|
|
|
352
453
|
}
|
|
353
454
|
]
|
|
354
455
|
});
|
|
355
|
-
if (config.stack.language === "typescript" && config.stack.typecheckCommand) {
|
|
356
|
-
hooks.PostToolUse.push({
|
|
357
|
-
_knitOwned: true,
|
|
358
|
-
matcher: "Write|Edit",
|
|
359
|
-
hooks: [
|
|
360
|
-
{
|
|
361
|
-
type: "command",
|
|
362
|
-
command: nodeHook(`
|
|
363
|
-
let d = "";
|
|
364
|
-
process.stdin.on("data", (c) => d += c);
|
|
365
|
-
process.stdin.on("end", () => {
|
|
366
|
-
try {
|
|
367
|
-
const i = JSON.parse(d);
|
|
368
|
-
const f = (i.tool_input && i.tool_input.file_path) || (i.tool_response && i.tool_response.filePath) || "";
|
|
369
|
-
if (!/\\.tsx?$/.test(f)) return;
|
|
370
|
-
${REPO_ROOT_JS}
|
|
371
|
-
require("child_process").execSync("npx tsc --noEmit --pretty false", { cwd: __getRoot(), stdio: "inherit" });
|
|
372
|
-
} catch (e) {}
|
|
373
|
-
});
|
|
374
|
-
`),
|
|
375
|
-
timeout: 30,
|
|
376
|
-
statusMessage: "Type checking..."
|
|
377
|
-
}
|
|
378
|
-
]
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
456
|
if (config.stack.language === "python") {
|
|
382
457
|
hooks.PostToolUse.push({
|
|
383
458
|
_knitOwned: true,
|
|
@@ -482,6 +557,97 @@ function generateHooks(config, rootPath) {
|
|
|
482
557
|
]
|
|
483
558
|
});
|
|
484
559
|
}
|
|
560
|
+
hooks.Stop.push({
|
|
561
|
+
_knitOwned: true,
|
|
562
|
+
hooks: [
|
|
563
|
+
{
|
|
564
|
+
type: "command",
|
|
565
|
+
command: nodeHook(`
|
|
566
|
+
try {
|
|
567
|
+
const fs = require("fs");
|
|
568
|
+
const path = require("path");
|
|
569
|
+
const classifiedPath = ${jsLit(CLASSIFIED_MARKER)};
|
|
570
|
+
const logPath = ${jsLit(TURN_EDIT_LOG)};
|
|
571
|
+
if (!fs.existsSync(classifiedPath) || !fs.existsSync(logPath)) return;
|
|
572
|
+
let marker;
|
|
573
|
+
try {
|
|
574
|
+
marker = JSON.parse(fs.readFileSync(classifiedPath, "utf-8"));
|
|
575
|
+
} catch (e) { return; }
|
|
576
|
+
const originalScope = (marker && (marker.scopeTier || marker.tier)) || "";
|
|
577
|
+
const originalRisk = (marker && marker.riskTier) || "low";
|
|
578
|
+
if (!originalScope) return;
|
|
579
|
+
// Parse turn-edit log \u2192 unique file set.
|
|
580
|
+
const seen = new Set();
|
|
581
|
+
const raw = fs.readFileSync(logPath, "utf-8");
|
|
582
|
+
for (const line of raw.split("\\n")) {
|
|
583
|
+
if (!line.trim()) continue;
|
|
584
|
+
try { const e = JSON.parse(line); if (e.file) seen.add(e.file); } catch (e2) {}
|
|
585
|
+
}
|
|
586
|
+
const touched = Array.from(seen);
|
|
587
|
+
const n = touched.length;
|
|
588
|
+
if (n === 0) return;
|
|
589
|
+
// Scope drift: trivial classification but turn touched 3+ files.
|
|
590
|
+
const scopeDrift = (originalScope === "trivial" && n >= 3) ||
|
|
591
|
+
(originalScope === "standard" && n >= 6);
|
|
592
|
+
// Risk drift: low-risk classification but turn touched risky files.
|
|
593
|
+
const riskyPatterns = /(\\btypes?\\.tsx?|\\bschema\\.|\\bauth\\.|\\bsecurity\\.|migrations?\\/)/i;
|
|
594
|
+
const riskyHit = touched.find((f) => riskyPatterns.test(f));
|
|
595
|
+
const riskDrift = originalRisk === "low" && !!riskyHit;
|
|
596
|
+
if (!scopeDrift && !riskDrift) return;
|
|
597
|
+
console.error("[knit] drift detector \u2014 turn touched " + n + " file(s); classification was scope=" + originalScope + ", risk=" + originalRisk);
|
|
598
|
+
if (scopeDrift) {
|
|
599
|
+
console.error(" scope drift: " + n + " files exceeds the " + originalScope + " threshold. Next time, re-classify when scope grows.");
|
|
600
|
+
}
|
|
601
|
+
if (riskDrift) {
|
|
602
|
+
console.error(" risk drift: low-risk classification but touched " + riskyHit + " \u2014 high-risk pattern (types/schema/auth/migrations). Should have triggered plan mode.");
|
|
603
|
+
}
|
|
604
|
+
console.error(" touched: " + touched.slice(0, 8).join(", ") + (touched.length > 8 ? " ... and " + (touched.length - 8) + " more" : ""));
|
|
605
|
+
} catch (e) {}
|
|
606
|
+
`),
|
|
607
|
+
timeout: 5,
|
|
608
|
+
statusMessage: "Knit: drift detector..."
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
});
|
|
612
|
+
hooks.Stop.push({
|
|
613
|
+
_knitOwned: true,
|
|
614
|
+
hooks: [
|
|
615
|
+
{
|
|
616
|
+
type: "command",
|
|
617
|
+
command: nodeHook(`
|
|
618
|
+
try {
|
|
619
|
+
const fs = require("fs");
|
|
620
|
+
const cfgPath = ${jsLit(PROTOCOL_CONFIG)};
|
|
621
|
+
const classifiedPath = ${jsLit(CLASSIFIED_MARKER)};
|
|
622
|
+
const claimPath = ${jsLit(CLAIM_MARKER)};
|
|
623
|
+
let level = "warn";
|
|
624
|
+
if (fs.existsSync(cfgPath)) {
|
|
625
|
+
try {
|
|
626
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
627
|
+
if (cfg && (cfg.level === "off" || cfg.level === "warn" || cfg.level === "block")) level = cfg.level;
|
|
628
|
+
} catch (parseErr) {}
|
|
629
|
+
}
|
|
630
|
+
if (level === "off") return;
|
|
631
|
+
if (!fs.existsSync(classifiedPath)) return;
|
|
632
|
+
let scope;
|
|
633
|
+
try {
|
|
634
|
+
const marker = JSON.parse(fs.readFileSync(classifiedPath, "utf-8"));
|
|
635
|
+
scope = (marker && (marker.scopeTier || marker.tier)) || "";
|
|
636
|
+
} catch (e) { return; }
|
|
637
|
+
if (scope !== "standard" && scope !== "complex") return;
|
|
638
|
+
if (fs.existsSync(claimPath)) return;
|
|
639
|
+
if (level === "block") {
|
|
640
|
+
console.error("[knit] BLOCKED: " + scope + " task ended without verify_claim. The REVIEW gate requires \u22651 knit_verify_claim call before LEARN \u2014 verify the agent's claims against the knowledge graph or rerun.");
|
|
641
|
+
process.exit(2);
|
|
642
|
+
}
|
|
643
|
+
console.error("[knit] reminder: " + scope + " task ended without knit_verify_claim. The REVIEW gate is the anti-slop guard \u2014 verify the agent's claims against the graph before declaring done. Set strictness=block via knit_set_protocol_strictness to make this hard.");
|
|
644
|
+
} catch (e) {}
|
|
645
|
+
`),
|
|
646
|
+
timeout: 5,
|
|
647
|
+
statusMessage: "Knit: REVIEW claim-gate..."
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
});
|
|
485
651
|
hooks.Stop.push({
|
|
486
652
|
_knitOwned: true,
|
|
487
653
|
hooks: [
|
|
@@ -628,261 +794,7 @@ function generateHooks(config, rootPath) {
|
|
|
628
794
|
return hooks;
|
|
629
795
|
}
|
|
630
796
|
|
|
631
|
-
// src/mcp/cache.ts
|
|
632
|
-
var cache = null;
|
|
633
|
-
var hooksRefreshed = /* @__PURE__ */ new Set();
|
|
634
|
-
function maybeRefreshHooks(rootPath, config) {
|
|
635
|
-
if (hooksRefreshed.has(rootPath)) return;
|
|
636
|
-
hooksRefreshed.add(rootPath);
|
|
637
|
-
const settingsPath = join(rootPath, ".claude", "settings.local.json");
|
|
638
|
-
if (!existsSync(settingsPath)) return;
|
|
639
|
-
try {
|
|
640
|
-
const existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
641
|
-
const storedVersion = existing?._knitHooks?.version ?? (existing?._engramHooks ? 0 : 0);
|
|
642
|
-
if (storedVersion < HOOKS_VERSION) {
|
|
643
|
-
writeKnitHooks(rootPath, config);
|
|
644
|
-
}
|
|
645
|
-
} catch {
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
function getBrain(rootPath) {
|
|
649
|
-
if (cache && cache.rootPath === rootPath) {
|
|
650
|
-
return cache;
|
|
651
|
-
}
|
|
652
|
-
void prewarmLatestVersion();
|
|
653
|
-
let autoInitialized = false;
|
|
654
|
-
const haveCentralized = existsSync(knowledgePath(rootPath));
|
|
655
|
-
const haveLegacy = existsSync(legacyKnowledgePath(rootPath));
|
|
656
|
-
if (!haveCentralized) {
|
|
657
|
-
if (haveLegacy) {
|
|
658
|
-
migrateLegacyData(rootPath);
|
|
659
|
-
} else {
|
|
660
|
-
autoInitialize(rootPath);
|
|
661
|
-
autoInitialized = true;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const scan = scanProject(rootPath);
|
|
665
|
-
const knowledge = buildKnowledge(rootPath, scan);
|
|
666
|
-
const reverseDeps = buildReverseDependencies(knowledge.importGraph);
|
|
667
|
-
const projectName = detectProjectName(rootPath);
|
|
668
|
-
const knowledgeBase = loadKnowledgeBase(knowledgebasePath(rootPath), projectName);
|
|
669
|
-
const config = {
|
|
670
|
-
name: projectName,
|
|
671
|
-
packageManager: scan.packageManager,
|
|
672
|
-
stack: scan.stack,
|
|
673
|
-
domains: scan.domains,
|
|
674
|
-
targetAgent: "claude-code",
|
|
675
|
-
tokenOptimization: "standard"
|
|
676
|
-
};
|
|
677
|
-
writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
|
|
678
|
-
saveKnowledgeBase(knowledgebasePath(rootPath), knowledgeBase);
|
|
679
|
-
if (!autoInitialized) {
|
|
680
|
-
maybeRefreshHooks(rootPath, config);
|
|
681
|
-
}
|
|
682
|
-
cache = {
|
|
683
|
-
rootPath,
|
|
684
|
-
knowledge,
|
|
685
|
-
reverseDeps,
|
|
686
|
-
knowledgeBase,
|
|
687
|
-
config,
|
|
688
|
-
loadedAt: Date.now(),
|
|
689
|
-
autoInitialized
|
|
690
|
-
};
|
|
691
|
-
return cache;
|
|
692
|
-
}
|
|
693
|
-
function autoInitialize(rootPath) {
|
|
694
|
-
const scan = scanProject(rootPath);
|
|
695
|
-
const knowledge = buildKnowledge(rootPath, scan);
|
|
696
|
-
const projectName = detectProjectName(rootPath);
|
|
697
|
-
const config = {
|
|
698
|
-
name: projectName,
|
|
699
|
-
packageManager: scan.packageManager,
|
|
700
|
-
stack: scan.stack,
|
|
701
|
-
domains: scan.domains,
|
|
702
|
-
targetAgent: "claude-code",
|
|
703
|
-
tokenOptimization: "standard"
|
|
704
|
-
};
|
|
705
|
-
mkdirSync(projectDataDir(rootPath), { recursive: true });
|
|
706
|
-
mkdirSync(learningsDir(rootPath), { recursive: true });
|
|
707
|
-
writeProjectClaudeMd(rootPath, config, knowledge);
|
|
708
|
-
writeKnitHooks(rootPath, config);
|
|
709
|
-
installAgentsForProject(rootPath, config, knowledge, null).catch((err) => {
|
|
710
|
-
process.stderr.write(`[knit] agent install background error: ${err?.message ?? err}
|
|
711
|
-
`);
|
|
712
|
-
});
|
|
713
|
-
Promise.resolve().then(() => {
|
|
714
|
-
try {
|
|
715
|
-
pruneSessionsByAge(rootPath, 90);
|
|
716
|
-
} catch (e) {
|
|
717
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
718
|
-
process.stderr.write(`[knit] session prune background error: ${msg}
|
|
719
|
-
`);
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
Promise.resolve().then(() => {
|
|
723
|
-
try {
|
|
724
|
-
const result = scanIntegrations(rootPath);
|
|
725
|
-
persistScanResult(rootPath, result);
|
|
726
|
-
} catch (e) {
|
|
727
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
728
|
-
process.stderr.write(`[knit] integration scan background error: ${msg}
|
|
729
|
-
`);
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
const learningsPath = learningsFilePath(rootPath, projectName);
|
|
733
|
-
if (!existsSync(learningsPath)) {
|
|
734
|
-
writeFileSync(learningsPath, generateLearningsContent(config), "utf-8");
|
|
735
|
-
}
|
|
736
|
-
const kbPath = knowledgebasePath(rootPath);
|
|
737
|
-
const kb = loadKnowledgeBase(kbPath, projectName);
|
|
738
|
-
const entries = readLearnings(learningsPath);
|
|
739
|
-
importFromMarkdown(kb, entries);
|
|
740
|
-
saveKnowledgeBase(kbPath, kb);
|
|
741
|
-
writeFileSync(knowledgePath(rootPath), JSON.stringify(knowledge, null, 2), "utf-8");
|
|
742
|
-
}
|
|
743
|
-
function migrateLegacyData(rootPath) {
|
|
744
|
-
mkdirSync(projectDataDir(rootPath), { recursive: true });
|
|
745
|
-
mkdirSync(learningsDir(rootPath), { recursive: true });
|
|
746
|
-
copyIfExists(legacyKnowledgePath(rootPath), knowledgePath(rootPath));
|
|
747
|
-
copyIfExists(legacyKnowledgebasePath(rootPath), knowledgebasePath(rootPath));
|
|
748
|
-
copyIfExists(legacyTeamsPath(rootPath), teamsPath(rootPath));
|
|
749
|
-
const legacyLearn = legacyLearningsDir(rootPath);
|
|
750
|
-
if (existsSync(legacyLearn)) {
|
|
751
|
-
for (const file of readdirSync(legacyLearn)) {
|
|
752
|
-
const src = join(legacyLearn, file);
|
|
753
|
-
const dst = join(learningsDir(rootPath), file);
|
|
754
|
-
try {
|
|
755
|
-
if (statSync(src).isFile() && !existsSync(dst)) {
|
|
756
|
-
copyFileSync(src, dst);
|
|
757
|
-
}
|
|
758
|
-
} catch {
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
const breadcrumb = migrationBreadcrumbPath(rootPath);
|
|
763
|
-
const newPath = projectDataDir(rootPath);
|
|
764
|
-
if (!existsSync(breadcrumb) && existsSync(legacyClaudeDir(rootPath))) {
|
|
765
|
-
const note = `Knit data migrated to ~/.knit/ on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.
|
|
766
|
-
|
|
767
|
-
Centralized location for this project:
|
|
768
|
-
${newPath}
|
|
769
|
-
|
|
770
|
-
The legacy files in this .claude/ directory are no longer read by engram and
|
|
771
|
-
can be deleted at your discretion. Future learnings, knowledge indexes, and
|
|
772
|
-
session memory live in the new path.
|
|
773
|
-
`;
|
|
774
|
-
try {
|
|
775
|
-
writeFileSync(breadcrumb, note, "utf-8");
|
|
776
|
-
} catch {
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
function writeProjectClaudeMd(rootPath, config, knowledge) {
|
|
781
|
-
const claudeMdPath = join(rootPath, "CLAUDE.md");
|
|
782
|
-
const block = generateClaudeMd(config, knowledge);
|
|
783
|
-
if (!existsSync(claudeMdPath)) {
|
|
784
|
-
writeFileSync(claudeMdPath, block, "utf-8");
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const existing = readFileSync(claudeMdPath, "utf-8");
|
|
788
|
-
if (existing.includes(KNIT_MARKER_START)) {
|
|
789
|
-
const { content } = spliceKnitBlock(existing, block);
|
|
790
|
-
writeFileSync(claudeMdPath, content, "utf-8");
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const sidecarDir = join(rootPath, ".claude");
|
|
794
|
-
const sidecarPath = join(sidecarDir, "KNIT.md");
|
|
795
|
-
mkdirSync(sidecarDir, { recursive: true });
|
|
796
|
-
const sidecar = `<!-- This file is Knit's per-project workflow. -->
|
|
797
|
-
<!-- Your CLAUDE.md exists without Knit markers, so Knit wrote here instead of clobbering it. -->
|
|
798
|
-
<!-- To include this content in CLAUDE.md, add: @.claude/KNIT.md -->
|
|
799
|
-
|
|
800
|
-
${block}`;
|
|
801
|
-
writeFileSync(sidecarPath, sidecar, "utf-8");
|
|
802
|
-
}
|
|
803
|
-
function copyIfExists(src, dst) {
|
|
804
|
-
if (existsSync(src) && !existsSync(dst)) {
|
|
805
|
-
mkdirSync(dirname(dst), { recursive: true });
|
|
806
|
-
copyFileSync(src, dst);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
function writeKnitHooks(rootPath, config) {
|
|
810
|
-
const claudeDir = join(rootPath, ".claude");
|
|
811
|
-
const settingsPath = join(claudeDir, "settings.local.json");
|
|
812
|
-
const fresh = generateSettings(config, rootPath);
|
|
813
|
-
if (!existsSync(settingsPath)) {
|
|
814
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
815
|
-
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
let existing;
|
|
819
|
-
try {
|
|
820
|
-
const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
821
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
existing = parsed;
|
|
825
|
-
} catch {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if ("_knitHooks" in existing || "_engramHooks" in existing) {
|
|
829
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
830
|
-
writeFileSync(settingsPath, JSON.stringify(fresh, null, 2), "utf-8");
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
const userHooksRaw = existing.hooks;
|
|
834
|
-
let userHooks;
|
|
835
|
-
if (userHooksRaw === void 0) {
|
|
836
|
-
userHooks = {};
|
|
837
|
-
} else if (userHooksRaw && typeof userHooksRaw === "object" && !Array.isArray(userHooksRaw)) {
|
|
838
|
-
for (const v of Object.values(userHooksRaw)) {
|
|
839
|
-
if (!Array.isArray(v)) return;
|
|
840
|
-
}
|
|
841
|
-
userHooks = { ...userHooksRaw };
|
|
842
|
-
} else {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
for (const event of Object.keys(fresh.hooks)) {
|
|
846
|
-
const userEntries = Array.isArray(userHooks[event]) ? userHooks[event] : [];
|
|
847
|
-
const preserved = userEntries.filter((entry) => {
|
|
848
|
-
if (!entry || typeof entry !== "object") return true;
|
|
849
|
-
const e = entry;
|
|
850
|
-
return e._knitOwned !== true && e._engramOwned !== true;
|
|
851
|
-
});
|
|
852
|
-
userHooks[event] = [...preserved, ...fresh.hooks[event]];
|
|
853
|
-
}
|
|
854
|
-
const merged = {
|
|
855
|
-
...existing,
|
|
856
|
-
hooks: userHooks,
|
|
857
|
-
_knitHooks: { ...fresh._knitHooks, merged: true }
|
|
858
|
-
};
|
|
859
|
-
delete merged._engramHooks;
|
|
860
|
-
mkdirSync(claudeDir, { recursive: true });
|
|
861
|
-
writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
862
|
-
}
|
|
863
|
-
function detectProjectName(rootPath) {
|
|
864
|
-
let name = basename(rootPath);
|
|
865
|
-
try {
|
|
866
|
-
const pkg = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf-8"));
|
|
867
|
-
if (pkg.name) name = pkg.name;
|
|
868
|
-
} catch {
|
|
869
|
-
}
|
|
870
|
-
return name;
|
|
871
|
-
}
|
|
872
|
-
function refreshBrain(rootPath) {
|
|
873
|
-
cache = null;
|
|
874
|
-
return getBrain(rootPath);
|
|
875
|
-
}
|
|
876
|
-
function detectProjectRoot() {
|
|
877
|
-
try {
|
|
878
|
-
return execSync("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
879
|
-
} catch {
|
|
880
|
-
return process.cwd();
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
797
|
export {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
detectProjectRoot
|
|
798
|
+
HOOKS_VERSION,
|
|
799
|
+
generateSettings
|
|
888
800
|
};
|
|
@@ -16,6 +16,11 @@ When to reach for other Knit tools:
|
|
|
16
16
|
- knit_get_workflow({phase}) \u2014 fetch protocol depth for one phase on demand. Do not try to remember the workflow; ask for it.
|
|
17
17
|
- knit_list_features \u2014 if you want to do X but the tool isn't visible, this surfaces what's hidden and how to enable it.
|
|
18
18
|
- knit_save_handoff \u2014 call when context degrades or session ends so the next session resumes cleanly.
|
|
19
|
+
- knit_verify_claim \u2014 fact-check one claim against the graph BEFORE LEARN on standard/complex scope (Stop-hook gate enforces this).
|
|
20
|
+
- knit_index_requirements / knit_generate_test_cases \u2014 for long-form spec / RFC / Jira-export ingestion. Indexes once, retrieves only relevant chunks per feature query.
|
|
21
|
+
- knit_get_fingerprint \u2014 detected stack (lang/framework/test/lint/build/CI) for choosing the right tooling per project.
|
|
22
|
+
- knit_infer_domains + knit_compose_template \u2014 propose CLAUDE.md auto-config sections from git co-change + import-graph + colocation signals.
|
|
23
|
+
- knit_get_calibration / knit_record_false_positive (with a direction tag like #complex-was-trivial) \u2014 feed the self-healing classifier so it tunes per-project over time.
|
|
19
24
|
|
|
20
25
|
The protocol enforces a 4-tier classifier:
|
|
21
26
|
- Inquiry: read-only "what / where / audit / explain" \u2014 just answer.
|