Skip to content

Commit be34613

Browse files
committed
feat(subagents): add git history to context assembler (#95)
- Add RelatedCommit type with hash, subject, author, date, files, issueRefs - Add relatedCommits to ContextPackage - Add gitHistorySearchUsed to ContextMetadata - Add includeGitHistory and maxGitCommitResults to ContextAssemblyOptions - Implement findRelatedCommits using GitIndexer semantic search - Add git commit section to formatContextPackage output - Update PlanAdapter to pass gitIndexer to assembleContext - Add includeGitHistory parameter to dev_plan tool - Add 6 tests for git history integration - Update plan-adapter tests for new signature Closes #95 Part of Epic: Intelligent Git History (v0.4.0) #90
1 parent f48dfc5 commit be34613

File tree

7 files changed

+323
-29
lines changed

7 files changed

+323
-29
lines changed

packages/mcp-server/bin/dev-agent-mcp.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,21 @@ async function main() {
156156
defaultSection: 'summary',
157157
});
158158

159+
// Create git extractor and indexer (needed by plan and history adapters)
160+
const gitExtractor = new LocalGitExtractor(repositoryPath);
161+
const gitVectorStorage = new VectorStorage({
162+
storePath: `${filePaths.vectors}-git`,
163+
});
164+
await gitVectorStorage.initialize();
165+
166+
const gitIndexer = new GitIndexer({
167+
extractor: gitExtractor,
168+
vectorStorage: gitVectorStorage,
169+
});
170+
159171
const planAdapter = new PlanAdapter({
160172
repositoryIndexer: indexer,
173+
gitIndexer,
161174
repositoryPath,
162175
defaultFormat: 'compact',
163176
timeout: 60000, // 60 seconds
@@ -198,19 +211,6 @@ async function main() {
198211
defaultTokenBudget: 2000,
199212
});
200213

201-
// Create git extractor and indexer for history adapter
202-
// Note: GitIndexer uses the same vector storage for commit embeddings
203-
const gitExtractor = new LocalGitExtractor(repositoryPath);
204-
const gitVectorStorage = new VectorStorage({
205-
storePath: `${filePaths.vectors}-git`,
206-
});
207-
await gitVectorStorage.initialize();
208-
209-
const gitIndexer = new GitIndexer({
210-
extractor: gitExtractor,
211-
vectorStorage: gitVectorStorage,
212-
});
213-
214214
const historyAdapter = new HistoryAdapter({
215215
gitIndexer,
216216
gitExtractor,

packages/mcp-server/src/adapters/__tests__/plan-adapter.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,13 @@ describe('PlanAdapter', () => {
8888
testLocation: '__tests__/',
8989
},
9090
relatedHistory: [],
91+
relatedCommits: [],
9192
metadata: {
9293
generatedAt: '2024-01-01T00:00:00Z',
9394
tokensUsed: 500,
9495
codeSearchUsed: true,
9596
historySearchUsed: false,
97+
gitHistorySearchUsed: false,
9698
repositoryPath: '/test/repo',
9799
},
98100
});
@@ -105,7 +107,7 @@ describe('PlanAdapter', () => {
105107
describe('metadata', () => {
106108
it('should have correct metadata', () => {
107109
expect(adapter.metadata.name).toBe('plan-adapter');
108-
expect(adapter.metadata.version).toBe('2.0.0');
110+
expect(adapter.metadata.version).toBe('2.1.0');
109111
expect(adapter.metadata.description).toContain('context');
110112
});
111113
});
@@ -265,7 +267,7 @@ describe('PlanAdapter', () => {
265267

266268
expect(utils.assembleContext).toHaveBeenCalledWith(
267269
29,
268-
mockIndexer,
270+
expect.objectContaining({ indexer: mockIndexer }),
269271
'/test/repo',
270272
expect.objectContaining({
271273
includeCode: false,

packages/mcp-server/src/adapters/built-in/plan-adapter.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Philosophy: Provide raw, structured context - let the LLM do the reasoning
66
*/
77

8-
import type { RepositoryIndexer } from '@lytics/dev-agent-core';
8+
import type { GitIndexer, RepositoryIndexer } from '@lytics/dev-agent-core';
99
import type { ContextAssemblyOptions } from '@lytics/dev-agent-subagents';
1010
import { assembleContext, formatContextPackage } from '@lytics/dev-agent-subagents';
1111
import { estimateTokensForText, startTimer } from '../../formatters/utils';
@@ -21,6 +21,11 @@ export interface PlanAdapterConfig {
2121
*/
2222
repositoryIndexer: RepositoryIndexer;
2323

24+
/**
25+
* Git indexer instance (for finding relevant commits)
26+
*/
27+
gitIndexer?: GitIndexer;
28+
2429
/**
2530
* Repository path
2631
*/
@@ -44,19 +49,21 @@ export interface PlanAdapterConfig {
4449
export class PlanAdapter extends ToolAdapter {
4550
readonly metadata = {
4651
name: 'plan-adapter',
47-
version: '2.0.0',
48-
description: 'GitHub issue context assembler',
52+
version: '2.1.0',
53+
description: 'GitHub issue context assembler with git history',
4954
author: 'Dev-Agent Team',
5055
};
5156

5257
private indexer: RepositoryIndexer;
58+
private gitIndexer?: GitIndexer;
5359
private repositoryPath: string;
5460
private defaultFormat: 'compact' | 'verbose';
5561
private timeout: number;
5662

5763
constructor(config: PlanAdapterConfig) {
5864
super();
5965
this.indexer = config.repositoryIndexer;
66+
this.gitIndexer = config.gitIndexer;
6067
this.repositoryPath = config.repositoryPath;
6168
this.defaultFormat = config.defaultFormat ?? 'compact';
6269
this.timeout = config.timeout ?? 60000; // 60 seconds default
@@ -105,6 +112,11 @@ export class PlanAdapter extends ToolAdapter {
105112
description: 'Maximum tokens for output (default: 4000)',
106113
default: 4000,
107114
},
115+
includeGitHistory: {
116+
type: 'boolean',
117+
description: 'Include related git commits (default: true)',
118+
default: true,
119+
},
108120
},
109121
required: ['issue'],
110122
},
@@ -118,6 +130,7 @@ export class PlanAdapter extends ToolAdapter {
118130
includeCode = true,
119131
includePatterns = true,
120132
tokenBudget = 4000,
133+
includeGitHistory = true,
121134
} = args;
122135

123136
// Validate issue number
@@ -150,19 +163,27 @@ export class PlanAdapter extends ToolAdapter {
150163
format,
151164
includeCode,
152165
includePatterns,
166+
includeGitHistory,
153167
tokenBudget,
154168
});
155169

156170
const options: ContextAssemblyOptions = {
157171
includeCode: includeCode as boolean,
158172
includePatterns: includePatterns as boolean,
159173
includeHistory: false, // TODO: Enable when GitHub indexer integration is ready
174+
includeGitHistory: (includeGitHistory as boolean) && !!this.gitIndexer,
160175
maxCodeResults: 10,
176+
maxGitCommitResults: 5,
161177
tokenBudget: tokenBudget as number,
162178
};
163179

164180
const contextPackage = await this.withTimeout(
165-
assembleContext(issue as number, this.indexer, this.repositoryPath, options),
181+
assembleContext(
182+
issue as number,
183+
{ indexer: this.indexer, gitIndexer: this.gitIndexer },
184+
this.repositoryPath,
185+
options
186+
),
166187
this.timeout
167188
);
168189

@@ -178,6 +199,7 @@ export class PlanAdapter extends ToolAdapter {
178199
context.logger.info('Context assembled', {
179200
issue,
180201
codeResults: contextPackage.relevantCode.length,
202+
commitResults: contextPackage.relatedCommits.length,
181203
hasPatterns: !!contextPackage.codebasePatterns.testPattern,
182204
tokens,
183205
duration_ms,

packages/subagents/src/planner/context-types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,26 @@ export interface RelatedHistory {
9191
summary?: string;
9292
}
9393

94+
/**
95+
* Related git commit
96+
*/
97+
export interface RelatedCommit {
98+
/** Commit hash (short) */
99+
hash: string;
100+
/** Commit subject line */
101+
subject: string;
102+
/** Author name */
103+
author: string;
104+
/** Commit date (ISO) */
105+
date: string;
106+
/** Files changed */
107+
filesChanged: string[];
108+
/** Issue/PR references found in commit */
109+
issueRefs: number[];
110+
/** Relevance score (0-1) */
111+
relevanceScore: number;
112+
}
113+
94114
/**
95115
* Complete context package for LLM consumption
96116
*/
@@ -103,6 +123,8 @@ export interface ContextPackage {
103123
codebasePatterns: CodebasePatterns;
104124
/** Related closed issues/PRs */
105125
relatedHistory: RelatedHistory[];
126+
/** Related git commits (from semantic search) */
127+
relatedCommits: RelatedCommit[];
106128
/** Metadata about the context assembly */
107129
metadata: ContextMetadata;
108130
}
@@ -119,6 +141,8 @@ export interface ContextMetadata {
119141
codeSearchUsed: boolean;
120142
/** Whether history search was used */
121143
historySearchUsed: boolean;
144+
/** Whether git history was searched */
145+
gitHistorySearchUsed: boolean;
122146
/** Repository path */
123147
repositoryPath: string;
124148
}
@@ -133,10 +157,14 @@ export interface ContextAssemblyOptions {
133157
includeHistory?: boolean;
134158
/** Include codebase patterns (default: true) */
135159
includePatterns?: boolean;
160+
/** Include git commit history (default: true) */
161+
includeGitHistory?: boolean;
136162
/** Maximum code results (default: 10) */
137163
maxCodeResults?: number;
138164
/** Maximum history results (default: 5) */
139165
maxHistoryResults?: number;
166+
/** Maximum git commit results (default: 5) */
167+
maxGitCommitResults?: number;
140168
/** Token budget for output (default: 4000) */
141169
tokenBudget?: number;
142170
}

packages/subagents/src/planner/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,6 @@ export class PlannerAgent implements Agent {
215215
export type * from './context-types';
216216
// Export types
217217
export type * from './types';
218+
export type { ContextAssemblyContext } from './utils/context-assembler';
219+
// Export context assembler utilities
220+
export { assembleContext, formatContextPackage } from './utils/context-assembler';

packages/subagents/src/planner/utils/__tests__/context-assembler.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,13 @@ describe('Context Assembler', () => {
225225
relevanceScore: 0.7,
226226
},
227227
],
228+
relatedCommits: [],
228229
metadata: {
229230
generatedAt: '2025-01-03T00:00:00Z',
230231
tokensUsed: 500,
231232
codeSearchUsed: true,
232233
historySearchUsed: true,
234+
gitHistorySearchUsed: false,
233235
repositoryPath: '/repo',
234236
},
235237
};
@@ -364,5 +366,126 @@ describe('Context Assembler', () => {
364366

365367
expect(output).toContain('**Issue #5:** Related bug (closed)');
366368
});
369+
370+
it('should format related commits', () => {
371+
const contextWithCommits: ContextPackage = {
372+
...mockContext,
373+
relatedCommits: [
374+
{
375+
hash: 'abc123',
376+
subject: 'feat: add authentication',
377+
author: 'dev',
378+
date: '2025-01-15T10:00:00Z',
379+
filesChanged: ['src/auth.ts', 'src/types.ts'],
380+
issueRefs: [42],
381+
relevanceScore: 0.9,
382+
},
383+
],
384+
};
385+
386+
const output = formatContextPackage(contextWithCommits);
387+
388+
expect(output).toContain('## Related Commits');
389+
expect(output).toContain('`abc123`');
390+
expect(output).toContain('feat: add authentication');
391+
expect(output).toContain('dev');
392+
expect(output).toContain('#42');
393+
expect(output).toContain('src/auth.ts');
394+
});
395+
396+
it('should truncate long file lists in commits', () => {
397+
const contextWithManyFiles: ContextPackage = {
398+
...mockContext,
399+
relatedCommits: [
400+
{
401+
hash: 'def456',
402+
subject: 'refactor: big change',
403+
author: 'dev',
404+
date: '2025-01-15T10:00:00Z',
405+
filesChanged: ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts'],
406+
issueRefs: [],
407+
relevanceScore: 0.8,
408+
},
409+
],
410+
};
411+
412+
const output = formatContextPackage(contextWithManyFiles);
413+
414+
expect(output).toContain('+2 more');
415+
});
416+
});
417+
418+
describe('Git History Integration', () => {
419+
const mockGitIndexer = {
420+
search: vi.fn().mockResolvedValue([
421+
{
422+
shortHash: 'abc123',
423+
subject: 'feat: add JWT auth',
424+
author: { name: 'developer', date: new Date('2025-01-15') },
425+
files: [{ path: 'src/auth.ts' }],
426+
refs: { issueRefs: [42] },
427+
},
428+
{
429+
shortHash: 'def456',
430+
subject: 'fix: token validation',
431+
author: { name: 'developer', date: new Date('2025-01-14') },
432+
files: [{ path: 'src/auth.ts' }, { path: 'src/utils.ts' }],
433+
refs: { issueRefs: [] },
434+
},
435+
]),
436+
};
437+
438+
it('should include related commits when git indexer is provided', async () => {
439+
const result = await assembleContext(
440+
42,
441+
{ indexer: mockIndexer, gitIndexer: mockGitIndexer as any },
442+
'/repo',
443+
{ includeGitHistory: true }
444+
);
445+
446+
expect(result.relatedCommits).toHaveLength(2);
447+
expect(result.relatedCommits[0].hash).toBe('abc123');
448+
expect(result.relatedCommits[0].subject).toBe('feat: add JWT auth');
449+
expect(result.metadata.gitHistorySearchUsed).toBe(true);
450+
});
451+
452+
it('should skip git history when includeGitHistory is false', async () => {
453+
const result = await assembleContext(
454+
42,
455+
{ indexer: mockIndexer, gitIndexer: mockGitIndexer as any },
456+
'/repo',
457+
{ includeGitHistory: false }
458+
);
459+
460+
expect(result.relatedCommits).toHaveLength(0);
461+
expect(mockGitIndexer.search).not.toHaveBeenCalled();
462+
});
463+
464+
it('should skip git history when git indexer is null', async () => {
465+
const result = await assembleContext(
466+
42,
467+
{ indexer: mockIndexer, gitIndexer: null },
468+
'/repo',
469+
{ includeGitHistory: true }
470+
);
471+
472+
expect(result.relatedCommits).toHaveLength(0);
473+
expect(result.metadata.gitHistorySearchUsed).toBe(false);
474+
});
475+
476+
it('should handle git search errors gracefully', async () => {
477+
const errorGitIndexer = {
478+
search: vi.fn().mockRejectedValue(new Error('Git search failed')),
479+
};
480+
481+
const result = await assembleContext(
482+
42,
483+
{ indexer: mockIndexer, gitIndexer: errorGitIndexer as any },
484+
'/repo',
485+
{ includeGitHistory: true }
486+
);
487+
488+
expect(result.relatedCommits).toHaveLength(0);
489+
});
367490
});
368491
});

0 commit comments

Comments
 (0)