knit-mcp 0.6.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.
@@ -0,0 +1,2041 @@
1
+ import {
2
+ appendSession,
3
+ getRecentSessions,
4
+ installAgentsForProject,
5
+ pruneSessionsByAge,
6
+ searchSessions,
7
+ sessionCount
8
+ } from "./chunk-TH5QPD5E.js";
9
+ import {
10
+ scanProject
11
+ } from "./chunk-LW6NOFHF.js";
12
+ import {
13
+ appendGlobalLearning,
14
+ buildGlobalLearning,
15
+ getRecentGlobalLearnings,
16
+ searchGlobalLearnings
17
+ } from "./chunk-FEOG4WTP.js";
18
+ import {
19
+ addEntry,
20
+ getFalsePositives,
21
+ getKBSummary,
22
+ queryByDomains,
23
+ recordCacheHit,
24
+ saveKnowledgeBase
25
+ } from "./chunk-BAUQEFYY.js";
26
+ import {
27
+ canonicalRepoRoot,
28
+ classificationMarkerPath,
29
+ knowledgebasePath,
30
+ learningsDir,
31
+ projectDataDir,
32
+ protocolConfigPath,
33
+ sessionsLogPath,
34
+ teamsPath,
35
+ worktreesRegistryPath
36
+ } from "./chunk-YI37OAJ7.js";
37
+
38
+ // src/mcp/handlers.ts
39
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, readdirSync, existsSync as existsSync4 } from "fs";
40
+ import { join as join2 } from "path";
41
+ import { statSync as statSync2 } from "fs";
42
+
43
+ // src/generators/workflow-protocol.ts
44
+ function getWorkflowSection(phase, ctx = {}) {
45
+ const normalized = phase.toLowerCase().trim();
46
+ const fn = SECTIONS[normalized];
47
+ return fn ? fn(ctx) : null;
48
+ }
49
+ function listWorkflowSections() {
50
+ return [
51
+ { name: "overview", description: "What the protocol is and how to navigate it." },
52
+ { name: "tier", description: "The four tiers (Inquiry / Trivial / Standard / Complex) as a decision aid." },
53
+ { name: "phases", description: "The 6-phase protocol overview + routing per tier." },
54
+ { name: "research", description: "Phase 1 \u2014 gather context, name affected files + domains." },
55
+ { name: "ideate", description: "Phase 2 (Complex only) \u2014 propose \u22652 approaches with trade-offs." },
56
+ { name: "plan", description: "Phase 3 (Complex only) \u2014 file-level plan + plan mode rules." },
57
+ { name: "execute", description: "Phase 4 \u2014 write the code. TDD for features, repro-first for bugs." },
58
+ { name: "optimize", description: "Phase 5 \u2014 parallel review agents with role briefings." },
59
+ { name: "review", description: "Phase 6 \u2014 gates pass before commit." },
60
+ { name: "tdd", description: "RED \u2192 GREEN \u2192 REFACTOR cycle in detail." },
61
+ { name: "learn", description: "Quality-gated LEARN \u2014 when to record, when to skip." },
62
+ { name: "handoff", description: "Session handoff when context degrades." },
63
+ { name: "ship", description: "Commit format + PR flow + production checklist." },
64
+ { name: "tools", description: "Knit MCP tools reference \u2014 what each is for." }
65
+ ];
66
+ }
67
+ var SECTIONS = {
68
+ overview,
69
+ tier,
70
+ phases,
71
+ research,
72
+ ideate,
73
+ plan,
74
+ execute,
75
+ optimize,
76
+ review,
77
+ tdd,
78
+ learn,
79
+ handoff,
80
+ ship,
81
+ tools
82
+ };
83
+ function overview(_) {
84
+ return `# Engram workflow \u2014 overview
85
+
86
+ This protocol is a decision aid. Read once, follow loosely, escalate when something doesn't fit. The principle: engram gives you data; you make the calls.
87
+
88
+ **Navigation:** call \`knit_get_workflow({phase})\` with any of:
89
+ - \`overview\` (this) \xB7 \`tier\` \xB7 \`phases\`
90
+ - \`research\` \xB7 \`ideate\` \xB7 \`plan\` \xB7 \`execute\` \xB7 \`optimize\` \xB7 \`review\`
91
+ - \`tdd\` \xB7 \`learn\` \xB7 \`handoff\` \xB7 \`ship\` \xB7 \`tools\`
92
+
93
+ Start of every session: call \`knit_load_session\` first. It returns prior sessions, learnings, false positives, project knowledge in one round trip.
94
+
95
+ When in doubt: under-classify. Easier to escalate mid-task than to downgrade.`;
96
+ }
97
+ function tier(_) {
98
+ return `# Tier classification \u2014 you decide, engram informs
99
+
100
+ Four tiers. You read the user's message and decide. No regex, no auto-rules.
101
+
102
+ | Tier | Smell | What you do |
103
+ |------|-------|-------------|
104
+ | **Inquiry** | Read-only. "What", "where", "audit", "explain", "status". | Just answer. No phases, no LEARN unless something durable surfaced. |
105
+ | **Trivial** | Single obvious change (typo, version bump, one-line fix). | EXECUTE \u2192 verify \u2192 done. |
106
+ | **Standard** | Bug fix, single-file feature, single domain. | RESEARCH \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW. |
107
+ | **Complex** | Cross-domain, touches types/auth/money, high-fanout file, new feature with unclear shape, **or a single user request that will span more than one commit / PR**. | Full 6 phases. Auto-enter plan mode on RESEARCH. |
108
+
109
+ Signals to consult before classifying (call any of these):
110
+ - \`knit_find_fanout\` \u2014 is the file central?
111
+ - \`knit_search_learnings\` \u2014 have we done this before?
112
+ - \`knit_query_dependents\` \u2014 what depends on what I'd touch?
113
+ - \`knit_search_sessions\` \u2014 did a past session tackle something similar?
114
+
115
+ **Multi-commit arcs.** If a single user request will span more than one commit or PR, the *arc* is Complex even if individual commits inside it are Standard or Trivial. Enter plan mode at the start of the arc.`;
116
+ }
117
+ function phases(_) {
118
+ return `# The 6-phase protocol
119
+
120
+ \`\`\`
121
+ RESEARCH \u2192 IDEATE \u2192 PLAN \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW
122
+ \u2191 |
123
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 LEARN \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
124
+ \`\`\`
125
+
126
+ Routing by tier:
127
+
128
+ | Tier | Phases |
129
+ |------|--------|
130
+ | Inquiry | (none \u2014 just answer) |
131
+ | Trivial | EXECUTE \u2192 verify |
132
+ | Standard | RESEARCH \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW |
133
+ | Complex | RESEARCH \u2192 IDEATE \u2192 PLAN \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW |
134
+
135
+ Each phase has one quality gate. Don't move on until it passes. Call \`knit_get_workflow({phase})\` for the depth on any phase.`;
136
+ }
137
+ function research(_) {
138
+ return `# RESEARCH (Standard + Complex)
139
+
140
+ Goal: understand what you're touching before you touch it.
141
+
142
+ **Standard tasks:** read the affected files directly. Check cross-domain ripple via \`knit_query_dependents\`. Pull relevant past sessions via \`knit_search_sessions\` if the area has history.
143
+
144
+ **Complex tasks:** spawn \`code-explorer\` agents per affected domain in parallel. Each scout reports complexity + risks. Use \`knit_find_fanout\` to spot universal-contract files.
145
+
146
+ **Gate:** can you name every file and domain that will be touched? If no, read more. If yes, proceed.
147
+
148
+ **Avoid:** speculative file reads. Avoid reading every file in the import graph "just in case." Read what the change demands, then stop.`;
149
+ }
150
+ function ideate(_) {
151
+ return `# IDEATE (Complex only)
152
+
153
+ Goal: surface at least 2 approaches with trade-offs before committing to one.
154
+
155
+ 1. Launch domain heads in parallel. Each head proposes an approach for its domain + risks.
156
+ 2. Synthesize into 2+ distinct options. Make them genuinely different \u2014 same approach with two coats of paint isn't IDEATE.
157
+ 3. Present options with trade-offs (complexity / risk / reversibility / scope).
158
+
159
+ **Gate:** user picks an approach.
160
+
161
+ For Standard, skip IDEATE \u2014 pick the obvious approach and state it out loud. User can override.`;
162
+ }
163
+ function plan(_) {
164
+ return `# PLAN (Complex only \u2014 auto plan mode)
165
+
166
+ **Plan mode auto-fires by phase, not by user keyword.** You enter plan mode when you decide to enter RESEARCH or PLAN. The user doesn't need to say "plan."
167
+
168
+ - **Inquiry / Trivial** \u2192 never plan mode.
169
+ - **Standard** \u2192 rarely; only if RESEARCH reveals unexpected complexity.
170
+ - **Complex** \u2192 plan mode opens at the start of RESEARCH. Stay in plan mode through IDEATE and PLAN. User's approval is required to **exit** plan mode (before EXECUTE).
171
+
172
+ What to produce in PLAN:
173
+ 1. File-level: exact files to create / modify / delete.
174
+ 2. Ordering: which domain goes first, what's sequential vs parallel.
175
+ 3. Test plan: how to verify the work end-to-end.
176
+ 4. Risk note: what could break + rollback path.
177
+
178
+ **Gate:** user says "go" / "approved" / "do it." After approval, exit plan mode and execute.`;
179
+ }
180
+ function execute(ctx) {
181
+ const buildHint = ctx.buildCommands?.typecheck ? `
182
+
183
+ Milestone check (every ~5 edits): run \`${ctx.buildCommands.typecheck}\` \u2014 still green?` : "";
184
+ return `# EXECUTE
185
+
186
+ Follow the approved plan strictly. No scope creep \u2014 surprises go into LEARN, not into the diff.
187
+
188
+ **New features:** TDD when applicable (see \`knit_get_workflow({phase: "tdd"})\`). Failing test \u2192 minimum impl \u2192 refactor.
189
+
190
+ **Bug fixes:** reproduce first. Write the failing test that captures the bug. Fix. Run full suite. No regressions.
191
+
192
+ **Cross-domain changes:** follow the cross-domain ripple rules from RESEARCH. If a type contract changes, all downstream domains get notified.${buildHint}
193
+
194
+ **Avoid:**
195
+ - Adding error handling for cases that can't happen.
196
+ - Adding comments that explain WHAT (well-named code already does that).
197
+ - Backwards-compat shims when the code can just change.
198
+ - Half-finished implementations. Either it works end-to-end or it doesn't ship.`;
199
+ }
200
+ function optimize(_) {
201
+ return `# OPTIMIZE (Standard + Complex)
202
+
203
+ Spawn review agents in parallel. Standard: 1\u20132 agents. Complex: as many as affected domains.
204
+
205
+ **Role briefing replaces frozen Domain Context Objects.** Each spawned agent gets a short briefing, not a 3K-token DCO. The agent has MCP access and pulls depth on demand.
206
+
207
+ Briefing template:
208
+ > You are a {role} for files X, Y, Z. Task: {one sentence}.
209
+ > Project brain available: \`knit_query_dependents\`, \`knit_get_false_positives\`, \`knit_search_learnings\`, \`knit_search_sessions\`. Call them if you need depth.
210
+ > Report findings as: severity / file / line / issue / suggestion.
211
+
212
+ Before passing false positives \u2014 filter to those whose file paths overlap the agent's task. Don't dump all entries; pass the 3 that apply.
213
+
214
+ Findings sorted CRITICAL / HIGH / MEDIUM / LOW.
215
+
216
+ **Gate:** zero CRITICAL findings. All HIGH addressed (fixed or explicitly deferred with reason).`;
217
+ }
218
+ function review(ctx) {
219
+ const gates = [];
220
+ if (ctx.buildCommands?.typecheck) gates.push(`- \`${ctx.buildCommands.typecheck}\``);
221
+ if (ctx.buildCommands?.lint) gates.push(`- \`${ctx.buildCommands.lint}\``);
222
+ if (ctx.buildCommands?.test) gates.push(`- \`${ctx.buildCommands.test}\``);
223
+ if (ctx.buildCommands?.build) gates.push(`- \`${ctx.buildCommands.build}\``);
224
+ const gatesBlock = gates.length > 0 ? `
225
+
226
+ Gates for this project:
227
+ ${gates.join("\n")}` : "";
228
+ return `# REVIEW (Standard + Complex)
229
+
230
+ **Layer 1 \u2014 automated gates.** typecheck + lint + test + build all green. If anything fails: fix, re-run.${gatesBlock}
231
+
232
+ **Layer 2 \u2014 functional verification.** If browser QA tools are available and the task touches UI or API, navigate the change end-to-end. Take screenshots as proof. If unavailable, note "browser verification skipped" and continue.
233
+
234
+ **Layer 3 \u2014 ship readiness (Complex + PR only):**
235
+ - Plan-vs-done diff: anything missed?
236
+ - Regression check: did existing functionality break?
237
+ - Confidence signal: "ready to ship" only when ALL layers pass.`;
238
+ }
239
+ function tdd(_) {
240
+ return `# TDD workflow
241
+
242
+ **New feature:**
243
+ 1. RED \u2014 write a test that describes the expected behavior. Run tests. New test MUST fail. If it passes, it isn't testing anything new.
244
+ 2. GREEN \u2014 write the simplest code that makes the test pass. No extras, no optimization, no cleanup.
245
+ 3. REFACTOR \u2014 clean up. Extract helpers, improve naming, kill duplication. Run tests after EVERY change.
246
+ 4. VERIFY COVERAGE \u2014 does the new code have tests? Are edge cases covered?
247
+
248
+ **Bug fix:**
249
+ 1. REPRODUCE \u2014 find the exact input that triggers the bug.
250
+ 2. WRITE FAILING TEST \u2014 captures the bug.
251
+ 3. FIX \u2014 minimum change to make the test pass.
252
+ 4. VERIFY \u2014 full test suite, no regressions.
253
+
254
+ **When NOT to TDD:**
255
+ - Trivial-tier work (typo, config, version bump).
256
+ - Doc updates.
257
+ - Behavior-preserving refactors (run existing tests).`;
258
+ }
259
+ function learn(_) {
260
+ return `# LEARN \u2014 opportunistic, not mandatory
261
+
262
+ **Default: don't record a learning.** Most tasks produce no durable insight.
263
+
264
+ Record one only when **all** are true:
265
+ - A non-obvious fact emerged (the kind future-you would forget by next month).
266
+ - The lesson generalizes to a hypothetical future task.
267
+ - You can name a specific tag where someone would search for it.
268
+
269
+ Quality check before \`knit_record_learning\`:
270
+ > If session N+1 searched for this tag, would this entry save them time?
271
+
272
+ If no \u2014 don't write. Manufactured learnings hurt the hit rate by burying real ones.
273
+
274
+ If a task finishes with no learning recorded \u2014 that's correct, not a failure.
275
+
276
+ **Sessions vs learnings.** Two different tools:
277
+ - \`knit_record_learning\` \u2014 durable, reusable insight (Stripe webhook signature rules).
278
+ - \`knit_save_session_summary\` \u2014 what this session accomplished (refactored auth middleware). Opt-in, narrative.
279
+
280
+ Use both. They serve different searches.`;
281
+ }
282
+ function handoff(_) {
283
+ return `# Session handoff
284
+
285
+ When context degrades \u2014 circular debugging, repeated failures, post-compaction confusion \u2014 call \`knit_save_handoff\` with:
286
+
287
+ - **goal** \u2014 what we're trying to accomplish
288
+ - **current_state** \u2014 where we are right now
289
+ - **files_in_flight** \u2014 what's been modified
290
+ - **what_changed** \u2014 commits and edits since session start
291
+ - **failed_attempts** *(mandatory)* \u2014 what was tried and why it failed. This is the load-bearing field that prevents the next session from repeating mistakes.
292
+ - **decisions_made** \u2014 choices and their reasoning
293
+ - **next_step** \u2014 the ONE most important thing to do next
294
+
295
+ Then tell the user to start a fresh session. The next session's \`knit_load_session\` will surface the handoff first.
296
+
297
+ **Never auto-handoff.** Try compacting context first. Handoff is for genuinely stuck.`;
298
+ }
299
+ function ship(ctx) {
300
+ const gates = [];
301
+ if (ctx.buildCommands?.typecheck) gates.push(ctx.buildCommands.typecheck);
302
+ if (ctx.buildCommands?.lint) gates.push(ctx.buildCommands.lint);
303
+ if (ctx.buildCommands?.test) gates.push(ctx.buildCommands.test);
304
+ if (ctx.buildCommands?.build) gates.push(ctx.buildCommands.build);
305
+ const gatesLine = gates.length > 0 ? `
306
+
307
+ Pre-commit (all must pass): \`${gates.join(" && ")}\`` : "";
308
+ return `# Commit & ship${gatesLine}
309
+
310
+ **Commit format:**
311
+ \`\`\`
312
+ <type>: <description>
313
+
314
+ <optional body \u2014 what and why, not how>
315
+ \`\`\`
316
+ Types: \`feat\`, \`fix\`, \`refactor\`, \`docs\`, \`test\`, \`chore\`, \`perf\`.
317
+
318
+ **PR flow:**
319
+ 1. Atomic commits \u2014 one concern per commit.
320
+ 2. Push to feature branch.
321
+ 3. PR with 1\u20133 bullet summary + test plan.
322
+ 4. Squash merge to main, delete branch.
323
+
324
+ **Production checklist** (only when shipping to prod, not every PR):
325
+ - No secrets in code or git history (\`git log -p --all -S "sk-"\`).
326
+ - \`.env\` in \`.gitignore\`; \`.env.example\` exists with placeholders.
327
+ - All user input validated at system boundaries.
328
+ - Build succeeds clean: \`rm -rf node_modules && npm ci && npm run build\`.
329
+ - Auth on protected routes; rate limiting on auth/submission endpoints.
330
+ - Verify production loads post-deploy. Check error monitoring.`;
331
+ }
332
+ function tools(_) {
333
+ return `# Knit MCP tools \u2014 reference
334
+
335
+ **Read the brain** (cheap, instant, no side effects):
336
+ | Tool | Use when |
337
+ |------|----------|
338
+ | \`knit_load_session\` | First call every session. Returns last sessions, handoff, learnings, false positives. |
339
+ | \`knit_query_imports\` | Before editing \u2014 what depends on this file? |
340
+ | \`knit_query_dependents\` | What does this file need to work? |
341
+ | \`knit_query_exports\` | Find a function without opening the file. |
342
+ | \`knit_query_tests\` | Is this tested? Or: list all untested files. |
343
+ | \`knit_find_fanout\` | High-risk files \u2014 change carefully. |
344
+ | \`knit_search_learnings\` | Did we solve this before? |
345
+ | \`knit_search_sessions\` | Did a past session work in this area? |
346
+ | \`knit_get_false_positives\` | Filter known non-issues out of review prompts. |
347
+ | \`knit_brain_status\` | Brain health + hit-rate + token-accounting. |
348
+ | \`knit_get_workflow\` | Fetch protocol depth for a specific phase. |
349
+
350
+ **Update the brain** (only when there's real signal):
351
+ | Tool | Use when |
352
+ |------|----------|
353
+ | \`knit_classify_task\` | At task start. Returns tier + phases. |
354
+ | \`knit_build_context\` | Get cross-domain ripple + pitfalls for the current task. |
355
+ | \`knit_record_learning\` | A non-obvious, reusable insight surfaced (pass the quality check first). |
356
+ | \`knit_record_false_positive\` | Review agent flagged a non-issue. |
357
+ | \`knit_save_session_summary\` | Session accomplished something a future session would search for. |
358
+ | \`knit_save_handoff\` | Context degrading \u2014 save state for next session. |
359
+ | \`knit_setup_project\` | First-time project setup for non-code domains (legal, research, marketing). |
360
+
361
+ **Team orchestration** (for parallel review boards):
362
+ | Tool | Use when |
363
+ |------|----------|
364
+ | \`knit_get_teams\` | Get auto-detected or custom teams. |
365
+ | \`knit_define_team\` | Create a custom team. |
366
+ | \`knit_start_team_review\` | Start a parallel review board. |
367
+ | \`knit_post_team_findings\` | Each team posts to the shared board. |
368
+ | \`knit_get_board_summary\` | Cross-team findings, severity-gated. |`;
369
+ }
370
+
371
+ // src/engine/worktrees.ts
372
+ import { execSync } from "child_process";
373
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "fs";
374
+ import { dirname, basename, resolve, join } from "path";
375
+ var EMPTY_REGISTRY = { version: 1, worktrees: [] };
376
+ function spawnWorktree(rootPath, teamName, taskDescription) {
377
+ const repoRoot = canonicalRepoRoot(rootPath);
378
+ const slug = slugify(teamName);
379
+ const ts = Date.now();
380
+ const branch = `engram/team-${slug}-${ts}`;
381
+ const worktreePath = resolve(dirname(repoRoot), `${basename(repoRoot)}-knit-${slug}-${ts}`);
382
+ if (existsSync(worktreePath)) {
383
+ throw new Error(`Worktree path already exists: ${worktreePath}`);
384
+ }
385
+ const registry = loadRegistry(rootPath);
386
+ const existing = registry.worktrees.find(
387
+ (w) => w.teamSlug === slug && w.status === "active"
388
+ );
389
+ if (existing) {
390
+ throw new Error(
391
+ `Team "${teamName}" already has an active worktree at ${existing.path}. Finalize it before spawning a new one.`
392
+ );
393
+ }
394
+ try {
395
+ execSync(
396
+ `git worktree add -b ${shellQuote(branch)} ${shellQuote(worktreePath)}`,
397
+ { cwd: repoRoot, stdio: "pipe" }
398
+ );
399
+ } catch (err) {
400
+ const msg = err instanceof Error ? err.message : String(err);
401
+ throw new Error(`git worktree add failed: ${msg}`);
402
+ }
403
+ const record = {
404
+ teamName,
405
+ teamSlug: slug,
406
+ path: worktreePath,
407
+ branch,
408
+ taskDescription,
409
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
410
+ status: "active"
411
+ };
412
+ registry.worktrees.push(record);
413
+ saveRegistry(rootPath, registry);
414
+ return record;
415
+ }
416
+ function listWorktrees(rootPath, includeFinalized = false) {
417
+ const registry = loadRegistry(rootPath);
418
+ for (const wt of registry.worktrees) {
419
+ if (wt.status === "active" && !existsSync(wt.path)) {
420
+ wt.status = "discarded";
421
+ }
422
+ }
423
+ saveRegistry(rootPath, registry);
424
+ return includeFinalized ? registry.worktrees : registry.worktrees.filter((w) => w.status === "active");
425
+ }
426
+ function finalizeWorktree(rootPath, teamSlugOrName, action) {
427
+ const registry = loadRegistry(rootPath);
428
+ const slug = slugify(teamSlugOrName);
429
+ const record = registry.worktrees.find(
430
+ (w) => w.status === "active" && (w.teamSlug === slug || w.teamName === teamSlugOrName)
431
+ );
432
+ if (!record) {
433
+ throw new Error(`No active worktree found for team "${teamSlugOrName}".`);
434
+ }
435
+ const repoRoot = canonicalRepoRoot(rootPath);
436
+ if (action === "discard") {
437
+ try {
438
+ execSync(`git worktree remove --force ${shellQuote(record.path)}`, { cwd: repoRoot, stdio: "pipe" });
439
+ } catch {
440
+ }
441
+ try {
442
+ execSync(`git branch -D ${shellQuote(record.branch)}`, { cwd: repoRoot, stdio: "pipe" });
443
+ } catch {
444
+ }
445
+ record.status = "discarded";
446
+ saveRegistry(rootPath, registry);
447
+ return { status: "discarded", worktree: record };
448
+ }
449
+ try {
450
+ execSync(`git merge --no-ff ${shellQuote(record.branch)}`, { cwd: repoRoot, stdio: "pipe" });
451
+ } catch (err) {
452
+ let conflictFiles = [];
453
+ try {
454
+ const out = execSync("git diff --name-only --diff-filter=U", { cwd: repoRoot, encoding: "utf-8" });
455
+ conflictFiles = out.split("\n").map((s) => s.trim()).filter(Boolean);
456
+ } catch {
457
+ }
458
+ const msg = err instanceof Error ? err.message : String(err);
459
+ return {
460
+ status: "conflict",
461
+ worktree: record,
462
+ conflictFiles,
463
+ message: `Merge conflict. Resolve in ${repoRoot}, then call knit_finalize_worktree again with action='merge' to retry, or 'discard' to throw away. ${msg}`
464
+ };
465
+ }
466
+ try {
467
+ execSync(`git worktree remove ${shellQuote(record.path)}`, { cwd: repoRoot, stdio: "pipe" });
468
+ } catch {
469
+ }
470
+ try {
471
+ execSync(`git branch -d ${shellQuote(record.branch)}`, { cwd: repoRoot, stdio: "pipe" });
472
+ } catch {
473
+ }
474
+ record.status = "merged";
475
+ saveRegistry(rootPath, registry);
476
+ return { status: "merged", worktree: record };
477
+ }
478
+ function loadRegistry(rootPath) {
479
+ const path = worktreesRegistryPath(rootPath);
480
+ if (!existsSync(path)) return { ...EMPTY_REGISTRY, worktrees: [] };
481
+ try {
482
+ const data = JSON.parse(readFileSync(path, "utf-8"));
483
+ if (data && data.version === 1 && Array.isArray(data.worktrees)) {
484
+ return data;
485
+ }
486
+ } catch {
487
+ }
488
+ return { ...EMPTY_REGISTRY, worktrees: [] };
489
+ }
490
+ function saveRegistry(rootPath, registry) {
491
+ const path = worktreesRegistryPath(rootPath);
492
+ mkdirSync(projectDataDir(rootPath), { recursive: true });
493
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
494
+ writeFileSync(tmp, JSON.stringify(registry, null, 2), "utf-8");
495
+ renameSync(tmp, path);
496
+ }
497
+ function slugify(s) {
498
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
499
+ }
500
+ function shellQuote(s) {
501
+ return `'${s.replace(/'/g, `'\\''`)}'`;
502
+ }
503
+
504
+ // src/engine/reflect.ts
505
+ function reflect(kb) {
506
+ const patterns = [];
507
+ const localEntries = kb.entries.map((e) => ({ ...e, _origin: "local" }));
508
+ let entries = localEntries;
509
+ if (kb.entries.length < 3) {
510
+ try {
511
+ const globals = getRecentGlobalLearnings(200);
512
+ const globalEntries = globals.map((g) => globalToKBEntry(g));
513
+ entries = [...localEntries, ...globalEntries];
514
+ } catch {
515
+ entries = localEntries;
516
+ }
517
+ }
518
+ if (entries.length < 3) return patterns;
519
+ const successes = entries.filter((e) => e.outcome === "success");
520
+ const successTagCounts = countTags(successes);
521
+ for (const [tag, count] of Object.entries(successTagCounts)) {
522
+ if (count >= 3) {
523
+ const relevant = successes.filter((e) => e.tags.includes(tag));
524
+ patterns.push({
525
+ id: `success-${tag}`,
526
+ type: "success-pattern",
527
+ description: `Consistent success in ${tag} domain (${count} successes)`,
528
+ confidence: Math.min(count * 2, 10),
529
+ evidence: relevant.slice(-3).map((e) => e.summary),
530
+ domains: [tag],
531
+ lastSeen: relevant[relevant.length - 1].date,
532
+ occurrences: count,
533
+ source: classifyOrigin(relevant)
534
+ });
535
+ }
536
+ }
537
+ const failures = entries.filter((e) => e.outcome === "failure");
538
+ const failureTagCounts = countTags(failures);
539
+ for (const [tag, count] of Object.entries(failureTagCounts)) {
540
+ if (count >= 2) {
541
+ const relevant = failures.filter((e) => e.tags.includes(tag));
542
+ patterns.push({
543
+ id: `failure-${tag}`,
544
+ type: "failure-pattern",
545
+ description: `Repeated failures in ${tag} (${count} times). Common lesson: ${relevant[relevant.length - 1].lesson}`,
546
+ confidence: Math.min(count * 3, 10),
547
+ evidence: relevant.map((e) => e.summary),
548
+ domains: [tag],
549
+ lastSeen: relevant[relevant.length - 1].date,
550
+ occurrences: count,
551
+ source: classifyOrigin(relevant)
552
+ });
553
+ }
554
+ }
555
+ const tagPairs = findTagPairs(entries);
556
+ for (const [pair, count] of tagPairs) {
557
+ if (count >= 3) {
558
+ const [tag1, tag2] = pair.split("+");
559
+ const supporting = entries.filter((e) => e.tags.includes(tag1) && e.tags.includes(tag2));
560
+ patterns.push({
561
+ id: `cooccur-${pair}`,
562
+ type: "co-occurrence",
563
+ description: `${tag1} and ${tag2} frequently appear together (${count} times) \u2014 changes in one likely affect the other`,
564
+ confidence: Math.min(count * 2, 10),
565
+ evidence: [],
566
+ domains: [tag1, tag2],
567
+ lastSeen: entries[entries.length - 1].date,
568
+ occurrences: count,
569
+ source: classifyOrigin(supporting)
570
+ });
571
+ }
572
+ }
573
+ const accessed = entries.filter((e) => e.accessCount >= 3);
574
+ for (const entry of accessed) {
575
+ patterns.push({
576
+ id: `insight-${entry.id}`,
577
+ type: "domain-insight",
578
+ description: `High-value insight (accessed ${entry.accessCount}x): ${entry.lesson}`,
579
+ confidence: Math.min(entry.accessCount, 10),
580
+ evidence: [entry.summary],
581
+ domains: entry.tags,
582
+ lastSeen: entry.lastAccessed || entry.date,
583
+ occurrences: entry.accessCount,
584
+ source: entry._origin
585
+ });
586
+ }
587
+ return patterns.sort((a, b) => b.confidence - a.confidence);
588
+ }
589
+ function globalToKBEntry(g) {
590
+ return {
591
+ id: `global:${g.id}`,
592
+ date: g.date,
593
+ summary: g.summary,
594
+ domains: g.tags,
595
+ approach: "",
596
+ outcome: g.outcome ?? "success",
597
+ lesson: g.lesson,
598
+ tags: g.tags,
599
+ accessCount: 0,
600
+ lastAccessed: null,
601
+ _origin: "global"
602
+ };
603
+ }
604
+ function classifyOrigin(supporting) {
605
+ let local = false;
606
+ let global = false;
607
+ for (const e of supporting) {
608
+ if (e._origin === "local") local = true;
609
+ else global = true;
610
+ if (local && global) return "mixed";
611
+ }
612
+ if (local && global) return "mixed";
613
+ return local ? "local" : "global";
614
+ }
615
+ function getAdaptiveSuggestions(kb, taskDomains) {
616
+ const suggestions = [];
617
+ const patterns = reflect(kb);
618
+ for (const pattern of patterns) {
619
+ const overlap = pattern.domains.some(
620
+ (d) => taskDomains.some((td) => td.toLowerCase().includes(d.replace("#", "").toLowerCase()) || d.replace("#", "").toLowerCase().includes(td.toLowerCase()))
621
+ );
622
+ if (!overlap) continue;
623
+ if (pattern.type === "failure-pattern") {
624
+ suggestions.push({
625
+ trigger: `Working in ${pattern.domains.join(", ")} domain`,
626
+ action: `Be careful \u2014 this area has failed ${pattern.occurrences} times before`,
627
+ reason: pattern.description,
628
+ confidence: pattern.confidence
629
+ });
630
+ }
631
+ if (pattern.type === "co-occurrence") {
632
+ suggestions.push({
633
+ trigger: `Touching ${pattern.domains[0]}`,
634
+ action: `Also check ${pattern.domains[1]} \u2014 they always change together`,
635
+ reason: `${pattern.occurrences} past changes affected both`,
636
+ confidence: pattern.confidence
637
+ });
638
+ }
639
+ if (pattern.type === "domain-insight" && pattern.confidence >= 5) {
640
+ suggestions.push({
641
+ trigger: `Working in ${pattern.domains.join(", ")}`,
642
+ action: pattern.description.replace(/^High-value insight.*: /, ""),
643
+ reason: `Validated ${pattern.occurrences} times across sessions`,
644
+ confidence: pattern.confidence
645
+ });
646
+ }
647
+ }
648
+ return suggestions.slice(0, 5);
649
+ }
650
+ function countTags(entries) {
651
+ const counts = {};
652
+ for (const entry of entries) {
653
+ for (const tag of entry.tags) {
654
+ counts[tag] = (counts[tag] || 0) + 1;
655
+ }
656
+ }
657
+ return counts;
658
+ }
659
+ function findTagPairs(entries) {
660
+ const pairs = {};
661
+ for (const entry of entries) {
662
+ const tags = entry.tags.filter((t) => t.startsWith("#"));
663
+ for (let i = 0; i < tags.length; i++) {
664
+ for (let j = i + 1; j < tags.length; j++) {
665
+ const pair = [tags[i], tags[j]].sort().join("+");
666
+ pairs[pair] = (pairs[pair] || 0) + 1;
667
+ }
668
+ }
669
+ }
670
+ return Object.entries(pairs).sort((a, b) => b[1] - a[1]);
671
+ }
672
+
673
+ // src/engine/teams.ts
674
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
675
+ import { dirname as dirname2 } from "path";
676
+ function buildDefaultTeams(domains) {
677
+ return domains.map((domain) => domainToTeam(domain));
678
+ }
679
+ function domainToTeam(domain) {
680
+ const teamMap = {
681
+ "UI": {
682
+ role: "Frontend Engineering",
683
+ focus: "User interface, components, accessibility, responsive design, state management",
684
+ reviewChecklist: [
685
+ "Components render correctly with all props",
686
+ "Accessibility: keyboard navigation, screen readers, ARIA labels",
687
+ "Responsive: works at 320px, 768px, 1024px, 1440px",
688
+ "Loading and error states handled",
689
+ "No layout shifts from dynamic content",
690
+ "Design system compliance"
691
+ ]
692
+ },
693
+ "API & Security": {
694
+ role: "Backend & Security Engineering",
695
+ focus: "API endpoints, authentication, authorization, input validation, rate limiting",
696
+ reviewChecklist: [
697
+ "All endpoints validate input (Zod/Joi/etc)",
698
+ "Authentication required on protected routes",
699
+ "Authorization checks (user can only access own resources)",
700
+ "Rate limiting on auth and submission endpoints",
701
+ "No SQL injection (parameterized queries)",
702
+ "No XSS (sanitized output)",
703
+ "Error responses don't leak internal details",
704
+ "CORS configured correctly"
705
+ ]
706
+ },
707
+ "Business Logic": {
708
+ role: "Core Logic Engineering",
709
+ focus: "Types, validations, calculations, data transformations, business rules",
710
+ reviewChecklist: [
711
+ "Type contracts are correct and complete",
712
+ "Calculations produce correct results for edge cases",
713
+ "Validation schemas match API expectations",
714
+ "No silent failures in data transformations",
715
+ "Immutable data patterns where applicable"
716
+ ]
717
+ },
718
+ "Infrastructure": {
719
+ role: "Infrastructure & Platform Engineering",
720
+ focus: "Database, email, webhooks, middleware, external integrations, deployment",
721
+ reviewChecklist: [
722
+ "Database migrations are safe and reversible",
723
+ "Queries are efficient (no N+1, proper indexes)",
724
+ "External API calls have timeouts and retries",
725
+ "Email/webhook payloads are correct",
726
+ "Environment variables documented",
727
+ "Docker/deployment configs are secure"
728
+ ]
729
+ },
730
+ "Quality Assurance": {
731
+ role: "QA & Testing",
732
+ focus: "Test coverage, test quality, build stability, regression detection",
733
+ reviewChecklist: [
734
+ "New code has tests (80%+ coverage target)",
735
+ "Tests are deterministic (no flaky tests)",
736
+ "Edge cases covered (empty, null, large inputs)",
737
+ "Integration tests for critical paths",
738
+ "Build passes in clean environment"
739
+ ]
740
+ }
741
+ };
742
+ const defaults = teamMap[domain.name] || {
743
+ role: `${domain.name} Engineering`,
744
+ focus: domain.description,
745
+ reviewChecklist: ["Code quality review", "Test coverage", "Error handling"]
746
+ };
747
+ return {
748
+ name: domain.name,
749
+ role: defaults.role || domain.name,
750
+ focus: defaults.focus || domain.description,
751
+ agents: domain.agents,
752
+ filePatterns: domain.filePatterns,
753
+ reviewChecklist: defaults.reviewChecklist || []
754
+ };
755
+ }
756
+ function generateTeamPrompt(team, taskDescription, domainContext, otherTeamFindings) {
757
+ let prompt = `You are the **${team.name} Team** (${team.role}).
758
+
759
+ **Your focus:** ${team.focus}
760
+
761
+ **Task:** ${taskDescription}
762
+
763
+ **Files in your domain:** ${team.filePatterns.join(", ")}
764
+
765
+ **Your review checklist:**
766
+ ${team.reviewChecklist.map((item) => `- [ ] ${item}`).join("\n")}
767
+
768
+ **Domain Context:**
769
+ ${JSON.stringify(domainContext, null, 2)}
770
+ `;
771
+ if (otherTeamFindings.length > 0) {
772
+ prompt += `
773
+ **Findings from other teams (react to these if they affect your domain):**
774
+ `;
775
+ for (const finding of otherTeamFindings) {
776
+ prompt += `- [${finding.severity}] ${finding.team}: ${finding.description} (${finding.file})
777
+ `;
778
+ }
779
+ }
780
+ prompt += `
781
+ **Report format:** For each finding, provide:
782
+ - Severity: CRITICAL / HIGH / MEDIUM / LOW
783
+ - File: which file
784
+ - Description: what's wrong
785
+ - Recommendation: specific fix
786
+
787
+ Only report findings you've VERIFIED against the actual code. Do not hallucinate issues.`;
788
+ return prompt;
789
+ }
790
+ var boards = /* @__PURE__ */ new Map();
791
+ var latestBoardId = null;
792
+ function startTeamBoard(taskId, taskDescription, teams) {
793
+ const board = {
794
+ taskId,
795
+ taskDescription,
796
+ teams,
797
+ findings: [],
798
+ status: Object.fromEntries(teams.map((t) => [t, "pending"])),
799
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
800
+ };
801
+ boards.set(taskId, board);
802
+ latestBoardId = taskId;
803
+ if (boards.size > 10) {
804
+ const oldest = boards.keys().next().value;
805
+ if (oldest) boards.delete(oldest);
806
+ }
807
+ return board;
808
+ }
809
+ function getTeamBoard(taskId) {
810
+ if (taskId) return boards.get(taskId) || null;
811
+ if (latestBoardId) return boards.get(latestBoardId) || null;
812
+ return null;
813
+ }
814
+ function markTeamWorking(teamName, taskId) {
815
+ const board = getTeamBoard(taskId);
816
+ if (board) board.status[teamName] = "working";
817
+ }
818
+ function postTeamFindings(teamName, findings, taskId) {
819
+ const board = getTeamBoard(taskId);
820
+ if (!board) return;
821
+ board.findings.push(...findings);
822
+ board.status[teamName] = "done";
823
+ }
824
+ function getOtherTeamFindings(excludeTeam, taskId) {
825
+ const board = getTeamBoard(taskId);
826
+ if (!board) return [];
827
+ return board.findings.filter((f) => f.team !== excludeTeam);
828
+ }
829
+ function allTeamsDone(taskId) {
830
+ const board = getTeamBoard(taskId);
831
+ if (!board) return false;
832
+ return Object.values(board.status).every((s) => s === "done");
833
+ }
834
+ function getBoardSummary(taskId) {
835
+ const board = getTeamBoard(taskId);
836
+ if (!board) {
837
+ return { total: 0, critical: 0, high: 0, medium: 0, low: 0, byTeam: {}, allDone: false };
838
+ }
839
+ const findings = board.findings;
840
+ const byTeam = {};
841
+ for (const f of findings) {
842
+ byTeam[f.team] = (byTeam[f.team] || 0) + 1;
843
+ }
844
+ return {
845
+ total: findings.length,
846
+ critical: findings.filter((f) => f.severity === "CRITICAL").length,
847
+ high: findings.filter((f) => f.severity === "HIGH").length,
848
+ medium: findings.filter((f) => f.severity === "MEDIUM").length,
849
+ low: findings.filter((f) => f.severity === "LOW").length,
850
+ byTeam,
851
+ allDone: allTeamsDone(taskId)
852
+ };
853
+ }
854
+ function loadCustomTeams(rootPath) {
855
+ const teamsFile = teamsPath(rootPath);
856
+ if (!existsSync2(teamsFile)) return null;
857
+ try {
858
+ const stat = statSync(teamsFile);
859
+ if (stat.size > 10 * 1024 * 1024) return null;
860
+ const teams = JSON.parse(readFileSync2(teamsFile, "utf-8"));
861
+ if (!Array.isArray(teams)) return null;
862
+ return teams;
863
+ } catch {
864
+ return null;
865
+ }
866
+ }
867
+ function saveCustomTeams(rootPath, teams) {
868
+ const teamsFile = teamsPath(rootPath);
869
+ const dir = dirname2(teamsFile);
870
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
871
+ writeFileSync2(teamsFile, JSON.stringify(teams, null, 2), "utf-8");
872
+ }
873
+
874
+ // src/engine/protocol-guard.ts
875
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
876
+ import { dirname as dirname3 } from "path";
877
+ var VALID_LEVELS = ["off", "warn", "block"];
878
+ function isValidStrictness(level) {
879
+ return VALID_LEVELS.includes(level);
880
+ }
881
+ function readProtocolConfig(rootPath) {
882
+ const path = protocolConfigPath(rootPath);
883
+ if (!existsSync3(path)) {
884
+ return { level: "warn", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
885
+ }
886
+ try {
887
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
888
+ const level = parsed.level && isValidStrictness(parsed.level) ? parsed.level : "warn";
889
+ const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString();
890
+ return { level, updatedAt };
891
+ } catch {
892
+ return { level: "warn", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
893
+ }
894
+ }
895
+ function writeProtocolConfig(rootPath, level) {
896
+ const path = protocolConfigPath(rootPath);
897
+ mkdirSync3(dirname3(path), { recursive: true });
898
+ const config = { level, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
899
+ writeFileSync3(path, JSON.stringify(config, null, 2), "utf-8");
900
+ return config;
901
+ }
902
+ function writeClassificationMarker(rootPath, marker) {
903
+ const path = classificationMarkerPath(rootPath);
904
+ mkdirSync3(dirname3(path), { recursive: true });
905
+ writeFileSync3(path, JSON.stringify(marker, null, 2), "utf-8");
906
+ }
907
+
908
+ // src/mcp/handlers.ts
909
+ function detectDomainsFromFiles(files) {
910
+ const domains = /* @__PURE__ */ new Set();
911
+ for (const file of files) {
912
+ if (file.includes("api/") || file.includes("auth")) domains.add("API & Security");
913
+ if (file.includes("components/") || file.includes(".tsx")) domains.add("UI");
914
+ if (file.includes("lib/") || file.includes("utils") || file.includes("types")) domains.add("Business Logic");
915
+ if (file.includes("db") || file.includes("email") || file.includes("middleware")) domains.add("Infrastructure");
916
+ if (file.includes("test")) domains.add("QA");
917
+ }
918
+ return domains;
919
+ }
920
+ var VALID_SEVERITIES = /* @__PURE__ */ new Set(["CRITICAL", "HIGH", "MEDIUM", "LOW"]);
921
+ function handleQueryImports(params, brain) {
922
+ const filePath = params.file_path;
923
+ const importers = brain.reverseDeps[filePath] || [];
924
+ const risk = importers.length >= 5 ? "HIGH" : importers.length >= 3 ? "MEDIUM" : "LOW";
925
+ return JSON.stringify({
926
+ file: filePath,
927
+ imported_by: importers,
928
+ count: importers.length,
929
+ risk,
930
+ instruction: importers.length >= 3 ? `This file has ${importers.length} dependents. Changes here will ripple. Update/test these files after editing: ${importers.slice(0, 5).join(", ")}` : "Low risk \u2014 few dependents."
931
+ });
932
+ }
933
+ function handleQueryDependents(params, brain) {
934
+ const filePath = params.file_path;
935
+ const deps = brain.knowledge.importGraph[filePath] || [];
936
+ return JSON.stringify({ file: filePath, depends_on: deps, count: deps.length });
937
+ }
938
+ function handleQueryExports(params, brain) {
939
+ const filePath = params.file_path;
940
+ const exports = brain.knowledge.exports[filePath] || [];
941
+ return JSON.stringify({
942
+ file: filePath,
943
+ exports: exports.map((e) => ({ name: e.name, kind: e.kind, line: e.line })),
944
+ count: exports.length
945
+ });
946
+ }
947
+ function handleQueryTests(params, brain) {
948
+ if (params.filter === "untested") {
949
+ const untested = brain.knowledge.testMap.untested;
950
+ return JSON.stringify({
951
+ untested_files: untested,
952
+ count: untested.length,
953
+ instruction: untested.length > 0 ? `${untested.length} files have no tests. Write tests for these before shipping.` : "All files have test coverage."
954
+ });
955
+ }
956
+ if (params.file_path) {
957
+ const tests = brain.knowledge.testMap.tested[params.file_path] || [];
958
+ return JSON.stringify({
959
+ file: params.file_path,
960
+ tested_by: tests,
961
+ has_tests: tests.length > 0,
962
+ instruction: tests.length > 0 ? `Tested by: ${tests.join(", ")}` : "NO TESTS. Write tests for this file before making changes."
963
+ });
964
+ }
965
+ return JSON.stringify({
966
+ tested_files: Object.keys(brain.knowledge.testMap.tested).length,
967
+ untested_files: brain.knowledge.testMap.untested.length,
968
+ test_files: brain.knowledge.testMap.testFiles.length
969
+ });
970
+ }
971
+ function handleFindFanout(params, brain) {
972
+ const minImporters = parseInt(params.min_importers || "3") || 3;
973
+ const fanout = [];
974
+ for (const [file, importers] of Object.entries(brain.reverseDeps)) {
975
+ if (importers.length >= minImporters) {
976
+ fanout.push({ file, importers: importers.length, imported_by: importers });
977
+ }
978
+ }
979
+ fanout.sort((a, b) => b.importers - a.importers);
980
+ return JSON.stringify({ high_fanout_files: fanout, count: fanout.length });
981
+ }
982
+ function handleSearchLearnings(params, brain) {
983
+ const domains = (params.domains || "").split(",").map((d) => d.trim()).filter(Boolean);
984
+ if (domains.length === 0) return JSON.stringify({ error: "domains parameter is required", query: [], results: [], count: 0 });
985
+ const results = queryByDomains(brain.knowledgeBase, domains);
986
+ if (results.length > 0) recordCacheHit(brain.knowledgeBase);
987
+ const hasFailures = results.some((r) => r.outcome === "failure");
988
+ return JSON.stringify({
989
+ query: domains,
990
+ results: results.map((r) => ({
991
+ summary: r.summary,
992
+ lesson: r.lesson,
993
+ outcome: r.outcome,
994
+ date: r.date,
995
+ tags: r.tags,
996
+ access_count: r.accessCount
997
+ })),
998
+ count: results.length,
999
+ 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."
1000
+ });
1001
+ }
1002
+ function handleGetFalsePositives(_params, brain) {
1003
+ const fps = getFalsePositives(brain.knowledgeBase);
1004
+ return JSON.stringify({
1005
+ false_positives: fps.map((fp) => ({ summary: fp.summary, lesson: fp.lesson, date: fp.date })),
1006
+ count: fps.length,
1007
+ instruction: "Include these in review agent prompts as DO NOT FLAG items."
1008
+ });
1009
+ }
1010
+ function handleBrainStatus(_params, brain) {
1011
+ const summary = getKBSummary(brain.knowledgeBase);
1012
+ const claudeMdBytes = (() => {
1013
+ try {
1014
+ return statSync2(join2(brain.rootPath, "CLAUDE.md")).size;
1015
+ } catch {
1016
+ return 0;
1017
+ }
1018
+ })();
1019
+ const totalSessions = sessionCount(brain.rootPath);
1020
+ const hitRate = summary.totalEntries > 0 ? Math.round(summary.accessedEntries / summary.totalEntries * 100) : 0;
1021
+ return JSON.stringify({
1022
+ ...summary,
1023
+ knowledge_index: {
1024
+ files_indexed: brain.knowledge.summary.totalFiles,
1025
+ total_lines: brain.knowledge.summary.totalLines,
1026
+ import_edges: Object.keys(brain.knowledge.importGraph).length,
1027
+ exports_mapped: Object.keys(brain.knowledge.exports).length
1028
+ },
1029
+ token_accounting: {
1030
+ claude_md_bytes: claudeMdBytes,
1031
+ claude_md_kb: Math.round(claudeMdBytes / 1024 * 10) / 10,
1032
+ session_count: totalSessions,
1033
+ learnings_hit_rate_pct: hitRate,
1034
+ 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."
1035
+ },
1036
+ cache_age_ms: Date.now() - brain.loadedAt,
1037
+ instruction: "Brain is ready. Next: call knit_classify_task with the files you plan to touch to get your tier and phases."
1038
+ });
1039
+ }
1040
+ function handleClassifyTask(params, brain) {
1041
+ const rawFiles = (params.files_to_touch || "").split(",").map((f) => f.trim()).filter(Boolean);
1042
+ const files = rawFiles.filter((f) => f !== "unknown");
1043
+ const description = (params.description || "").toLowerCase();
1044
+ const domains = detectDomainsFromFiles(files);
1045
+ const crossDomainRipple = [];
1046
+ for (const file of files) {
1047
+ const importers = brain.reverseDeps[file] || [];
1048
+ if (importers.length >= 3) crossDomainRipple.push(`${file} is high-fanout (${importers.length} dependents)`);
1049
+ }
1050
+ const isTypes = files.some((f) => f.includes("types") || f.includes("schema"));
1051
+ const isAuth = files.some((f) => f.includes("auth") || f.includes("security"));
1052
+ const isNewProject = files.length === 0 || rawFiles.includes("unknown");
1053
+ const descriptionIsComplex = description.includes("architect") || description.includes("build from scratch") || description.includes("new project") || description.includes("system") || description.length > 100;
1054
+ const tier2 = isNewProject ? descriptionIsComplex ? "complex" : "standard" : domains.size >= 3 || isTypes || isAuth || files.length > 3 ? "complex" : domains.size >= 2 || files.length > 1 ? "standard" : "trivial";
1055
+ const phases2 = tier2 === "complex" ? ["RESEARCH", "IDEATE", "PLAN", "EXECUTE", "OPTIMIZE", "REVIEW", "LEARN"] : tier2 === "standard" ? ["RESEARCH", "EXECUTE", "OPTIMIZE", "REVIEW", "LEARN"] : ["EXECUTE", "VERIFY", "LEARN"];
1056
+ const instruction = tier2 === "complex" ? "ENTER PLAN MODE NOW. Call EnterPlanMode tool immediately. Do NOT start coding without a plan. This task touches 3+ domains and requires RESEARCH \u2192 IDEATE \u2192 PLAN \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW \u2192 LEARN." : tier2 === "standard" ? "Follow phases: RESEARCH \u2192 EXECUTE \u2192 OPTIMIZE \u2192 REVIEW \u2192 LEARN. No plan mode needed but do research first." : "Simple task. EXECUTE \u2192 VERIFY \u2192 LEARN. Do it directly, then record what you learned.";
1057
+ try {
1058
+ writeClassificationMarker(brain.rootPath, {
1059
+ turnId: `${Date.now()}-${process.pid}`,
1060
+ classifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
1061
+ tier: tier2,
1062
+ files
1063
+ });
1064
+ } catch {
1065
+ }
1066
+ return JSON.stringify({
1067
+ tier: tier2,
1068
+ affected_domains: [...domains],
1069
+ phases: phases2,
1070
+ files_count: files.length,
1071
+ cross_domain_ripple: crossDomainRipple,
1072
+ auto_plan_mode: tier2 === "complex",
1073
+ instruction,
1074
+ reasoning: tier2 === "complex" ? `Complex: ${domains.size} domains affected${isTypes ? ", touches shared types" : ""}${isAuth ? ", security-sensitive" : ""}` : tier2 === "standard" ? `Standard: ${domains.size} domain(s), ${files.length} file(s)` : `Trivial: 1 domain, simple change`
1075
+ });
1076
+ }
1077
+ function handleSetProtocolStrictness(params, brain) {
1078
+ const level = (params.level || "").trim().toLowerCase();
1079
+ if (!isValidStrictness(level)) {
1080
+ return JSON.stringify({
1081
+ status: "error",
1082
+ error: `Invalid level: "${params.level}". Must be one of: off, warn, block.`
1083
+ });
1084
+ }
1085
+ const config = writeProtocolConfig(brain.rootPath, level);
1086
+ return JSON.stringify({
1087
+ status: "set",
1088
+ level: config.level,
1089
+ updated_at: config.updatedAt,
1090
+ applies_to: "next-tool-call",
1091
+ note: level === "block" ? "PreToolUse hook will now HARD BLOCK Edit/Write without knit_classify_task first." : level === "warn" ? "PreToolUse hook will print a reminder but not block." : "Protocol Guard disabled. No checks on Edit/Write."
1092
+ });
1093
+ }
1094
+ function handleGetProtocolStrictness(_params, brain) {
1095
+ const config = readProtocolConfig(brain.rootPath);
1096
+ return JSON.stringify({ level: config.level, updated_at: config.updatedAt });
1097
+ }
1098
+ function handleBuildContext(params, brain) {
1099
+ const files = (params.files_to_touch || "").split(",").map((f) => f.trim()).filter(Boolean);
1100
+ const affectedDomains = detectDomainsFromFiles(files);
1101
+ const knownPitfalls = [];
1102
+ const ripple = [];
1103
+ for (const file of files) {
1104
+ const importers = brain.reverseDeps[file] || [];
1105
+ if (importers.length > 0) ripple.push(`${file} is imported by: ${importers.join(", ")}`);
1106
+ }
1107
+ const domainTags = [...affectedDomains].map((d) => d.toLowerCase().replace(/[^a-z]/g, ""));
1108
+ const learnings = queryByDomains(brain.knowledgeBase, domainTags);
1109
+ for (const l of learnings) knownPitfalls.push(`${l.summary}: ${l.lesson}`);
1110
+ const fps = getFalsePositives(brain.knowledgeBase);
1111
+ return JSON.stringify({
1112
+ domain_context: {
1113
+ affected_domains: [...affectedDomains],
1114
+ files_to_touch: files,
1115
+ cross_domain_ripple: ripple,
1116
+ known_pitfalls: knownPitfalls,
1117
+ false_positives: fps.map((fp) => `${fp.summary}: ${fp.lesson}`)
1118
+ },
1119
+ instruction: "Pass this entire object to every agent prompt in EXECUTE, OPTIMIZE, and REVIEW phases."
1120
+ });
1121
+ }
1122
+ function handleRecordLearning(params, brain) {
1123
+ if (!params.summary?.trim() && !params.lesson?.trim()) {
1124
+ return JSON.stringify({ error: "summary and lesson are required \u2014 cannot record empty learning" });
1125
+ }
1126
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1127
+ const entry = {
1128
+ date,
1129
+ summary: params.summary || "Untitled learning",
1130
+ domains: (params.domains || "general").split(",").map((d) => d.trim()),
1131
+ approach: params.approach || "",
1132
+ outcome: ["success", "partial", "failure"].includes(params.outcome) ? params.outcome : "success",
1133
+ lesson: params.lesson || "",
1134
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#"))
1135
+ };
1136
+ addEntry(brain.knowledgeBase, entry);
1137
+ saveKnowledgeBase(knowledgebasePath(brain.rootPath), brain.knowledgeBase);
1138
+ const learnDir = learningsDir(brain.rootPath);
1139
+ const mdFiles = existsSync4(learnDir) ? readdirSync(learnDir).filter((f) => f.endsWith(".md") && f !== "sessions.md") : [];
1140
+ if (mdFiles.length > 0) {
1141
+ const mdPath = join2(learnDir, mdFiles[0]);
1142
+ const mdEntry = `
1143
+ ## ${date} ${entry.summary}
1144
+ **Domain(s):** ${entry.domains.join(", ")}
1145
+ **Approach:** ${entry.approach}
1146
+ **Outcome:** ${entry.outcome}
1147
+ **Lesson:** ${entry.lesson}
1148
+ **Tags:** ${entry.tags.join(" ")}
1149
+ `;
1150
+ const existing = readFileSync4(mdPath, "utf-8");
1151
+ writeFileSync4(mdPath, existing + mdEntry, "utf-8");
1152
+ }
1153
+ return JSON.stringify({
1154
+ status: "recorded",
1155
+ entry: { date, summary: entry.summary, tags: entry.tags },
1156
+ kb_total: brain.knowledgeBase.entries.length,
1157
+ instruction: "Learning recorded. You may now report task as complete."
1158
+ });
1159
+ }
1160
+ function handleRecordFalsePositive(params, brain) {
1161
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1162
+ const entry = {
1163
+ date,
1164
+ summary: params.summary || "Untitled FP",
1165
+ domains: ["General"],
1166
+ approach: "Verified manually",
1167
+ outcome: "success",
1168
+ lesson: params.reason || "Confirmed non-issue",
1169
+ tags: [...(params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")), "#false-positive"]
1170
+ };
1171
+ addEntry(brain.knowledgeBase, entry);
1172
+ saveKnowledgeBase(knowledgebasePath(brain.rootPath), brain.knowledgeBase);
1173
+ return JSON.stringify({
1174
+ status: "recorded",
1175
+ summary: entry.summary,
1176
+ total_false_positives: getFalsePositives(brain.knowledgeBase).length,
1177
+ instruction: "This will be included in future agent prompts as a DO NOT FLAG item."
1178
+ });
1179
+ }
1180
+ function handleSaveHandoff(params, brain) {
1181
+ const handoffPath = join2(brain.rootPath, "handoff.md");
1182
+ const content = `# Session Handoff
1183
+
1184
+ **Goal:** ${params.goal || "Not specified"}
1185
+
1186
+ **Current State:** ${params.current_state || "Not specified"}
1187
+
1188
+ **Files in Flight:** ${params.files_in_flight || "None"}
1189
+
1190
+ **What Changed:** ${params.what_changed || "Nothing"}
1191
+
1192
+ **Failed Attempts:**
1193
+ ${params.failed_attempts || "None documented"}
1194
+
1195
+ **Decisions Made:** ${params.decisions_made || "None"}
1196
+
1197
+ **Next Step:** ${params.next_step || "Not specified"}
1198
+
1199
+ ---
1200
+ *Saved: ${(/* @__PURE__ */ new Date()).toISOString()}*
1201
+ `;
1202
+ writeFileSync4(handoffPath, content, "utf-8");
1203
+ return JSON.stringify({ status: "saved", path: "handoff.md", instruction: "Next session will read handoff.md first." });
1204
+ }
1205
+ function handleSetupProject(params, brain) {
1206
+ const description = params.description || "";
1207
+ const projectType = params.project_type || "auto";
1208
+ const domainNames = params.domains ? params.domains.split(",").map((d) => d.trim()) : inferDomainsFromDescription(description, projectType);
1209
+ const teamRoles = params.team_roles ? params.team_roles.split(",").map((r) => r.trim()) : domainNames;
1210
+ const teams = domainNames.map((domain, i) => ({
1211
+ name: domain.charAt(0).toUpperCase() + domain.slice(1).replace(/-/g, " "),
1212
+ role: `${teamRoles[i] || domain} specialist`,
1213
+ focus: `${domain} domain for: ${description.slice(0, 200)}`,
1214
+ agents: ["code-reviewer"],
1215
+ // generic — the PROMPT is what matters, not the agent type
1216
+ filePatterns: ["**/*"],
1217
+ reviewChecklist: [`Review ${domain} quality`, `Check ${domain} completeness`, `Verify ${domain} accuracy`]
1218
+ }));
1219
+ saveCustomTeams(brain.rootPath, teams);
1220
+ addEntry(brain.knowledgeBase, {
1221
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1222
+ summary: `Project setup: ${description.slice(0, 100)}`,
1223
+ domains: domainNames,
1224
+ approach: `Project type: ${projectType}. Domains: ${domainNames.join(", ")}`,
1225
+ outcome: "success",
1226
+ lesson: `This is a ${projectType} project. Key domains: ${domainNames.join(", ")}`,
1227
+ tags: ["#project-setup", ...domainNames.map((d) => `#${d.toLowerCase().replace(/\s+/g, "-")}`)]
1228
+ });
1229
+ saveKnowledgeBase(knowledgebasePath(brain.rootPath), brain.knowledgeBase);
1230
+ return JSON.stringify({
1231
+ status: "configured",
1232
+ project_type: projectType,
1233
+ domains: domainNames,
1234
+ teams_created: teams.length,
1235
+ teams: teams.map((t) => ({ name: t.name, role: t.role })),
1236
+ instruction: `Project configured with ${teams.length} teams. Use knit_start_team_review to run parallel team analysis. Use knit_classify_task to classify tasks before starting.`
1237
+ });
1238
+ }
1239
+ var DOMAIN_TEMPLATES = {
1240
+ // Code (handled by scanner, these are fallbacks)
1241
+ code: ["frontend", "backend", "database", "testing", "devops"],
1242
+ // Business
1243
+ startup: ["market-research", "business-model", "financial-projections", "competitive-analysis", "pitch-preparation"],
1244
+ marketing: ["market-research", "content-strategy", "campaign-creation", "analytics", "optimization"],
1245
+ sales: ["prospecting", "outreach", "pipeline-management", "deal-analysis", "forecasting"],
1246
+ // Research & Analysis
1247
+ research: ["literature-review", "data-collection", "analysis", "synthesis", "reporting"],
1248
+ finance: ["market-analysis", "risk-assessment", "portfolio-strategy", "compliance", "reporting"],
1249
+ "data-science": ["data-collection", "data-cleaning", "feature-engineering", "model-training", "evaluation"],
1250
+ // Creative
1251
+ writing: ["research", "outlining", "drafting", "editing", "publishing"],
1252
+ journalism: ["source-management", "investigation", "fact-checking", "writing", "editorial-review"],
1253
+ music: ["songwriting", "arrangement", "production", "mixing-mastering", "distribution"],
1254
+ video: ["pre-production", "scripting", "filming", "editing", "distribution"],
1255
+ // Design & Product
1256
+ design: ["user-research", "information-architecture", "visual-design", "prototyping", "usability-testing"],
1257
+ product: ["discovery", "requirements", "design", "development", "launch"],
1258
+ gamedev: ["game-design", "level-design", "art-assets", "programming", "playtesting"],
1259
+ // Technical
1260
+ devops: ["inventory", "migration-planning", "implementation", "security-review", "monitoring"],
1261
+ security: ["threat-modeling", "vulnerability-assessment", "penetration-testing", "remediation", "compliance"],
1262
+ architecture: ["requirements-analysis", "system-design", "component-design", "integration", "documentation"],
1263
+ // Domain-specific
1264
+ legal: ["document-review", "risk-identification", "compliance-check", "contract-analysis", "recommendations"],
1265
+ medical: ["data-collection", "clinical-analysis", "safety-review", "statistical-analysis", "reporting"],
1266
+ education: ["curriculum-design", "content-creation", "assessment-design", "review", "delivery"],
1267
+ realestate: ["market-research", "property-valuation", "financial-analysis", "risk-assessment", "recommendations"],
1268
+ hr: ["job-analysis", "candidate-sourcing", "screening", "interview-assessment", "onboarding"],
1269
+ consulting: ["discovery", "analysis", "strategy", "recommendations", "implementation-planning"]
1270
+ };
1271
+ function inferDomainsFromDescription(description, projectType) {
1272
+ if (DOMAIN_TEMPLATES[projectType]) {
1273
+ return DOMAIN_TEMPLATES[projectType];
1274
+ }
1275
+ const desc = description.toLowerCase();
1276
+ const typeScores = [];
1277
+ for (const [type, domains] of Object.entries(DOMAIN_TEMPLATES)) {
1278
+ let score = 0;
1279
+ if (desc.includes(type.replace("-", " "))) score += 10;
1280
+ if (desc.includes(type)) score += 10;
1281
+ for (const domain of domains) {
1282
+ const keywords = domain.replace(/-/g, " ").split(" ");
1283
+ for (const kw of keywords) {
1284
+ if (kw.length > 3 && desc.includes(kw)) score += 2;
1285
+ }
1286
+ }
1287
+ if (score > 0) typeScores.push([type, score]);
1288
+ }
1289
+ typeScores.sort((a, b) => b[1] - a[1]);
1290
+ if (typeScores.length > 0 && typeScores[0][1] >= 4) {
1291
+ return DOMAIN_TEMPLATES[typeScores[0][0]];
1292
+ }
1293
+ return ["planning", "research", "execution", "review", "delivery"];
1294
+ }
1295
+ function handleGetTeams(_params, brain) {
1296
+ const custom = loadCustomTeams(brain.rootPath);
1297
+ if (custom) return JSON.stringify({ source: "custom", teams: custom, count: custom.length });
1298
+ const scan = scanProject(brain.rootPath);
1299
+ const defaults = buildDefaultTeams(scan.domains);
1300
+ return JSON.stringify({ source: "auto-detected", teams: defaults, count: defaults.length });
1301
+ }
1302
+ function handleDefineTeam(params, brain) {
1303
+ const existing = loadCustomTeams(brain.rootPath) || [];
1304
+ const newTeam = {
1305
+ name: params.name,
1306
+ role: params.role,
1307
+ focus: params.focus,
1308
+ agents: (params.agents || "code-reviewer").split(",").map((a) => a.trim()),
1309
+ filePatterns: (params.file_patterns || "src/**").split(",").map((p) => p.trim()),
1310
+ reviewChecklist: (params.checklist || "").split("|").map((c) => c.trim()).filter(Boolean)
1311
+ };
1312
+ const idx = existing.findIndex((t) => t.name === newTeam.name);
1313
+ if (idx >= 0) existing[idx] = newTeam;
1314
+ else existing.push(newTeam);
1315
+ saveCustomTeams(brain.rootPath, existing);
1316
+ return JSON.stringify({ status: "saved", team: newTeam, total_teams: existing.length });
1317
+ }
1318
+ function handleStartTeamReview(params, brain) {
1319
+ const teamNames = params.teams === "all" || !params.teams ? (loadCustomTeams(brain.rootPath) || buildDefaultTeams([])).map((t) => t.name) : params.teams.split(",").map((t) => t.trim());
1320
+ const board = startTeamBoard(`review-${Date.now()}`, params.task_description, teamNames);
1321
+ return JSON.stringify({
1322
+ status: "started",
1323
+ board_id: board.taskId,
1324
+ teams: teamNames,
1325
+ instruction: `Launch ${teamNames.length} agents IN PARALLEL. For each team, call knit_get_team_prompt, then spawn an Agent. After each returns, call knit_post_team_findings. Finally, call knit_get_board_summary.`
1326
+ });
1327
+ }
1328
+ function handleGetTeamPrompt(params, brain) {
1329
+ const teams = loadCustomTeams(brain.rootPath) || buildDefaultTeams([
1330
+ { name: params.team_name, description: "", filePatterns: ["src/**"], agents: ["code-reviewer"] }
1331
+ ]);
1332
+ const team = teams.find((t) => t.name === params.team_name);
1333
+ if (!team) return JSON.stringify({ error: `Team "${params.team_name}" not found` });
1334
+ markTeamWorking(params.team_name);
1335
+ const files = (params.files_to_review || "").split(",").map((f) => f.trim()).filter(Boolean);
1336
+ const domainContext = {
1337
+ files_to_review: files.length > 0 ? files : team.filePatterns,
1338
+ knowledge_summary: {
1339
+ total_files: brain.knowledge.summary.totalFiles,
1340
+ high_fanout: brain.knowledge.summary.highFanoutFiles,
1341
+ untested: brain.knowledge.summary.untestedFiles
1342
+ }
1343
+ };
1344
+ const otherFindings = getOtherTeamFindings(params.team_name);
1345
+ const prompt = generateTeamPrompt(team, getTeamBoard()?.taskDescription || "", domainContext, otherFindings);
1346
+ return JSON.stringify({ team: team.name, prompt, agents_to_use: team.agents, instruction: "Spawn an Agent with this prompt." });
1347
+ }
1348
+ function handlePostTeamFindings(params, _brain) {
1349
+ let findings;
1350
+ try {
1351
+ const raw = JSON.parse(params.findings || "[]");
1352
+ findings = raw.map((f) => ({
1353
+ team: params.team_name,
1354
+ severity: VALID_SEVERITIES.has(String(f.severity).toUpperCase()) ? String(f.severity).toUpperCase() : "MEDIUM",
1355
+ file: f.file || "unknown",
1356
+ description: f.description || "",
1357
+ recommendation: f.recommendation || "",
1358
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1359
+ }));
1360
+ } catch {
1361
+ findings = [{
1362
+ team: params.team_name,
1363
+ severity: "LOW",
1364
+ file: "unknown",
1365
+ description: params.findings || "No structured findings",
1366
+ recommendation: "",
1367
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1368
+ }];
1369
+ }
1370
+ postTeamFindings(params.team_name, findings);
1371
+ const summary = getBoardSummary();
1372
+ return JSON.stringify({
1373
+ status: "posted",
1374
+ team: params.team_name,
1375
+ findings_count: findings.length,
1376
+ board_summary: summary,
1377
+ all_done: summary.allDone
1378
+ });
1379
+ }
1380
+ function handleGetBoardSummary(_params, _brain) {
1381
+ const board = getTeamBoard();
1382
+ if (!board) return JSON.stringify({ error: "No active review board. Call knit_start_team_review first." });
1383
+ const summary = getBoardSummary();
1384
+ const criticals = board.findings.filter((f) => f.severity === "CRITICAL");
1385
+ const highs = board.findings.filter((f) => f.severity === "HIGH");
1386
+ return JSON.stringify({
1387
+ task: board.taskDescription,
1388
+ ...summary,
1389
+ team_status: board.status,
1390
+ critical_findings: criticals.map((f) => `[${f.team}] ${f.file}: ${f.description}`),
1391
+ high_findings: highs.map((f) => `[${f.team}] ${f.file}: ${f.description}`),
1392
+ gate: summary.critical > 0 ? "BLOCKED \u2014 fix CRITICAL findings before proceeding" : summary.high > 0 ? "WARNING \u2014 HIGH findings should be addressed" : "PASSED \u2014 no blocking findings"
1393
+ });
1394
+ }
1395
+ function handleReflect(_params, brain) {
1396
+ const patterns = reflect(brain.knowledgeBase);
1397
+ if (patterns.length === 0) {
1398
+ return JSON.stringify({
1399
+ patterns: [],
1400
+ message: "Not enough data yet. Record more learnings (minimum 3) for patterns to emerge. Also try knit_search_global_learnings for cross-project patterns."
1401
+ });
1402
+ }
1403
+ return JSON.stringify({
1404
+ patterns: patterns.slice(0, 10).map((p) => ({
1405
+ type: p.type,
1406
+ description: p.description,
1407
+ confidence: p.confidence,
1408
+ occurrences: p.occurrences,
1409
+ domains: p.domains
1410
+ })),
1411
+ total_patterns: patterns.length,
1412
+ insight: patterns[0].confidence >= 7 ? `Strongest pattern: ${patterns[0].description}` : "Patterns are forming but not yet high-confidence. Keep recording learnings."
1413
+ });
1414
+ }
1415
+ function handleGetSuggestions(params, brain) {
1416
+ const domains = (params.domains || "").split(",").map((d) => d.trim()).filter(Boolean);
1417
+ if (domains.length === 0) {
1418
+ return JSON.stringify({ error: "domains parameter required", suggestions: [] });
1419
+ }
1420
+ const suggestions = getAdaptiveSuggestions(brain.knowledgeBase, domains);
1421
+ if (suggestions.length === 0) {
1422
+ return JSON.stringify({
1423
+ suggestions: [],
1424
+ message: `No patterns yet for domains: ${domains.join(", ")}. Try knit_search_global_learnings for cross-project insights.`
1425
+ });
1426
+ }
1427
+ return JSON.stringify({
1428
+ domains_queried: domains,
1429
+ suggestions,
1430
+ message: `${suggestions.length} adaptive suggestions based on past patterns in these domains.`
1431
+ });
1432
+ }
1433
+ function handleRecordGlobalLearning(params, brain) {
1434
+ const summary = (params.summary || "").slice(0, 500);
1435
+ const lesson = (params.lesson || "").slice(0, 2e3);
1436
+ const tags = (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#"));
1437
+ const outcomeRaw = (params.outcome || "").toLowerCase();
1438
+ const outcome = ["success", "partial", "failure"].includes(outcomeRaw) ? outcomeRaw : void 0;
1439
+ if (!summary || !lesson || tags.length === 0) {
1440
+ return JSON.stringify({ error: "summary, lesson, and tags are all required" });
1441
+ }
1442
+ const entry = buildGlobalLearning(brain.rootPath, { summary, lesson, tags, outcome });
1443
+ appendGlobalLearning(entry);
1444
+ return JSON.stringify({
1445
+ status: "saved",
1446
+ id: entry.id,
1447
+ project: entry.projectName,
1448
+ instruction: "Cross-project learning saved. Future knit_search_global_learnings calls from any project will find it."
1449
+ });
1450
+ }
1451
+ function handleSearchGlobalLearnings(params, _brain) {
1452
+ const query = (params.query || "").trim();
1453
+ const limit = Math.max(1, Math.min(50, parseInt(params.limit || "10", 10) || 10));
1454
+ if (!query) {
1455
+ return JSON.stringify({ error: "query is required", results: [] });
1456
+ }
1457
+ const matches = searchGlobalLearnings(query, limit);
1458
+ return JSON.stringify({
1459
+ query,
1460
+ count: matches.length,
1461
+ results: matches.map((m) => ({
1462
+ id: m.id,
1463
+ date: m.date,
1464
+ from_project: m.projectName,
1465
+ summary: m.summary,
1466
+ lesson: m.lesson,
1467
+ tags: m.tags,
1468
+ outcome: m.outcome
1469
+ })),
1470
+ instruction: matches.length === 0 ? "No cross-project matches. This area might be new across all your projects." : `Found ${matches.length} cross-project learning(s). Review before duplicating work.`
1471
+ });
1472
+ }
1473
+ function handleLoadSession(_params, brain) {
1474
+ const root = brain.rootPath;
1475
+ const sessionsFile = sessionsLogPath(root);
1476
+ let lastSession = null;
1477
+ if (existsSync4(sessionsFile)) {
1478
+ const content = readFileSync4(sessionsFile, "utf-8");
1479
+ const sessions = content.split(/^## Session/m).slice(1);
1480
+ if (sessions.length > 0) {
1481
+ const last = sessions[sessions.length - 1].trim();
1482
+ lastSession = last.slice(0, 300);
1483
+ }
1484
+ }
1485
+ const handoffPath = join2(root, "handoff.md");
1486
+ let handoff2 = null;
1487
+ if (existsSync4(handoffPath)) {
1488
+ handoff2 = readFileSync4(handoffPath, "utf-8").slice(0, 2e3);
1489
+ }
1490
+ const topLearnings = brain.knowledgeBase.entries.filter((e) => e.accessCount > 0).sort((a, b) => b.accessCount - a.accessCount).slice(0, 5).map((e) => ({ summary: e.summary, lesson: e.lesson, tags: e.tags, accessed: e.accessCount }));
1491
+ const fps = brain.knowledgeBase.entries.filter((e) => e.tags.includes("#false-positive")).map((e) => ({ summary: e.summary, lesson: e.lesson }));
1492
+ const teamsFile = teamsPath(root);
1493
+ let teams = [];
1494
+ if (existsSync4(teamsFile)) {
1495
+ try {
1496
+ const t = JSON.parse(readFileSync4(teamsFile, "utf-8"));
1497
+ teams = t.map((team) => team.name);
1498
+ } catch {
1499
+ }
1500
+ }
1501
+ const knowledge = {
1502
+ files: brain.knowledge.summary.totalFiles,
1503
+ imports: Object.keys(brain.knowledge.importGraph).length,
1504
+ high_fanout: brain.knowledge.summary.highFanoutFiles,
1505
+ untested: brain.knowledge.summary.untestedFiles.slice(0, 5)
1506
+ };
1507
+ const metrics = {
1508
+ total_sessions: brain.knowledgeBase.metrics.totalSessions,
1509
+ total_learnings: brain.knowledgeBase.entries.length,
1510
+ cache_hits: brain.knowledgeBase.metrics.cacheHits
1511
+ };
1512
+ const recentSessions = getRecentSessions(root, 3).map((s) => ({
1513
+ date: s.date,
1514
+ branch: s.branch ?? null,
1515
+ summary: s.summary ?? "",
1516
+ tags: s.tags ?? [],
1517
+ outcome: s.outcome
1518
+ }));
1519
+ const patterns = reflect(brain.knowledgeBase).slice(0, 3).map((p) => ({ type: p.type, description: p.description, confidence: p.confidence }));
1520
+ return JSON.stringify({
1521
+ session_context: {
1522
+ last_session: lastSession,
1523
+ handoff: handoff2,
1524
+ has_unfinished_work: handoff2 !== null
1525
+ },
1526
+ intelligence: {
1527
+ top_learnings: topLearnings,
1528
+ false_positives: fps,
1529
+ patterns
1530
+ },
1531
+ project: {
1532
+ knowledge,
1533
+ teams,
1534
+ metrics,
1535
+ recent_sessions: recentSessions
1536
+ },
1537
+ instruction: handoff2 ? "UNFINISHED WORK DETECTED. Read the handoff above \u2014 pick up where the last session left off. Do NOT start fresh." : topLearnings.length > 0 ? `Session loaded. ${topLearnings.length} key learnings available. ${fps.length} false positives to suppress. Call knit_classify_task to begin.` : recentSessions.length > 0 ? `Session loaded. ${recentSessions.length} recent sessions on file. Call knit_classify_task to begin.` : "Fresh brain \u2014 no past learnings yet. Call knit_classify_task to begin your first task."
1538
+ });
1539
+ }
1540
+ function handleSaveSessionSummary(params, brain) {
1541
+ const validOutcomes = ["shipped", "wip", "failed", "unknown"];
1542
+ const outcomeRaw = params.outcome || "unknown";
1543
+ const outcome = validOutcomes.includes(outcomeRaw) ? outcomeRaw : "unknown";
1544
+ const entry = {
1545
+ id: `${Date.now()}-agent`,
1546
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1547
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1548
+ summary: (params.summary || "").slice(0, 500),
1549
+ tags: (params.tags || "").split(/\s+/).filter((t) => t.startsWith("#")),
1550
+ outcome,
1551
+ filesTouched: params.files_touched ? params.files_touched.split(",").map((f) => f.trim()).filter(Boolean) : void 0,
1552
+ domainsTouched: params.domains ? params.domains.split(",").map((d) => d.trim()).filter(Boolean) : void 0
1553
+ };
1554
+ appendSession(brain.rootPath, entry);
1555
+ return JSON.stringify({
1556
+ status: "saved",
1557
+ id: entry.id,
1558
+ summary: entry.summary,
1559
+ instruction: "Session summary recorded. Future knit_search_sessions calls can find this."
1560
+ });
1561
+ }
1562
+ function handleGetWorkflow(params, brain) {
1563
+ const phase = (params.phase || "").trim().toLowerCase();
1564
+ if (!phase) {
1565
+ return JSON.stringify({
1566
+ sections: listWorkflowSections(),
1567
+ instruction: "Call knit_get_workflow with one of the section names to fetch its content."
1568
+ });
1569
+ }
1570
+ const buildCommands = {
1571
+ typecheck: brain.config.stack.typecheckCommand ?? void 0,
1572
+ lint: brain.config.stack.lintCommand ?? void 0,
1573
+ test: brain.config.stack.testFramework ? `${brain.config.packageManager === "unknown" ? "npm" : brain.config.packageManager} test` : void 0,
1574
+ build: brain.config.stack.buildCommand ?? void 0
1575
+ };
1576
+ const content = getWorkflowSection(phase, { buildCommands });
1577
+ if (content === null) {
1578
+ return JSON.stringify({
1579
+ error: `Unknown phase: "${phase}".`,
1580
+ available: listWorkflowSections().map((s) => s.name)
1581
+ });
1582
+ }
1583
+ return JSON.stringify({
1584
+ phase,
1585
+ content,
1586
+ instruction: "Apply this section to the current task. For another phase, call knit_get_workflow again with that phase name."
1587
+ });
1588
+ }
1589
+ function handleInstallAgent(params, brain) {
1590
+ const name = (params.name || "").trim();
1591
+ const refresh = (params.refresh || "").toLowerCase() === "true";
1592
+ if (!name) return JSON.stringify({ error: "name is required" });
1593
+ installAgentsForProject(
1594
+ brain.rootPath,
1595
+ brain.config,
1596
+ brain.knowledge,
1597
+ brain.knowledgeBase,
1598
+ { only: [name], refresh }
1599
+ ).catch((err) => {
1600
+ process.stderr.write(`[knit] handleInstallAgent background error for ${name}: ${err?.message ?? err}
1601
+ `);
1602
+ });
1603
+ return JSON.stringify({
1604
+ status: "queued",
1605
+ agent: name,
1606
+ target: `<project>/.claude/agents/knit-${name}.md`,
1607
+ instruction: "Install started in background. File will be ready within a few seconds. If it fails, see stderr \u2014 engram does not throw from this handler."
1608
+ });
1609
+ }
1610
+ function handleSpawnTeamWorktree(params, brain) {
1611
+ const teamName = (params.team_name || "").trim();
1612
+ const taskDescription = (params.task_description || "").trim();
1613
+ if (!teamName) {
1614
+ return JSON.stringify({ error: "team_name is required" });
1615
+ }
1616
+ if (!taskDescription) {
1617
+ return JSON.stringify({ error: "task_description is required" });
1618
+ }
1619
+ try {
1620
+ const record = spawnWorktree(brain.rootPath, teamName, taskDescription);
1621
+ return JSON.stringify({
1622
+ status: "spawned",
1623
+ team_name: record.teamName,
1624
+ team_slug: record.teamSlug,
1625
+ path: record.path,
1626
+ branch: record.branch,
1627
+ task_description: record.taskDescription,
1628
+ instruction: `Worktree ready at ${record.path}. Pass this path to the team's agents. They should cd there and make their changes on branch ${record.branch}. When done, call knit_finalize_team_worktree with action="merge".`
1629
+ });
1630
+ } catch (err) {
1631
+ const msg = err instanceof Error ? err.message : String(err);
1632
+ return JSON.stringify({ error: msg });
1633
+ }
1634
+ }
1635
+ function handleListTeamWorktrees(params, brain) {
1636
+ const includeFinalized = (params.include_finalized || "").toLowerCase() === "true";
1637
+ const records = listWorktrees(brain.rootPath, includeFinalized);
1638
+ return JSON.stringify({
1639
+ count: records.length,
1640
+ worktrees: records.map((w) => ({
1641
+ team_name: w.teamName,
1642
+ team_slug: w.teamSlug,
1643
+ path: w.path,
1644
+ branch: w.branch,
1645
+ task_description: w.taskDescription,
1646
+ created_at: w.createdAt,
1647
+ status: w.status
1648
+ }))
1649
+ });
1650
+ }
1651
+ function handleFinalizeTeamWorktree(params, brain) {
1652
+ const teamName = (params.team_name || "").trim();
1653
+ const action = (params.action || "").trim().toLowerCase();
1654
+ if (!teamName) {
1655
+ return JSON.stringify({ error: "team_name is required" });
1656
+ }
1657
+ if (action !== "merge" && action !== "discard") {
1658
+ return JSON.stringify({ error: 'action must be "merge" or "discard"' });
1659
+ }
1660
+ try {
1661
+ const result = finalizeWorktree(brain.rootPath, teamName, action);
1662
+ return JSON.stringify({
1663
+ status: result.status,
1664
+ team_name: result.worktree.teamName,
1665
+ branch: result.worktree.branch,
1666
+ conflict_files: result.conflictFiles,
1667
+ message: result.message,
1668
+ instruction: result.status === "merged" ? `Worktree merged and removed. Branch ${result.worktree.branch} deleted.` : result.status === "discarded" ? `Worktree discarded. Branch ${result.worktree.branch} deleted; changes lost.` : `Merge conflict on ${result.conflictFiles?.length ?? 0} file(s). Resolve in the main repo, then call this tool again with action="merge" to retry, or "discard" to throw away.`
1669
+ });
1670
+ } catch (err) {
1671
+ const msg = err instanceof Error ? err.message : String(err);
1672
+ return JSON.stringify({ error: msg });
1673
+ }
1674
+ }
1675
+ function handleSearchSessions(params, brain) {
1676
+ const query = params.query || "";
1677
+ const limit = Math.max(1, Math.min(50, parseInt(params.limit || "10", 10) || 10));
1678
+ if (!query.trim()) {
1679
+ return JSON.stringify({ error: "query is required", results: [] });
1680
+ }
1681
+ const matches = searchSessions(brain.rootPath, query, limit);
1682
+ return JSON.stringify({
1683
+ query,
1684
+ count: matches.length,
1685
+ results: matches.map((s) => ({
1686
+ id: s.id,
1687
+ date: s.date,
1688
+ branch: s.branch ?? null,
1689
+ summary: s.summary ?? "",
1690
+ tags: s.tags ?? [],
1691
+ outcome: s.outcome,
1692
+ files_modified: s.filesModified ?? (s.filesTouched?.length ?? 0)
1693
+ })),
1694
+ instruction: matches.length === 0 ? "No matching sessions. This might be the first time we tackle this area." : `Found ${matches.length} matching past session(s). Review summaries before duplicating prior work.`
1695
+ });
1696
+ }
1697
+ function handlePruneSessions(params, brain) {
1698
+ const raw = parseInt(params.max_age_days || "90", 10);
1699
+ const maxAgeDays = Number.isFinite(raw) && raw > 0 ? Math.min(raw, 36500) : 90;
1700
+ try {
1701
+ const { kept, pruned } = pruneSessionsByAge(brain.rootPath, maxAgeDays);
1702
+ return JSON.stringify({
1703
+ status: "ok",
1704
+ kept,
1705
+ pruned,
1706
+ max_age_days: maxAgeDays,
1707
+ instruction: pruned === 0 ? `No sessions older than ${maxAgeDays} days. Nothing to prune.` : `Pruned ${pruned} session(s) older than ${maxAgeDays} days. ${kept} kept.`
1708
+ });
1709
+ } catch (err) {
1710
+ const msg = err instanceof Error ? err.message : String(err);
1711
+ return JSON.stringify({ status: "error", error: msg });
1712
+ }
1713
+ }
1714
+
1715
+ // src/mcp/tools.ts
1716
+ function getToolDefinitions() {
1717
+ return [
1718
+ // ── Query (read the brain) ───────────────────────────────────
1719
+ {
1720
+ name: "knit_query_imports",
1721
+ description: "Who imports this file? Returns the reverse dependency list \u2014 call before editing to know the blast radius.",
1722
+ inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
1723
+ },
1724
+ {
1725
+ name: "knit_query_dependents",
1726
+ description: "What does this file import? Returns the file's own dependencies.",
1727
+ inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
1728
+ },
1729
+ {
1730
+ name: "knit_query_exports",
1731
+ description: "What does this file expose? Functions, classes, interfaces, types, constants.",
1732
+ inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path." } }, required: ["file_path"] }
1733
+ },
1734
+ {
1735
+ name: "knit_query_tests",
1736
+ description: 'Is this file tested? Or pass filter="untested" to list all untested files.',
1737
+ inputSchema: { type: "object", properties: { file_path: { type: "string", description: "Relative file path (optional)." }, filter: { type: "string", description: '"untested" to list all untested files.' } } }
1738
+ },
1739
+ {
1740
+ name: "knit_find_fanout",
1741
+ description: "High-fanout files \u2014 imported by many others. These are the contracts; change carefully.",
1742
+ inputSchema: { type: "object", properties: { min_importers: { type: "string", description: "Minimum importers to qualify (default: 3)." } } }
1743
+ },
1744
+ {
1745
+ name: "knit_search_learnings",
1746
+ description: "Search past learnings by domain tag. Returns prior lessons and mistakes to avoid.",
1747
+ inputSchema: { type: "object", properties: { domains: { type: "string", description: "Comma-separated domain tags." } }, required: ["domains"] }
1748
+ },
1749
+ {
1750
+ name: "knit_get_false_positives",
1751
+ description: "Confirmed non-issues. Pass to review agents so they don't re-flag them.",
1752
+ inputSchema: { type: "object", properties: {} }
1753
+ },
1754
+ {
1755
+ name: "knit_brain_status",
1756
+ description: "Brain health + token-accounting: learnings, hit rate, CLAUDE.md size, session count.",
1757
+ inputSchema: { type: "object", properties: {} }
1758
+ },
1759
+ // ── Update (write to the brain) ──────────────────────────────
1760
+ {
1761
+ name: "knit_classify_task",
1762
+ description: "Call first on every task. Classifies tier (trivial/standard/complex), returns phases + domains + auto plan mode flag. Also triggers project auto-init.",
1763
+ inputSchema: { type: "object", properties: { files_to_touch: { type: "string", description: 'Comma-separated files, or "unknown" for new projects.' }, description: { type: "string", description: "Brief task description." } }, required: ["files_to_touch"] }
1764
+ },
1765
+ {
1766
+ name: "knit_build_context",
1767
+ description: "Build a context object for the current task: domains, ripple effects, pitfalls, false positives.",
1768
+ inputSchema: { type: "object", properties: { files_to_touch: { type: "string", description: "Comma-separated files." } }, required: ["files_to_touch"] }
1769
+ },
1770
+ {
1771
+ name: "knit_record_learning",
1772
+ description: "Record a non-obvious, reusable insight. Quality check first: would session N+1 searching this tag be glad it exists? If no, skip.",
1773
+ inputSchema: { type: "object", properties: { summary: { type: "string", description: "One-line summary." }, domains: { type: "string", description: "Comma-separated domains." }, approach: { type: "string", description: "What approach was taken." }, outcome: { type: "string", description: "success | partial | failure." }, lesson: { type: "string", description: "What to repeat or avoid." }, tags: { type: "string", description: 'Space-separated tags (e.g. "#api #auth").' } }, required: ["summary", "lesson", "tags"] }
1774
+ },
1775
+ {
1776
+ name: "knit_record_false_positive",
1777
+ description: "Mark a finding as a confirmed non-issue so future review agents stop re-flagging it.",
1778
+ inputSchema: { type: "object", properties: { summary: { type: "string", description: "What was flagged." }, reason: { type: "string", description: "Why it's not a real issue." }, tags: { type: "string", description: "Domain tags." } }, required: ["summary", "reason"] }
1779
+ },
1780
+ {
1781
+ name: "knit_save_handoff",
1782
+ description: "Save state for the next session when context degrades. failed_attempts is the load-bearing field.",
1783
+ inputSchema: { type: "object", properties: { goal: { type: "string", description: "What we're trying to accomplish." }, current_state: { type: "string", description: "Where we are now." }, files_in_flight: { type: "string", description: "Files being modified." }, what_changed: { type: "string", description: "Commits and edits." }, failed_attempts: { type: "string", description: "What was tried and why it failed." }, decisions_made: { type: "string", description: "Important choices." }, next_step: { type: "string", description: "ONE most important next thing." } }, required: ["goal", "current_state", "failed_attempts", "next_step"] }
1784
+ },
1785
+ {
1786
+ name: "knit_setup_project",
1787
+ description: "Describe a project (especially non-code: research, legal, marketing). Generates domain-specific teams.",
1788
+ inputSchema: {
1789
+ type: "object",
1790
+ properties: {
1791
+ project_type: { type: "string", description: "code | research | analysis | writing | design | custom." },
1792
+ description: { type: "string", description: "What the project does." },
1793
+ domains: { type: "string", description: "Comma-separated domains." },
1794
+ team_roles: { type: "string", description: "Comma-separated team roles." }
1795
+ },
1796
+ required: ["description"]
1797
+ }
1798
+ },
1799
+ // ── Teams (parallel review board) ────────────────────────────
1800
+ {
1801
+ name: "knit_get_teams",
1802
+ description: "List agent teams configured for this project.",
1803
+ inputSchema: { type: "object", properties: {} }
1804
+ },
1805
+ {
1806
+ name: "knit_define_team",
1807
+ description: "Create or update a custom agent team.",
1808
+ inputSchema: { type: "object", properties: { name: { type: "string", description: "Team name." }, role: { type: "string", description: "Team role." }, focus: { type: "string", description: "Team focus area." }, agents: { type: "string", description: "Comma-separated agent types." }, file_patterns: { type: "string", description: "Comma-separated globs." }, checklist: { type: "string", description: "Pipe-separated review items." } }, required: ["name", "role", "focus"] }
1809
+ },
1810
+ {
1811
+ name: "knit_start_team_review",
1812
+ description: "Start a parallel team review with a shared findings board.",
1813
+ inputSchema: { type: "object", properties: { task_description: { type: "string", description: "What the teams review." }, teams: { type: "string", description: 'Comma-separated team names or "all".' } }, required: ["task_description"] }
1814
+ },
1815
+ {
1816
+ name: "knit_get_team_prompt",
1817
+ description: "Get the prompt for a team, including other teams' findings.",
1818
+ inputSchema: { type: "object", properties: { team_name: { type: "string", description: "Team name." }, files_to_review: { type: "string", description: "Comma-separated files." } }, required: ["team_name"] }
1819
+ },
1820
+ {
1821
+ name: "knit_post_team_findings",
1822
+ description: "Post a team's findings to the shared board.",
1823
+ inputSchema: { type: "object", properties: { team_name: { type: "string", description: "Team posting." }, findings: { type: "string", description: "JSON array of findings." } }, required: ["team_name", "findings"] }
1824
+ },
1825
+ {
1826
+ name: "knit_get_board_summary",
1827
+ description: "Cross-team findings, severity-gated.",
1828
+ inputSchema: { type: "object", properties: {} }
1829
+ },
1830
+ // ── Session memory ───────────────────────────────────────────
1831
+ {
1832
+ name: "knit_load_session",
1833
+ description: "Call at session start. Returns last sessions, handoff, top learnings, false positives, teams, project knowledge in one round trip.",
1834
+ inputSchema: { type: "object", properties: {} }
1835
+ },
1836
+ {
1837
+ name: "knit_save_session_summary",
1838
+ description: "Opt-in. Record a narrative summary if this session accomplished something a future session would search for. Quality check first.",
1839
+ inputSchema: {
1840
+ type: "object",
1841
+ properties: {
1842
+ summary: { type: "string", description: "One-line summary." },
1843
+ tags: { type: "string", description: 'Space-separated tags like "#auth #refactor".' },
1844
+ outcome: { type: "string", description: "shipped | wip | failed | unknown." },
1845
+ files_touched: { type: "string", description: "Comma-separated files (optional)." },
1846
+ domains: { type: "string", description: "Comma-separated domains (optional)." }
1847
+ },
1848
+ required: ["summary", "tags", "outcome"]
1849
+ }
1850
+ },
1851
+ {
1852
+ name: "knit_prune_sessions",
1853
+ description: "Prune entries older than max_age_days (default 90) from this project's sessions.jsonl. Atomic rewrite. Also runs automatically on auto-init.",
1854
+ inputSchema: {
1855
+ type: "object",
1856
+ properties: {
1857
+ max_age_days: { type: "string", description: "Maximum age in days (default 90)." }
1858
+ }
1859
+ }
1860
+ },
1861
+ {
1862
+ name: "knit_search_sessions",
1863
+ description: "Search past sessions by free text over summary + tags + branch. Check before duplicating prior work.",
1864
+ inputSchema: {
1865
+ type: "object",
1866
+ properties: {
1867
+ query: { type: "string", description: "Free text or tag." },
1868
+ limit: { type: "string", description: "Max results (default 10)." }
1869
+ },
1870
+ required: ["query"]
1871
+ }
1872
+ },
1873
+ // ── Workflow on demand ───────────────────────────────────────
1874
+ {
1875
+ name: "knit_get_workflow",
1876
+ description: "Fetch protocol depth for one phase. Sections: overview, tier, phases, research, ideate, plan, execute, optimize, review, tdd, learn, handoff, ship, tools. Omit phase to list all.",
1877
+ inputSchema: {
1878
+ type: "object",
1879
+ properties: {
1880
+ phase: { type: "string", description: "Section name. Omit to list all." }
1881
+ }
1882
+ }
1883
+ },
1884
+ // ── Parallel team worktrees ──────────────────────────────────
1885
+ {
1886
+ name: "knit_spawn_team_worktree",
1887
+ description: "Create a git worktree for a team. Multiple agents within the team can work in parallel inside it without colliding with other teams.",
1888
+ inputSchema: {
1889
+ type: "object",
1890
+ properties: {
1891
+ team_name: { type: "string", description: 'Team display name (e.g., "UI", "API & Security").' },
1892
+ task_description: { type: "string", description: "What this team is doing." }
1893
+ },
1894
+ required: ["team_name", "task_description"]
1895
+ }
1896
+ },
1897
+ {
1898
+ name: "knit_list_team_worktrees",
1899
+ description: "List active team worktrees. Pass include_finalized=true for full history.",
1900
+ inputSchema: {
1901
+ type: "object",
1902
+ properties: {
1903
+ include_finalized: { type: "string", description: '"true" for full history (default: active only).' }
1904
+ }
1905
+ }
1906
+ },
1907
+ // ── Cross-project learnings (Model C — global pool) ─────────
1908
+ {
1909
+ name: "knit_record_global_learning",
1910
+ description: "Opt-in. Record a learning to the cross-project pool when the insight generalizes beyond this project (e.g., Stripe webhook signature rules). Per-project knit_record_learning stays primary.",
1911
+ inputSchema: {
1912
+ type: "object",
1913
+ properties: {
1914
+ summary: { type: "string", description: "One-line summary." },
1915
+ lesson: { type: "string", description: "Generalizable lesson." },
1916
+ tags: { type: "string", description: "Space-separated tags." },
1917
+ outcome: { type: "string", description: "success | partial | failure (optional)." }
1918
+ },
1919
+ required: ["summary", "lesson", "tags"]
1920
+ }
1921
+ },
1922
+ {
1923
+ name: "knit_search_global_learnings",
1924
+ description: "Search the cross-project learnings pool. Use to check whether a similar lesson has been recorded in any project on this machine.",
1925
+ inputSchema: {
1926
+ type: "object",
1927
+ properties: {
1928
+ query: { type: "string", description: "Free text or tag." },
1929
+ limit: { type: "string", description: "Max results (default 10)." }
1930
+ },
1931
+ required: ["query"]
1932
+ }
1933
+ },
1934
+ // ── Pattern reflection (now backed by Model C, useful with ≥3 entries) ──
1935
+ {
1936
+ name: "knit_reflect",
1937
+ description: "Detect patterns across recorded learnings (per-project + global pool). Returns recurring themes, repeated failures, domain co-occurrences. Needs \u22653 learnings to surface anything meaningful.",
1938
+ inputSchema: { type: "object", properties: {} }
1939
+ },
1940
+ {
1941
+ name: "knit_get_suggestions",
1942
+ description: 'Adaptive suggestions for the current task based on past patterns in given domains. "Based on history, watch out for X."',
1943
+ inputSchema: { type: "object", properties: { domains: { type: "string", description: "Comma-separated domains for this task." } }, required: ["domains"] }
1944
+ },
1945
+ {
1946
+ name: "knit_install_agent",
1947
+ description: "Install or refresh one subagent. Writes <project>/.claude/agents/knit-<name>.md, personalized with project context. Use mid-session if a team needs an agent that isn't on disk yet.",
1948
+ inputSchema: {
1949
+ type: "object",
1950
+ properties: {
1951
+ name: { type: "string", description: 'Agent name (e.g., "typescript-pro", "security-engineer").' },
1952
+ refresh: { type: "string", description: '"true" to force re-fetch even if cached.' }
1953
+ },
1954
+ required: ["name"]
1955
+ }
1956
+ },
1957
+ {
1958
+ name: "knit_finalize_team_worktree",
1959
+ description: "Merge or discard a team's worktree. Merge surfaces conflict files without destroying the worktree on failure.",
1960
+ inputSchema: {
1961
+ type: "object",
1962
+ properties: {
1963
+ team_name: { type: "string", description: "Team name or slug." },
1964
+ action: { type: "string", description: '"merge" or "discard".' }
1965
+ },
1966
+ required: ["team_name", "action"]
1967
+ }
1968
+ },
1969
+ // ── Protocol Guard ───────────────────────────────────────────
1970
+ {
1971
+ name: "knit_set_protocol_strictness",
1972
+ description: "Set Protocol Guard strictness for this project. off = no checks. warn = reminder only (default). block = hard-fail Edit/Write without prior knit_classify_task.",
1973
+ inputSchema: { type: "object", properties: { level: { type: "string", description: "One of: off | warn | block." } }, required: ["level"] }
1974
+ },
1975
+ {
1976
+ name: "knit_get_protocol_strictness",
1977
+ description: "Read current Protocol Guard strictness level for this project.",
1978
+ inputSchema: { type: "object", properties: {} }
1979
+ }
1980
+ ];
1981
+ }
1982
+ var handlers = {
1983
+ knit_query_imports: handleQueryImports,
1984
+ knit_query_dependents: handleQueryDependents,
1985
+ knit_query_exports: handleQueryExports,
1986
+ knit_query_tests: handleQueryTests,
1987
+ knit_find_fanout: handleFindFanout,
1988
+ knit_search_learnings: handleSearchLearnings,
1989
+ knit_get_false_positives: handleGetFalsePositives,
1990
+ knit_brain_status: handleBrainStatus,
1991
+ knit_classify_task: handleClassifyTask,
1992
+ knit_build_context: handleBuildContext,
1993
+ knit_record_learning: handleRecordLearning,
1994
+ knit_record_false_positive: handleRecordFalsePositive,
1995
+ knit_save_handoff: handleSaveHandoff,
1996
+ knit_setup_project: handleSetupProject,
1997
+ knit_get_teams: handleGetTeams,
1998
+ knit_define_team: handleDefineTeam,
1999
+ knit_start_team_review: handleStartTeamReview,
2000
+ knit_get_team_prompt: handleGetTeamPrompt,
2001
+ knit_post_team_findings: handlePostTeamFindings,
2002
+ knit_get_board_summary: handleGetBoardSummary,
2003
+ knit_load_session: handleLoadSession,
2004
+ knit_save_session_summary: handleSaveSessionSummary,
2005
+ knit_search_sessions: handleSearchSessions,
2006
+ knit_prune_sessions: handlePruneSessions,
2007
+ knit_get_workflow: handleGetWorkflow,
2008
+ knit_spawn_team_worktree: handleSpawnTeamWorktree,
2009
+ knit_list_team_worktrees: handleListTeamWorktrees,
2010
+ knit_finalize_team_worktree: handleFinalizeTeamWorktree,
2011
+ knit_record_global_learning: handleRecordGlobalLearning,
2012
+ knit_search_global_learnings: handleSearchGlobalLearnings,
2013
+ knit_reflect: handleReflect,
2014
+ knit_get_suggestions: handleGetSuggestions,
2015
+ knit_install_agent: handleInstallAgent,
2016
+ knit_set_protocol_strictness: handleSetProtocolStrictness,
2017
+ knit_get_protocol_strictness: handleGetProtocolStrictness
2018
+ };
2019
+ function handleToolCall(toolName, params, brain) {
2020
+ if (params.file_path) {
2021
+ const decoded = decodeURIComponent(params.file_path).replace(/\\/g, "/");
2022
+ if (decoded.includes("..") || decoded.startsWith("/") || decoded.includes("\0")) {
2023
+ return JSON.stringify({ error: "Invalid file path \u2014 no traversal or absolute paths allowed" });
2024
+ }
2025
+ params.file_path = decoded;
2026
+ }
2027
+ for (const key of ["summary", "description", "lesson", "reason", "goal", "current_state", "next_step", "files_in_flight", "what_changed", "failed_attempts", "decisions_made", "task_description", "approach", "tags", "team_name", "name", "role", "focus", "file_patterns", "checklist", "domains", "team_roles", "project_type"]) {
2028
+ if (params[key]) {
2029
+ params[key] = params[key].slice(0, 5e3).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
2030
+ }
2031
+ }
2032
+ const handler = handlers[toolName];
2033
+ if (!handler) {
2034
+ return JSON.stringify({ error: `Unknown tool: ${toolName}` });
2035
+ }
2036
+ return handler(params, brain);
2037
+ }
2038
+ export {
2039
+ getToolDefinitions,
2040
+ handleToolCall
2041
+ };