Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions packages/mcp-server/bin/dev-agent-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,21 @@ async function main() {
defaultSection: 'summary',
});

// Create git extractor and indexer (needed by plan and history adapters)
const gitExtractor = new LocalGitExtractor(repositoryPath);
const gitVectorStorage = new VectorStorage({
storePath: `${filePaths.vectors}-git`,
});
await gitVectorStorage.initialize();

const gitIndexer = new GitIndexer({
extractor: gitExtractor,
vectorStorage: gitVectorStorage,
});

const planAdapter = new PlanAdapter({
repositoryIndexer: indexer,
gitIndexer,
repositoryPath,
defaultFormat: 'compact',
timeout: 60000, // 60 seconds
Expand Down Expand Up @@ -198,19 +211,6 @@ async function main() {
defaultTokenBudget: 2000,
});

// Create git extractor and indexer for history adapter
// Note: GitIndexer uses the same vector storage for commit embeddings
const gitExtractor = new LocalGitExtractor(repositoryPath);
const gitVectorStorage = new VectorStorage({
storePath: `${filePaths.vectors}-git`,
});
await gitVectorStorage.initialize();

const gitIndexer = new GitIndexer({
extractor: gitExtractor,
vectorStorage: gitVectorStorage,
});

const historyAdapter = new HistoryAdapter({
gitIndexer,
gitExtractor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ describe('PlanAdapter', () => {
testLocation: '__tests__/',
},
relatedHistory: [],
relatedCommits: [],
metadata: {
generatedAt: '2024-01-01T00:00:00Z',
tokensUsed: 500,
codeSearchUsed: true,
historySearchUsed: false,
gitHistorySearchUsed: false,
repositoryPath: '/test/repo',
},
});
Expand All @@ -105,7 +107,7 @@ describe('PlanAdapter', () => {
describe('metadata', () => {
it('should have correct metadata', () => {
expect(adapter.metadata.name).toBe('plan-adapter');
expect(adapter.metadata.version).toBe('2.0.0');
expect(adapter.metadata.version).toBe('2.1.0');
expect(adapter.metadata.description).toContain('context');
});
});
Expand Down Expand Up @@ -265,7 +267,7 @@ describe('PlanAdapter', () => {

expect(utils.assembleContext).toHaveBeenCalledWith(
29,
mockIndexer,
expect.objectContaining({ indexer: mockIndexer }),
'/test/repo',
expect.objectContaining({
includeCode: false,
Expand Down
30 changes: 26 additions & 4 deletions packages/mcp-server/src/adapters/built-in/plan-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Philosophy: Provide raw, structured context - let the LLM do the reasoning
*/

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

/**
* Git indexer instance (for finding relevant commits)
*/
gitIndexer?: GitIndexer;

/**
* Repository path
*/
Expand All @@ -44,19 +49,21 @@ export interface PlanAdapterConfig {
export class PlanAdapter extends ToolAdapter {
readonly metadata = {
name: 'plan-adapter',
version: '2.0.0',
description: 'GitHub issue context assembler',
version: '2.1.0',
description: 'GitHub issue context assembler with git history',
author: 'Dev-Agent Team',
};

private indexer: RepositoryIndexer;
private gitIndexer?: GitIndexer;
private repositoryPath: string;
private defaultFormat: 'compact' | 'verbose';
private timeout: number;

constructor(config: PlanAdapterConfig) {
super();
this.indexer = config.repositoryIndexer;
this.gitIndexer = config.gitIndexer;
this.repositoryPath = config.repositoryPath;
this.defaultFormat = config.defaultFormat ?? 'compact';
this.timeout = config.timeout ?? 60000; // 60 seconds default
Expand Down Expand Up @@ -105,6 +112,11 @@ export class PlanAdapter extends ToolAdapter {
description: 'Maximum tokens for output (default: 4000)',
default: 4000,
},
includeGitHistory: {
type: 'boolean',
description: 'Include related git commits (default: true)',
default: true,
},
},
required: ['issue'],
},
Expand All @@ -118,6 +130,7 @@ export class PlanAdapter extends ToolAdapter {
includeCode = true,
includePatterns = true,
tokenBudget = 4000,
includeGitHistory = true,
} = args;

// Validate issue number
Expand Down Expand Up @@ -150,19 +163,27 @@ export class PlanAdapter extends ToolAdapter {
format,
includeCode,
includePatterns,
includeGitHistory,
tokenBudget,
});

const options: ContextAssemblyOptions = {
includeCode: includeCode as boolean,
includePatterns: includePatterns as boolean,
includeHistory: false, // TODO: Enable when GitHub indexer integration is ready
includeGitHistory: (includeGitHistory as boolean) && !!this.gitIndexer,
maxCodeResults: 10,
maxGitCommitResults: 5,
tokenBudget: tokenBudget as number,
};

const contextPackage = await this.withTimeout(
assembleContext(issue as number, this.indexer, this.repositoryPath, options),
assembleContext(
issue as number,
{ indexer: this.indexer, gitIndexer: this.gitIndexer },
this.repositoryPath,
options
),
this.timeout
);

Expand All @@ -178,6 +199,7 @@ export class PlanAdapter extends ToolAdapter {
context.logger.info('Context assembled', {
issue,
codeResults: contextPackage.relevantCode.length,
commitResults: contextPackage.relatedCommits.length,
hasPatterns: !!contextPackage.codebasePatterns.testPattern,
tokens,
duration_ms,
Expand Down
28 changes: 28 additions & 0 deletions packages/subagents/src/planner/context-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ export interface RelatedHistory {
summary?: string;
}

/**
* Related git commit
*/
export interface RelatedCommit {
/** Commit hash (short) */
hash: string;
/** Commit subject line */
subject: string;
/** Author name */
author: string;
/** Commit date (ISO) */
date: string;
/** Files changed */
filesChanged: string[];
/** Issue/PR references found in commit */
issueRefs: number[];
/** Relevance score (0-1) */
relevanceScore: number;
}

/**
* Complete context package for LLM consumption
*/
Expand All @@ -103,6 +123,8 @@ export interface ContextPackage {
codebasePatterns: CodebasePatterns;
/** Related closed issues/PRs */
relatedHistory: RelatedHistory[];
/** Related git commits (from semantic search) */
relatedCommits: RelatedCommit[];
/** Metadata about the context assembly */
metadata: ContextMetadata;
}
Expand All @@ -119,6 +141,8 @@ export interface ContextMetadata {
codeSearchUsed: boolean;
/** Whether history search was used */
historySearchUsed: boolean;
/** Whether git history was searched */
gitHistorySearchUsed: boolean;
/** Repository path */
repositoryPath: string;
}
Expand All @@ -133,10 +157,14 @@ export interface ContextAssemblyOptions {
includeHistory?: boolean;
/** Include codebase patterns (default: true) */
includePatterns?: boolean;
/** Include git commit history (default: true) */
includeGitHistory?: boolean;
/** Maximum code results (default: 10) */
maxCodeResults?: number;
/** Maximum history results (default: 5) */
maxHistoryResults?: number;
/** Maximum git commit results (default: 5) */
maxGitCommitResults?: number;
/** Token budget for output (default: 4000) */
tokenBudget?: number;
}
3 changes: 3 additions & 0 deletions packages/subagents/src/planner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,6 @@ export class PlannerAgent implements Agent {
export type * from './context-types';
// Export types
export type * from './types';
export type { ContextAssemblyContext } from './utils/context-assembler';
// Export context assembler utilities
export { assembleContext, formatContextPackage } from './utils/context-assembler';
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,13 @@ describe('Context Assembler', () => {
relevanceScore: 0.7,
},
],
relatedCommits: [],
metadata: {
generatedAt: '2025-01-03T00:00:00Z',
tokensUsed: 500,
codeSearchUsed: true,
historySearchUsed: true,
gitHistorySearchUsed: false,
repositoryPath: '/repo',
},
};
Expand Down Expand Up @@ -364,5 +366,126 @@ describe('Context Assembler', () => {

expect(output).toContain('**Issue #5:** Related bug (closed)');
});

it('should format related commits', () => {
const contextWithCommits: ContextPackage = {
...mockContext,
relatedCommits: [
{
hash: 'abc123',
subject: 'feat: add authentication',
author: 'dev',
date: '2025-01-15T10:00:00Z',
filesChanged: ['src/auth.ts', 'src/types.ts'],
issueRefs: [42],
relevanceScore: 0.9,
},
],
};

const output = formatContextPackage(contextWithCommits);

expect(output).toContain('## Related Commits');
expect(output).toContain('`abc123`');
expect(output).toContain('feat: add authentication');
expect(output).toContain('dev');
expect(output).toContain('#42');
expect(output).toContain('src/auth.ts');
});

it('should truncate long file lists in commits', () => {
const contextWithManyFiles: ContextPackage = {
...mockContext,
relatedCommits: [
{
hash: 'def456',
subject: 'refactor: big change',
author: 'dev',
date: '2025-01-15T10:00:00Z',
filesChanged: ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts'],
issueRefs: [],
relevanceScore: 0.8,
},
],
};

const output = formatContextPackage(contextWithManyFiles);

expect(output).toContain('+2 more');
});
});

describe('Git History Integration', () => {
const mockGitIndexer = {
search: vi.fn().mockResolvedValue([
{
shortHash: 'abc123',
subject: 'feat: add JWT auth',
author: { name: 'developer', date: new Date('2025-01-15') },
files: [{ path: 'src/auth.ts' }],
refs: { issueRefs: [42] },
},
{
shortHash: 'def456',
subject: 'fix: token validation',
author: { name: 'developer', date: new Date('2025-01-14') },
files: [{ path: 'src/auth.ts' }, { path: 'src/utils.ts' }],
refs: { issueRefs: [] },
},
]),
};

it('should include related commits when git indexer is provided', async () => {
const result = await assembleContext(
42,
{ indexer: mockIndexer, gitIndexer: mockGitIndexer as any },
'/repo',
{ includeGitHistory: true }
);

expect(result.relatedCommits).toHaveLength(2);
expect(result.relatedCommits[0].hash).toBe('abc123');
expect(result.relatedCommits[0].subject).toBe('feat: add JWT auth');
expect(result.metadata.gitHistorySearchUsed).toBe(true);
});

it('should skip git history when includeGitHistory is false', async () => {
const result = await assembleContext(
42,
{ indexer: mockIndexer, gitIndexer: mockGitIndexer as any },
'/repo',
{ includeGitHistory: false }
);

expect(result.relatedCommits).toHaveLength(0);
expect(mockGitIndexer.search).not.toHaveBeenCalled();
});

it('should skip git history when git indexer is null', async () => {
const result = await assembleContext(
42,
{ indexer: mockIndexer, gitIndexer: null },
'/repo',
{ includeGitHistory: true }
);

expect(result.relatedCommits).toHaveLength(0);
expect(result.metadata.gitHistorySearchUsed).toBe(false);
});

it('should handle git search errors gracefully', async () => {
const errorGitIndexer = {
search: vi.fn().mockRejectedValue(new Error('Git search failed')),
};

const result = await assembleContext(
42,
{ indexer: mockIndexer, gitIndexer: errorGitIndexer as any },
'/repo',
{ includeGitHistory: true }
);

expect(result.relatedCommits).toHaveLength(0);
});
});
});
Loading