@jussmor/commit-memory-mcp 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/db/client.js +56 -17
- package/dist/git/insights.d.ts +22 -0
- package/dist/git/insights.js +79 -0
- package/dist/mcp/server.js +355 -2
- package/dist/search/embeddings.d.ts +1 -0
- package/dist/search/embeddings.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,8 @@ build_context_pack({
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
Use this before invoking a coding subagent to keep prompts small and focused.
|
|
111
|
-
|
|
111
|
+
The pack is assembled main-first: promoted facts on `main` are treated as baseline truth, then branch/feature facts are added as in-flight overlay.
|
|
112
|
+
If no rows are found in strict scope, the server falls back automatically to broader scope levels.
|
|
112
113
|
|
|
113
114
|
Important: if you provide `domain`/`feature`/`branch` tags in `build_context_pack`, use the same tags during `sync_pr_context` for best precision.
|
|
114
115
|
|
package/dist/db/client.js
CHANGED
|
@@ -372,6 +372,7 @@ export function promoteContextFacts(db, options) {
|
|
|
372
372
|
}
|
|
373
373
|
export function buildContextPack(db, options) {
|
|
374
374
|
const taskType = options.taskType ?? "general";
|
|
375
|
+
const GLOBAL_BRANCH = "main";
|
|
375
376
|
function runQuery(params) {
|
|
376
377
|
const clauses = [];
|
|
377
378
|
const values = [taskType];
|
|
@@ -383,7 +384,11 @@ export function buildContextPack(db, options) {
|
|
|
383
384
|
clauses.push("scope_feature = ?");
|
|
384
385
|
values.push(options.feature);
|
|
385
386
|
}
|
|
386
|
-
if (params.
|
|
387
|
+
if (params.forcedBranch) {
|
|
388
|
+
clauses.push("scope_branch = ?");
|
|
389
|
+
values.push(params.forcedBranch);
|
|
390
|
+
}
|
|
391
|
+
else if (params.includeBranch && options.branch) {
|
|
387
392
|
clauses.push("scope_branch = ?");
|
|
388
393
|
values.push(options.branch);
|
|
389
394
|
}
|
|
@@ -444,27 +449,61 @@ export function buildContextPack(db, options) {
|
|
|
444
449
|
updatedAt: String(row.updated_at ?? ""),
|
|
445
450
|
}));
|
|
446
451
|
}
|
|
447
|
-
const
|
|
452
|
+
const seenIds = new Set();
|
|
453
|
+
const pack = [];
|
|
454
|
+
const addRows = (rows) => {
|
|
455
|
+
for (const row of rows) {
|
|
456
|
+
if (pack.length >= options.limit) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (seenIds.has(row.id)) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
seenIds.add(row.id);
|
|
463
|
+
pack.push(row);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
// 1) Main branch domain context is the durable source-of-truth baseline.
|
|
467
|
+
if (pack.length < options.limit) {
|
|
468
|
+
addRows(runQuery({
|
|
469
|
+
includeDomain: true,
|
|
470
|
+
includeFeature: false,
|
|
471
|
+
includeBranch: false,
|
|
472
|
+
forcedBranch: GLOBAL_BRANCH,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
// 2) Main branch global context fills any remaining baseline slots.
|
|
476
|
+
if (pack.length < options.limit) {
|
|
477
|
+
addRows(runQuery({
|
|
478
|
+
includeDomain: false,
|
|
479
|
+
includeFeature: false,
|
|
480
|
+
includeBranch: false,
|
|
481
|
+
forcedBranch: GLOBAL_BRANCH,
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
// 3) Branch-local feature context overlays main for active, in-flight work.
|
|
485
|
+
addRows(runQuery({
|
|
448
486
|
includeDomain: true,
|
|
449
487
|
includeFeature: true,
|
|
450
488
|
includeBranch: true,
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
|
|
489
|
+
}));
|
|
490
|
+
// 4) Domain-wide branch context provides additional short-lived signal.
|
|
491
|
+
if (pack.length < options.limit) {
|
|
492
|
+
addRows(runQuery({
|
|
493
|
+
includeDomain: true,
|
|
494
|
+
includeFeature: false,
|
|
495
|
+
includeBranch: false,
|
|
496
|
+
}));
|
|
454
497
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
498
|
+
// 5) Final safety net from all promoted context.
|
|
499
|
+
if (pack.length < options.limit) {
|
|
500
|
+
addRows(runQuery({
|
|
501
|
+
includeDomain: false,
|
|
502
|
+
includeFeature: false,
|
|
503
|
+
includeBranch: false,
|
|
504
|
+
}));
|
|
462
505
|
}
|
|
463
|
-
return
|
|
464
|
-
includeDomain: false,
|
|
465
|
-
includeFeature: false,
|
|
466
|
-
includeBranch: false,
|
|
467
|
-
});
|
|
506
|
+
return pack;
|
|
468
507
|
}
|
|
469
508
|
export function archiveFeatureContext(db, options) {
|
|
470
509
|
const now = new Date().toISOString();
|
package/dist/git/insights.d.ts
CHANGED
|
@@ -60,6 +60,28 @@ export declare function mainBranchOvernightBrief(options: {
|
|
|
60
60
|
subject: string;
|
|
61
61
|
}>;
|
|
62
62
|
};
|
|
63
|
+
export declare function extractFeatureBranchCommits(options: {
|
|
64
|
+
repoPath: string;
|
|
65
|
+
featureBranch: string;
|
|
66
|
+
baseBranch: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
}): {
|
|
69
|
+
featureName: string;
|
|
70
|
+
featureBranch: string;
|
|
71
|
+
baseBranch: string;
|
|
72
|
+
commits: Array<{
|
|
73
|
+
sha: string;
|
|
74
|
+
author: string;
|
|
75
|
+
date: string;
|
|
76
|
+
subject: string;
|
|
77
|
+
files: string[];
|
|
78
|
+
}>;
|
|
79
|
+
topFiles: Array<{
|
|
80
|
+
filePath: string;
|
|
81
|
+
touchCount: number;
|
|
82
|
+
}>;
|
|
83
|
+
affectedModules: string[];
|
|
84
|
+
};
|
|
63
85
|
export declare function resumeFeatureSessionBrief(options: {
|
|
64
86
|
worktreePath: string;
|
|
65
87
|
baseBranch: string;
|
package/dist/git/insights.js
CHANGED
|
@@ -154,6 +154,85 @@ export function mainBranchOvernightBrief(options) {
|
|
|
154
154
|
commits,
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
export function extractFeatureBranchCommits(options) {
|
|
158
|
+
const repoPath = path.resolve(options.repoPath);
|
|
159
|
+
const featureName = options.featureBranch
|
|
160
|
+
.replace(/^feature\//, "")
|
|
161
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
162
|
+
const limit = Math.max(1, options.limit);
|
|
163
|
+
let commitsRaw;
|
|
164
|
+
try {
|
|
165
|
+
commitsRaw = runGit(repoPath, [
|
|
166
|
+
"log",
|
|
167
|
+
`--format=%H%x1f%an%x1f%aI%x1f%s`,
|
|
168
|
+
`-n${limit}`,
|
|
169
|
+
`${options.baseBranch}..${options.featureBranch}`,
|
|
170
|
+
]).trim();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return {
|
|
174
|
+
featureName,
|
|
175
|
+
featureBranch: options.featureBranch,
|
|
176
|
+
baseBranch: options.baseBranch,
|
|
177
|
+
commits: [],
|
|
178
|
+
topFiles: [],
|
|
179
|
+
affectedModules: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (!commitsRaw) {
|
|
183
|
+
return {
|
|
184
|
+
featureName,
|
|
185
|
+
featureBranch: options.featureBranch,
|
|
186
|
+
baseBranch: options.baseBranch,
|
|
187
|
+
commits: [],
|
|
188
|
+
topFiles: [],
|
|
189
|
+
affectedModules: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const fileCounts = new Map();
|
|
193
|
+
const commits = commitsRaw.split("\n").map((line) => {
|
|
194
|
+
const [sha = "", author = "", date = "", subject = ""] = line.split("\x1f");
|
|
195
|
+
let files = [];
|
|
196
|
+
try {
|
|
197
|
+
const filesRaw = runGit(repoPath, [
|
|
198
|
+
"show",
|
|
199
|
+
"--name-only",
|
|
200
|
+
"--pretty=format:",
|
|
201
|
+
sha,
|
|
202
|
+
]).trim();
|
|
203
|
+
files = filesRaw
|
|
204
|
+
.split("\n")
|
|
205
|
+
.map((f) => f.trim())
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore per-commit failures
|
|
210
|
+
}
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
return { sha, author, date, subject, files };
|
|
215
|
+
});
|
|
216
|
+
const topFiles = Array.from(fileCounts.entries())
|
|
217
|
+
.map(([filePath, touchCount]) => ({ filePath, touchCount }))
|
|
218
|
+
.sort((a, b) => b.touchCount - a.touchCount)
|
|
219
|
+
.slice(0, 15);
|
|
220
|
+
const moduleSet = new Set();
|
|
221
|
+
for (const { filePath } of topFiles) {
|
|
222
|
+
const parts = filePath.split("/");
|
|
223
|
+
if (parts.length >= 2) {
|
|
224
|
+
moduleSet.add(parts.slice(0, 2).join("/"));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
featureName,
|
|
229
|
+
featureBranch: options.featureBranch,
|
|
230
|
+
baseBranch: options.baseBranch,
|
|
231
|
+
commits,
|
|
232
|
+
topFiles,
|
|
233
|
+
affectedModules: Array.from(moduleSet).slice(0, 10),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
157
236
|
export function resumeFeatureSessionBrief(options) {
|
|
158
237
|
const worktreePath = path.resolve(options.worktreePath);
|
|
159
238
|
const branch = runGit(worktreePath, [
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,10 +6,14 @@ import { execFileSync } from "node:child_process";
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
-
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.js";
|
|
10
|
-
import { commitDetails, explainPathActivity, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
9
|
+
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertContextFact, upsertWorktreeSession, } from "../db/client.js";
|
|
10
|
+
import { commitDetails, explainPathActivity, extractFeatureBranchCommits, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
11
11
|
import { listActiveWorktrees } from "../git/worktree.js";
|
|
12
12
|
import { syncPullRequestContext } from "../pr/sync.js";
|
|
13
|
+
import { callOllamaLlm } from "../search/embeddings.js";
|
|
14
|
+
function normalizeFeatureName(branch) {
|
|
15
|
+
return branch.replace(/^feature\//, "").replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
16
|
+
}
|
|
13
17
|
function fetchRemote(repoPath) {
|
|
14
18
|
execFileSync("git", ["-C", repoPath, "fetch", "--all", "--prune"], {
|
|
15
19
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -287,6 +291,41 @@ export async function startMcpServer() {
|
|
|
287
291
|
required: [],
|
|
288
292
|
},
|
|
289
293
|
},
|
|
294
|
+
{
|
|
295
|
+
name: "learn_feature",
|
|
296
|
+
description: "Save feature knowledge to the RAG DB. If agentContent is provided (recommended), it is stored directly as the feature's understanding — the agent should investigate the source code itself and pass its findings here. If agentContent is omitted, falls back to git-commit-based inference.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
featureBranch: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "e.g. feature/messaging",
|
|
303
|
+
},
|
|
304
|
+
baseBranch: { type: "string" },
|
|
305
|
+
limit: { type: "number" },
|
|
306
|
+
agentContent: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Plain-text description of what the feature does, written by the agent after reading actual source files. When provided, this becomes the stored knowledge (confidence 0.95) and git metadata is appended as supporting context.",
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
required: ["featureBranch"],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "sync_feature_knowledge",
|
|
316
|
+
description: "Update the AI knowledge for a feature branch using new commits and PR decisions since the last sync. Bootstraps automatically if no prior knowledge exists.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: "object",
|
|
319
|
+
properties: {
|
|
320
|
+
featureBranch: { type: "string" },
|
|
321
|
+
baseBranch: { type: "string" },
|
|
322
|
+
owner: { type: "string" },
|
|
323
|
+
repo: { type: "string" },
|
|
324
|
+
limit: { type: "number" },
|
|
325
|
+
},
|
|
326
|
+
required: ["featureBranch"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
290
329
|
{
|
|
291
330
|
name: "pre_plan_sync_brief",
|
|
292
331
|
description: "Run sync + overnight + feature resume analysis before planning work.",
|
|
@@ -641,6 +680,320 @@ export async function startMcpServer() {
|
|
|
641
680
|
content: [{ type: "text", text: JSON.stringify(prePlan, null, 2) }],
|
|
642
681
|
};
|
|
643
682
|
}
|
|
683
|
+
if (request.params.name === "learn_feature") {
|
|
684
|
+
const featureBranch = String(request.params.arguments?.featureBranch ?? "").trim();
|
|
685
|
+
const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
|
|
686
|
+
const limit = Number(request.params.arguments?.limit ?? 50);
|
|
687
|
+
const agentContent = String(request.params.arguments?.agentContent ?? "").trim() || null;
|
|
688
|
+
if (!featureBranch) {
|
|
689
|
+
return {
|
|
690
|
+
content: [{ type: "text", text: "featureBranch is required" }],
|
|
691
|
+
isError: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const featureName = normalizeFeatureName(featureBranch);
|
|
695
|
+
const data = extractFeatureBranchCommits({
|
|
696
|
+
repoPath,
|
|
697
|
+
featureBranch,
|
|
698
|
+
baseBranch,
|
|
699
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 50,
|
|
700
|
+
});
|
|
701
|
+
const authors = [...new Set(data.commits.map((c) => c.author))].join(", ");
|
|
702
|
+
const now = new Date().toISOString();
|
|
703
|
+
let content;
|
|
704
|
+
let confidence;
|
|
705
|
+
let aiGenerated;
|
|
706
|
+
if (agentContent) {
|
|
707
|
+
// Agent investigated the source code directly — highest confidence.
|
|
708
|
+
// Append git metadata as supporting context.
|
|
709
|
+
const gitMeta = [
|
|
710
|
+
``,
|
|
711
|
+
`--- Git metadata (${data.commits.length} commits, authors: ${authors || "unknown"}) ---`,
|
|
712
|
+
`Top files: ${data.topFiles
|
|
713
|
+
.slice(0, 5)
|
|
714
|
+
.map((f) => f.filePath)
|
|
715
|
+
.join(", ") || "(none)"}`,
|
|
716
|
+
`Top modules: ${data.affectedModules.slice(0, 4).join(", ") || "(none)"}`,
|
|
717
|
+
].join("\n");
|
|
718
|
+
content = agentContent + gitMeta;
|
|
719
|
+
confidence = 0.95;
|
|
720
|
+
aiGenerated = true;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// Fallback: infer from git commits + optional Ollama synthesis.
|
|
724
|
+
const fileList = data.topFiles
|
|
725
|
+
.map((f) => ` - ${f.filePath} (touched ${f.touchCount}x)`)
|
|
726
|
+
.join("\n");
|
|
727
|
+
const commitList = data.commits
|
|
728
|
+
.slice(0, 20)
|
|
729
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
730
|
+
.join("\n");
|
|
731
|
+
const prompt = [
|
|
732
|
+
`You are analyzing a Git feature branch called "${featureBranch}".`,
|
|
733
|
+
``,
|
|
734
|
+
`Top changed files:`,
|
|
735
|
+
fileList || " (none)",
|
|
736
|
+
``,
|
|
737
|
+
`Commit history (most recent first):`,
|
|
738
|
+
commitList || " (none)",
|
|
739
|
+
``,
|
|
740
|
+
`Answer in 3-5 sentences:`,
|
|
741
|
+
`1. What does this feature do?`,
|
|
742
|
+
`2. What modules/areas of the codebase does it affect?`,
|
|
743
|
+
`3. What does it NOT do or what is explicitly out of scope?`,
|
|
744
|
+
].join("\n");
|
|
745
|
+
const llmSummary = await callOllamaLlm(prompt);
|
|
746
|
+
const fallbackSummary = [
|
|
747
|
+
`Feature "${featureName}" spans ${data.commits.length} commit(s) by ${authors || "(unknown)"}.`,
|
|
748
|
+
`Top modules: ${data.affectedModules.slice(0, 4).join(", ") || "(unknown)"}.`,
|
|
749
|
+
`Top files: ${data.topFiles
|
|
750
|
+
.slice(0, 3)
|
|
751
|
+
.map((f) => f.filePath)
|
|
752
|
+
.join(", ") || "(none)"}.`,
|
|
753
|
+
`Commit subjects: ${data.commits
|
|
754
|
+
.slice(0, 5)
|
|
755
|
+
.map((c) => c.subject)
|
|
756
|
+
.join("; ")}.`,
|
|
757
|
+
].join(" ");
|
|
758
|
+
content = llmSummary ?? fallbackSummary;
|
|
759
|
+
confidence = llmSummary ? 0.85 : 0.6;
|
|
760
|
+
aiGenerated = llmSummary !== null;
|
|
761
|
+
}
|
|
762
|
+
const db = openDatabase(dbPath);
|
|
763
|
+
try {
|
|
764
|
+
upsertContextFact(db, {
|
|
765
|
+
id: `feature-knowledge:${featureName}`,
|
|
766
|
+
sourceType: "feature-agent",
|
|
767
|
+
sourceRef: featureBranch,
|
|
768
|
+
domain: "",
|
|
769
|
+
feature: featureName,
|
|
770
|
+
branch: featureBranch,
|
|
771
|
+
taskType: "feature-knowledge",
|
|
772
|
+
title: `Feature knowledge: ${featureName}`,
|
|
773
|
+
content,
|
|
774
|
+
priority: 0.9,
|
|
775
|
+
confidence,
|
|
776
|
+
status: "promoted",
|
|
777
|
+
createdAt: now,
|
|
778
|
+
updatedAt: now,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
db.close();
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
content: [
|
|
786
|
+
{
|
|
787
|
+
type: "text",
|
|
788
|
+
text: JSON.stringify({
|
|
789
|
+
featureName,
|
|
790
|
+
learned: content,
|
|
791
|
+
filesAnalyzed: data.topFiles.length,
|
|
792
|
+
commitsAnalyzed: data.commits.length,
|
|
793
|
+
agentProvided: agentContent !== null,
|
|
794
|
+
aiGenerated,
|
|
795
|
+
confidence,
|
|
796
|
+
savedAt: now,
|
|
797
|
+
}, null, 2),
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (request.params.name === "sync_feature_knowledge") {
|
|
803
|
+
const featureBranch = String(request.params.arguments?.featureBranch ?? "").trim();
|
|
804
|
+
const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
|
|
805
|
+
const owner = String(request.params.arguments?.owner ?? "").trim();
|
|
806
|
+
const repo = String(request.params.arguments?.repo ?? "").trim();
|
|
807
|
+
const limit = Number(request.params.arguments?.limit ?? 50);
|
|
808
|
+
if (!featureBranch) {
|
|
809
|
+
return {
|
|
810
|
+
content: [{ type: "text", text: "featureBranch is required" }],
|
|
811
|
+
isError: true,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
const featureName = normalizeFeatureName(featureBranch);
|
|
815
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 50;
|
|
816
|
+
// Load existing knowledge from DB
|
|
817
|
+
let existingKnowledge = null;
|
|
818
|
+
let existingUpdatedAt = null;
|
|
819
|
+
let existingCreatedAt = null;
|
|
820
|
+
const dbRead = openDatabase(dbPath);
|
|
821
|
+
try {
|
|
822
|
+
const existing = dbRead
|
|
823
|
+
.prepare("SELECT content, created_at, updated_at FROM context_facts WHERE id = ? LIMIT 1")
|
|
824
|
+
.get(`feature-knowledge:${featureName}`);
|
|
825
|
+
existingKnowledge = existing?.content ?? null;
|
|
826
|
+
existingUpdatedAt = existing?.updated_at ?? null;
|
|
827
|
+
existingCreatedAt = existing?.created_at ?? null;
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
dbRead.close();
|
|
831
|
+
}
|
|
832
|
+
// Bootstrap via learn_feature logic if no prior knowledge
|
|
833
|
+
if (!existingKnowledge) {
|
|
834
|
+
const bootData = extractFeatureBranchCommits({
|
|
835
|
+
repoPath,
|
|
836
|
+
featureBranch,
|
|
837
|
+
baseBranch,
|
|
838
|
+
limit: safeLimit,
|
|
839
|
+
});
|
|
840
|
+
const bootFileList = bootData.topFiles
|
|
841
|
+
.map((f) => ` - ${f.filePath} (touched ${f.touchCount}x)`)
|
|
842
|
+
.join("\n");
|
|
843
|
+
const bootCommitList = bootData.commits
|
|
844
|
+
.slice(0, 20)
|
|
845
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
846
|
+
.join("\n");
|
|
847
|
+
const bootPrompt = [
|
|
848
|
+
`You are analyzing a Git feature branch called "${featureBranch}".`,
|
|
849
|
+
`Top changed files:\n${bootFileList || " (none)"}`,
|
|
850
|
+
`Commit history:\n${bootCommitList || " (none)"}`,
|
|
851
|
+
`Answer in 3-5 sentences: 1. What does this feature do? 2. What modules does it affect? 3. What is out of scope?`,
|
|
852
|
+
].join("\n\n");
|
|
853
|
+
const bootLlm = await callOllamaLlm(bootPrompt);
|
|
854
|
+
existingKnowledge =
|
|
855
|
+
bootLlm ??
|
|
856
|
+
`Feature "${featureName}" has ${bootData.commits.length} commit(s). Top modules: ${bootData.affectedModules.slice(0, 4).join(", ") || "(unknown)"}.`;
|
|
857
|
+
const now = new Date().toISOString();
|
|
858
|
+
const dbBoot = openDatabase(dbPath);
|
|
859
|
+
try {
|
|
860
|
+
upsertContextFact(dbBoot, {
|
|
861
|
+
id: `feature-knowledge:${featureName}`,
|
|
862
|
+
sourceType: "feature-agent",
|
|
863
|
+
sourceRef: featureBranch,
|
|
864
|
+
domain: "",
|
|
865
|
+
feature: featureName,
|
|
866
|
+
branch: featureBranch,
|
|
867
|
+
taskType: "feature-knowledge",
|
|
868
|
+
title: `Feature knowledge: ${featureName}`,
|
|
869
|
+
content: existingKnowledge,
|
|
870
|
+
priority: 0.9,
|
|
871
|
+
confidence: bootLlm ? 0.85 : 0.6,
|
|
872
|
+
status: "promoted",
|
|
873
|
+
createdAt: now,
|
|
874
|
+
updatedAt: now,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
dbBoot.close();
|
|
879
|
+
}
|
|
880
|
+
existingUpdatedAt = now;
|
|
881
|
+
existingCreatedAt = now;
|
|
882
|
+
}
|
|
883
|
+
// Fetch remote and extract commits
|
|
884
|
+
fetchRemote(repoPath);
|
|
885
|
+
const data = extractFeatureBranchCommits({
|
|
886
|
+
repoPath,
|
|
887
|
+
featureBranch,
|
|
888
|
+
baseBranch,
|
|
889
|
+
limit: safeLimit,
|
|
890
|
+
});
|
|
891
|
+
// Only process commits newer than last sync
|
|
892
|
+
const newCommits = existingUpdatedAt
|
|
893
|
+
? data.commits.filter((c) => c.date > existingUpdatedAt)
|
|
894
|
+
: data.commits;
|
|
895
|
+
// Gather PR decisions for referenced PRs in new commits
|
|
896
|
+
const referencedPrNumbers = newCommits
|
|
897
|
+
.map((c) => detectReferencedPrNumber(c.subject))
|
|
898
|
+
.filter((n) => n !== null);
|
|
899
|
+
let prDecisionsSummary = "";
|
|
900
|
+
if (referencedPrNumbers.length > 0) {
|
|
901
|
+
const dbPr = openDatabase(dbPath);
|
|
902
|
+
try {
|
|
903
|
+
const parts = [];
|
|
904
|
+
for (const prNum of referencedPrNumbers.slice(0, 5)) {
|
|
905
|
+
const { pr, decisions } = loadPullRequestContext(dbPr, prNum, owner || undefined, repo || undefined);
|
|
906
|
+
if (pr) {
|
|
907
|
+
parts.push(`PR #${prNum} "${pr["title"]}": ${decisions
|
|
908
|
+
.slice(0, 3)
|
|
909
|
+
.map((d) => d["summary"])
|
|
910
|
+
.join("; ")}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
prDecisionsSummary = parts.join("\n");
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
dbPr.close();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const newCommitList = newCommits
|
|
920
|
+
.slice(0, 20)
|
|
921
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
922
|
+
.join("\n");
|
|
923
|
+
const updatePrompt = [
|
|
924
|
+
`You previously documented this feature:`,
|
|
925
|
+
`"${existingKnowledge}"`,
|
|
926
|
+
``,
|
|
927
|
+
`New commits since last sync:`,
|
|
928
|
+
newCommitList || " (no new commits)",
|
|
929
|
+
prDecisionsSummary ? `\nPR decisions:\n${prDecisionsSummary}` : "",
|
|
930
|
+
``,
|
|
931
|
+
`Write an updated 3-5 sentence understanding of the feature. If nothing changed, return the previous text unchanged.`,
|
|
932
|
+
].join("\n");
|
|
933
|
+
const updatedSummary = newCommits.length > 0
|
|
934
|
+
? ((await callOllamaLlm(updatePrompt)) ?? existingKnowledge)
|
|
935
|
+
: existingKnowledge;
|
|
936
|
+
const now = new Date().toISOString();
|
|
937
|
+
const dbWrite = openDatabase(dbPath);
|
|
938
|
+
try {
|
|
939
|
+
upsertContextFact(dbWrite, {
|
|
940
|
+
id: `feature-knowledge:${featureName}`,
|
|
941
|
+
sourceType: "feature-agent",
|
|
942
|
+
sourceRef: featureBranch,
|
|
943
|
+
domain: "",
|
|
944
|
+
feature: featureName,
|
|
945
|
+
branch: featureBranch,
|
|
946
|
+
taskType: "feature-knowledge",
|
|
947
|
+
title: `Feature knowledge: ${featureName}`,
|
|
948
|
+
content: updatedSummary,
|
|
949
|
+
priority: 0.9,
|
|
950
|
+
confidence: 0.85,
|
|
951
|
+
status: "promoted",
|
|
952
|
+
createdAt: existingCreatedAt ?? now,
|
|
953
|
+
updatedAt: now,
|
|
954
|
+
});
|
|
955
|
+
// Audit log row (status: draft — invisible to default build_context_pack)
|
|
956
|
+
if (newCommits.length > 0) {
|
|
957
|
+
const auditDate = now.split("T")[0] ?? now;
|
|
958
|
+
upsertContextFact(dbWrite, {
|
|
959
|
+
id: `feature-change-log:${featureName}:${auditDate}`,
|
|
960
|
+
sourceType: "feature-agent",
|
|
961
|
+
sourceRef: featureBranch,
|
|
962
|
+
domain: "",
|
|
963
|
+
feature: featureName,
|
|
964
|
+
branch: featureBranch,
|
|
965
|
+
taskType: "change-log",
|
|
966
|
+
title: `Change log: ${featureName} on ${auditDate}`,
|
|
967
|
+
content: `New commits: ${newCommits
|
|
968
|
+
.map((c) => c.subject)
|
|
969
|
+
.join("; ")}. ${prDecisionsSummary}`.trim(),
|
|
970
|
+
priority: 0.7,
|
|
971
|
+
confidence: 0.8,
|
|
972
|
+
status: "draft",
|
|
973
|
+
createdAt: now,
|
|
974
|
+
updatedAt: now,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
dbWrite.close();
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
content: [
|
|
983
|
+
{
|
|
984
|
+
type: "text",
|
|
985
|
+
text: JSON.stringify({
|
|
986
|
+
featureName,
|
|
987
|
+
previousKnowledge: existingKnowledge,
|
|
988
|
+
updatedKnowledge: updatedSummary,
|
|
989
|
+
newCommitsAnalyzed: newCommits.length,
|
|
990
|
+
totalCommitsInBranch: data.commits.length,
|
|
991
|
+
syncedAt: now,
|
|
992
|
+
}, null, 2),
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
}
|
|
644
997
|
return {
|
|
645
998
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
646
999
|
isError: true,
|
|
@@ -52,6 +52,30 @@ export async function embedText(text) {
|
|
|
52
52
|
}
|
|
53
53
|
return normalize(payload.embedding);
|
|
54
54
|
}
|
|
55
|
+
export async function callOllamaLlm(prompt) {
|
|
56
|
+
const model = process.env.OLLAMA_CHAT_MODEL;
|
|
57
|
+
if (!model) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const baseUrl = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`${baseUrl}/api/generate`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ model, prompt, stream: false }),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const payload = (await response.json());
|
|
71
|
+
return typeof payload.response === "string" && payload.response.trim()
|
|
72
|
+
? payload.response.trim()
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
55
79
|
export function getExpectedDimension() {
|
|
56
80
|
return process.env.OLLAMA_EMBED_MODEL
|
|
57
81
|
? Number.parseInt(process.env.COMMIT_RAG_DIMENSION ?? "", 10) ||
|
package/package.json
CHANGED